@lotics/ui 22.2.0 → 23.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.
Files changed (68) hide show
  1. package/AGENTS.md +9 -2
  2. package/MIGRATION.md +142 -2
  3. package/docs/ai_patterns.md +27 -25
  4. package/docs/catalog.md +119 -98
  5. package/docs/composition.md +134 -42
  6. package/docs/data_entry.md +67 -31
  7. package/docs/templates.md +86 -68
  8. package/examples/tpl_allocate.tsx +5 -5
  9. package/examples/tpl_attendance.tsx +2 -2
  10. package/examples/tpl_calendar.tsx +6 -6
  11. package/examples/tpl_dashboard.tsx +9 -9
  12. package/examples/tpl_dieline.tsx +2 -2
  13. package/examples/tpl_item_list.tsx +64 -37
  14. package/examples/tpl_lookup.tsx +5 -5
  15. package/examples/tpl_pick.tsx +6 -6
  16. package/examples/tpl_pivot.tsx +3 -3
  17. package/examples/tpl_record.tsx +919 -611
  18. package/examples/tpl_report.tsx +2 -2
  19. package/examples/tpl_rollup.tsx +5 -5
  20. package/examples/tpl_shifts.tsx +5 -5
  21. package/examples/tpl_statements.tsx +7 -7
  22. package/examples/tpl_stock.tsx +2 -2
  23. package/examples/tpl_task_board.tsx +42 -26
  24. package/examples/tpl_tower.tsx +6 -6
  25. package/package.json +3 -2
  26. package/src/agent_run.tsx +40 -25
  27. package/src/breakdown.tsx +1 -1
  28. package/src/calendar/calendar_view.tsx +1 -1
  29. package/src/change_review.tsx +9 -8
  30. package/src/chip_group.tsx +12 -2
  31. package/src/choice_list.tsx +2 -2
  32. package/src/confidence.tsx +2 -2
  33. package/src/data_grid.tsx +1 -1
  34. package/src/detail_row.tsx +50 -58
  35. package/src/file_dropzone.tsx +1 -1
  36. package/src/file_gallery_modal.tsx +3 -3
  37. package/src/file_rows.tsx +1 -1
  38. package/src/finding.tsx +4 -4
  39. package/src/form_field.tsx +1 -1
  40. package/src/format_date.ts +2 -2
  41. package/src/heatmap.tsx +1 -1
  42. package/src/inline_button.tsx +84 -0
  43. package/src/inline_date_picker.tsx +17 -10
  44. package/src/inline_edit.tsx +298 -59
  45. package/src/inline_member_select.tsx +8 -3
  46. package/src/inline_number_input.tsx +11 -4
  47. package/src/inline_select.tsx +26 -13
  48. package/src/inline_text_input.tsx +12 -4
  49. package/src/inline_time_picker.tsx +10 -4
  50. package/src/ledger.tsx +2 -2
  51. package/src/locale.tsx +7 -3
  52. package/src/matrix.tsx +1 -1
  53. package/src/number_input.tsx +18 -7
  54. package/src/pipeline.tsx +1 -1
  55. package/src/popover.tsx +1 -1
  56. package/src/press_door.tsx +1 -1
  57. package/src/pressable_highlight.tsx +1 -1
  58. package/src/progress_bar.tsx +3 -3
  59. package/src/record_summary.tsx +2 -2
  60. package/src/result_header.tsx +2 -2
  61. package/src/sequence.tsx +170 -0
  62. package/src/share_or_download.ts +2 -2
  63. package/src/step_progress.tsx +7 -5
  64. package/src/stepper.tsx +1 -1
  65. package/src/task.tsx +6 -6
  66. package/src/text_input_field.tsx +19 -1
  67. package/src/text_utils.ts +1 -1
  68. package/src/linked_record_box.tsx +0 -157
package/docs/templates.md CHANGED
@@ -92,15 +92,23 @@ Laws for any screen where a record's data is edited and a primary action produce
92
92
  genuine activity FEED (who changed what, when), not a schedule of payments.
93
93
  7. **Header = identity + provenance.** Id, the key identifier, created-at. No metrics or badges
94
94
  duplicating data a section already owns.
95
- 8. **Destructive placement.** Solid `danger`, bottom-left after the entity's fields, ONE
95
+ 8. **Action placement a verb goes where its effect lands.** Value verbs on the value
96
+ (`InlineButton`), row verbs on the row, an ADD below the list it extends (primary, left —
97
+ where the new row will appear, and primary in the `EmptyState` too: one verb, one weight),
98
+ a committing act in its own row at the section's end,
99
+ deletion in the `DangerZone`. Section headings carry identity and view controls only; an Add
100
+ in the heading sits above what it extends and competes with the title. A committing act rides
101
+ an empty-label `DetailRow` so it lands on the CONTROL column — never right-floated, which
102
+ aligns to nothing (only overlay footers right-align).
103
+ 9. **Destructive placement.** Solid `danger`, bottom-left after the entity's fields, ONE
96
104
  convention page-wide, confirmed by `Alert`. Never in a heading row, never one-tap.
