@lotics/ui 7.8.0 → 7.10.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 (63) hide show
  1. package/AGENTS.md +92 -43
  2. package/examples/tpl_allocate.tsx +6 -6
  3. package/examples/tpl_assistant.tsx +1 -1
  4. package/examples/tpl_attendance.tsx +1 -1
  5. package/examples/tpl_billing.tsx +2 -2
  6. package/examples/tpl_briefing.tsx +1 -1
  7. package/examples/tpl_calendar.tsx +1 -1
  8. package/examples/tpl_compare.tsx +1 -1
  9. package/examples/tpl_crosscheck.tsx +1 -1
  10. package/examples/tpl_dashboard.tsx +1 -1
  11. package/examples/tpl_detail.tsx +1 -1
  12. package/examples/tpl_dieline.tsx +2 -2
  13. package/examples/tpl_directory.tsx +5 -8
  14. package/examples/tpl_draft.tsx +2 -2
  15. package/examples/tpl_extract.tsx +1 -1
  16. package/examples/tpl_intake.tsx +1 -1
  17. package/examples/{tpl_dossier.tsx → tpl_item_list.tsx} +138 -97
  18. package/examples/tpl_lookup.tsx +2 -2
  19. package/examples/tpl_match.tsx +2 -2
  20. package/examples/tpl_order.tsx +1 -1
  21. package/examples/tpl_pick.tsx +64 -68
  22. package/examples/tpl_pivot.tsx +1 -1
  23. package/examples/tpl_quick.tsx +1 -1
  24. package/examples/tpl_ratedesk.tsx +1 -1
  25. package/examples/tpl_record_plain.tsx +121 -16
  26. package/examples/tpl_report.tsx +1 -1
  27. package/examples/tpl_rollup.tsx +1 -1
  28. package/examples/tpl_settings.tsx +1 -1
  29. package/examples/tpl_shifts.tsx +1 -1
  30. package/examples/tpl_stock.tsx +1 -1
  31. package/examples/tpl_task_board.tsx +2 -2
  32. package/examples/tpl_tasks.tsx +1 -1
  33. package/examples/tpl_timeline.tsx +1 -1
  34. package/examples/tpl_tower.tsx +1 -1
  35. package/examples/tpl_triage.tsx +1 -1
  36. package/examples/tpl_wizard.tsx +1 -1
  37. package/package.json +5 -1
  38. package/src/card.tsx +6 -13
  39. package/src/danger_zone.tsx +58 -0
  40. package/src/data_grid.tsx +1 -1
  41. package/src/detail_row.tsx +8 -1
  42. package/src/inline_edit.tsx +14 -4
  43. package/src/inline_select.tsx +9 -8
  44. package/src/inline_static.tsx +63 -0
  45. package/src/kpi_strip.tsx +5 -5
  46. package/src/pressable_row.tsx +22 -10
  47. package/src/sort_header.tsx +9 -4
  48. package/src/summary_line.tsx +76 -0
  49. package/src/table.tsx +27 -11
  50. package/src/use_selection.test.ts +66 -0
  51. package/src/use_selection.ts +75 -0
  52. package/examples/app_orders.tsx +0 -405
  53. package/examples/tpl_approvals.tsx +0 -375
  54. package/examples/tpl_batch.tsx +0 -234
  55. package/examples/tpl_callsheet.tsx +0 -481
  56. package/examples/tpl_convert.tsx +0 -478
  57. package/examples/tpl_crm_desk.tsx +0 -541
  58. package/examples/tpl_dispatch.tsx +0 -289
  59. package/examples/tpl_inventory.tsx +0 -299
  60. package/examples/tpl_pipeline.tsx +0 -400
  61. package/examples/tpl_reconcile.tsx +0 -275
  62. package/examples/tpl_record.tsx +0 -302
  63. package/examples/tpl_run.tsx +0 -235
package/AGENTS.md CHANGED
@@ -69,7 +69,8 @@ Pick by capability, not by name. (→ the source file for the API.)
69
69
  via `TimePicker`.
