@lotics/ui 5.8.0 → 5.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 (104) hide show
  1. package/AGENTS.md +184 -56
  2. package/examples/tpl_approvals.tsx +3 -3
  3. package/examples/tpl_convert.tsx +11 -23
  4. package/examples/tpl_dossier.tsx +5 -4
  5. package/examples/tpl_order.tsx +1 -1
  6. package/examples/tpl_pipeline.tsx +400 -0
  7. package/examples/tpl_ratedesk.tsx +1 -1
  8. package/examples/tpl_record.tsx +6 -5
  9. package/examples/tpl_shifts.tsx +7 -5
  10. package/examples/tpl_task_board.tsx +473 -0
  11. package/examples/tpl_tasks.tsx +440 -0
  12. package/package.json +19 -6
  13. package/src/accordion.tsx +1 -1
  14. package/src/agent_progress.tsx +1 -1
  15. package/src/agent_run.tsx +1 -1
  16. package/src/breakdown.tsx +2 -2
  17. package/src/button.tsx +12 -3
  18. package/src/calendar/calendar_view.tsx +10 -10
  19. package/src/calendar/month_view.tsx +30 -11
  20. package/src/calendar/time_grid_view.tsx +25 -7
  21. package/src/card_select_item.tsx +11 -13
  22. package/src/check_circle.tsx +88 -0
  23. package/src/checkbox_input.tsx +5 -1
  24. package/src/chip.tsx +1 -1
  25. package/src/chip_group.tsx +1 -1
  26. package/src/column_filter.tsx +3 -2
  27. package/src/combobox.tsx +84 -219
  28. package/src/confidence.tsx +14 -3
  29. package/src/control_surface.ts +22 -7
  30. package/src/data_grid.tsx +159 -0
  31. package/src/date_calendar.tsx +46 -23
  32. package/src/date_field.tsx +10 -2
  33. package/src/date_filter.tsx +5 -9
  34. package/src/date_picker.tsx +78 -26
  35. package/src/date_picker_value.ts +11 -0
  36. package/src/date_range_filter_field.tsx +6 -4
  37. package/src/drawer.tsx +1 -1
  38. package/src/file_dropzone.tsx +5 -0
  39. package/src/file_gallery_modal.tsx +227 -139
  40. package/src/file_grid.tsx +184 -0
  41. package/src/file_preview.web.tsx +349 -12
  42. package/src/file_preview_types.ts +49 -0
  43. package/src/file_row.tsx +118 -0
  44. package/src/file_rows.tsx +109 -0
  45. package/src/file_thumbnail.tsx +132 -81
  46. package/src/file_thumbnail_grid.tsx +135 -55
  47. package/src/files_editor.tsx +232 -0
  48. package/src/filter_chip.tsx +25 -12
  49. package/src/focus_ring_pressable.tsx +34 -0
  50. package/src/form_picker.tsx +3 -3
  51. package/src/gantt/gantt_view.tsx +25 -7
  52. package/src/heatmap.tsx +5 -1
  53. package/src/icon_button.tsx +14 -3
  54. package/src/image_gallery.tsx +8 -14
  55. package/src/index.css +8 -12
  56. package/src/inline_date_picker.tsx +11 -11
  57. package/src/inline_edit.tsx +22 -6
  58. package/src/inline_member_select.tsx +1 -1
  59. package/src/inline_select.tsx +9 -8
  60. package/src/inline_text_input.tsx +4 -1
  61. package/src/link.tsx +11 -1
  62. package/src/link_button.tsx +1 -1
  63. package/src/list_item.tsx +2 -2
  64. package/src/member_select.tsx +9 -4
  65. package/src/menu_button.tsx +2 -2
  66. package/src/menu_list_item.tsx +2 -2
  67. package/src/number_input.tsx +11 -4
  68. package/src/option_list.tsx +211 -0
  69. package/src/pdfjs_worker.d.ts +5 -0
  70. package/src/peek.tsx +1 -1
  71. package/src/picker.tsx +24 -213
  72. package/src/pressable_highlight.tsx +42 -23
  73. package/src/radio_picker.tsx +2 -2
  74. package/src/range_slider.tsx +35 -7
  75. package/src/react_native.d.ts +3 -0
  76. package/src/scroll_to_bottom.tsx +1 -1
  77. package/src/section.tsx +7 -2
  78. package/src/segmented_control.tsx +2 -2
  79. package/src/select.tsx +299 -0
  80. package/src/sort_header.tsx +1 -1
  81. package/src/sources.tsx +1 -1
  82. package/src/spreadsheet_view.tsx +43 -17
  83. package/src/status_grid.tsx +4 -3
  84. package/src/stepper.tsx +1 -1
  85. package/src/switch.tsx +5 -1
  86. package/src/switch_button.tsx +1 -1
  87. package/src/switcher.tsx +1 -1
  88. package/src/table.tsx +5 -1
  89. package/src/tabs.tsx +2 -2
  90. package/src/text_input_field.tsx +15 -4
  91. package/src/time_picker.tsx +11 -4
  92. package/src/timeline.tsx +1 -1
  93. package/src/uploading_thumbnail.tsx +212 -0
  94. package/src/use_focus_ring.ts +80 -0
  95. package/src/use_hover.ts +26 -0
  96. package/src/use_list_keyboard_nav.test.ts +71 -0
  97. package/src/use_list_keyboard_nav.ts +52 -5
  98. package/src/use_option_list.test.ts +193 -0
  99. package/src/use_option_list.ts +354 -0
  100. package/src/command_menu.tsx +0 -205
  101. package/src/hover_action.tsx +0 -54
  102. package/src/picker_menu.tsx +0 -355
  103. package/src/tag_input.tsx +0 -203
  104. package/src/time_field.tsx +0 -297
package/AGENTS.md CHANGED
@@ -8,7 +8,7 @@ every app inherits), never inline a one-off `View`/`Text` rebuild — that forfe
8
8
  typeahead, async search, virtualization, and a11y the primitive already ships.
9
9
 
10
10
  - Import per module: `import { Combobox } from "@lotics/ui/combobox"`.
11
- - Floating content (`Dialog` · `Popover` · `Tooltip` · `Alert` · `CommandMenu`) needs a
11
+ - Floating content (`Dialog` · `Popover` · `Tooltip` · `Alert` · `OptionList`) needs a
12
12
  `PortalHost` at the app root.
13
13
  - RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
