@lotics/ui 46.1.0 → 46.3.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 +24 -0
- package/MIGRATION.md +87 -0
- package/docs/ai_patterns.md +34 -14
- package/docs/catalog.md +109 -22
- package/docs/composition.md +48 -6
- package/docs/templates.md +3 -1
- package/docs/testing.md +6 -0
- package/package.json +3 -2
- package/src/agent_run.tsx +74 -58
- package/src/agent_run_pane.tsx +7 -5
- package/src/alert.css +0 -1
- package/src/alert.tsx +8 -0
- package/src/axis_label_indices.ts +84 -0
- package/src/bar_chart.tsx +137 -16
- package/src/dialog.tsx +46 -24
- package/src/drawer.tsx +21 -2
- package/src/file_gallery_modal.tsx +3 -0
- package/src/line_chart.tsx +2 -2
- package/src/locale.tsx +4 -4
- package/src/modal.tsx +23 -3
- package/src/overlay_layer.ts +65 -0
- package/src/page_content.tsx +8 -22
- package/src/page_header.tsx +60 -11
- package/src/popover.tsx +29 -5
- package/src/skip_link.tsx +2 -1
- package/src/stacked_bar_chart.tsx +31 -1
- package/src/text.tsx +21 -0
- package/src/tooltip.tsx +2 -1
- package/src/use_change_set.ts +66 -17
- package/src/use_scroll_seam.ts +79 -0
- package/src/line_chart_labels.ts +0 -32
package/AGENTS.md
CHANGED
|
@@ -75,6 +75,30 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
75
75
|
- **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
|
|
76
76
|
hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
|
|
77
77
|
→ [composition.md §"Color discipline"](./docs/composition.md).
|
|
78
|
+
- **An overlay paints in the order it was OPENED, and a scroller that swaps content opens the new
|
|
79
|
+
content at the top.** Compose overlays wherever you like — a `Dialog` written beside the
|
|
80
|
+
`Drawer` whose row opens it works, both ways round, and a `Dialog` opened from an open `Popover`
|
|
81
|
+
paints over its panel. An overlay of your OWN gets that for free by rendering its react-native
|
|
82
|
+
`Modal` **only while open** (`{open && <Modal visible …>}`); mounted while closed it takes its
|
|
83
|
+
body-level slot on the app's first paint and is then covered by anything opened later —
|
|
84
|
+
rendering and announcing perfectly, dead to every press. The z-index rungs beside the overlays
|
|
85
|
+
are published in `overlay_layer`, so never hand-pick a number to clear a Lotics overlay.
|
|
86
|
+
`DrawerScrollArea` / `DialogScrollArea` / `ModalBody` take a `scrollKey` and the routed
|
|
87
|
+
`Popover` takes the same seam off its route, so a swapped-in record opens at the top and the
|
|
88
|
+
reader returns to their place on the way back.
|
|
89
|
+
→ [composition.md §"Canvas & content column"](./docs/composition.md)
|
|
90
|
+
→ [catalog.md §"Overlays & navigation"](./docs/catalog.md)
|
|
91
|
+
- **ONE VALUE, ONE RENDERING, inside one card — the chart draws the entity its table draws.**
|
|
92
|
+
A value's filter renders it the way its cell does, and the same rule reaches the chart beside
|
|
93
|
+
the table: `StackedBarRow` and `BarChartItem` take a `leading` slot for the entity's own mark.
|
|
94
|
+
A LEGEND row is the exception — its subject is a category, and the swatch is that identity.
|
|
95
|
+
→ [composition.md §"A register that TRIAGES"](./docs/composition.md).
|
|
96
|
+
- **A row says which side HOLDS; `numberOfLines` handles the side that gives way.** The clamp
|
|
97
|
+
brings its own `minWidth: 0` + `flexShrink: 1` (a clamp that cannot shrink cannot clamp), so
|
|
98
|
+
what a call site still has to state is `flexShrink: 0` on the figure or control that must stay
|
|
99
|
+
whole. Same law on a categorical axis: the LABELS thin to fit, the bars never do.
|
|
100
|
+
→ [composition.md §"Typography"](./docs/composition.md),
|
|
101
|
+
[catalog.md §"Numbers & charts"](./docs/catalog.md).
|
|
78
102
|
- **Hand-typed type is off-system — and it always lands too small.** Every run of language is
|
|
79
103
|
`<Text size= weight= color=>` on the fixed rungs. **Grep the diff: `fontSize:` / `lineHeight:` /
|
|
80
104
|
`letterSpacing:` outside `src/` is a bug.** → [composition.md §"Typography"](./docs/composition.md).
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,93 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 46.3.0
|
|
8
|
+
|
|
9
|
+
**A `Text` with `numberOfLines` now declares `flexShrink: 1` + `minWidth: 0` for you.**
|
|
10
|
+
A clamp that cannot shrink cannot clamp: on web a clamped `Text` is `white-space: nowrap`, so
|
|
11
|
+
`min-width: auto` floors it at the WHOLE string; on native a `Text` is `flexShrink: 0`. Either way
|
|
12
|
+
two clamped values on one row laid out at intrinsic width and ran off the frame. Both declarations
|
|
13
|
+
now come with the clamp, and both are no-ops until a row actually overflows.
|
|
14
|
+
|
|
15
|
+
Nothing's API changed, but **a layout default moved under every clamped `Text` in your app** — this
|
|
16
|
+
is the one 46.3.0 change to re-check by looking. Two shapes are worth a pass:
|
|
17
|
+
|
|
18
|
+
- A flex ROW where a clamped `Text` was the side that HELD and an unclamped sibling gave way. Both
|
|
19
|
+
can shrink now, so the wrong one may truncate.
|
|
20
|
+
- A flex COLUMN with a bounded height (a fixed-height card, a `maxHeight` panel). `flexShrink` acts
|
|
21
|
+
on the MAIN axis, so a clamped `Text` that used to hold its height can now be compressed.
|
|
22
|
+
|
|
23
|
+
The pair sits BEFORE your own `style`, so the site that must stay whole says so:
|
|
24
|
+
|
|
25
|
+
```diff
|
|
26
|
+
-<Text numberOfLines={1}>{amount}</Text>
|
|
27
|
+
+<Text numberOfLines={1} style={{ flexShrink: 0 }}>{amount}</Text>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That is how a figure keeps winning over its label. `minWidth` is overridden the same way
|
|
31
|
+
(`style={{ minWidth: "auto" }}`) on the rare surface that wants the old min-content floor back.
|
|
32
|
+
|
|
33
|
+
**An overlay of your OWN must render its react-native `Modal` only while it is OPEN.**
|
|
34
|
+
`Dialog`, `Drawer`, `Modal` and `FileGalleryModal` now do — nothing in their API changed — and a
|
|
35
|
+
hand-rolled overlay outside the kit has to do the same, or it loses to them:
|
|
36
|
+
|
|
37
|
+
```diff
|
|
38
|
+
-<Modal visible={open} onRequestClose={close} transparent>
|
|
39
|
+
- …
|
|
40
|
+
-</Modal>
|
|
41
|
+
+{open && (
|
|
42
|
+
+ <Modal visible onRequestClose={close} transparent>
|
|
43
|
+
+ …
|
|
44
|
+
+ </Modal>
|
|
45
|
+
+)}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
react-native-web appends a `Modal`'s body-level `<div>` on its FIRST render and never re-orders
|
|
49
|
+
it, so an overlay mounted while closed takes its slot on the app's first paint and is then covered
|
|
50
|
+
by any overlay opened later — while rendering, reading and announcing perfectly, with only its
|
|
51
|
+
presses dead. Nothing is lost by the change: react-native-web renders a closed `Modal`'s children
|
|
52
|
+
as `null` already, so only the empty portal div was being held. A takeover with
|
|
53
|
+
`animationType="fade"` keeps its fade IN and now closes immediately, as `FileGalleryModal` always
|
|
54
|
+
has; state that must survive a close belongs outside the `Modal` element either way.
|
|
55
|
+
|
|
56
|
+
**The overlay z-index rungs are now published constants.** The numbers are unchanged for `Tooltip`
|
|
57
|
+
and `Alert` (`10000`) and for a toast (`10001`); **the skip link moved `10001` → `10002`**, off the
|
|
58
|
+
rung it was sharing with a toast. Read the rung instead of naming a number:
|
|
59
|
+
|
|
60
|
+
```diff
|
|
61
|
+
-import { View } from "react-native";
|
|
62
|
+
+import { NOTIFICATION_Z } from "@lotics/ui/overlay_layer";
|
|
63
|
+
…
|
|
64
|
+
- zIndex: 10001,
|
|
65
|
+
+ zIndex: NOTIFICATION_Z,
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The full table is in `docs/catalog.md` § Overlays & navigation: `OVERLAY_Z` (every overlay, and
|
|
69
|
+
`Popover`), `OVERLAY_Z_ABOVE` (tooltip/alert), `NOTIFICATION_Z` (a toast), `SKIP_LINK_Z`.
|
|
70
|
+
|
|
71
|
+
**`PageContent`'s title band is now a `PageHeader`**, so the two stop drifting. `title`,
|
|
72
|
+
`titleRight` and `description` are unchanged, and `PageHeader`'s `title` became optional — but the
|
|
73
|
+
band's rhythm is now `PageHeader`'s everywhere: a description sits **8px** under the title (it was
|
|
74
|
+
flush in `PageHeader`) and the band clears **16px** before the content (it was 24 in
|
|
75
|
+
`PageContent`). Nothing to change; worth one look at a page shell you care about.
|
|
76
|
+
|
|
77
|
+
**`useChangeSet`'s per-proposal default is a MAP, not a predicate.** `initial: "accepted"` /
|
|
78
|
+
`"pending"` are unchanged.
|
|
79
|
+
|
|
80
|
+
```diff
|
|
81
|
+
-useChangeSet(ids, { initial: (id) => (overwrites.has(id) ? "rejected" : "accepted") })
|
|
82
|
+
+const initial = useMemo(
|
|
83
|
+
+ () => new Map(rows.filter((r) => r.overwrites).map((r) => [r.id, "rejected" as const])),
|
|
84
|
+
+ [rows],
|
|
85
|
+
+);
|
|
86
|
+
+useChangeSet(ids, { initial })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
An id the map does not name arrives `accepted`, as before. A value rather than a function is what
|
|
90
|
+
lets the default be an ordinary dependency: `status` and the group arrays derive from it together,
|
|
91
|
+
so `status` changes identity whenever a decision does — which a screen that memoizes its rows on
|
|
92
|
+
`review.status` needs, and which a predicate held out of the dependency lists could not give.
|
|
93
|
+
|
|
7
94
|
## 46.0.0
|
|
8
95
|
|
|
9
96
|
**`AgentRun` / `AgentProgress` drop `stepsLabel`, and the `agentRun` locale slice drops `steps`.**
|
package/docs/ai_patterns.md
CHANGED
|
@@ -177,8 +177,15 @@ PUT and rolls the calls out BELOW it on press.
|
|
|
177
177
|
|
|
178
178
|
**ONE row for the whole run, and NO prose until it ends.** While the agent works, the entire feed
|
|
179
179
|
is a single row. It names the call in flight — "Searching records" — and says
|
|
180
|
-
**"Thinking
|
|
181
|
-
calls.
|
|
180
|
+
**"Thinking"** whenever the model is writing rather than calling, which is every gap between
|
|
181
|
+
calls. Nothing rolls out unless the reader asks.
|
|
182
|
+
|
|
183
|
+
**An unsettled row BREATHES**, label and dot together — the label on the same opacity rhythm the
|
|
184
|
+
dot's halo runs (750ms each way), so one line carries one animation rather than two that drift
|
|
185
|
+
apart. That breath is what says "in progress", which is why no live label ends in an ellipsis:
|
|
186
|
+
punctuation was standing in for motion, and a screen reader drops it anyway. A row that is not
|
|
187
|
+
working does not breathe — settled, and `awaiting` a human decision, which is parked rather than
|
|
188
|
+
in flight.
|
|
182
189
|
|
|
183
190
|
**No step count.** The row says WHAT is happening and stops there. A tally was a number the reader
|
|
184
191
|
could act on in no way: live, the label already changes on every call, so it added no motion the
|
|
@@ -208,12 +215,14 @@ reasoning part. An interleaved-thinking run therefore rebuilt the stack one grou
|
|
|
208
215
|
reader watched finished rows pile up while they waited, and was then left with that pile sitting
|
|
209
216
|
above the answer at the answer's weight, every row of it stale.
|
|
210
217
|
|
|
211
|
-
**Expanding
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
**Expanding works the same live and settled** — the timeline opens IN FULL, and the HOST'S OWN
|
|
219
|
+
scroller keeps the newest row in view. `AgentRun` never frames itself. It shipped once with a
|
|
220
|
+
live-only 220px window, and that window was a scroll container nested inside whatever scroll
|
|
221
|
+
container the run already sat in: in chat, an inverted list inside an inverted list, which painted
|
|
222
|
+
stale content over the neighbouring messages and took the wheel from the surface the reader was
|
|
223
|
+
on. A component in document flow cannot know whether it is inside a scroller, so it must not act
|
|
224
|
+
as one. A host that genuinely needs a bound takes the framing job itself — `collapseProcess={false}`
|
|
225
|
+
plus its own `FollowScroll`. Settling never closes what a reader opened.
|
|
217
226
|
|
|
218
227
|
The split is POSITIONAL (`splitTimeline` in `agent_transform`): what comes after the last tool
|
|
219
228
|
call is the answer, everything before it is the work. Nothing has to decide whether a paragraph
|
|
@@ -261,10 +270,10 @@ tinted panel rather than another step row, and it carries `role="alert"` so a sc
|
|
|
261
270
|
action (a secondary `Button`) sits INSIDE that panel so the operator can re-fire the run; omit it
|
|
262
271
|
and the panel carries the message alone.
|
|
263
272
|
|
|
264
|
-
**`FollowScroll` — only when you took the job (`collapseProcess={false}`).**
|
|
265
|
-
|
|
266
|
-
scroller
|
|
267
|
-
the pointer happens to be over. Reach for the wrapper when the run is a raw timeline you are
|
|
273
|
+
**`FollowScroll` — only when you took the job (`collapseProcess={false}`).** On the default, the
|
|
274
|
+
run is plain content in your flow and your own scroller follows it; adding a wrapper there nests a
|
|
275
|
+
scroller in a scroller — the outer never overflows, the inner holds the content, and the wheel
|
|
276
|
+
lands on whichever the pointer happens to be over. Reach for the wrapper when the run is a raw timeline you are
|
|
268
277
|
framing yourself — a dialog, drawer, or fixed-height panel where the feed grows BELOW the fold and
|
|
269
278
|
a plain scroll container doesn't follow. Wrap it then:
|
|
270
279
|
`<FollowScroll style={{ maxHeight: … }}><AgentRun … collapseProcess={false} /></FollowScroll>` — the same
|
|
@@ -281,7 +290,7 @@ for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={
|
|
|
281
290
|
[the SDK doc](../../app-sdk/docs/ai.md) for the hook. The `ai` package is a regular dependency of
|
|
282
291
|
`@lotics/ui` — its part TYPES resolve transitively in any consumer's typecheck with nothing to
|
|
283
292
|
install; the imports are type-only (purity-enforced), so no `ai` runtime ever enters a bundle.
|
|
284
|
-
**A run with no parts yet renders the breathing "Starting
|
|
293
|
+
**A run with no parts yet renders the breathing "Starting" row** (streaming state only) — the
|
|
285
294
|
live dot plus a label that pulses, covering the gap between the CTA press and the first streamed
|
|
286
295
|
part (upload + run creation + first token). **The law: pass
|
|
287
296
|
`state="streaming"` from the moment the paid CTA fires — uploads included — and render
|
|
@@ -370,6 +379,17 @@ alignment and rhythm.
|
|
|
370
379
|
`keptCount`/`total`/`settled`. `initial` defaults to **`accepted`** so an operator drops
|
|
371
380
|
exceptions instead of approving eight identical lines; pass `pending` when each change
|
|
372
381
|
genuinely deserves its own verdict, and gate the commit on `settled`.
|
|
382
|
+
- **A set that mixes KINDS defaults per kind: `initial` also takes a `Map<id, decision>`.** A
|
|
383
|
+
review usually holds both — filling a blank should arrive `accepted`, overwriting a value a
|
|
384
|
+
human already set should arrive `rejected`, so the destructive half is opt-in — and one
|
|
385
|
+
global default puts one of the two on the wrong footing. An id the map does not name arrives
|
|
386
|
+
`accepted`. Build it with a `useMemo` over the proposals your screen already holds, which is
|
|
387
|
+
what keeps the hook ignorant of what a proposal IS; a VALUE (rather than a predicate written
|
|
388
|
+
inline) is what lets `status` and the group arrays derive from it together, so `status`
|
|
389
|
+
changes identity the moment a decision moves and a screen memoizing its rows on it re-runs.
|
|
390
|
+
Deciding those rows by calling `reject()` from the result handler instead writes overrides
|
|
391
|
+
nobody made, and `undo` then hands the row back **accepted** — the destructive change
|
|
392
|
+
silently in, from the control that means "undo".
|
|
373
393
|
|
|
374
394
|
### The shapes, and what each one is
|
|
375
395
|
|
|
@@ -518,7 +538,7 @@ whole pane with no backend ([`tpl_item_list`](../examples/tpl_item_list.tsx) doe
|
|
|
518
538
|
|
|
519
539
|
**Customize through the seams, or drop to the primitives.** `labelForCall` / `renderToolOutput`
|
|
520
540
|
pass through to `AgentRun`; the `run` object is the data seam. There is deliberately NO
|
|
521
|
-
empty-state slot — `AgentRun` renders its own localized "Starting
|
|
541
|
+
empty-state slot — `AgentRun` renders its own localized "Starting" row on zero parts, and the law
|
|
522
542
|
above forbids hand-rolling a placeholder. An app that wants a different ARRANGEMENT does not fight
|
|
523
543
|
the pane: `AgentRun`, `ClarifyWizard`, `ClarifyWizardScope`/`Actions`, `FollowScroll` and
|
|
524
544
|
`DialogScrollArea` all remain exported, and composing them is what this pane itself does.
|
package/docs/catalog.md
CHANGED
|
@@ -443,6 +443,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
443
443
|
numbers), `weight`, `color`, alignment, tabular numerals. **`leading="tight"`** sets the line
|
|
444
444
|
box for a STACKED PAIR rather than for prose — a subject over its supporting line, a value over
|
|
445
445
|
its annotation; see composition.md, which also covers the `gap` that goes with it.
|
|
446
|
+
**`numberOfLines`** clamps, and brings the `minWidth: 0` + `flexShrink: 1` that makes clamping
|
|
447
|
+
possible on a flex row — a clamp that cannot shrink cannot clamp. Both are no-ops until the row
|
|
448
|
+
overflows, and a `style` on the call site still wins, so a value that must never give way says
|
|
449
|
+
`flexShrink: 0` and keeps holding over its own label.
|
|
446
450
|
- **`eyebrow`** — `Eyebrow`: the small quiet label above or beside a VALUE (`xs` muted medium) —
|
|
447
451
|
a metric caption, a field name in a cell, an artifact tag. Takes `color` (for a VERDICT word
|
|
448
452
|
like "Mismatch") and `align`, and deliberately takes no `size`, `weight` or `transform`:
|
|
@@ -702,16 +706,21 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
702
706
|
useSeparator` inserts and what menus/popovers put between option groups.
|
|
703
707
|
- **`container`** — `Container`: centers content at a max width (`ContainerSize` sm|md|lg,
|
|
704
708
|
`CONTAINER_SIZES`).
|
|
705
|
-
- **`page_header`** — `PageHeader`: the page's title band
|
|
709
|
+
- **`page_header`** — `PageHeader`: the page's title band, and the ONE place that band is
|
|
710
|
+
defined — `PageContent`'s `title`/`titleRight`/`description` render one of these rather than a
|
|
711
|
+
second copy of the row. `actions` puts page-level CTAs on
|
|
706
712
|
the title row (right-aligned) and `trailing` puts a control immediately AFTER the title;
|
|
707
713
|
`left`/`right` form a separate nav row above. Split `trailing` from `actions` by what the
|
|
708
714
|
control acts on: `actions` do something to the page's CONTENT (create, sort, export),
|
|
709
715
|
`trailing` changes what is AROUND it (a side-panel toggle, a view switch). A panel toggle
|
|
710
716
|
filed under `actions` reads as a peer of "create one of these". Under a title too long for
|
|
711
717
|
the row the TITLE gives way and wraps while `trailing`
|
|
712
|
-
and `actions` keep their width.
|
|
718
|
+
and `actions` keep their width. **The band owns the space under itself** — 16px, whether it
|
|
719
|
+
is rendered on a hand-rolled screen or by `PageContent`; a shell adds nothing after it, or
|
|
720
|
+
the same band closes on two different rhythms depending on which shell a screen picked.
|
|
713
721
|
- **`page_content`** — `PageContent` + `PAGE_SIZES`: the page's padded, width-capped content
|
|
714
|
-
region — a centred column with optional `title`/`titleRight`/`description
|
|
722
|
+
region — a centred column with optional `title`/`titleRight`/`description` (which render a
|
|
723
|
+
`PageHeader`, so the band's row law and rhythm have one home), `header`/`footer`
|
|
715
724
|
slots and `fullscreen`. **Reach for it before hand-rolling a screen shell.** Its side
|
|
716
725
|
padding is `pagePad` (`@lotics/ui/spacing`), so a screen that
|
|
717
726
|
genuinely cannot use it — one with a rail, a side panel, or a scroller it must hold a ref to —
|
|
@@ -1586,15 +1595,56 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1586
1595
|
|
|
1587
1596
|
### Overlays & navigation
|
|
1588
1597
|
|
|
1598
|
+
**Every overlay paints in the order it was OPENED**, so mount them wherever composition wants — a
|
|
1599
|
+
`Dialog` written beside the `Drawer` whose row opens it is the ordinary shape and it works; a
|
|
1600
|
+
drawer opened from a dialog covers the dialog, which is the same rule the other way round. There
|
|
1601
|
+
is no layer prop and nothing at the call site has to know the tree order. It covers every
|
|
1602
|
+
body-level surface the kit has, `Popover` included: a `Dialog` or a file-preview `Modal` opened
|
|
1603
|
+
from an open popover paints OVER the panel that opened it. `Alert` and `Tooltip` stay above every
|
|
1604
|
+
overlay at any depth.
|
|
1605
|
+
|
|
1606
|
+
**The one rule an overlay of your OWN has to follow: render the react-native `Modal` element only
|
|
1607
|
+
while it is open** (`{open && <Modal visible …>}`, or an early `return null`). react-native-web
|
|
1608
|
+
appends a `Modal`'s body-level `<div>` on its FIRST render and never re-orders it, so an overlay
|
|
1609
|
+
mounted while closed has already taken its slot before anything opened — and once two overlays tie
|
|
1610
|
+
at the same z-index, that slot is what decides. The symptom is nasty precisely because it is not
|
|
1611
|
+
visual: the covered surface renders, reads and announces perfectly, and only its presses do
|
|
1612
|
+
nothing. Nothing is lost by not mounting it — react-native-web renders a closed `Modal`'s children
|
|
1613
|
+
as `null` anyway, so only the empty portal div was ever being held, and anything that must survive
|
|
1614
|
+
a close (a router, a context, form state) belongs outside the `Modal` element regardless.
|
|
1615
|
+
|
|
1616
|
+
**The rungs beside the overlays are a published contract** (`overlay_layer`), because a surface
|
|
1617
|
+
OUTSIDE the kit that has to clear a Lotics overlay cannot do it with a hand-picked literal:
|
|
1618
|
+
|
|
1619
|
+
| z-index | what sits there |
|
|
1620
|
+
|---|---|
|
|
1621
|
+
| `9999` | every overlay, and `Popover` — `OVERLAY_Z` |
|
|
1622
|
+
| `10000` | `Tooltip`, `Alert` — `OVERLAY_Z_ABOVE` |
|
|
1623
|
+
| `10001` | a transient notification / toast — `NOTIFICATION_Z` |
|
|
1624
|
+
| `10002` | the skip link — `SKIP_LINK_Z` |
|
|
1625
|
+
|
|
1626
|
+
Overlays all share one rung on purpose: DOM order settles the tie and DOM order is open order.
|
|
1627
|
+
`Popover` is on it too — it is no `Modal`, but it portals into the nearest `PortalHost`, so one
|
|
1628
|
+
opened inside an overlay is already inside that overlay's stacking context and a page-level one is
|
|
1629
|
+
a child of the app root, which every overlay's body-level box follows.
|
|
1630
|
+
|
|
1589
1631
|
- **`portal`** — `PortalHost` + `Portal`: the floating-content mount point; the app root
|
|
1590
1632
|
needs ONE `PortalHost` or `Dialog`/`Popover`/`Tooltip`/`Alert`/`OptionList` cannot render.
|
|
1591
1633
|
- **`dialog`** — `Dialog` (+ `DialogHeader`/`DialogHeaderTitle`/`DialogFooter`, `useDialog`,
|
|
1592
1634
|
`useDialogNavigation`): the centered card over a scrim; BAKES a screen router in
|
|
1593
|
-
(`<Dialog><Screen route="">…` — see `screen_router`).
|
|
1635
|
+
(`<Dialog><Screen route="">…` — see `screen_router`). `DialogScrollArea` takes the same
|
|
1636
|
+
**`scrollKey`** seam as `DrawerScrollArea` for a pane that swaps its body in place; a dialog
|
|
1637
|
+
navigating between `Screen`s does not need it, because a stacked screen stays mounted and keeps
|
|
1638
|
+
its own offset.
|
|
1594
1639
|
- **`drawer`** — `Drawer` + `DrawerScrollArea` + `DrawerFooter`: the docked side panel with scrim; the
|
|
1595
1640
|
register row's edit surface. ONE standard width (600px) — pass `width` only for a genuinely
|
|
1596
1641
|
exceptional panel; full-width on small screens. `DrawerScrollArea` is the guttered content
|
|
1597
|
-
region — `DialogScrollArea`'s counterpart — so never hand-pad a drawer body.
|
|
1642
|
+
region — `DialogScrollArea`'s counterpart — so never hand-pad a drawer body. Give it a
|
|
1643
|
+
**`scrollKey`** (the open content's identity — `scrollKey={openChild?.id}`, where the undefined
|
|
1644
|
+
leg names the root content rather than opting out) on a drawer that SWAPS its body in place, so
|
|
1645
|
+
the new content opens at the top and the previous one's offset returns on the way back — the seam
|
|
1646
|
+
`DialogScrollArea` and `ModalBody` take too, and the one thing a React `key` on the scroller
|
|
1647
|
+
cannot buy, since that resets the parent's place along with the child's. It always
|
|
1598
1648
|
scrolls: a padded box around a `flex:1` scroller insets that scroller's viewport, ending the
|
|
1599
1649
|
list short of the panel with the last row clipped. Two bodies skip it and take the `Drawer`'s
|
|
1600
1650
|
bare slot: a FULL-BLEED one (a band spanning the panel, a record screen with its own gutters),
|
|
@@ -1605,7 +1655,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1605
1655
|
edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog
|
|
1606
1656
|
(centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it
|
|
1607
1657
|
to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader
|
|
1608
|
-
(eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody
|
|
1658
|
+
(eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody (which takes the
|
|
1659
|
+
**`scrollKey`** seam — a wizard stepping between steps swaps its body in place), a pinned
|
|
1609
1660
|
ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a
|
|
1610
1661
|
focused capture / multi-step wizard / a console the user steps INTO; pick Dialog when the
|
|
1611
1662
|
surface is a card the user can see context around.
|
|
@@ -1631,11 +1682,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1631
1682
|
The built-in scroller is **full-bleed horizontally** and re-insets its content by the same
|
|
1632
1683
|
amount, so a scrolling body reaches the panel's real edges while the text stays on the
|
|
1633
1684
|
same column the header and footer use. Nothing to opt into; `disableBodyScroll` consumers
|
|
1634
|
-
(which own their scroll) are untouched.
|
|
1685
|
+
(which own their scroll) are untouched. It also carries the **scroll seam** every scroller
|
|
1686
|
+
that swaps its body in place takes — keyed on the routed popover's current route, with no prop
|
|
1687
|
+
at call site, and inert in a popover that has no sub-screens.
|
|
1635
1688
|
- **`popover_nav`** — `usePopoverNav` + `PopoverScreen` + `PopoverNavHeader` — the popover's
|
|
1636
1689
|
built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes,
|
|
1637
1690
|
`goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the
|
|
1638
|
-
root and screens render conditionally (unmounted when inactive
|
|
1691
|
+
root and screens render conditionally (unmounted when inactive, so a screen's own state does
|
|
1692
|
+
not survive the round trip — the panel's SCROLL does: the screens share `PopoverContent`'s one
|
|
1693
|
+
scroller, which takes the ROUTE as its seam key, so a sub-screen opens at the top with its
|
|
1694
|
+
`PopoverNavHeader` in view and the root list's offset returns on the way back),
|
|
1639
1695
|
`PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack`
|
|
1640
1696
|
(`right` slot, `backLabel`). For a multi-screen menu inside ONE popover; route PATTERNS,
|
|
1641
1697
|
`params`, and stacked-alive screens are `screen_router`'s job.
|
|
@@ -1661,6 +1717,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1661
1717
|
content) — it is what `SectionHeadingTitle.info`/`CardHeaderTitle.info` render.
|
|
1662
1718
|
- **`alert`** — `Alert` (+ `AlertButton`/`AlertOptions`): the blocking confirm —
|
|
1663
1719
|
`Alert.alert(title, message, [{cancel}, {destructive}])`.
|
|
1720
|
+
- **`overlay_layer`** — `OVERLAY_Z` / `OVERLAY_Z_ABOVE` / `NOTIFICATION_Z` / `SKIP_LINK_Z`: the
|
|
1721
|
+
ONE paint-order table for everything that reaches `document.body` (the rungs are in the table
|
|
1722
|
+
above), and the file that states why open order works. Constants only — there is no layer to
|
|
1723
|
+
render and nothing to claim; an overlay of your own gets its place by mounting its react-native
|
|
1724
|
+
`Modal` only while it is open. Never write one of these numbers as a literal.
|
|
1664
1725
|
- **`overlay_scope`** — `isOverlayScopeActive` / `pushOverlayScope` / `useOverlayScope`: the
|
|
1665
1726
|
module-level open-overlay counter every overlay primitive reports into; the host's
|
|
1666
1727
|
shortcut registry reads it synchronously to floor page-level shortcuts while any overlay
|
|
@@ -1715,11 +1776,24 @@ component rather than showing it at zero.
|
|
|
1715
1776
|
it carries SHAPE (rising, spiky, flat) and no readable values, which is exactly what a
|
|
1716
1777
|
register wants. Reach for a real chart the moment someone needs to read a value off it.
|
|
1717
1778
|
- **`bar_chart`** / **`line_chart`** / **`pie_chart`** — `BarChart` / `LineChart` /
|
|
1718
|
-
`PieChart`: the canonical SVG chart set (no recharts).
|
|
1719
|
-
as the track fits and thins the rest
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1779
|
+
`PieChart`: the canonical SVG chart set (no recharts). **A categorical axis prints as many
|
|
1780
|
+
labels as the track fits and thins the rest** — `LineChart` and `BarChart`'s vertical
|
|
1781
|
+
orientation both, because it is the axis's rule and not one chart's. It is anchored on the LAST
|
|
1782
|
+
position: the newest reading is the one a reader looks up, and anchoring there is what keeps the
|
|
1783
|
+
spacing uniform; the first position is labelled only when it clears the same distance, so a
|
|
1784
|
+
series whose length does not divide evenly drops its opening label rather than crowding the one
|
|
1785
|
+
beside it. **The BARS never thin** — how many bars a card carries is a question about the data,
|
|
1786
|
+
never about how wide a label happens to be, and an unlabelled bar keeps its slot so the row
|
|
1787
|
+
above stays in line. **What thins is the whole annotation, label AND value together**, because a
|
|
1788
|
+
bar's name and its figure are one reading: a number under an unnamed bar is a quantity of nothing
|
|
1789
|
+
a reader can name, and the surviving annotation is widened across the room its dropped neighbours
|
|
1790
|
+
vacated — room the figure needs as much as the name does. So a DENSE vertical `BarChart` (more
|
|
1791
|
+
bars than `track ÷ 50px`: a 30-day daily series in a 600px card keeps roughly ten) shows the
|
|
1792
|
+
figures only for the bars it labels. When every bar's number has to be readable, that is a
|
|
1793
|
+
`Table` or the horizontal orientation, which gives each entity its own row and its own value
|
|
1794
|
+
column. `BarChart`'s horizontal orientation is one row per entity: **`labelWidth`**
|
|
1795
|
+
sizes the fixed label column (default 80, which fits a date and not a company name), and each
|
|
1796
|
+
item's **`leading`** slot carries that entity's own mark ahead of its name.
|
|
1723
1797
|
- **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
|
|
1724
1798
|
plain sm tabular count beside it.
|
|
1725
1799
|
- **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one. Same API shape as the bar — a real `value`/`max` rather than a
|
|
@@ -1741,7 +1815,10 @@ component rather than showing it at zero.
|
|
|
1741
1815
|
cost make-up per product line. `series` (key/label/colour, one hue family per dimension)
|
|
1742
1816
|
drives a legend that is on by default; each `StackedBarRow` takes `label` + optional `meta`,
|
|
1743
1817
|
a pre-formatted headline `value` with its `valueTone`, and a `caption` sentence, so colour is
|
|
1744
|
-
never the only channel.
|
|
1818
|
+
never the only channel. **`leading`** is the entity's own mark (`BrandMark`, `Avatar`, a status
|
|
1819
|
+
dot), centred on the label's line box — a row's segment colours belong to the MEASURE, so unlike
|
|
1820
|
+
a legend row this one carries no identity until you give it one, and a chart beside a table over
|
|
1821
|
+
the same entities has to draw them the same way (composition.md § the register's own craft). Between `stacked_progress_bar` (one whole, own track) and `breakdown`
|
|
1745
1822
|
(one whole + ranked share rows beneath it): reach here the moment there are SEVERAL wholes to
|
|
1746
1823
|
compare.
|
|
1747
1824
|
- **`waterfall_chart`** — `WaterfallChart`: the BRIDGE — an opening level, the signed steps
|
|
@@ -2094,13 +2171,15 @@ component rather than showing it at zero.
|
|
|
2094
2171
|
hatch — input/error untouched). A per-tool failure shows amber with the reason in its expanded
|
|
2095
2172
|
Error panel; a run-level breaking `error` (outside `parts`) renders as a terminal danger row, with
|
|
2096
2173
|
an optional `onRetry` Button under it. While it runs the whole feed is ONE row and NO prose: the
|
|
2097
|
-
row names the call in flight ("Searching records") and says "Thinking
|
|
2098
|
-
model is writing rather than calling
|
|
2174
|
+
row names the call in flight ("Searching records") and says "Thinking" whenever the
|
|
2175
|
+
model is writing rather than calling; an unsettled row BREATHES, label and dot on one rhythm,
|
|
2176
|
+
which is why no live label carries an ellipsis (a row that is parked `awaiting` does not breathe). The agent's between-call narration is never shown — the one
|
|
2099
2177
|
message needing a response is a bulk-mutation confirmation, and the agent waits there, so the run
|
|
2100
2178
|
stops and that text becomes the answer. A reply with no tool calls is all answer and still streams.
|
|
2101
|
-
At settle the work folds to "{last action}" with the answer below. Expanding
|
|
2102
|
-
|
|
2103
|
-
|
|
2179
|
+
At settle the work folds to "{last action}" with the answer below. Expanding opens the timeline
|
|
2180
|
+
IN FULL, live or settled — the run never frames itself, and the HOST'S scroller follows it (a
|
|
2181
|
+
self-imposed window nests a scroller inside the host's own, which in chat means an inverted list
|
|
2182
|
+
inside an inverted list). Stays fully open while a call is parked, when a SETTLED run ended on
|
|
2104
2183
|
a tool call, and when the work is already one row. `collapseProcess={false}` hands the job back to
|
|
2105
2184
|
the caller (a surface that already frames the run — it then owns the `FollowScroll` too);
|
|
2106
2185
|
`summarizeRun(steps)` renames the row.
|
|
@@ -2134,7 +2213,8 @@ component rather than showing it at zero.
|
|
|
2134
2213
|
the chat message list's inverted mechanism packaged as a wrapper (single-cell inverted list),
|
|
2135
2214
|
so the follow happens at LAYOUT level: each growth paints already pinned (no flash), scrolling
|
|
2136
2215
|
up releases it, and short content still reads top-down. Wrap a streaming `AgentRun` with it in
|
|
2137
|
-
any bounded container (a dialog, a drawer, a panel — put the height bound on `style`)
|
|
2216
|
+
any bounded container (a dialog, a drawer, a panel — put the height bound on `style`) — but ONLY
|
|
2217
|
+
one you framed yourself with `collapseProcess={false}`, or it nests a scroller in a scroller;
|
|
2138
2218
|
`AgentProgress`'s expanded panel uses it. Inherits the inverted pattern's quirks (scrollbar
|
|
2139
2219
|
thumb runs opposite; keyboard paging inverted) — same as chat. A full-page chat list is
|
|
2140
2220
|
already inverted and doesn't need it.
|
|
@@ -2202,7 +2282,14 @@ component rather than showing it at zero.
|
|
|
2202
2282
|
`ids` is normally a memo over query rows — a new array every render, which is the
|
|
2203
2283
|
`Maximum update depth exceeded` loop). `initial` defaults to `accepted` so the operator drops
|
|
2204
2284
|
exceptions instead of approving eight identical lines; pass `pending` when each change deserves
|
|
2205
|
-
its own verdict and gate the commit on `settled`.
|
|
2285
|
+
its own verdict and gate the commit on `settled`. **`initial` also takes a
|
|
2286
|
+
`ReadonlyMap<id, decision>`**, for a set that mixes kinds with different safe defaults —
|
|
2287
|
+
filling a blank arrives `accepted`, overwriting a value a human already set arrives `rejected`,
|
|
2288
|
+
so the destructive half is opt-in; an id the map does not name arrives `accepted`. Build it in a
|
|
2289
|
+
`useMemo` over your own rows: it is a VALUE, so `status` and the groups derive from it together
|
|
2290
|
+
and `status` changes identity whenever a decision does — which is what a screen memoizing its
|
|
2291
|
+
rows on `status` needs. Do not reach for `reject()` in the result handler instead: that writes
|
|
2292
|
+
overrides the operator never made, so `undo` hands the row back `accepted`.
|
|
2206
2293
|
- **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
|
|
2207
2294
|
(the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
|
|
2208
2295
|
the question — e.g. a wizard's "1 / 3"). `ClarifyOption` requires
|
|
@@ -2231,7 +2318,7 @@ component rather than showing it at zero.
|
|
|
2231
2318
|
or test DECLARES one rather than mapping from `ClarifyWizardQuestion` (an option is a label +
|
|
2232
2319
|
description; the answer's value IS the label, the `ask_user_choice` wire shape). Seams:
|
|
2233
2320
|
`labelForCall` / `renderToolOutput`; no empty-state slot by design (`AgentRun` owns the
|
|
2234
|
-
localized "Starting
|
|
2321
|
+
localized "Starting" row). Worked example: `tpl_item_list`'s intake dialog.
|
|
2235
2322
|
- **`choice_list`** — `ChoiceList` + `ChoiceOption`: selectable answer options as
|
|
2236
2323
|
divider-separated rows (no bordered cards) with a per-row focus ring + hover wash; the
|
|
2237
2324
|
agent's quick-reply surface. `allowCustom` appends an always-visible borderless multiline field whose
|
package/docs/composition.md
CHANGED
|
@@ -69,6 +69,28 @@ the subject into rows.
|
|
|
69
69
|
the same left edge as the title above it and the actions below it. Never hand-pad inside one,
|
|
70
70
|
and never hand-roll one: the three surfaces disagree on the number (24 / responsive / 20) and
|
|
71
71
|
only the surface knows which it is.
|
|
72
|
+
- **ANY scroller whose body is SWAPPED opens the new content at the top.** Stated as a rule and
|
|
73
|
+
not as a count of the surfaces that happen to have one today: `ModalBody`,
|
|
74
|
+
`DialogScrollArea` and `DrawerScrollArea` each take a `scrollKey` — the content's identity (a
|
|
75
|
+
record id, a step name) — and the kit's routed `Popover` takes the same seam with no prop at
|
|
76
|
+
all, because its ROUTE already is that identity. A scroller you add that swaps its body joins
|
|
77
|
+
the rule rather than being added to a list. `scrollKey={openChild?.id}` is the right shape:
|
|
78
|
+
an ABSENT key names the root content, not an opt-out, so the offset comes back when the child
|
|
79
|
+
closes. Opting out needs no signal — a surface whose key never changes never scrolls. Without it the container keeps the offset it
|
|
80
|
+
was holding for content that no longer exists, so a swapped-in record opens part-way down
|
|
81
|
+
itself with its heading off-screen above — invisible until the first list long enough to
|
|
82
|
+
scroll, which is the same list that made the swap worth having. The key opens unseen content
|
|
83
|
+
at the top and restores the offset when it comes BACK, which is what a React `key` on the
|
|
84
|
+
scroll area cannot do: that rebuilds the container, resetting the parent's place along with
|
|
85
|
+
the child's. Omit it where the body is one thing, and inside a `Screen`-routed dialog, where
|
|
86
|
+
a stacked screen stays mounted and already keeps its own offset.
|
|
87
|
+
- **Overlays paint in the order they were OPENED, and no call site has to know it.** Mount them
|
|
88
|
+
wherever composition wants — a `Dialog` sitting always-mounted beside the `Drawer` whose row
|
|
89
|
+
opens it is the ordinary shape, and it is the one that used to fail: the dialog claimed its DOM
|
|
90
|
+
slot on the app's first paint, the drawer landed after it, and the dialog then rendered
|
|
91
|
+
perfectly and read correctly with every control under the panel DEAD. Whatever opens last is on
|
|
92
|
+
top, both ways round, so a drawer opened from a dialog covers it too. `Alert` and `Tooltip` sit
|
|
93
|
+
above every overlay at any depth — they are ABOUT the surface under them.
|
|
72
94
|
- **The responsive one is READ, never re-derived — `useDialogGutter()`, no argument.** Inside
|
|
73
95
|
a `Dialog` that is what the header, the scroll area, the footer and any pane a caller drops
|
|
74
96
|
in all call. The `Dialog` resolves `small ? 16 : 24` ONCE, off the SCREEN, and publishes it.
|
|
@@ -829,7 +851,11 @@ unconditionally and let an empty `value` disable it, rather than revealing it on
|
|
|
829
851
|
|
|
830
852
|
Pressing a PRIMARY entity row opens the record workspace in a `Drawer` with `onPrev`/`onNext`/
|
|
831
853
|
`position` ("3/24") over the visible ordering (←/→ arrow keys are built in). Key the drawer body by
|
|
832
|
-
record id so per-record state resets on step.
|
|
854
|
+
record id so per-record state resets on step. A drawer that instead REPLACES its body with a child
|
|
855
|
+
record — the shape where a row inside the panel opens a second record rather than stacking a
|
|
856
|
+
second drawer — passes that id as `DrawerScrollArea`'s `scrollKey` (`scrollKey={openChild?.id}` —
|
|
857
|
+
the undefined leg is the list's own identity), so the child opens at the top and the list keeps its
|
|
858
|
+
place on the way back. Facts are `DetailRow`s; the commit bar is a
|
|
833
859
|
`DrawerFooter` (a hairline-topped band pinned to the panel bottom — render it as the LAST child,
|
|
834
860
|
after the scroll body; actions sit right, a leading `<Text style={{ flex: 1 }}>` hint pushes them
|
|
835
861
|
there). The open row shows a `selected` highlight (and `marked` for a bulk-ticked row). `Peek` is
|
|
@@ -1318,6 +1344,15 @@ colours, same shapes. Re-rendering it as plain text makes the reader translate b
|
|
|
1318
1344
|
presentations of one field, and the two drift the first time either side gains a value. This holds
|
|
1319
1345
|
for every custom-rendered value, not just status.
|
|
1320
1346
|
|
|
1347
|
+
**The same rule reaches the CHART beside the table.** One entity, one rendering, everywhere inside
|
|
1348
|
+
one card: if the table's first cell carries the entity's mark (`BrandMark`, `Avatar`, a status
|
|
1349
|
+
dot), the chart's rows carry it too — `StackedBarRow` and `BarChartItem` each take a `leading`
|
|
1350
|
+
slot for exactly that, laid out on the label's own line box. It is worst where the mark carries
|
|
1351
|
+
most (a channel, a platform, a person), and the two ways out that are not the slot are both
|
|
1352
|
+
losses: dropping the mark from the table to match makes *both* halves anonymous, and hand-drawing
|
|
1353
|
+
the chart to get one slot loses the component. A LEGEND row is the exception and needs no slot —
|
|
1354
|
+
its subject is a category, and the colour swatch already is that category's identity.
|
|
1355
|
+
|
|
1321
1356
|
**Group by what implies a different ACTION, never by a category the reader can already see.**
|
|
1322
1357
|
Bands like *Waiting on you* / *Gone quiet* / *Open* / *Closed* each name a different response, so
|
|
1323
1358
|
the grouping tells the reader something no column does. Grouping by a value that is already a
|
|
@@ -1780,13 +1815,20 @@ complaint it was meant to fix, because a reader who cannot read a label reports
|
|
|
1780
1815
|
cluttered rather than as small. Recede with INK, at the same rung. `xs` is for genuine meta (a
|
|
1781
1816
|
timestamp, a count), not for a label that a person actually has to read.
|
|
1782
1817
|
|
|
1783
|
-
**A LABEL sharing a row with a flexible VALUE must be told which one
|
|
1818
|
+
**A LABEL sharing a row with a flexible VALUE must be told which one HOLDS.** Two items in one
|
|
1784
1819
|
flex row, and whichever cannot shrink forces the other to. It is invisible wherever the values
|
|
1785
1820
|
happen to fit, which is usually the width it was built at, and it appears the first time a real
|
|
1786
|
-
record carries a long one.
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1821
|
+
record carries a long one. Say which one stays whole — `flexShrink: 0` on a fixed-length key, on a
|
|
1822
|
+
figure, on a control — and pick by which is still USEFUL truncated: `RC-2026-0…` is unusable,
|
|
1823
|
+
`(555) 384-7…` is still recognisably a phone number.
|
|
1824
|
+
|
|
1825
|
+
The clipping half needs only **`numberOfLines`**, which now brings its own `minWidth: 0` +
|
|
1826
|
+
`flexShrink: 1`: a clamp that cannot shrink cannot clamp, so the prop that says "cut this to fit"
|
|
1827
|
+
is what makes it possible. (Neither platform gave it for free — on web a clamped `Text` is
|
|
1828
|
+
`white-space: nowrap`, so `min-width: auto` floors it at the WHOLE string; on native a `Text` is
|
|
1829
|
+
`flexShrink: 0`. A row of two clamped values laid out at intrinsic width and ran off the frame.)
|
|
1830
|
+
Both are no-ops until a row actually overflows, and a `style` on the call site still wins — which
|
|
1831
|
+
is how a figure keeps holding over its own label.
|
|
1790
1832
|
|
|
1791
1833
|
|
|
1792
1834
|
|
package/docs/templates.md
CHANGED
|
@@ -450,7 +450,9 @@ The corollaries, each of which a register is routinely missing:
|
|
|
450
450
|
DRAFTS; it does not write. Every drafted record then renders in full — all four fields as
|
|
451
451
|
they will be stored, each editable in place, a `DiffMark` per card, Keep/Drop via
|
|
452
452
|
`useChangeSet` (`initial: "accepted"` — the operator drops the exceptions rather than
|
|
453
|
-
approving six identical records
|
|
453
|
+
approving six identical records; a set that also OVERWRITES values a human set passes a
|
|
454
|
+
`Map<id, decision>` instead, so that half arrives dropped) — and ONE commit named for its outcome
|
|
455
|
+
creates them. The
|
|
454
456
|
manual form beside it skips all of that and saves direct. Only the DOCUMENT path needs the
|
|
455
457
|
gate. The receipt still follows the write — it states the outcome, ROUTES, and carries the
|
|
456
458
|
deterministic checks (a checksum, a count reconciliation).
|
package/docs/testing.md
CHANGED
|
@@ -46,6 +46,12 @@ Assert two things, not one: that the content appeared, and that it **dismisses**
|
|
|
46
46
|
backdrop, so clicking the trigger a second time is often intercepted; click the
|
|
47
47
|
backdrop or press `Escape`.
|
|
48
48
|
|
|
49
|
+
Two overlays open at once stack by the order they were OPENED, so the newest one
|
|
50
|
+
takes the press. If a driver reports `subtree intercepts pointer events` on a
|
|
51
|
+
control of the overlay you just opened, that is a real defect and not this
|
|
52
|
+
anatomy: check with `document.elementFromPoint` at the control's centre, which
|
|
53
|
+
names whatever is actually on top.
|
|
54
|
+
|
|
49
55
|
## Custom pointer drag is not `dragTo`
|
|
50
56
|
|
|
51
57
|
The calendar and gantt drags are built on `use_pointer_drag`, which listens for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "46.
|
|
3
|
+
"version": "46.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"./popover_nav": "./src/popover_nav.tsx",
|
|
151
151
|
"./popover": "./src/popover.tsx",
|
|
152
152
|
"./overlay_scope": "./src/overlay_scope.ts",
|
|
153
|
+
"./overlay_layer": "./src/overlay_layer.ts",
|
|
153
154
|
"./switcher": "./src/switcher.tsx",
|
|
154
155
|
"./menu_button": "./src/menu_button.tsx",
|
|
155
156
|
"./menu_list_item": "./src/menu_list_item.tsx",
|
|
@@ -334,7 +335,7 @@
|
|
|
334
335
|
"dependencies": {
|
|
335
336
|
"@lotics/docx": "^0.3.0",
|
|
336
337
|
"@lotics/markdown-editor": "^0.1.0",
|
|
337
|
-
"@lotics/xlsx": "^0.
|
|
338
|
+
"@lotics/xlsx": "^0.3.0",
|
|
338
339
|
"ai": "^7.0.30",
|
|
339
340
|
"mdast-util-from-markdown": "^2.0.3",
|
|
340
341
|
"mdast-util-gfm-footnote": "^2.1.0",
|