70
70
  - **Edit a record's fields in place** — the `Inline*` family: `InlineTextInput` ·
71
71
  `InlineNumberInput` · `InlineSelect` · `InlineMemberSelect` · `InlineDatePicker` ·
72
- `InlineTimePicker` (see §Data entry).
72
+ `InlineTimePicker`; a READ-ONLY field in that same column uses `InlineStatic` (matches the
73
+ editor box exactly, no input chrome, so it aligns pixel-for-pixel) (see §Data entry).
73
74
  - **Tasks / to-dos** — there is NO Task component (a task row varies too much app-to-app to abstract
74
75
  well — a Reminders row and a Linear column-grid share almost nothing). Compose the row directly:
75
76
  `CheckCircle` (the completion ring — fills with a spring, distinct from the square `Checkbox`) + an
@@ -80,7 +81,7 @@ Pick by capability, not by name. (→ the source file for the API.)
80
81
  - **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
81
82
  headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer` to edit.
82
83
  It scales by PAGING — renders one page, never the whole set — so you FILTER + search, you don't
83
- group. Worked example: `tpl_dossier`. Never an HTML `<table>` or a `.map` of rows.
84
+ group. Worked example: `tpl_item_list`. Never an HTML `<table>` or a `.map` of rows.
84
85
  - **Inline-managed grouped table** (MODERATE — hundreds, low-thousands — you MANAGE in view) — the
85
86
  `DataGrid` primitive: a grouped, sortable grid whose cells are LIVE inline editors (ANY field — a
86
87
  column is `{ key, label, width?, sortable?, cell: (item) => ReactNode }`, so `InlineMemberSelect` /
@@ -91,8 +92,9 @@ Pick by capability, not by name. (→ the source file for the API.)
91
92
  per-group add row (`renderGroupFooter`, aligned with the exported `gridRowStyle` + the column
92
93
  widths). It renders ALL rows (no virtualization), so it's only for sets small enough to hold in
93
94
  view — at 10k+ it lags, and grouping + pagination/infinite don't compose; use the register instead.
94
- Worked examples: `tpl_task_board` (with a `CheckCircle` leading), `tpl_pipeline` (no leading).
95
- - **Numbers & charts** — `KPIStrip` / `KPICard` / `Metric` (headline figures), `TrendChip`
95
+ Worked example: `tpl_task_board` (a `CheckCircle` leading).
96
+ - **Numbers & charts** — `KPIStrip` (the dashboard stat band) · `SummaryLine` (the light inline
97
+ register/list summary — below the toolbar, from the filtered rows) · `KPICard` / `Metric` (headline figures), `TrendChip`
96
98
  (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
97
99
  recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
98
100
  (a stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
@@ -111,10 +113,11 @@ Pick by capability, not by name. (→ the source file for the API.)
111
113
  `CardFooter`), `Section` (the card-less twin of `Card`: bare gap-spaced region + `SectionHeading`/
112
114
  `SectionHeadingTitle`/`SectionHeadingMeta`; no body component — children are the body), `SectionCard`,
113
115
  `PageHeader` /
114
- `PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`, `Stepper`.
116
+ `PageContent`, `Stack`, `Spacer`, `Divider`, `Accordion`, `Tabs`, `SegmentedControl`, `Stepper`,
117
+ `DangerZone` (the destructive section — delete/archive — set apart at the bottom of a record/settings surface).
115
118
  - **Rows & registers** — `PressableRow` (THE register row), `ListItem`, `MenuButton`,
116
- `MenuListItem`, `DetailRow` (label+value for drawers/peeks), `ActionMenu` (⋯), `FloatingActionBar`
117
- (bulk-select bar).
119
+ `MenuListItem`, `DetailRow` (label+value for drawers/peeks; optional `trailing` slot for a
120
+ right-side action/badge/unit), `ActionMenu` (⋯), `FloatingActionBar` (bulk-select bar).
118
121
  - **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`,
119
122
  `Counter`), `Chip`.