14
14
  `div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
@@ -24,18 +24,21 @@ Pick by capability, not by name. (→ the source file for the API.)
24
24
 
25
25
  - **Actions** — `Button` (labelled; `title` is MANDATORY; `color` = emphasis/risk),
26
26
  `IconButton` (icon-ONLY — the circular affordance; an icon-only Button is a type error, so reach
27
- here. `size` `lg` 40px / `md` 28px / `sm` 24px, the SAME `color` palette as Button, plus `loading`;
28
- needs `accessibilityLabel`/`tooltip`), `LinkButton` (quiet ghost
27
+ here. `size` `lg` 40px / `md` 28px / `sm` 24px, the SAME `color` palette as Button, plus `loading`
28
+ and `elevated` (white fill + border + shadow, for a button sitting ON imagery — a tile's remove ✕,
29
+ an overlay's retry); needs `accessibilityLabel`/`tooltip`), `LinkButton` (quiet ghost
29
30
  action — Clear / Select all), `Chip` (dismissible facet chip). A button is never a raw
30
31
  `Pressable`. For a link OUT (a URL / record / document) use `Link` — underline+blue, the
31
32
  destination signal; the opposite of `LinkButton` (a quiet in-app action, never underlined).
32
- - **Pick from a list** — `Picker` (known options, native typeahead, single/multi/custom-render,
33
- no search box). Search-as-you-type / async / create-new → `Combobox`. A selectable card row →
34
- `CardSelectItem`. For a SELECT-FIELD picker, render each option as its colored chip
35
- (`renderOptionContent={(o) => <OptionBadge value={o} />}`).
36
- - **Pick member(s)** `MemberSelect` (a `Picker` that renders each option as a `MemberChip`,
33
+ - **Pick from a list** — `Picker` (native `<select>`, plain label-only single, native typeahead)
34
+ or `Select` (the rich one — custom-rendered options, single/multi, select-all). Search-as-you-type
35
+ / async / create-new `Combobox`. A selectable card row `CardSelectItem`. For a SELECT-FIELD
36
+ picker, render each option as its colored chip on `Select`
37
+ (`renderOptionContent={(o) => <OptionBadge value={o} />}`). `Select`/`Combobox` all open
38
+ the one shared `OptionList` body; `Picker` is the only one that's a native dropdown.
39
+ - **Pick member(s)** — `MemberSelect` (a `Select` that renders each option as a `MemberChip`,
37
40
  single or multi) — the ready member picker; pass it the roster (`members={useMembers().members}`).
38
- Don't re-wire `Picker` + `renderOptionContent` + a directory by hand. To edit a `select_member`
41
+ Don't re-wire `Select` + `renderOptionContent` + a directory by hand. To edit a `select_member`
39
42
  field IN PLACE (chip at rest → picker on click) use its inline-edit twin `InlineMemberSelect`.
40
43
  - **A person / member (display)** — `MemberChip` (avatar + name + optional secondary line) — the
41
44
  ONE way to show a member inline: a picker option, an assignee, a `select_member` value. Pure:
@@ -46,16 +49,37 @@ Pick by capability, not by name. (→ the source file for the API.)
46
49
  badge) — never hand-map option-key → color. Feed it a resolved option (`useFieldOptions` for a
47
50
  picker option, or `byKey(readSelect(cell)[0]?.key)` for a stored value); multi-select wraps to
48
51
  one badge each; a missing/unknown color token degrades to neutral.
49
- - **Tags / multi-select chip box** — `TagInput` (chips + an Add-popover checklist + create), NOT
50
- `Combobox multi` (see §Data entry).
52
+ - **Tags / multi-value chip box** — `Select multi` whose `renderSelected` returns a removable
53
+ `<Chip onDismiss={remove}>` (with `searchable` + `allowCustom`). The chip box is COMPOSED, not a
54
+ separate control — there's no `display` mode; `renderSelected` is the seam (see §Data entry).
51
55
  - **Text & form** — `TextInputField`, `NumberInput`, `SearchInput`; wrap with `FormField`;
52
56
  `Checkbox`, `Switch`, `RadioPicker`; dates via `DatePicker` / `DateRangeFilterField`, times
53
57
  via `TimePicker`.
54
58
  - **Edit a record's fields in place** — the `Inline*` family: `InlineTextInput` ·
55
59
  `InlineNumberInput` · `InlineSelect` · `InlineMemberSelect` · `InlineDatePicker` ·
56
60
  `InlineTimePicker` (see §Data entry).
57
- - **Tabular data** — `Table` (columns defined once; sortable headers via `SortHeader`; paired
58
- with `Pagination`). Never an HTML `<table>` or a `.map` of rows.
61
+ - **Tasks / to-dos** — there is NO Task component (a task row varies too much app-to-app to abstract
62
+ well a Reminders row and a Linear column-grid share almost nothing). Compose the row directly:
63
+ `CheckCircle` (the completion ring — fills with a spring, distinct from the square `Checkbox`) + an
64
+ inline-editable title (`InlineTextInput` with `struck` for done) + whatever meta cells you add
65
+ (`OptionBadge` dot tags, `InlineDatePicker`, `InlineMemberSelect`). The one reusable primitive is
66
+ `CheckCircle`; the rest is composition — see `tpl_tasks` (quick list) and `tpl_task_board` (columns).
67
+ - **Tabular data — pick by SCALE + intent.** Two columnar shapes, and the choice is about data size:
68
+ - **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
69
+ headers via `SortHeader`; paired with `Pagination`) + read-only rows that open a `Drawer` to edit.
70
+ It scales by PAGING — renders one page, never the whole set — so you FILTER + search, you don't
71
+ group. Worked example: `tpl_dossier`. Never an HTML `<table>` or a `.map` of rows.
72
+ - **Inline-managed grouped table** (MODERATE — hundreds, low-thousands — you MANAGE in view) — the
73
+ `DataGrid` primitive: a grouped, sortable grid whose cells are LIVE inline editors (ANY field — a
74
+ column is `{ key, label, width?, sortable?, cell: (item) => ReactNode }`, so `InlineMemberSelect` /
75
+ `InlineDatePicker` / `InlineNumberInput` / `InlineSelect` / a borderless `Select multi` for tags / colour-dot `OptionBadge`).
76
+ `DataGrid` owns the sortable header + collapsible grouped sections + aligned rows + an optional
77
+ per-row `leading` slot (a `CheckCircle`); YOU own the data, the sort/group/filter/collapse STATE
78
+ (`cycleSort` + `sortBy`, a `collapsed` Set), the toolbar (`SearchInput` + `FilterChip`s), and the
79
+ per-group add row (`renderGroupFooter`, aligned with the exported `gridRowStyle` + the column
80
+ widths). It renders ALL rows (no virtualization), so it's only for sets small enough to hold in
81
+ view — at 10k+ it lags, and grouping + pagination/infinite don't compose; use the register instead.
82
+ Worked examples: `tpl_task_board` (with a `CheckCircle` leading), `tpl_pipeline` (no leading).
59
83
  - **Numbers & charts** — `KPIStrip` / `KPICard` / `Metric` (headline figures), `TrendChip`
60
84
  (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
61
85
  recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
@@ -70,11 +94,25 @@ Pick by capability, not by name. (→ the source file for the API.)
70
94
  (bulk-select bar).
71
95
  - **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`,
