@lotics/ui 11.8.3 → 11.8.4

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  > app with two renames IN THIS ORDER: first `size="xxl"` → `size="xxxl"`, then `size="xl"` →
7
7
  > `size="xxl"` — every existing usage keeps its exact look. Also: `SectionHeadingTitle` is now
8
8
  > ALWAYS `##` (22/24), and disabled `Button`s fade their label to the zinc-400 disabled ink on
9
- > every variant.
9
+ > every variant EXCEPT `primary`, which keeps inverted ink over its washed surface.
10
10
 
11
11
  The component kit for Lotics custom-code apps and the product frontend: React-Native-Web
12
12
  primitives (renders on web **and** native), data-entry patterns, AI surfaces, a composition
package/docs/catalog.md CHANGED
@@ -36,7 +36,7 @@ Pick by capability, not by name. (→ the source file for the API.)
36
36
  `Button` (labelled; `title` is MANDATORY — the type blocks a title-less Button; `color` =
37
37
  emphasis/risk), `IconButton` (icon-ONLY — the circular affordance; an icon-only Button is a
38
38
  type error, so reach here. `size` `lg` 40px / `md` 28px / `sm` 24px — md/sm keep a 40px touch
39
- target via hitSlop — the SAME `color` palette as Button, plus `loading` and `elevated` (white
39
+ target via hitSlop — the SAME `color` palette as Button plus `none`/`white`, plus `loading` and `elevated` (white
40
40
  fill + border + shadow, for a button sitting ON imagery — a tile's remove ✕, an overlay's
41
41
  retry); needs `accessibilityLabel`/`tooltip`), `TextLink` (underlined text that's OPTIONALLY
42
42
  an action (`onPress`) or a link (`href` — a real web anchor) — or, with neither, plain
@@ -212,7 +212,9 @@ this first for "manage a record's attachments"), `FileDropzone`, `FileRows`
212
212
  trailing ⋯ menu = Download · Open-external · Remove; the default "here are some files"
213
213
  surface), `FileGrid` (the upload-aware grid: completed files + a live upload queue in one
214
214
  surface — `FilesEditor` is this + the toolbar; reach for `FileGrid` bare when you own the
215
- chrome), `FileThumbnail` / `FileThumbnailGrid` (square tiles, display-only),
215
+ chrome), `FileThumbnail` / `FileThumbnailGrid` (square tiles; `onPress`/`onFilePress` makes each
216
+ tile a pressable door — e.g. tap-to-preview — carrying an accessible button role + filename
217
+ label; `selectedIds` for a selection overlay),
216
218
  `UploadingThumbnail` (the single in-flight tile FileGrid renders — reach for it only when
217
219
  hand-rolling a non-grid upload layout), `FileRow` (a horizontal file/document LINE —
218
220
  badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge /
@@ -578,7 +580,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
578
580
  repeating the same labelWidth/trailingWidth. Worked example:
579
581
  [`tpl_record`](../examples/tpl_record.tsx).
580
582
  - **`record_summary`** — `RecordSummary`: the identity band of a record detail/drawer — ONE
581
- row: `title` xl semibold tabular · `subtitle` sm muted · `status` Badge slot · optional
583
+ row: `title` xxl semibold tabular · `subtitle` sm muted · `status` Badge slot · optional
582
584
  `metric` {label,value,tone,note} pinned right, the band's ONE accent. The record's FIELDS
583
585
  never live in the header: compose them as `DetailTable`s in the sections below. Replaces
584
586
  hand-rolled record headers (mixed scales, several competing figures, color noise).
@@ -46,7 +46,8 @@ click away onto non-focusable space) does NOT restore — a mouse user has no ne
46
46
  preserve, and a bare `.focus()` would scroll the target into view, jerking the page (the
47
47
  restore is gated on modality via `shouldRestoreFocusOnClose`).
48
48
 
49
- **Typed dates.** `InlineDatePicker`'s keyboard mode is the kit's segmented `DateField`: type
49
+ **Typed dates.** `InlineDatePicker`'s keyboard mode is an internal segmented date field
50
+ (`DateField` — not exported; reached only through `InlineDatePicker`): type
50
51
  the date in the locale's own field order (dd/MM/yyyy where the locale says so — pass
