@exponential-at/ui 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/NOTICE +54 -0
- package/README.md +360 -2
- package/catalog/a11y.json +668 -0
- package/catalog/basic-map.json +104 -0
- package/catalog/code.json +58 -0
- package/catalog/core.catalog.json +4752 -0
- package/catalog/core.schema.json +22667 -0
- package/catalog/host.json +64 -0
- package/catalog/layout.json +20 -0
- package/catalog/limits.json +9 -0
- package/catalog/locale.json +48 -0
- package/catalog/macros.json +3417 -0
- package/catalog/recipes.json +61 -0
- package/catalog/strings.json +72 -0
- package/catalog/style.json +128 -0
- package/catalog/theme.schema.json +42543 -0
- package/catalog/tokens.json +47 -0
- package/conformance/README.md +217 -0
- package/conformance/baseline.ts +101 -0
- package/conformance/browser-page.ts +146 -0
- package/conformance/browser-suites.ts +683 -0
- package/conformance/compare.ts +201 -0
- package/conformance/dom.ts +96 -0
- package/conformance/dump.ts +163 -0
- package/conformance/fonts.json +154 -0
- package/conformance/manifest.json +224 -0
- package/conformance/parity-compare.ts +191 -0
- package/conformance/parity-report.schema.json +73 -0
- package/conformance/parity.ts +412 -0
- package/conformance/pixel.ts +177 -0
- package/conformance/primitives-page.ts +49 -0
- package/conformance/report.schema.json +66 -0
- package/conformance/run.ts +118 -0
- package/dist/chunks/index-98npjvsd.js +3940 -0
- package/dist/chunks/index-98npjvsd.js.map +27 -0
- package/dist/chunks/index-gwkt14w3.js +7314 -0
- package/dist/chunks/index-gwkt14w3.js.map +10 -0
- package/dist/chunks/index-njt7yzzm.js +134 -0
- package/dist/chunks/index-njt7yzzm.js.map +9 -0
- package/dist/chunks/index-we7va6v6.js +7984 -0
- package/dist/chunks/index-we7va6v6.js.map +15 -0
- package/dist/chunks/index-xrm6f7wc.js +2563 -0
- package/dist/chunks/index-xrm6f7wc.js.map +10 -0
- package/dist/conformance.js +182749 -0
- package/dist/conformance.js.map +10 -0
- package/dist/entries/prompt.js +251 -0
- package/dist/entries/prompt.js.map +11 -0
- package/dist/entries/schema.js +1725 -0
- package/dist/entries/schema.js.map +11 -0
- package/dist/host/index.js +122 -0
- package/dist/host/index.js.map +9 -0
- package/dist/index.js +3213 -0
- package/dist/index.js.map +27 -0
- package/dist/types/src/a11y.d.ts +21 -0
- package/dist/types/src/animation.d.ts +76 -0
- package/dist/types/src/basic-map.d.ts +38 -0
- package/dist/types/src/builder.d.ts +50 -0
- package/dist/types/src/catalog.d.ts +54 -0
- package/dist/types/src/catalog.described.d.ts +33 -0
- package/dist/types/src/catalog.generated.d.ts +829 -0
- package/dist/types/src/chart.d.ts +46 -0
- package/dist/types/src/code.d.ts +44 -0
- package/dist/types/src/color.d.ts +20 -0
- package/dist/types/src/conformance.d.ts +39 -0
- package/dist/types/src/connector/app.d.ts +14 -0
- package/dist/types/src/connector/connector.d.ts +77 -0
- package/dist/types/src/connector/index.d.ts +4 -0
- package/dist/types/src/css.d.ts +29 -0
- package/dist/types/src/define.d.ts +66 -0
- package/dist/types/src/direction.d.ts +16 -0
- package/dist/types/src/dynamic.d.ts +159 -0
- package/dist/types/src/entries/prompt.d.ts +11 -0
- package/dist/types/src/entries/schema.d.ts +8 -0
- package/dist/types/src/expr.d.ts +71 -0
- package/dist/types/src/extension.d.ts +4 -0
- package/dist/types/src/format.d.ts +112 -0
- package/dist/types/src/geometry.d.ts +40 -0
- package/dist/types/src/host/contract.d.ts +188 -0
- package/dist/types/src/host/decoders.d.ts +61 -0
- package/dist/types/src/host/index.d.ts +11 -0
- package/dist/types/src/host/package.d.ts +58 -0
- package/dist/types/src/host/policy.d.ts +105 -0
- package/dist/types/src/host/router.d.ts +34 -0
- package/dist/types/src/host/runtime.d.ts +205 -0
- package/dist/types/src/host/sources.d.ts +15 -0
- package/dist/types/src/host/transports.d.ts +109 -0
- package/dist/types/src/index.d.ts +63 -0
- package/dist/types/src/layout.d.ts +20 -0
- package/dist/types/src/limits.d.ts +21 -0
- package/dist/types/src/list.d.ts +133 -0
- package/dist/types/src/locale.d.ts +22 -0
- package/dist/types/src/macros.d.ts +29 -0
- package/dist/types/src/overlay.d.ts +27 -0
- package/dist/types/src/prompt.d.ts +44 -0
- package/dist/types/src/recipes.d.ts +17 -0
- package/dist/types/src/reducer.d.ts +41 -0
- package/dist/types/src/resizable.d.ts +38 -0
- package/dist/types/src/schema.d.ts +7 -0
- package/dist/types/src/strings.d.ts +13 -0
- package/dist/types/src/style.d.ts +201 -0
- package/dist/types/src/test-preload.d.ts +1 -0
- package/dist/types/src/text-shimmer.d.ts +24 -0
- package/dist/types/src/theme-types.d.ts +162 -0
- package/dist/types/src/theme.d.ts +73 -0
- package/dist/types/src/themes.d.ts +22 -0
- package/dist/types/src/tree-guides.d.ts +17 -0
- package/dist/types/src/types.d.ts +314 -0
- package/dist/types/src/validate.d.ts +31 -0
- package/dist/types/src/web/base-css.d.ts +1 -0
- package/dist/types/src/web/box-css.d.ts +55 -0
- package/dist/types/src/web/coerce.d.ts +32 -0
- package/dist/types/src/web/data.d.ts +105 -0
- package/dist/types/src/web/dates.d.ts +12 -0
- package/dist/types/src/web/form.d.ts +44 -0
- package/dist/types/src/web/host-types.d.ts +119 -0
- package/dist/types/src/web/index.d.ts +28 -0
- package/dist/types/src/web/inputs.d.ts +55 -0
- package/dist/types/src/web/list.d.ts +13 -0
- package/dist/types/src/web/listbox.d.ts +14 -0
- package/dist/types/src/web/markdown.d.ts +96 -0
- package/dist/types/src/web/media.d.ts +62 -0
- package/dist/types/src/web/parts.d.ts +4 -0
- package/dist/types/src/web/primitives/alert-dialog.d.ts +10 -0
- package/dist/types/src/web/primitives/alert.d.ts +9 -0
- package/dist/types/src/web/primitives/avatar-color.d.ts +2 -0
- package/dist/types/src/web/primitives/avatar.d.ts +13 -0
- package/dist/types/src/web/primitives/badge.d.ts +10 -0
- package/dist/types/src/web/primitives/breadcrumb.d.ts +10 -0
- package/dist/types/src/web/primitives/button.d.ts +6 -0
- package/dist/types/src/web/primitives/calendar.d.ts +43 -0
- package/dist/types/src/web/primitives/card.d.ts +9 -0
- package/dist/types/src/web/primitives/checkbox.d.ts +5 -0
- package/dist/types/src/web/primitives/choice-tiles.d.ts +32 -0
- package/dist/types/src/web/primitives/cn.d.ts +3 -0
- package/dist/types/src/web/primitives/code-block.d.ts +24 -0
- package/dist/types/src/web/primitives/command.d.ts +134 -0
- package/dist/types/src/web/primitives/context-ring.d.ts +24 -0
- package/dist/types/src/web/primitives/dialog-arms.d.ts +6 -0
- package/dist/types/src/web/primitives/dialog.d.ts +24 -0
- package/dist/types/src/web/primitives/disclosure-header.d.ts +5 -0
- package/dist/types/src/web/primitives/dock.d.ts +42 -0
- package/dist/types/src/web/primitives/dropdown-menu.d.ts +14 -0
- package/dist/types/src/web/primitives/empty-state.d.ts +15 -0
- package/dist/types/src/web/primitives/glass-card.d.ts +4 -0
- package/dist/types/src/web/primitives/hover-card.d.ts +7 -0
- package/dist/types/src/web/primitives/icon-disc.d.ts +12 -0
- package/dist/types/src/web/primitives/index.d.ts +48 -0
- package/dist/types/src/web/primitives/input.d.ts +5 -0
- package/dist/types/src/web/primitives/kbd.d.ts +4 -0
- package/dist/types/src/web/primitives/label.d.ts +3 -0
- package/dist/types/src/web/primitives/menu-surface.d.ts +17 -0
- package/dist/types/src/web/primitives/meter.d.ts +21 -0
- package/dist/types/src/web/primitives/popover.d.ts +3 -0
- package/dist/types/src/web/primitives/progress-ring.d.ts +5 -0
- package/dist/types/src/web/primitives/progress.d.ts +8 -0
- package/dist/types/src/web/primitives/scroll-area.d.ts +49 -0
- package/dist/types/src/web/primitives/segmented-control.d.ts +5 -0
- package/dist/types/src/web/primitives/select.d.ts +47 -0
- package/dist/types/src/web/primitives/separator.d.ts +3 -0
- package/dist/types/src/web/primitives/sheet-chrome.d.ts +4 -0
- package/dist/types/src/web/primitives/sheet.d.ts +20 -0
- package/dist/types/src/web/primitives/skeleton.d.ts +3 -0
- package/dist/types/src/web/primitives/spinner.d.ts +3 -0
- package/dist/types/src/web/primitives/switch.d.ts +4 -0
- package/dist/types/src/web/primitives/table.d.ts +11 -0
- package/dist/types/src/web/primitives/tabs.d.ts +13 -0
- package/dist/types/src/web/primitives/textarea.d.ts +3 -0
- package/dist/types/src/web/primitives/toast.d.ts +163 -0
- package/dist/types/src/web/primitives/tooltip.d.ts +3 -0
- package/dist/types/src/web/primitives/truncate.d.ts +7 -0
- package/dist/types/src/web/primitives/typeahead-portal.d.ts +12 -0
- package/dist/types/src/web/share.d.ts +6 -0
- package/dist/types/src/web/table.d.ts +20 -0
- package/dist/types/src/web/theme-css.d.ts +69 -0
- package/dist/types/src/web/urls.d.ts +21 -0
- package/dist/web/index.js +2303 -0
- package/dist/web/index.js.map +26 -0
- package/dist/web/primitives/index.js +1450 -0
- package/dist/web/primitives/index.js.map +57 -0
- package/docs/components.generated.json +6238 -0
- package/docs/round-1-contract.md +679 -0
- package/docs/round-2-contract.md +476 -0
- package/docs/round-3-contract.md +174 -0
- package/docs/themes.generated.json +3503 -0
- package/fixtures/animations.json +2890 -0
- package/fixtures/bench-list.json +58 -0
- package/fixtures/bind-time.json +30664 -0
- package/fixtures/catalog-basic-map.json +1692 -0
- package/fixtures/catalog-components.json +9323 -0
- package/fixtures/catalog-extension.json +620 -0
- package/fixtures/catalog-macros.json +36370 -0
- package/fixtures/code-tokens.json +1993 -0
- package/fixtures/conformance-baseline.json +55544 -0
- package/fixtures/conformance-cases.json +49 -0
- package/fixtures/conformance-known.json +2123 -0
- package/fixtures/control-geometry.json +4200 -0
- package/fixtures/dashboard.data.json +74 -0
- package/fixtures/dashboard.json +752 -0
- package/fixtures/demo-surface.json +47 -0
- package/fixtures/format.json +870 -0
- package/fixtures/host-policy.json +1067 -0
- package/fixtures/host-router.json +1152 -0
- package/fixtures/host-transport.json +590 -0
- package/fixtures/interactions-round1.json +773 -0
- package/fixtures/kitchen-sink.expanded.json +10844 -0
- package/fixtures/kitchen-sink.json +3049 -0
- package/fixtures/layout-geometry-round1.json +7278 -0
- package/fixtures/layout-geometry.json +2072 -0
- package/fixtures/overlay-geometry.json +272 -0
- package/fixtures/primitive-specimens.json +58 -0
- package/fixtures/prompt-budget.json +33 -0
- package/fixtures/resizable.json +714 -0
- package/fixtures/round4-contract.json +2985 -0
- package/fixtures/specimens.json +9832 -0
- package/fixtures/style-conditions.json +336 -0
- package/fixtures/template-items.json +2215 -0
- package/fixtures/text-direction.json +309 -0
- package/fixtures/text-shimmer.json +96 -0
- package/fixtures/theme-extends.json +573 -0
- package/fixtures/theme-invalid.json +1296 -0
- package/fixtures/theme-recipes.json +58600 -0
- package/fixtures/theme-resolved.json +23949 -0
- package/fixtures/toast-stack.json +215 -0
- package/fixtures/tree-guides.json +224 -0
- package/fixtures/virtual-list.json +733 -0
- package/generated/ExponentialUICatalog.generated.kt +67 -0
- package/generated/ExponentialUICatalog.generated.swift +67 -0
- package/generated/ExponentialUIThemes.generated.kt +23 -0
- package/generated/ExponentialUIThemes.generated.swift +23 -0
- package/generated/catalog.descriptions.generated.rs +6 -0
- package/generated/catalog.generated.rs +80 -0
- package/generated/catalog.themes.generated.rs +34 -0
- package/generated/core.descriptions.json +2505 -0
- package/generated/core.runtime.json +3609 -0
- package/package.json +87 -4
- package/themes/exponential.recipes.json +49 -0
- package/themes/exponential.theme.json +755 -0
- package/themes/neutral.theme.json +5676 -0
- package/themes/orange.theme.json +43 -0
- package/themes/playful.theme.json +219 -0
- package/vendor/a2ui/LICENSE.txt +203 -0
- package/vendor/a2ui/README.md +27 -0
- package/vendor/a2ui/v0_9/basic_catalog.json +1387 -0
- package/vendor/a2ui/v0_9/basic_catalog_rules.txt +5 -0
- package/vendor/a2ui/v0_9/client_capabilities.json +97 -0
- package/vendor/a2ui/v0_9/client_to_server.json +104 -0
- package/vendor/a2ui/v0_9/common_types.json +315 -0
- package/vendor/a2ui/v0_9/server_to_client.json +132 -0
- package/vendor/json-render/LICENSE.txt +201 -0
- package/vendor/json-render/README.md +13 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# Exponential UI — round 2 contract
|
|
2
|
+
|
|
3
|
+
> **Round 4 (VAPP-103):** this is the round's historical record. Names it
|
|
4
|
+
> uses that round 3 folded (ToggleGroup, TabBar, ButtonGroup, DropdownMenu,
|
|
5
|
+
> ContextMenu, Sheet, HoverCard, Pill, EntityChip, Band, RowList and the
|
|
6
|
+
> ListRow family) are REMOVED from the catalog; `round-3-contract.md` §4 maps
|
|
7
|
+
> each to what replaces it.
|
|
8
|
+
|
|
9
|
+
Round 2 adds the authoring features round 1 left out, settles the open
|
|
10
|
+
contract questions in TS and Rust together, and turns the 53 gpui-vs-web
|
|
11
|
+
divergence causes into decisions. This file is the CONTRACT half: what
|
|
12
|
+
changed in `@exponential-at/ui` and what each renderer implements to match.
|
|
13
|
+
Everything here is locked by a fixture or a test (`src/round2.test.ts`).
|
|
14
|
+
|
|
15
|
+
Read §0, the sections you own, then §9 (your checklist) and §10.
|
|
16
|
+
|
|
17
|
+
## 0. What moved
|
|
18
|
+
|
|
19
|
+
| area | change | source of truth | locked by |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| Resizable | new native: panels + handles, bound `sizes`, limits, keys | `core.catalog.json`, `src/resizable.ts` | `fixtures/resizable.json` |
|
|
22
|
+
| `position: sticky` | pinned inside the nearest scroller | `catalog/style.json` | `style.test.ts`, `round2.test.ts` |
|
|
23
|
+
| `backdropBlur` | `$blur.<name>` only; new token group `blur` | `style.json`, `tokens.json`, themes | `theme-resolved.json`, `round2.test.ts` |
|
|
24
|
+
| `animation` | 8 keyframe sets, motion-token timing, reduced motion | `style.json` `animations`, `src/animation.ts` | `fixtures/animations.json` |
|
|
25
|
+
| `direction` | on ANY node, inherited; textAlign start/end per node | `style.json`, `src/direction.ts` | `fixtures/text-direction.json` |
|
|
26
|
+
| Formatter | host-supplied number/currency/percent/date/relative time; the English fallback takes the surface zone as a UTC offset | `src/format.ts`, `locale.json` `englishFallback` | `fixtures/format.json` |
|
|
27
|
+
| host API | `surface`: settings (+ `timeZone`), Formatter methods, commands (+ `scrollToIndex`); version 2 | `catalog/host.json` | `host.test.ts` |
|
|
28
|
+
| display of non-strings | a bound number in a text prop shows `412` | `src/format.ts displayString` | `format.json` `display` |
|
|
29
|
+
| built-in strings | 8 ids for the labels renderers still hard-coded | `catalog/strings.json` | `round2.test.ts` |
|
|
30
|
+
| templates | template nodes LIFTED out of the tree into `templates`; self-instantiating ones reported, kept in place | `src/reducer.ts` | `template-items.json`, `kitchen-sink.expanded.json`, `catalog-basic-map.json` |
|
|
31
|
+
| keys | missing/empty/duplicate keys → `#<index>`; suffixes accumulate, `.`/`~` escaped | `src/list.ts` | `template-items.json` |
|
|
32
|
+
| lists | one-axis window, sections + sticky headers, `scrollToIndex` | `src/list.ts`, `a11y.json` | `fixtures/virtual-list.json` |
|
|
33
|
+
| parts | every laid-out part is a recipes part, dumped as `<id>.<part>[.<i>]` | `catalog/recipes.json` | `theme.test.ts`, the harness |
|
|
34
|
+
| layout numbers | field/media intrinsic sizes, window, Resizable, TreeGuides | `catalog/layout.json` | generated constants |
|
|
35
|
+
| Video / Image / Chart / overlays | explicit sizes instead of browser defaults | `core.catalog.json`, `layout.json`, §7 | `conformance-known.json` |
|
|
36
|
+
| conformance | manifest v2 (24 suites), layout-box dump, real Nunito + Fira Code, 53 decisions | `src/conformance.ts`, `conformance/` | `manifest.json`, `compare.test.ts` |
|
|
37
|
+
| prompt | teaches sticky, backdropBlur, animation, Resizable, sections | `src/prompt.ts` | `prompt-budget.json` (6475 / 6500) |
|
|
38
|
+
|
|
39
|
+
Counts: 83 components (47 natives, 36 macros), 31 functions (17 core),
|
|
40
|
+
65 built-in strings, 16 token groups.
|
|
41
|
+
|
|
42
|
+
## 1. Resizable
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{ "id": "split", "component": "Resizable", "children": ["nav", "main"],
|
|
46
|
+
"direction": { "base": "vertical", "md": "horizontal" },
|
|
47
|
+
"sizes": { "path": "/ui/split" }, "panels": [{ "min": 20 }, { "min": 30, "collapsible": true }],
|
|
48
|
+
"handle": true, "style": { "height": 480 } }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| prop | type | meaning |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `direction` | horizontal \| vertical, responsive | panels side by side or stacked |
|
|
54
|
+
| `sizes` | `[number]`, bindable, two-way | percent per child (sum 100) |
|
|
55
|
+
| `panels` | `[panel {min?, max?, collapsible?}]` | limits by index; `min` default 10 (`layout.json` `panelMin`), max 100 |
|
|
56
|
+
| `handle` | boolean | draw a grip on each handle |
|
|
57
|
+
| `style` | style | the group box (give it a height) |
|
|
58
|
+
|
|
59
|
+
Event `change {sizes}` after every drag end or key. A bound `sizes` is
|
|
60
|
+
written back (two-way, like every bindable native).
|
|
61
|
+
|
|
62
|
+
**Numbers** (`src/resizable.ts`, all percent, rounded to 1e-6):
|
|
63
|
+
|
|
64
|
+
| function | rule |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `normalizeSizes(sizes, count, panels)` | valid given sizes kept, missing ones share the rest of 100 (equal split when none given), scaled to 100, clamped to [min, max] (a collapsible panel may stay 0), the difference handed to panels with room in index order; a `min` above 100/count drops to it |
|
|
67
|
+
| `resizePanels(sizes, handle, delta, panels)` | from the sizes at the START of the drag; only panels `handle` and `handle + 1` change; clamped by both panels' limits; a collapsible panel dragged below half its min collapses to 0; a collapsed one dragged past half its min reopens at max(min, delta), clamped like any drag |
|
|
68
|
+
| `keyboardResize(…, key, orientation, direction)` | arrows move the handle on screen by `resizeStep` (10): horizontal Left/Right (rtl: ArrowRight shrinks the first panel), vertical Up/Down; Home/End = first panel to its min/max; Enter toggles collapse of the first panel if collapsible, else the second |
|
|
69
|
+
| `panelExtents(sizes, container, handle)` | px = (container − handles × `$control.hairline`) × size / 100 |
|
|
70
|
+
| `dragDelta(px, container, count, orientation, direction)` | pointer px → percent (rtl horizontal flips) |
|
|
71
|
+
|
|
72
|
+
**Layout**: a flex row (column) of panels and handles; every handle is
|
|
73
|
+
`$control.hairline` thick in layout with an 8 px hit area
|
|
74
|
+
(`resizeHandleHit`) centred on it; panels stretch across. **Recipes**:
|
|
75
|
+
`Resizable/root|panel|handle|grip`, `when: {direction, handle}`; neutral:
|
|
76
|
+
handle `$color.border` → `$color.ring` on hover/focus/pressed, grip a 6×24
|
|
77
|
+
pill (24×6 vertical). **A11y**: role group; each handle a focusable
|
|
78
|
+
`separator` (orientation of the line, valuenow = the panel before it,
|
|
79
|
+
valuemin/max = its limits, controls = that panel), named `$string.resize`.
|
|
80
|
+
|
|
81
|
+
## 2. Style
|
|
82
|
+
|
|
83
|
+
| key | values | rule |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `position` | + `sticky` | relative in layout, then pinned by its `top`/`bottom`/`left`/`right`/`inset*` inside the nearest scrolling ancestor (overflow scroll/auto, else the surface scroller) while its parent is in view |
|
|
86
|
+
| `backdropBlur` | `$blur.sm\|md\|lg\|xl` only | blurs what is BEHIND the box through its translucent background. No platform blur → paint the background alone (gpui today), so authors pair it with a translucent `backgroundColor`. CSS: `backdrop-filter` + `-webkit-` |
|
|
87
|
+
| `animation` | pulse spin fade-in slide-in-up/down/left/right shimmer | `style.json` `animations` |
|
|
88
|
+
| `animationDuration` | `$motion.<name>` only | replaces the set's duration token (factor kept) |
|
|
89
|
+
| `direction` | ltr \| rtl, ANY node | inherited; flex order, logical insets/paddings, mirrored glyphs, textAlign start/end and the bidi paragraph direction follow the node's resolved direction |
|
|
90
|
+
|
|
91
|
+
Recipes may set `backdropBlur` and `animation` too. Tokens: `blur`
|
|
92
|
+
(neutral: 4 / 12 / 16 / 24 px; the others inherit).
|
|
93
|
+
|
|
94
|
+
**Animations** (`src/animation.ts`):
|
|
95
|
+
|
|
96
|
+
| name | keyframes | duration | easing | runs | reduced motion |
|
|
97
|
+
|---|---|---|---|---|---|
|
|
98
|
+
| pulse | opacity 1 → 0.5 → 1 | `$motion.slow` × 7 | `$ease.standard` | ∞ | opacity 1 |
|
|
99
|
+
| spin | rotate 0 → 360° | `$motion.slow` × 4 | linear | ∞ | 0° |
|
|
100
|
+
| fade-in | opacity 0 → 1 | `$motion.standard` | `$ease.decelerate` | 1 | end state |
|
|
101
|
+
| slide-in-up/down/left/right | opacity 0 → 1, translate 8 px → 0 (up = from below, left = from the right) | `$motion.standard` | `$ease.decelerate` | 1 | end state |
|
|
102
|
+
| shimmer | a band (background colour, alpha 0 → .5 → 0) sweeps −1 → 1 box widths, clipped to the box | `$motion.slow` × 5 | linear | ∞ | no band |
|
|
103
|
+
|
|
104
|
+
Channels compose OUTSIDE the node's own `transform`, about the box centre;
|
|
105
|
+
opacity MULTIPLIES the node's own (`paintedOpacity`: own 0.5 + pulse →
|
|
106
|
+
0.5 … 0.25; a finished fade-in keeps the author's 0.6). Channels are
|
|
107
|
+
physical: `direction` never mirrors them (rtl too: `slide-in-left` moves
|
|
108
|
+
left, the shimmer sweeps left → right). Easing applies per keyframe segment
|
|
109
|
+
(CSS semantics). An animation starts when the node enters the tree (first
|
|
110
|
+
paint, `visible` turning true, a template item appearing).
|
|
111
|
+
|
|
112
|
+
CSS: `@keyframes xui-<name>` (`keyframesCss`) + `animationCss(name,
|
|
113
|
+
animationTiming(name, theme))`, using the individual `translate`/`rotate`/
|
|
114
|
+
`scale` properties. Opacity keyframes move `--xui-a-opacity`, and
|
|
115
|
+
`styleToCss` writes `opacity: calc(<own> * var(--xui-a-opacity, 1))`;
|
|
116
|
+
register both custom properties once per document with
|
|
117
|
+
`ANIMATION_PROPERTIES_CSS` (unregistered, they jump instead of
|
|
118
|
+
interpolating).
|
|
119
|
+
|
|
120
|
+
**Text alignment** (`src/direction.ts`): `nodeTextAlign` = style
|
|
121
|
+
`textAlign`, else a Text's `align`, else `start`; start/end resolve against
|
|
122
|
+
the NODE's direction (`physicalTextAlign`). The Rust core's `Visual` carries
|
|
123
|
+
the resolved physical align; gpui and the natives shape text with the
|
|
124
|
+
node's direction as the bidi paragraph direction.
|
|
125
|
+
|
|
126
|
+
## 3. Formatting
|
|
127
|
+
|
|
128
|
+
ONE `Formatter` per surface, from `SurfaceSettings.locale` and the new
|
|
129
|
+
`timeZone` (IANA; default the platform's, UTC in fixtures). The zone is the
|
|
130
|
+
HOST's: it builds the formatter (or, for the Rust fallback, the UTC offset)
|
|
131
|
+
in it. The Rust core has no zone database and no zone setting: the zone
|
|
132
|
+
reaches it only through the formatter (FFI: `HostFormatter`, or `HostZone`
|
|
133
|
+
for the fallback; `FfiSettings.timeZone` is kept for the host):
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
interface Formatter {
|
|
137
|
+
readonly locale: string
|
|
138
|
+
number(value: number, o?: { decimals?: number; grouping?: boolean }): string // 0..3 digits by default
|
|
139
|
+
currency(value: number, code: string, o?: { decimals?: number; grouping?: boolean }): string // the code's minor digits
|
|
140
|
+
percent(value: number, o?: { decimals?: number }): string // 0.256 → 26%
|
|
141
|
+
date(value: unknown, o?: { format?: string; style?: `short`|`medium`|`long`|`full`; time?: boolean }): string
|
|
142
|
+
relativeTime(value: unknown, now: number): string // yesterday, in 2 hours
|
|
143
|
+
plural(value: number): `zero`|`one`|`two`|`few`|`many`|`other`
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| platform | formatter |
|
|
148
|
+
|---|---|
|
|
149
|
+
| TS (React) | `intlFormatter(locale, timeZone)` (Intl) |
|
|
150
|
+
| Rust core | a `Formatter` trait; default `EnglishFormatter` over `LOCALE_JSON` `englishFallback`, at a host-supplied UTC offset per instant (gpui: the local zone) |
|
|
151
|
+
| FFI (Swift, Kotlin) | a foreign `Formatter` callback interface the host passes in `SurfaceSettings` (Foundation / java.text·android.icu), else the core's English one |
|
|
152
|
+
|
|
153
|
+
Rules: date values are `yyyy-mm-dd` (a calendar day, no zone), an ISO
|
|
154
|
+
date-time (no offset = UTC) or epoch ms; anything else formats to ``.
|
|
155
|
+
Instants show in the surface zone; a calendar day never shifts. The English
|
|
156
|
+
fallback has no zone database: it takes `ZoneOffset = (utcMs) => minutes`
|
|
157
|
+
(TS `englishFormatter(zone)`, `fixedOffset`, `intlZoneOffset`; Rust: the
|
|
158
|
+
host passes the platform's offset). Its patterns, joiners (`, ` / ` at `)
|
|
159
|
+
and the unknown-currency template are data (`englishFallback`
|
|
160
|
+
`datePatterns`, `timePattern`, `dateTimeJoin`, `unknownCurrency`): read
|
|
161
|
+
them, never copy them. A numeric string is `[ws][+-](digits[.digits]|.digits)
|
|
162
|
+
[e[+-]digits][ws]`; hex, `Infinity` and separators are not numbers.
|
|
163
|
+
`format` = a TR35 subset (`y yy yyyy M MM MMM MMMM d dd E EEE EEEE h hh H HH
|
|
164
|
+
m mm s ss a`, quoted literals, `''`), ASCII digits, locale names; `style` =
|
|
165
|
+
the locale preset (`time: true` appends the short time). Relative time
|
|
166
|
+
picks the unit by |Δ| (< 1 min seconds, < 1 h minutes, < 1 day hours, < 7
|
|
167
|
+
days days, < 30 days weeks, < 365 days months of 30 days, else years),
|
|
168
|
+
truncated toward zero, numeric:auto phrases (now, yesterday, last week).
|
|
169
|
+
Rounding = half away from zero on the shortest round-trip decimal.
|
|
170
|
+
|
|
171
|
+
**Bind table**: `bindFunctionNames` now includes `formatNumber`,
|
|
172
|
+
`formatCurrency`, `formatDate`, `pluralize` (A2UI basic) and the core
|
|
173
|
+
`formatPercent{value, decimals}`, `formatRelativeTime{value, now?}`; all run
|
|
174
|
+
through the surface Formatter (`ResolveOptions.formatter`, `now`). A node
|
|
175
|
+
using `formatRelativeTime` without `now` re-binds at least once a minute.
|
|
176
|
+
`pluralize`: value 0 with a `zero` arm → it, else the CLDR category, else
|
|
177
|
+
`other`.
|
|
178
|
+
|
|
179
|
+
**Where renderers format**: Table `number`/`currency`/`percent`/`date`/
|
|
180
|
+
`relativeTime` cells (`column.currency`, `column.decimals`), NumberField
|
|
181
|
+
display, chart tick labels and value labels, DatePicker/DateRangePicker/
|
|
182
|
+
TimePicker trigger text, calendar month and weekday names (`date(…, {format:
|
|
183
|
+
"MMMM yyyy"})`, `"EEE"`), the format functions.
|
|
184
|
+
|
|
185
|
+
**Display of non-strings** (`displayString`): a `string`/`markdown` prop
|
|
186
|
+
bound to a number shows ECMAScript `Number.prototype.toString` (`412`,
|
|
187
|
+
`1.5`, `0.30000000000000004`, `1e+21`, `-0` → `0`), booleans `true`/`false`,
|
|
188
|
+
nothing for null/objects/arrays/NaN. No locale (use `formatNumber`).
|
|
189
|
+
|
|
190
|
+
**Strings**: `invalidValue` (a failed check without a message), `message`
|
|
191
|
+
(the Composer field's name), `codeBlock` (an untitled CodeBlock region),
|
|
192
|
+
`dialog` (an untitled Dialog/Drawer), `table` (an uncaptioned Table),
|
|
193
|
+
`carousel` / `slide` (role descriptions), `resize` (a Resizable handle).
|
|
194
|
+
Replace every hard-coded English label with them: React `natives/shared.tsx`
|
|
195
|
+
`Invalid value`, `inputs.tsx` Composer `aria-label`, `code.tsx` region
|
|
196
|
+
label, `overlays.tsx` title-less `DialogPrimitive.Title {kind}`, media
|
|
197
|
+
`aria-roledescription`; gpui `paint.rs` Table/FileUpload component-name
|
|
198
|
+
labels, `paint/date.rs MONTH_NAMES` (→ Formatter); Compose
|
|
199
|
+
`Semantics.kt`/`NodeInfo.kt` `Loading` (`$string.loading`),
|
|
200
|
+
`SurfaceMeasurer.kt`/`TextFields.kt` `Message`, `MediaLeaves.kt` `Page N`
|
|
201
|
+
(`pageOf`); SwiftUI `NodeInfo.swift`/`NodeView.swift` `Loading`,
|
|
202
|
+
`ControlLeaves.swift`/`SurfaceMeasurer.swift` `Message`, `MediaLeaves.swift`
|
|
203
|
+
`Page N`.
|
|
204
|
+
|
|
205
|
+
**Conformance** replays `format.json` through the English fallback
|
|
206
|
+
(exact). A platform Formatter (Foundation, android.icu) may run too, after
|
|
207
|
+
mapping U+202F and U+00A0 to a space on both sides: newer ICU puts U+202F
|
|
208
|
+
before AM/PM. `zoned` cases carry `timeZone` (ICU) and `offsetMinutes`
|
|
209
|
+
(fallback).
|
|
210
|
+
|
|
211
|
+
## 4. Templates, keys, rows
|
|
212
|
+
|
|
213
|
+
**Lifted templates.** A node a `template.component` names is the TEMPLATE:
|
|
214
|
+
the reducer lifts it out of every children list and slot (any depth, root
|
|
215
|
+
excepted) into `ReduceResult.templates[id]`, validated and expanded like the
|
|
216
|
+
root, in discovery order; it never also renders in place. The flat path
|
|
217
|
+
builds referenced ids eagerly; an id nobody defines is an issue
|
|
218
|
+
(`template: no component with this id`). Nested templates are lifted too.
|
|
219
|
+
|
|
220
|
+
**Cycles.** A template that would instantiate itself is an issue
|
|
221
|
+
(`template: cycle through this id`) and is NOT lifted: it stays where it was
|
|
222
|
+
and its owner renders no items. That covers a template naming its own owner,
|
|
223
|
+
an ancestor of its owner, the root (issue on the root id), and loops through
|
|
224
|
+
other templates (each one on the loop is reported).
|
|
225
|
+
`kitchen-sink.expanded.json` gains `templates: {"list-item": …}` and loses
|
|
226
|
+
the in-place `list-item`.
|
|
227
|
+
|
|
228
|
+
**Keys** (`templateItemKeys`): the value at `template.key` (a pointer
|
|
229
|
+
relative to the item; objects as JSON) — `#<index>` when missing, null, ``
|
|
230
|
+
or a DUPLICATE of an earlier key, more `#`s until unique; no key = the
|
|
231
|
+
index. Table rows: `tableRowKeys(rows, rowKey = "id")`, same rules.
|
|
232
|
+
|
|
233
|
+
**Instance ids**: every node of an item is painted as `<node id><suffix>`,
|
|
234
|
+
suffix = the enclosing item's suffix + `.<key>`; it ACCUMULATES through
|
|
235
|
+
nested templates (`issue.ops.1`). The key is escaped first
|
|
236
|
+
(`instanceSegment`: `~` → `~0`, `.` → `~1`), so `a.b` + `c` (`.a~1b.c`)
|
|
237
|
+
and `a` + `b.c` (`.a.b~1c`) differ. The Rust `item_suffix` appends, never
|
|
238
|
+
replaces. Table part ids keep the raw row key.
|
|
239
|
+
|
|
240
|
+
**List sections**: `sectionBy` (a field) groups CONSECUTIVE template items
|
|
241
|
+
(data order kept); slot `section` is row-scoped (List `slotScope: row`) and
|
|
242
|
+
binds once per section with the literal scope `{value, count, index}`
|
|
243
|
+
(`sectionScope` / `bindSectionHeader`); `stickyHeaders` pins the current one.
|
|
244
|
+
A header is a heading (level 3) in the a11y tree.
|
|
245
|
+
|
|
246
|
+
**Rust, same pass**: layout reads props through `resolve_node_props` (a
|
|
247
|
+
literal Table `rows` holding `{path}` is DATA, `bind-time.json`
|
|
248
|
+
`Table/slot-cell:literal-rows`); `reconcile.rs` and gpui events/paint show
|
|
249
|
+
non-string text values with `display_string`, never `as_str`; the FFI
|
|
250
|
+
exposes `bindRowSlotJson(slot, rowsProp, rows, index, data, options) →
|
|
251
|
+
node | null` and `bindSectionHeaderJson(slot, section, index, data,
|
|
252
|
+
options)`.
|
|
253
|
+
|
|
254
|
+
## 5. Virtual lists
|
|
255
|
+
|
|
256
|
+
One axis, vertical or horizontal (`direction`). Offsets start at the list's
|
|
257
|
+
content start (the inline START of a horizontal list: its right edge in
|
|
258
|
+
rtl, whatever sign the platform's scroll position uses); item i starts at Σ(extentⱼ + gap) for j < i, where a
|
|
259
|
+
`divided` list adds `$control.hairline` to the gap (the divider centred in
|
|
260
|
+
it). Unmeasured items take the MEAN extent of the measured items once any
|
|
261
|
+
is measured (the content length stays stable while rows measure in), else
|
|
262
|
+
`$control.row` (`itemExtents(measured, row)`; `windows` cases with
|
|
263
|
+
`measured` + `row`).
|
|
264
|
+
|
|
265
|
+
| function | rule |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `virtualWindow(extents, gap, scroll, viewport, overscan = 5, offsets?)` | every item intersecting [scroll, scroll + viewport) (an item ending exactly at `scroll` counts) ± `windowOverscan`; `before`/`after` spacers. Pass cached `itemOffsets` (rebuilt only when an extent changes): two binary searches per frame |
|
|
268
|
+
| `scrollOffsetForIndex(…, index, viewport, scroll, align, inset, offsets?)` | start / center / end / nearest (default: the smallest move; none when visible); `inset` = the pinned header's extent; clamped to the content |
|
|
269
|
+
| `scrollOffsetForItem(rows, rowExtents, gap, dataIndex, …, stickyHeaders)` | a sectioned list: the DATA index → its flat row (absent = no move), `inset` = its own section header's extent when sticky |
|
|
270
|
+
| `listSections` + `sectionRows` | the flat row list: a header before each section |
|
|
271
|
+
| `stickyHeader(rowOffsets, rowExtents, headerRows, scroll)` | the last header at or before `scroll` pins at min(scroll, nextHeaderStart − extent); rendered even outside the window |
|
|
272
|
+
|
|
273
|
+
Windowing starts past `windowThreshold` (50). A list without a bounded size
|
|
274
|
+
windows against its nearest scrolling ancestor (overflow auto/scroll AND
|
|
275
|
+
taller content) or the host viewport. `scrollToIndex {id, index, align?}`
|
|
276
|
+
(a host command, `a11y.json`) takes the DATA index (before a local sort),
|
|
277
|
+
renders the item first, subtracts the pinned header. Items keep their
|
|
278
|
+
position in the whole list for assistive tech (setsize/posinset).
|
|
279
|
+
|
|
280
|
+
**Bench** (`fixtures/bench-list.json`): 100,000 `ListRow`s on a 390×800
|
|
281
|
+
surface, neutral theme; report `firstPaintMs`, `scrollStepMs` (100 viewport
|
|
282
|
+
steps), `scrollToIndexMs` (index 50000, start), `renderedItems` in each
|
|
283
|
+
renderer's README with the machine.
|
|
284
|
+
|
|
285
|
+
## 6. Parts (conformance coverage)
|
|
286
|
+
|
|
287
|
+
`catalog/recipes.json` `native.<C>.parts` lists EVERY part a painter lays
|
|
288
|
+
out. New: Switch/body, Radio/items, Slider/header, Table/body,
|
|
289
|
+
CodeBlock/body + code, FileUpload/title + browse + fileIcon, NumberField/input,
|
|
290
|
+
ChipInput/input, Tooltip/anchor, Accordion/count, List/section, Resizable/*.
|
|
291
|
+
|
|
292
|
+
| rule | |
|
|
293
|
+
|---|---|
|
|
294
|
+
| id | `<node id>.<part>` (`code.header`) |
|
|
295
|
+
| per item | `.<index>` (Radio `item.0`, Tabs `tab.1`, Accordion `trigger.1`, FileUpload `file.0`, ChipInput `chip.0`, CodeBlock `line.3`, List `divider.1` = before item 1, Table `headerCell.2`) |
|
|
296
|
+
| Table rows | `.<row key>` (`row.alice`, `checkbox.alice`, `cell.alice.0`, `checkbox.header`) |
|
|
297
|
+
| web | `data-xui-id` + `data-xui-part` on the part's element |
|
|
298
|
+
| paint parts (never dumped) | Switch/thumb, Slider/thumb + range, Ring track/fill, focus rings, arrows, decorations |
|
|
299
|
+
|
|
300
|
+
## 7. Explicit sizes (no browser defaults)
|
|
301
|
+
|
|
302
|
+
`catalog/layout.json` (generated as `layoutConstantNames`/`Values`,
|
|
303
|
+
`LAYOUT_JSON`): `windowThreshold` 50, `windowOverscan` 5, `resizeStep` 10,
|
|
304
|
+
`panelMin` 10, `resizeHandleHit` 8, `fieldIntrinsicWidth` 160,
|
|
305
|
+
`mediaIntrinsicWidth` 320, `mediaAspectRatio` 1.7777778,
|
|
306
|
+
`treeGuideColumn` 16.
|
|
307
|
+
|
|
308
|
+
| component | contract |
|
|
309
|
+
|---|---|
|
|
310
|
+
| overlays (Dialog, Drawer, Sheet, AlertDialog, Popover, HoverCard, Tooltip, DropdownMenu with a trigger slot) | LAYOUT-TRANSPARENT: the frame is the trigger's; the trigger is the flex item (a Link stretches in a stretch row, a Button keeps its control height); nothing without a trigger |
|
|
311
|
+
| DropdownMenu default trigger | outline button: `icon` (when set) + `label`, no chevron |
|
|
312
|
+
| Video | `aspectRatio` prop (default 16:9): height = width / ratio; max-content width 320, min-content 0 |
|
|
313
|
+
| Image | `aspectRatio`, else 16:9 without a height; the box never waits for the picture; `Image/fallback` fills it, glyph `$control.iconLg` centred |
|
|
314
|
+
| Chart | `height` = the WHOLE box (title, legend, axes inside); min-content width 0 (shrinks in a row) |
|
|
315
|
+
| AudioPlayer | title line (`AudioPlayer/track`) + `$spacing.xs` + controls row (`AudioPlayer/controls` = `$control.row`) |
|
|
316
|
+
| text fields (Input, Textarea, NumberField, ChipInput) | max-content width 160 |
|
|
317
|
+
| picker triggers (Select, DatePicker, DateRangePicker, TimePicker) | content-sized (text + glyph + padding + border), no 160 floor |
|
|
318
|
+
| Badge, Pill | 2 × paddingHorizontal + content; an absent `$if` part adds no gap |
|
|
319
|
+
| ToggleGroup | content-sized (align-self start) unless `fill` |
|
|
320
|
+
| TreeGuides | depth × 16 wide, stretched to its row; `TreeGuides/line` `width` = the stroke |
|
|
321
|
+
| Slider | header + `$spacing.xs` + a track row as tall as the thumb; the 6 px rail centred |
|
|
322
|
+
| Radio | `Radio/root` gap; an option row = max(dot, label line) |
|
|
323
|
+
| Table | header (cell + hairline) + rows (cell + hairline) + caption (line + 2 × paddingVertical) |
|
|
324
|
+
| CodeBlock | border + header (recipe padding + copy button + hairline) + lines × lineHeight; padding only from recipes. The header's `title` part = `title`, else the `language` id (`ts`; none for `plain`), so a header never stands empty |
|
|
325
|
+
| FileUpload | the drop zone holds icon, title, hint and the Browse button (`$control.buttonSm`, outline) |
|
|
326
|
+
| Text, Markdown | an empty text = 0 lines; a paragraph = its runs, no extra line |
|
|
327
|
+
| Switch | `[label + description][switch]`: label first, the track at the end; the row toggles |
|
|
328
|
+
| Checkbox, Radio | control first, then the label column; never overlapping |
|
|
329
|
+
| Accordion | `count` = its own muted part after the title |
|
|
330
|
+
| Carousel | only the active page is placed; the others are aria-hidden + inert, may stay mounted, carry `data-xui-inactive`. 2+ pages: `Carousel/root` gap + a centred `controls` row = `previous` + `indicator` (the dots) + `next`, spaced by `Carousel/controls` gap; the buttons' size from their recipes (28 × 28), no CSS literals |
|
|
331
|
+
| text in fields | starts at border + `paddingHorizontal`; no painter padding |
|
|
332
|
+
|
|
333
|
+
### Painter asks (VAPP-100)
|
|
334
|
+
|
|
335
|
+
What the core hands every painter so none re-derives it:
|
|
336
|
+
|
|
337
|
+
| item | contract |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `FfiTextStyle.letterSpacing`, `textTransform`, `fontStyle` | resolved like the other text fields (letter spacing × font scale); measure and paint with them |
|
|
340
|
+
| `FfiLeaf.ownerComponent` | the native or MACRO owning a part leaf (`Tabs`, `Stepper`); null for a plain node |
|
|
341
|
+
| `FfiLeaf.text` | a number or boolean in the text prop = its display string (§3) |
|
|
342
|
+
| `Surface.effectiveTheme()` | the `Theme` the core resolves against: extends, density and contrast applied |
|
|
343
|
+
| `setHovered(ids)` / `setHover(id, on)`, `FfiNode.hovered` + `interactionStates` | the host's pointer hover; recipes' `hover` and `:hover` resolve through it |
|
|
344
|
+
| `FfiNode.hoverStyled` (Rust `Surface::hover_styled`) | the node restyles under the pointer: a `:hover` style block, or a recipe rule on `state: hover` its props match, in the effective theme. A painter tracks a mouse over these nodes (and pressables, triggers, fields) and reports the hover; re-read after a theme switch |
|
|
345
|
+
| `markDirty(index)` after a load | a Markdown is measured again; an Image box keeps its ratio's height |
|
|
346
|
+
| Stepper | `number` and `label` carry the step `status` (`$recipe`): the current number takes the marker's ink (`primaryForeground`), an upcoming one and an upcoming label are muted |
|
|
347
|
+
|
|
348
|
+
## 8. Conformance
|
|
349
|
+
|
|
350
|
+
- **Manifest v2** (`CONFORMANCE_VERSION` 2, 24 suites, 1642 cases): new
|
|
351
|
+
suites `bind`, `style-conditions`, `code-tokens` (round 1's, now counted)
|
|
352
|
+
and `format`, `template-items`, `text-direction`, `resizable`,
|
|
353
|
+
`virtual-list`, `animations`. Runners read the version from the manifest.
|
|
354
|
+
- **Dump** (`conformance/dom.ts`): frames are LAYOUT boxes (`LAYOUT_ONLY_CSS`
|
|
355
|
+
turns transforms and animations off while measuring); `[data-xui-inactive]`
|
|
356
|
+
subtrees are skipped.
|
|
357
|
+
- **Fonts**: Nunito (7 static instances of Google Fonts' variable font) and
|
|
358
|
+
Fira Code 6.2 (4 weights) under `conformance/fonts/` with their OFL
|
|
359
|
+
texts; `fonts.json` maps them; not shipped in the npm package. Licence row:
|
|
360
|
+
see the report.
|
|
361
|
+
- **Kitchen sink**: a `panels` Resizable is the last section (the kitchen
|
|
362
|
+
sink uses every visible component).
|
|
363
|
+
- **Decisions**: `fixtures/conformance-known.json` `causes` (53 origins of
|
|
364
|
+
the 2026-10-08 run) + `rules` (decisions with no origin today). The gpui
|
|
365
|
+
ratchet reads only `cases`; its writer must keep `causes` and `rules`.
|
|
366
|
+
|
|
367
|
+
| group | nodes | right | fix |
|
|
368
|
+
|---|---|---|---|
|
|
369
|
+
| overlay trigger rows | resp-drawer, menu, hover-card(-trigger), alert-dialog, dialog, drawer, sheet, popover, tooltip | layout-transparent (§7) | React (wrappers fill/stretch, menu chevron); gpui (Link trigger stretch) |
|
|
370
|
+
| media sizes | audio, page-1, video | §7 | React (audio row, video ratio); gpui (Image ratio, video ratio) |
|
|
371
|
+
| chart heights | chart, chart-line, chart-donut | gpui (`height` = whole box) | React |
|
|
372
|
+
| control heights | code, table, nf-files | gpui | React |
|
|
373
|
+
| control heights | form-plan, form-volume | web | gpui |
|
|
374
|
+
| picker row widths | form-due, form-sub | web | gpui |
|
|
375
|
+
| badge and pill widths | hdr-badge (+ hdr-title/sub), nav-inbox.count (+ label), resp-nav-inbox.count (+ label), pill-1 (+ pill-2..4, footer-spacer) | web | gpui |
|
|
376
|
+
| empty and number text | list-item.meta.*, list-item.body.title(.*) | web | core + gpui (`display_string`, 0-line empty text); contract (lifted template) |
|
|
377
|
+
| sparkline shrink | sparkline (+ spark-label, progress-bound.*) | web | gpui (Chart min-content 0) |
|
|
378
|
+
| toggle group stretch | toggles | web | gpui |
|
|
379
|
+
| tree guides | tree-guides | gpui | React |
|
|
380
|
+
| card row | card-row.body.title/subtitle | web | gpui (a sibling part is 30 px wider) |
|
|
381
|
+
| markdown lines | main-md (+ nav-card) | web | gpui |
|
|
382
|
+
| transforms | tab-bar.item.1.icon | gpui (paint-only) | the dump (done) |
|
|
383
|
+
|
|
384
|
+
Rules without an origin today: Switch order (React), Checkbox overlap
|
|
385
|
+
(React), field text inset 14 vs 23 (gpui), Accordion count (gpui + React),
|
|
386
|
+
image fallbacks (both), carousel pages (React marks, dump skips), rtl bidi
|
|
387
|
+
of trailing punctuation (gpui), resp-states line breaks (survivor: UAX #14
|
|
388
|
+
both sides, inside the ±1 line tolerance), part coverage (React).
|
|
389
|
+
|
|
390
|
+
**Ratchet procedure** once React and gpui land: `bun run --filter
|
|
391
|
+
@exponential-at/ui conformance -- --write-baseline` (intended web changes:
|
|
392
|
+
lifted template, Resizable, layout-box dump, Nunito/Fira Code, the React
|
|
393
|
+
fixes), then `EXP_UI_WRITE_FIXTURES=1 cargo test -p exponential-ui-gpui
|
|
394
|
+
--test conformance` (keeps `causes`/`rules`), target zero origins; a
|
|
395
|
+
survivor gets `fix: ["survivor"]` + `reason`.
|
|
396
|
+
|
|
397
|
+
**Shots**: `specimens.json` now gives the round-1 containers real content
|
|
398
|
+
(Sidebar nav, AppBar action, AlertDialog/HoverCard triggers, ContextMenu
|
|
399
|
+
target, Form fields, ScrollArea rows, Resizable panels). View-catalog ids
|
|
400
|
+
to add (web + android here, ios + desktop on the Mac): `exponential-ui-`
|
|
401
|
+
`scroll-area sidebar app-bar code-block kbd label breadcrumb tab-bar
|
|
402
|
+
stepper alert-dialog hover-card context-menu toast sparkline form
|
|
403
|
+
number-field rating chip-input date-range-picker time-picker file-upload
|
|
404
|
+
resizable`.
|
|
405
|
+
|
|
406
|
+
## 9. Per-renderer checklists
|
|
407
|
+
|
|
408
|
+
### React (`@exponential-at/ui-react`)
|
|
409
|
+
1. Resizable native (flex + hairline handles, pointer capture from the drag START sizes, `keyboardResize`, `normalizeSizes`, two-way `sizes`, separator a11y, recipes incl. grip).
|
|
410
|
+
2. `position: sticky` (CSS), `backdropBlur` (`styleToCss` already maps it), `animation` (`ANIMATION_PROPERTIES_CSS` + `keyframesCss` once per document, replacing the own `--xui-band` rule; `styleToCss(…, theme)` multiplies opacity; drop it under reduced motion), `direction` on inner boxes (the CSS cascade does it; mirror glyphs by the node's direction).
|
|
411
|
+
3. Formatter: build `intlFormatter(locale, timeZone)` per surface, pass it to the bind pass (`formatter`), use it for Table cells (new `currency`/`percent`/`relativeTime` types), NumberField, chart ticks, pickers, calendar names.
|
|
412
|
+
4. Strings: replace every hard-coded label with the eight ids of §3.
|
|
413
|
+
5. Templates: read `templates` from the reduce result (drop `findNode` in-place lookup); keys already `#<index>`.
|
|
414
|
+
6. List: `sectionBy` + `section` slot (`bindSectionHeader`), `stickyHeaders`, horizontal windowing with cached offsets, gap/divider in the offsets, `scrollToIndex` command (`scrollOffsetForItem` when sectioned), setsize/posinset; bench → README.
|
|
415
|
+
7. Parts: `data-xui-id="<id>.<part>[.<i>]"` + `data-xui-part` on every part of §6; `data-xui-inactive` on inactive carousel pages.
|
|
416
|
+
8. §7 fixes on the web side: overlays layout-transparent (wrapper `display: contents` or trigger-sized), DropdownMenu without chevron, Chart frame = `height`, AudioPlayer controls row, Video `aspect-ratio`, Table/CodeBlock spacing from recipes only, FileUpload Browse button, TreeGuides columns, Switch label first, Checkbox no overlap.
|
|
417
|
+
9. Regenerate the web baseline after 1–8 (§8).
|
|
418
|
+
|
|
419
|
+
### Rust core (`exponential-ui`) + FFI
|
|
420
|
+
1. Load the new catalog: Resizable, List `slotScope: row` + `section` slot, `panel` def, cellType, Video `aspectRatio`, 31 functions; `generated_drift.rs` counts (83 components); `layout.json` (`LAYOUT_JSON`).
|
|
421
|
+
2. `theme.rs`: token group `blur` (field, `NUMBER_GROUPS`, completeness, overlay); recipe keys `backdropBlur` (token-only `blur`) and `animation` (enum); replay `theme-resolved.json`.
|
|
422
|
+
3. `style_check.rs`: `direction` no longer root-only (spec-driven), `sticky`, the two token keys; `animations` table (`style.json`) → `animation.rs` port (timing, `cubic_bezier`, frames) replaying `animations.json` within 1e-3.
|
|
423
|
+
4. `format.rs`: the `Formatter` trait + `EnglishFormatter` from `englishFallback` (patterns, joiners, unknown currency READ from it; a UTC-offset-per-instant zone; the numeric-string grammar); replay `format.json` `calls` + `zoned`; the six format functions in the bind table through the surface formatter (the host's, in its zone; `ZonedEnglishFormatter` at the host's offset); `display_string`; replay `format.json`.
|
|
424
|
+
5. `reducer.rs`: lift templates into `templates` (nested + flat, nested templates, the missing-id issue, cycles kept in place); replay `template-items.json` `reduce`, `catalog-basic-map.json`, `kitchen-sink.expanded.json`.
|
|
425
|
+
6. `list.rs`: keys (`#<index>`), `table_row_keys`, `item_extents` (unmeasured = the measured mean, else `$control.row`; `ListWindow` the same), `item_suffix` accumulation with the `~0`/`~1` escape, `virtual_window` + `scroll_offset_for_index` over cached offsets (binary search), `scroll_offset_for_item`, sections + sticky; replay `template-items.json`, `virtual-list.json`.
|
|
426
|
+
7. `resizable.rs`: the five functions (reopen at max(min, delta)); replay `resizable.json` within 1e-6.
|
|
427
|
+
`animation.rs`: `keyframes_css` writes `--xui-a-opacity`, `ANIMATION_PROPERTIES_CSS`, `painted_opacity`; replay `animations.json` `css`, `properties`, `opacity`.
|
|
428
|
+
8. `direction`: per-node direction + physical align into `Visual`; replay `text-direction.json`.
|
|
429
|
+
9. `data.rs`/`reconcile.rs`: layout via `resolve_node_props`; `display_string` instead of `as_str` for text values.
|
|
430
|
+
10. `host/contract.rs`: host.json version 2 + the `surface` section.
|
|
431
|
+
11. FFI: `bindRowSlotJson`, `bindSectionHeaderJson`, `scrollToIndex` command, a foreign `Formatter` + `FfiSettings.timeZone` (the host's; the core's fallback gets the host's UTC offset through `HostZone`); regenerate bindings (`generate-bindings.sh`); bench 100k (`bench.rs`) → README.
|
|
432
|
+
|
|
433
|
+
### gpui (`exponential-ui-gpui`)
|
|
434
|
+
1. Resizable (drag, keys, AccessKit separator), sticky (paint offset inside the scroller), backdropBlur (fallback: background only, documented), animations from the core frames (reduced motion = rest).
|
|
435
|
+
2. §7 gpui-side fixes: Image/Video ratio, Chart min-content 0, picker triggers without the 160 floor, Radio gap, Slider row height, Badge/Pill no gap for absent parts, ToggleGroup content-sized, Link trigger stretch, Markdown paragraph lines, empty text = 0 lines, number text via `display_string`, Accordion count part, field text inset, bidi paragraph direction = node direction, the CardRow part.
|
|
436
|
+
3. Formatter: core `EnglishFormatter` at the local UTC offset (or a host one) for Table cells, NumberField, chart ticks, `paint/date.rs` month names; strings for the Table/FileUpload labels; painted opacity = own × frame.
|
|
437
|
+
4. List: horizontal windowing, sections + sticky headers, `scrollToIndex`, unbounded-height windows against the scroller; bench headless → README.
|
|
438
|
+
5. Ratchet: keep `causes`/`rules` when writing `conformance-known.json`; lower the counts.
|
|
439
|
+
|
|
440
|
+
### SwiftUI (Mac run)
|
|
441
|
+
1. Resizable (`DragGesture` from the start sizes, `accessibilityAdjustableAction` on handles), sticky (`pinnedViews` / offset), backdropBlur (`.background(.ultraThinMaterial)` by radius: sm/md thin, lg/xl regular; else background only), animations (`TimelineView` with the core frames; `accessibilityReduceMotion` = rest).
|
|
442
|
+
2. Formatter: a Foundation formatter over the FFI callback interface (`NumberFormatter`, `Date.FormatStyle`, `RelativeDateTimeFormatter` with the core's unit choice), locale + timeZone from settings.
|
|
443
|
+
3. Templates from `templates`; keys/suffixes from the core; List sections (`Section` headers, pinned), `ScrollViewReader.scrollTo` for `scrollToIndex`.
|
|
444
|
+
4. Strings (`Loading`, `Message`, `Page N`, §3), parts, §7 sizes, Switch label first; captures for the 22 specimen ids (§8).
|
|
445
|
+
|
|
446
|
+
### Compose
|
|
447
|
+
1. Resizable (`draggable` from the start sizes, semantics `setProgress`), sticky (`stickyHeader` in LazyColumn, offset elsewhere), backdropBlur (API 31+ `RenderEffect` on a background copy, else background only), animations (`rememberInfiniteTransition` from the core frames; animator scale 0 = rest).
|
|
448
|
+
2. Formatter over the FFI callback (`android.icu.text.NumberFormat`, `DateFormat`, `RelativeDateTimeFormatter`), locale + timeZone from settings.
|
|
449
|
+
3. Templates from `templates`; LazyColumn/LazyRow keys = the core keys; `stickyHeader` sections; `LazyListState.scrollToItem`.
|
|
450
|
+
4. Strings (`Loading`, `Message`, `Page N`), parts, §7 sizes, Switch label first; JVM bench → README; web + android captures for the 22 specimen ids.
|
|
451
|
+
|
|
452
|
+
## 10. Breaking changes
|
|
453
|
+
|
|
454
|
+
- `direction` is valid on any node (was root-only).
|
|
455
|
+
- `ReduceResult.templates`; a template node listed as a child no longer renders in place; the flat path validates template components.
|
|
456
|
+
- `bindFunctionNames` + 6 format functions; `functions.names` + `formatPercent`, `formatRelativeTime`; `functions.core` 17.
|
|
457
|
+
- List has a row-scoped `section` slot (`rowSlotComponents` = Table, List).
|
|
458
|
+
- `cellType` + `currency`, `percent`, `relativeTime`; `column` + `currency`, `decimals`.
|
|
459
|
+
- Chart `height` = the whole box (round 1 painters differed).
|
|
460
|
+
- Image without `aspectRatio` or a height is 16:9 (round 1 had no rule; the web took the picture's own ratio once loaded): a portrait or square image given only a width is now cropped or letterboxed (`fit`). Set `aspectRatio`.
|
|
461
|
+
- `keyframesCss` animates `--xui-a-opacity`, not `opacity`; register `ANIMATION_PROPERTIES_CSS`.
|
|
462
|
+
- Instance suffixes escape `~` and `.` in keys.
|
|
463
|
+
- A self-instantiating template is no longer lifted (issue + kept in place).
|
|
464
|
+
- Format functions read numeric strings by the §3 grammar (`0x10`, `Infinity` → ``).
|
|
465
|
+
- `catalog/host.json` version 2 (`surface`).
|
|
466
|
+
- Token group `blur` (every root theme must define it); recipe keys + `backdropBlur`, `animation`.
|
|
467
|
+
- Conformance version 2; `conformance-known.json` gains `causes`/`rules`.
|
|
468
|
+
- Strings + 8 ids; `styleToCss(style, fonts, theme?)`.
|
|
469
|
+
|
|
470
|
+
## 11. Deferred
|
|
471
|
+
|
|
472
|
+
- Per-renderer implementation of everything above (the renderer lanes).
|
|
473
|
+
- Backdrop blur on gpui: no backdrop sampling in the pinned gpui; the background-only fallback stands until upstream adds one (smallest step: a gpui `BackdropFilter` element upstream).
|
|
474
|
+
- A localized string table per locale (strings stay host overrides).
|
|
475
|
+
- Calendar-aware relative days (`yesterday` across midnight in the surface zone): today 24 h buckets; smallest step: a `calendarDays` option on `relativeTime`.
|
|
476
|
+
- Cascading resize (pushing past the neighbour into the next panel): only adjacent panels change.
|