72
96
  `Counter`), `Chip`.
73
- - **Overlays** — `Dialog`, `Drawer` (+ `DrawerFooter`), `Popover`, `Tooltip`, `CommandMenu`,
97
+ - **Overlays** — `Dialog`, `Drawer` (+ `DrawerFooter`), `Popover`, `Tooltip`, `OptionList`
98
+ (the searchable list body — host it in a `Popover`/`Dialog` for a command palette),
74
99
  `Alert` (the blocking confirm).
75
100
  - **Status / feedback** — `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`,
76
101
  `CompletionState`, `ActivityIndicator` / `Loading`, `Skeleton`.
77
- - **Files** — `FileDropzone`, `FileThumbnail` / `FileThumbnailGrid`, `FilePreview` /
102
+ - **Files** — `FilesEditor` (THE all-in-one attachment field: an upload-aware grid + a toolbar below
103
+ it that swaps into a batch SELECT mode, full-screen preview, download/share, and Alert-confirmed
104
+ remove — the host only owns `files` + wires `onAdd`/`onRemove`; mirrors the frontend
105
+ `cell_files_editor`. Reach for this first for "manage a record's attachments"), `FileDropzone`,
106
+ `FileRows` (batteries-included file LIST: tap a row → built-in
107
+ full-screen gallery, with a per-row trailing ⋯ menu = Download · Open-external · Remove; the default "here are some files"
108
+ surface), `FileGrid` (the upload-aware grid: completed files + a live upload
109
+ queue in one surface — `FilesEditor` is this + the toolbar; reach for `FileGrid` bare when you own
110
+ the chrome), `FileThumbnail` / `FileThumbnailGrid`
111
+ (square tiles, display-only), `UploadingThumbnail` (the single in-flight tile FileGrid renders —
112
+ reach for it only when hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document
113
+ LINE — badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge / action
114
+ / remove; `onPress` makes the whole row a pressable door, `trailing` stays an independently-pressable
115
+ sibling; for checklists & readable lists), `FileBadge` (the two-tone type mark), `FilePreview` /
78
116
  `FileGalleryModal`, `ImageGallery`; for gated CRUD compose locally with `useSelectionMode` +
79
117
  `shareOrDownloadFiles` + `rotateImageToBlob` (see §Data entry → Attachments).
80
118
  - **Specialized work surfaces** — `ScanField` (scan/verify), `Stepper` (a guided run / progress
@@ -111,7 +149,7 @@ This is the most common thing to get right. Match the JOB to the pattern:
111
149
  | a RELATED record (pick or make) | **find-or-create** (`Combobox allowCustom`) | one control covers both |
112
150
  | REPEATING rows you build & revise | **line items** (create→preview→edit) | add / edit / remove, live totals |
113
151
  | CHARGES that bill onto documents | **billing** (`tpl_billing`) | the invoice document is the unit |
114
- | a multi-value TAG field | **`TagInput`** | a chip box, not a search input |
152
+ | a multi-value TAG field | **`Select multi`** (`renderSelected` → `Chip`) | chips composed, not a separate control |
115
153
  | ONE choice from a small visible set | **`ChipGroup` pills** (or `RadioPicker`) | required single-select, one tap, every option visible |
116
154
  | a STATUS with terminal outcomes | **disposition** (open → resolve → revise) | guides the decision |
117
155
  | FILES | **attachment field** (dropzone + grid + gallery) | add / preview / delete |
@@ -124,10 +162,12 @@ form mode or a preview↔edit card — make each VALUE inline-editable: it reads
124
162
  hover tints it (no pencil — that shifts), click swaps the input in **at the same height** (zero
125
163
  reflow, the whole point), and it commits on blur (Enter saves, Escape reverts) or via
126
164
  `controls="buttons"` (✓ primary / ✕). One per type — `InlineTextInput` · `InlineNumberInput`
127
- (`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats a
128
- `Picker` menu in a popover so the row never grows; `renderValue` renders the RESTING value as a
165
+ (`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats an
166
+ `OptionList` in a popover so the row never grows; `renderSelected` renders the RESTING value as a
129
167
  node — a colored `OptionBadge`, not just a label) · `InlineMemberSelect` (a `MemberChip` at rest →
130
- member picker; the inline twin of `MemberSelect`) · `InlineDatePicker` · `InlineTimePicker` — all
168
+ member picker; the inline twin of `MemberSelect`) · `InlineDatePicker` (`format="datetime"` for
169
+ always-on time; `optionalTime` to let the user ADD/REMOVE a time — the value's own shape, date vs
170
+ datetime, is the source of truth) · `InlineTimePicker` — all
131
171
  on `useInlineEdit` + `InlineEditView` (custom inputs join via those). `onSave` is async: the
132
172
  saving spinner sits INSIDE the control (never a sibling — that reflows); an error shows inline
133
173
  without losing the edit. Pair with `DetailRow` (label left, inline value right). Not every field
@@ -178,13 +218,15 @@ missing methods (`Alert.alert` listing each) rather than a vague "incomplete." A
178
218
  **deposit** is its own card and its own receipt — never folded into the total due. Composition over
179
219
  `Card` + `NumberInput` + `Picker` + `Badge`; no new primitive.
180
220
 
