@lotics/ui 22.3.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 +22 -22
  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 +4 -4
  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
@@ -6,6 +6,7 @@ import { colors, solid, type ColorName } from "@lotics/ui/colors";
6
6
  import { ActionMenu, type ActionMenuItem } from "@lotics/ui/action_menu";
7
7
  import { Alert } from "@lotics/ui/alert";
8
8
  import { Badge } from "@lotics/ui/badge";
9
+ import { InlineButton } from "@lotics/ui/inline_button";
9
10
  import { Button } from "@lotics/ui/button";
10
11
  import { CardFooter } from "@lotics/ui/card";
11
12
  import { CheckboxInput } from "@lotics/ui/checkbox_input";
@@ -73,14 +74,14 @@ import { InlineTextInput } from "@lotics/ui/inline_text_input";
73
74
  import { useSelection } from "@lotics/ui/use_selection";
74
75
 
75
76
  // ─────────────────────────────────────────────────────────────────────────────
76
- // Template · Register — THE canonical register, and the ONE work-execution list
77
+ // Template, Register — THE canonical register, and the ONE work-execution list
77
78
  // shape. A flat, paginated, SELECTABLE list of records: one toolbar row (search +
78
- // consolidated filters LEFT, New CTA RIGHT) · a light SummaryLine of the filtered
79
- // view · a sortable, Divider-separated register where every row has a leading
79
+ // consolidated filters LEFT, New CTA RIGHT), a light SummaryLine of the filtered
80
+ // view, a sortable, Divider-separated register where every row has a leading
80
81
  // checkbox (a row that can't take the bulk action — here an "Awaiting docs" case —
81
82
  // gets a DISABLED one), a per-row PRINT action button, and a ⋯ overflow (press the
82
- // row = the sequenced workspace Drawer) · a select-all band · footer totals +
83
- // Pagination · a FloatingActionBar for the bulk action over the ticked rows. The
83
+ // row = the sequenced workspace Drawer), a select-all band, footer totals +
84
+ // Pagination, a FloatingActionBar for the bulk action over the ticked rows. The
84
85
  // example is a documents-workflow register, but this one shape subsumes the old
85
86
  // approvals / dispatch / batch / run screens — register, per-row action, gated
86
87
  // selection, and act-on-many all in one.
@@ -212,7 +213,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
212
213
  </TaskStatus>
213
214
  <TaskTitle>
214
215
  <InlineTextInput
215
- variant="cell"
216
+ variant="bare"
216
217
  value={t.label}
217
218
  onSave={(v) => patch(t.id, { label: v })}
218
219
  struck={t.done}
@@ -234,7 +235,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
234
235
  value={t.assignee}
235
236
  onSave={(m) => patch(t.id, { assignee: m })}
236
237
  placeholder="Assign…"
237
- accessibilityLabel={`Assignee · ${t.label}`}
238
+ accessibilityLabel={`Assignee for ${t.label}`}
238
239
  />
239
240
  </TaskSubRow>
240
241
  <TaskSubRow label="Due">
