@lotics/ui 12.1.2 → 13.7.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/docs/templates.md CHANGED
@@ -41,8 +41,7 @@ the package index is [../AGENTS.md](../AGENTS.md).
41
41
  | A register / consolidated work-list (browse, per-row action, bulk action) | `tpl_item_list` |
42
42
  | A guided sequence of physical tasks (scan, confirm, next) | `tpl_pick` |
43
43
  | Splitting one source amount across many targets | `tpl_allocate` |
44
- | A record's create/edit surface — also the settings shape | `tpl_record` |
45
- | A personal to-do list | `tpl_tasks` |
44
+ | A record's create/edit surface — also the settings shape (its Tasks section IS the canonical task pattern) | `tpl_record` |
46
45
  | A team task board (inline-managed grouped table) | `tpl_task_board` |
47
46
  | Financial statements | `tpl_statements` |
48
47
  | A scoped lookup report with export | `tpl_report` |
@@ -65,34 +64,41 @@ Laws for any screen where a record's data is edited and a primary action produce
65
64
  2. **Static shape.** The page never changes shape from output-selection state. Conditional
66
65
  rendering is DATA-driven only (a control appears because two stored values differ), never
67
66
  selection-driven. Relevance is an advisory `Callout` with a jump-to-section action — warn,
68
- never hide, never hard-block. One sanctioned disclosure: a TRANSIENT fill-what's-missing
69
- editor expanding under a just-checked item is progressive disclosure of the gate, not
70
- shape change — it edits the record field in place and collapses once satisfied.
67
+ never hide, never hard-block. One sanctioned disclosure: a fill-what's-missing `Inset`
68
+ editor CO-LOCATED under a just-checked item is progressive disclosure of the gate, not
69
+ shape change — it edits the record field in place and clears once satisfied.
71
70
  3. **Colocation & ownership.** Every output option (selections, print/export switches) lives in
72
71
  the output section beside its action; a per-entity selection carries its owner's name in its
73
72
  heading. Persisted record DATA never lives there — data belongs in data sections. Misfiled
74
73
  data announces itself as callout special-cases; the exception disappearing is the sign the
75
74
  model is right. (HOMING is what's forbidden, not repair: the output section may host a
76
- transient fill editor that saves onto the record field — same storage, the field's home row
77
- shows the value — rendered only while the field blocks a checked output.)
75
+ co-located fill editor that saves onto the record field — same storage, the field's home
76
+ row shows the value — rendered only while a checked output still lacks the field.)
78
77
  4. **Resolved state.** Controls show the EFFECTIVE state, never raw storage: a derived/suggested
79
78
  selection renders checked; the first manual change persists the full explicit set; a
80
79
  reset-to-auto affordance appears only in explicit mode. What's shown = what happens.
81
80
  5. **Decision budget.** Every user decision earns its place — derive it, default it, or
82
81
  conditionally render it; ask only the underivable. Suggestions are one-tap-save chips; a
83
82
  placeholder states the honest effective fallback (what actually happens when empty), never
84
- an example that lies.
85
- 6. **Header = identity + provenance.** Id, the key identifier, created-at. No metrics or badges
83
+ an example that lies. A status/stage is the canonical DERIVED value — computed from its
84
+ inputs (dates, rollups, amounts), never a `select` the user maintains: if a human must set
85
+ it by hand, it isn't a status.
86
+ 6. **Right section per data kind.** Files live in the ONE Documents desk near the top — never a
87
+ `files`/`FilesEditor` field scattered into a stage or detail section (a record has a single
88
+ Documents desk; → the **Documents** section below). A history / payment / event schedule is
89
+ a plain DATED LIST (Text rows or a simple `Table`), never a `Timeline` — `Timeline` is a
90
+ genuine activity FEED (who changed what, when), not a schedule of payments.
91
+ 7. **Header = identity + provenance.** Id, the key identifier, created-at. No metrics or badges
86
92
  duplicating data a section already owns.
87
- 7. **Destructive placement.** Solid `danger`, bottom-left after the entity's fields, ONE
93
+ 8. **Destructive placement.** Solid `danger`, bottom-left after the entity's fields, ONE
88
94
  convention page-wide, confirmed by `Alert`. Never in a heading row, never one-tap.
89
- 8. **Create-then-refine.** An add is one click creating a draft edited in place — drafts render
95
+ 9. **Create-then-refine.** An add is one click creating a draft edited in place — drafts render
90
96
  as empty inline editors with placeholders, never as fake data. No type-a-name-then-click
91
97
  forms.
