@lotics/ui 22.1.0 → 22.2.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 +2 -2
- package/docs/composition.md +53 -2
- package/docs/data_entry.md +17 -0
- package/package.json +1 -1
- package/src/accordion.tsx +6 -1
- package/src/agent_run.tsx +5 -1
- package/src/column_filter.tsx +3 -0
- package/src/combobox.tsx +5 -0
- package/src/comments_thread.tsx +1 -1
- package/src/option_list.tsx +5 -0
- package/src/scan_field.tsx +5 -0
- package/src/search_input.tsx +5 -0
package/AGENTS.md
CHANGED
|
@@ -15,9 +15,9 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
15
15
|
| Doc | Read it for |
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` for a custom-code app's `vite.config.ts`). Read before building any screen; reuse first. |
|
|
18
|
-
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
18
|
+
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
19
|
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split (modify → review-before-apply; create → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer) |
|
|
20
|
-
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude, banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace. |
|
|
20
|
+
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget). |
|
|
22
22
|
|
|
23
23
|
## Iron rules
|
package/docs/composition.md
CHANGED
|
@@ -74,8 +74,23 @@ restyle a heading level per-page.
|
|
|
74
74
|
section's lead group. Group leads INSIDE a subsection's rows are `md` medium at most. Do NOT
|
|
75
75
|
hand-roll `Text weight="semibold"` group leads inside a section, and never promote a subsection
|
|
76
76
|
to its own section-level heading just to separate it.
|
|
77
|
-
- **Eyebrow
|
|
78
|
-
|
|
77
|
+
- **Eyebrow vs group lead — a label is one or the other, and the difference is what it
|
|
78
|
+
LABELS.** Both are hand-written `Text`; picking by feel is how a title ends up whispering.
|
|
79
|
+
|
|
80
|
+
| | Eyebrow | Group lead |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| Labels | a VALUE — beside or above it | a BLOCK — a titled sub-part of a section |
|
|
83
|
+
| Treatment | `xs` muted medium | `sm`–`md` medium (never semibold — that rung is a heading) |
|
|
84
|
+
| Examples | a metric's caption, a field name in a cell, an artifact tag ("Proposed") | "Deciding factor" over the fact it names, "Delivery" over an address stack |
|
|
85
|
+
|
|
86
|
+
The eyebrow is calibrated for DENSE surfaces — a register row, a drawer's detail stack, a
|
|
87
|
+
table cell — where a 12px label sits beside dozens of siblings and quiet is the point. On a
|
|
88
|
+
low-density surface (a single-answer page, a public result screen) the same treatment reads as
|
|
89
|
+
an afterthought: there are no siblings for it to be quiet among. A label that titles a block
|
|
90
|
+
someone must ACT on is a group lead, and `sm medium` is its floor.
|
|
91
|
+
|
|
92
|
+
- **Eyebrow** — `<Text size="xs" color="muted" weight="medium">`, **sentence case, NEVER
|
|
93
|
+
`transform="uppercase"`**: a small quiet label above or beside a VALUE — an artifact tag
|
|
79
94
|
("Proposed", "Question", "Suggested edit"), a field name, a metric caption, a minor one-line
|
|
80
95
|
label. **All-caps is banned** — it reads as shouting, and reflexively uppercasing every little
|
|
81
96
|
label is the #1 thing that makes a surface feel templated. Sentence case + medium weight, full
|
|
@@ -566,6 +581,30 @@ Anything pressable is ≥ 40px tall (8px minimum gap between pressables). 16px b
|
|
|
566
581
|
canvas padding, 16–20 inside bands, 10–12 between content lines. Density comes from alignment +
|
|
567
582
|
hierarchy, not cramming.
|
|
568
583
|
|
|
584
|
+
## Testing an overlay component — assert through the logic, prove in a browser
|
|
585
|
+
|
|
586
|
+
**A `Popover`-backed surface does not mount under jsdom.** It positions in a `useLayoutEffect` +
|
|
587
|
+
rAF against `getBoundingClientRect`, which jsdom answers with zeroes, so the content never renders
|
|
588
|
+
no matter how long you `waitFor`. That covers everything built on it: the `Combobox` listbox,
|
|
589
|
+
`Select`, `Menu`, `Tooltip`, `InfoPopover`, `Peek`. The kit's own suite is pure-logic for exactly
|
|
590
|
+
this reason — there is no render test for any of them.
|
|
591
|
+
|
|
592
|
+
The trap is that the CONTROL works fine, so the failure looks like a wiring bug: the input takes
|
|
593
|
+
its value, `aria-expanded` flips to `true`, and `role="option"` is nowhere. Time goes into
|
|
594
|
+
debugging the component instead of the harness.
|
|
595
|
+
|
|
596
|
+
So split the assertion:
|
|
597
|
+
|
|
598
|
+
- **the decision** — whatever the selection drives (which rate is reachable, which rows survive a
|
|
599
|
+
filter) belongs in a pure module with its own test. That is where the consequential logic lives,
|
|
600
|
+
and it tests far better there: every branch, no DOM.
|
|
601
|
+
- **the wiring** — that picking an option calls the handler and the screen re-renders — is a
|
|
602
|
+
browser check (`lotics app dev` + Playwright), which the quality bar requires anyway.
|
|
603
|
+
|
|
604
|
+
A component test can still assert everything BEFORE the overlay opens: the empty default, the
|
|
605
|
+
placeholder, the disabled state, and — often the point — what the screen refuses to claim while
|
|
606
|
+
nothing is selected.
|
|
607
|
+
|
|
569
608
|
## Microcopy — content, not mechanics
|
|
570
609
|
|
|
571
610
|
UI copy states WHAT something is — domain content or one real rule — never how the widget
|
|
@@ -592,6 +631,18 @@ that praises it is marketing.
|
|
|
592
631
|
cut-off, the box misses the sailing").
|
|
593
632
|
- **The deletion test**: if removing the line changes nothing for the reader, remove the line —
|
|
594
633
|
`description` is optional for a reason; when no invisible truth is worth stating, omit it.
|
|
634
|
+
- **NEVER join text with a middot — ` · ` is banned.** It is punctuation doing a word's job: it
|
|
635
|
+
asserts that two things are related while refusing to say how, leaving the reader to guess
|
|
636
|
+
between "and", "of", "for", "then", and "here is another fact". It is also the single loudest
|
|
637
|
+
templated-metadata tell (`5 min read · Jan 3 · Design`), and screen readers drop it, so the
|
|
638
|
+
only thing carrying the relationship vanishes. Use the mark that MEANS what you intend:
|
|
639
|
+
**`:`** for a run-in label naming its value (`Hàng hoá: <goods>`), **`( )`** for a
|
|
640
|
+
supplementary count or qualifier (`Đã đọc biểu thuế (2 bước)`), **`;`** between two related
|
|
641
|
+
clauses, **`,`** in a genuine list of like items, a real preposition when the relation is
|
|
642
|
+
semantic (`Tính thuế cho 8415.10.20`) — or a **line break plus ink contrast**, which is what
|
|
643
|
+
a middot is usually a substitute for. Facts of two different KINDS want two lines, not one
|
|
644
|
+
line with a glyph between them. (Verbatim quoted DATA keeps its own delimiters — this governs
|
|
645
|
+
copy we write, not text we reproduce.)
|
|
595
646
|
- **Plain language over jargon; effect over mechanism** ("Record visibility", not "Row-Level
|
|
596
647
|
Privacy") — if a term needs explaining, replace it. **One concept = one word everywhere** —
|
|
597
648
|
pick the term once and reuse it on every surface.
|
package/docs/data_entry.md
CHANGED
|
@@ -198,6 +198,23 @@ Past two columns the label→field link breaks — MANY inputs means GROUPING in
|
|
|
198
198
|
wrap their OWN `FormField` (and don't put a `style` on it) — for a grid cell use a bare
|
|
199
199
|
`FormField style={half}` wrapping `DatePicker`/`Picker`.
|
|
200
200
|
|
|
201
|
+
## Browser autofill — suppressed on search, kept on forms
|
|
202
|
+
|
|
203
|
+
**A control that owns a suggestion list must suppress the browser's.** Chrome offers saved form
|
|
204
|
+
values on any text input it does not recognize as opted out, and on a `Combobox` the two dropdowns
|
|
205
|
+
STACK — the component's own options render behind the browser's autofill panel, which reads as a
|
|
206
|
+
rendering bug in the app.
|
|
207
|
+
|
|
208
|
+
The kit sets `autoComplete="off"` for you on every control whose content is a query rather than a
|
|
209
|
+
personal detail: `ComboboxInput`, `SearchInput`, `ScanField`, `ColumnFilter`, and the shared
|
|
210
|
+
`OptionList` search. Nothing to pass.
|
|
211
|
+
|
|
212
|
+
It deliberately does NOT do this on `FormTextInput`, the inline editors, or the comment composer.
|
|
213
|
+
A contact form SHOULD autofill a name, an email and a phone number — suppressing it there is a real
|
|
214
|
+
usability loss, and which fields those are is the app author's call, not the kit's. Pass
|
|
215
|
+
`autoComplete` yourself on a form field (`"name"`, `"tel"`, `"email"`, `"off"`); it forwards to the
|
|
216
|
+
DOM input.
|
|
217
|
+
|
|
201
218
|
## Find-or-create — the `Combobox` family IS the control
|
|
202
219
|
|
|
203
220
|
`Combobox` is COMPOUND: a root holds the DATA + behaviour (its internal option-list engine —
|
package/package.json
CHANGED
package/src/accordion.tsx
CHANGED
|
@@ -130,7 +130,12 @@ export function AccordionTitle(props: AccordionTitleProps) {
|
|
|
130
130
|
return (
|
|
131
131
|
<View style={styles.title}>
|
|
132
132
|
{props.icon ? <Icon name={props.icon} size={16} color={props.iconColor ?? colors.zinc[500]} /> : null}
|
|
133
|
-
|
|
133
|
+
{/* MEDIUM, like every other row identity in the kit (`ListItem`,
|
|
134
|
+
`MenuButton`, `MenuListItem`, `DetailRow` label). This was the one
|
|
135
|
+
pressable-row title at regular weight, so in a long register — 200
|
|
136
|
+
instruments, say — the thing you scan and press carried no more weight
|
|
137
|
+
than the prose above the list. */}
|
|
138
|
+
<Text size="sm" weight="medium" numberOfLines={1} style={styles.titleText}>
|
|
134
139
|
{props.children}
|
|
135
140
|
</Text>
|
|
136
141
|
</View>
|
package/src/agent_run.tsx
CHANGED
|
@@ -365,10 +365,14 @@ function ReasoningDisclosure(props: { text: string; streaming?: boolean; expande
|
|
|
365
365
|
|
|
366
366
|
function SummaryLabel({ label, count, stepsLabel }: { label: string; count: number; stepsLabel: (n: number) => string }) {
|
|
367
367
|
return (
|
|
368
|
+
// PARENTHESES, not a middot. The count is supplementary to the action, and
|
|
369
|
+
// parentheses say so in a mark that survives text extraction and screen
|
|
370
|
+
// readers; a middot asserts a relationship without naming it, and the ink
|
|
371
|
+
// contrast here was already doing the separating anyway.
|
|
368
372
|
<Text size="sm" weight="medium" numberOfLines={1}>
|
|
369
373
|
{label}
|
|
370
374
|
<Text size="sm" color="muted">
|
|
371
|
-
{"
|
|
375
|
+
{" (" + stepsLabel(count) + ")"}
|
|
372
376
|
</Text>
|
|
373
377
|
</Text>
|
|
374
378
|
);
|
package/src/column_filter.tsx
CHANGED
|
@@ -118,6 +118,9 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
118
118
|
>
|
|
119
119
|
{column.type === "text" ? (
|
|
120
120
|
<TextInputField
|
|
121
|
+
// A filter value is never a saved personal detail, so the browser
|
|
122
|
+
// has nothing useful to offer and a dropdown to obscure the rows with.
|
|
123
|
+
autoComplete="off"
|
|
121
124
|
autoFocus
|
|
122
125
|
value={value?.kind === "text" ? value.query : ""}
|
|
123
126
|
onChangeText={(query) => onChange({ kind: "text", query })}
|
package/src/combobox.tsx
CHANGED
|
@@ -389,6 +389,11 @@ export function ComboboxInput(props: ComboboxInputProps) {
|
|
|
389
389
|
editable={!ctx.disabled}
|
|
390
390
|
autoFocus={ctx.autoFocus}
|
|
391
391
|
autoCapitalize="none"
|
|
392
|
+
// The browser must not open its OWN suggestion list over this one.
|
|
393
|
+
// Chrome offers saved form values on any unmarked text input, and on a
|
|
394
|
+
// control that already owns a listbox the two dropdowns stack — the
|
|
395
|
+
// component's options end up behind the browser's autofill panel.
|
|
396
|
+
autoComplete="off"
|
|
392
397
|
autoCorrect={false}
|
|
393
398
|
accessibilityLabel={accessibilityLabel}
|
|
394
399
|
style={[
|
package/src/comments_thread.tsx
CHANGED
|
@@ -213,7 +213,7 @@ function CommentRow(props: CommentRowProps) {
|
|
|
213
213
|
<Text weight="medium">{displayName}</Text>
|
|
214
214
|
<Text size="sm" color="zinc-500">
|
|
215
215
|
{formatTimestamp(comment.created_at)}
|
|
216
|
-
{comment.updated_at !== comment.created_at ? `
|
|
216
|
+
{comment.updated_at !== comment.created_at ? ` (${labels.edited})` : ""}
|
|
217
217
|
</Text>
|
|
218
218
|
<Spacer size={8} />
|
|
219
219
|
{isEditing ? (
|
package/src/option_list.tsx
CHANGED
|
@@ -68,6 +68,11 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
68
68
|
<View testID={testID} style={[styles.container, small ? { flex: 1 } : { maxHeight: 480 }]}>
|
|
69
69
|
{search.mode === "internal" ? (
|
|
70
70
|
<TextInputField
|
|
71
|
+
// The browser must not open its OWN suggestion list over this one.
|
|
72
|
+
// Chrome offers saved form values on any unmarked text input, and on a
|
|
73
|
+
// control that already owns a listbox the two dropdowns stack — the
|
|
74
|
+
// component's options end up behind the browser's autofill panel.
|
|
75
|
+
autoComplete="off"
|
|
71
76
|
icon="search"
|
|
72
77
|
value={list.query}
|
|
73
78
|
onChangeText={list.setQuery}
|
package/src/scan_field.tsx
CHANGED
|
@@ -37,6 +37,11 @@ export function ScanField(props: ScanFieldProps) {
|
|
|
37
37
|
return (
|
|
38
38
|
<View style={{ gap: 6 }}>
|
|
39
39
|
<TextInputField
|
|
40
|
+
// The browser must not open its OWN suggestion list over a scan target.
|
|
41
|
+
// Chrome offers saved form values on any unmarked text input, and on a
|
|
42
|
+
// control that already owns a listbox the two dropdowns stack — the
|
|
43
|
+
// component's options end up behind the browser's autofill panel.
|
|
44
|
+
autoComplete="off"
|
|
40
45
|
value={value}
|
|
41
46
|
onChangeText={onChangeText}
|
|
42
47
|
onSubmitEditing={onScan}
|
package/src/search_input.tsx
CHANGED
|
@@ -46,6 +46,11 @@ export function SearchInput(props: SearchInputProps) {
|
|
|
46
46
|
<View style={style as ViewStyle}>
|
|
47
47
|
<TextInputField
|
|
48
48
|
{...rest}
|
|
49
|
+
// The browser must not open its OWN suggestion list over this one.
|
|
50
|
+
// Chrome offers saved form values on any unmarked text input, and on a
|
|
51
|
+
// control that already owns a listbox the two dropdowns stack — the
|
|
52
|
+
// component's options end up behind the browser's autofill panel.
|
|
53
|
+
autoComplete="off"
|
|
49
54
|
ref={mergedRef}
|
|
50
55
|
onKeyPress={handleKeyPress}
|
|
51
56
|
icon="search"
|