181
- ### Tag field — `TagInput`, NOT `Combobox multi`
182
- A tag field's resting state should be a tidy CHIP BOX: `TagInput` is a bordered box of removable
183
- chips + an Add affordance; searching/creating happens in a POPOVER (a searchable, checkable list +
184
- an optional `allowCreate` row), never an inline token input. `Combobox multi` squeezes a
185
- `TextInput` among the chips right for a SEARCH field that accumulates picks, wrong for a chip box
186
- you occasionally add to. One owner per shape: chips-you-edit `TagInput`; search-that-accumulates
187
- `Combobox multi`.
221
+ ### Tag / multi-value field — `Select multi`
222
+ A tag field's resting state should be a tidy CHIP BOX and that's just a multi `Select` whose
223
+ `renderSelected` returns a removable `<Chip onDismiss={remove}>`. There's NO separate component and no
224
+ `display` mode: `renderSelected(item, { remove })` composes the anchor render a `Chip` with `remove`
225
+ for the ✕, or a plain `OptionBadge`/`MemberChip`/custom pill that ignores it; `renderOptionContent`
226
+ renders the menu rows. `searchable` adds the filter field, `allowCustom` the create row. Borderless for
227
+ a grid cell? Pass a `style`, never a `variant`. `Combobox` is the SINGLE-value sibling — for a search
228
+ that emits one pick at a time and renders the selection elsewhere, use `reflectSelection={false}`
229
+ (see `tpl_*`).
188
230
 
189
231
  ### Disposition — lifecycle status is ASYMMETRIC by phase
190
232
  When a status is an OPEN default plus terminal OUTCOMES the user decides (Lead → Closed/Lost, Draft
@@ -198,24 +240,54 @@ quiet, REVERSIBLE **Change**: a popover STATE SWITCHER (each state a colored dot
198
240
  marked + disabled), not a generic text menu. A composition (Badge + Button + Popover/MenuButton).
199
241
 
200
242
  ### Attachments — a full add / preview / DELETE field
243
+ **Default: `<FilesEditor files onAdd onRemove>`** — it bundles the grid + a toolbar (Upload · Select ·
244
+ Download all) + a batch SELECT mode + the gallery + confirmed-remove; the host only owns `files` and
245
+ wires `onAdd` (picked → its upload) / `onRemove`. Reach for the lower-level pieces below only when you
246
+ need custom chrome:
201
247
  Capture with `<FileDropzone onFiles accept label hint dropLabel height>` (drag-over lights the
202
- accent; click falls back to a picker); display what landed with `<FileThumbnailGrid files>` ABOVE
248
+ accent; click falls back to a picker); display what landed with **`<FileGrid files uploads>`** ABOVE
203
249
  the dropzone (existing files are the content; the dropzone sinks to the bottom as the "add more"
204
- affordance — only the empty state leads with it). Make it CRUDable by wiring the grid's two
205
- callbacks: `onFilePress` set a `number|null` index that drives `<FileGalleryModal files
206
- activeIndex onIndexChange>` (built-in prev/next/ESC/rotate); `onRemove` drop the file from state
207
- (the grid renders a on each thumbnail automatically). Keep label+grid+dropzone on a `gap` (a bare
208
- `CardBody` has none). `FileThumbnail` shows the right surface per MIME (image thumbnail · doc card ·
209
- media card), and takes `uploading` to dim it with a spinner while the bytes are still going up.
210
- Never hand-build a drop well, a file row grid, or a preview modal. In a custom-code APP, don't
211
- hand-roll the upload+preview state either `@lotics/app-sdk` `useAttachments()` gives the chat's
212
- instant-preview lifecycle (local object-URL now, `file_id` on complete); map each `AttachedFile` to a
213
- `DisplayFile` (snake camel: `preview_url`→`url`, `mime_type`→`mimeType`) for
214
- `<FileThumbnail file={…} uploading={f.status === "uploading"} />`.
250
+ affordance — only the empty state leads with it). `FileGrid` is the upload-aware grid: `files` are
251
+ the saved/completed `DisplayFile`s, `uploads` is the LIVE add-queue (`FileUpload[]`) it interleaves
252
+ both and renders each in-flight tile itself a LABELED status overlay (uploading spinner · "Retrying"
253
+ · "Paused" · "Upload failed" + a retry button · "Can't upload" for a dead/empty file) — so you never
254
+ hand-map an upload to a `FileThumbnail`. Localize the labels with `labels.upload` (an
255
+ `UploadStatusLabels`; the Lotics `@lotics/ui-internal` adapter already injects vi/en). Make it CRUDable
256
+ by wiring its callbacks: `onFilePress`
257
+ set a `number|null` index that drives `<FileGalleryModal files activeIndex onIndexChange>` — a
258
+ FULL-SCREEN viewer with a toolbar (filename · counter · inline action buttons download, optional
259
+ `onOpenExternal`/`onRemove` · close-✕), prev/next, ESC, and rotate; `onDisplayRemove`/`onUploadRemove`
260
+ drop the file / cancel the upload (the grid renders a ✕ on each tile automatically); `onRetry` /
261
+ `onRetryAll` for failed uploads. In a sandboxed custom-code app the toolbar's "open in new tab" can't
262
+ pop a window — pass `onOpenExternal` wired to the SDK's `openExternal` (omit it elsewhere and the
263
+ action hides). PDF/Word/Excel/CSV all render INLINE here (pdf.js / `@lotics/docx` / `@lotics/xlsx`,
264
+ lazy) — no native `<iframe>` viewer, so they work inside the cross-origin app iframe. Keep
265
+ label+grid+dropzone on a `gap` (a bare `CardBody` has none). By default `FileGrid` tiles FILL the
266
+ container width — a uniform size (≥ `minItemWidth`, default 96) so full rows span the width and reflow
267
+ as it changes; a short last row keeps that size and left-aligns (no stretched tiles, no empty cells).
268
+ Pass `columns` for a fixed column count, or `itemSize` for exact fixed-size tiles. For a dense strip
269
+ in a FIXED-width container that must NOT wrap, pass `singleRow` — it fits as many tiles as the width
270
+ allows and collapses the rest into a clickable "+N" overflow tile (`onOverflowPress(hiddenCount)`,
271
+ which the host wires — e.g. open the gallery at the first hidden file). This is the responsive overflow
272
+ pattern (avatar stack / Finder); prefer it over a hardcoded `maxVisible`, which can't adapt to width.
273
+ Under the hood it composes `FileThumbnail` (the completed tile — right surface per
274
+ MIME: image thumbnail · a doc tile with the `FileBadge` centered + a single-line filename · media
275
+ card; also takes `isTemplate` to overlay a TMPL marker) and `UploadingThumbnail` (the in-flight tile);
276
+ reach for either directly only when hand-rolling a NON-grid layout. **When the filename must be
277
+ readable, use `FileRow`** — a horizontal LINE (`FileBadge` or a `placeholder` + the FULL name + a meta
278
+ line + a composable `trailing` slot). `onPress` makes the whole row a pressable door (open the file);
279
+ `trailing` (a status `Badge`, an action, a remove ✕) stays an independently-pressable sibling. For
280
+ attachment lists / message files / document checklists where a square tile truncates the name. Never hand-build a drop well, a file grid, the upload tiles, or a preview
281
+ modal. In a custom-code APP, don't hand-roll the upload+preview state either — `@lotics/app-sdk`
282
+ `useAttachments()` gives the chat's instant-preview lifecycle (local object-URL now, `file_id` on
283
+ complete); map each `AttachedFile` to a `FileUpload` (ready → `{ status: "complete", id, file: {…} }`,
284
+ else `{ status, id, filename, mimeType: mime_type, previewUrl: preview_url }`) and feed `FileGrid`'s
285
+ `uploads` — `onUploadRemove={remove}`.
215
286
 
