@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.
- package/AGENTS.md +184 -56
- package/examples/tpl_approvals.tsx +3 -3
- package/examples/tpl_convert.tsx +11 -23
- package/examples/tpl_dossier.tsx +5 -4
- package/examples/tpl_order.tsx +1 -1
- package/examples/tpl_pipeline.tsx +400 -0
- package/examples/tpl_ratedesk.tsx +1 -1
- package/examples/tpl_record.tsx +6 -5
- package/examples/tpl_shifts.tsx +7 -5
- package/examples/tpl_task_board.tsx +473 -0
- package/examples/tpl_tasks.tsx +440 -0
- package/package.json +19 -6
- package/src/accordion.tsx +1 -1
- package/src/agent_progress.tsx +1 -1
- package/src/agent_run.tsx +1 -1
- package/src/breakdown.tsx +2 -2
- package/src/button.tsx +12 -3
- package/src/calendar/calendar_view.tsx +10 -10
- package/src/calendar/month_view.tsx +30 -11
- package/src/calendar/time_grid_view.tsx +25 -7
- package/src/card_select_item.tsx +11 -13
- package/src/check_circle.tsx +88 -0
- package/src/checkbox_input.tsx +5 -1
- package/src/chip.tsx +1 -1
- package/src/chip_group.tsx +1 -1
- package/src/column_filter.tsx +3 -2
- package/src/combobox.tsx +84 -219
- package/src/confidence.tsx +14 -3
- package/src/control_surface.ts +22 -7
- package/src/data_grid.tsx +159 -0
- package/src/date_calendar.tsx +46 -23
- package/src/date_field.tsx +10 -2
- package/src/date_filter.tsx +5 -9
- package/src/date_picker.tsx +78 -26
- package/src/date_picker_value.ts +11 -0
- package/src/date_range_filter_field.tsx +6 -4
- package/src/drawer.tsx +1 -1
- package/src/file_dropzone.tsx +5 -0
- package/src/file_gallery_modal.tsx +227 -139
- package/src/file_grid.tsx +184 -0
- package/src/file_preview.web.tsx +349 -12
- package/src/file_preview_types.ts +49 -0
- package/src/file_row.tsx +118 -0
- package/src/file_rows.tsx +109 -0
- package/src/file_thumbnail.tsx +132 -81
- package/src/file_thumbnail_grid.tsx +135 -55
- package/src/files_editor.tsx +232 -0
- package/src/filter_chip.tsx +25 -12
- package/src/focus_ring_pressable.tsx +34 -0
- package/src/form_picker.tsx +3 -3
- package/src/gantt/gantt_view.tsx +25 -7
- package/src/heatmap.tsx +5 -1
- package/src/icon_button.tsx +14 -3
- package/src/image_gallery.tsx +8 -14
- package/src/index.css +8 -12
- package/src/inline_date_picker.tsx +11 -11
- package/src/inline_edit.tsx +22 -6
- package/src/inline_member_select.tsx +1 -1
- package/src/inline_select.tsx +9 -8
- package/src/inline_text_input.tsx +4 -1
- package/src/link.tsx +11 -1
- package/src/link_button.tsx +1 -1
- package/src/list_item.tsx +2 -2
- package/src/member_select.tsx +9 -4
- package/src/menu_button.tsx +2 -2
- package/src/menu_list_item.tsx +2 -2
- package/src/number_input.tsx +11 -4
- package/src/option_list.tsx +211 -0
- package/src/pdfjs_worker.d.ts +5 -0
- package/src/peek.tsx +1 -1
- package/src/picker.tsx +24 -213
- package/src/pressable_highlight.tsx +42 -23
- package/src/radio_picker.tsx +2 -2
- package/src/range_slider.tsx +35 -7
- package/src/react_native.d.ts +3 -0
- package/src/scroll_to_bottom.tsx +1 -1
- package/src/section.tsx +7 -2
- package/src/segmented_control.tsx +2 -2
- package/src/select.tsx +299 -0
- package/src/sort_header.tsx +1 -1
- package/src/sources.tsx +1 -1
- package/src/spreadsheet_view.tsx +43 -17
- package/src/status_grid.tsx +4 -3
- package/src/stepper.tsx +1 -1
- package/src/switch.tsx +5 -1
- package/src/switch_button.tsx +1 -1
- package/src/switcher.tsx +1 -1
- package/src/table.tsx +5 -1
- package/src/tabs.tsx +2 -2
- package/src/text_input_field.tsx +15 -4
- package/src/time_picker.tsx +11 -4
- package/src/timeline.tsx +1 -1
- package/src/uploading_thumbnail.tsx +212 -0
- package/src/use_focus_ring.ts +80 -0
- package/src/use_hover.ts +26 -0
- package/src/use_list_keyboard_nav.test.ts +71 -0
- package/src/use_list_keyboard_nav.ts +52 -5
- package/src/use_option_list.test.ts +193 -0
- package/src/use_option_list.ts +354 -0
- package/src/command_menu.tsx +0 -205
- package/src/hover_action.tsx +0 -54
- package/src/picker_menu.tsx +0 -355
- package/src/tag_input.tsx +0 -203
- 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` · `
|
|
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
|
-
|
|
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` (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 `
|
|
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-
|
|
50
|
-
|
|
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
|
-
- **
|
|
58
|
-
|
|
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`, `
|
|
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** — `
|
|
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 | **`
|
|
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
|
|
128
|
-
`
|
|
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`
|
|
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 — `
|
|
182
|
-
A tag field's resting state should be a tidy CHIP BOX
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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).
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
`
|
|
214
|
-
|
|
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),
|
|
218
|
-
|
|
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 +
|
|
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.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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 (`
|
|
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 + `
|
|
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 ·
|
|
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 (OptionList — the 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 `
|
|
613
|
-
list · list_item · menu_button · menu_list_item · detail_row · pressable_row ·
|
|
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
|
-
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
package/examples/tpl_convert.tsx
CHANGED
|
@@ -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 {
|
|
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 +
|
|
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
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
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 chips — the `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}
|
|
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"
|
|
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"}
|
|
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
|
-
|
|
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 ?
|
|
238
|
+
: (d.received ? receivedBadge : undefined)}
|
|
251
239
|
/>
|
|
252
240
|
))}
|
|
253
241
|
</View>
|
package/examples/tpl_dossier.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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); }}
|
package/examples/tpl_order.tsx
CHANGED
|
@@ -477,7 +477,7 @@ export function TplOrder() {
|
|
|
477
477
|
</Screen>
|
|
478
478
|
</Dialog>
|
|
479
479
|
|
|
480
|
-
<FileGalleryModal files={files} activeIndex={activeFile} onIndexChange={setActiveFile}
|
|
480
|
+
<FileGalleryModal files={files} activeIndex={activeFile} onIndexChange={setActiveFile} />
|
|
481
481
|
</ScrollView>
|
|
482
482
|
);
|
|
483
483
|
}
|