51
52
  `locale`), digits auto-advance, and a typed separator (`/` `.` `-`) advances a single-digit
52
53
  day/month; Enter or blur commits, Escape reverts, Alt+ArrowDown floats the calendar. The
@@ -82,9 +83,15 @@ One per type:
82
83
  CLOSING commits the new set in one `onSave` — never a borderless `Select` posing as an inline
83
84
  field.
84
85
 
85
- All are built on **`useInlineEdit`** + **`InlineEditView`** (custom inputs join the family via
86
- those). `onSave` is async: the saving spinner sits INSIDE the control at its right edge (never a
87
- sibling that reflows); an error shows inline without losing the edit.
86
+ They split across two shared shells. The same-height **input-swap** editors (`InlineTextInput`,
87
+ `InlineNumberInput`, `InlineTimePicker`) render **`InlineEditFrame`** driven by the
88
+ **`useInlineEdit`** hook (the view⇄edit toggle, draft buffer, and async save) swap a custom
89
+ input into that pair to join the family. The **popover** editors (`InlineSelect`,
90
+ `InlineMemberSelect`, `InlineDatePicker`, `InlineTagSelect`) instead compose **`InlineEditView`**
91
+ (the resting box, wired as the popover trigger) with their OWN `useState` for open/draft/saving/
92
+ error — a pick, not a keystroke, is the edit, so `useInlineEdit` doesn't fit. `onSave` is async
93
+ either way: the saving spinner sits INSIDE the control at its right edge (never a sibling — that
94
+ reflows); an error shows inline without losing the edit.
88
95
 
89
96
  To let a value be UNSET (a diff-write CLEAR — unassign, remove a due date, drop a select), pass
90
97
  **`onClear`** to `InlineSelect` / `InlineMemberSelect` / `InlineDatePicker`. It surfaces through
@@ -139,7 +146,8 @@ wrap their OWN `FormField` (and don't put a `style` on it) — for a grid cell u
139
146
 
140
147
  ## Find-or-create — the `Combobox` family IS the control
141
148
 
142
- `Combobox` is COMPOUND: a root holds the DATA + behaviour (the shared `useOptionList` engine —
149
+ `Combobox` is COMPOUND: a root holds the DATA + behaviour (its internal option-list engine —
150
+ `useOptionList`, not exported —
143
151
  `options`, `value`, `onValueChange`, `onSearchChange`, `allowCustom`, the per-row
144
152
  `getOptionDescription`/`renderOptionContent` so they're typed against the option `data`), and the
145
153
  parts render the CHROME, composed as children — never a render-prop pile:
@@ -198,17 +206,22 @@ When work crosses departments (sales → operations → accounting), the HANDOFF
198
206
  changing desks — never an inbox, a notification, or a copied task. Two types:
199
207
 
200
208
  **(A) Same entity** → a STAGE field on the shared record: sections carry OWNER dot tags, and the
201
- handoff is MANAGED AS TASKS — each desk's checklist on the record (the task-row vocabulary:
202
- sibling `CheckCircle` + struck transparent `InlineTextInput` + per-task `InlineMemberSelect`
203
- assignees + the `CaptureRow` add-affordance — never a decorative progress strip). A NEW record
204
- starts with an EMPTY checklist tasks truly vary. The commons split in two: MANDATORY tasks are
205
- seeded by the app (a workflow on create, per record type) no human types them; common-but-
206
- OPTIONAL tasks appear as `SuggestionChip`s under the list a PILL, never a row, so a suggestion
207
- can't be mistaken for a task (tap = materialize, = dismiss for this record; already-present
208
- labels filter out; suggestions never count in done/total and pause while a filter narrows the
209
- view) the same suggestion grammar as the billing Standard pill. Real rows carry a `menu` (an
210
- `ActionMenu`; Delete lives BEHIND it, danger-styled and last never a bare a stray tap can
211
- hit), completing the checklist's CRUD.
209
+ handoff is MANAGED AS TASKS — each desk's checklist on the record. The **`Checklist`** compound
210
+ OWNS the list geometry (row height, ring/title alignment, and the one trailing column, set once
211
+ via `trailingWidth`)compose it, never hand-roll the row. Each **`ChecklistRow`** takes a
212
+ `control` (a `CheckCircle` a read-only ring when it has no `onChange`), its title as children (a
213
+ struck transparent `InlineTextInput`, or plain `Text`), then EITHER a `trailing` cell on wide
214
+ surfaces (the per-task assignee `InlineMemberSelect`, sized by the list's `trailingWidth`) OR a
215
+ `meta` line under the title on NARROW surfaces (a drawer/popover checklist the two are mutually
216
+ exclusive), plus a `menu` for the row's options never a decorative progress strip. `CaptureRow`
217
+ closes the list as its add-affordance. A NEW record starts with an EMPTY checklist tasks truly
218
+ vary. The commons split in two: MANDATORY tasks are seeded by the app (a workflow on create, per
219
+ record type) no human types them; common-but-OPTIONAL tasks appear as `SuggestionChip`s under
220
+ the list — a PILL, never a row, so a suggestion can't be mistaken for a task (tap = materialize,
221
+ ✕ = dismiss for this record; already-present labels filter out; suggestions never count in
222
+ done/total and pause while a filter narrows the view) — the same suggestion grammar as the billing
223
+ Standard pill. The row's `menu` (an `ActionMenu`; Delete lives BEHIND it, danger-styled and last —
224
+ never a bare ✕ a stray tap can hit) completes the checklist's CRUD.
212
225
 