216
287
  **Gated file CRUD is a PATTERN, composed locally — not a sealed kit component.** When a file field must
217
- GUARD deletion (operator-facing, accidental delete is a real risk), compose it in the app from the kit
218
- pieces; don't reach for a one-size widget (the action set + layout vary per app the host frontend's
288
+ GUARD deletion (operator-facing, accidental delete is a real risk), DON'T use `FileGrid` (its per-tile
289
+ is exactly the stray-tap risk you're guarding against) compose it in the app from the kit pieces;
290
+ don't reach for a one-size widget (the action set + layout vary per app — the host frontend's
219
291
  `files_editor` and each app compose their own). The recipe: `FileDropzone` (add) + `FileThumbnailGrid
220
292
  files selectedIds onFilePress` (pass `selectedIds` ONLY in select mode) + `FileGalleryModal` (view +
221
293
  rotate + persist) + a `Dialog` confirm, all driven by **`useSelectionMode()`** — the reusable LOGIC. The
@@ -379,7 +451,8 @@ used to carry now reads structurally — **provenance** is an uppercase microlab
379
451
  (`PROPOSED` · `MATCH` · `MISMATCH` · `SUGGESTED EDIT` · `QUESTION`), and the agent's **reasoning** is
380
452
  a left-ruled margin note (a hairline rule + muted text), quoted apart from the facts and the human's
381
453
  controls — while **status/severity/diffs use functional colour** the way the rest of the kit does:
382
- - **Confidence** — a 3-tick meter + word, emerald / amber / zinc by level.
454
+ - **Confidence** — a 3-tick meter + the full phrase ("High confidence" / "Medium" / "Low"), emerald /
455
+ amber / zinc by level; `labels` to translate (one phrase per level).
383
456
  - **Finding severity** — a coloured dot `Badge` + metric: red critical, amber warning, blue note,
384
457
  emerald on-track (+ stacking order, most severe first).
385
458
  - **Stepper / AgentRun nodes** — progress dots on a spine: `current` a **pulsing accent ring** (white
@@ -434,11 +507,35 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
434
507
  (Tabs/SegmentedControl/RadioPicker) keep ONE stop at `0`, the rest `-1`. And NEVER let a FOCUSED
435
508
  control unmount — a conditional pointer affordance that vanishes on use (e.g. an "apply suggested
436
509
  value" pill shown only while a field is empty) must be `tabIndex={-1}`, or the browser drops focus
437
- to `<body>` and the next Tab jumps to the page's first focusable. A mouse-opened popover trigger
438
- (Picker/Combobox/InlineSelect/InlineDatePicker) gets no `:focus-visible` ring, so it wears
439
- **`ACTIVE_RING`** (`control_surface.ts`, a `0 0 0 2px zinc-900` box-shadow mirroring the focus
440
- outline) on its open state reuse that token, don't hand-roll a thin 1px edge. Full rules + the
441
- keyboard test: `docs/accessibility.md` Focus & tab order.
510
+ to `<body>` and the next Tab jumps to the page's first focusable. Full rules + the keyboard test:
511
+ `docs/accessibility.md` Focus & tab order.
512
+ - **Focus rings are per-component, never global.** There is NO global focus CSS — `index.css` only
513
+ resets the native outline. Every interactive control paints its OWN ring; nothing rings unless it
514
+ opts in. Three ways, pick by base:
515
+ - **`FocusRingPressable`** (`@lotics/ui/focus_ring_pressable`) — a `Pressable` that rings on keyboard
516
+ focus; the default for a raw interactive element (cell, tile, nav button, menu option). Forwards
517
+ all `PressableProps` (onPress, accessibilityRole/Label/State, ref, style). Reach for this before
518
+ hand-rolling a Pressable + the hook.
519
+ - **`PressableHighlight`** — set the opt-in **`focusRing`** prop (or read `state.focusVisible` from
520
+ its style-fn / children for a bespoke treatment, e.g. `CardSelectItem` which rings on hover/press/focus).
521
+ - **`useFocusRing`** (`@lotics/ui/use_focus_ring`) — the underlying hook for inputs / custom surfaces.
522
+ Returns `{ focusVisible, focused, focusProps }`; spread `focusProps`, apply
523
+ `focusVisible && { boxShadow: FOCUS_RING }` (comma-join any existing box-shadow). `useFocusRing({ always: true })`
524
+ for text-like inputs/selects (ring on ANY focus, as browsers do for typing-capable fields); plain
525
+ controls omit it (keyboard-only).
526
+ The ring is the shared **`FOCUS_RING`** token (`@lotics/ui/control_surface`, `0 0 0 2px zinc-900`). A
527
+ mouse-opened popover trigger (Select/Combobox/InlineSelect/InlineDatePicker) wears the SAME token on
528
+ its open state so it reads identically to a keyboard-focused control. **Coverage is a contract:** EVERY
529
+ interactive control must ring (a focusable control with no focus treatment is a bug — the per-Pressable
530
+ audit is the gate); a surface that shouldn't ring is a non-control → `tabIndex={-1}`, not a missing ring.
531
+ - **A trigger's hover/focus is a contract too — build it on `PressableHighlight`, never a raw `Pressable`.**
532
+ Every popover/menu/select anchor (`Select`, `FilterChip`, `Switcher`, the inline editors, a custom
533
+ `PopoverTrigger`) rides `PressableHighlight` (or `FocusRingPressable`): it paints the hover **wash** +
534
+ focus ring AROUND the caller's `style`, so a borderless in-cell override (`style={{ borderColor:
535
+ "transparent" }}` on a `Select`) keeps the wash and reads like its inline neighbours. A raw `Pressable`
536
+ that hand-rolls its own hover is the bug — a caller's `borderColor` then erases the affordance. Layer the
537
+ hover-border BEFORE `style` (a transparent edge suppresses it → wash only) and the open ring (`FOCUS_RING`)
538
+ on the open state.
442
539
  - **Cards are banded — and composable** (all from `@lotics/ui/card`):
443
540
  ```tsx