92
- 9. **Optimistic feedback.** Every edit echoes locally, saves in the background, reconciles on
93
- refetch, reverts + surfaces on error. Silent success, loud failure — no persistent "saved"
94
- chrome.
95
- 10. **Chrome discipline.** Kit spacing scale only; ONE divider rule (between sibling blocks,
98
+ 10. **Optimistic feedback.** Every edit echoes locally, saves in the background, reconciles on
99
+ refetch, reverts + surfaces on error. Silent success, loud failure — no persistent "saved"
100
+ chrome.
101
+ 11. **Chrome discipline.** Kit spacing scale only; ONE divider rule (between sibling blocks,
96
102
  never doubled with section hairlines, none trailing); no state-echo labels or filler
97
103
  captions — explanations live in `info` popovers and empty states (→ [composition grammar
98
104
  §Microcopy](./composition.md)). A "why" question about a control is answered by tracing
@@ -167,15 +173,18 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
167
173
 
168
174
  - **One toolbar row** — search + a status `Select` + facet `FilterChip`s LEFT, the New CTA
169
175
  RIGHT; a light `SummaryLine` of the filtered view below.
170
- - **A sortable, `Divider`-separated `Table`** where every row carries a leading checkbox; a
171
- row that can't take the bulk action gets a **disabled** checkbox (the same gating as any
172
- blocked line). A per-row action `Button` (here Print) is the row's primary action in the
173
- trailing column, `⋯` its overflow. A select-all band, footer totals + `Pagination`, and a
176
+ - **A sortable, `Divider`-separated `Table`** where the record's KEY/identifier is the FIRST
177
+ column and every row carries a leading checkbox; a row that can't take the bulk action gets a
178
+ **disabled** checkbox (the same gating as any blocked line). A row's status is
179
+ `Badge variant="dot"` never the tonal `Badge` (that's the drawer/header twin; [composition
180
+ grammar §"Status indicators have a WEIGHT"](./composition.md)) — and the SAME dot vocabulary
181
+ drives the status facet. A per-row action `Button` (here Print) is the row's primary action in
182
+ the trailing column, `⋯` its overflow. A select-all band, footer totals + `Pagination`, and a
174
183
  `FloatingActionBar` carrying the bulk action while rows are ticked.
175
184
  - **A row press opens the PRODUCTION workspace `Drawer`**, which demonstrates the full
176
185
  record-workspace anatomy section by section:
177
186
  - **Details `DetailTable`** showing every inline field type — text, `InlineMemberSelect`,
178
- dates incl. optional time, dot-select, money number, the `InlineTagSelect` tag field,
187
+ dates incl. optional time, dot-select, money number, an `InlineSelect multi` tag field,
179
188
  and `InlineStatic` + a System badge for read-only values. Detail rows earn trailing CTAs
180
189
  where deserved (Phone → Call, Due → Today) — the case where `trailingWidth` IS set.
181
190
  - **Linked records** — the customer's other records as `ListItem`s (title + description at
@@ -236,8 +245,8 @@ billing, and quick-capture templates. Top → bottom:
236
245
  and `error` (derived, e.g. overdue Due) — always explicit, never an ⓘ; the inline editors
237
246
  demo their own transient save error (a throwing `onSave`). The Classification group works
238
247
  **the right-input-per-field law**: a field gets the control its SHAPE wants, never a
239
- default text box — ≤5 exclusive options the user should see → `RadioPicker` (column when
240
- options carry descriptions, row when short), a longer closed list → `InlineSelect` (rich
248
+ default text box — ≤5 exclusive options the user should see → `RadioPicker` (a stacked
249
+ column of full-width choices), a longer closed list → `InlineSelect` (rich
241
250
  options carry a description line, shown in the resting row too), a pick from a big
242
251
  REGISTRY → `Combobox` (search is the input), a boolean → `CheckboxInput`; a DEPENDENT
243
252
  field renders only while its parent value makes it real (no disabled ghost rows).
@@ -248,9 +257,15 @@ billing, and quick-capture templates. Top → bottom:
248
257
  file-capable edit form injected via `renderEditForm`) + THE kit `Composer` (attach via
249
258
  `actionsButton`, staged files in its `files` slot) — never a bespoke comment box.