213
226
  Open tasks INFORM the handoff, they NEVER block it: the CTA stays enabled, the count warns, and
214
227
  open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (assignee
@@ -216,8 +229,9 @@ open tasks carry over. The handoff CTA opens a DIALOG for the receiving desk (as
216
229
  the record left this register.
217
230
 
218
231
  Tasks PEEK from the register: the done/total column is a pressable compact-`ProgressBar` trigger
219
- whose popover holds the checklist on FIXED columns — every row ring · struck `InlineTextInput`
220
- title (flex) · quick-reassign `InlineMemberSelect` (140) no expandable rows (tags/files depth is
232
+ whose popover holds the same `Checklist` on FIXED columns (`trailingWidth={140}`) — every
233
+ `ChecklistRow`'s ring `control` · struck `InlineTextInput` title (flex) · quick-reassign
234
+ `InlineMemberSelect` `trailing` — no expandable rows (tags/files depth is
221
235
  the Task list template's lesson, not the peek's); the popover body is `PopoverContent`'s own
222
236
  ScrollView (`disableBodyScroll` is ONLY for children that manage their own scroll, like
223
237
  `OptionList`). The DRAWER carries a real Tasks SECTION in the Record template's shape (heading +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "11.8.3",
3
+ "version": "11.8.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -131,6 +131,8 @@
131
131
  "./use_screen_size": "./src/use_screen_size.ts",
132
132
  "./use_auto_grow_height": "./src/use_auto_grow_height.ts",
133
133
  "./use_focus_ring": "./src/use_focus_ring.ts",
134
+ "./interaction_modality": "./src/interaction_modality.ts",
135
+ "./inline_focus": "./src/inline_focus.ts",
134
136
  "./use_hover": "./src/use_hover.ts",
135
137
  "./focus_ring_pressable": "./src/focus_ring_pressable.tsx",
136
138
  "./control_surface": "./src/control_surface.ts",
@@ -108,6 +108,7 @@ export function FileThumbnail(props: FileThumbnailProps) {
108
108
  mimeType={file.mimeType}
109
109
  size={size}
110
110
  isTemplate={isTemplate}
111
+ accessibilityLabel={file.filename}
111
112
  onPress={onPress ?? (() => Linking.openURL(file.url))}
112
113
  onLongPress={onLongPress}
113
114
  />
@@ -179,17 +180,27 @@ interface DocumentBadgeProps {
179
180
  isTemplate?: boolean;
180
181
  onPress?: () => void;
181
182
  onLongPress?: () => void;
183
+ /** Accessible name for the pressable badge — the filename it stands for.
184
+ * Falls back to the file-type label (e.g. "PDF") when omitted. */
185
+ accessibilityLabel?: string;
182
186
  }
183
187
 
184
188
  /**
185
189
  * Compact two-tone file badge with document lines and colored label.
186
190
  */
187
191
  export function DocumentBadge(props: DocumentBadgeProps) {
188
- const { mimeType, isTemplate, onPress, onLongPress } = props;
192
+ const { mimeType, isTemplate, onPress, onLongPress, accessibilityLabel } = props;
189
193
  const { focusVisible, focusProps } = useFocusRing();
190
194
 
191
195
  return (
192
- <Pressable onPress={onPress} onLongPress={onLongPress} {...focusProps} style={focusVisible ? { boxShadow: FOCUS_RING, borderRadius: 6 } : undefined}>
196
+ <Pressable
197
+ onPress={onPress}
198
+ onLongPress={onLongPress}
199
+ accessibilityRole="button"
200
+ accessibilityLabel={accessibilityLabel ?? resolveMime(mimeType).label}
201
+ {...focusProps}
202
+ style={focusVisible ? { boxShadow: FOCUS_RING, borderRadius: 6 } : undefined}
203
+ >
193
204
  <FileBadge mimeType={mimeType} isTemplate={isTemplate} />
194
205
  </Pressable>
195
206
  );
@@ -234,6 +245,8 @@ export function DocumentCard(props: DocumentCardProps) {
234
245
  ]}
235
246
  onPress={onPress}
236
247
  onLongPress={onLongPress}
248
+ accessibilityRole="button"
249
+ accessibilityLabel={filename}
237
250
  {...focusProps}
238
251
  >
239
252
  <FileBadge mimeType={mimeType} size={badgeSize} isTemplate={isTemplate} />
@@ -280,6 +293,8 @@ export function MediaCard(props: MediaCardProps) {
280
293
  style={({ pressed }) => [styles.mediaCardCompact, sizeStyle, { backgroundColor: color }, pressed && styles.pressedDim, focusVisible && { boxShadow: FOCUS_RING }]}
281
294
  onPress={onPress}
282
295
  onLongPress={onLongPress}
296
+ accessibilityRole="button"
297
+ accessibilityLabel={filename}
283
298
  {...focusProps}
284
299
  >
285
300
  <Icon name={icon} size={iconSize} color={colors.white} />
@@ -292,7 +307,7 @@ export function MediaCard(props: MediaCardProps) {
292
307
  const backdrop = iconSize + 20;
293
308
 
294
309
  return (
295
- <Pressable style={[styles.documentCard, sizeStyle, focusVisible && { boxShadow: FOCUS_RING }]} onPress={onPress} onLongPress={onLongPress} {...focusProps}>
310
+ <Pressable style={[styles.documentCard, sizeStyle, focusVisible && { boxShadow: FOCUS_RING }]} onPress={onPress} onLongPress={onLongPress} accessibilityRole="button" accessibilityLabel={filename} {...focusProps}>
296
311
  {(state) => (
297
312
  <>
298
313
  <View style={[styles.mediaCardBody, { backgroundColor: color }]}>
@@ -343,7 +358,7 @@ function ImageThumbnail(props: ImageThumbnailProps) {
343
358
  : { width: "100%" as const, height: "100%" as const };
344
359
 
345
360
  return (
346
- <Pressable onPress={onPress} onLongPress={onLongPress} {...focusProps} style={[styles.imageThumbnail, sizeStyle, focusVisible && { boxShadow: FOCUS_RING }]}>
361
+ <Pressable onPress={onPress} onLongPress={onLongPress} accessibilityRole={onPress ? "button" : "image"} accessibilityLabel={file.filename} {...focusProps} style={[styles.imageThumbnail, sizeStyle, focusVisible && { boxShadow: FOCUS_RING }]}>
347
362
  {(state) => (
348
363
  <>
349
364
  <Image