@lotics/ui 16.2.0 → 17.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MIGRATION.md CHANGED
@@ -4,6 +4,53 @@ Breaking changes, newest first — normally per major, plus the rare minor that
4
4
  anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
5
  this file exists only to move an app from one release to the next.
6
6
 
7
+ ## v17 from 16.x
8
+
9
+ **`Checklist` / `ChecklistRow` are DELETED.** They are replaced by the `Task` compound
10
+ (`@lotics/ui/task`), which composes the anatomy instead of configuring it:
11
+
12
+ ```tsx
13
+ <TaskList> {/* was <Checklist trailingWidth={152}> */}
14
+ <TaskItem> {/* was <ChecklistRow …> */}
15
+ <TaskStatus><CheckCircle …/></TaskStatus> {/* was control= */}
16
+ <TaskTitle><InlineTextInput …/></TaskTitle> {/* was children */}
17
+ <TaskFields><InlineDatePicker …/></TaskFields> {/* was trailing= OR meta= */}
18
+ <TaskActions><ActionMenu …/></TaskActions> {/* was menu= */}
19
+ <TaskDetail>…</TaskDetail> {/* was expansion= */}
20
+ <TaskList>…</TaskList> {/* was subtasks= */}
21
+ </TaskItem>
22
+ </TaskList>
23
+ ```
24
+
25
+ Three of those are behaviour changes, not renames:
26
+
27
+ - **`trailing` and `meta` collapse into `TaskFields`.** They were mutually exclusive by type,
28
+ so an author had to decide "wide surface" or "narrow" while writing the JSX and was wrong on
29
+ the other one. `TaskFields` sits inline while the row has room and takes its own line when it
30
+ does not — intrinsically, with no viewport query, so it is also right inside a drawer.
31
+ - **`subtasks` becomes a NESTED `TaskList`.** The old `{key, label, checked, onToggle}[]` meant
32
+ a child could never carry a due date, a menu or children of its own. A subtask is now a task.
33
+ What you lose is the built-in chevron and remaining-count: collapsing is the app's call now —
34
+ hold a boolean and render the nested list or don't.
35
+ - **`note` is GONE, with no replacement.** A task's free text is its title, a descriptor
36
+ (`TaskFields`) or detail (`TaskDetail`); a fourth place to write invited writing it twice.
37
+ Move the value into whichever of those it actually is, or drop it.
38
+
39
+ Rows are now **44px minimum** (`density="dense"` opts down to 32 for a pointer-driven
40
+ register, set once on the list). The old row was 32 and the old subtask row 24 — under the
41
+ minimum tap target. `controlWidth` moves from `Checklist` to `TaskList` unchanged; the
42
+ `checklist` locale slice is deleted (its strings were all note/subtask chrome).
43
+
44
+ **`CheckCircle`'s `checked: boolean` is replaced by `state: "none" | "partial" | "done"`.**
45
+ Migrate with `checked={x}` → `state={x ? "done" : "none"}`; `onChange` is untouched (still
46
+ `(done: boolean) => void`, since the ring's GESTURE stays binary — finish or reopen). The new
47
+ middle position exists because a binary ring has to lie about it: a task at "doing", or a step
48
+ whose vocabulary reads "No portal account → Account created → Filed online", is neither finished
49
+ nor untouched in the middle, and an empty ring reports untouched — in the gutter column that is
50
+ the fastest scan on a task surface. Wherever a row already carries a three-position status, feed
51
+ it through (`state={s.status === "done" ? "done" : s.status === "doing" ? "partial" : "none"}`)
52
+ rather than leaving the ring binary.
53
+
7
54
  ## v16 from 15.x
8
55
 
9
56
  **`Confidence` IS now a callout with a REQUIRED basis** — `children` (what was checked and
package/docs/catalog.md CHANGED
@@ -105,12 +105,13 @@ summary is `Ledger`.
105
105
  ### Tasks / to-dos — pick by ALTITUDE
106
106
 
107
107
  A task-management PAGE (many tasks, grouping, filters, expandable rows) is COMPOSITION —
108
- there is NO Task component: `CheckCircle` + a struck `InlineTextInput` + your meta cells
109
- (`InlineDatePicker`/`InlineMemberSelect`/`InlineSelect`, all `variant="cell"`); see `tpl_record`'s
110
- Tasks section (the canonical task pattern) and [`tpl_task_board`](../examples/tpl_task_board.tsx) (columns). A record's own small CHECKLIST
111
- (the 5–8 tasks living in a drawer/section) is the `Checklist`/`ChecklistRow` compound +
112
- `SuggestionChip` commons + `CaptureRow` see [`tpl_record`](../examples/tpl_record.tsx) /
113
- [`tpl_item_list`](../examples/tpl_item_list.tsx).
108
+ the `Task` compound owns the row — `TaskStatus` (a `CheckCircle`), `TaskTitle` (a struck
109
+ `InlineTextInput`), `TaskFields` (your `InlineDatePicker`/`InlineMemberSelect`/`InlineSelect`
110
+ cells, all `variant="cell"`), `TaskActions`. The SAME composition serves a record's 5–8-task
111
+ drawer checklist and a grouped desk board, because `TaskFields` reflows instead of being
112
+ authored per surface; add `SuggestionChip` commons + `CaptureRow` for the checklist case. See
113
+ [`tpl_record`](../examples/tpl_record.tsx) / [`tpl_item_list`](../examples/tpl_item_list.tsx),
114
+ and [`tpl_task_board`](../examples/tpl_task_board.tsx) for columns.
114
115
 
115
116
  ### Tabular data — pick by SCALE + intent
116
117
 
@@ -747,53 +748,37 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
747
748
 
748
749
  ### Tasks & checklists
749
750
 
750
- - **`check_circle`** — `CheckCircle`: the completion ring — an empty ring that springs to a
751
- filled check when done, distinct from the square checkbox; the task/to-do/checklist
752
- toggle.
753
- - **`checklist`** `Checklist` + `ChecklistRow`the record-scoped checklist COMPOUND: it
754
- owns GEOMETRY only (row minHeight 32, gap 12, control/title alignment, the `note`/`meta`/
755
- `subtasks`/`expansion` indent, ONE `trailingWidth` so assignee cells column-align) while content
756
- stays composed`control` takes the `CheckCircle` (omit onChange = read-only ring; a
757
- PICKER list puts a `CheckboxInput` here and sets `controlWidth={24}` so the indent
758
- tracks), children the struck transparent `InlineTextInput` or plain `Text`, `trailing` an
759
- `InlineMemberSelect`, `menu` the row's options
760
- (`{items: ActionMenuItem[], accessibilityLabel}`) Delete lives BEHIND the menu,
761
- danger-styled and last, never a bare (omit on read-only rows). NARROW surfaces (a
762
- drawer/peek checklist) put the editors on the `meta` line instead of `trailing` the
763
- second line indents to the title's text edge so the TITLE keeps the full width; wide
764
- surfaces use `trailing`; never both. **`note` is the row's own free-text line** —
765
- `{value, onSave, addLabel?, editLabel?, placeholder?}` (`ChecklistRowNote`): the note
766
- renders as a muted line directly under the title (above `meta`, on its text edge) and is
767
- ALWAYS visible never behind `expansion` presses to edit in place, and saving EMPTY
768
- removes it (`onSave("")`, whitespace trimmed first clearing IS the delete, so a note
769
- needs no entry). A row with no note shows a small `sticky-note` add button instead,
770
- which opens the focused empty editor. It composes with `trailing` AND `meta` (a second
771
- LINE, not a replacement), the save may be async (spinner + inline error, editor stays
772
- open), and chrome comes from the `checklist` locale slice (`addNote`, `editNote(note)`
773
- it wraps the text so the line announces with it `notePlaceholder`). Never hand-roll a
774
- muted sub-line + editor on a checklist row. **`subtasks` are the row's collapsible CHILD
775
- STEPS** — `{items, expanded?, onToggleExpanded?, countLabel?}` (`ChecklistRowSubtasks`),
776
- each item a `ChecklistSubtask` `{key, label, checked, onToggle}` and nothing more (no
777
- note, no assignee, no ⋯: a step needing those is a TASKgive it a row). The parent grows
778
- a chevron expander on the TRAILING side of the title (never the leading edge that's
779
- `control`, and a chevron there would knock the title off the text edge on every row
780
- WITHOUT steps) carrying the count of UNCHECKED steps beside it; expanded, the children
781
- render as compact `CheckboxInput` + label rows on the title's text edge, denser than the
782
- parent (24 vs 32) and struck+muted when checked. `items: []` renders NOTHING, not even the
783
- expander, so a row can pass the prop unconditionally. Expansion is controlled with
784
- `expanded` + `onToggleExpanded`, or UNCONTROLLED (omit `expanded`): open on mount while any
785
- step is unchecked, then the user's toggle wins — checking the last step must not yank the
786
- list shut. A11y: the expander keeps ONE stable name (`countLabel(missing)` → the
787
- `checklist` locale slice's `subtasks(missing)` → English) with `aria-expanded` carrying the
788
- state, and the children sit in a `role="group"` named by the row's own title, so they are
789
- announced UNDER their parent instead of as loose checkboxes. Never hand-roll indented child
790
- rows with a bespoke chevron. `expansion` is a BLOCK slot under the row on
791
- the same indent — a transient inline fill editor, a drill-down — rendered only while open
792
- (`tpl_record`'s Document set rows are the worked example). SUGGESTIONS are never rows:
793
- offer the commons as `SuggestionChip`s under the list. Close the list with `CaptureRow`.
794
- There is deliberately NO monolithic Task component — richer task-management rows compose
795
- their own anatomy directly. Hand-rolling this checkbox-row anatomy in a template is how
796
- alignment drifts — the compound exists so it can't.
751
+ - **`check_circle`** — `CheckCircle`: the completion ring — `state="none" | "partial" | "done"`,
752
+ springing to a filled check when done, distinct from the square checkbox; the
753
+ task/to-do/checklist control. `partial` half-fills it, for the row that is underway but not
754
+ finished (a task at "doing", a step whose children are half ticked) a binary ring reports
755
+ that row as untouched, in the column readers scan fastest. The CLICK stays binary
756
+ (`onChange(done)`) so the ring means one thing everywhere; `partial` is reached through
757
+ whatever NAMES it a status cell, or children ticking off never by cycling the ring. Keep
758
+ it monochrome and let colour live in the status cell.
759
+ - **`task`** `TaskList` + `TaskItem` + `TaskStatus` / `TaskTitle` / `TaskFields` /
760
+ `TaskActions` / `TaskDetail` the task COMPOUND, for anything from a 5-item drawer
761
+ checklist to a grouped desk board. `TaskList` owns geometry only (the `controlWidth`
762
+ column every title aligns on, the indent, and `density`: `comfortable` = a 44px minimum
763
+ tap target, `dense` = 32 for a pointer-driven register). Everything else is composed, and
764
+ JSX order is screen order nothing inspects child types.
765
+
766
+ `TaskStatus` takes the `CheckCircle` (omit `onChange` for a read-only ring; a PICKER list
767
+ puts a `CheckboxInput` here and sets `controlWidth={24}`). `TaskTitle` takes the struck
768
+ transparent `InlineTextInput` or plain `Text`, and claims a readable minimum width which
769
+ is what makes the rest reflow. `TaskFields` is the descriptor cluster (due, assignee,
770
+ chips): it sits inline after the title while the row has room and takes its own line when
771
+ it does not. That is intrinsic, so the SAME JSX is right on a record page and in a drawer —
772
+ never author two variants, and never reach for a viewport query. `TaskActions` carries the
773
+ row's `ActionMenu`; Delete lives BEHIND it, danger-styled and last, never a bare ✕.
774
+ `TaskDetail` is a block under the row on the title's text edgean inline fill editor, a
775
+ drill-down rendered only while open.
776
+
777
+ **Subtasks are a nested `TaskList`**, so a step IS a task: give one a due date, a menu or
778
+ children of its own and it works. Collapsing belongs to the apphold a boolean and render
779
+ the nested list or don't. There is deliberately **no note slot**: a task's free text is its
780
+ title, a descriptor, or detail, and a fourth place to write invited writing it twice.
781
+
797
782
  - **`suggestion_chip`** — `SuggestionChip`: the dismissible SUGGESTION pill — an item the
798
783
  record could have but doesn't yet (a common task, an expected line) as a `Chip` whose
799
784
  press MATERIALIZES it (plus glyph + label, one tuned anatomy) and whose ✕ refuses it;
@@ -255,34 +255,62 @@ When work crosses departments (sales → operations → accounting), the HANDOFF
255
255
  changing desks — never an inbox, a notification, or a copied task. Two types:
256
256
 
257
257
  **(A) Same entity** → a STAGE field on the shared record: sections carry OWNER dot tags, and the
258
- handoff is MANAGED AS TASKS — each desk's checklist on the record. The **`Checklist`** compound
259
- OWNS the list geometry (row height, ring/title alignment, and the one trailing column, set once
260
- via `trailingWidth`) — compose it, never hand-roll the row. Each **`ChecklistRow`** takes a
261
- `control` (a `CheckCircle` — a read-only ring when it has no `onChange`), its title as children (a
262
- struck transparent `InlineTextInput`, or plain `Text`), then EITHER a `trailing` cell on wide
263
- surfaces (the per-task assignee `InlineMemberSelect`, sized by the list's `trailingWidth`) OR a
264
- `meta` line under the title on NARROW surfaces (a drawer/popover checklist the two are mutually
265
- exclusive), plus a `menu` for the row's options — never a decorative progress strip. `CaptureRow`
266
- closes the list as its add-affordance. A task's own free text — the "why it's stuck" nothing else on
267
- the record holds is the row's **`note`** (`{value, onSave}`): a muted line under the title, ALWAYS
268
- visible (a note behind an expansion gets written twice), pressed to edit in place, and REMOVED by
269
- saving it empty so it needs no entry and no second field. A note-less row carries a small add
270
- button that opens the focused editor. It composes with `trailing` AND `meta` a second LINE, not a
271
- replacement so a per-row note is never a hand-rolled sub-line, a comment thread, or an
272
- expansion. A task made of several ticks takes **`subtasks`** (`{items, expanded?,
273
- onToggleExpanded?, countLabel?}`) rather than N sibling rows: the parent grows a chevron + the
274
- count of unchecked steps, and the children — `{key, label, checked, onToggle}` and NOTHING else
275
- render indented to the title's text edge when open. The line is sharp: a step that needs its own
276
- note, due date, assignee, or menu is a TASK, so give it a row; subtasks are for the ticks that
277
- only make sense under their parent. Omit `expanded` and the row opens itself while work is left,
278
- then hands the state to the user. A NEW record starts with an EMPTY checklist tasks truly
258
+ handoff is MANAGED AS TASKS — each desk's checklist on the record. The **`Task`** compound owns
259
+ the list geometry (the control column, the indent, and `density` 44px targets by default) —
260
+ compose it, never hand-roll the row. A **`TaskItem`** carries a `TaskStatus` (a `CheckCircle` — a
261
+ read-only ring when it has no `onChange`), a `TaskTitle` (a struck transparent `InlineTextInput`,
262
+ or plain `Text`), a `TaskFields` cluster (the per-task assignee `InlineMemberSelect`, the due
263
+ date) and a `TaskActions` menu never a decorative progress strip. `TaskFields` sits inline
264
+ while the row has room and takes its own line when it does not, so the drawer and the record page
265
+ run the SAME composition. `CaptureRow` closes the list as its add-affordance.
266
+
267
+ A task's own free text is its TITLE, a **`TaskCaption`** or a **`TaskDetail`** there is no
268
+ fourth place to write, because a fourth place gets written in twice. `TaskCaption` is the row's
269
+ state IN WORDS ("3 of 5 papers received", "waiting on the yard") on its own line under the title;
270
+ it is not a `TaskFields` cell, because a field is a VALUE the reader scans down a column and a
271
+ caption is a SENTENCE about one row merging them pushes prose into the value column, where it
272
+ aligns with nothing.
273
+
274
+ **Subtasks are TASKS** nest a `TaskList` inside the `TaskItem`, and the child carries a due
275
+ date, a menu and children of its own. There is no separate subtask shape to outgrow, and no
276
+ built-in chevron: collapsing is the app's call (hold a boolean, render the nested list or don't).
277
+
278
+ **State flows from the LEAVES.** Where a row's position is computable from what it owns, DERIVE
279
+ it — a stored status is a second copy of what the fields already say, and two copies drift. Give
280
+ the parent's ring the bulk gesture where its children ARE the fact (tick it, every child ticks);
281
+ where the ring answers a DIFFERENT field the children do not determine — a date, say — coupling
282
+ them asserts something the operator never said.
283
+
284
+ **`TaskFields` holds CONTROLS.** Where exactly one field determines a row's status, that cell IS
285
+ the field's editor: a read-only copy above an editable control puts one value on the row twice,
286
+ under two labels. Everything else derived is prose in `TaskCaption`. A read-only pill parked in
287
+ the column sits `TASK_TEXT_INSET` left of its neighbours (a `variant="cell"` control insets its
288
+ text by that much; a bare `Badge` by nothing) and reads as pressable when it is not. A `Badge`
289
+ INSIDE a cell control is fine — it inherits that cell's inset, and the 12px its label sits in by
290
+ is the dot and its gap, not padding.
291
+
292
+ **A status reports what is OUTSTANDING, never what is settled.** A finished row already says so
293
+ twice — a full ring and its date — so naming the settled condition is a third copy of one fact.
294
+ The empty end is worse than redundant: "every document is in" and "nobody has looked yet" are the
295
+ same empty list, so a badge asserting the first states something no one established. Stay silent
296
+ at both ends, and separate "is it under way" (which drives the ring) from "has it anything to
297
+ say" (which drives the caption) — fusing them forces every pending state to invent a label.
298
+
299
+ `CheckCircle` has THREE positions (`state="none" | "partial" | "done"`), and `partial` is not
300
+ optional dressing: a step whose vocabulary reads "No portal account → Account created → Filed
301
+ online" is neither finished nor untouched in the middle, and a binary ring calls it untouched — in
302
+ the gutter column, which is the fastest scan on the surface. Its CLICK stays binary (finish /
303
+ reopen) so the ring means one thing everywhere; `partial` is reached through whatever NAMES it —
304
+ an `InlineSelect` status cell in that step's own words, or children ticking off — never by cycling
305
+ the ring. Keep the ring MONOCHROME and let colour live in the status cell, or the row double-codes
306
+ one fact. A NEW record starts with an EMPTY checklist — tasks truly
279
307
  vary. The commons split in two: MANDATORY tasks are seeded by the app (a workflow on create, per
280
308
  record type) — no human types them; common-but-OPTIONAL tasks appear as `SuggestionChip`s under
281
309
  the list — a PILL, never a row, so a suggestion can't be mistaken for a task (tap = materialize,
282
310
  ✕ = dismiss for this record; already-present labels filter out; suggestions never count in
283
311
  done/total and pause while a filter narrows the view) — the same suggestion grammar as the billing
284
- Standard pill. The row's `menu` (an `ActionMenu`; Delete lives BEHIND it, danger-styled and last
285
- never a bare ✕ a stray tap can hit) completes the checklist's CRUD.
312
+ Standard pill. `TaskActions` holds an `ActionMenu` (Delete lives BEHIND it, danger-styled and last
313
+ never a bare ✕ a stray tap can hit), which completes the list's CRUD.
286
314
 
287
315
  Open tasks INFORM the handoff, they NEVER block it: the CTA stays enabled, the count warns, and
288
316
  open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (assignee
@@ -290,9 +318,9 @@ open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (as
290
318
  the record left this register.
291
319
 
292
320
  Tasks PEEK from the register: the done/total column is a pressable compact-`ProgressBar` trigger
293
- whose popover holds the same `Checklist` on FIXED columns (`trailingWidth={140}`) every
294
- `ChecklistRow`'s ring `control` · struck `InlineTextInput` title (flex) · quick-reassign
295
- `InlineMemberSelect` `trailing` — no expandable rows (tags/files depth is
321
+ whose popover holds the same `Task` list every `TaskItem`'s ring `TaskStatus` · struck
322
+ `InlineTextInput` `TaskTitle` (flex) · quick-reassign `InlineMemberSelect` in `TaskFields`, which
323
+ takes its own line at popover width — no expandable rows (tags/files depth is
296
324
  the Task list template's lesson, not the peek's); the popover body is `PopoverContent`'s own
297
325
  ScrollView (`disableBodyScroll` is ONLY for children that manage their own scroll, like
298
326
  `OptionList`). The DRAWER carries a real Tasks SECTION in the Record template's shape (heading +
package/docs/templates.md CHANGED
@@ -284,9 +284,9 @@ billing, and quick-capture templates. Top → bottom:
284
284
  file-capable edit form injected via `renderEditForm`) + THE kit `Composer` (attach via
285
285
  `actionsButton`, staged files in its `files` slot) — never a bespoke comment box.
286
286
  - **Tasks** — the full task-list grammar: clearable Group-by `FilterChip` (+ assignee/status
287
- chips), the `CaptureRow` on top, `Checklist`/`ChecklistRow` rows and per-row menus,
287
+ chips), the `CaptureRow` on top, `TaskList`/`TaskItem` rows and per-row `TaskActions` menus,
288
288
  suggestions as `SuggestionChip`s; groups divide via the `SubsectionStack` beat with plain
289
- text heads (no dot badges). Each row's trailing carries **due + assignee as COMPACT grid CELLS** —
289
+ text heads (no dot badges). Each row's `TaskFields` carries **due + assignee as COMPACT grid CELLS** —
290
290
  the SAME `InlineDatePicker` / `InlineMemberSelect` used in the General section, in **`variant="cell"`**
291
291
  (transparent rest + wash on hover, no glyph/chevron): the due a urgency-coloured date (`tone`
292
292
  = `dueTone` — red past due, amber ≤3d, else muted), the assignee an **`avatarOnly`** bare avatar
@@ -353,13 +353,12 @@ billing, and quick-capture templates. Top → bottom:
353
353
  REPLACES the set — derived paperwork, never intake; the desk register above keeps what
354
354
  arrived. **READINESS**: a form declares the RECORD FIELDS it reads (`needs`); the fields'
355
355
  HOME stays their DATA section (the colocation law) with descriptions naming their
356
- consumers ("printed on the delivery note"). The picker rows ride `Checklist`/
357
- `ChecklistRow` (`controlWidth={24}` for the `CheckboxInput`) — the compound owns the row
358
- geometry and the `meta`/`expansion` indent; hand-rolling the anatomy is how alignment
359
- drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing (the
360
- `meta` line: 13px `circle-alert` + "Needs: …" the actual field labels — muted at rest so
356
+ consumers ("printed on the delivery note"). The picker rows ride `TaskList`/`TaskItem`
357
+ (`controlWidth={24}` for the `CheckboxInput` in `TaskStatus`) — the compound owns the row
358
+ geometry and the indent; hand-rolling the anatomy is how alignment drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing (the
359
+ `TaskFields` line: 13px `circle-alert` + "Needs: …" the actual field labels muted at rest so
361
360
  gaps show WITHOUT checking, warning once checked because it now blocks). The FIX sits
362
- right under it in the row's `expansion` — an `Inset` (a FORM surface, NEVER a `Callout`:
361
+ right under it in the row's `TaskDetail` — an `Inset` (a FORM surface, NEVER a `Callout`:
363
362
  a warning Callout is an ARIA `alert`, wrong around a form) with a `FormTextInput` per gap
364
363
  (label + the consumer hint as `description`) and a "Save fields" (secondary — the section
365
364
  keeps ONE primary, disabled while every draft is empty). Save writes the record fields
@@ -21,7 +21,7 @@ import { IconButton } from "@lotics/ui/icon_button";
21
21
  import { ListItem } from "@lotics/ui/list_item";
22
22
  import { PressableHighlight } from "@lotics/ui/pressable_highlight";
23
23
  import { CaptureRow } from "@lotics/ui/capture_row";
24
- import { Checklist, ChecklistRow } from "@lotics/ui/checklist";
24
+ import { TaskActions, TaskFields, TaskItem, TaskList, TaskStatus, TaskTitle } from "@lotics/ui/task";
25
25
  import { SuggestionChip } from "@lotics/ui/suggestion_chip";
26
26
  import { Screen, ScreenRouter, useScreenRouter } from "@lotics/ui/screen_router";
27
27
  import { SummaryLine } from "@lotics/ui/summary_line";
@@ -203,49 +203,52 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
203
203
  const addSuggested = (label: string) => onChange([...tasks, { id: newTaskId(), label, done: false, assignee: null, due: "" }]);
204
204
  const pending = suggestions.filter((l) => !tasks.some((t) => t.label === l));
205
205
  return (
206
- <Checklist>
207
- {/* NARROW anatomy: the title owns its full line (readable in a drawer
208
- or peek); assignee + due edit on the indented meta line below */}
206
+ <TaskList>
207
+ {/* ONE anatomy for every surface. The assignee and due editors sit inline
208
+ after the title when the row has room, and take their own line when it
209
+ does not — a drawer, a peek, a phone. The template no longer picks. */}
209
210
  {tasks.map((t) => (
210
- <ChecklistRow
211
- key={t.id}
212
- control={<CheckCircle checked={t.done} onChange={(on) => patch(t.id, { done: on })} accessibilityLabel={t.label} />}
213
- menu={{
214
- items: [{ key: "xoa", label: "Delete task", icon: "trash", danger: true, onPress: () => onChange(tasks.filter((x) => x.id !== t.id)) }],
215
- accessibilityLabel: `Task options: ${t.label}`,
216
- }}
217
- meta={
218
- <>
219
- <View style={{ width: 150 }}>
220
- <InlineMemberSelect
221
- variant="cell"
222
- members={MEMBERS}
223
- value={t.assignee}
224
- onSave={(m) => patch(t.id, { assignee: m })}
225
- placeholder="Assign…"
226
- accessibilityLabel={`Assignee · ${t.label}`}
227
- />
228
- </View>
229
- <View style={{ width: 130 }}>
230
- <InlineDatePicker
231
- variant="cell"
232
- value={t.due}
233
- onSave={(v) => patch(t.id, { due: v })}
234
- placeholder="Set due…"
235
- accessibilityLabel={`Due · ${t.label}`}
236
- />
237
- </View>
238
- </>
239
- }
240
- >
241
- <InlineTextInput
242
- variant="cell"
243
- value={t.label}
244
- onSave={(v) => patch(t.id, { label: v })}
245
- struck={t.done}
246
- accessibilityLabel="Task title"
247
- />
248
- </ChecklistRow>
211
+ <TaskItem key={t.id}>
212
+ <TaskStatus>
213
+ <CheckCircle state={t.done ? "done" : "none"} onChange={(on) => patch(t.id, { done: on })} accessibilityLabel={t.label} />
214
+ </TaskStatus>
215
+ <TaskTitle>
216
+ <InlineTextInput
217
+ variant="cell"
218
+ value={t.label}
219
+ onSave={(v) => patch(t.id, { label: v })}
220
+ struck={t.done}
221
+ accessibilityLabel="Task title"
222
+ />
223
+ </TaskTitle>
224
+ <TaskFields>
225
+ <View style={{ width: 150 }}>
226
+ <InlineMemberSelect
227
+ variant="cell"
228
+ members={MEMBERS}
229
+ value={t.assignee}
230
+ onSave={(m) => patch(t.id, { assignee: m })}
231
+ placeholder="Assign…"
232
+ accessibilityLabel={`Assignee · ${t.label}`}
233
+ />
234
+ </View>
235
+ <View style={{ width: 130 }}>
236
+ <InlineDatePicker
237
+ variant="cell"
238
+ value={t.due}
239
+ onSave={(v) => patch(t.id, { due: v })}
240
+ placeholder="Set due…"
241
+ accessibilityLabel={`Due · ${t.label}`}
242
+ />
243
+ </View>
244
+ </TaskFields>
245
+ <TaskActions>
246
+ <ActionMenu
247
+ items={[{ key: "xoa", label: "Delete task", icon: "trash", danger: true, onPress: () => onChange(tasks.filter((x) => x.id !== t.id)) }]}
248
+ accessibilityLabel={`Task options: ${t.label}`}
249
+ />
250
+ </TaskActions>
251
+ </TaskItem>
249
252
  ))}
250
253
  {/* SUGGESTIONS are pills, not rows — a chip can't be mistaken for a
251
254
  task. Tap = materialize; ✕ = dismiss it for this record. */}
@@ -257,7 +260,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
257
260
  </View>
258
261
  ) : null}
259
262
  <CaptureRow value={draft} onChangeText={setDraft} onSubmit={add} placeholder="Add a task…" accessibilityLabel="Add a task" />
260
- </Checklist>
263
+ </TaskList>
261
264
  );
262
265
  }
263
266