@lotics/ui 16.1.0 → 17.0.0

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
 
@@ -571,7 +572,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
571
572
  select.
572
573
  - **`form_date_picker`** — `FormDatePicker`: `FormField` wrapping a `DatePicker` — one
573
574
  labeled date field. (Omits `style`; for a grid cell use a bare `FormField style={half}`
574
- around `DatePicker`.)
575
+ around `DatePicker`.) Defaults the picker's `accessibilityLabel` to its own visible label
576
+ — the FormField label text is not otherwise associated with the segment inputs. A bare
577
+ `DatePicker` outside a FormField must be given `accessibilityLabel` explicitly; range
578
+ formats suffix each half with the locale's start/end-date names.
575
579
  - **`switch_button`** — `SwitchButton`: the full-ROW toggle — a `PressableHighlight` row
576
580
  (optional icon + medium title left, `Switch` pinned right) where the whole row IS the
577
581
  switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu
@@ -605,7 +609,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
605
609
  ### Inline editing & record surfaces
606
610
 
607
611
  - **`inline_edit`** — `useInlineEdit` + `InlineEditView` / `InlineEditFrame` +
608
- `INLINE_CONTROL_HEIGHT` (40) + `inlineValueTextStyle`: the engine custom inline editors
612
+ `useInlineEditFocusRestore` (returns focus to the RESTING control when a KEYBOARD close drops
613
+ it to `<body>` — what an editor with its own resting shape, e.g. the checklist `note` line,
614
+ reaches for instead of the frame) + `INLINE_CONTROL_HEIGHT` (40) +
615
+ `inlineValueTextStyle`: the engine custom inline editors
609
616
  join through — a view ⇄ edit toggle, a draft buffer, async `onSave` with the spinner
610
617
  INSIDE the control and inline error, commit on blur (Enter saves, Escape reverts) or
611
618
  `controls="buttons"`; KEYBOARD focus on the closed view opens edit mode with the input
@@ -657,7 +664,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
657
664
  label+value row for drawer/peek detail; in FORM mode (`labelWidth` set) the value column
658
665
  FILLS the row so a stack of inline editors all span the same width + none jumps wider on
659
666
  edit; optional `trailing` slot renders a right-side action/badge after the value (units
660
- belong IN the value via `InlineNumberInput format`). The FIELD-ANNOTATION vocabulary (same
667
+ belong IN the value via `InlineNumberInput format`). The LABEL WRAPS inside its column and
668
+ is never clipped — a fixed `labelWidth` would otherwise ellipsize every long field name
669
+ ("Registered business address"), and a name the reader can't finish is worse than a taller
670
+ row; a wrapped label's FIRST line stays level with the value's first control line while a
671
+ one-line label still centers on it (both modes, no prop). The FIELD-ANNOTATION vocabulary (same
661
672
  names + meanings as `FormField`), always EXPLICIT — a row never hides guidance behind an ⓘ:
662
673
  **`description`** = a fact / persistent guidance (muted), under the VALUE (stacked mode
663
674
  mirrors the form order label · description · control); **`warning`** = a consequence to weigh
@@ -737,27 +748,37 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
737
748
 
738
749
  ### Tasks & checklists
739
750
 