250
259
  - **Tasks** — the full task-list grammar: clearable Group-by `FilterChip` (+ assignee/status
251
- chips), the `CaptureRow` on top, `Checklist`/`ChecklistRow` rows with `InlineMemberSelect`
252
- assignees and per-row menus, suggestions as `SuggestionChip`s; groups divide via the
253
- `SubsectionStack` beat with plain text heads (no dot badges).
260
+ chips), the `CaptureRow` on top, `Checklist`/`ChecklistRow` rows and per-row menus,
261
+ suggestions as `SuggestionChip`s; groups divide via the `SubsectionStack` beat with plain
262
+ text heads (no dot badges). Each row's trailing carries **due + assignee as COMPACT grid CELLS** —
263
+ the SAME `InlineDatePicker` / `InlineMemberSelect` used in the General section, in **`variant="cell"`**
264
+ (transparent rest + wash on hover, no glyph/chevron): the due a urgency-coloured date (`tone`
265
+ = `dueTone` — red past due, amber ≤3d, else muted), the assignee an **`avatarOnly`** bare avatar
266
+ (dashed add-ghost when unset), sized to the list's `trailingWidth` so the title never overlaps.
267
+ (Contrast the General section's dates/assignee, the same editors in the default `variant="form"`
268
+ — a zinc-50 chip + hover-border + the full member chip.)
254
269
  - **Documents** — the Agents "Document desk" pattern (shared with `tpl_documents` — change
255
270
  it in BOTH): the register `Table` (search · Add files) whose selection feeds the