444
541
  <Card style={{ padding: 0 }}>
@@ -479,6 +576,16 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
479
576
  padded container, NO `Divider`s (the gap + rounded hover separate them).
480
577
  Right-hand columns align only if every trailing element is FIXED-width (give each trailing action
481
578
  a fixed `width`, so amount/status columns don't jitter).
579
+ - **Row actions are ALWAYS-VISIBLE siblings, never hover-revealed, never nested.** A pressable row =
580
+ a role-less surface (the hover/press wash, via DOM `mouseenter` so it covers the whole row) + an
581
+ accessible "Open …" door (the body, a button) + trailing actions (⋯, remove ✕, edit) as SIBLINGS of
582
+ the door — a button must not contain another button, so the action is independently pressable and
583
+ the ✕ isn't swallowed by the row press (`FileRow` and `PressableRow` are the references). Actions
584
+ stay **visible** — a hover-only action is invisible to keyboard and touch users. The ONLY exception
585
+ is a DENSE tree/register where a persistent per-row action would clutter (e.g. the DB sidebar):
586
+ reveal on hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its
587
+ opacity, so it's still keyboard-reachable. Do that locally — there is no shared hover-reveal
588
+ primitive (the old `HoverAction` made the keyboard-inaccessible anti-pattern easy and was removed).
482
589
  - **Master-detail = list + workspace `Drawer` with sequencing.** Pressing a PRIMARY entity row
483
590
  opens the record workspace in a `Drawer` with `onPrev`/`onNext`/`position` ("3/24") over the
484
591
  visible ordering (←/→ built in). Key the drawer body by record id so per-record state resets on
@@ -503,7 +610,7 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
503
610
  · `ChipGroup` is THE one-of-N lens (≤ ~10 options the user flips between; include "All"; counts in
504
611
  the label). Filters ONE list to a SUBSET — including by process stage / lifecycle state.
505
612
  · `FilterChip` is THE secondary-dimension filter — a compact chip that opens a composed popover
506
- editor (`PickerMenu multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
613
+ editor (`OptionList multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
507
614
  pick via the `{({close}) => …}` render prop.
508
615
  · `ColumnFilter` is the TYPED column filter — give it a `FilterableColumn` (`text` / `number` /
509
616
  `select`) + a controlled `ColumnFilterValue` and it renders the right editor (contains / range /
@@ -566,13 +673,30 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
566
673
  `tpl_inventory` (threshold actions) · `tpl_dossier` (high-volume paginated register) ·
567
674
  `tpl_approvals` (verdicts: bulk + over-policy `Dialog`) · `tpl_reconcile` (pair matching) ·
568
675
  `tpl_dispatch` (capacity allocation) · `tpl_batch` (compose from parts) · `tpl_pick` (guided
569
- `ScanField` run) · `tpl_allocate` (`RemainderMeter` split) · `tpl_run` (preview → resolve → post).
676
+ `ScanField` run) · `tpl_allocate` (`RemainderMeter` split) · `tpl_run` (preview → resolve → post) ·
677
+ `tpl_tasks` (the QUICK LIST — Apple-Reminders shape for personal lists: `CheckCircle` rows you tick +
678
+ expand in place to edit (`Inline*`, stable row — the meta summary never shifts on expand), colour-dot
679
+ tags, an attachments field (`FileThumbnailGrid` → tap a tile to the gallery for download + confirmed
680
+ remove + an `Add attachment` `pickFiles` button), search + a clearable group-by (✕ → ungrouped) + tag
681
+ filter chips, and a primary Draft-from-notes CTA `Composer → AgentRun → ChangeReview`) ·
682
+ `tpl_task_board` (the COLUMNS shape: a search · group-by · filter toolbar over a grouped, sortable
683
+ grid of inline-editable cells — assignee/due/status/tags set directly, a files cell (thumbnail glance →
684
+ popover grid → gallery), per-group add rows that pre-set the group's field, and the same Draft-from-notes
685
+ CTA) · `tpl_pipeline` (the SAME inline-managed grouped-table shape, a second domain — deals grouped by
686
+ stage with a money-value cell + owner + close-date edited inline, its own Draft-from-notes CTA drafting
687
+ deals; proof the shape generalizes beyond tasks). All three: filters/search left, primary CTA right, one
688
+ row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option). **`tpl_task_board` + `tpl_pipeline` are the inline-managed
689
+ grouped table — MODERATE data you manage in view (group/sort/filter/edit-in-place, renders all
690
+ rows). For thousands+ you BROWSE, that's `tpl_dossier`'s paginated register, not this.** Both build
691
+ on the `DataGrid` primitive (the grid shape, extracted once these two uses confirmed it); each owns
692
+ only its data, toolbar, columns and per-group add. There is NO Task component — a task ROW varies
693
+ too much to bake into one, so rows compose `CheckCircle` + `InlineTextInput` + inline-editor cells.
570
694
  - **Data capture** — `tpl_intake` (multi-fieldset form + attachments) · `tpl_order` (the
571
695
  transactional form: find-or-create + line items + attachment CRUD + 2-col grid) · `tpl_billing`
572
696
  (charges→invoice→collect: the invoice DOCUMENT is the unit — editable charge lines + method,
573
697
  live total, status, inline-gated issue; validated receipt; separate deposit) · `tpl_quick`
574
698
  (quick-capture log) · `tpl_wizard` (`Stepper` form + review) · `tpl_record` (inline edit + Stage
575
- disposition + `TagInput` + Documents grid; `tpl_record_plain` = card-less).
699
+ disposition + `Select` tags + Documents grid; `tpl_record_plain` = card-less).
576
700
  - **Records & lookup** — `tpl_detail` (full record + tabs) · `tpl_directory` (searchable register) ·
577
701
  `tpl_timeline` (audit feed).
