Task list

The list of tasks wherever they appear: a record, a dashboard, the /tasks overview. packages/ui/src/components/TaskList.tsx, CSS apps/web/src/ds/task-list.css (.tl-*) plus the drag layer task-list-dnd.css. Grouping and the standalone Návrhy view on /tasks are host behaviour, not component props — see below; the grouping logic itself lives in apps/web/src/lib/task-groups.ts.

One list, three width modes — and each mode changes exactly one thing, the assignee:

modewhat changes
narrowthe assignee is an avatar only (the name is in the tooltip) — drawers, panels, dashboards
wide (default)the assignee is an avatar plus a name, as an entity reference — record detail
fullthe /tasks overview: edge-to-edge, grouped rows, the linked-entity slot

Below 1101px the full mode folds its layout, not its render: the DOM stays complete.

Since 15 Sep 2026 (AD-approved) — the default row, not a variant

The row was rebuilt against the Linear model in every width mode:

  • No hairline. The row carries no border under it; the column grid and the hover fill (--hover-surface) lead the eye down the list instead.
  • One row gap. Every gap inside the row — priority to state glyph, lead to title, tag to tag — is --sp-4 (12px). One rhythm, not several.
  • No column header. No header bar sits above the list in any mode, full included. The content is self-describing.
  • No readable id, no state-name text in the row. The task's detail (and its row on a project) still carries the id; the state name lives only in the state glyph's title tooltip.
  • No attachment count. The paperclip-plus-count is gone from the counts group; comment and subtask counts stay.

When to use

  • Any list of tasks: on a record, in a section, on a dashboard, on the overview.
  • With subtasks, groups, progress and an add row — all of it belongs to this one component.

When not to use

  • A list of anything else — that is Item list.
  • A single task's detail — that is the task dialog.
  • A kanban of tasks; the board is a different pattern.

