@lotics/ui 5.9.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 +127 -44
- package/examples/tpl_approvals.tsx +3 -3
- package/examples/tpl_dossier.tsx +5 -4
- package/examples/tpl_pipeline.tsx +400 -0
- package/examples/tpl_record.tsx +5 -4
- 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 +10 -5
- 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 +43 -42
- package/src/file_preview.web.tsx +25 -11
- package/src/file_preview_types.ts +9 -0
- package/src/file_row.tsx +5 -1
- package/src/file_rows.tsx +6 -2
- package/src/file_thumbnail.tsx +13 -4
- package/src/file_thumbnail_grid.tsx +5 -0
- 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 +2 -2
- package/src/image_gallery.tsx +8 -10
- 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/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/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/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.
|
|
@@ -30,13 +30,15 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
30
30
|
action — Clear / Select all), `Chip` (dismissible facet chip). A button is never a raw
|
|
31
31
|
`Pressable`. For a link OUT (a URL / record / document) use `Link` — underline+blue, the
|
|
32
32
|
destination signal; the opposite of `LinkButton` (a quiet in-app action, never underlined).
|
|
33
|
-
- **Pick from a list** — `Picker` (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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`,
|
|
38
40
|
single or multi) — the ready member picker; pass it the roster (`members={useMembers().members}`).
|
|
39
|
-
Don't re-wire `
|
|
41
|
+
Don't re-wire `Select` + `renderOptionContent` + a directory by hand. To edit a `select_member`
|
|
40
42
|
field IN PLACE (chip at rest → picker on click) use its inline-edit twin `InlineMemberSelect`.
|
|
41
43
|
- **A person / member (display)** — `MemberChip` (avatar + name + optional secondary line) — the
|
|
42
44
|
ONE way to show a member inline: a picker option, an assignee, a `select_member` value. Pure:
|
|
@@ -47,16 +49,37 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
47
49
|
badge) — never hand-map option-key → color. Feed it a resolved option (`useFieldOptions` for a
|
|
48
50
|
picker option, or `byKey(readSelect(cell)[0]?.key)` for a stored value); multi-select wraps to
|
|
49
51
|
one badge each; a missing/unknown color token degrades to neutral.
|
|
50
|
-
- **Tags / multi-
|
|
51
|
-
|
|
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).
|
|
52
55
|
- **Text & form** — `TextInputField`, `NumberInput`, `SearchInput`; wrap with `FormField`;
|
|
53
56
|
`Checkbox`, `Switch`, `RadioPicker`; dates via `DatePicker` / `DateRangeFilterField`, times
|
|
54
57
|
via `TimePicker`.
|
|
55
58
|
- **Edit a record's fields in place** — the `Inline*` family: `InlineTextInput` ·
|
|
56
59
|
`InlineNumberInput` · `InlineSelect` · `InlineMemberSelect` · `InlineDatePicker` ·
|
|
57
60
|
`InlineTimePicker` (see §Data entry).
|
|
58
|
-
- **
|
|
59
|
-
|
|
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).
|
|
60
83
|
- **Numbers & charts** — `KPIStrip` / `KPICard` / `Metric` (headline figures), `TrendChip`
|
|
61
84
|
(delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG set — no
|
|
62
85
|
recharts), `RingGauge`, `ProgressBar` / `StackedProgressBar` / `StepProgress`, `Breakdown`
|
|
@@ -71,14 +94,20 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
71
94
|
(bulk-select bar).
|
|
72
95
|
- **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`,
|
|
73
96
|
`Counter`), `Chip`.
|
|
74
|
-
- **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),
|
|
75
99
|
`Alert` (the blocking confirm).
|
|
76
100
|
- **Status / feedback** — `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`,
|
|
77
101
|
`CompletionState`, `ActivityIndicator` / `Loading`, `Skeleton`.
|
|
78
|
-
- **Files** — `
|
|
79
|
-
|
|
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"
|
|
80
108
|
surface), `FileGrid` (the upload-aware grid: completed files + a live upload
|
|
81
|
-
queue in one surface —
|
|
109
|
+
queue in one surface — `FilesEditor` is this + the toolbar; reach for `FileGrid` bare when you own
|
|
110
|
+
the chrome), `FileThumbnail` / `FileThumbnailGrid`
|
|
82
111
|
(square tiles, display-only), `UploadingThumbnail` (the single in-flight tile FileGrid renders —
|
|
83
112
|
reach for it only when hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document
|
|
84
113
|
LINE — badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge / action
|
|
@@ -120,7 +149,7 @@ This is the most common thing to get right. Match the JOB to the pattern:
|
|
|
120
149
|
| a RELATED record (pick or make) | **find-or-create** (`Combobox allowCustom`) | one control covers both |
|
|
121
150
|
| REPEATING rows you build & revise | **line items** (create→preview→edit) | add / edit / remove, live totals |
|
|
122
151
|
| CHARGES that bill onto documents | **billing** (`tpl_billing`) | the invoice document is the unit |
|
|
123
|
-
| a multi-value TAG field | **`
|
|
152
|
+
| a multi-value TAG field | **`Select multi`** (`renderSelected` → `Chip`) | chips composed, not a separate control |
|
|
124
153
|
| ONE choice from a small visible set | **`ChipGroup` pills** (or `RadioPicker`) | required single-select, one tap, every option visible |
|
|
125
154
|
| a STATUS with terminal outcomes | **disposition** (open → resolve → revise) | guides the decision |
|
|
126
155
|
| FILES | **attachment field** (dropzone + grid + gallery) | add / preview / delete |
|
|
@@ -133,10 +162,12 @@ form mode or a preview↔edit card — make each VALUE inline-editable: it reads
|
|
|
133
162
|
hover tints it (no pencil — that shifts), click swaps the input in **at the same height** (zero
|
|
134
163
|
reflow, the whole point), and it commits on blur (Enter saves, Escape reverts) or via
|
|
135
164
|
`controls="buttons"` (✓ primary / ✕). One per type — `InlineTextInput` · `InlineNumberInput`
|
|
136
|
-
(`format` for currency/units) · `InlineSelect` (plain options OR `renderOptionContent`; floats
|
|
137
|
-
`
|
|
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
|
|
138
167
|
node — a colored `OptionBadge`, not just a label) · `InlineMemberSelect` (a `MemberChip` at rest →
|
|
139
|
-
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
|
|
140
171
|
on `useInlineEdit` + `InlineEditView` (custom inputs join via those). `onSave` is async: the
|
|
141
172
|
saving spinner sits INSIDE the control (never a sibling — that reflows); an error shows inline
|
|
142
173
|
without losing the edit. Pair with `DetailRow` (label left, inline value right). Not every field
|
|
@@ -187,13 +218,15 @@ missing methods (`Alert.alert` listing each) rather than a vague "incomplete." A
|
|
|
187
218
|
**deposit** is its own card and its own receipt — never folded into the total due. Composition over
|
|
188
219
|
`Card` + `NumberInput` + `Picker` + `Badge`; no new primitive.
|
|
189
220
|
|
|
190
|
-
### Tag field — `
|
|
191
|
-
A tag field's resting state should be a tidy CHIP BOX
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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_*`).
|
|
197
230
|
|
|
198
231
|
### Disposition — lifecycle status is ASYMMETRIC by phase
|
|
199
232
|
When a status is an OPEN default plus terminal OUTCOMES the user decides (Lead → Closed/Lost, Draft
|
|
@@ -207,6 +240,10 @@ quiet, REVERSIBLE **Change**: a popover STATE SWITCHER (each state a colored dot
|
|
|
207
240
|
marked + disabled), not a generic text menu. A composition (Badge + Button + Popover/MenuButton).
|
|
208
241
|
|
|
209
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:
|
|
210
247
|
Capture with `<FileDropzone onFiles accept label hint dropLabel height>` (drag-over lights the
|
|
211
248
|
accent; click falls back to a picker); display what landed with **`<FileGrid files uploads>`** ABOVE
|
|
212
249
|
the dropzone (existing files are the content; the dropzone sinks to the bottom as the "add more"
|
|
@@ -218,8 +255,8 @@ hand-map an upload to a `FileThumbnail`. Localize the labels with `labels.upload
|
|
|
218
255
|
`UploadStatusLabels`; the Lotics `@lotics/ui-internal` adapter already injects vi/en). Make it CRUDable
|
|
219
256
|
by wiring its callbacks: `onFilePress`
|
|
220
257
|
→ set a `number|null` index that drives `<FileGalleryModal files activeIndex onIndexChange>` — a
|
|
221
|
-
FULL-SCREEN viewer with a toolbar (filename · counter ·
|
|
222
|
-
`onOpenExternal`/`onRemove`
|
|
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`
|
|
223
260
|
→ drop the file / cancel the upload (the grid renders a ✕ on each tile automatically); `onRetry` /
|
|
224
261
|
`onRetryAll` for failed uploads. In a sandboxed custom-code app the toolbar's "open in new tab" can't
|
|
225
262
|
pop a window — pass `onOpenExternal` wired to the SDK's `openExternal` (omit it elsewhere and the
|
|
@@ -414,7 +451,8 @@ used to carry now reads structurally — **provenance** is an uppercase microlab
|
|
|
414
451
|
(`PROPOSED` · `MATCH` · `MISMATCH` · `SUGGESTED EDIT` · `QUESTION`), and the agent's **reasoning** is
|
|
415
452
|
a left-ruled margin note (a hairline rule + muted text), quoted apart from the facts and the human's
|
|
416
453
|
controls — while **status/severity/diffs use functional colour** the way the rest of the kit does:
|
|
417
|
-
- **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).
|
|
418
456
|
- **Finding severity** — a coloured dot `Badge` + metric: red critical, amber warning, blue note,
|
|
419
457
|
emerald on-track (+ stacking order, most severe first).
|
|
420
458
|
- **Stepper / AgentRun nodes** — progress dots on a spine: `current` a **pulsing accent ring** (white
|
|
@@ -469,11 +507,35 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
469
507
|
(Tabs/SegmentedControl/RadioPicker) keep ONE stop at `0`, the rest `-1`. And NEVER let a FOCUSED
|
|
470
508
|
control unmount — a conditional pointer affordance that vanishes on use (e.g. an "apply suggested
|
|
471
509
|
value" pill shown only while a field is empty) must be `tabIndex={-1}`, or the browser drops focus
|
|
472
|
-
to `<body>` and the next Tab jumps to the page's first focusable.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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.
|
|
477
539
|
- **Cards are banded — and composable** (all from `@lotics/ui/card`):
|
|
478
540
|
```tsx
|
|
479
541
|
<Card style={{ padding: 0 }}>
|
|
@@ -548,7 +610,7 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
|
|
|
548
610
|
· `ChipGroup` is THE one-of-N lens (≤ ~10 options the user flips between; include "All"; counts in
|
|
549
611
|
the label). Filters ONE list to a SUBSET — including by process stage / lifecycle state.
|
|
550
612
|
· `FilterChip` is THE secondary-dimension filter — a compact chip that opens a composed popover
|
|
551
|
-
editor (`
|
|
613
|
+
editor (`OptionList multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
|
|
552
614
|
pick via the `{({close}) => …}` render prop.
|
|
553
615
|
· `ColumnFilter` is the TYPED column filter — give it a `FilterableColumn` (`text` / `number` /
|
|
554
616
|
`select`) + a controlled `ColumnFilterValue` and it renders the right editor (contains / range /
|
|
@@ -611,13 +673,30 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
611
673
|
`tpl_inventory` (threshold actions) · `tpl_dossier` (high-volume paginated register) ·
|
|
612
674
|
`tpl_approvals` (verdicts: bulk + over-policy `Dialog`) · `tpl_reconcile` (pair matching) ·
|
|
613
675
|
`tpl_dispatch` (capacity allocation) · `tpl_batch` (compose from parts) · `tpl_pick` (guided
|
|
614
|
-
`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.
|
|
615
694
|
- **Data capture** — `tpl_intake` (multi-fieldset form + attachments) · `tpl_order` (the
|
|
616
695
|
transactional form: find-or-create + line items + attachment CRUD + 2-col grid) · `tpl_billing`
|
|
617
696
|
(charges→invoice→collect: the invoice DOCUMENT is the unit — editable charge lines + method,
|
|
618
697
|
live total, status, inline-gated issue; validated receipt; separate deposit) · `tpl_quick`
|
|
619
698
|
(quick-capture log) · `tpl_wizard` (`Stepper` form + review) · `tpl_record` (inline edit + Stage
|
|
620
|
-
disposition + `
|
|
699
|
+
disposition + `Select` tags + Documents grid; `tpl_record_plain` = card-less).
|
|
621
700
|
- **Records & lookup** — `tpl_detail` (full record + tabs) · `tpl_directory` (searchable register) ·
|
|
622
701
|
`tpl_timeline` (audit feed).
|
|
623
702
|
- **Planning & time** — `tpl_calendar` · `tpl_attendance` · `tpl_shifts`.
|
|
@@ -649,22 +728,23 @@ option_badge (OptionBadge — a select value as its configured colored badge) ·
|
|
|
649
728
|
member_chip (MemberChip — avatar + name; the universal person render) ·
|
|
650
729
|
member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
|
|
651
730
|
status_badge · button · icon_button · link · link_button · chip · tabs · segmented_control ·
|
|
652
|
-
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`) ·
|
|
653
732
|
text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
|
|
654
733
|
radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
|
|
655
734
|
inline_text_input · inline_number_input · inline_select · inline_member_select · inline_date_picker ·
|
|
656
735
|
inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
|
|
657
|
-
`InlineEditView`; `InlineSelect`/`InlineMemberSelect` take `
|
|
658
|
-
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 ·
|
|
659
739
|
floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
|
|
660
740
|
columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
|
|
661
|
-
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) ·
|
|
662
742
|
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
|
|
663
|
-
|
|
743
|
+
alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
664
744
|
CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
|
|
665
745
|
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
|
666
746
|
status_grid (StatusGrid + StatusLegend) · heatmap · legend_item · remainder_meter · allocation_row ·
|
|
667
|
-
scan_field · file_dropzone · 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 ·
|
|
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 ·
|
|
668
748
|
file_gallery_modal · image_gallery · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
|
|
669
749
|
spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
|
|
670
750
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
@@ -678,4 +758,7 @@ finding (Finding — one ranked briefing/audit insight) · discrepancy (Discrepa
|
|
|
678
758
|
disagrees across sources) · triage_row (TriageRow — an item the agent classified + routed) ·
|
|
679
759
|
scored_option (ScoredOption — an AI-ranked candidate) ·
|
|
680
760
|
format_money · format_date · colors (solid · tint · ramp · ColorName ·
|
|
681
|
-
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_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); }}
|