@lotics/ui 7.8.0 → 7.9.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 +71 -32
- package/examples/tpl_allocate.tsx +6 -6
- package/examples/tpl_assistant.tsx +1 -1
- package/examples/tpl_attendance.tsx +1 -1
- package/examples/tpl_billing.tsx +2 -2
- package/examples/tpl_briefing.tsx +1 -1
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_compare.tsx +1 -1
- package/examples/tpl_crosscheck.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_detail.tsx +1 -1
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_directory.tsx +5 -8
- package/examples/tpl_draft.tsx +2 -2
- package/examples/tpl_extract.tsx +1 -1
- package/examples/tpl_intake.tsx +1 -1
- package/examples/{tpl_dossier.tsx → tpl_item_list.tsx} +138 -97
- package/examples/tpl_lookup.tsx +2 -2
- package/examples/tpl_match.tsx +2 -2
- package/examples/tpl_order.tsx +1 -1
- package/examples/tpl_pick.tsx +64 -68
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_quick.tsx +1 -1
- package/examples/tpl_ratedesk.tsx +1 -1
- package/examples/tpl_record.tsx +1 -1
- package/examples/tpl_record_plain.tsx +1 -1
- package/examples/tpl_report.tsx +1 -1
- package/examples/tpl_rollup.tsx +1 -1
- package/examples/tpl_settings.tsx +1 -1
- package/examples/tpl_shifts.tsx +1 -1
- package/examples/tpl_stock.tsx +1 -1
- package/examples/tpl_task_board.tsx +2 -2
- package/examples/tpl_tasks.tsx +1 -1
- package/examples/tpl_timeline.tsx +1 -1
- package/examples/tpl_tower.tsx +1 -1
- package/examples/tpl_triage.tsx +1 -1
- package/examples/tpl_wizard.tsx +1 -1
- package/package.json +3 -1
- package/src/card.tsx +6 -13
- package/src/data_grid.tsx +1 -1
- package/src/kpi_strip.tsx +5 -5
- package/src/pressable_row.tsx +22 -10
- package/src/sort_header.tsx +9 -4
- package/src/summary_line.tsx +76 -0
- package/src/table.tsx +27 -11
- package/src/use_selection.test.ts +66 -0
- package/src/use_selection.ts +75 -0
- package/examples/app_orders.tsx +0 -405
- package/examples/tpl_approvals.tsx +0 -375
- package/examples/tpl_batch.tsx +0 -234
- package/examples/tpl_callsheet.tsx +0 -481
- package/examples/tpl_convert.tsx +0 -478
- package/examples/tpl_crm_desk.tsx +0 -541
- package/examples/tpl_dispatch.tsx +0 -289
- package/examples/tpl_inventory.tsx +0 -299
- package/examples/tpl_pipeline.tsx +0 -400
- package/examples/tpl_reconcile.tsx +0 -275
- package/examples/tpl_run.tsx +0 -235
package/AGENTS.md
CHANGED
|
@@ -80,7 +80,7 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
80
80
|
- **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
|
|
81
81
|
headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer` to edit.
|
|
82
82
|
It scales by PAGING — renders one page, never the whole set — so you FILTER + search, you don't
|
|
83
|
-
group. Worked example: `
|
|
83
|
+
group. Worked example: `tpl_item_list`. Never an HTML `<table>` or a `.map` of rows.
|
|
84
84
|
- **Inline-managed grouped table** (MODERATE — hundreds, low-thousands — you MANAGE in view) — the
|
|
85
85
|
`DataGrid` primitive: a grouped, sortable grid whose cells are LIVE inline editors (ANY field — a
|
|
86
86
|
column is `{ key, label, width?, sortable?, cell: (item) => ReactNode }`, so `InlineMemberSelect` /
|
|
@@ -91,8 +91,9 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
91
91
|
per-group add row (`renderGroupFooter`, aligned with the exported `gridRowStyle` + the column
|
|
92
92
|
widths). It renders ALL rows (no virtualization), so it's only for sets small enough to hold in
|
|
93
93
|
view — at 10k+ it lags, and grouping + pagination/infinite don't compose; use the register instead.
|
|
94
|
-
Worked
|
|
95
|
-
- **Numbers & charts** — `KPIStrip`
|
|
94
|
+
Worked example: `tpl_task_board` (a `CheckCircle` leading).
|
|
95
|
+
- **Numbers & charts** — `KPIStrip` (the dashboard stat band) · `SummaryLine` (the light inline
|
|
96
|
+
register/list summary — below the toolbar, from the filtered rows) · `KPICard` / `Metric` (headline figures), `TrendChip`
|
|
96
97
|
(delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
|
|
97
98
|
recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
|
|
98
99
|
(a stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
|
|
@@ -489,9 +490,9 @@ decisions, or a ranked set, don't cram it into chat prose. Each pairs a template
|
|
|
489
490
|
- **Match** (`tpl_match`, body-first `ReviewCard` + `MatchSides`) — the agent proposes PAIRINGS with
|
|
490
491
|
its reasoning; each is a `ReviewCard` (no title, so body-first) whose body is a two-sided `MatchSides`
|
|
491
492
|
pairing (source → proposed counterpart), with the confidence + reason beneath, accept / reassign /
|
|
492
|
-
reject, high-confidence batch-accept. The AI reconciliation / dedup / correlation queue. (
|
|
493
|
-
deterministic
|
|
494
|
-
|
|
493
|
+
reject, high-confidence batch-accept. The AI reconciliation / dedup / correlation queue. (Strip the agent's
|
|
494
|
+
reasoning and it would be a plain deterministic match register — the reasoning is the tell for when
|
|
495
|
+
you need this.)
|
|
495
496
|
- **Briefing** (`tpl_briefing`, `Finding`) — a generated narrative over the data + a RANKED list of
|
|
496
497
|
what needs attention, each finding carrying a severity, a metric, its sources, and one action. The
|
|
497
498
|
leadership digest / anomaly scan — the agent's COMMENTARY, not a raw dashboard.
|
|
@@ -616,10 +617,21 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
616
617
|
</Card>
|
|
617
618
|
```
|
|
618
619
|
Never nest cards; never hand-compose a title or footer band. `info` is standard, not garnish.
|
|
619
|
-
- **
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
620
|
+
- **Flat work execution vs carded monitoring.** A `Card` is ALWAYS a bordered, lifted container —
|
|
621
|
+
there is no "flat card". MONITORING / dashboard screens group with cards (and the `KPIStrip`
|
|
622
|
+
band). WORK-EXECUTION screens — registers, lists, the worklists an operator acts on — are FLAT:
|
|
623
|
+
the register/list sits DIRECTLY on the white canvas with no Card wrapper; separation comes from
|
|
624
|
+
the row wash, gaps, and `Divider`/`CardHeader`/`CardFooter` bands used BARE. Reach for a Card only
|
|
625
|
+
where a screen holds a DISTINCT region that must be told apart from another; a single register or
|
|
626
|
+
list filling the screen needs none.
|
|
627
|
+
- **Summaries — two altitudes, never mixed.** A DASHBOARD opens with the boxed stat band
|
|
628
|
+
`<KPIStrip items={[{label, value, trend?, caption?, tone?, info?}…]}>` (a `Card` of `KPICard`
|
|
629
|
+
columns). A REGISTER/LIST (work-execution) screen instead carries a LIGHT inline
|
|
630
|
+
`<SummaryLine items={[{label, value, tone?, info?}…]}>` BELOW its toolbar — a small `Metric` + a
|
|
631
|
+
muted label per item, recomputed from the FILTERED rows so it always describes WHAT'S SHOWN. Both
|
|
632
|
+
are INFORMATIONAL — they never filter or navigate (that's the tabs/chips' job); if there are no
|
|
633
|
+
cross-cutting numbers, drop them. Do NOT put a `KPIStrip` on a register page or a `SummaryLine` on
|
|
634
|
+
a dashboard. Card stat rails use `KPICard`.
|
|
623
635
|
- **Numbers**: free-standing numerals `<Text tabular>`. Money: `formatMoney(n)` from
|
|
624
636
|
`@lotics/ui/format_money` (`compact` for strip captions). Dates: `formatDate(value, opts)` from
|
|
625
637
|
`@lotics/ui/format_date` — the date-VALUE formatter. `format` (date STYLE) ∈ `date` (22/05/2026) ·
|
|
@@ -642,13 +654,22 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
642
654
|
`Drawer` (sequenced); read-only drill-downs expand via `Accordion` or glance via `Peek`; every
|
|
643
655
|
other row gets an `ActionMenu` (⋯ → MenuButton items: destructive last + **confirmed** via
|
|
644
656
|
`Alert`). The whole surface is the door: register rows are `PressableRow` (full-bleed wash incl.
|
|
645
|
-
nested controls)
|
|
646
|
-
· **Register (default)** —
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
657
|
+
nested controls), Divider-separated:
|
|
658
|
+
· **Register (default, `variant="register"`)** — the rounded full-width hover/open/`marked` wash
|
|
659
|
+
spans the whole row (incl. nested controls); content sits on the 20px gutter so a `Table` header
|
|
660
|
+
+ cells align. The Divider BETWEEN rows is the resting separation. The COLUMNAR register is
|
|
661
|
+
`Table`/`TableRow`/`TableCell` (columns once → header + widths can't drift; ONE hairline under the
|
|
662
|
+
header; the rows below it Divider-separated; a sortable column shows its `SortHeader` sort glyph
|
|
663
|
+
ALWAYS so it reads as sortable). Make it SELECTABLE with the `Table` `leading` gutter + `selectAll`
|
|
664
|
+
slot — a `CheckboxInput` per `TableRow` (`leading`) + a select-all in the header band, the ticked
|
|
665
|
+
rows `marked`, paired with a `FloatingActionBar`; the selection state is the **`useSelection()`**
|
|
666
|
+
hook (`selected`/`count`/`toggle`/`setAll`/`allSelected`/`indeterminate`/`clear` — gating which
|
|
667
|
+
rows CAN be ticked stays with you: pass only the selectable ids to `setAll`/`allSelected`/
|
|
668
|
+
`indeterminate`). `tpl_item_list` is the reference. Paginate OUTSIDE (slice + `Pagination` in the
|
|
669
|
+
`CardFooter`).
|
|
670
|
+
· **`variant="bleed"`** (legacy) — px-20 square, `Divider`-separated; only for an edge-to-edge data
|
|
671
|
+
grid that genuinely wants hard rules.
|
|
672
|
+
· **`variant="inset"`** — grouped lists in a padded container (Accordion drill-downs).
|
|
652
673
|
Right-hand columns align only if every trailing element is FIXED-width (give each trailing action
|
|
653
674
|
a fixed `width`, so amount/status columns don't jitter).
|
|
654
675
|
- **Row actions are ALWAYS-VISIBLE siblings, never hover-revealed, never nested.** A pressable row =
|
|
@@ -667,6 +688,15 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
667
688
|
step. Facts are `DetailRow`s; the commit bar is a `DrawerFooter`. The open row shows a selected
|
|
668
689
|
highlight (and `marked` — a resting blue tint — for a bulk-ticked row). `Peek` is ONLY for
|
|
669
690
|
secondary references, never the primary row press.
|
|
691
|
+
- **Two work shapes, no side panels.** A work screen is ONE of two shapes — never a persistent
|
|
692
|
+
side-by-side split (it dies on a phone, and the "detail" of a list belongs in a `Drawer`/sheet,
|
|
693
|
+
not a second column). (1) **Work each** — a flat register/list; press a row → its workspace
|
|
694
|
+
`Drawer` (above). (2) **Act on many** — a flat list with a leading `CheckboxInput` per row + a
|
|
695
|
+
select-all `CardHeader` band, plus a bottom-pinned `<FloatingActionBar count label onClear>`
|
|
696
|
+
carrying the bulk action(s) while ≥1 row is ticked (`tpl_item_list`); a row
|
|
697
|
+
that can't take the action has NO checkbox. When a region genuinely must sit beside the list (a
|
|
698
|
+
source/remainder summary, a capacity picker) make it a TOP summary header or move it into a
|
|
699
|
+
Popover/sheet at the decision point — not a standing column.
|
|
670
700
|
- **One control radius** — every interactive control (`Button`, the inputs/selects/pickers,
|
|
671
701
|
`SearchInput`, `MenuButton`, `Tabs`, the `SegmentedControl` track, `ChipGroup`/`FilterChip`/
|
|
672
702
|
`Chip`) wears `CONTROL_RADIUS` (10, from `control_surface.ts`) — never a literal. Containers
|
|
@@ -700,6 +730,10 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
700
730
|
(`icon="map-pin"`, `clearable`) with an `arrow-right` between — flight-search style.
|
|
701
731
|
· `Tabs` switch between VIEWS/sections (different content/layout) — never a subset of one list. A tab earns a `status` dot (optional `TabOption.status`) only when its area needs attention (a blocker / missing item) — the resting state has none.
|
|
702
732
|
`SegmentedControl` chooses a MODE/PARAMETER of the SAME view (2–4 peers, no panel swap).
|
|
733
|
+
· **Layout**: search + secondary filters LEFT, the primary CTA RIGHT, in ONE band. A `ChipGroup`
|
|
734
|
+
one-of-N lens is fine ONLY when the band has no `SearchInput`; once a search is present, the
|
|
735
|
+
status filter becomes a `Select`/`FilterChip` dropdown — never a row of pills competing with the
|
|
736
|
+
search.
|
|
703
737
|
- **Footer actions align RIGHT.** `PopoverFooter`/`DialogFooter`/`DrawerFooter` default
|
|
704
738
|
`align="end"` — commit at the right edge, secondary to its left. Never left-flow a Save/Submit.
|
|
705
739
|
- **Empty result**: `<EmptyState message hint? icon? action?>` — never a bare muted Text.
|
|
@@ -744,12 +778,19 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
744
778
|
paginated register → export) · `tpl_tower` (live `StatusGrid` wallboard) · `tpl_pivot` (the
|
|
745
779
|
`Matrix` cross-tab desk: one dimension × another, number + heat per cell + row/col/grand totals,
|
|
746
780
|
press a cell to drill the list behind it) · `tpl_rollup` (hierarchical totals).
|
|
747
|
-
- **Work execution** —
|
|
748
|
-
|
|
749
|
-
`
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
781
|
+
- **Work execution** — every screen is FLAT (no Card) and ONE of two shapes: a register/list you
|
|
782
|
+
WORK EACH of (press a row → workspace `Drawer`) or a list you ACT ON MANY of (tick rows →
|
|
783
|
+
`FloatingActionBar`) — never a side-by-side panel. `tpl_item_list` (THE canonical register and the
|
|
784
|
+
consolidated work-list: search + a status `Select` + facet `FilterChip`s LEFT, the New CTA RIGHT in
|
|
785
|
+
one toolbar row, a `SummaryLine` below, then a sortable, Divider-separated `Table` where every row
|
|
786
|
+
carries a leading checkbox; a select-all band, footer `Pagination`, and a `FloatingActionBar` for
|
|
787
|
+
the bulk action over the ticked rows; a per-row PRINT button (the trailing action column) is the
|
|
788
|
+
row's primary action and the ⋯ its overflow; a row that can't take the bulk action gets a DISABLED
|
|
789
|
+
checkbox (here an "Awaiting docs" case — the same gating as a blocked run line) — a row press opens
|
|
790
|
+
the workspace `Drawer`. This ONE template subsumes the old approvals / dispatch / batch / run
|
|
791
|
+
screens: register, per-row action, gated selection, and act-on-many in one) · `tpl_pick` (guided `ScanField` run — a single-focus task column with
|
|
792
|
+
the pick path collapsed into an `Accordion` below) · `tpl_allocate` (`RemainderMeter` split — a
|
|
793
|
+
source/remainder summary header over the invoice list + Apply) ·
|
|
753
794
|
`tpl_tasks` (the QUICK LIST — Apple-Reminders shape for personal lists: `CheckCircle` rows you tick +
|
|
754
795
|
expand in place to edit (`Inline*`, stable row — the meta summary never shifts on expand), colour-dot
|
|
755
796
|
tags, an attachments field (`FileThumbnailGrid` → tap a tile to the gallery for download + confirmed
|
|
@@ -762,13 +803,11 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
762
803
|
board, a different action per row; the link's pressable fills the cell to match the inline editors),
|
|
763
804
|
a trailing **`ActionMenu`** (the ⋯ overflow — a `danger` Remove + confirm
|
|
764
805
|
today, more row actions later), per-group add rows that pre-set the group's field, and the same
|
|
765
|
-
Draft-from-notes CTA)
|
|
766
|
-
|
|
767
|
-
deals; proof the shape generalizes beyond tasks). All three: filters/search left, primary CTA right, one
|
|
768
|
-
row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option). **`tpl_task_board` + `tpl_pipeline` are the inline-managed
|
|
806
|
+
Draft-from-notes CTA). Both `tpl_tasks` and `tpl_task_board`: filters/search left, primary CTA right, one
|
|
807
|
+
row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option). **`tpl_task_board` is the inline-managed
|
|
769
808
|
grouped table — MODERATE data you manage in view (group/sort/filter/edit-in-place, renders all
|
|
770
|
-
rows). For thousands+ you BROWSE, that's `
|
|
771
|
-
on the `DataGrid` primitive (the
|
|
809
|
+
rows). For thousands+ you BROWSE, that's `tpl_item_list`'s paginated register, not this.** It builds
|
|
810
|
+
on the `DataGrid` primitive (the inline-managed grouped-table shape); it owns
|
|
772
811
|
only its data, toolbar, columns and per-group add. There is NO Task component — a task ROW varies
|
|
773
812
|
too much to bake into one, so rows compose `CheckCircle` + `InlineTextInput` + inline-editor cells
|
|
774
813
|
as **sibling** controls in a plain `View`. NEVER wrap the whole row in a `Pressable accessibilityRole="button"`
|
|
@@ -822,15 +861,15 @@ list · list_item · menu_button · menu_list_item · detail_row · pressable_ro
|
|
|
822
861
|
check_circle (CheckCircle — the completion ring: an empty ring that springs to a filled check when done, distinct from the square checkbox; the task/to-do/checklist toggle. Compose task rows directly, no Task component) · action_menu ·
|
|
823
862
|
floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
|
|
824
863
|
columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
|
|
825
|
-
sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register.
|
|
864
|
+
sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register. Example: `tpl_task_board`) · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; **navigable** via `Step.onPress` (both orientations — the whole step is the tap target) + `active` to wash the selected one, so it doubles as a section/phase switcher; the guided-run / agent-feed primitive — subsumes the old StepList) ·
|
|
826
865
|
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · modal (Modal + ModalHeader + ModalBody + ModalFooter — the full-bleed, edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a focused capture / multi-step wizard / a console the user steps INTO, where surrounding chrome is a distraction; pick Dialog when the surface is a card the user can see context around) · popover · tooltip ·
|
|
827
866
|
alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
828
|
-
CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
|
|
867
|
+
CalloutText · CalloutActions) · kpi_card · kpi_strip · summary_line (SummaryLine — the light inline summary of a register/list's FILTERED view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band) · metric · trend_chip · sparkline ·
|
|
829
868
|
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
|
830
869
|
funnel (Funnel — conversion funnel: narrowing bars + the step rate as a bold aligned headline row, count below; `orientation` vertical|horizontal, `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of stacked_progress_bar's whole-split) ·
|
|
831
870
|
status_grid (StatusGrid + StatusLegend) · heatmap (density cross-tab — colour-only, no numbers) · matrix (Matrix — the PIVOT cross-tab: band-compound `Matrix` root + `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) + `Matrix.Legend`; the data layer `matrixTotals`/`MatrixAxisItem`/`MatrixCellRef` import from `@lotics/ui/matrix_totals`. Pick over `Heatmap` when the NUMBER and totals matter, not just where it clusters) · legend_item · remainder_meter · allocation_row ·
|
|
832
871
|
scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines are LAZY (dynamic-imported, ~free until a doc of that type is opened) so custom-code APPS can use it too — they just add `@lotics/docx` + `@lotics/xlsx` to their deps. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
|
|
833
|
-
file_gallery_modal · image_gallery · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
872
|
+
file_gallery_modal · image_gallery · use_selection (useSelection — always-on multi-select state for a register/list: the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`; selectability gating stays with the caller. The checkbox-always-visible counterpart to `use_selection_mode`) · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
834
873
|
spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
|
|
835
874
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
836
875
|
expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
|
|
@@ -5,7 +5,7 @@ import { colors } from "@lotics/ui/colors";
|
|
|
5
5
|
import { AllocationRow } from "@lotics/ui/allocation_row";
|
|
6
6
|
import { Badge } from "@lotics/ui/badge";
|
|
7
7
|
import { Button } from "@lotics/ui/button";
|
|
8
|
-
import {
|
|
8
|
+
import { CardFooter } from "@lotics/ui/card";
|
|
9
9
|
import { Divider } from "@lotics/ui/divider";
|
|
10
10
|
import { RemainderMeter } from "@lotics/ui/remainder_meter";
|
|
11
11
|
import { formatMoney } from "@lotics/ui/format_money";
|
|
@@ -63,7 +63,7 @@ export function TplAllocate() {
|
|
|
63
63
|
: "Payment fully applied across the selected invoices";
|
|
64
64
|
|
|
65
65
|
return (
|
|
66
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
66
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
67
67
|
<View style={{ width: "100%", maxWidth: 1000, alignSelf: "center", gap: 16 }}>
|
|
68
68
|
{/* header */}
|
|
69
69
|
<View style={{ gap: 2 }}>
|
|
@@ -72,7 +72,7 @@ export function TplAllocate() {
|
|
|
72
72
|
</View>
|
|
73
73
|
|
|
74
74
|
{/* the source — the payment, and the remainder as you place it */}
|
|
75
|
-
<
|
|
75
|
+
<View>
|
|
76
76
|
<View style={{ padding: 20, flexDirection: "row", alignItems: "flex-start", gap: 24, flexWrap: "wrap" }}>
|
|
77
77
|
<View style={{ gap: 2, flexGrow: 1, flexBasis: 220 }}>
|
|
78
78
|
<Text size="xs" color="muted" transform="uppercase">Payment received</Text>
|
|
@@ -87,10 +87,10 @@ export function TplAllocate() {
|
|
|
87
87
|
</View>
|
|
88
88
|
</View>
|
|
89
89
|
</View>
|
|
90
|
-
</
|
|
90
|
+
</View>
|
|
91
91
|
|
|
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
95
|
<Text size="xs" color="muted" transform="uppercase">{`Open invoices · ${INVOICES.length}`}</Text>
|
|
96
96
|
</View>
|
|
@@ -113,7 +113,7 @@ export function TplAllocate() {
|
|
|
113
113
|
<Text size="xs" color={over ? "danger" : "muted"} style={{ flex: 1 }}>{status}</Text>
|
|
114
114
|
<Button title="Apply payment" color="primary" disabled={over || allocated === 0} onPress={() => {}} />
|
|
115
115
|
</CardFooter>
|
|
116
|
-
</
|
|
116
|
+
</View>
|
|
117
117
|
</View>
|
|
118
118
|
</ScrollView>
|
|
119
119
|
);
|
|
@@ -84,7 +84,7 @@ export function TplAssistant() {
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
|
-
<View style={{ flex: 1, backgroundColor: colors.
|
|
87
|
+
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
|
88
88
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.zinc[200], backgroundColor: colors.background }}>
|
|
89
89
|
<View style={{ width: 24, height: 24, borderRadius: 7, alignItems: "center", justifyContent: "center", backgroundColor: colors.zinc[100] }}>
|
|
90
90
|
<View style={{ width: 4, height: 4, borderRadius: 2, backgroundColor: colors.zinc[900] }} />
|
|
@@ -269,7 +269,7 @@ export function TplAttendance() {
|
|
|
269
269
|
const seqIndex = openNv ? sorted.findIndex((nv) => nv.id === openNv.id) : -1;
|
|
270
270
|
|
|
271
271
|
return (
|
|
272
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
272
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
273
273
|
<View style={{ width: "100%", maxWidth: 920, alignSelf: "center", gap: 16 }}>
|
|
274
274
|
{/* header band */}
|
|
275
275
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
package/examples/tpl_billing.tsx
CHANGED
|
@@ -233,7 +233,7 @@ export function TplBilling() {
|
|
|
233
233
|
};
|
|
234
234
|
|
|
235
235
|
return (
|
|
236
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
236
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
237
237
|
<View style={{ width: "100%", maxWidth: 640, alignSelf: "center", gap: 16 }}>
|
|
238
238
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
239
239
|
<Text size="xl" weight="semibold">Billing</Text>
|
|
@@ -299,7 +299,7 @@ export function TplBilling() {
|
|
|
299
299
|
|
|
300
300
|
{/* deposit band — tinted + labelled separate, never part of the total */}
|
|
301
301
|
<Divider />
|
|
302
|
-
<CardBody style={{ backgroundColor: colors.
|
|
302
|
+
<CardBody style={{ backgroundColor: colors.white, gap: 8 }}>
|
|
303
303
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
304
304
|
<Text weight="semibold">Deposit</Text>
|
|
305
305
|
<Badge label="Collected separately" color="zinc" />
|
|
@@ -54,7 +54,7 @@ export function TplBriefing() {
|
|
|
54
54
|
const attention = findings.filter((f) => f.severity === "critical" || f.severity === "warning").length;
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
57
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
58
58
|
<View style={{ width: "100%", maxWidth: 860, alignSelf: "center", gap: 16 }}>
|
|
59
59
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
|
|
60
60
|
<View style={{ flex: 1, gap: 2 }}>
|
|
@@ -204,7 +204,7 @@ export function TplCalendar() {
|
|
|
204
204
|
|
|
205
205
|
return (
|
|
206
206
|
<View style={{ flex: 1 }}>
|
|
207
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
207
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
208
208
|
<View style={{ width: "100%", maxWidth: 1040, alignSelf: "center", gap: 16 }}>
|
|
209
209
|
{/* header band */}
|
|
210
210
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|
package/examples/tpl_compare.tsx
CHANGED
|
@@ -90,7 +90,7 @@ export function TplCompare() {
|
|
|
90
90
|
const pick = OPTIONS.find((o) => o.id === chosen) ?? null;
|
|
91
91
|
|
|
92
92
|
return (
|
|
93
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
93
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
94
94
|
<View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
|
|
95
95
|
<View style={{ gap: 2 }}>
|
|
96
96
|
<Text size="xl" weight="semibold">Freight options</Text>
|
|
@@ -72,7 +72,7 @@ export function TplCrosscheck() {
|
|
|
72
72
|
const openCount = SEED.length - resolvedCount;
|
|
73
73
|
|
|
74
74
|
return (
|
|
75
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
75
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
76
76
|
<View style={{ width: "100%", maxWidth: 760, alignSelf: "center", gap: 16 }}>
|
|
77
77
|
<View style={{ gap: 2 }}>
|
|
78
78
|
<Text size="xl" weight="semibold">Document cross-check</Text>
|
|
@@ -306,7 +306,7 @@ export function TplDashboard() {
|
|
|
306
306
|
const ky = (key ? KY_DU_LIEU[key] : undefined) ?? KY_DU_LIEU[KY_CUOI];
|
|
307
307
|
|
|
308
308
|
return (
|
|
309
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
309
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
310
310
|
<View style={{ width: "100%", maxWidth: 960, alignSelf: "center", gap: 16 }}>
|
|
311
311
|
{/* header band — the period filter lives here: presets + custom
|
|
312
312
|
range in the popover (and time, via includeTime, where a
|
package/examples/tpl_detail.tsx
CHANGED
|
@@ -191,7 +191,7 @@ export function TplDetail() {
|
|
|
191
191
|
const [tab, setTab] = useState<string>("tongquan");
|
|
192
192
|
|
|
193
193
|
return (
|
|
194
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
194
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
195
195
|
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
196
196
|
{/* breadcrumb line */}
|
|
197
197
|
<Text size="xs" color="muted">Orders / SO-2026-0042</Text>
|
package/examples/tpl_dieline.tsx
CHANGED
|
@@ -164,7 +164,7 @@ export function TplDieline() {
|
|
|
164
164
|
};
|
|
165
165
|
|
|
166
166
|
return (
|
|
167
|
-
<View style={{ flex: 1, backgroundColor: colors.
|
|
167
|
+
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
|
168
168
|
{/* slim top bar */}
|
|
169
169
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.zinc[200], backgroundColor: colors.background }}>
|
|
170
170
|
<Text size="sm" weight="semibold" style={{ flex: 1 }}>Carton design</Text>
|
|
@@ -341,7 +341,7 @@ function EmptyFrame() {
|
|
|
341
341
|
|
|
342
342
|
function BuildingFrame() {
|
|
343
343
|
return (
|
|
344
|
-
<View style={[frame.box, { backgroundColor: colors.
|
|
344
|
+
<View style={[frame.box, { backgroundColor: colors.white }]}>
|
|
345
345
|
<DotsIndicator size={9} color={colors.zinc[900]} />
|
|
346
346
|
<Text size="sm" color="muted">Designing your dieline…</Text>
|
|
347
347
|
</View>
|
|
@@ -170,7 +170,7 @@ export function TplDirectory() {
|
|
|
170
170
|
const seqIndex = openPartner ? matches.findIndex((p) => p.id === openPartner.id) : -1;
|
|
171
171
|
|
|
172
172
|
return (
|
|
173
|
-
<View style={{ flex: 1, backgroundColor: colors.
|
|
173
|
+
<View style={{ flex: 1, backgroundColor: colors.white }}>
|
|
174
174
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 28 }}>
|
|
175
175
|
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
176
176
|
{/* header band */}
|
|
@@ -225,7 +225,7 @@ export function TplDirectory() {
|
|
|
225
225
|
</FilterChip>
|
|
226
226
|
</View>
|
|
227
227
|
|
|
228
|
-
{/* the register: one card,
|
|
228
|
+
{/* the register: one card, gap-separated rounded `register` rows (no per-row Divider) */}
|
|
229
229
|
<Card style={{ padding: 0 }}>
|
|
230
230
|
{matches.length === 0 ? (
|
|
231
231
|
<EmptyState
|
|
@@ -233,12 +233,9 @@ export function TplDirectory() {
|
|
|
233
233
|
hint="Try another keyword or switch the type filter"
|
|
234
234
|
/>
|
|
235
235
|
) : (
|
|
236
|
-
<View style={{ paddingVertical: 6 }}>
|
|
237
|
-
{matches.map((p
|
|
238
|
-
<
|
|
239
|
-
{i > 0 ? <Divider /> : null}
|
|
240
|
-
<PartnerRow p={p} selected={p.id === openId} onPress={() => setOpenId(p.id)} />
|
|
241
|
-
</View>
|
|
236
|
+
<View style={{ paddingVertical: 6, gap: 2 }}>
|
|
237
|
+
{matches.map((p) => (
|
|
238
|
+
<PartnerRow key={p.id} p={p} selected={p.id === openId} onPress={() => setOpenId(p.id)} />
|
|
242
239
|
))}
|
|
243
240
|
</View>
|
|
244
241
|
)}
|
package/examples/tpl_draft.tsx
CHANGED
|
@@ -73,7 +73,7 @@ export function TplDraft() {
|
|
|
73
73
|
const streamed = phase === "drafting" ? target.split(" ").slice(0, revealed).join(" ") : "";
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
|
-
<ScrollView style={{ backgroundColor: colors.
|
|
76
|
+
<ScrollView style={{ backgroundColor: colors.white }} contentContainerStyle={{ padding: 28, paddingBottom: 80 }}>
|
|
77
77
|
<View style={{ maxWidth: 720, width: "100%", alignSelf: "center", gap: 16 }}>
|
|
78
78
|
<View style={{ gap: 2 }}>
|
|
79
79
|
<Text size="xl" weight="semibold">Reply to inquiry</Text>
|
|
@@ -134,7 +134,7 @@ export function TplDraft() {
|
|
|
134
134
|
) : null}
|
|
135
135
|
|
|
136
136
|
{phase === "drafting" ? (
|
|
137
|
-
<View style={{ borderWidth: 1, borderColor: colors.zinc[200], borderRadius: 10, padding: 14, gap: 10, backgroundColor: colors.
|
|
137
|
+
<View style={{ borderWidth: 1, borderColor: colors.zinc[200], borderRadius: 10, padding: 14, gap: 10, backgroundColor: colors.white }}>
|
|
138
138
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
139
139
|
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Drafting</Text>
|
|
140
140
|
<DotsIndicator size={5} color={colors.zinc[900]} />
|
package/examples/tpl_extract.tsx
CHANGED
|
@@ -113,7 +113,7 @@ export function TplExtract() {
|
|
|
113
113
|
const confirmedCount = RECORDS.filter((r) => statuses[r.id] === "confirmed").length;
|
|
114
114
|
|
|
115
115
|
return (
|
|
116
|
-
<ScrollView style={{ backgroundColor: colors.
|
|
116
|
+
<ScrollView style={{ backgroundColor: colors.white }} contentContainerStyle={{ padding: 28, paddingBottom: 120 }}>
|
|
117
117
|
<View style={{ maxWidth: 720, width: "100%", alignSelf: "center", gap: 16 }}>
|
|
118
118
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
|
|
119
119
|
<View style={{ flex: 1, gap: 2 }}>
|
package/examples/tpl_intake.tsx
CHANGED
|
@@ -70,7 +70,7 @@ export function TplIntake() {
|
|
|
70
70
|
: undefined;
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.
|
|
73
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
74
74
|
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
75
75
|
{/* header band */}
|
|
76
76
|
<View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
|