578
702
  - **Planning & time** — `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
@@ -604,22 +728,23 @@ option_badge (OptionBadge — a select value as its configured colored badge) ·
604
728
  member_chip (MemberChip — avatar + name; the universal person render) ·
605
729
  member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
606
730
  status_badge · button · icon_button · link · link_button · chip · tabs · segmented_control ·
607
- picker · combobox · tag_input (TagInputchip box + Add-popover; for tags, not Combobox multi) ·
731
+ picker (native `<select>`, plain label-only single) · select (Select — rich/custom-rendered, single/multi, select-all, chips via `renderSelected` + `searchable` + `allowCustom` — the tag field is just a multi Select; opens `OptionList`) · option_list (OptionListthe ONE shared searchable listbox body every selector opens: single/multi, optional internal search, create row, keyboard + native-`<select>` typeahead; host it directly in a `Popover`/`Dialog` for a command palette) · combobox (single-select editable search input; browses on focus; no `multi` — multi-value chips → `Select multi`) ·
608
732
  text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
609
733
  radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
610
734
  inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
611
735
  inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
612
- `InlineEditView`; `InlineSelect`/`InlineMemberSelect` take `renderValue` for a chip/badge at rest) ·
613
- list · list_item · menu_button · menu_list_item · detail_row · pressable_row · action_menu ·
736
+ `InlineEditView`; `InlineSelect`/`InlineMemberSelect` take `renderSelected` for a chip/badge at rest) ·
737
+ list · list_item · menu_button · menu_list_item · detail_row · pressable_row ·
738
+ 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 ·
614
739
  floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
615
740
  columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
616
- sort_header · table · 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`; the guided-run / agent-feed primitive — subsumes the old StepList) ·
741
+ 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`; the guided-run / agent-feed primitive — subsumes the old StepList) ·
617
742
  step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
618
- command_menu · alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
743
+ alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
619
744
  CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
620
745
  bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
621
746
  status_grid (StatusGrid + StatusLegend) · heatmap · legend_item · remainder_meter · allocation_row ·
622
- scan_field · file_dropzone · file_thumbnail · file_thumbnail_grid · file_preview ·
747
+ 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) + built-in gallery + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `labels`, `galleryLabels`); 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 ·
623
748
  file_gallery_modal · image_gallery · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
624
749
  spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
625
750
  comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
@@ -633,4 +758,7 @@ finding (Finding — one ranked briefing/audit insight) · discrepancy (Discrepa
633
758
  disagrees across sources) · triage_row (TriageRow — an item the agent classified + routed) ·
634
759
  scored_option (ScoredOption — an AI-ranked candidate) ·
635
760
  format_money · format_date · colors (solid · tint · ramp · ColorName ·
636
- isColorName · asColorName — coerce a stored option/status token to a ColorName, neutral fallback).
761
+ isColorName · asColorName — coerce a stored option/status token to a ColorName, neutral fallback) ·
762
+ use_focus_ring (useFocusRing — keyboard-aware focus state for painting a control's own ring; see Focus rings) ·
763
+ focus_ring_pressable (FocusRingPressable — a Pressable that rings on keyboard focus; the raw-control default) ·
764
+ control_surface (CONTROL_HEIGHT · CONTROL_RADIUS · FOCUS_RING · chipSurfaceStyle — the shared control-surface tokens).
@@ -17,7 +17,7 @@ import { FilterChip, selectSummary } from "@lotics/ui/filter_chip";
17
17
  import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
18
18
  import { FormField } from "@lotics/ui/form_field";
19
19
  import { KPIStrip } from "@lotics/ui/kpi_strip";
20
- import { PickerMenu } from "@lotics/ui/picker_menu";
20
+ import { OptionList } from "@lotics/ui/option_list";
21
21
  import { Popover, PopoverContent, PopoverFooter, PopoverTrigger } from "@lotics/ui/popover";
22
22
  import { RangeSlider, rangeSummary } from "@lotics/ui/range_slider";
23
23
  import { PressableRow } from "@lotics/ui/pressable_row";
@@ -255,10 +255,10 @@ export function TplApprovals() {
255
255
  onValueChange={(t) => { setTab(t); setOpenId(null); }}
256
256
  />
257
257
  <FilterChip label="Requester" summary={reqSummary} onClear={() => setReqSel([])} clearLabel="Clear requester filter">
258
- <PickerMenu multi enableSelectAll options={REQUESTERS} value={reqSel} onValueChange={setReqSel} />
258
+ <OptionList search={{ mode: "none" }} multi options={REQUESTERS} value={reqSel} onValueChange={setReqSel} />
259
259
  </FilterChip>
260
260
  <FilterChip label="Role" summary={roleSummary} onClear={() => setRoleSel([])} clearLabel="Clear role filter">
261
- <PickerMenu multi options={ROLES} value={roleSel} onValueChange={setRoleSel} />
261
+ <OptionList search={{ mode: "none" }} multi options={ROLES} value={roleSel} onValueChange={setRoleSel} />
262
262
  </FilterChip>
263
263
  <FilterChip label="Amount" summary={amtSummary} onClear={() => setAmt([AMT_MIN, AMT_MAX])} clearLabel="Clear amount filter">
264
264
  <RangeSlider
@@ -8,7 +8,7 @@ import { Card } from "@lotics/ui/card";
8
8
  import { DetailRow } from "@lotics/ui/detail_row";
9
9
  import { Divider } from "@lotics/ui/divider";
10
10
  import { Drawer, DrawerFooter } from "@lotics/ui/drawer";
11
- import { FileBadge } from "@lotics/ui/file_badge";
11
+ import { FileRow } from "@lotics/ui/file_row";
12
12
  import { FileDropzone } from "@lotics/ui/file_dropzone";
13
13
  import { FileThumbnailGrid } from "@lotics/ui/file_thumbnail_grid";
14
14
  import { FileGalleryModal } from "@lotics/ui/file_gallery_modal";
@@ -37,8 +37,8 @@ import { Timeline, type TimelineItem } from "@lotics/ui/timeline";
37
37
  // required-document checklist with a free FILE DUMP for anything extra. The
38
38
  // drawer's ◀▶ keys walk the manager down the queue without closing it.
39
39
  //
40
- // Reference composition — Drawer (list nav) + Stepper + FileBadge (+ placeholder)
41
- // + Pagination + DetailRow.
40
+ // Reference composition — Drawer (list nav) + Stepper + FileRow (badge + name +
41
+ // meta + a trailing status/action slot) + Pagination + DetailRow.
42
42
  // ─────────────────────────────────────────────────────────────────────────────
43
43
 
44
44
  const CODES: Record<string, string> = {
@@ -141,21 +141,9 @@ function QueueRow({ app, active, onPress }: { app: Applicant; active: boolean; o
141
141
  );
142
142
  }
143
143
 
144
- // A document linea FileBadge (or a same-footprint placeholder) + name + meta.
145
- function FileRow({ name, meta, mimeType, placeholder, tone, action }: {
146
- name: string; meta?: string; mimeType?: string; placeholder?: boolean; tone?: "received" | "ready"; action?: ReactNode;
147
- }) {
148
- return (
149
- <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
150
- <FileBadge size={30} mimeType={mimeType} placeholder={placeholder} />
151
- <View style={{ flex: 1 }}>
152
- <Text size="sm" weight="medium">{name}</Text>
153
- {meta ? <Text size="xs" color="muted">{meta}</Text> : null}
154
- </View>
155
- {action ?? (tone === "received" ? <Badge variant="dot" label="Received" color="emerald" /> : tone === "ready" ? <Badge variant="dot" label="Generated" color="blue" /> : null)}
156
- </View>
157
- );
158
- }
144
+ // Document status chipsthe `trailing` slot content for the kit's `FileRow`.
145
+ const receivedBadge = <Badge variant="dot" label="Received" color="emerald" />;
146
+ const generatedBadge = <Badge variant="dot" label="Generated" color="blue" />;
159
147
 
160
148
  // The extra-documents zone — the data-capture attachment field: a thumbnail grid
161
149
  // (click to preview in the gallery, ✕ to delete) above a dropzone. Read-only once
@@ -189,7 +177,7 @@ function UploadsField({ uploads, editable, onAdd, onRemove }: {
189
177
  ) : uploads.length === 0 ? (
190
178
  <Text size="sm" color="muted">No extra files.</Text>
191
179
  ) : null}
192
- <FileGalleryModal files={uploads} activeIndex={active} onIndexChange={setActive} captionHint="ESC to close · ←/→ to move" />
180
+ <FileGalleryModal files={uploads} activeIndex={active} onIndexChange={setActive} />
193
181
  </View>
194
182
  );
195
183
  }
@@ -217,7 +205,7 @@ function StageSection({ app, index, onMarkDoc, onAddUploads, onRemoveUpload }: {
217
205
  if (status === "pending") {
218
206
  body = <Text size="sm" color="muted">{`Unlocks after ${STAGES[index - 1].label.toLowerCase()}.`}</Text>;
219
207
  } else if (index === 0) {
220
- body = <FileRow name="Application fee receipt" meta={status === "done" ? `${formatMoney(app.fee)} · issued` : formatMoney(app.fee)} mimeType="application/pdf" tone={status === "done" ? "ready" : undefined} />;
208
+ body = <FileRow name="Application fee receipt" meta={status === "done" ? `${formatMoney(app.fee)} · issued` : formatMoney(app.fee)} mimeType="application/pdf" trailing={status === "done" ? generatedBadge : undefined} />;
221
209
  } else if (index === 1) {
222
210
  body = (
223
211
  <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
@@ -229,7 +217,7 @@ function StageSection({ app, index, onMarkDoc, onAddUploads, onRemoveUpload }: {
229
217
  body = (
230
218
  <View style={{ gap: 10 }}>
231
219
  {GENERATED_DOCS.map((d) => (
232
- <FileRow key={d} name={d} mimeType="application/pdf" meta={status === "done" ? "generated" : "template"} tone={status === "done" ? "ready" : undefined} />
220
+ <FileRow key={d} name={d} mimeType="application/pdf" meta={status === "done" ? "generated" : "template"} trailing={status === "done" ? generatedBadge : undefined} />
233
221
  ))}
234
222
  </View>
235
223
  );
@@ -245,9 +233,9 @@ function StageSection({ app, index, onMarkDoc, onAddUploads, onRemoveUpload }: {
245
233
  meta={d.received ? `${d.file} · received` : "Awaiting upload"}
246
234
  mimeType={d.received ? "application/pdf" : undefined}
247
235
  placeholder={!d.received}
248
- action={status === "current"
236
+ trailing={status === "current"
249
237
  ? (d.received ? <Button title="Remove" color="muted" onPress={() => onMarkDoc(d.name, false)} /> : <Button title="Upload" color="secondary" onPress={() => onMarkDoc(d.name, true)} />)
250
- : (d.received ? <Badge variant="dot" label="Received" color="emerald" /> : undefined)}
238
+ : (d.received ? receivedBadge : undefined)}
251
239
  />
252
240
  ))}
253
241
  </View>
@@ -21,7 +21,7 @@ import { Table, TableRow, TableCell, type TableColumn } from "@lotics/ui/table";
21
21
  import { SearchInput } from "@lotics/ui/search_input";
22
22
  import { ChipGroup } from "@lotics/ui/chip_group";
23
23
  import { FilterChip, selectSummary } from "@lotics/ui/filter_chip";
24
- import { PickerMenu } from "@lotics/ui/picker_menu";
24
+ import { OptionList } from "@lotics/ui/option_list";
25
25
  import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
26
26
  import { Combobox } from "@lotics/ui/combobox";
27
27
  import type { PickerOption } from "@lotics/ui/picker";
@@ -356,9 +356,9 @@ export function TplDossier() {
356
356
  onClear={() => { setAssignee([]); setPage(0); }}
357
357
  clearLabel="Clear assignee filter"
358
358
  >
359
- <PickerMenu
359
+ <OptionList
360
+ search={{ mode: "none" }}
360
361
  multi
361
- enableSelectAll
362
362
  options={ASSIGNEES}
363
363
  value={assignee}
364
364
  onValueChange={(v) => { setAssignee(v); setPage(0); }}
@@ -373,7 +373,8 @@ export function TplDossier() {
373
373
  clearLabel="Clear fee filter"
374
374
  >
375
375
  {({ close }) => (
376
- <PickerMenu
376
+ <OptionList
377
+ search={{ mode: "none" }}
377
378
  options={[{ value: "paid", label: "Paid" }, { value: "unpaid", label: "Unpaid" }]}
378
379
  value={feeStatus}
379
380
  onValueChange={(v) => { setFeeStatus(v); setPage(0); }}
@@ -477,7 +477,7 @@ export function TplOrder() {
477
477
  </Screen>
478
478
  </Dialog>
479
479
 
480
- <FileGalleryModal files={files} activeIndex={activeFile} onIndexChange={setActiveFile} captionHint="ESC to close · ←/→ to move" />
480
+ <FileGalleryModal files={files} activeIndex={activeFile} onIndexChange={setActiveFile} />
481
481
  </ScrollView>
482
482
  );
483
483
  }