740
- - **`check_circle`** — `CheckCircle`: the completion ring — an empty ring that springs to a
741
- filled check when done, distinct from the square checkbox; the task/to-do/checklist
742
- toggle.
743
- - **`checklist`** `Checklist` + `ChecklistRow`the record-scoped checklist COMPOUND: it
744
- owns GEOMETRY only (row minHeight 32, gap 12, control/title alignment, the `meta`/
745
- `expansion` indent, ONE `trailingWidth` so assignee cells column-align) while content
746
- stays composed`control` takes the `CheckCircle` (omit onChange = read-only ring; a
747
- PICKER list puts a `CheckboxInput` here and sets `controlWidth={24}` so the indent
748
- tracks), children the struck transparent `InlineTextInput` or plain `Text`, `trailing` an
749
- `InlineMemberSelect`, `menu` the row's options
750
- (`{items: ActionMenuItem[], accessibilityLabel}`) Delete lives BEHIND the menu,
751
- danger-styled and last, never a bare (omit on read-only rows). NARROW surfaces (a
752
- drawer/peek checklist) put the editors on the `meta` line instead of `trailing` the
753
- second line indents to the title's text edge so the TITLE keeps the full width; wide
754
- surfaces use `trailing`; never both. `expansion` is a BLOCK slot under the row on the
755
- same indent a transient inline fill editor, a drill-down rendered only while open
756
- (`tpl_record`'s Document set rows are the worked example). SUGGESTIONS are never rows:
757
- offer the commons as `SuggestionChip`s under the list. Close the list with `CaptureRow`.
758
- There is deliberately NO monolithic Task component richer task-management rows compose
759
- their own anatomy directly. Hand-rolling this checkbox-row anatomy in a template is how
760
- 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 edge — an 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 app — hold 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
+
761
782
  - **`suggestion_chip`** — `SuggestionChip`: the dismissible SUGGESTION pill — an item the
762
783
  record could have but doesn't yet (a common task, an expected line) as a `Chip` whose
763
784
  press MATERIALIZES it (plus glyph + label, one tuned anatomy) and whose ✕ refuses it;
@@ -440,7 +440,9 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
440
440
  fact or consequence, never widget mechanics, never a second sentence; `warning` marks a
441
441
  consequence, not decoration. Rows share ONE
442
442
  alignment law: the row top-aligns and label · control · trailing each center within the
443
- first control line — a tall value block never drags the label. A FLAT value row
443
+ first control line — a tall value block never drags the label, and a LABEL too long for its
444
+ column WRAPS (never clipped, never abbreviated to fit) with its first line still level with
445
+ the value. A FLAT value row
444
446
  (`InlineStatic`, plain `Text`) sets **`DetailRow flat`**: the annotation tucks up by the
445
447
  control band's slack so the perceived gap under the text equals the gap under a chip —
446
448
  the band's invisible bottom half must never read as a hole above the description.
@@ -157,7 +157,9 @@ narrower than its neighbours'. Rows share ONE alignment law: the row top-aligns
157
157
  control · trailing each center within the first control line — annotations (the three tones
158
158
  under the value: `description` muted · `warning` amber · `error` danger, each indented to the
159
159
  chip's 8px text inset) grow the row downward without dragging the label. The full annotation
160
- vocabulary lives in composition.md §Field annotations.
160
+ vocabulary lives in composition.md §Field annotations. A long LABEL wraps inside its column
161
+ instead of clipping — never shorten a field name to fit the label width, and never reach for a
162
+ tooltip to recover the tail; the wrapped label's first line stays level with the value.
161
163
 
162
164
  ### The editability affordance
163
165
 
@@ -253,22 +255,46 @@ When work crosses departments (sales → operations → accounting), the HANDOFF
253
255
  changing desks — never an inbox, a notification, or a copied task. Two types:
254
256
 
255
257
  **(A) Same entity** → a STAGE field on the shared record: sections carry OWNER dot tags, and the
256
- handoff is MANAGED AS TASKS — each desk's checklist on the record. The **`Checklist`** compound
257
- OWNS the list geometry (row height, ring/title alignment, and the one trailing column, set once
258
- via `trailingWidth`) — compose it, never hand-roll the row. Each **`ChecklistRow`** takes a
259
- `control` (a `CheckCircle` — a read-only ring when it has no `onChange`), its title as children (a
260
- struck transparent `InlineTextInput`, or plain `Text`), then EITHER a `trailing` cell on wide
261
- surfaces (the per-task assignee `InlineMemberSelect`, sized by the list's `trailingWidth`) OR a
262
- `meta` line under the title on NARROW surfaces (a drawer/popover checklist the two are mutually
263
- exclusive), plus a `menu` for the row's options — never a decorative progress strip. `CaptureRow`
264
- closes the list as its add-affordance. 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 parent's position is computable from its children, DERIVE
279
+ it and render the parent's status read-only — an editable copy lets a row disagree with its own
280
+ children, and makes the operator enter one fact twice. Give the parent's ring the bulk gesture
281
+ instead (tick it, every child ticks). A parent with nothing to derive from is set by hand.
282
+
283
+ `CheckCircle` has THREE positions (`state="none" | "partial" | "done"`), and `partial` is not
284
+ optional dressing: a step whose vocabulary reads "No portal account → Account created → Filed
285
+ online" is neither finished nor untouched in the middle, and a binary ring calls it untouched — in
286
+ the gutter column, which is the fastest scan on the surface. Its CLICK stays binary (finish /
287
+ reopen) so the ring means one thing everywhere; `partial` is reached through whatever NAMES it —
288
+ an `InlineSelect` status cell in that step's own words, or children ticking off — never by cycling
289
+ the ring. Keep the ring MONOCHROME and let colour live in the status cell, or the row double-codes
290
+ one fact. A NEW record starts with an EMPTY checklist — tasks truly
265
291
  vary. The commons split in two: MANDATORY tasks are seeded by the app (a workflow on create, per
266
292
  record type) — no human types them; common-but-OPTIONAL tasks appear as `SuggestionChip`s under
267
293
  the list — a PILL, never a row, so a suggestion can't be mistaken for a task (tap = materialize,
268
294
  ✕ = dismiss for this record; already-present labels filter out; suggestions never count in
269
295
  done/total and pause while a filter narrows the view) — the same suggestion grammar as the billing
270
- Standard pill. The row's `menu` (an `ActionMenu`; Delete lives BEHIND it, danger-styled and last
271
- never a bare ✕ a stray tap can hit) completes the checklist's CRUD.
296
+ Standard pill. `TaskActions` holds an `ActionMenu` (Delete lives BEHIND it, danger-styled and last
297
+ never a bare ✕ a stray tap can hit), which completes the list's CRUD.
272
298
 
273
299
  Open tasks INFORM the handoff, they NEVER block it: the CTA stays enabled, the count warns, and
274
300
  open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (assignee
@@ -276,9 +302,9 @@ open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (as
276
302
  the record left this register.
277
303
 
278
304
  Tasks PEEK from the register: the done/total column is a pressable compact-`ProgressBar` trigger
279
- whose popover holds the same `Checklist` on FIXED columns (`trailingWidth={140}`) every
280
- `ChecklistRow`'s ring `control` · struck `InlineTextInput` title (flex) · quick-reassign
281
- `InlineMemberSelect` `trailing` — no expandable rows (tags/files depth is
305
+ whose popover holds the same `Task` list every `TaskItem`'s ring `TaskStatus` · struck
306
+ `InlineTextInput` `TaskTitle` (flex) · quick-reassign `InlineMemberSelect` in `TaskFields`, which
307
+ takes its own line at popover width — no expandable rows (tags/files depth is
282
308
  the Task list template's lesson, not the peek's); the popover body is `PopoverContent`'s own
283
309
  ScrollView (`disableBodyScroll` is ONLY for children that manage their own scroll, like
284
310
  `OptionList`). The DRAWER carries a real Tasks SECTION in the Record template's shape (heading +
package/docs/templates.md CHANGED
@@ -284,15 +284,20 @@ 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
293
293
  (dashed add-ghost when unset), sized to the list's `trailingWidth` so the title never overlaps.
294
294
  (Contrast the General section's dates/assignee, the same editors in the default `variant="form"`
295
- — a zinc-50 chip + hover-border + the full member chip.)
295
+ — a zinc-50 chip + hover-border + the full member chip.) Two rows carry a **`note`** — the kit's
296
+ per-row muted second line (press to edit, save empty to remove); the rest show its add button.
297
+ ONE row ("Book the carrier") carries **`subtasks`** — four child ticks behind the kit's chevron +
298
+ remaining-count expander, open because work is left; every other row passes an EMPTY list, which
299
+ renders nothing at all. It is the line to copy: a booking's steps carry only a tick, so they stay
300
+ child rows — anything needing its own note/due/assignee would be a task instead.
296
301
  - **Documents** — the Agents "Document desk" pattern (this template is its worked example —
297
302
  see the Agents chapter below): the register `Table` (search · Add files) whose selection
298
303
  feeds the `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI).
@@ -348,13 +353,12 @@ billing, and quick-capture templates. Top → bottom:
348
353
  REPLACES the set — derived paperwork, never intake; the desk register above keeps what
349
354
  arrived. **READINESS**: a form declares the RECORD FIELDS it reads (`needs`); the fields'
350
355
  HOME stays their DATA section (the colocation law) with descriptions naming their
351
- consumers ("printed on the delivery note"). The picker rows ride `Checklist`/
352
- `ChecklistRow` (`controlWidth={24}` for the `CheckboxInput`) — the compound owns the row
353
- geometry and the `meta`/`expansion` indent; hand-rolling the anatomy is how alignment
354
- drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing (the
355
- `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
356
360
  gaps show WITHOUT checking, warning once checked because it now blocks). The FIX sits
357
- 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`:
358
362
  a warning Callout is an ARIA `alert`, wrong around a form) with a `FormTextInput` per gap
359
363
  (label + the consumer hint as `description`) and a "Save fields" (secondary — the section
360
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