256
271
  `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI). Every
@@ -260,13 +275,13 @@ billing, and quick-capture templates. Top → bottom:
260
275
  sanctioned divergence: GENERATION — on the record page this desk holds what ARRIVES and
261
276
  Create moves to the Document set OUTPUT section below; `tpl_documents` (no output
262
277
  pipeline on its page) keeps the toolbar "Create documents" dialog.
263
- - **Customer — the LINKED-RECORD reference implementation** (inline, no wrapper component):
264
- another record's data NEVER renders as field rows on this one — a BORDERED, fully
265
- pressable box (glyph anchor · identity · facts stacked VERTICALLY, legible at any width)
266
- whose press opens the detail drawer (read-only rows + the navigate verb); the action row
267
- sits INSIDE the box destructive (Remove, `danger`) LEFT, go-to (Open record) RIGHT.
268
- Built on `PressableRow` + the keyboard door, never `role="button"` on the box — the
269
- press anatomy is [composition.md's linked-record law](./composition.md).
278
+ - **Customer — the LINKED-RECORD reference** via **`LinkedRecordBox`**: another record's data
279
+ NEVER renders as field rows on this one — a BORDERED, fully pressable box (glyph anchor ·
280
+ identity · facts stacked VERTICALLY) whose press opens the detail drawer; the verbs ride the
281
+ `actions` slot INSIDE the box destructive (Remove, `danger`) LEFT, go-to (Open record)
282
+ RIGHT. The component owns the keyboard-door a11y contract (never `role="button"` on the box
283
+ see [composition.md's linked-record law](./composition.md)); the template supplies the icon,
284
+ facts, and localized verbs.
270
285
  Empty state = the find-or-create search, custom row → the create `Dialog` (tax ID + Fetch;
271
286
  its field errors ride `DetailRow error`, not a loose line).
272
287
  - **Fees — the DETAILED money ledger** (both directions), distinct from Billing's invoice
@@ -301,24 +316,19 @@ billing, and quick-capture templates. Top → bottom:
301
316
  consumers ("printed on the delivery note"). The picker rows ride `Checklist`/
302
317
  `ChecklistRow` (`controlWidth={24}` for the `CheckboxInput`) — the compound owns the row
303
318
  geometry and the `meta`/`expansion` indent; hand-rolling the anatomy is how alignment
304
- drifts. Every not-ready row shows BOTH the mark and the explicit trigger the mark says
305
- WHAT (the `meta` line: 12px `circle-alert` + "Needs: …", muted ink until checked,
306
- warning once it blocks), the trigger says HOW: a colored `TextLink` ("Add missing
307
- fields", link-blue) BESIDE the label — an inline row action; a `Button` here outweighs
308
- the row. It toggles the TRANSIENT fill editor in the row's `expansion` ("These values
309
- save onto the order and unlock the form.") — `FormTextInput`s (label + the consumer
310
- hint as `description`), Cancel / "Save fields" (secondary the section keeps ONE
311
- primary, disabled while a draft is empty). Checking NEVER expands anything by itself
312
- an expansion that only a side effect reveals is no affordance. Save writes the record
313
- field (its home row shows the value) and the panel collapses; mark + trigger vanish =
314
- ready, and a ready form stays silent. Drafts are keyed BY FIELD, so two forms needing
315
- the same field share one draft and one Save resolves both. The trigger HIDES while its
316
- panel is open the panel's Cancel is the closer. Missing fields are the gating law's
317
- DEGRADED-BUT-VALID case (those fields print blank, filled by hand later): Create stays
318
- ENABLED, the co-located warning `Callout` states the consequence ("… print blank unless
319
- filled above"), and the press CONFIRMS via `Alert` — per-form "form — fields" lines,
320
- Cancel back to the inline fixes / "Generate anyway". Only an empty pick disables, and
321
- silently. A form can also be DATA-conditional (the hazardous-goods note renders only
319
+ drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing (the
320
+ `meta` line: 13px `circle-alert` + "Needs: …" the actual field labels — muted at rest so
321
+ gaps show WITHOUT checking, warning once checked because it now blocks). The FIX sits
322
+ right under it in the row's `expansion` — an `Inset` (a FORM surface, NEVER a `Callout`:
323
+ a warning Callout is an ARIA `alert`, wrong around a form) with a `FormTextInput` per gap
324
+ (label + the consumer hint as `description`) and a "Save fields" (secondary the section
325
+ keeps ONE primary, disabled while every draft is empty). Save writes the record fields
326
+ (their home rows show the values); the need resolves, the mark clears, the fill drops
327
+ away. Drafts are keyed BY FIELD, so a field two checked forms share is entered ONCE and
328
+ one Save resolves both. A ready form stays SILENT. Missing fields are the gating law's
329
+ DEGRADED-BUT-VALID case (they print blank, filled by hand later): Create stays ENABLED, a
330
+ co-located warning `Callout` states the consequence, and the press CONFIRMS via `Alert`.
331
+ Only an empty pick disables, and silently. A form can also be DATA-conditional (the hazardous-goods note renders only
322
332
  while the record's Hazardous goods boolean is set — record data may reshape the list,
323
333
  output selection never does).
324
334
  - **Delivery receipt — the QUICK-ISSUE form, its OWN section** (in the rail): one document
@@ -363,29 +373,13 @@ billing, and quick-capture templates. Top → bottom:
363
373
  below that it collapses to the pinned bar. Sizing the column is the SHELL's job — inner
364
374
  blocks are plain `width: "100%"`, never a second `maxWidth` + `alignSelf: "center"`.
365
375
 
366
- ### `tpl_tasks` — the quick list
367
-
368
- The personal to-do shape: quiet `CheckCircle` rows you tick to finish and tap to **expand in
369
- place** to edit (`Inline*` editors; the row is stable — the meta summary never shifts on
370
- expand), colour-dot tags (`OptionBadge`), an attachments field (`FilesEditor` — thumbnail
371
- grid, tap a tile for the gallery with download + confirmed remove, plus its Add button),
372
- search + a clearable group-by (✕ → ungrouped) + tag filter chips, and a primary
373
- Draft-from-notes CTA (`Composer` → `AgentRun` → `ChangeReview`). No assignee — the list is
374
- yours.
375
-
376
- **Warning — never a button in a button.** There is deliberately NO `Task` component: a task
377
- row varies too much to bake into one, so rows compose `CheckCircle` + `InlineTextInput` +
378
- inline-editor cells as **sibling** controls in a plain `View`. NEVER wrap the whole row in a
379
- `Pressable accessibilityRole="button"` (for tap-to-expand) with those controls nested inside:
380
- React Native Web renders the row AND each control as a `<button>`, so you get `<button>`s
381
- inside a `<button>` — invalid DOM, and the inner controls drop out of the keyboard tab order.
382
- Give "expand" its own affordance (a trailing `Pressable` over the meta/chevron), as
383
- `tpl_tasks` does.
384
-
385
376
  ### `tpl_task_board` — the columns shape
386
377
 
387
378
  The manager's board: a search · group-by · filter toolbar over a grouped, sortable grid of
388
- LIVE inline-editable cells — assignee/due/status/tags set directly in the cell. It builds on
379
+ LIVE editable cells — assignee/due/status/tags ALL via `Inline*` editors in **`variant="cell"`**
380
+ (`InlineMemberSelect` · `InlineDatePicker` · `InlineSelect`, and tags an `InlineSelect multi
381
+ allowCustom` — create-a-tag included), set directly in the cell. Every editable cell + the custom
382
+ Files/Action cells share ONE hover language (the `zinc-100` wash); no cell reveals a border. It builds on
389
383
  the `DataGrid` primitive (the inline-managed grouped-table shape) and **owns only its data,
390
384
  toolbar, columns, and per-group add**; `DataGrid` owns the sortable header, the collapsible
391
385
  grouped sections, and the aligned rows. It demonstrates:
@@ -397,14 +391,15 @@ grouped sections, and the aligned rows. It demonstrates:
397
391
  workflow: one board, a different action per row; the link's pressable fills the cell to
398
392
  match the inline editors.
399
393
  - A trailing `ActionMenu` (the `⋯` overflow — a `danger` Remove behind a confirm `Alert`).
400
- - CUSTOM pressable cells (files, the action link) hover with the control-surface BORDER
401
- reveal (`HOVER_BORDER` from `@lotics/ui/control_surface`) — the same language as the
402
- inline-editor cells beside them, never their own background wash.
394
+ - CUSTOM pressable cells (files, the action link) hover with the **background-tint WASH**
395
+ (`zinc-100`) — the standard CELL language, matching the `variant="cell"` editors beside them.
396
+ (A `variant="form"` editor reveals a border; a `variant="cell"` editor uses the wash — the two
397
+ hover languages, one per surface.)
403
398
  - Per-group add rows that pre-set the group's field (align with the exported `gridRowStyle`),
404
- and the same Draft-from-notes CTA as `tpl_tasks`.
399
+ and a Draft-from-notes CTA.
405
400
 
406
- Both `tpl_tasks` and `tpl_task_board`: filters/search left, primary CTA right, one toolbar
407
- row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option).
401
+ `tpl_task_board`: filters/search left, primary CTA right, one toolbar row; group-by holds
402
+ only real dimensions, ✕ clears to ungrouped (no "Nothing" option).
408
403
 
409
404
  **Altitude rule**: `tpl_task_board` is the inline-managed grouped table — MODERATE data you
410
405
  manage in view (group/sort/filter/edit-in-place; it renders ALL rows). For thousands+ you
@@ -56,7 +56,6 @@ import { Callout, CalloutText } from "@lotics/ui/callout";
56
56
  import { Timeline, type TimelineItem } from "@lotics/ui/timeline";
57
57
  import { Link } from "@lotics/ui/link";
58
58
  import { TextInputField } from "@lotics/ui/text_input_field";
59
- import { InlineTagSelect } from "@lotics/ui/inline_tag_select";
60
59
  import { Section, SectionHeading, SectionHeadingMeta, SectionHeadingTitle } from "@lotics/ui/section_heading";
61
60
  import { DangerZone } from "@lotics/ui/danger_zone";
62
61
  import { ChipGroup } from "@lotics/ui/chip_group";
@@ -209,7 +208,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
209
208
  <>
210
209
  <View style={{ width: 150 }}>
211
210
  <InlineMemberSelect
212
- background="transparent"
211
+ variant="cell"
213
212
  members={MEMBERS}
214
213
  value={t.assignee}
215
214
  onSave={(m) => patch(t.id, { assignee: m })}
@@ -219,7 +218,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
219
218
  </View>
220
219
  <View style={{ width: 130 }}>
221
220
  <InlineDatePicker
222
- background="transparent"
221
+ variant="cell"
223
222
  value={t.due}
224
223
  onSave={(v) => patch(t.id, { due: v })}
225
224
  placeholder="Set due…"
@@ -230,7 +229,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
230
229
  }
231
230
  >
232
231
  <InlineTextInput
233
- background="transparent"
232
+ variant="cell"
234
233
  value={t.label}
235
234
  onSave={(v) => patch(t.id, { label: v })}
236
235
  struck={t.done}
@@ -618,11 +617,12 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
618
617
  <InlineNumberInput value={declared} onSave={persist(setDeclared)} min={0} format={(v) => (v == null ? "" : formatMoney(v))} accessibilityLabel="Declared value" />
619
618
  </DetailRow>
620
619
  <DetailRow label="Tags">
621
- <InlineTagSelect
620
+ <InlineSelect
621
+ multi
622
622
  value={tags}
623
623
  onSave={persist(setTags)}
624
624
  options={TAG_OPTIONS}
625
- renderTag={renderTagBadge}
625
+ renderSelected={renderTagBadge}
626
626
  placeholder="Add tags…"
627
627
  accessibilityLabel="Tags"
628
628
  />
@@ -957,7 +957,6 @@ function RecordDrawer({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, on
957
957
  hs.khach
958
958
  )
959
959
  }
960
- width={440}
961
960
  onPrev={canGoBack ? undefined : onPrev}
962
961
  onNext={canGoBack ? undefined : onNext}
963
962
  position={canGoBack ? undefined : position}