@lotics/ui 18.1.0 → 19.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 +65 -0
- package/docs/catalog.md +78 -26
- package/docs/data_entry.md +55 -21
- package/docs/templates.md +17 -17
- package/examples/tpl_item_list.tsx +26 -26
- package/examples/tpl_record.tsx +38 -38
- package/package.json +1 -1
- package/src/detail_row.tsx +7 -1
- package/src/file_grid.tsx +5 -0
- package/src/file_thumbnail.tsx +27 -10
- package/src/file_thumbnail_grid.tsx +4 -0
- package/src/linked_record_box.tsx +30 -13
- package/src/task.tsx +347 -135
- package/src/task_metrics.test.ts +48 -0
- package/src/task_metrics.ts +55 -0
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,71 @@ 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
|
+
## v19 from 18.x
|
|
8
|
+
|
|
9
|
+
**`TaskFields` is DELETED, with `fieldsWidth`.** A task row was carrying a
|
|
10
|
+
value COLUMN — a width declared on the list and reserved by every row. It aligned, and it read
|
|
11
|
+
badly: a two-word field name sat at the title's origin while its control started a quarter of
|
|
12
|
+
the surface away, and nothing could close the gap because the column was sized for the widest
|
|
13
|
+
value on the page. A task row now carries its TITLE; the fields a user can SET hang under it as
|
|
14
|
+
`TaskSubRow`s, name beside value.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// BEFORE
|
|
18
|
+
<TaskList fieldsWidth={320}>
|
|
19
|
+
<TaskItem>
|
|
20
|
+
<TaskStatus><CheckCircle …/></TaskStatus>
|
|
21
|
+
<TaskTitle>File it on the portal</TaskTitle>
|
|
22
|
+
<TaskFields>
|
|
23
|
+
<View style={{ flexBasis: 208 }}><InlineSelect variant="cell" …/></View>
|
|
24
|
+
<View style={{ width: 104 }}><InlineDatePicker variant="cell" …/></View>
|
|
25
|
+
</TaskFields>
|
|
26
|
+
<TaskActions><ActionMenu …/></TaskActions>
|
|
27
|
+
</TaskItem>
|
|
28
|
+
</TaskList>
|
|
29
|
+
|
|
30
|
+
// AFTER
|
|
31
|
+
<TaskList>
|
|
32
|
+
<TaskItem>
|
|
33
|
+
<TaskStatus><CheckCircle …/></TaskStatus>
|
|
34
|
+
<TaskTitle>File it on the portal</TaskTitle>
|
|
35
|
+
<TaskActions><ActionMenu …/></TaskActions>
|
|
36
|
+
<TaskSubRow label="Hard copy"><InlineSelect …/></TaskSubRow>
|
|
37
|
+
<TaskSubRow label="Completed on"><InlineDatePicker …/></TaskSubRow>
|
|
38
|
+
</TaskItem>
|
|
39
|
+
</TaskList>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Porting a row, cell by cell:
|
|
43
|
+
|
|
44
|
+
- **Every cell becomes one `TaskSubRow`, and it needs a NAME.** The column header the cells
|
|
45
|
+
never had is now the label — write what the value IS ("Due", "Assignee", "Completed on"), not
|
|
46
|
+
what it does.
|
|
47
|
+
- **Drop the wrapper `View`s and the widths.** A sub-row's label column sizes itself to that
|
|
48
|
+
ONE task's labels and its value takes the slack up to a field's width; nothing is passed in.
|
|
49
|
+
- **Drop `variant="cell"`.** A cell variant is for a grid; a sub-row's value is a form-variant
|
|
50
|
+
`Inline*` editor (the default) — the zinc-50 chip that says "editable".
|
|
51
|
+
- **A row that needed SCANNABLE columns — the same four values compared down twenty rows — was
|
|
52
|
+
never a task list.** Move it to `Table`/`DataGrid`.
|
|
53
|
+
|
|
54
|
+
`TaskCaption` (prose about the row) and `TaskDetail` (a free-form block: a chart, a table, a
|
|
55
|
+
form with its own submit) are unchanged.
|
|
56
|
+
|
|
57
|
+
**`TaskList` gains `actionWidth` — the ⋯ is a COLUMN, the mirror of the control gutter.** Both
|
|
58
|
+
pinned controls sit outside the row's flow and the ROW reserves each edge (`controlWidth` on the
|
|
59
|
+
left, `actionWidth` on the right, each plus the row gap); what is left between them is ONE content
|
|
60
|
+
box every line spans exactly — title, caption, sub-row, detail, nested list. Nothing to pass in
|
|
61
|
+
the normal case: it defaults to 28, an `ActionMenu`'s own width, so every task surface reserves
|
|
62
|
+
the same column for free. **Pass `actionWidth={0}` on a list where NO row carries actions**, so it
|
|
63
|
+
does not pay for a column it never uses — that is the only call site this adds.
|
|
64
|
+
|
|
65
|
+
It is not decoration. With the fields hung under the row, a self-sizing menu in the row's FLOW
|
|
66
|
+
made the first line stop where the menu began while every full-width line beneath it ran on to
|
|
67
|
+
the container — and a row without a menu ended somewhere else again, so no two right edges on the
|
|
68
|
+
surface agreed. `TaskTitle` therefore no longer claims a minimum width (nothing shares its line to
|
|
69
|
+
wrap against), and `TaskDetail` now ends on the content edge instead of overhanging it by its own
|
|
70
|
+
inset.
|
|
71
|
+
|
|
7
72
|
## v18 from 17.x
|
|
8
73
|
|
|
9
74
|
**`TimePicker` is a segmented field, not `<input type="time">`.** A native time input takes
|
package/docs/catalog.md
CHANGED
|
@@ -106,10 +106,13 @@ summary is `Ledger`.
|
|
|
106
106
|
|
|
107
107
|
A task-management PAGE (many tasks, grouping, filters, expandable rows) is COMPOSITION —
|
|
108
108
|
the `Task` compound owns the row — `TaskStatus` (a `CheckCircle`), `TaskTitle` (a struck
|
|
109
|
-
`InlineTextInput`), `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
`InlineTextInput`), `TaskActions`. **The row carries the TITLE; the fields a user can SET hang
|
|
110
|
+
under it as `TaskSubRow`s** (`InlineDatePicker`/`InlineMemberSelect`/`InlineSelect`, each with
|
|
111
|
+
its NAME beside it), never a `DetailTable` in a `TaskDetail`, which declares a grid of its own.
|
|
112
|
+
The SAME composition serves a record's 5–8-task drawer checklist and a grouped desk board,
|
|
113
|
+
because a sub-row reflows instead of being authored per surface; add `SuggestionChip` commons +
|
|
114
|
+
`CaptureRow` for the checklist case. **A list you scan DOWN columns — the same four values
|
|
115
|
+
compared across twenty rows — is a `Table`, not a `TaskList`.** See
|
|
113
116
|
[`tpl_record`](../examples/tpl_record.tsx) / [`tpl_item_list`](../examples/tpl_item_list.tsx),
|
|
114
117
|
and [`tpl_task_board`](../examples/tpl_task_board.tsx) for columns.
|
|
115
118
|
|
|
@@ -487,7 +490,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
487
490
|
they are exclusive at the type level**: WITH it (+ the required `doorLabel`) the whole box is a
|
|
488
491
|
keyboard door into the record's detail and `facts` values are TEXT (`""` → "—"); WITHOUT it the
|
|
489
492
|
box is a STATIC card — no door, no tab stop, no pointer, nothing announced as a button — and a
|
|
490
|
-
`facts` value may be a NODE (an inline editor; a node prints as authored, no "—").
|
|
493
|
+
`facts` value may be a NODE (an inline editor; a node prints as authored, no "—"). A node fact
|
|
494
|
+
gets the CONTROL BAND on both cells — its label centres on the control instead of printing at
|
|
495
|
+
the band's top — while a string fact keeps its bare text line, so a long value that wraps still
|
|
496
|
+
tops out level with its label. Reach for the
|
|
491
497
|
static one when the linked record has NO page of its own: nothing to open, so the box is where
|
|
492
498
|
its values are read and edited, and the only interactive parts are `actions` + the fact nodes.
|
|
493
499
|
Under a door, `actions` is the ONLY place a control may live (a fact editor there would compete
|
|
@@ -635,8 +641,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
635
641
|
|
|
636
642
|
- **`inline_edit`** — `useInlineEdit` + `InlineEditView` / `InlineEditFrame` +
|
|
637
643
|
`useInlineEditFocusRestore` (returns focus to the RESTING control when a KEYBOARD close drops
|
|
638
|
-
it to `<body>` — what
|
|
639
|
-
|
|
644
|
+
it to `<body>` — what a CUSTOM editor that renders its own resting control reaches for instead
|
|
645
|
+
of the frame, which already calls it) + `INLINE_CONTROL_HEIGHT` (40) +
|
|
640
646
|
`inlineValueTextStyle`: the engine custom inline editors
|
|
641
647
|
join through — a view ⇄ edit toggle, a draft buffer, async `onSave` with the spinner
|
|
642
648
|
INSIDE the control and inline error, commit on blur (Enter saves, Escape reverts) or
|
|
@@ -703,7 +709,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
703
709
|
on a flat-text value row (`InlineStatic`, plain `Text`) tucks the annotation up by the
|
|
704
710
|
control band's slack so its gap matches a chip row's.
|
|
705
711
|
`DetailTable`: the compound parent of
|
|
706
|
-
a row STACK — `labelWidth` (default 130
|
|
712
|
+
a row STACK — `labelWidth` (default `DETAIL_LABEL_WIDTH`, 130 — the kit's label column, also
|
|
713
|
+
the floor a `TaskSubRow`'s label sizes from) / `trailingWidth` / `minHeight` (default 40, the
|
|
707
714
|
inline-control grid) declared ONCE + the 8px row gap; with `trailingWidth` every row
|
|
708
715
|
reserves the trailing column so value cells share one width and trailing items align at
|
|
709
716
|
one x, like a table. RESPONSIVE with no prop: it measures its own container (onLayout, not
|
|
@@ -781,28 +788,73 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
781
788
|
(`onChange(done)`) so the ring means one thing everywhere; `partial` is reached through
|
|
782
789
|
whatever NAMES it — a status cell, or children ticking off — never by cycling the ring. Keep
|
|
783
790
|
it monochrome and let colour live in the status cell.
|
|
784
|
-
- **`task`** — `TaskList` + `TaskItem` + `TaskStatus` / `TaskTitle` / `
|
|
785
|
-
`TaskActions` / `TaskDetail` — the task COMPOUND, for anything
|
|
786
|
-
checklist to a grouped desk board. `TaskList` owns geometry only
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
791
|
+
- **`task`** — `TaskList` + `TaskItem` + `TaskStatus` / `TaskTitle` / `TaskCaption` /
|
|
792
|
+
`TaskActions` / `TaskSubRow` / `TaskDetail` — the task COMPOUND, for anything
|
|
793
|
+
from a 5-item drawer checklist to a grouped desk board. `TaskList` owns geometry only: the two
|
|
794
|
+
GUTTERS — `controlWidth` (the left one, that every title aligns on, and so the INDENT step) and
|
|
795
|
+
`actionWidth` (the right one, that every ⋯ pins into; `0` on a list whose rows carry no
|
|
796
|
+
actions) — plus `density` (`comfortable` = a 44px minimum tap target, `dense` = 32 for a
|
|
797
|
+
pointer-driven register). Everything else is composed, and JSX order is screen order — nothing
|
|
798
|
+
inspects child types.
|
|
790
799
|
|
|
791
800
|
`TaskStatus` takes the `CheckCircle` (omit `onChange` for a read-only ring; a PICKER list
|
|
792
|
-
puts a `CheckboxInput` here and sets `controlWidth={24}`). `TaskTitle` takes the
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
row's
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
801
|
+
puts a `CheckboxInput` here and sets `controlWidth={24}`). `TaskTitle` takes the
|
|
802
|
+
`variant="cell"` `InlineTextInput` — or the title TEXT itself as a string child
|
|
803
|
+
(`<TaskTitle struck={done}>{label}</TaskTitle>`), which is the READ-ONLY form: the compound
|
|
804
|
+
applies the cell inset and the row's band, so a plain `<Text>` + a hand-rolled
|
|
805
|
+
`TASK_TEXT_INSET` is never needed and a long title that wraps keeps its first line beside the
|
|
806
|
+
control. It spans the content box between the gutters, like every line hung beneath it.
|
|
807
|
+
`TaskCaption` is the row's state IN WORDS on its own line under the title — a
|
|
808
|
+
sentence, never a field. `TaskActions` carries the row's ⋯ `ActionMenu`, pinned into the RIGHT
|
|
809
|
+
gutter on the first line (Delete lives BEHIND it, danger-styled and last, never a bare ✕).
|
|
810
|
+
`TaskDetail` is a FREE-FORM block under the row on the title's text edge — a chart, a table, a
|
|
811
|
+
form with its own submit — rendered only while open. The module also exports
|
|
812
|
+
**`TASK_TEXT_INSET`** (9), the inset a `variant="cell"` control puts on its own text: every
|
|
813
|
+
slot above already applies it, so reach for it ONLY when a custom title NODE (not a cell
|
|
814
|
+
control) has to land on the same text edge.
|
|
815
|
+
|
|
816
|
+
**THE ⋯ IS A COLUMN, THE MIRROR OF THE CONTROL GUTTER.** Both pinned controls sit outside the
|
|
817
|
+
flow and the ROW reserves each edge (`actionWidth` + the row gap), so what is left between them
|
|
818
|
+
is ONE content box every line spans exactly — title, caption, sub-row, detail, nested list. That
|
|
819
|
+
is what keeps a list where only SOME rows carry a menu straight: a menu-less row's content ends
|
|
820
|
+
on the same x as a menu-carrying one's, and so does everything hanging under either. A nested
|
|
821
|
+
list hands the parent's reservation back before its own rows re-take it, so the column does not
|
|
822
|
+
step inward per level. Declare `actionWidth={0}` when NO row in the list carries actions.
|
|
823
|
+
|
|
824
|
+
**THE ROW CARRIES THE TITLE; THE FIELDS THE USER CAN SET ARE SUB-ROWS BENEATH IT.** One task's
|
|
825
|
+
own fields hang under it as **`TaskSubRow`** (`label` · the control · an optional muted
|
|
826
|
+
`description`), indented ONE step — the same step a nested `TaskList` takes, because belonging
|
|
827
|
+
is expressed by indentation and there is only one device for it. Label and value sit ADJACENT
|
|
828
|
+
so the eye pairs them, and the sub-rows of ONE task share a label column sized to THEIR OWN
|
|
829
|
+
labels: nothing is declared on the list and no width is passed in, so a name is never sized for
|
|
830
|
+
a field three tasks further down. The value takes the slack between a readable minimum and a
|
|
831
|
+
field's maximum — on a phone or in a narrow drawer it drops onto its own line under the label
|
|
832
|
+
rather than ellipsizing beside it. Pass a form-variant `Inline*` editor (the default), not
|
|
833
|
+
`variant="cell"`: a cell variant belongs to a grid.
|
|
834
|
+
|
|
835
|
+
**ANYTHING THAT NEEDS SCANNABLE COLUMNS IS A `Table`, NOT A `TaskList`.** The compound spent a
|
|
836
|
+
version carrying a declared value column so cells would line up down the list; it aligned and
|
|
837
|
+
it read worse — each field's name ended up a quarter of the surface from its control, sized for
|
|
838
|
+
the widest value on the page. If the job is comparing the same four values across twenty rows,
|
|
839
|
+
that is a table, and `Table`/`DataGrid` are built for it. A `DetailTable labelWidth={…}` inside
|
|
840
|
+
a `TaskDetail` is the other shape to avoid: it declares a grid whose label and control land at
|
|
841
|
+
two x positions matching nothing above them — a form pasted into a list.
|
|
842
|
+
|
|
843
|
+
**A row's FIRST LINE is a BAND** — 44px `comfortable`, 32 `dense`. Both gutter controls centre
|
|
844
|
+
in it and `TaskTitle` claims it, which is what keeps both columns of
|
|
845
|
+
controls straight as rows grow: a caption, a sub-row, a detail block, a nested list or a
|
|
846
|
+
wrapped title all hang BELOW the band instead of dragging the control down into the gap under
|
|
847
|
+
the title. So every sub-line sits the band's slack (12px comfortable) beneath the title's
|
|
848
|
+
words, the same whether the title is text or an editor. A title node TALLER than the band
|
|
849
|
+
overrides it — an inline editor is 40px, so a `dense` list of editable titles renders 40px rows
|
|
850
|
+
and its ring reads a few px high; `dense` is for text/static rows. A sub-row takes the TALLER
|
|
851
|
+
of the band and the 40px inline-control height, so its label's first line still meets its
|
|
852
|
+
control's centre in a `dense` list.
|
|
853
|
+
|
|
854
|
+
**Subtasks are a nested `TaskList`**, so a step IS a task: give one a field, a menu or
|
|
803
855
|
children of its own and it works. Collapsing belongs to the app — hold a boolean and render
|
|
804
856
|
the nested list or don't. There is deliberately **no note slot**: a task's free text is its
|
|
805
|
-
title, a
|
|
857
|
+
title, a caption, or detail, and a fourth place to write invited writing it twice.
|
|
806
858
|
|
|
807
859
|
- **`suggestion_chip`** — `SuggestionChip`: the dismissible SUGGESTION pill — an item the
|
|
808
860
|
record could have but doesn't yet (a common task, an expected line) as a `Chip` whose
|
package/docs/data_entry.md
CHANGED
|
@@ -258,22 +258,58 @@ changing desks — never an inbox, a notification, or a copied task. Two types:
|
|
|
258
258
|
handoff is MANAGED AS TASKS — each desk's checklist on the record. The **`Task`** compound owns
|
|
259
259
|
the list geometry (the control column, the indent, and `density` — 44px targets by default) —
|
|
260
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
|
|
262
|
-
or
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
read-only ring when it has no `onChange`), a `TaskTitle` (a `variant="cell"` `InlineTextInput`,
|
|
262
|
+
or the title TEXT as a string child — `<TaskTitle struck={done}>{label}</TaskTitle>` is the
|
|
263
|
+
read-only form, and the compound owns its inset and band, so never hand-roll a `Text` title) and
|
|
264
|
+
a `TaskActions` ⋯ menu — never a decorative progress strip. The fields the desk SETS (the
|
|
265
|
+
assignee `InlineMemberSelect`, the due date) hang UNDER the row as `TaskSubRow`s, which reflow on
|
|
266
|
+
their own, so the drawer and the record page run the SAME composition. `CaptureRow` closes the
|
|
267
|
+
list as its add-affordance.
|
|
268
|
+
|
|
269
|
+
**A row's first line is a BAND** (44px comfortable, 32 dense): both gutter controls centre in it,
|
|
270
|
+
`TaskTitle` claims it, and everything else — caption, detail, nested list, a title's wrapped
|
|
271
|
+
second line — hangs below it. That is what keeps a column of controls straight down a list of
|
|
272
|
+
mixed-height rows, so a control never drifts into the gap under the title it belongs to.
|
|
266
273
|
|
|
267
274
|
A task's own free text is its TITLE, a **`TaskCaption`** or a **`TaskDetail`** — there is no
|
|
268
275
|
fourth place to write, because a fourth place gets written in twice. `TaskCaption` is the row's
|
|
269
276
|
state IN WORDS ("3 of 5 papers received", "waiting on the yard") on its own line under the title;
|
|
270
|
-
it is not a
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
it is not a field, because a field is a NAMED VALUE the reader sets and a caption is a SENTENCE
|
|
278
|
+
about one row — labelling prose makes it read as a field nobody can edit.
|
|
279
|
+
|
|
280
|
+
**THE ROW CARRIES THE TITLE; THE FIELDS THE USER CAN SET ARE SUB-ROWS BENEATH IT.** A field
|
|
281
|
+
belonging to ONE task — its due date, its assignee, a portal login, a reference number — hangs
|
|
282
|
+
under it as a **`TaskSubRow`** (`label` · the control · an optional muted `description`), indented
|
|
283
|
+
ONE step, with the NAME beside its VALUE so the eye pairs them. Belonging is expressed by that
|
|
284
|
+
indentation, which is the same step a nested `TaskList` takes — one device, at every depth. The
|
|
285
|
+
sub-rows of ONE task share a label column sized to THEIR OWN labels: nothing is declared on the
|
|
286
|
+
list, no width is passed in, and no field is sized for a name three tasks further down. The value
|
|
287
|
+
takes the slack between a readable minimum and a field's maximum, dropping onto its own line under
|
|
288
|
+
the label on a surface too narrow to seat both. Use the form-variant `Inline*` editor (the
|
|
289
|
+
default) — `variant="cell"` belongs to a grid.
|
|
290
|
+
|
|
291
|
+
**Anything that needs SCANNABLE COLUMNS is a `Table`, not a `TaskList`.** The compound once
|
|
292
|
+
carried a declared value column so cells lined up down the list; it aligned, and it read worse —
|
|
293
|
+
each name ended up a quarter of the surface from its control, sized for the widest value on the
|
|
294
|
+
page. Comparing the same four values across twenty rows is a table's job. A `DetailTable` inside a
|
|
295
|
+
`TaskDetail` is the same mistake in miniature: a grid whose label and control land at two x
|
|
296
|
+
positions matching nothing above them, a form pasted into a list. `TaskDetail` keeps the FREE-FORM
|
|
297
|
+
block (a chart, a table, a form with its own submit) — it has no name to hang on the indent, so a
|
|
298
|
+
rule down its left edge is what ties it to the row.
|
|
299
|
+
|
|
300
|
+
**The ⋯ is a COLUMN — the mirror of the control gutter.** Both pinned controls sit outside the
|
|
301
|
+
row's flow and the ROW reserves each edge (`controlWidth` on the left, `actionWidth` on the
|
|
302
|
+
right, each plus the row gap), leaving ONE content box between them that every line spans exactly:
|
|
303
|
+
the title, a caption, a sub-row, a detail block, a nested list. That is what keeps a list where
|
|
304
|
+
only SOME rows carry a menu straight — a menu-less row's content ends on the same x as a
|
|
305
|
+
menu-carrying one's, and so does everything hanging beneath either. A nested list hands the
|
|
306
|
+
parent's reservation back before its own rows re-take it, so the column does not step inward per
|
|
307
|
+
level. Declare `actionWidth={0}` when NO row in the list carries actions, so it pays nothing for a
|
|
308
|
+
column it never uses.
|
|
309
|
+
|
|
310
|
+
**Subtasks are TASKS** — nest a `TaskList` inside the `TaskItem`, and the child carries fields,
|
|
311
|
+
a menu and children of its own. There is no separate subtask shape to outgrow, and no built-in
|
|
312
|
+
chevron: collapsing is the app's call (hold a boolean, render the nested list or don't).
|
|
277
313
|
|
|
278
314
|
**State flows from the LEAVES.** Where a row's position is computable from what it owns, DERIVE
|
|
279
315
|
it — a stored status is a second copy of what the fields already say, and two copies drift. Give
|
|
@@ -281,13 +317,11 @@ the parent's ring the bulk gesture where its children ARE the fact (tick it, eve
|
|
|
281
317
|
where the ring answers a DIFFERENT field the children do not determine — a date, say — coupling
|
|
282
318
|
them asserts something the operator never said.
|
|
283
319
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
under two
|
|
287
|
-
|
|
288
|
-
|
|
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.
|
|
320
|
+
**A sub-row holds a CONTROL.** Where exactly one field determines a row's status, the sub-row IS
|
|
321
|
+
that field's editor: a read-only copy of it elsewhere on the row puts one value on screen twice,
|
|
322
|
+
under two names. Everything else derived is prose in `TaskCaption` — a read-only pill given a
|
|
323
|
+
field's label reads as pressable when it is not. A `Badge` INSIDE the editor (an
|
|
324
|
+
`InlineSelect renderSelected`) is fine: it inherits the control's own inset.
|
|
291
325
|
|
|
292
326
|
**A status reports what is OUTSTANDING, never what is settled.** A finished row already says so
|
|
293
327
|
twice — a full ring and its date — so naming the settled condition is a third copy of one fact.
|
|
@@ -319,8 +353,8 @@ the record left this register.
|
|
|
319
353
|
|
|
320
354
|
Tasks PEEK from the register: the done/total column is a pressable compact-`ProgressBar` trigger
|
|
321
355
|
whose popover holds the same `Task` list — every `TaskItem`'s ring `TaskStatus` · struck
|
|
322
|
-
`InlineTextInput` `TaskTitle` (flex) · quick-reassign `InlineMemberSelect` in `
|
|
323
|
-
|
|
356
|
+
`InlineTextInput` `TaskTitle` (flex) · a quick-reassign `InlineMemberSelect` in a `TaskSubRow`
|
|
357
|
+
under it — no expandable rows (tags/files depth is
|
|
324
358
|
the Task list template's lesson, not the peek's); the popover body is `PopoverContent`'s own
|
|
325
359
|
ScrollView (`disableBodyScroll` is ONLY for children that manage their own scroll, like
|
|
326
360
|
`OptionList`). The DRAWER carries a real Tasks SECTION in the Record template's shape (heading +
|
package/docs/templates.md
CHANGED
|
@@ -286,18 +286,17 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
286
286
|
- **Tasks** — the full task-list grammar: clearable Group-by `FilterChip` (+ assignee/status
|
|
287
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
|
|
290
|
-
the SAME `InlineDatePicker` / `InlineMemberSelect` used in the General section, in
|
|
291
|
-
|
|
292
|
-
= `dueTone` — red past due, amber ≤3d, else muted), the assignee
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
child rows — anything needing its own note/due/assignee would be a task instead.
|
|
289
|
+
text heads (no dot badges). Each row's **due + assignee are `TaskSubRow`s UNDER the title** —
|
|
290
|
+
the SAME `InlineDatePicker` / `InlineMemberSelect` used in the General section, in the default
|
|
291
|
+
form variant, each with its NAME beside it one indent step in: the due an urgency-coloured date
|
|
292
|
+
(`tone` = `dueTone` — red past due, amber ≤3d, else muted), the assignee the full member chip.
|
|
293
|
+
A task row is not a table row — a value column reserved on the list put each name a quarter of
|
|
294
|
+
the record from its own control. ONE row ("Book the carrier") carries **SUBTASKS** — a nested
|
|
295
|
+
`TaskList` of four child ticks, each a `TaskItem` with a STRING `TaskTitle`; every other row
|
|
296
|
+
holds an empty array and renders no list at all. There is no built-in expander: collapsing is
|
|
297
|
+
the app's call, and this template always shows them. It is the line to copy — a booking's steps
|
|
298
|
+
carry only a tick, so they stay child rows; anything needing its own due date or assignee is a
|
|
299
|
+
task instead.
|
|
301
300
|
- **Documents** — the Agents "Document desk" pattern (this template is its worked example —
|
|
302
301
|
see the Agents chapter below): the register `Table` (search · Add files) whose selection
|
|
303
302
|
feeds the `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI).
|
|
@@ -354,12 +353,13 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
354
353
|
arrived. **READINESS**: a form declares the RECORD FIELDS it reads (`needs`); the fields'
|
|
355
354
|
HOME stays their DATA section (the colocation law) with descriptions naming their
|
|
356
355
|
consumers ("printed on the delivery note"). The picker rows ride `TaskList`/`TaskItem`
|
|
357
|
-
(`controlWidth={24}` for the `CheckboxInput` in `TaskStatus
|
|
358
|
-
|
|
359
|
-
|
|
356
|
+
(`controlWidth={24}` for the `CheckboxInput` in `TaskStatus`, and `actionWidth={0}` because no
|
|
357
|
+
row here carries a ⋯, so the list pays nothing for the action gutter) — the compound owns the
|
|
358
|
+
row geometry and both gutters; hand-rolling the anatomy is how alignment drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing — a
|
|
359
|
+
**`TaskCaption`**, not a field: 13px `circle-alert` + "Needs: …" the actual field labels,
|
|
360
360
|
muted at rest so gaps show WITHOUT checking, warning once checked because it now blocks. It is
|
|
361
|
-
a SENTENCE about the row, and `
|
|
362
|
-
|
|
361
|
+
a SENTENCE about the row, and a `TaskSubRow` is a NAMED value the reader sets, so prose given a
|
|
362
|
+
label reads as a field nobody can edit. The FIX sits
|
|
363
363
|
right under it in the row's `TaskDetail` — an `Inset` (a FORM surface, NEVER a `Callout`:
|
|
364
364
|
a warning Callout is an ARIA `alert`, wrong around a form) with a `FormTextInput` per gap
|
|
365
365
|
(label + the consumer hint as `description`) and a "Save fields" (secondary — the section
|
|
@@ -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 { TaskActions,
|
|
24
|
+
import { TaskActions, TaskItem, TaskList, TaskStatus, TaskSubRow, 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";
|
|
@@ -204,9 +204,10 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
|
|
|
204
204
|
const pending = suggestions.filter((l) => !tasks.some((t) => t.label === l));
|
|
205
205
|
return (
|
|
206
206
|
<TaskList>
|
|
207
|
-
{/* ONE anatomy for every surface. The
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
{/* ONE anatomy for every surface. The row is the title; the assignee and
|
|
208
|
+
the due date hang under it as named fields, and the pair splits onto
|
|
209
|
+
two lines only when the surface is too narrow to seat both — a drawer,
|
|
210
|
+
a peek, a phone. The template no longer picks. */}
|
|
210
211
|
{tasks.map((t) => (
|
|
211
212
|
<TaskItem key={t.id}>
|
|
212
213
|
<TaskStatus>
|
|
@@ -221,33 +222,32 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
|
|
|
221
222
|
accessibilityLabel="Task title"
|
|
222
223
|
/>
|
|
223
224
|
</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
225
|
<TaskActions>
|
|
246
226
|
<ActionMenu
|
|
247
227
|
items={[{ key: "xoa", label: "Delete task", icon: "trash", danger: true, onPress: () => onChange(tasks.filter((x) => x.id !== t.id)) }]}
|
|
248
228
|
accessibilityLabel={`Task options: ${t.label}`}
|
|
249
229
|
/>
|
|
250
230
|
</TaskActions>
|
|
231
|
+
{/* the task's own FIELDS hang UNDER it, name beside value, one indent
|
|
232
|
+
step in — never cells in a shared column, which put each name a
|
|
233
|
+
quarter of the surface from the control it names */}
|
|
234
|
+
<TaskSubRow label="Assignee">
|
|
235
|
+
<InlineMemberSelect
|
|
236
|
+
members={MEMBERS}
|
|
237
|
+
value={t.assignee}
|
|
238
|
+
onSave={(m) => patch(t.id, { assignee: m })}
|
|
239
|
+
placeholder="Assign…"
|
|
240
|
+
accessibilityLabel={`Assignee · ${t.label}`}
|
|
241
|
+
/>
|
|
242
|
+
</TaskSubRow>
|
|
243
|
+
<TaskSubRow label="Due">
|
|
244
|
+
<InlineDatePicker
|
|
245
|
+
value={t.due}
|
|
246
|
+
onSave={(v) => patch(t.id, { due: v })}
|
|
247
|
+
placeholder="Set due…"
|
|
248
|
+
accessibilityLabel={`Due · ${t.label}`}
|
|
249
|
+
/>
|
|
250
|
+
</TaskSubRow>
|
|
251
251
|
</TaskItem>
|
|
252
252
|
))}
|
|
253
253
|
{/* SUGGESTIONS are pills, not rows — a chip can't be mistaken for a
|
|
@@ -652,7 +652,7 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
|
|
|
652
652
|
</Section>
|
|
653
653
|
<Divider />
|
|
654
654
|
{/* TASKS — the Record template's section shape: heading + the compact
|
|
655
|
-
meter, the shared
|
|
655
|
+
meter, the shared checklist below (capture at the end);
|
|
656
656
|
the same state feeds the register column + its peek. */}
|
|
657
657
|
<Section>
|
|
658
658
|
<SectionHeading>
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -32,7 +32,7 @@ import { CheckCircle } from "@lotics/ui/check_circle";
|
|
|
32
32
|
import { ProgressBar } from "@lotics/ui/progress_bar";
|
|
33
33
|
import { FilterChip } from "@lotics/ui/filter_chip";
|
|
34
34
|
import { CaptureRow } from "@lotics/ui/capture_row";
|
|
35
|
-
import { TaskActions, TaskCaption, TaskDetail,
|
|
35
|
+
import { TaskActions, TaskCaption, TaskDetail, TaskItem, TaskList, TaskStatus, TaskSubRow, TaskTitle } from "@lotics/ui/task";
|
|
36
36
|
import { ActionMenu, type ActionMenuItem } from "@lotics/ui/action_menu";
|
|
37
37
|
import { SuggestionChip } from "@lotics/ui/suggestion_chip";
|
|
38
38
|
import { OptionList } from "@lotics/ui/option_list";
|
|
@@ -162,8 +162,8 @@ interface StageTask {
|
|
|
162
162
|
stage: Desk;
|
|
163
163
|
done: boolean;
|
|
164
164
|
assignee: string | null;
|
|
165
|
-
/** ISO date (`""` = no due).
|
|
166
|
-
*
|
|
165
|
+
/** ISO date (`""` = no due). It hangs under the row as a named `TaskSubRow`
|
|
166
|
+
* field, coloured by urgency (`tone`), press to edit. */
|
|
167
167
|
due: string;
|
|
168
168
|
/** The task's CHILD STEPS (`[]` = none). They render as a NESTED `TaskList`,
|
|
169
169
|
* so a step is a task: give one a due date, a menu or children of its own and
|
|
@@ -209,12 +209,10 @@ const newTaskId = () => `t_${(taskSeq += 1)}`;
|
|
|
209
209
|
const TASK_SEEDS: StageTask[] = [
|
|
210
210
|
{ id: "t1", label: "Confirm pricing with the customer", stage: "sales", done: true, assignee: "mem_01", due: dueIn(-10), subtasks: [] },
|
|
211
211
|
{ id: "t2", label: "Attach the signed quote", stage: "sales", done: true, assignee: "mem_01", due: "", subtasks: [] },
|
|
212
|
-
// two seeded NOTES — the "why it's stuck" a task carries and nothing else
|
|
213
|
-
// holds; every other row shows the add affordance instead
|
|
214
212
|
{ id: "t3", label: "Verify the customer's tax ID", stage: "sales", done: false, assignee: null, due: dueIn(-2), subtasks: [] },
|
|
215
213
|
// the one task with SUBTASKS — a booking is a sequence, and its steps carry
|
|
216
214
|
// nothing but a tick, so they stay child rows instead of four more tasks
|
|
217
|
-
// (
|
|
215
|
+
// (collapsing is the app's call — this template always renders them)
|
|
218
216
|
{ id: "t4", label: "Book the carrier", stage: "operations", done: false, assignee: "mem_02", due: dueIn(1), subtasks: [
|
|
219
217
|
{ key: "s1", label: "Confirm the pickup window", done: true },
|
|
220
218
|
{ key: "s2", label: "Send the packing list", done: false },
|
|
@@ -1851,7 +1849,7 @@ export function TplRecord() {
|
|
|
1851
1849
|
) : null}
|
|
1852
1850
|
</SubsectionHeading>
|
|
1853
1851
|
) : null}
|
|
1854
|
-
{/* rows ride the
|
|
1852
|
+
{/* rows ride the Task compound — one geometry, EVERY
|
|
1855
1853
|
desk's rows fully editable (the stage gates the handoff
|
|
1856
1854
|
and Billing, never task editing — planning ahead on a
|
|
1857
1855
|
later desk is normal work) */}
|
|
@@ -1879,19 +1877,22 @@ export function TplRecord() {
|
|
|
1879
1877
|
<TaskTitle>
|
|
1880
1878
|
<InlineTextInput variant="cell" value={t.label} onSave={(v) => renameTask(t.id, v)} struck={t.done} accessibilityLabel="Task title" />
|
|
1881
1879
|
</TaskTitle>
|
|
1882
|
-
{/* Due + assignee as COMPACT grid cells (`variant="cell"`): the due an
|
|
1883
|
-
urgency-coloured date (no calendar glyph), the assignee a bare AVATAR
|
|
1884
|
-
(`avatarOnly`). They sit inline on a wide record and take their own
|
|
1885
|
-
line in a drawer — the SAME JSX, no surface guess. */}
|
|
1886
|
-
<TaskFields>
|
|
1887
|
-
<View style={{ width: 104 }}>
|
|
1888
|
-
<InlineDatePicker variant="cell" tone={dueTone(t.due, t.done)} value={t.due || null} onSave={(v) => dueTask(t.id, v)} onClear={() => dueTask(t.id, "")} placeholder="Due…" locale="en-US" accessibilityLabel={`Due · ${t.label}`} />
|
|
1889
|
-
</View>
|
|
1890
|
-
<InlineMemberSelect variant="cell" avatarOnly members={TEAM} value={t.assignee} onSave={(m) => assignTask(t.id, m)} accessibilityLabel={`Assignee · ${t.label}`} />
|
|
1891
|
-
</TaskFields>
|
|
1892
1880
|
<TaskActions>
|
|
1893
1881
|
<ActionMenu items={menuItems} accessibilityLabel={`Task options: ${t.label}`} />
|
|
1894
1882
|
</TaskActions>
|
|
1883
|
+
{/* Due + assignee are the task's own FIELDS, so they hang UNDER it —
|
|
1884
|
+
each name beside the control it names, indented one step. They were
|
|
1885
|
+
cells in a value column the LIST declared, which is what put a
|
|
1886
|
+
two-word label a quarter of the record away from its own editor.
|
|
1887
|
+
The due keeps its urgency tone; the assignee names the member
|
|
1888
|
+
rather than showing a bare avatar, because a labelled field has the
|
|
1889
|
+
room for it. */}
|
|
1890
|
+
<TaskSubRow label="Due">
|
|
1891
|
+
<InlineDatePicker tone={dueTone(t.due, t.done)} value={t.due || null} onSave={(v) => dueTask(t.id, v)} onClear={() => dueTask(t.id, "")} placeholder="Not set" locale="en-US" accessibilityLabel={`Due · ${t.label}`} />
|
|
1892
|
+
</TaskSubRow>
|
|
1893
|
+
<TaskSubRow label="Assignee">
|
|
1894
|
+
<InlineMemberSelect members={TEAM} value={t.assignee} onSave={(m) => assignTask(t.id, m)} placeholder="Unassigned" accessibilityLabel={`Assignee · ${t.label}`} />
|
|
1895
|
+
</TaskSubRow>
|
|
1895
1896
|
{t.subtasks.length > 0 ? (
|
|
1896
1897
|
<TaskList>
|
|
1897
1898
|
{t.subtasks.map((sub) => (
|
|
@@ -1899,12 +1900,11 @@ export function TplRecord() {
|
|
|
1899
1900
|
<TaskStatus>
|
|
1900
1901
|
<CheckCircle state={sub.done ? "done" : "none"} onChange={(on) => toggleSubtask(t.id, sub.key, on)} accessibilityLabel={sub.label} />
|
|
1901
1902
|
</TaskStatus>
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
</TaskTitle>
|
|
1903
|
+
{/* A STRING title: the compound insets it like the parent's
|
|
1904
|
+
inline editor and seats it in the row's band, so the two
|
|
1905
|
+
columns of titles line up and the ring stays beside the
|
|
1906
|
+
words even when a long subtask wraps. `struck` for done. */}
|
|
1907
|
+
<TaskTitle struck={sub.done}>{sub.label}</TaskTitle>
|
|
1908
1908
|
</TaskItem>
|
|
1909
1909
|
))}
|
|
1910
1910
|
</TaskList>
|
|
@@ -2243,11 +2243,14 @@ export function TplRecord() {
|
|
|
2243
2243
|
<SubsectionHeading>
|
|
2244
2244
|
<SubsectionHeadingTitle>{g.party(customer?.name ?? null)}</SubsectionHeadingTitle>
|
|
2245
2245
|
</SubsectionHeading>
|
|
2246
|
-
{/* the PICKER rows ride the
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
alignment drifts. controlWidth 24 =
|
|
2250
|
-
|
|
2246
|
+
{/* the PICKER rows ride the Task compound — IT owns the
|
|
2247
|
+
geometry (the row band, the two gutters, the indent
|
|
2248
|
+
everything hung under a row takes); hand-rolling this
|
|
2249
|
+
anatomy is how alignment drifts. controlWidth 24 =
|
|
2250
|
+
CheckboxInput; and no row here carries a ⋯, so the list
|
|
2251
|
+
declines the action gutter and its captions run to the
|
|
2252
|
+
surface's edge. */}
|
|
2253
|
+
<TaskList controlWidth={24} actionWidth={0}>
|
|
2251
2254
|
{shown.map((f) => {
|
|
2252
2255
|
// READINESS per row, CO-LOCATED: a not-ready row's mark NAMES what's
|
|
2253
2256
|
// missing (muted at rest — discoverable without checking; warning once
|
|
@@ -2263,17 +2266,14 @@ export function TplRecord() {
|
|
|
2263
2266
|
<TaskStatus>
|
|
2264
2267
|
<CheckboxInput accessibilityLabel={f.label} checked={checked} onChange={(on) => toggleForm(f.id, on)} />
|
|
2265
2268
|
</TaskStatus>
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
<Text size="sm" style={{ flexShrink: 1, paddingHorizontal: TASK_TEXT_INSET }}>{f.label}</Text>
|
|
2271
|
-
</TaskTitle>
|
|
2269
|
+
{/* A STRING title carries the cell inset and the row's band from the
|
|
2270
|
+
compound, so it sits in line with the editable titles elsewhere on
|
|
2271
|
+
the page and the checkbox stays beside it above its caption. */}
|
|
2272
|
+
<TaskTitle>{f.label}</TaskTitle>
|
|
2272
2273
|
{/* "Needs: Tax ID, Delivery address" is a SENTENCE about this row, so it
|
|
2273
|
-
is a `TaskCaption` — its own line under the title.
|
|
2274
|
-
`
|
|
2275
|
-
|
|
2276
|
-
it, and reads as a control it is not. */}
|
|
2274
|
+
is a `TaskCaption` — its own line under the title. Not a
|
|
2275
|
+
`TaskSubRow`, which is a NAMED value the reader SETS: prose given a
|
|
2276
|
+
label reads as a field nobody can edit. */}
|
|
2277
2277
|
{missing.length > 0 ? (
|
|
2278
2278
|
<TaskCaption>
|
|
2279
2279
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 5 }}>
|