Anatomy

  • Row .tl-row (44px, no border): lead .tl-lead = priority .tl-prio · state glyph .tl-state, disclosure .tl-disc for subtasks, title .tl-title (struck through when done), counts .tl-counts (comments, subtasks — no attachments), then the right-hand group.
  • The lead is exactly two marks, --sp-4 apart: a priority slot that is always reserved (.tl-prio, a 24px box — --sp-6, the size of .acrm-icobtn.sm — empty when the task has no priority, so every title lines up) and the state glyph of the task's bucket (TASK_STATUS_KIND_ICON in TASK_STATUS_KIND_COLOR, both from @acrm/ui, at --ico-sm). A done task is a filled blue circle with a white tick; an open one an empty blue circle. Clicking the state finishes or reopens the task; its title names the state (e.g. „Blokováno — označit jako hotové") — that text is never drawn in the row itself.
  • .tl-rcheck (the round checkbox) is left only for AI suggestion rows: a suggestion is not a task yet and has no state.
  • Right side, Linear order: the linked entity (full only), then tags, then assignee, then due date.
  • Tags .tl-tags.tl-tags-slot are their own row element (TaskTags; the row gets .has-tags) and draw at every width — narrow, wide and full alike (11 Oct 2026, acrm !4964; RULES §20). They sit right-aligned just before assignee and due (the slot carries margin-left: auto, the meta follows at the row gap); on a phone they drop to their own full-width line after the meta. The rules live in apps/web/src/ds/item-list.css, next to the Item list tags slot they mirror.
  • Pills are .acrm-pill.sm (Status → Pill), one tone per label picked from the palette by tag name; at most two pills draw, the rest collapse into .tl-more (+N) with the full list in a title tooltip.
  • A tag can also be a TLTagChip { label, title, onClick, dot } (25 Sep 2026, FA197 — Call Intelligence §1): a neutral clickable button.acrm-pill.sm with no tone from the name, --pill-dot from dot (a token only, typically the speaker colour; without it the grey dot) and title as the tooltip — e.g. 41:10 „Přehrát od 41:10" plays the recording from that moment. The canon draws the markup itself (the same pill as TimeChip in the meeting minutes); the host supplies data only.
  • Linked slot — one entity, never „+N" (full mode only, .tl-rel, right-aligned): a project (task.project, with a .tl-milestone diamond chip when it has a milestone) takes priority; otherwise the linked deal; otherwise the first linked contact. A task never shows more than one reference, and never a project alongside a deal/contact chip.
  • Assignee: wide mode shows an avatar plus the full name; narrow and full show a badge plus initials only (.tl-person.ini, full name in title), because a bare face isn't legible at those widths and a full name doesn't fit the column.
  • Due date .tl-rdue (.now warning, .over critical).
  • In full mode, the due and assignee columns size from content and sit at opposite edges of their shared width: --tl-due-ch (set by the component from the longest due string in the list) sizes and left-aligns the due column, the assignee column is avatar-plus-initials wide and right-aligns — so the gap between them is the row's own gap, never a padded box on either side.
  • Priority tooltip: TLTask.priorityTitle becomes the title of .tl-prio (FA199, „Priorita: vysoká (navrženo)"); the slot stays aria-hidden — the title is for the pointer, the row already carries the task for a screen reader.
  • AI suggestion row (.tl-row.sug) has the same lead shape as a task row (priority slot, state slot) but a suggestion is not a task yet: the state slot is a muted, non-interactive glyph .tl-state.sug (a dashed circle circle-dashed since 25 Sep 2026, FA200 — a task that is not a task yet; the solid circle is an open task; not the bucket glyph, and not a button) — there is no checkbox to tick. A suggestion is accepted or dismissed only through .tl-sugact (Zamítnout · Upravit · Schválit).
  • Subtasks (.tl-row.sub) indent 28px per depth level from the row's left edge; the right-hand columns stay on the same axis as every other row. A light connector — a vertical line from the row's top edge curving into the row's vertical centre — draws in the indented gutter (::before, coloured --border).
  • Groups .tl-grp carry a label, a count or a progress bar, and a leading mark .tl-grp-ic for the grouping axis (state glyph, assignee face, milestone diamond, or type icon — the host supplies the node via TLGroup.icon); a suggested group is the AI proposal with its own approve and dismiss actions (.tl-sugact, revealed on hover or focus). The bar itself is a tinted strip (--surface-75, --radius-sm) at the row's own height (44px) and type scale, coloured only --fg-secondary — it is not a heading. The tinted look is scoped to the list root — .tl .tl-grp, never bare .tl-grp. The class is shared canon: the Aktivita timeline (.tlx-body .tl-grp), the calls page and the entity timeline draw their day/section strips with it too, and an unscoped restyle tints those as well (caught by timeline-lead.test.ts). The bare selector keeps its original hairline form for those surfaces.
  • Add row .tl-addrow — a ghost row at the end.
  • Reordering by drag uses .tl-reorder (set on a group that was given onReorder), .dragging on the lifted row and the insertion line .tl-drop-line. Since 16 September 2026 the gesture is built on pointer events, not HTML5 drag-and-drop: dragstart never fires from a finger, so the old implementation worked with a mouse and silently did nothing on a touchscreen.
  • A press on an unfinished row arms a gesture; it becomes a drag after 4px of movement with a mouse, or after a 320ms hold with a finger (a finger that moves more than 8px inside that hold is scrolling, so the gesture is cancelled and the list scrolls as usual). Only the lifted row gets touch-action: none — putting it on every row would kill scrolling in the whole list.
  • A finished row is a drop target but not a handle (data-tl-item marks what can be picked up); the drop position is read from data-tl-grp (a useId() per group) and data-tl-pos, so a row can only ever land inside its own group — between groups a drop has nowhere to write.
  • Pointer moves are tracked on document, not on the row, so a fast drag that leaves the row keeps working, and touchmove is registered non-passive so the page cannot scroll underneath.

Mobile (≤720px)

App-only layer — this lives in apps/web/src/ds/responsive.css, not in the ported task-list.css; the mobile fold is a rule of the host app's responsive layer, same as the rest of that file, not something the component ships.

  • The row becomes a two-line grid: the lead .tl-lead spans both rows on the left (grid-row: span 2), the title wraps to two lines (-webkit-line-clamp: 2, 14px), and due date + assignee sit on the second line below the title.
  • The linked-entity slot .tl-rel is not rendered (display: none) — it wouldn't fit. The state glyph .tl-state stays visible and clickable. Due date and assignee keep the same order as desktop; only the linked entity drops.
  • AI suggestion actions (.tl-sugact) are visible at rest, full width, below the title — there is no hover on a touchscreen, so the hover-reveal from desktop is replaced by an always-shown row of buttons with a touch-sized height.
  • The due column is capped (.tl.tl .tl-rdue, min(var(--tl-w-due), 9ch), with .tl-meta sized to the same ceiling): --tl-w-due is measured from the longest due string in the list, so one task worded „za 3 týdny" pushed the title into two words on every row. Over the cap the date ellipsises rather than steal the title's width.
  • One gutter, like Pošta (owner-approved 28 Sep 2026, acrm !4114): the page surface .tk-main keeps only --sp-1 on a phone, so with the row's own 12 px the text sits on 16 px from the edge and the due date ends at the same right edge as the mail list (row 16–374 px at 390). Two gutters (24 + 12 px) left half the screen empty.
  • Due date aligned right in its fixed column (.tl.tl .tl-rdue { text-align: right }): dates below each other still align, and a short „26. 9." no longer leaves ~20 px before the edge.
  • Assignee = the mark only on a phone: the initials .tl-person.ini .nm are hidden and the owner column shrinks to --ico-sm (--tl-w-owner). Mark + initials said the same thing twice in ~80 px; the name lives in title and aria-label (see Accessibility). Desktop keeps mark + initials.
  • Project task row on a phone (ProjectTaskRow on the project Úkoly tab, apps/web/src/ds/project-tasks.css, approved 29 Sep 2026): the same compact row as this list and as Linear Mobile's issue list — priority, key, state glyph, one-line title with ellipsis, assignee + due on the right. Tags .ptsk-tags and the state name .ptsk-statename are not drawn (they live in the task detail); .ptsk-meta loses its fixed 48 % width and keeps only the assignee and due columns. The „Čeká na předchůdce" pill shows the short label „Čeká" (full text in title; the predecessor itself stays in the task detail rail). At 390 px the title went from 0–22 px to 148 px and the row no longer overflows. Desktop unchanged.
  • Návrhy has its own entry on a phone (button.tk-navrhy-vstup, apps/web/src/ds/tasks.css): the desktop chip row is folded away at this width, so the view would be unreachable. The button is a full-width row above the list with the sparkles mark, the count and a trailing chevron, and is at least 44px tall (calc(var(--sp-7) + var(--sp-1)), WCAG 2.5.8). <Icon> renders an svg.ti, so the chevron is styled through > .ti while the leading mark sits in .ic.

Host behaviour — grouping & the Návrhy view (/tasks, ACRM-273)

These are decisions the /tasks page (apps/web/src/pages/TasksPage.tsx) makes before handing the component finished groups, not props of TaskList itself — the component only ever renders whatever TLGroup[] it's given; it has no notion of „Seskupit" or a Návrhy view.

  • Seskupit — a NavSelect chip next to Řadit, options none · by state · by milestone · by assignee · by type (TaskGroupBy / TASK_GROUP_KEYS, apps/web/src/lib/task-groups.ts). It groups the already-loaded page, not the whole scope: the server can sort but not group, and a group could straddle a page boundary. Each group's count is its own row count, not a server total (unlike the single default group, which still shows the server's count). Group order follows first occurrence in the already-sorted list — grouping adds no ordering of its own. Tasks with no value for the chosen key land in one group at the end, never mixed in among named groups. Drag reordering is offered in every sort, and with grouping on (acrm !3687, !3695): the ungrouped list always gets onReorder, and when the page is grouped every group gets it too — a row is dragged within its own group, and the order is written for the whole list. The drop itself switches Řadit to Vlastní (a manual order is what the user just expressed), but it does not overwrite a sort the user changed while the write was in flight. Until then the feature was reachable only after finding Řadit → Vlastní first, which nobody did.
  • Návrhy view — a separate page state (sugView), entered through a chip with a sparkles icon and a count badge; the chip renders only when there is at least one suggestion (or the view is already open — closing it needs the chip too). Inside the view the suggestion collector stands alone and uncapped (no „Zobrazit vše" ceiling, unlike the same collector shown above the regular list, which caps at a handful), and the list's own controls — Řadit, Seskupit, filters, Kompaktní/Plné — are hidden: there is nothing to sort or filter in a queue of one section.
  • .tl-navrhy — a wrapper class the Návrhy view puts on its TaskList container; above 1101px it narrows --tl-w-lead to just the priority + state-glyph width, since a suggestion needs no more.

Quiet add row — .tl-addrow is THE primitive (AD 28 Sep 2026, acrm !4068)

One shape for every "add an item at the end of a list" affordance: "Přidat úkol / podúkol", "Přidat milník" (project milestones), the empty state of the segment Share dialog ("Přidat uživatele, tým…"). No Button ghost, no text link, no dashed box — a third shape of the same thing is not allowed.

  • Anatomy: .tl-addrow › .ic (18 px slot, glyph plus at 14 px) + .l (label, 13/500, --fg-muted); min-height 32 px; hover = --hover-surface across the full width, label to --fg-secondary.
  • Element: a real <button type="button" class="tl-addrow"> — button.tl-addrow resets the native button (no border/background, full width, left-aligned). The task list itself still uses div role="button" (tree indent via --tl-depth).
  • Inside a host with its own inset (milestones zone, Share dialog .shd-add): pull the row out by its inner padding (margin-inline: calc(-1 var(--sp-5)); width: calc(100% + 2 var(--sp-5))) so the label lines up with the content above (measured 0 px) and the hover runs edge to edge — the same bleed as an embedded ItemList.

Suggestion actions on touch (acrm !4069, 28 Sep 2026)

On touch ((hover: none)) and on phones (≤ 720 px) the suggestion actions (Zamítnout / Upravit / Schválit on task suggestions; Zamítnout / Přijmout on CRM change suggestions of a meeting) are always visible — there is no hover to reveal them. On the phone they get their own line, right-aligned; the task title truncates and owner + due stay on the first line (.tl-meta becomes display: contents for suggestion rows). Button height on touch = the DS touch minimum calc(var(--sp-7) + var(--sp-1)) (44 px), the same expression as the dialog footer (acrm !4007). Desktop keeps the hover reveal.

Best practices

Do

  • Pick the width mode from the space, and let it decide how the assignee reads.
  • Give the list loading and error instead of rendering nothing — both states are built in, with a retry.
  • Show progress where the list has an end (2 z 5 hotovo), not on an open queue.
  • Keep the add row; a task list without a way to add a task is a dead end.
  • Pass statusKind and priority on every task node (toTLTask does it from Task.status / Task.priority), so the lead tells in-progress and blocked from open.
  • Pass stateName so the state glyph's title names the bucket — the row itself stays glyph-only.
  • Use task.project (with milestone when there is one) for the linked slot when a task belongs to a project; reserve links (deal/contacts) for tasks that don't.

Don't

  • Reimplement the row to add a column; the modes are the extension point.
  • Use full in a narrow column of a wide window — the breakpoint is the viewport, so it belongs to /tasks.
  • Hide a zero count by removing the icon; the row keeps its shape.
  • Bring back a checkbox, a coloured pill for priority, a hairline under the row or a column header — the lead is the one place state and priority are drawn, and the grid plus hover fill are what lead the eye now.
  • Draw more than one linked entity, or a „+N" for contacts — one reference, chosen by the project → deal → first contact order.
  • Give an AI suggestion row a checkbox or a live state button — a suggestion isn't a task yet; .tl-state.sug stays a muted, non-interactive glyph until it's approved.
  • Keep a second table of state colours or glyphs in the app; they live in @acrm/ui lib/task-glyphs.ts.

Content

  • Titles are what the user would write on a sticky note: Zavolat Josefu Novákovi.
  • Dates follow the year convention; „dnes“ and „zítra“ are words, not dates.
  • item-list — the canonical row for everything that is not a task.
  • pill — the .sm tag chips in the right-hand group.
  • section-block — the heading a task list usually sits under.
  • dialog — the task detail.
  • Stories: components--task-list--default, --variants, --sizes, --states, --with-icon-and-kbd, --composition; legacy overview components--task-list, components--task-list-plne-.

Source: docs/task-list/task-list.usage.md · GitLab