120
123
  - **Overlays** — `Dialog` (centered card over a scrim), `Modal` (+ `ModalHeader`/`ModalBody`/
@@ -187,15 +190,22 @@ hover reveals its border (it's a field — no grey tint, no pencil that shifts),
187
190
  reflow, the whole point), and it commits on blur (Enter saves, Escape reverts) or via
188
191
  `controls="buttons"` (✓ primary / ✕). One per type — `InlineTextInput` · `InlineNumberInput`
189
192
  (`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats an
190
- `OptionList` in a popover so the row never grows; `renderSelected` renders the RESTING value as a
191
- node — a colored `OptionBadge`, not just a label) · `InlineMemberSelect` (a `MemberChip` at rest →
193
+ `OptionList` in a popover so the row never grows; the RESTING value renders like its option
194
+ (`renderOptionContent` by default; `renderSelected` overrides) — a colored `OptionBadge`, not just a label) · `InlineMemberSelect` (a `MemberChip` at rest →
192
195
  member picker; the inline twin of `MemberSelect`) · `InlineDatePicker` (`format="datetime"` for
193
196
  always-on time; `optionalTime` to let the user ADD/REMOVE a time — the value's own shape, date vs
194
197
  datetime, is the source of truth) · `InlineTimePicker` — all
195
198
  on `useInlineEdit` + `InlineEditView` (custom inputs join via those). `onSave` is async: the
196
199
  saving spinner sits INSIDE the control (never a sibling — that reflows); an error shows inline
197
- without losing the edit. Pair with `DetailRow` (label left, inline value right). Not every field
198
- is a same-height swap — a tag field, a status, or an attachment grid edit in place too (below).
200
+ without losing the edit. Pair with `DetailRow` (label left, inline value right). A READ-ONLY value
201
+ in the same column — a computed total, a system ID, a synced/locked field is `InlineStatic`: it
202
+ copies the editor box exactly (height, padding, 1px transparent border) but is non-interactive, so
203
+ it sits FLUSH with the editors instead of reading as a disabled input. To hang a right-side
204
+ action/badge/unit off a row, use `DetailRow`'s `trailing` slot (wrap the editor in a `flex:1` View
205
+ so it takes the slack and the trailing pins right) — see the "Row anatomy" section of
206
+ `tpl_record_plain`, which also reads top→bottom as a full drawer record (header → fields →
207
+ `DangerZone`). Not every field is a same-height swap — a tag field, a status, or an attachment grid
208
+ edit in place too (below).
199
209
 
200
210
  ### Fieldset form — fields lay out on a RESPONSIVE two-column grid
201
211
  Never hard-code columns, never 3-up. A fieldset is `flexDirection:row, flexWrap:wrap,
@@ -489,9 +499,9 @@ decisions, or a ranked set, don't cram it into chat prose. Each pairs a template
489
499
  - **Match** (`tpl_match`, body-first `ReviewCard` + `MatchSides`) — the agent proposes PAIRINGS with
490
500
  its reasoning; each is a `ReviewCard` (no title, so body-first) whose body is a two-sided `MatchSides`
491
501
  pairing (source → proposed counterpart), with the confidence + reason beneath, accept / reassign /
492
- reject, high-confidence batch-accept. The AI reconciliation / dedup / correlation queue. (The
493
- deterministic cousin
494
- `tpl_reconcile` has no agent reasoning — that's the tell for which to reach for.)
502
+ reject, high-confidence batch-accept. The AI reconciliation / dedup / correlation queue. (Strip the agent's
503
+ reasoning and it would be a plain deterministic match register — the reasoning is the tell for when
504
+ you need this.)
495
505
  - **Briefing** (`tpl_briefing`, `Finding`) — a generated narrative over the data + a RANKED list of
496
506
  what needs attention, each finding carrying a severity, a metric, its sources, and one action. The
497
507
  leadership digest / anomaly scan — the agent's COMMENTARY, not a raw dashboard.
@@ -616,10 +626,21 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
616
626
  </Card>
617
627
  ```
618
628
  Never nest cards; never hand-compose a title or footer band. `info` is standard, not garnish.
619
- - **The stat band**: `<KPIStrip items={[{label, value, trend?, caption?, tone?, info?}…]}>` is the
620
- ONE way to open a screen with numbers. KPI strips are INFORMATIONAL they never filter or
621
- navigate (that's the tabs/chips' job), and they carry cross-cutting health numbers the tabs
622
- can't show; if there are none, drop the strip. Card stat rails use `KPICard`.
629
+ - **Flat work execution vs carded monitoring.** A `Card` is ALWAYS a bordered, lifted container —
630
+ there is no "flat card". MONITORING / dashboard screens group with cards (and the `KPIStrip`
631
+ band). WORK-EXECUTION screens registers, lists, the worklists an operator acts on — are FLAT:
632
+ the register/list sits DIRECTLY on the white canvas with no Card wrapper; separation comes from
633
+ the row wash, gaps, and `Divider`/`CardHeader`/`CardFooter` bands used BARE. Reach for a Card only
634
+ where a screen holds a DISTINCT region that must be told apart from another; a single register or
635
+ list filling the screen needs none.
636
+ - **Summaries — two altitudes, never mixed.** A DASHBOARD opens with the boxed stat band
637
+ `<KPIStrip items={[{label, value, trend?, caption?, tone?, info?}…]}>` (a `Card` of `KPICard`
638
+ columns). A REGISTER/LIST (work-execution) screen instead carries a LIGHT inline
639
+ `<SummaryLine items={[{label, value, tone?, info?}…]}>` BELOW its toolbar — a small `Metric` + a
640
+ muted label per item, recomputed from the FILTERED rows so it always describes WHAT'S SHOWN. Both
641
+ are INFORMATIONAL — they never filter or navigate (that's the tabs/chips' job); if there are no
642
+ cross-cutting numbers, drop them. Do NOT put a `KPIStrip` on a register page or a `SummaryLine` on
643
+ a dashboard. Card stat rails use `KPICard`.
623
644
  - **Numbers**: free-standing numerals `<Text tabular>`. Money: `formatMoney(n)` from
624
645
  `@lotics/ui/format_money` (`compact` for strip captions). Dates: `formatDate(value, opts)` from
625
646
  `@lotics/ui/format_date` — the date-VALUE formatter. `format` (date STYLE) ∈ `date` (22/05/2026) ·
@@ -642,13 +663,22 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
642
663
  `Drawer` (sequenced); read-only drill-downs expand via `Accordion` or glance via `Peek`; every
643
664
  other row gets an `ActionMenu` (⋯ → MenuButton items: destructive last + **confirmed** via
644
665
  `Alert`). The whole surface is the door: register rows are `PressableRow` (full-bleed wash incl.
645
- nested controls). Two row patterns, never un-separated:
646
- · **Register (default)** — px-20 square rows, wash to the card edges, separated by full-bleed
647
- `Divider`s (`{i > 0 ? <Divider/> : null}`), NO wrapping padding. A COLUMNAR register is
648
- `Table`/`TableRow`/`TableCell` (columns defined once header + widths can't drift); paginate
649
- OUTSIDE (slice + `Pagination` in the `CardFooter`).
650
- · **Inset contained list** grouped/gap-separated lists: `PressableRow variant="inset"` in a
651
- padded container, NO `Divider`s (the gap + rounded hover separate them).
666
+ nested controls), Divider-separated:
667
+ · **Register (default, `variant="register"`)** — the rounded full-width hover/open/`marked` wash
668
+ spans the whole row (incl. nested controls); content sits on the 20px gutter so a `Table` header
669
+ + cells align. The Divider BETWEEN rows is the resting separation. The COLUMNAR register is
670
+ `Table`/`TableRow`/`TableCell` (columns once → header + widths can't drift; ONE hairline under the
671
+ header; the rows below it Divider-separated; a sortable column shows its `SortHeader` sort glyph
672
+ ALWAYS so it reads as sortable). Make it SELECTABLE with the `Table` `leading` gutter + `selectAll`
673
+ slot — a `CheckboxInput` per `TableRow` (`leading`) + a select-all in the header band, the ticked
674
+ rows `marked`, paired with a `FloatingActionBar`; the selection state is the **`useSelection()`**
675
+ hook (`selected`/`count`/`toggle`/`setAll`/`allSelected`/`indeterminate`/`clear` — gating which
676
+ rows CAN be ticked stays with you: pass only the selectable ids to `setAll`/`allSelected`/
677
+ `indeterminate`). `tpl_item_list` is the reference. Paginate OUTSIDE (slice + `Pagination` in the
678
+ `CardFooter`).
679
+ · **`variant="bleed"`** (legacy) — px-20 square, `Divider`-separated; only for an edge-to-edge data
680
+ grid that genuinely wants hard rules.
681
+ · **`variant="inset"`** — grouped lists in a padded container (Accordion drill-downs).
652
682
  Right-hand columns align only if every trailing element is FIXED-width (give each trailing action
653
683
  a fixed `width`, so amount/status columns don't jitter).
654
684
  - **Row actions are ALWAYS-VISIBLE siblings, never hover-revealed, never nested.** A pressable row =
@@ -667,6 +697,15 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
667
697
  step. Facts are `DetailRow`s; the commit bar is a `DrawerFooter`. The open row shows a selected
668
698
  highlight (and `marked` — a resting blue tint — for a bulk-ticked row). `Peek` is ONLY for
669
699
  secondary references, never the primary row press.
700
+ - **Two work shapes, no side panels.** A work screen is ONE of two shapes — never a persistent
701
+ side-by-side split (it dies on a phone, and the "detail" of a list belongs in a `Drawer`/sheet,
702
+ not a second column). (1) **Work each** — a flat register/list; press a row → its workspace
703
+ `Drawer` (above). (2) **Act on many** — a flat list with a leading `CheckboxInput` per row + a
704
+ select-all `CardHeader` band, plus a bottom-pinned `<FloatingActionBar count label onClear>`
705
+ carrying the bulk action(s) while ≥1 row is ticked (`tpl_item_list`); a row
706
+ that can't take the action has NO checkbox. When a region genuinely must sit beside the list (a
707
+ source/remainder summary, a capacity picker) make it a TOP summary header or move it into a
708
+ Popover/sheet at the decision point — not a standing column.
670
709
  - **One control radius** — every interactive control (`Button`, the inputs/selects/pickers,
671
710
  `SearchInput`, `MenuButton`, `Tabs`, the `SegmentedControl` track, `ChipGroup`/`FilterChip`/
672
711
  `Chip`) wears `CONTROL_RADIUS` (10, from `control_surface.ts`) — never a literal. Containers
@@ -700,6 +739,10 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
700
739
  (`icon="map-pin"`, `clearable`) with an `arrow-right` between — flight-search style.
701
740
  · `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
741
  `SegmentedControl` chooses a MODE/PARAMETER of the SAME view (2–4 peers, no panel swap).
742
+ · **Layout**: search + secondary filters LEFT, the primary CTA RIGHT, in ONE band. A `ChipGroup`
743
+ one-of-N lens is fine ONLY when the band has no `SearchInput`; once a search is present, the
744
+ status filter becomes a `Select`/`FilterChip` dropdown — never a row of pills competing with the
745
+ search.
703
746
  - **Footer actions align RIGHT.** `PopoverFooter`/`DialogFooter`/`DrawerFooter` default
704
747
  `align="end"` — commit at the right edge, secondary to its left. Never left-flow a Save/Submit.
705
748
  - **Empty result**: `<EmptyState message hint? icon? action?>` — never a bare muted Text.
@@ -744,12 +787,19 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
744
787
  paginated register → export) · `tpl_tower` (live `StatusGrid` wallboard) · `tpl_pivot` (the
745
788
  `Matrix` cross-tab desk: one dimension × another, number + heat per cell + row/col/grand totals,
746
789
  press a cell to drill the list behind it) · `tpl_rollup` (hierarchical totals).
747
- - **Work execution** — `app_orders` (stage-gated work queue) · `tpl_crm_desk` (assign & work +
748
- `FloatingActionBar`) · `tpl_callsheet` (call console) · `tpl_convert` (staged conversion) ·
749
- `tpl_inventory` (threshold actions) · `tpl_dossier` (high-volume paginated register) ·
750
- `tpl_approvals` (verdicts: bulk + over-policy `Dialog`) · `tpl_reconcile` (pair matching) ·
751
- `tpl_dispatch` (capacity allocation) · `tpl_batch` (compose from parts) · `tpl_pick` (guided
752
- `ScanField` run) · `tpl_allocate` (`RemainderMeter` split) · `tpl_run` (preview resolve → post) ·
790
+ - **Work execution** — every screen is FLAT (no Card) and ONE of two shapes: a register/list you
791
+ WORK EACH of (press a row → workspace `Drawer`) or a list you ACT ON MANY of (tick rows
792
+ `FloatingActionBar`) never a side-by-side panel. `tpl_item_list` (THE canonical register and the
793
+ consolidated work-list: search + a status `Select` + facet `FilterChip`s LEFT, the New CTA RIGHT in
794
+ one toolbar row, a `SummaryLine` below, then a sortable, Divider-separated `Table` where every row
795
+ carries a leading checkbox; a select-all band, footer `Pagination`, and a `FloatingActionBar` for
796
+ the bulk action over the ticked rows; a per-row PRINT button (the trailing action column) is the
797
+ row's primary action and the ⋯ its overflow; a row that can't take the bulk action gets a DISABLED
798
+ checkbox (here an "Awaiting docs" case — the same gating as a blocked run line) — a row press opens
799
+ the workspace `Drawer`. This ONE template subsumes the old approvals / dispatch / batch / run
800
+ screens: register, per-row action, gated selection, and act-on-many in one) · `tpl_pick` (guided `ScanField` run — a single-focus task column with
801
+ the pick path collapsed into an `Accordion` below) · `tpl_allocate` (`RemainderMeter` split — a
802
+ source/remainder summary header over the invoice list + Apply) ·
753
803
  `tpl_tasks` (the QUICK LIST — Apple-Reminders shape for personal lists: `CheckCircle` rows you tick +
754
804
  expand in place to edit (`Inline*`, stable row — the meta summary never shifts on expand), colour-dot
755
805
  tags, an attachments field (`FileThumbnailGrid` → tap a tile to the gallery for download + confirmed
@@ -762,13 +812,11 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
762
812
  board, a different action per row; the link's pressable fills the cell to match the inline editors),
763
813
  a trailing **`ActionMenu`** (the ⋯ overflow — a `danger` Remove + confirm
764
814
  today, more row actions later), per-group add rows that pre-set the group's field, and the same
765
- Draft-from-notes CTA) · `tpl_pipeline` (the SAME inline-managed grouped-table shape, a second domain — deals grouped by
766
- stage with a money-value cell + owner + close-date edited inline, its own Draft-from-notes CTA drafting
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
815
+ Draft-from-notes CTA). Both `tpl_tasks` and `tpl_task_board`: filters/search left, primary CTA right, one
816
+ row; group-by holds only real dimensions, clears to ungrouped (no "Nothing" option). **`tpl_task_board` is the inline-managed
769
817
  grouped table — MODERATE data you manage in view (group/sort/filter/edit-in-place, renders all
770
- rows). For thousands+ you BROWSE, that's `tpl_dossier`'s paginated register, not this.** Both build
771
- on the `DataGrid` primitive (the grid shape, extracted once these two uses confirmed it); each owns
818
+ rows). For thousands+ you BROWSE, that's `tpl_item_list`'s paginated register, not this.** It builds
819
+ on the `DataGrid` primitive (the inline-managed grouped-table shape); it owns
772
820
  only its data, toolbar, columns and per-group add. There is NO Task component — a task ROW varies
773
821
  too much to bake into one, so rows compose `CheckCircle` + `InlineTextInput` + inline-editor cells
774
822
  as **sibling** controls in a plain `View`. NEVER wrap the whole row in a `Pressable accessibilityRole="button"`
@@ -779,7 +827,7 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
779
827
  transactional form: find-or-create + line items + attachment CRUD + 2-col grid) · `tpl_billing`
780
828
  (charges→invoice→collect: the invoice DOCUMENT is the unit — editable charge lines + method,
781
829
  live total, status, inline-gated issue; validated receipt; separate deposit) · `tpl_quick`
782
- (quick-capture log) · `tpl_wizard` (`Stepper` form + review) · `tpl_record` (inline edit + Stage
830
+ (quick-capture log) · `tpl_wizard` (`Stepper` form + review) · `tpl_record_plain` (inline edit + Stage
783
831
  disposition + `Select` tags + Documents grid; `tpl_record_plain` = card-less).
784
832
  - **Records & lookup** — `tpl_detail` (full record + tabs) · `tpl_directory` (searchable register) ·
785
833
  `tpl_timeline` (audit feed).
@@ -817,20 +865,21 @@ text_input_field · number_input · search_input · form_field · checkbox · ch
817
865
  radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
818
866
  inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
819
867
  inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
820
- `InlineEditView`; `InlineSelect`/`InlineMemberSelect` take `renderSelected` for a chip/badge at rest) ·
821
- list · list_item · menu_button · menu_list_item · detail_row · pressable_row ·
868
+ `InlineEditView`; `InlineSelect`/`InlineMemberSelect` render the resting value like its option — `renderOptionContent` by default, `renderSelected` to override — a chip/badge at rest, not just text) ·
869
+ inline_static (InlineStatic a READ-ONLY value matching the Inline* box metrics EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column; non-interactive, NOT a disabled input; `muted`/`tabular`/`align="right"` for a number column) ·
870
+ list · list_item · menu_button · menu_list_item · detail_row (DetailRow — label+value row for drawer/peek detail; optional `trailing` slot renders a right-side action/badge/unit after the value) · danger_zone (DangerZone — the destructive section: a soft danger-tinted frame (`tint`/`solid`, never raw hex) + a danger heading + a description + a destructive action slot (children, e.g. a `danger` Button); sits APART at the bottom of a record/settings surface) · pressable_row ·
822
871
  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