97
- 9. **Create-then-refine.** An add is one click creating a draft edited in place — drafts render
98
- as empty inline editors with placeholders, never as fake data. No type-a-name-then-click
99
- forms.
100
- 10. **Optimistic feedback.** Every edit echoes locally, saves in the background, reconciles on
105
+ 10. **Create-then-refine.** An add is one click creating a draft edited in place — drafts render
106
+ as empty inline editors with placeholders, never as fake data. No type-a-name-then-click
107
+ forms.
108
+ 11. **Optimistic feedback.** Every edit echoes locally, saves in the background, reconciles on
101
109
  refetch, reverts + surfaces on error. Silent success, loud failure — no persistent "saved"
102
110
  chrome.
103
- 11. **Chrome discipline.** Kit spacing scale only; ONE divider rule (between sibling blocks,
111
+ 12. **Chrome discipline.** Kit spacing scale only; ONE divider rule (between sibling blocks,
104
112
  never doubled with section hairlines, none trailing); no state-echo labels or filler
105
113
  captions — explanations live in `info` popovers and empty states (→ [composition grammar
106
114
  §Microcopy](./composition.md)). A "why" question about a control is answered by tracing
@@ -114,10 +122,10 @@ records behind each number.
114
122
  ### `tpl_dashboard` — the executive overview
115
123
 
116
124
  One screen answering "how is this period going, where is it stuck, who pays the bills".
117
- Bands: header + period · `KPIStrip` · a revenue chart (`BarChart`, emerald = money) beside
118
- the pipeline (`StackedProgressBar`, blue = pipeline) · a **Needs attention** `Accordion`
125
+ Bands: header + period, `KPIStrip`, a revenue chart (`BarChart`, emerald = money) beside
126
+ the pipeline (`StackedProgressBar`, blue = pipeline), a **Needs attention** `Accordion`
119
127
  (icon + label + count `Badge`; expanding opens the records behind the count, each row
120
- carrying its domain `⋯ ActionMenu`) · top customers (`Avatar` + revenue + margin badge; the
128
+ carrying its domain `⋯ ActionMenu`), top customers (`Avatar` + revenue + margin badge; the
121
129
  name `Peek`s the dossier). Also exercises the wider chart family (`LineChart`, `PieChart`,
122
130
  `RingGauge`, `Sparkline`, `TrendFooter`, `Funnel`) and `DateRangeFilterField` — the date
123
131
  filter actually filters the KPI datasets. Start here for any "state of the operation"
@@ -138,8 +146,8 @@ carry meaning, one `ColorName` per state, no hand-picked hex.
138
146
 
139
147
  Continuous monitoring of hundreds of live units (machines, gates, sensors). Three bands
140
148
  answer the three ops questions in order: `KPIStrip` + `StatusGrid` = "is everything OK right
141
- now?" (scan for red; the `StatusLegend` drills a state) · an exceptions rail = "where do I
142
- look first?" (severity, then longest unresolved) · a `Heatmap` = "when/where does it
149
+ now?" (scan for red; the `StatusLegend` drills a state), an exceptions rail = "where do I
150
+ look first?" (severity, then longest unresolved), a `Heatmap` = "when/where does it
143
151
  cluster?" (pressing a cell explains the day). Cells and exception rows open the SAME
144
152
  sequenced unit `Drawer` (with a `Timeline` of the unit's events). One semantic color
145
153
  reference per state — every weight (grid cell, legend dot, drawer badge) derives from that
@@ -192,10 +200,12 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
192
200
  record-workspace anatomy section by section:
193
201
  - **Details `DetailTable`** showing every inline field type — text, `InlineMemberSelect`,
194
202
  dates incl. optional time, dot-select, money number, an `InlineSelect multi` tag field,
195
- and `InlineStatic` + a System badge for read-only values. Detail rows earn trailing CTAs
196
- where deserved (Phone → Call, Due → Today) the case where `trailingWidth` IS set.
203
+ and `InlineStatic` + a System badge for read-only values. It is also the worked example of
204
+ where a VERB goes: Phone → Call and Due → Today are both `InlineButton`s in the field's
205
+ `actions` (on the surface, disabled while empty) — the date editor included, since a field
206
+ anchors its overlay to its own box; the System badge, not being a verb, sits beside its value.
197
207
  - **Linked records** — the customer's other records as `ListItem`s (title + description at
198
- the column edge, badge · amount · chevron); pressing one PUSHES an **editable** workspace
208
+ the column edge, badge, amount, chevron); pressing one PUSHES an **editable** workspace
199
209
  for that record inside the drawer via the hosted `ScreenRouter` — while `canGoBack` the
200
210
  drawer header swaps to a back button + the pushed id and the ◀ ▶ sequencer hides; back
201
211
  pops with scroll preserved.