@@ -242,7 +243,7 @@ function TasksChecklist({ tasks, onChange, suggestions, onDismissSuggestion }: {
242
243
  value={t.due}
243
244
  onSave={(v) => patch(t.id, { due: v })}
244
245
  placeholder="Set due…"
245
- accessibilityLabel={`Due · ${t.label}`}
246
+ accessibilityLabel={`Due for ${t.label}`}
246
247
  />
247
248
  </TaskSubRow>
248
249
  </TaskItem>
@@ -439,7 +440,7 @@ function HoSoRow({ hs, daThu, tasks, onTasksChange, suggestions, onDismissSugges
439
440
  }
440
441
 
441
442
  // The case workspace behind a register row — the drawer mirrors a REAL record
442
- // workspace: inline-editable fields (the zinc-50 chips; `InlineStatic` for the
443
+ // workspace: inline-editable fields (the bordered control surface; `InlineStatic` for the
443
444
  // system-owned ones) and the record's CHARGES SUMMARY (grouped fee rows with
444
445
  // the status as a quiet suffix → divider → ONE emphasized closing number — the
445
446
  // same shape at popover density). Keyed by id from the caller so per-record
@@ -473,12 +474,12 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
473
474
  // (bar, group sums, the closing number) recomputes from the two lists.
474
475
  const charges = isSeed
475
476
  ? [
476
- { id: "f1", item: "Service fee", amount: hs.phi, basis: "Fixed tariff · type A", chargedOn: "03/06", by: "Sarah Chen", ref: "" },
477
+ { id: "f1", item: "Service fee", amount: hs.phi, basis: "Fixed tariff, type A", chargedOn: "03/06", by: "Sarah Chen", ref: "" },
477
478
  { id: "f2", item: "Notarization", amount: 350_000, basis: "Per notarized copy × 2", chargedOn: "04/06", by: "David Park", ref: "" },
478
479
  { id: "f3", item: "Government levy", amount: 1_200_000, basis: "State fee schedule 2026", chargedOn: "04/06", by: "System", ref: "LEVY-2026-118" },
479
480
  ]
480
481
  : hs.phi > 0
481
- ? [{ id: "f1", item: "Service fee", amount: hs.phi, basis: "Fixed tariff · type A", chargedOn: "Today", by: "Sarah Chen", ref: "" }]
482
+ ? [{ id: "f1", item: "Service fee", amount: hs.phi, basis: "Fixed tariff, type A", chargedOn: "Today", by: "Sarah Chen", ref: "" }]
482
483
  : [];
483
484
  const [receipts, setReceipts] = useState(() =>
484
485
  isSeed
@@ -542,11 +543,11 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
542
543
  const [logNote, setLogNote] = useState("");
543
544
  const [followUp, setFollowUp] = useState<string | null>(null);
544
545
  const [events, setEvents] = useState<TimelineItem[]>(() => isSeed ? [
545
- { id: "e1", icon: "phone", iconColor: colors.amber[500], label: "Call — call back", description: "Asked for the signed contract · follow up 08/06", right: <Text size="xs" color="muted">04/06</Text> },
546
- { id: "e2", icon: "credit-card", iconColor: colors.emerald[500], label: "Payment received — 1.550.000 ₫", description: "Cash · disbursements advance", right: <Text size="xs" color="muted">03/06</Text> },
546
+ { id: "e1", icon: "phone", iconColor: colors.amber[500], label: "Call — call back", description: "Asked for the signed contract; follow up 08/06", right: <Text size="xs" color="muted">04/06</Text> },
547
+ { id: "e2", icon: "credit-card", iconColor: colors.emerald[500], label: "Payment received — 1.550.000 ₫", description: "Cash for disbursements advance", right: <Text size="xs" color="muted">03/06</Text> },
547
548
  { id: "e3", icon: "phone", iconColor: colors.zinc[400], label: "Call — no answer", right: <Text size="xs" color="muted">03/06</Text> },
548
549
  { id: "e4", icon: "file-text", iconColor: colors.zinc[500], label: "Receipt printed", right: <Text size="xs" color="muted">03/06</Text> },
549
- { id: "e5", icon: "phone", iconColor: colors.emerald[500], label: "Call — connected", description: "Walked through the required documents · 1 file", right: <Text size="xs" color="muted">02/06</Text>, details: attachmentDetails([sampleDoc("checklist", "#8b5cf6")]) },
550
+ { id: "e5", icon: "phone", iconColor: colors.emerald[500], label: "Call — connected", description: "Walked through the required documents; 1 file", right: <Text size="xs" color="muted">02/06</Text>, details: attachmentDetails([sampleDoc("checklist", "#8b5cf6")]) },
550
551
  { id: "e6", icon: "plus", iconColor: colors.blue[500], label: "Record created", right: <Text size="xs" color="muted">02/06</Text> },
551
552
  ] : [
552
553
  { id: "e1", icon: "plus", iconColor: colors.blue[500], label: "Record created", right: <Text size="xs" color="muted">Today</Text> },
@@ -572,7 +573,7 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
572
573
  icon: meta ? meta.icon : "message-square",
573
574
  iconColor: meta ? meta.color : colors.blue[500],
574
575
  label: meta ? meta.label : note,
575
- description: parts.join(" · ") || undefined,
576
+ description: parts.join("; ") || undefined,
576
577
  right: <Text size="xs" color="muted">Today</Text>,
577
578
  details: docs.length > 0 ? attachmentDetails(docs) : undefined,
578
579
  },
@@ -591,28 +592,49 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
591
592
  <Text size="lg" weight="semibold" tabular>{hs.ma}</Text>
592
593
  <Badge label={TRANG_THAI[hs.trangThai].label} color={TRANG_THAI[hs.trangThai].color} />
593
594
  </View>
594
- <Text size="sm" color="muted">{`Received ${hs.ngayNhan} · ${hs.phuTrach}`}</Text>
595
+ <Text size="sm" color="muted">{`Received ${hs.ngayNhan} by ${hs.phuTrach}`}</Text>
595
596
  </View>
596
597
  <Divider />
597
598
  {/* DETAILS — every field type in the inline vocabulary, on ONE
598
- DetailTable grid: text · member · dates (plain + optional-time) ·
599
- select-with-dots · money number · a multi-value TAG field · a
599
+ DetailTable grid: text, member, dates (plain + optional-time),
600
+ select-with-dots, money number, a multi-value TAG field, a
600
601
  system-owned static. The chips mark what's editable. */}
601
602
  <Section>
602
603
  <SectionHeading>
603
604
  <SectionHeadingTitle>Details</SectionHeadingTitle>
604
605
  </SectionHeading>
605
- <DetailTable labelWidth={120} trailingWidth={76}>
606
+ <DetailTable labelWidth={120}>
606
607
  <DetailRow label="Customer"><InlineTextInput value={khach} onSave={persist(setKhach)} accessibilityLabel="Customer" /></DetailRow>
607
- <DetailRow label="Phone" trailing={<Button title="Call" color="secondary" icon="phone" onPress={() => {}} />}>
608
- <InlineTextInput value={dienThoai} onSave={persist(setDienThoai)} accessibilityLabel="Phone" />
608
+ {/* A verb about the VALUE rides the FIELD — `InlineButton` in the
609
+ editor's `actions`, never a column beside it. Passed
610
+ unconditionally and disabled when there is nothing to act on: a
611
+ verb that appears once the value is filled resizes the field as
612
+ the user types. */}
613
+ <DetailRow label="Phone">
614
+ <InlineTextInput
615
+ value={dienThoai}
616
+ onSave={persist(setDienThoai)}
617
+ accessibilityLabel="Phone"
618
+ actions={<InlineButton title="Call" accessibilityLabel="Call this number" disabled={!dienThoai} onPress={() => {}} />}
619
+ />
609
620
  </DetailRow>
610
621
  <DetailRow label="Assignee">
611
622
  <InlineMemberSelect members={MEMBERS} value={assignee} onSave={persist(setAssignee)} placeholder="Assign…" accessibilityLabel="Assignee" />
612
623
  </DetailRow>
613
624
  <DetailRow label="Received"><InlineDatePicker value={ngayNhan} onSave={persist(setNgayNhan)} accessibilityLabel="Received date" /></DetailRow>
614
- <DetailRow label="Due" trailing={<Button title="Today" color="secondary" onPress={() => void persist(setDue)(toISODate(new Date()))} />}>
615
- <InlineDatePicker value={due} optionalTime onSave={persist(setDue)} placeholder="Set due date…" accessibilityLabel="Due date" />
625
+ {/* The verb rides the FIELD, like every other value verb — including
626
+ on a popover editor: the field exposes its outer box separately
627
+ from the control inside it, so the calendar still opens at the
628
+ field's own width. */}
629
+ <DetailRow label="Due">
630
+ <InlineDatePicker
631
+ value={due}
632
+ optionalTime
633
+ onSave={persist(setDue)}
634
+ placeholder="Set due date…"
635
+ accessibilityLabel="Due date"
636
+ actions={<InlineButton title="Today" accessibilityLabel="Set due date to today" onPress={() => void persist(setDue)(toISODate(new Date()))} />}
637
+ />
616
638
  </DetailRow>
617
639
  <DetailRow label="Priority">
618
640
  <InlineSelect
@@ -642,8 +664,13 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
642
664
  accessibilityLabel="Tags"
643
665
  />
644
666
  </DetailRow>
645
- <DetailRow label="Record ID" trailing={<Badge label="System" color="zinc" />}>
646
- <InlineStatic value={hs.ma} tabular />
667
+ {/* A `Badge` is not a verb and never was a column — it qualifies the
668
+ VALUE, so it sits beside it inside the value cell. */}
669
+ <DetailRow label="Record ID" flat>
670
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
671
+ <InlineStatic value={hs.ma} tabular />
672
+ <Badge label="System" color="zinc" />
673
+ </View>
647
674
  </DetailRow>
648
675
  </DetailTable>
649
676
  </Section>
@@ -727,7 +754,7 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
727
754
  </FileDropTarget>
728
755
  <Divider />
729
756
  {/* the PAYMENT summary — a plain financial STATEMENT: the two sides
730
- of the ledger (charges owed · receipts received), every amount on
757
+ of the ledger (charges owed, receipts received), every amount on
731
758
  ONE right-aligned tabular column, a divider-set emphasized closing
732
759
  number, and the record-payment popover that appends a receipt.
733
760
  No bars, no charts — the numbers are the interface. */}
@@ -751,7 +778,7 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
751
778
  <View style={{ gap: 8 }}>
752
779
  <View style={{ gap: 2 }}>
753
780
  <Text size="sm" weight="semibold">{c.item}</Text>
754
- <Text size="xs" color="muted">{`Charged ${c.chargedOn} · by ${c.by}`}</Text>
781
+ <Text size="xs" color="muted">{`Charged ${c.chargedOn} by ${c.by}`}</Text>
755
782
  </View>
756
783
  <DetailRow label="Basis" labelSize="xs" labelWidth={72}><Text size="xs">{c.basis}</Text></DetailRow>
757
784
  <DetailRow label="Amount" labelSize="xs" labelWidth={72}><Text size="xs" tabular>{formatMoney(c.amount)}</Text></DetailRow>
@@ -767,7 +794,7 @@ function HoSoWorkspace({ hs, daThu, onPaid, tasks, onTasksChange, suggestions, o
767
794
  </LedgerGroup>
768
795
  <LedgerGroup label="Received" total={received > 0 ? -received : undefined}>
769
796
  {receipts.map((r) => (
770
- <LedgerRow key={r.id} label={r.note} meta={`${r.date} · ${r.method}`} value={-r.amount} tone="success" />
797
+ <LedgerRow key={r.id} label={r.note} meta={`${r.date}, ${r.method}`} value={-r.amount} tone="success" />
771
798
  ))}
772
799
  </LedgerGroup>
773
800
  <LedgerTotal label="Outstanding" value={outstanding} tone="danger" zeroLabel="Paid in full" />
@@ -1009,7 +1036,7 @@ function LinkedRecordScreen({ ma }: { ma: string }) {
1009
1036
  <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 24, gap: 16 }}>
1010
1037
  <View style={{ flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
1011
1038
  <Badge label={TRANG_THAI[r.trangThai].label} color={TRANG_THAI[r.trangThai].color} />
1012
- <Text size="sm" color="muted">{`Linked record · ${r.khach}`}</Text>
1039
+ <Text size="sm" color="muted">{`Linked record: ${r.khach}`}</Text>
1013
1040
  </View>
1014
1041
  <DetailTable labelWidth={120}>
1015
1042
  <DetailRow label="Customer"><InlineTextInput value={khach} onSave={persist(setKhach)} accessibilityLabel="Customer" /></DetailRow>
@@ -1073,12 +1100,12 @@ type Proposal = { id: string; khach: string; dienThoai: string; phi: number; ord
1073
1100
  // What the (mock) import drafts, per the wizard's grouping answer. A real app
1074
1101
  // takes these from the import run's structured output.
1075
1102
  const PROPOSED_BY_CUSTOMER: Proposal[] = [
1076
- { id: "pc1", khach: "Meridian Trading Co.", dienThoai: "(555) 014-2288", phi: 240, orders: "PO-7311 · PO-7314" },
1077
- { id: "pc2", khach: "Northgate Textiles", dienThoai: "(555) 019-4471", phi: 180, orders: "PO-7312 · PO-7316" },
1078
- { id: "pc3", khach: "Blue Harbor Foods", dienThoai: "", phi: 210, orders: "PO-7313 · PO-7315" },
1103
+ { id: "pc1", khach: "Meridian Trading Co.", dienThoai: "(555) 014-2288", phi: 240, orders: "PO-7311, PO-7314" },
1104
+ { id: "pc2", khach: "Northgate Textiles", dienThoai: "(555) 019-4471", phi: 180, orders: "PO-7312, PO-7316" },
1105
+ { id: "pc3", khach: "Blue Harbor Foods", dienThoai: "", phi: 210, orders: "PO-7313, PO-7315" },
1079
1106
  ];
1080
1107
  const PROPOSED_BY_ORDER: Proposal[] = PROPOSED_BY_CUSTOMER.flatMap((c) =>
1081
- c.orders.split(" · ").map((po, i) => ({ id: `${c.id}-${i}`, khach: c.khach, dienThoai: c.dienThoai, phi: Math.round(c.phi / 2), orders: po })),
1108
+ c.orders.split(", ").map((po, i) => ({ id: `${c.id}-${i}`, khach: c.khach, dienThoai: c.dienThoai, phi: Math.round(c.phi / 2), orders: po })),
1082
1109
  );
1083
1110
 
1084
1111
  // The wizard's questions come FROM the analysis (a real app renders them off the
@@ -1223,7 +1250,7 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
1223
1250
  // The wizard's answers are load-bearing: the unknown customer's row
1224
1251
  // states how it landed (created + linked / left unassigned / the
1225
1252
  // user's own instruction).
1226
- detail: [p.dienThoai, formatMoney(p.phi), p.khach === "Blue Harbor Foods" ? customerPlan : ""].filter(Boolean).join(" · "),
1253
+ detail: [p.dienThoai, formatMoney(p.phi), p.khach === "Blue Harbor Foods" ? customerPlan : ""].filter(Boolean).join(", "),
1227
1254
  ref: p.orders,
1228
1255
  // The mock flags one record's reference (a failed checksum); a
1229
1256
  // real app computes this over the STORED value.
@@ -1445,7 +1472,7 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
1445
1472
  <Icon name={CUSTOMER_OPTIONS.some((o) => o.label === khach) ? "building-2" : "plus"} size={16} color={colors.zinc[400]} />
1446
1473
  <Text size="sm" weight="medium" style={{ flex: 1 }} numberOfLines={1}>
1447
1474
  {khach}
1448
- {CUSTOMER_OPTIONS.some((o) => o.label === khach) ? "" : " · new customer"}
1475
+ {CUSTOMER_OPTIONS.some((o) => o.label === khach) ? "" : " (new customer)"}
1449
1476
  </Text>
1450
1477
  <Link size="sm" onPress={() => setKhach("")} accessibilityLabel="Change customer">Change</Link>
1451
1478
  </View>
@@ -1657,7 +1684,7 @@ export function TplItemList() {
1657
1684
  <Select
1658
1685
  placeholder="Status"
1659
1686
  options={TABS.map((t) => ({
1660
- label: `${t.label} · ${t.value === "all" ? preTab.length : dem(t.value)}`,
1687
+ label: `${t.label} (${t.value === "all" ? preTab.length : dem(t.value)})`,
1661
1688
  value: t.value,
1662
1689
  }))}
1663
1690
  value={tab}
@@ -1750,7 +1777,7 @@ export function TplItemList() {
1750
1777
  ) : null}
1751
1778
  <CardFooter>
1752
1779
  <Text size="sm" color="muted" tabular style={{ flex: 1 }}>
1753
- {`${filtered.length} records · Total fees ${formatMoney(tongPhi)}`}
1780
+ {`${filtered.length} records, ${formatMoney(tongPhi)} in fees`}
1754
1781
  </Text>
1755
1782
  <Pagination
1756
1783
  page={page}
@@ -15,7 +15,7 @@ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
15
15
  type Part = UIMessagePart<UIDataTypes, UITools>;
16
16
 
17
17
  // ─────────────────────────────────────────────────────────────────────────────
18
- // Template · Answer desk — describe the goods on the LEFT, the agent RANKS the
18
+ // Template, Answer desk — describe the goods on the LEFT, the agent RANKS the
19
19
  // matching codes (nearest matches, the top one Recommended), you pick one and its
20
20
  // structured ANSWER pins on the RIGHT. The flow is input → matches → pick, not a
21
21
  // single confident verdict: classification is ambiguous, so the alternatives are
@@ -52,7 +52,7 @@ interface Candidate {
52
52
  const CANDIDATES: Candidate[] = [
53
53
  {
54
54
  id: "c1",
55
- label: "Frozen shrimp · retail packs ≤ 1 kg",
55
+ label: "Frozen shrimp, retail packs ≤ 1 kg",
56
56
  recommended: true,
57
57
  code: "0306.17.10",
58
58
  heading: "Frozen shrimp & prawns of the family Penaeidae — not in airtight containers",
@@ -83,7 +83,7 @@ const CANDIDATES: Candidate[] = [
83
83
  },
84
84
  {
85
85
  id: "c2",
86
- label: "Frozen shrimp · other (bulk / catering)",
86
+ label: "Frozen shrimp, other (bulk / catering)",
87
87
  code: "0306.17.90",
88
88
  heading: "Frozen shrimp & prawns of the family Penaeidae — other",
89
89
  confidence: "medium",
@@ -195,7 +195,7 @@ export function TplLookup() {
195
195
 
196
196
  return (
197
197
  <View style={{ flex: 1, flexDirection: "row", backgroundColor: colors.white }}>
198
- {/* ── LEFT · command + session ───────────────────────────────── */}
198
+ {/* ── LEFT, command + session ───────────────────────────────── */}
199
199
  <View style={{ width: 380, backgroundColor: colors.white, borderRightWidth: 1, borderRightColor: colors.zinc[200] }}>
200
200
  <View style={{ paddingHorizontal: 20, paddingTop: 18, paddingBottom: 12, gap: 2 }}>
201
201
  <Text size="md" weight="semibold">HS classification</Text>
@@ -248,7 +248,7 @@ export function TplLookup() {
248
248
  </View>
249
249
  </View>
250
250
 
251
- {/* ── RIGHT · the pinned structured answer ───────────────────── */}
251
+ {/* ── RIGHT, the pinned structured answer ───────────────────── */}
252
252
  <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 28, paddingBottom: 48 }}>
253
253
  <View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
254
254
  <View style={{ gap: 4 }}>
@@ -15,7 +15,7 @@ import { Stepper, Step, type StepStatus } from "@lotics/ui/stepper";
15
15
  import { CompletionState } from "@lotics/ui/completion_state";
16
16
 
17
17
  // ─────────────────────────────────────────────────────────────────────────────
18
- // Template · Guided queue — a warehouse PICK RUN. The work is a SEQUENCE of
18
+ // Template, Guided queue — a warehouse PICK RUN. The work is a SEQUENCE of
19
19
  // physical tasks, not a list to browse: the run hands the operator the next bin,
20
20
  // they scan to verify they're at the right shelf, confirm the count, and the
21
21
  // next task takes its place. A single focus column — the CURRENT task is the only
@@ -68,7 +68,7 @@ function ShortGate({ line, onShort }: { line: PickLine; onShort: (actual: number
68
68
  <PopoverContent style={{ width: 300 }} disableBodyScroll>
69
69
  <View style={{ gap: 14 }}>
70
70
  <View style={{ gap: 2 }}>
71
- <Text size="sm" weight="semibold">{`Short pick · ${line.bin}`}</Text>
71
+ <Text size="sm" weight="semibold">{`Short pick: ${line.bin}`}</Text>
72
72
  <Text size="xs" color="muted">Record what you actually took — the rest is flagged for backfill.</Text>
73
73
  </View>
74
74
  <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
@@ -142,7 +142,7 @@ function Complete({ lines, onReset }: { lines: PickLine[]; onReset: () => void }
142
142
  return (
143
143
  <CompletionState
144
144
  title="Wave picked"
145
- summary={`${lines.length} lines · ${units} units${shorts > 0 ? ` · ${shorts} short` : ""}`}
145
+ summary={`${lines.length} lines, ${units} units${shorts > 0 ? `, ${shorts} short` : ""}`}
146
146
  >
147
147
  <Button title="Start next wave" color="muted" onPress={onReset} />
148
148
  <Button title="Hand to packing" color="primary" onPress={() => {}} />
@@ -150,7 +150,7 @@ function Complete({ lines, onReset }: { lines: PickLine[]; onReset: () => void }
150
150
  );
151
151
  }
152
152
 
153
- // The whole run at a glance: done · now · up next. Read-only — the run is
153
+ // The whole run at a glance: done, now, up next. Read-only — the run is
154
154
  // sequential, so position is shown, not chosen. Lives in a collapsed disclosure
155
155
  // below the task so the current step keeps the focus.
156
156
  function RunPath({ lines, currentIndex }: { lines: PickLine[]; currentIndex: number }) {
@@ -200,7 +200,7 @@ export function TplPick() {
200
200
  <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12, flexWrap: "wrap" }}>
201
201
  <View style={{ gap: 2, flex: 1 }}>
202
202
  <Text size="xxl" weight="semibold">Queue W-0142</Text>
203
- <Text size="sm" color="muted">7 lines for 3 outbound orders · Cart C-07</Text>
203
+ <Text size="sm" color="muted">7 lines for 3 outbound orders on Cart C-07</Text>
204
204
  </View>
205
205
  <Button title="Pause wave" color="muted" onPress={() => {}} />
206
206
  </View>
@@ -208,7 +208,7 @@ export function TplPick() {
208
208
  {/* run progress — the one number that says how close to done */}
209
209
  <View style={{ gap: 6 }}>
210
210
  <View style={{ flexDirection: "row", alignItems: "center" }}>
211
- <Text size="sm" color="muted" style={{ flex: 1 }}>{`${done.length} of ${lines.length} lines · ${pickedUnits} of ${totalUnits} units`}</Text>
211
+ <Text size="sm" color="muted" style={{ flex: 1 }}>{`${done.length} of ${lines.length} lines, ${pickedUnits} of ${totalUnits} units`}</Text>
212
212
  {shorts > 0 ? <Text size="sm" color="danger" tabular>{`${shorts} short`}</Text> : null}
213
213
  </View>
214
214
  <ProgressBar value={done.length} max={lines.length} format="none" color={solid("blue")} />
@@ -14,7 +14,7 @@ import { type MatrixCellRef } from "@lotics/ui/matrix_totals";
14
14
  import { PressableRow } from "@lotics/ui/pressable_row";
15
15
 
16
16
  // ─────────────────────────────────────────────────────────────────────────────
17
- // Template · Pivot — one dimension crossed against another, then drill.
17
+ // Template, Pivot — one dimension crossed against another, then drill.
18
18
  // The Matrix is the hero: warehouse × category, the number IN each cell and a
19
19
  // heat wash behind it so the concentration reads at a glance, row/column/grand
20
20
  // totals down the edges. Pressing a cell is a door — the register below filters
@@ -61,7 +61,7 @@ const NAMES = ["Cotton tee", "Wool throw", "USB hub", "Trail flask", "Rain shell
61
61
 
62
62
  const SKUS: Sku[] = (() => {
63
63
  const out: Sku[] = [];
64
- // A deliberate concentration in Newark · Electronics so a hot cell has a story.
64
+ // A deliberate concentration in Newark, Electronics so a hot cell has a story.
65
65
  for (let i = 0; i < 64; i++) {
66
66
  const wRoll = hash(i, 1) % 100;
67
67
  const warehouse: WarehouseKey =
@@ -172,7 +172,7 @@ export function TplPivot() {
172
172
  </CardHeaderTitle>
173
173
  {cell ? (
174
174
  <Chip onDismiss={() => { setCell(null); setOpenIdx(null); }} dismissTooltip="Clear cell">
175
- <Text size="xs" weight="medium" color="muted">{`${whOf(cell.row as WarehouseKey).label} · ${catOf(cell.col as CategoryKey).label}`}</Text>
175
+ <Text size="xs" weight="medium" color="muted">{`${whOf(cell.row as WarehouseKey).label}, ${catOf(cell.col as CategoryKey).label}`}</Text>
176
176
  </Chip>
177
177
  ) : null}
178
178
  </CardHeader>