@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.
- package/AGENTS.md +9 -2
- package/MIGRATION.md +142 -2
- package/docs/ai_patterns.md +22 -22
- package/docs/catalog.md +119 -98
- package/docs/composition.md +134 -42
- package/docs/data_entry.md +67 -31
- package/docs/templates.md +86 -68
- package/examples/tpl_allocate.tsx +5 -5
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +6 -6
- package/examples/tpl_dashboard.tsx +9 -9
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_item_list.tsx +64 -37
- package/examples/tpl_lookup.tsx +5 -5
- package/examples/tpl_pick.tsx +6 -6
- package/examples/tpl_pivot.tsx +3 -3
- package/examples/tpl_record.tsx +919 -611
- package/examples/tpl_report.tsx +2 -2
- package/examples/tpl_rollup.tsx +5 -5
- package/examples/tpl_shifts.tsx +5 -5
- package/examples/tpl_statements.tsx +7 -7
- package/examples/tpl_stock.tsx +2 -2
- package/examples/tpl_task_board.tsx +42 -26
- package/examples/tpl_tower.tsx +6 -6
- package/package.json +3 -2
- package/src/agent_run.tsx +4 -4
- package/src/breakdown.tsx +1 -1
- package/src/calendar/calendar_view.tsx +1 -1
- package/src/change_review.tsx +9 -8
- package/src/chip_group.tsx +12 -2
- package/src/choice_list.tsx +2 -2
- package/src/confidence.tsx +2 -2
- package/src/data_grid.tsx +1 -1
- package/src/detail_row.tsx +50 -58
- package/src/file_dropzone.tsx +1 -1
- package/src/file_gallery_modal.tsx +3 -3
- package/src/file_rows.tsx +1 -1
- package/src/finding.tsx +4 -4
- package/src/form_field.tsx +1 -1
- package/src/format_date.ts +2 -2
- package/src/heatmap.tsx +1 -1
- package/src/inline_button.tsx +84 -0
- package/src/inline_date_picker.tsx +17 -10
- package/src/inline_edit.tsx +298 -59
- package/src/inline_member_select.tsx +8 -3
- package/src/inline_number_input.tsx +11 -4
- package/src/inline_select.tsx +26 -13
- package/src/inline_text_input.tsx +12 -4
- package/src/inline_time_picker.tsx +10 -4
- package/src/ledger.tsx +2 -2
- package/src/locale.tsx +7 -3
- package/src/matrix.tsx +1 -1
- package/src/number_input.tsx +18 -7
- package/src/pipeline.tsx +1 -1
- package/src/popover.tsx +1 -1
- package/src/press_door.tsx +1 -1
- package/src/pressable_highlight.tsx +1 -1
- package/src/progress_bar.tsx +3 -3
- package/src/record_summary.tsx +2 -2
- package/src/result_header.tsx +2 -2
- package/src/sequence.tsx +170 -0
- package/src/share_or_download.ts +2 -2
- package/src/step_progress.tsx +7 -5
- package/src/stepper.tsx +1 -1
- package/src/task.tsx +6 -6
- package/src/text_input_field.tsx +19 -1
- package/src/text_utils.ts +1 -1
- 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
|
|
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)
|
|
79
|
-
// view
|
|
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)
|
|
83
|
-
// Pagination
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
546
|
-
{ id: "e2", icon: "credit-card", iconColor: colors.emerald[500], label: "Payment received — 1.550.000 ₫", description: "Cash
|
|
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
|
|
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("
|
|
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}
|
|
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
|
|
599
|
-
select-with-dots
|
|
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}
|
|
606
|
+
<DetailTable labelWidth={120}>
|
|
606
607
|
<DetailRow label="Customer"><InlineTextInput value={khach} onSave={persist(setKhach)} accessibilityLabel="Customer" /></DetailRow>
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
1077
|
-
{ id: "pc2", khach: "Northgate Textiles", dienThoai: "(555) 019-4471", phi: 180, orders: "PO-7312
|
|
1078
|
-
{ id: "pc3", khach: "Blue Harbor Foods", dienThoai: "", phi: 210, orders: "PO-7313
|
|
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("
|
|
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) ? "" : "
|
|
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}
|
|
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
|
|
1780
|
+
{`${filtered.length} records, ${formatMoney(tongPhi)} in fees`}
|
|
1754
1781
|
</Text>
|
|
1755
1782
|
<Pagination
|
|
1756
1783
|
page={page}
|
package/examples/tpl_lookup.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
|
15
15
|
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
16
16
|
|
|
17
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
-
// Template
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 }}>
|
package/examples/tpl_pick.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")} />
|
package/examples/tpl_pivot.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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}
|
|
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>
|