@@ -223,7 +233,7 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
223
233
  analysis surfaced (described options, one custom-answer slot; its Cancel/Back/Next/Submit
224
234
  live in the `DialogFooter` — `ClarifyWizardScope` wraps the Dialog, `ClarifyWizardActions`
225
235
  rides the footer) → the IMPORT stream CREATES the records and the dialog ends on the RESULT
226
- LIST — one hand-rolled result row per record (outcome mark · what landed · key figures —
236
+ LIST — one hand-rolled result row per record (outcome mark, what landed, key figures —
227
237
  the anatomy in ai_patterns §the one law; secondary "Done" to dismiss) — the one law's
228
238
  creation branch
229
239
  (ai_patterns.md): no review gate on a create, the register IS the review, correction uses
@@ -249,7 +259,7 @@ progress, and a short-pick exception that flags-and-advances without stalling, e
249
259
  Apply ONE source across MANY targets until the remainder is zero (cash application,
250
260
  stock-to-orders, budget distribution). The inverse of a batch builder: batch SUMS parts up to
251
261
  a total; allocation SPLITS a fixed total DOWN with a remainder that must hit zero. The
252
- `RemainderMeter` is the spine — under (left on account) · exact (apply) · over (blocked).
262
+ `RemainderMeter` is the spine — under (left on account), exact (apply), over (blocked).
253
263
  Oldest-first auto-allocates; each `AllocationRow` can be filled or typed.
254
264
 
255
265
  ### `tpl_record` — THE record surface
@@ -260,16 +270,16 @@ left panel as the bare circular `BackButton`). First paint is a `Skeleton` MIRRO
260
270
  layout, never a spinner. It absorbed the old order-form, inline-record, intake, settings,
261
271
  billing, and quick-capture templates. Top → bottom:
262
272
 
263
- - A MINIMAL header: `RecordSummary` (title · subtitle ONLY — no stage chip, no metric; Progress
264
- carries the desk, Handoff carries what it created, money lives in Billing), then the record's
273
+ - A MINIMAL header: `RecordSummary` (title, subtitle ONLY — no stage chip, no metric; Progress
274
+ carries the desk, money lives in Billing), then the record's
265
275
  ATTENTION state as a co-located `Callout` (e.g. overdue — record-scoped state reads here;
266
- field-scoped state stays on its field), then a quiet `SummaryLine` (desk · documents
267
- · to collect).
276
+ field-scoped state stays on its field), then a quiet `SummaryLine` (desk, documents
277
+ , to collect).
268
278
  - **GENERAL — the MAIN details, FIRST in the rail**; every later section is supplementary.
269
279
  A `SubsectionStack` (space-only beat) of a headingless LEAD group (the key facts) plus
270
280
  named groups (e.g. Order / Fulfilment / System — system identifiers get a HOME, not a
271
- per-row badge). Every group's `DetailTable` repeats the same `labelWidth`/`trailingWidth`
272
- so the grid aligns through the groups. The FIELD-ANNOTATION vocabulary is the worked
281
+ per-row badge). Every group's `DetailTable` repeats the same `labelWidth` so the grid
282
+ aligns through the groups. The FIELD-ANNOTATION vocabulary is the worked
273
283
  example here: `description` (persistent, under the value, incl. a deliberately LONG one)
274
284
  and `error` (derived, e.g. overdue Due) — always explicit, never an ⓘ; the inline editors
275
285
  demo their own transient save error (a throwing `onSave`). The Classification group works
@@ -281,7 +291,10 @@ billing, and quick-capture templates. Top → bottom:
281
291
  field renders only while its parent value makes it real (no disabled ghost rows).
282
292
  Radio/checkbox/search are PERSISTENT controls — the control is the best display;
283
293
  prose-shaped values stay inline editors.
284
- - **Comments** — a coworker's note is the first thing read on a shared record: `CommentList`
294
+ - **Comments — a docked PANEL, not a section.** It is about the whole record, so it has no place
295
+ in a top-to-bottom read and no rail entry: it pins in the page's RIGHT gutter (the column the
296
+ outline rail's balance already reserved), scrolling inside itself so a long thread never
297
+ lengthens the record, and collapses onto the pinned bar's right on narrow. `CommentList`
285
298
  (author-only edit/delete; attachments as a pressable `FileGrid` → gallery preview; a
286
299
  file-capable edit form injected via `renderEditForm`) + THE kit `Composer` (attach via
287
300
  `actionsButton`, staged files in its `files` slot) — never a bespoke comment box.
@@ -295,7 +308,7 @@ billing, and quick-capture templates. Top → bottom:
295
308
  handoff shape for the rules; for a record that genuinely needs a CHECKLIST, the task-list
296
309
  grammar is worked in `tpl_item_list`.
297
310
  - **Documents** — the Agents "Document desk" pattern (this template is its worked example —
298
- see the Agents chapter below): the register `Table` (search · Add files) whose selection
311
+ see the Agents chapter below): the register `Table` (search, Add files) whose selection
299
312
  feeds the `FloatingActionBar` → ONE "Use AI" fork (extract / cross-check / edit-with-AI).
