@lotics/ui 41.3.0 → 41.4.1
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 -0
- package/MIGRATION.md +17 -0
- package/docs/catalog.md +3 -1
- package/docs/composition.md +23 -0
- package/docs/testing.md +70 -0
- package/package.json +1 -1
- package/src/markdown.web.tsx +56 -2
package/AGENTS.md
CHANGED
|
@@ -18,6 +18,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
18
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 (`InlineFiles` for a record ROW — list + one CTA, `multiple` decides add-vs-replace; the `FilesEditor` COMPOUND for a whole section — 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, whether a multiline value keeps its fixed reserve or `autoGrow`s (who decides the length — the field, or whoever is typing), 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 — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; 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, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
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 (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
|
+
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
21
22
|
| [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) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
22
23
|
|
|
23
24
|
## Iron rules
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
## 41.4.0 — embedded markdown is demoted in the OUTLINE, not only on the type ladder
|
|
8
|
+
|
|
9
|
+
`<Markdown variant="embedded">` sized its headings down and left them as the tags the author
|
|
10
|
+
wrote, so an `h2` inside a record value still landed in heading navigation as a PEER of the
|
|
11
|
+
page's own sections. Measured on a record surface, the outline read:
|
|
12
|
+
|
|
13
|
+
> Activity · "Tóm tắt cuộc họp (AI) — …" · "Bài học …" · Details · Next action
|
|
14
|
+
|
|
15
|
+
— a model's call-summary title between two real sections, contributed from inside ONE row of a
|
|
16
|
+
feed that can hold twenty more. Nothing visual finds this, and fixing the size is what makes it
|
|
17
|
+
invisible: the defect stops looking wrong at the moment it stops being measurable.
|
|
18
|
+
|
|
19
|
+
Embedded headings now render `h4`/`h5`/`h6` (h1→h4, h2→h5, h3 and deeper→h6). They stay
|
|
20
|
+
headings, so a reader can still navigate inside a long summary; they simply can no longer reach
|
|
21
|
+
a section's level. **Nothing to migrate** — document-scale markdown is untouched, and the
|
|
22
|
+
embedded type scale is unchanged.
|
|
23
|
+
|
|
7
24
|
## 41.3.0 — `Timeline` rows, embedded markdown, and a disclosure that is not a link
|
|
8
25
|
|
|
9
26
|
Nothing here removes an API or breaks a type. Two of the changes alter what your rows render on
|
package/docs/catalog.md
CHANGED
|
@@ -413,7 +413,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
413
413
|
field on a record, a summary in a feed row. Reach for `"embedded"` whenever the
|
|
414
414
|
text was not written by whoever built the screen, which is now the common case
|
|
415
415
|
because the writer is routinely a model; at document scale an `h1` it emits is
|
|
416
|
-
the same rung as your `SectionHeadingTitle`.
|
|
416
|
+
the same rung as your `SectionHeadingTitle`. Embedded demotes the heading LEVEL as well as
|
|
417
|
+
the scale (h1→h4, h2→h5, h3+→h6): sizing alone leaves a model's `##` announcing as a PEER of
|
|
418
|
+
your own sections in heading navigation, which no visual check can see.
|
|
417
419
|
- **`markdown_types`** — `MarkdownProps`; types only.
|
|
418
420
|
- **`markdown.css`** — import once for the web markdown styling.
|
|
419
421
|
- **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle`.
|
package/docs/composition.md
CHANGED
|
@@ -1047,6 +1047,29 @@ most common, because it survives every other check: each piece is defensible alo
|
|
|
1047
1047
|
question "what does this tell me that the screen does not already say" catches decoration wearing
|
|
1048
1048
|
an information costume.
|
|
1049
1049
|
|
|
1050
|
+
## `accessibilityState` never reaches the DOM — write `aria-*` yourself
|
|
1051
|
+
|
|
1052
|
+
React Native Web maps a subset of RN's accessibility props and **silently drops
|
|
1053
|
+
`accessibilityState`**. So a control written the RN way announces nothing:
|
|
1054
|
+
|
|
1055
|
+
```tsx
|
|
1056
|
+
// renders, typechecks, and the DOM carries no state at all
|
|
1057
|
+
<Pressable accessibilityRole="tab" accessibilityState={{ selected: isActive }} />
|
|
1058
|
+
|
|
1059
|
+
// what actually reaches the DOM
|
|
1060
|
+
<Pressable accessibilityRole="tab" aria-selected={isActive} />
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
The trap is the failure's shape rather than its cause. There is no type error —
|
|
1064
|
+
the prop is declared — no runtime warning, and the control looks and behaves
|
|
1065
|
+
correctly; only a screen reader and the DOM inspector disagree with the source.
|
|
1066
|
+
Nothing in a normal loop surfaces it, so it survives review, tests and QA.
|
|
1067
|
+
|
|
1068
|
+
Write the raw `aria-*` attribute for any STATE (`aria-selected`, `aria-checked`,
|
|
1069
|
+
`aria-expanded`, `aria-disabled`, `aria-current`). `accessibilityRole` and
|
|
1070
|
+
`accessibilityLabel` do map and are fine as-is. **Verify in the DOM, never in the
|
|
1071
|
+
source** — the source is what lies here.
|
|
1072
|
+
|
|
1050
1073
|
## Testing an overlay component — assert through the logic, prove in a browser
|
|
1051
1074
|
|
|
1052
1075
|
**A `Popover`-backed surface does not mount under jsdom.** It positions in a `useLayoutEffect` +
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Driving the kit in a browser
|
|
2
|
+
|
|
3
|
+
Three kit anatomies where the accessibility tree says one thing and an automated
|
|
4
|
+
driver has to do another. Each is **by design** — the shape that makes the
|
|
5
|
+
component correct for a keyboard and a screen reader is the shape that defeats a
|
|
6
|
+
naive `click()` — so each will read as a bug the first time, and the wrong
|
|
7
|
+
reaction (`force: true`, or "the component is broken") costs a debugging session.
|
|
8
|
+
|
|
9
|
+
Everything here is true wherever the kit renders. The iframe that a Lotics app
|
|
10
|
+
runs inside adds one more rule on top — `lotics docs building_an_app` § 8.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## A pressable row: click the surface, not the named button
|
|
15
|
+
|
|
16
|
+
A row that presses open **and** carries its own controls is a role-less
|
|
17
|
+
`PressableRow` with a `PressDoor` sibling (`@lotics/ui` AGENTS.md — *a button
|
|
18
|
+
never contains a control*). The door is what a keyboard and a screen reader
|
|
19
|
+
use: it holds the tab stop, the accessible name and the focus ring, and it is an
|
|
20
|
+
**empty absolutely-positioned element**. The cells render above it.
|
|
21
|
+
|
|
22
|
+
So the a11y tree shows `button "Open ACME-1042"`, and clicking it fails:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
subtree intercepts pointer events
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That is the design working, not a defect — the visible row is above the door
|
|
29
|
+
precisely so a pointer lands on the row rather than the door. Drive it the way a
|
|
30
|
+
mouse user does:
|
|
31
|
+
|
|
32
|
+
- **click the row container** — the `generic [cursor=pointer]` wrapping the door;
|
|
33
|
+
the click bubbles to `PressableRow`
|
|
34
|
+
- or `dispatchEvent('click')` on the door directly
|
|
35
|
+
- or exercise the keyboard path, which is the one the door exists for: focus the
|
|
36
|
+
door and press `Enter`
|
|
37
|
+
|
|
38
|
+
**Never reach for `force: true`.** It suppresses the actionability check that is
|
|
39
|
+
telling you the truth, and the click then lands somewhere you did not choose.
|
|
40
|
+
|
|
41
|
+
## Overlays render at the top of the DOM, not inside their trigger
|
|
42
|
+
|
|
43
|
+
Popovers, tooltips and dialogs go through a portal (`dom_portal`), so their
|
|
44
|
+
content is a sibling near the end of the document rather than a descendant of
|
|
45
|
+
the control that opened it. Looking for it under the trigger finds nothing.
|
|
46
|
+
|
|
47
|
+
Assert two things, not one: that the content appeared, and that it **dismisses**
|
|
48
|
+
— outside-click and `Escape` both. An open overlay usually has a click-catching
|
|
49
|
+
backdrop, so clicking the trigger a second time is often intercepted; click the
|
|
50
|
+
backdrop or press `Escape`.
|
|
51
|
+
|
|
52
|
+
## Custom pointer drag is not `dragTo`
|
|
53
|
+
|
|
54
|
+
The calendar and gantt drags are built on `use_pointer_drag`, which listens for
|
|
55
|
+
real `pointerdown` / `pointermove` / `pointerup`. Playwright's `dragTo` and mouse
|
|
56
|
+
emulation do not drive them — the sequence simply does nothing, with no error.
|
|
57
|
+
|
|
58
|
+
Dispatch the events yourself, **in the frame's own context** so the coordinates
|
|
59
|
+
and the window are the right ones. From the dragged element,
|
|
60
|
+
`el.ownerDocument.defaultView` is that window:
|
|
61
|
+
|
|
62
|
+
1. `pointerdown` on the element
|
|
63
|
+
2. `pointermove` on the window, past the drag threshold (a few pixels — a smaller
|
|
64
|
+
move is treated as a click, deliberately)
|
|
65
|
+
3. `pointerup` on the window, with `clientX`/`clientY` at the drop target
|
|
66
|
+
|
|
67
|
+
Then check **both** halves: re-snapshot for the optimistic move, and re-read the
|
|
68
|
+
record to confirm the mutation actually persisted. An optimistic move that never
|
|
69
|
+
reached the server looks identical on screen, which is the whole reason to check
|
|
70
|
+
the second one.
|
package/package.json
CHANGED
package/src/markdown.web.tsx
CHANGED
|
@@ -8,10 +8,14 @@ import { colors } from "./colors";
|
|
|
8
8
|
import type { MarkdownProps } from "./markdown_types";
|
|
9
9
|
|
|
10
10
|
export function Markdown({ children, variant = "document" }: MarkdownProps) {
|
|
11
|
+
const embedded = variant === "embedded";
|
|
11
12
|
return (
|
|
12
13
|
<View>
|
|
13
|
-
<div className={
|
|
14
|
-
<ReactMarkdown
|
|
14
|
+
<div className={embedded ? "ui-markdown ui-markdown-embedded" : "ui-markdown"}>
|
|
15
|
+
<ReactMarkdown
|
|
16
|
+
remarkPlugins={[remarkGfmSafe]}
|
|
17
|
+
components={embedded ? embeddedComponents : markdownComponents}
|
|
18
|
+
>
|
|
15
19
|
{children}
|
|
16
20
|
</ReactMarkdown>
|
|
17
21
|
</div>
|
|
@@ -24,6 +28,56 @@ const markdownComponents = {
|
|
|
24
28
|
table: CopyableTable,
|
|
25
29
|
};
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* EMBEDDED MARKDOWN IS DEMOTED IN THE OUTLINE, not only on the type ladder.
|
|
33
|
+
*
|
|
34
|
+
* Sizing the headings down fixes what a screen SHOWS and nothing about what it
|
|
35
|
+
* ANNOUNCES: an `h2` the author of the text happened to write is still an `h2`,
|
|
36
|
+
* so it lands in heading navigation as a PEER of the page's own sections.
|
|
37
|
+
* Measured on a record surface, the outline read
|
|
38
|
+
*
|
|
39
|
+
* Activity · "Tóm tắt cuộc họp (AI) — …" · "Bài học …" · Details · Next action
|
|
40
|
+
*
|
|
41
|
+
* — a model's call-summary title sitting between two real sections, from inside
|
|
42
|
+
* one row of a feed that can hold twenty more. No visual probe finds this, and
|
|
43
|
+
* fixing the size is what makes it invisible: the defect stops looking wrong at
|
|
44
|
+
* the exact moment it stops being measurable.
|
|
45
|
+
*
|
|
46
|
+
* The page's sections are `h2` (`SectionHeading`), and embedded markdown is a
|
|
47
|
+
* VALUE inside one — inside a row, usually — so it starts at `h4`: deep enough
|
|
48
|
+
* that it can never be a section's peer, shallow enough to keep its own
|
|
49
|
+
* hierarchy. It stays a heading rather than becoming a `<div>`, because a long
|
|
50
|
+
* summary genuinely has parts and a reader navigating INSIDE it should still
|
|
51
|
+
* find them.
|
|
52
|
+
*/
|
|
53
|
+
const EMBEDDED_HEADING_LEVEL: Record<string, "h4" | "h5" | "h6"> = {
|
|
54
|
+
h1: "h4",
|
|
55
|
+
h2: "h5",
|
|
56
|
+
h3: "h6",
|
|
57
|
+
h4: "h6",
|
|
58
|
+
h5: "h6",
|
|
59
|
+
h6: "h6",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type HeadingProps = React.ComponentPropsWithoutRef<"h4"> & { node?: unknown };
|
|
63
|
+
|
|
64
|
+
function embeddedHeading(from: keyof typeof EMBEDDED_HEADING_LEVEL) {
|
|
65
|
+
const Tag = EMBEDDED_HEADING_LEVEL[from];
|
|
66
|
+
return function EmbeddedHeading({ node: _node, children, ...rest }: HeadingProps) {
|
|
67
|
+
return <Tag {...rest}>{children}</Tag>;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const embeddedComponents = {
|
|
72
|
+
...markdownComponents,
|
|
73
|
+
h1: embeddedHeading("h1"),
|
|
74
|
+
h2: embeddedHeading("h2"),
|
|
75
|
+
h3: embeddedHeading("h3"),
|
|
76
|
+
h4: embeddedHeading("h4"),
|
|
77
|
+
h5: embeddedHeading("h5"),
|
|
78
|
+
h6: embeddedHeading("h6"),
|
|
79
|
+
};
|
|
80
|
+
|
|
27
81
|
function CopyableTable(props: React.ComponentPropsWithoutRef<"table"> & { node?: unknown }) {
|
|
28
82
|
const { node: _node, children, ...rest } = props;
|
|
29
83
|
const tableRef = useRef<HTMLTableElement>(null);
|