@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 +1 -1
- package/docs/catalog.md +5 -3
- package/docs/data_entry.md +32 -18
- package/package.json +3 -1
- package/src/file_thumbnail.tsx +19 -4
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
|
|
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
|
|
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`
|
|
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).
|
package/docs/data_entry.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 (
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
220
|
-
|
|
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
|
+
"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",
|
package/src/file_thumbnail.tsx
CHANGED
|
@@ -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
|
|
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
|