300
313
  The template is the worked example of the THREE-way, WHOLE-SURFACE file intake: the Add-files
301
314
  CTA plus a `<FileDropTarget … paste disabled={aiOpen}>` wrapping the ENTIRE section stack (not
@@ -308,32 +321,43 @@ billing, and quick-capture templates. Top → bottom:
308
321
  fills it as a real thumbnail, a document centers its badge in it — never a bare `FileBadge`
309
322
  (mixed footprints misalign the identity column). The desk holds what ARRIVES; generation
310
323
  lives in the Document set OUTPUT section below.
311
- - **Customer — the LINKED-RECORD reference** via **`LinkedRecordBox`**: another record's data
312
- NEVER renders as field rows on this one a BORDERED, fully pressable box (glyph anchor ·
313
- identity · facts stacked VERTICALLY) whose press opens the detail drawer; the verbs ride the
314
- `actions` slot INSIDE the box destructive (Remove, `danger`) LEFT, go-to (Open record)
315
- RIGHT. The component owns the keyboard-door a11y contract (never `role="button"` on the box
316
- see [composition.md's linked-record law](./composition.md)); the template supplies the icon,
317
- facts, and localized verbs.
318
- Empty state = the find-or-create search, custom row → the create `Dialog` (tax ID + Fetch;
319
- its field errors ride `DetailRow error`, not a loose line).
324
+ - **Customer — a REFERENCE, and therefore one of General's field rows**, not a section of its
325
+ own (see [composition.md's reference law](./composition.md)). It wears the inline editor's
326
+ resting surface with an `InlineButton` **Open** inside it; pressing the surface PEEKS the
327
+ facts, and the peek's footer carries detach LEFT / Open RIGHT — so the row itself shows no
328
+ destructive verb at all. The tax-ID problem that gates invoicing is the row's `warning`, not
329
+ a floating `Callout`. Transport's three party rows are the same shape repeated, which is where
330
+ it earns itself: N references as field rows scan as one list, where N cards would be a wall.
331
+ Empty state = the find-or-create `Combobox` in the same value slot, custom row → the create
332
+ `Dialog` (tax ID + Fetch; its field errors ride `DetailRow error`, not a loose line).
333
+ **Reference (the buyer's own) is the `actions` worked example**: its `Copy` is an
334
+ `InlineButton` ON the field, passed unconditionally and `disabled` while empty — not in the
335
+ row's `trailing`, and not conditional, which would resize the field as you type.
320
336
  - **Fees — the DETAILED money ledger** (both directions), distinct from Billing's invoice
321
- documents: a `SummaryLine` (collected · to collect · to pay, warning-toned when overdue) →
322
- a `priority`-annotated register `Table` (Fee · Type · Party · Amount · Status — status in
337
+ documents: a `SummaryLine` (collected, to collect, to pay, warning-toned when overdue) →
338
+ a `priority`-annotated register `Table` (Fee, Type, Party, Amount, Status — status in
323
339
  plain ink, danger only when overdue) → EVERY row opens a right-docked entity `Drawer`
324
340
  (◀ ▶ + position stepping) with all fields inline-editable and a confirmed Remove in the
325
341
  `DrawerFooter`. "Add fee" is create-then-refine: a blank fee opens straight in the drawer.
326
- - **Billing**every group a `Subsection` on ONE beat: per-invoice bands (charge chips with
327
- ghost list prices + one-tap "Standard …" fill, "How paid…" selects), the collect band
328
- (owns the total + the receipt), the deposit. THE ACTION-GATING LAW is the worked example:
329
- a not-ready Issue is DISABLED and the reason is a co-located `Callout` at the gate's
330
- scope, once a band-local problem (charged line missing its method) in the band, a broken
331
- record premise (no customer / invalid tax ID) at the section top; self-evident empties
332
- stay silent, and NEVER prose beside a CTA. Issuing gates on record premises only — never
333
- silently on stage. It is also the worked example of **an irreversible action taking an ID,
334
- not a captured object**: `onIssue(inv.key)` with the invoice resolved where the confirm
335
- renders, so the quoted total is the one that will be billed even when the press was held
336
- for a charge cell's write (data_entry.md § Inline edit).
342
+ - **Billing — a REGISTER, the same shape as Fees.** Both sections are a list of money items
343
+ where each item has detail and a per-item act, so both are a compact `Table` whose row opens
344
+ its FULL detail in a right-docked `Drawer` (◀ step the invoices) the drill-down law.
345
+ It was three inline bands stacked, each with its own charge table, callout and action row,
346
+ plus two more bands after them: five subsections and four primaries in one section, which is
347
+ what made the area read as spread. A row now carries title, lines, total, a DERIVED state
348
+ `Badge`, the issued ref; the charge editors (ghost list prices with one-tap "Standard …"
349
+ fill, "How paid…" selects) live in the drawer with the lines they bill, and the section ends
350
+ with ONE summary (`SummaryLine`: to collect, deposit, issued) and ONE action row.
351
+ THE ACTION-GATING LAW is still the worked example: a not-ready Issue is DISABLED and the
352
+ reason is a co-located `Callout` at the gate's SCOPE, once an invoice's own inconsistency
353
+ (a charged line with no method) in its drawer, a broken record premise (no customer / invalid
354
+ tax ID) at the section top; self-evident empties stay silent, and NEVER prose beside a CTA.
355
+ Issuing gates on record premises only — never silently on stage. Issue sits in the DRAWER's
356
+ footer, which is the one place a CTA right-aligns (an overlay footer; on the page it would
357
+ ride the control column). It is also the worked example of **an irreversible action taking an
358
+ ID, not a captured object** — the confirm re-resolves the invoice from its key, so the quoted
359
+ total is the one that will be billed even when the press was held for a charge cell's write
360
+ (data_entry.md § Inline edit).
337
361
  - **Document set — the OUTPUT desk, the last WORK section** (the composition rules' output
338
362
  law worked on the record surface: the top is intake, the bottom produces on demand).
339
363
  Forms group PER PARTY on the `SubsectionStack` beat — each party a `SubsectionHeading` +
@@ -374,17 +398,11 @@ billing, and quick-capture templates. Top → bottom:
374
398
  a label-left capture form (auto-stamped date/receipt-no, record-prefilled fields, the
375
399
  right input each) whose CTA and produced `FileRow` ride an empty-label `DetailRow` — the
376
400
  form-action alignment law (data_entry.md), worked.
377
- - **Activity the audit trail**: a `Timeline` (32px discs, 40px rows) of every change —
378
- who and when with DRILL-DOWN `details` (a field change carries its From To diff, a
379
- document entry a pressable `FileRow` preview) and LIVE appends from the surface's real
380
- actions (issue, fees, handoff).
381
- - **Handoff ONCE.** Handing off CREATES the next desk's record on ITS table and links it
382
- here: the dialog collects the receiver (`MemberSelect`, confirm disabled until chosen) +
383
- an optional note; confirm writes the trail mark and the SIBLING record, shown as the same
384
- linked-record box (its drawer holds the reference info + Open record + **Recall**). After
385
- the handoff there is NO further CTA — the act to leave the NEW desk rides that desk's stage
386
- in Progress, and this section shows only what the handoff produced (with an empty state when
387
- it has produced nothing yet).
401
+ - **No Activity or Handoff section.** Progress owns both: it says where the record SITS and
402
+ carries the act that moves it, so a second section repeating the handoff and a `Timeline` of
403
+ every change beside it were two more places to look for one answer. What that costs is
404
+ explicit: the sibling record a handoff creates has no home on this surface, and there is no
405
+ audit trail. Both belong on the Progress stage that produced them if an app needs them back.
388
406
 
389
407
  Recall is the undo: confirmed, it withdraws the sibling, clears the custody stamp that
390
408
  handoff wrote, and returns the record to the desk BEFORE it — one step, never a jump to the
@@ -437,8 +455,8 @@ divorced from the state that gated it. A stage says the position outright, and t
437
455
  stage it belongs to. Reach for a checklist only when the rows are genuinely N independent
438
456
  tickable items (`TaskList`); a record's own progress is a pipeline.
439
457
 
440
- Built on the `Pipeline` compound (`Pipeline` · `PipelineStage` · `PipelineField` ·
441
- `PipelineNote` · `PipelineActions`), which composes over `Stepper` — `Stepper` renders POSITION,
458
+ Built on the `Pipeline` compound (`Pipeline`, `PipelineStage`, `PipelineField`,
459
+ `PipelineNote`, `PipelineActions`), which composes over `Stepper` — `Stepper` renders POSITION,
442
460
  this adds the anatomy that turns a position into a workspace.
443
461
 
444
462
  Three rules the anatomy holds, each learned from getting it wrong:
@@ -466,9 +484,9 @@ and tickable in any order are a `tpl_task_board`, not a pipeline.
466
484
 
467
485
  ### `tpl_task_board` — the columns shape
468
486
 
469
- The manager's board: a search · group-by · filter toolbar over a grouped, sortable grid of
470
- LIVE editable cells — assignee/due/status/tags ALL via `Inline*` editors in **`variant="cell"`**
471
- (`InlineMemberSelect` · `InlineDatePicker` · `InlineSelect`, and tags an `InlineSelect multi
487
+ The manager's board: a search, group-by, filter toolbar over a grouped, sortable grid of
488
+ LIVE editable cells — assignee/due/status/tags ALL via `Inline*` editors in **`variant="bare"`**
489
+ (`InlineMemberSelect`, `InlineDatePicker`, `InlineSelect`, and tags an `InlineSelect multi
472
490
  allowCustom` — create-a-tag included), set directly in the cell. Every editable cell + the custom
473
491
  Files/Action cells share ONE hover language (the `zinc-100` wash); no cell reveals a border. It builds on
474
492
  the `DataGrid` primitive (the inline-managed grouped-table shape) and **owns only its data,
@@ -483,8 +501,8 @@ grouped sections, and the aligned rows. It demonstrates:
483
501
  match the inline editors.
484
502
  - A trailing `ActionMenu` (the `⋯` overflow — a `danger` Remove behind a confirm `Alert`).
485
503
  - CUSTOM pressable cells (files, the action link) hover with the **background-tint WASH**
486
- (`zinc-100`) — the standard CELL language, matching the `variant="cell"` editors beside them.
487
- (A `variant="form"` editor reveals a border; a `variant="cell"` editor uses the wash — the two
504
+ (`zinc-100`) — the ROW's own hover language, distinct from the field frame a `bare` editor
505
+ fades in on its own hover. (The row washes; the field's edge draws — the two
488
506
  hover languages, one per surface.)
489
507
  - Per-group add rows that pre-set the group's field (align with the exported `gridRowStyle`),
490
508
  and a Draft-from-notes CTA.
@@ -532,8 +550,8 @@ week so the copied screen is evergreen.
532
550
 
533
551
  ### `tpl_attendance` — the attendance desk
534
552
 
535
- Who is in, who is late, who is out, plus the weekly per-person grid. Header + month `Picker`
536
- · `KPIStrip` (the four counts) · `Tabs` Today / This week. "Today" = one banded roster
553
+ Who is in, who is late, who is out, plus the weekly per-person grid. Header + month `Picker`,
554
+ `KPIStrip` (the four counts), `Tabs` Today / This week. "Today" = one banded roster
537
555
  `Card` — each row press-opens the person's workspace `Drawer` (◀ ▶ sequencing) and carries a
538
556
  `⋯ ActionMenu` with the day's quick fixes. "This week" = per-person `Accordion` rows: day
539
557
  markers expanding to the day-by-day detail in place. Presence is one accent; badge colors
@@ -587,8 +605,8 @@ phase runs inside a `FollowScroll` (swapped back to `DialogScrollArea` on the ph
587
605
  so review opens at the top):
588
606
 
589
607
  - **Extract** (read the documents, fill the record) — the open `ChangeFields` review: ONE
590
- `Change` section per record whose body stacks a `ChangeField` per proposed value (label ·
591
- the − band when replacing · the editable + value). **Editing IS the review**; an add has no
608
+ `Change` section per record whose body stacks a `ChangeField` per proposed value (label,
609
+ the − band when replacing, the editable + value). **Editing IS the review**; an add has no
592
610
  before, a removal is the − band alone, and a conflict shows the read-only outcome band over
593
611
  candidate rows + a type-another-value third option. Proposed line items arrive as
594
612
  `ChangeRecord` cards; Keep-all + one outcome-named Apply commit.
@@ -11,13 +11,13 @@ import { RemainderMeter } from "@lotics/ui/remainder_meter";
11
11
  import { formatMoney } from "@lotics/ui/format_money";
12
12
 
13
13
  // ─────────────────────────────────────────────────────────────────────────────
14
- // Template · Allocation / split — apply ONE source across MANY targets until the
14
+ // Template, Allocation / split — apply ONE source across MANY targets until the
15
15
  // remainder is zero. Here: cash application — a received payment spread across a
16
16
  // customer's open invoices (the same shape serves stock-to-orders, landed-cost,
17
17
  // budget distribution). The inverse of the batch builder: batch SUMS parts up to
18
18
  // a total; allocation SPLITS a fixed total DOWN, with a remainder that must hit
19
- // zero. The RemainderMeter is the spine — under (left on account) · exact (apply)
20
- // · over (blocked). Oldest-first auto-allocates; each row can be filled or typed.
19
+ // zero. The RemainderMeter is the spine — under (left on account), exact (apply),
20
+ // over (blocked). Oldest-first auto-allocates; each row can be filled or typed.
21
21
  // ─────────────────────────────────────────────────────────────────────────────
22
22
 
23
23
  interface Invoice {
@@ -77,7 +77,7 @@ export function TplAllocate() {
77
77
  <View style={{ gap: 2, flexGrow: 1, flexBasis: 220 }}>
78
78
  <Text size="xs" color="muted" weight="medium">Payment received</Text>
79
79
  <Text size="xxxl" weight="semibold" tabular>{formatMoney(PAYMENT.amount)}</Text>
80
- <Text size="sm" color="muted">{`${PAYMENT.from} · ref ${PAYMENT.ref} · ${PAYMENT.date}`}</Text>
80
+ <Text size="sm" color="muted">{`${PAYMENT.from}, ref ${PAYMENT.ref}, ${PAYMENT.date}`}</Text>
81
81
  </View>
82
82
  <View style={{ gap: 10, flexGrow: 1, flexBasis: 300 }}>
83
83
  <RemainderMeter total={PAYMENT.amount} allocated={allocated} format={formatMoney} />
@@ -92,7 +92,7 @@ export function TplAllocate() {
92
92
  {/* the targets — the open invoices, each absorbing part of the payment */}
93
93
  <View>
94
94
  <View style={{ paddingHorizontal: 20, paddingVertical: 12 }}>
95
- <Text size="xs" color="muted" weight="medium">{`Open invoices · ${INVOICES.length}`}</Text>
95
+ <Text size="xs" color="muted" weight="medium">{`Open invoices (${INVOICES.length})`}</Text>
96
96
  </View>
97
97
  <Divider />
98
98
  {INVOICES.map((inv, i) => (
@@ -17,11 +17,11 @@ import { Picker } from "@lotics/ui/picker";
17
17
  import { Tabs } from "@lotics/ui/tabs";
18
18
 
19
19
  // ─────────────────────────────────────────────────────────────────────────────
20
- // Template · Attendance. The daily attendance desk: who is in, who
20
+ // Template, Attendance. The daily attendance desk: who is in, who
21
21
  // is late, who is out, plus the weekly per-person grid. Emerald = presence;
22
22
  // badge colors carry the status meaning (amber late / red absent / blue leave).
23
23
  //
24
- // Grammar: zinc-50 canvas · header + month Picker · KPIStrip (four counts) ·
24
+ // Grammar: zinc-50 canvas, header + month Picker, KPIStrip (four counts),
25
25
  // Tabs Today / This week. "Today" = one banded roster Card — each row
26
26
  // press-opens the employee workspace Drawer (◀ ▶ sequencing) and carries a
27
27
  // ⋯ ActionMenu with the day's quick fixes. "This week" = per-person Accordion
@@ -19,12 +19,12 @@ import {
19
19
  } from "@lotics/ui/calendar";
20
20
 
21
21
  // ─────────────────────────────────────────────────────────────────────────────
22
- // Template · Calendar — the delivery/schedule desk. One real
22
+ // Template, Calendar — the delivery/schedule desk. One real
23
23
  // CalendarView (week grid) over the current week's deliveries / reconciliations /
24
24
  // customer visits, then a "Today" agenda card listing today's three slots.
25
25
  //
26
- // Grammar: zinc-50 canvas · header band (title + week label + Today) ·
27
- // calendar card at a real height · agenda card of pressable rows. Every slot
26
+ // Grammar: zinc-50 canvas, header band (title + week label + Today),
27
+ // calendar card at a real height, agenda card of pressable rows. Every slot
28
28
  // is a door: press (grid event or agenda row) opens the sequenced workspace
29
29
  // Drawer; agenda rows also carry the ⋯ quick-actions menu. Event colors carry
30
30
  // meaning: blue = delivery, emerald = reconciliation, amber = customer.
@@ -141,8 +141,8 @@ const SLOT_ACTIONS = [
141
141
  // Compact slot workspace — identity + key facts + the two real actions.
142
142
  function SlotWorkspace({ e }: { e: CalendarEvent<AgendaMeta> }) {
143
143
  const thoiGian = e.allDay
144
- ? `${fmtDate(e.start)} – ${e.end ? fmtDate(e.end) : ""} · all day`
145
- : `${fmtDate(e.start)} · ${fmtTime(e.start)} – ${e.end ? fmtTime(e.end) : ""}`;
144
+ ? `${fmtDate(e.start)} – ${e.end ? fmtDate(e.end) : ""} (all day)`
145
+ : `${fmtDate(e.start)}, ${fmtTime(e.start)} – ${e.end ? fmtTime(e.end) : ""}`;
146
146
  const rows: [string, React.ReactNode][] = [
147
147
  ["Time", <Text key="tg" size="sm" tabular>{thoiGian}</Text>],
148
148
  ...(e.data
@@ -236,7 +236,7 @@ export function TplCalendar() {
236
236
  <Card style={{ padding: 0 }}>
237
237
  <CardHeader>
238
238
  <CardHeaderTitle>Today</CardHeaderTitle>
239
- <CardHeaderMeta>{`${todayLabel} · ${todayEvents.length} slots`}</CardHeaderMeta>
239
+ <CardHeaderMeta>{`${todayLabel} (${todayEvents.length} slots)`}</CardHeaderMeta>
240
240
  </CardHeader>
241
241
  <View style={{ paddingHorizontal: 16, paddingVertical: 6 }}>
242
242
  {todayEvents.map((e) => (
@@ -36,15 +36,15 @@ import { Funnel } from "@lotics/ui/funnel";
36
36
  import { formatMoney } from "@lotics/ui/format_money";
37
37
 
38
38
  // ─────────────────────────────────────────────────────────────────────────────
39
- // Template · Dashboard — the executive operations dashboard. One
39
+ // Template, Dashboard — the executive operations dashboard. One
40
40
  // screen that answers "how is this month going, where is it stuck, who pays
41
41
  // the bills".
42
42
  //
43
- // Bands: header + period badge · KPI strip (KPIStrip) · revenue by
43
+ // Bands: header + period badge, KPI strip (KPIStrip), revenue by
44
44
  // month (BarChart, emerald = money) next to the sales pipeline
45
- // (StackedProgressBar, blue = pipeline) · Needs attention (Accordion rows —
45
+ // (StackedProgressBar, blue = pipeline), Needs attention (Accordion rows —
46
46
  // icon + label + count Badge; expanding opens the records behind the
47
- // count, each record carries its domain ⋯ ActionMenu) · Top customers
47
+ // count, each record carries its domain ⋯ ActionMenu), Top customers
48
48
  // (Avatar + revenue + margin badge; the name Peeks the dossier).
49
49
  // ─────────────────────────────────────────────────────────────────────────────
50
50
 
@@ -78,19 +78,19 @@ const KY_DU_LIEU: Record<
78
78
  doanhThu: 986_000_000, doanhThuTrend: -6, doanhThuCaption: "Mar: ₫1.05B",
79
79
  bienLai: 16.8, bienLaiTrend: -1, bienLaiCaption: "Mar: 17.4%",
80
80
  donDangXuLy: 31, donCaption: "2 orders past delivery date",
81
- congNo: 512_000_000, congNoCaption: "8 customers overdue · up 4%",
81
+ congNo: 512_000_000, congNoCaption: "8 customers overdue, up 4%",
82
82
  },
83
83
  "2026-05": {
84
84
  doanhThu: 1_152_000_000, doanhThuTrend: 17, doanhThuCaption: "Apr: ₫986M",
85
85
  bienLai: 17.1, bienLaiTrend: 2, bienLaiCaption: "Apr: 16.8%",
86
86
  donDangXuLy: 34, donCaption: "3 orders past delivery date",
87
- congNo: 449_000_000, congNoCaption: "5 customers overdue · down 12%",
87
+ congNo: 449_000_000, congNoCaption: "5 customers overdue, down 12%",
88
88
  },
89
89
  "2026-06": {
90
90
  doanhThu: 1_284_000_000, doanhThuTrend: 11, doanhThuCaption: "May: ₫1.15B",
91
91
  bienLai: 18.4, bienLaiTrend: 1, bienLaiCaption: "May: 17.1%",
92
92
  donDangXuLy: 37, donCaption: "4 orders past delivery date",
93
- congNo: 486_000_000, congNoCaption: "6 customers overdue · up 8%",
93
+ congNo: 486_000_000, congNoCaption: "6 customers overdue, up 8%",
94
94
  },
95
95
  };
96
96
  const KY_CUOI = "2026-06";
@@ -329,7 +329,7 @@ export function TplDashboard() {
329
329
  ]}
330
330
  />
331
331
 
332
- {/* chart row 1: revenue by month (BarChart + TrendFooter) · margin trend (LineChart) */}
332
+ {/* chart row 1: revenue by month (BarChart + TrendFooter), margin trend (LineChart) */}
333
333
  <View style={{ flexDirection: "row", gap: 16, alignItems: "stretch" }}>
334
334
  <Card style={{ padding: 0, flex: 3 }}>
335
335
  <CardHeader>
@@ -364,7 +364,7 @@ export function TplDashboard() {
364
364
  </Card>
365
365
  </View>
366
366
 
367
- {/* chart row 2: pipeline funnel · revenue mix · on-time delivery */}
367
+ {/* chart row 2: pipeline funnel, revenue mix, on-time delivery */}
368
368
  <View style={{ flexDirection: "row", gap: 16, alignItems: "stretch" }}>
369
369
  <Card style={{ padding: 0, flex: 3 }}>
370
370
  <CardHeader>
@@ -24,7 +24,7 @@ const MOCK_PHOTO: DisplayFile = {
24
24
  };
25
25
 
26
26
  // ─────────────────────────────────────────────────────────────────────────────
27
- // Template · AI design canvas (the carton "dieline" app) — the FLAGSHIP shape:
27
+ // Template, AI design canvas (the carton "dieline" app) — the FLAGSHIP shape:
28
28
  // the page IS the design. The dieline fills the centre, a FLOATING composer sits
29
29
  // at the bottom, and a pinned PARAMS PANEL (a hand-composed label/value field-card
30
30
  // in live-edit mode) floats at the centre-right. Empty → drop a photo in the
@@ -248,7 +248,7 @@ function ParamsPanel({ dims, onEditParam, onDownload }: { dims: Dims; onEditPara
248
248
  <View style={panel.header}>
249
249
  <View style={{ flex: 1, gap: 1 }}>
250
250
  <Text size="sm" weight="semibold">Parameters</Text>
251
- {open ? <Text size="xs" color="muted">FEFCO 0201 · B-flute</Text> : null}
251
+ {open ? <Text size="xs" color="muted">FEFCO 0201, B-flute</Text> : null}
252
252
  </View>
253
253
  <IconButton icon={open ? "minus" : "plus"} accessibilityLabel={open ? "Minimize parameters" : "Expand parameters"} onPress={() => setOpen((o) => !o)} />
254
254
  </View>