872
  floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
824
873
  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. Examples: `tpl_task_board`, `tpl_pipeline`) · 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) ·
874
+ 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
875
  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
876
  alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
828
- CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
877
+ 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
878
  bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
830
879
  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
880
  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
881
  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 ·
882
+ 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
883
  spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
835
884
  comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
836
885
  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 { Card, CardFooter } from "@lotics/ui/card";
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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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
- <Card style={{ padding: 0 }}>
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
- </Card>
90
+ </View>
91
91
 
92
92
  {/* the targets — the open invoices, each absorbing part of the payment */}
93
- <Card style={{ padding: 0 }}>
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
- </Card>
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.zinc[50] }}>
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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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 }}>
@@ -233,7 +233,7 @@ export function TplBilling() {
233
233
  };
234
234
 
235
235
  return (
236
- <ScrollView style={{ flex: 1, backgroundColor: colors.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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.zinc[50], gap: 8 }}>
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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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 }}>
@@ -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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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
@@ -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.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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>
@@ -164,7 +164,7 @@ export function TplDieline() {
164
164
  };
165
165
 
166
166
  return (
167
- <View style={{ flex: 1, backgroundColor: colors.zinc[50] }}>
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.zinc[50] }]}>
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.zinc[50] }}>
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, hairline-separated rows */}
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, i) => (
238
- <View key={p.id}>
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
  )}
@@ -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.zinc[50] }} contentContainerStyle={{ padding: 28, paddingBottom: 80 }}>
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.zinc[50] }}>
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]} />
@@ -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.zinc[50] }} contentContainerStyle={{ padding: 28, paddingBottom: 120 }}>
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 }}>
@@ -70,7 +70,7 @@ export function TplIntake() {
70
70
  : undefined;
71
71
 
72
72
  return (
73
- <ScrollView style={{ flex: 1, backgroundColor: colors.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
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 }}>