@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,679 @@
|
|
|
1
|
+
# Exponential UI — round 1 contract (renderer hardening)
|
|
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 1 makes one A2UI surface render faithfully, responsively and
|
|
10
|
+
accessibly on web, desktop, iOS and Android. This file is the CONTRACT half:
|
|
11
|
+
what changed in `@exponential-at/ui` (catalog, macros, style, tokens,
|
|
12
|
+
themes, the TS reference, fixtures, generated constants) and what each
|
|
13
|
+
renderer (React, the Rust core, gpui, SwiftUI, Compose) must implement to
|
|
14
|
+
match. Everything below is locked by a fixture or a test in this package;
|
|
15
|
+
the section numbers are the ones the catalog `$comment`s cite.
|
|
16
|
+
|
|
17
|
+
Reading order for a renderer lane: §0 (what moved), then the section for
|
|
18
|
+
the area you own, then §9 (your checklist), §10 (breaking changes).
|
|
19
|
+
|
|
20
|
+
## 0. What moved, at a glance
|
|
21
|
+
|
|
22
|
+
| area | change | source of truth | locked by |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| bound macro inputs (audit bug A) | the expander EMITS function calls for bound inputs; renderers evaluate at bind time | `catalog/macros.json` `$comment`, `src/expr.ts`, `src/macros.ts`, `src/dynamic.ts` | `fixtures/catalog-macros.json` (`bound:*` cases), `fixtures/bind-time.json` |
|
|
25
|
+
| two-way binding for macros | `$set` → `functionCall: {call: "set"}` on the part's action | `catalog/macros.json`, `src/macros.ts`, `src/dynamic.ts runAction` | `bind-time.json` `presses` |
|
|
26
|
+
| core functions | `percent add sub eq lt clamp cond fallback concat coalesce text map len fill` + action `set`; the BIND table adds the basic `and or not required` (`bindFunctionNames`) | `core.catalog.json` `functions.core`, `src/expr.ts CORE_FUNCTIONS`, `src/dynamic.ts BIND_FUNCTIONS` | `expr.test.ts`, `bind-time.json`, `round1-fixes.test.ts` |
|
|
27
|
+
| style conditions | max-width, min/max-height, orientation, hover, prefers-reduced-motion, `$breakpoint.*`; states `:hover` `:focus-visible` `:pressed` | `catalog/style.json` `conditions`, `src/style.ts resolveConditions` | `fixtures/style-conditions.json` |
|
|
28
|
+
| style keys | 23 new keys (transition, transform, gradient, per-side widths, per-corner radii, text styling, overflowX/Y, visibility, pointerEvents, userSelect, cursor, …) | `catalog/style.json` | `style.test.ts`, schema |
|
|
29
|
+
| responsive props | `{base, sm?, md?, lg?, xl?}` on `responsive` props | `core.catalog.json`, `src/macros.ts` | `catalog-macros.json` (`responsive:*`), kitchen sink |
|
|
30
|
+
| catalog | 21 new components, Table macro → native, new props everywhere | `core.catalog.json`, `macros.json`, `recipes.json` | `catalog-components.json`, `catalog-macros.json` |
|
|
31
|
+
| built-in strings | `$string.<id>`, host-overridable table | `catalog/strings.json`, `src/strings.ts` | `bind-time.json`, `round1.test.ts` |
|
|
32
|
+
| locale | week start, likely region (CLDR), language aliases, RTL, mirrored glyphs | `catalog/locale.json`, `src/locale.ts` | `round1.test.ts`, `round1-fixes.test.ts` |
|
|
33
|
+
| CodeBlock tokenizer | one scanner, 15 languages | `catalog/code.json`, `src/code.ts` | `fixtures/code-tokens.json` |
|
|
34
|
+
| chart numbers | extents, nice ticks, colour order, donut hole | `src/chart.ts` | `round1.test.ts` |
|
|
35
|
+
| accessibility | machine-readable `roles`; role + notes + keyboard per component; macro parts carry role/states/name (`$a11y` → `accessibility`); rules, host commands | `catalog/a11y.json`, `catalog/macros.json`, `src/a11y.ts`, `src/macros.ts` | `round1.test.ts`, `round1-fixes.test.ts`, `catalog-macros.json` |
|
|
36
|
+
| bind pass | per-ROW Table slot cells, DATA props verbatim, bound accessible names | `src/dynamic.ts` | `bind-time.json` (`extra`, `rowSlots`) |
|
|
37
|
+
| theming | breakpoint/ease/density tokens, chart6–8, shadow xl, contrast overlays, `system` mode, recipe motion | `catalog/tokens.json`, `themes/*`, `src/theme.ts` | `theme-*.json`, `control-geometry.json` |
|
|
38
|
+
| prompt | teaches responsive rules, states, `visible`, keys; common props described once | `src/prompt.ts` | `prompt-budget.json` (6451 / 6500 tokens) |
|
|
39
|
+
|
|
40
|
+
## 1. Bound macro inputs and two-way binding
|
|
41
|
+
|
|
42
|
+
### The bug
|
|
43
|
+
Macros expand in the reducer before any data exists. `expr.ts` used to
|
|
44
|
+
compute `$percent`, string interpolation, `$cond`, `$eq`, `$add` on the
|
|
45
|
+
`{path}` OBJECT: Progress `value: {path}` drew `width: "0%"`, Pagination
|
|
46
|
+
`page: {path}` printed `[object Object] / 5`, Collapsible `open: {path}` was
|
|
47
|
+
always open (a binding object is truthy) and never wrote back, a bound Pill
|
|
48
|
+
`label` passed only by luck.
|
|
49
|
+
|
|
50
|
+
### The design (chosen: emit, never re-expand)
|
|
51
|
+
Re-expanding on every data change would make expansion a function of data
|
|
52
|
+
and force every renderer to re-run the macro table per frame. Instead, when
|
|
53
|
+
ANY input of a template expression is DYNAMIC (an A2UI data binding
|
|
54
|
+
`{path}` or a function call `{call, args}`), the expander emits an A2UI
|
|
55
|
+
function call over the CORE functions; the renderer evaluates it at bind
|
|
56
|
+
time exactly like an authored call. Expansion stays a pure function of the
|
|
57
|
+
surface, so the TS reference and the Rust core stay line-by-line mirrors.
|
|
58
|
+
|
|
59
|
+
| template form | literal inputs | a dynamic input emits |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `"{props.x}"` (whole string) as a MEMBER of props/style/context/`$recipe` or an array item | the value | the dynamic value itself (two-way binding survives) |
|
|
62
|
+
| `"{props.x\|d}"` as a member | value, else `d` | the dynamic value itself (the fallback is dropped) |
|
|
63
|
+
| `"{props.x\|d}"` as an OPERAND (inside any `$`-object, `$cond`/`$map` results included) | value, else `d` | `fallback{value, default: d}` |
|
|
64
|
+
| `"a {props.x} b"` | the joined string | `concat{values: ["a ", x, " b"]}` (empty pieces dropped) |
|
|
65
|
+
| `"{!props.x}"` | boolean | `not{value}` |
|
|
66
|
+
| `"{len(props.x)}"` | length | `len{value}` |
|
|
67
|
+
| `"{range(props.x)}"` (for `$each`) | `[0..n-1]` | nothing (no items) |
|
|
68
|
+
| `$map {from, cases, default}` | the case | `map{value, cases, default}` |
|
|
69
|
+
| `$cond [c, then, else]` | then/else | `cond{if, then, else}` |
|
|
70
|
+
| `$eq` `$lt` `$add` `$sub` `$percent` | value | `eq{a,b}` `lt{a,b}` `add{a,b}` `sub{a,b}` `percent{value,max}` |
|
|
71
|
+
| `$text` | String(v) | `text{value}` |
|
|
72
|
+
| `$coalesce [a, b, …]` | first truthy | literals that are falsy are skipped; the first truthy literal BEFORE any dynamic wins; else `coalesce{values: [dyn…, first truthy literal]}` |
|
|
73
|
+
| `$clamp [v, min, max]` | the clamped number | `clamp{value, min, max}` |
|
|
74
|
+
| `$fill [template, {name: v}]` | ALWAYS `fill{template, params}` (the template is a `$string.<id>` only the surface's table resolves) | `fill{template, params}` |
|
|
75
|
+
| `$not` | boolean | `not{value}` |
|
|
76
|
+
| `$if` / `$any` on a part | decides at expansion | the part IS emitted with `visible` = the dynamic condition (`and{values}` for several `$if`s, `or{values}` for several `$any`s; one alone is itself) |
|
|
77
|
+
| `$each` over a dynamic array | one part per item | nothing (use List with a template) |
|
|
78
|
+
|
|
79
|
+
Template KEYS interpolate too (`"@media (max-width: $breakpoint.{props.collapseBelow|md})"`).
|
|
80
|
+
|
|
81
|
+
Truthiness everywhere (expander and renderer) = `truthy`: `undefined`,
|
|
82
|
+
`null`, `false` and `""` are false; `0` is TRUE.
|
|
83
|
+
|
|
84
|
+
### Core functions (`core.catalog.json` `functions.core`)
|
|
85
|
+
`percent{value, max}` → `"N%"` clamped 0..100, two decimals, `0%` when max
|
|
86
|
+
≤ 0 · `add{a,b}` / `sub{a,b}` (non-numbers count 0) · `eq{a,b}` strict,
|
|
87
|
+
EXCEPT a number equals a string spelling it (`5` = `"5"`: form and URL state
|
|
88
|
+
arrive as strings; `0` ≠ `""`, `1` ≠ `true`) · `lt{a,b}` numeric ·
|
|
89
|
+
`clamp{value, min?, max?}` (a number; a missing bound does not clamp; min
|
|
90
|
+
wins when max < min) · `cond{if, then, else}` · `fallback{value, default}`
|
|
91
|
+
(value unless undefined/null; `0`, `""` and `false` are values) ·
|
|
92
|
+
`concat{values}` (undefined/null as "") · `coalesce{values}` (first truthy)
|
|
93
|
+
· `text{value}` (String, missing stays missing) · `map{value, cases,
|
|
94
|
+
default}` (`cases[String(value)]`) · `len{value}` (array or string length,
|
|
95
|
+
else 0) · `fill{template, params}` (`{name}` placeholders filled, unknown
|
|
96
|
+
ones stay; the template arrives already resolved through the string table) ·
|
|
97
|
+
`set{path, value}` (ACTION only).
|
|
98
|
+
|
|
99
|
+
**The bind table is MORE than the core names.** The expander also emits the
|
|
100
|
+
basic catalog's `not` (`{!x}`, `$not`), `and` (several bound `$if`s) and
|
|
101
|
+
`or` (several bound `$any`s), with the same truthiness; `required` completes
|
|
102
|
+
the reference table. A renderer implements ALL of `bindFunctionNames`
|
|
103
|
+
(generated; = `src/dynamic.ts BIND_FUNCTIONS`): a table of only
|
|
104
|
+
`coreFunctionNames` resolves Collapsible's `set` value `not{…}` to nothing.
|
|
105
|
+
Arguments resolve at ANY depth (arrays and objects of bindings/calls).
|
|
106
|
+
|
|
107
|
+
### Two-way binding: `$set` and the combined action
|
|
108
|
+
A template part may say `"$set": {"press": {"prop": "open", "value": "{!props.open}"}}`.
|
|
109
|
+
When the author's `props.open` is a DataBinding `{path: P}`, the part's
|
|
110
|
+
`press` action gets `functionCall: {call: "set", args: {path: P, value: <evaluated value>}}`
|
|
111
|
+
next to any routed author event (key order `event`, then `functionCall`). A
|
|
112
|
+
literal prop adds nothing — the host keeps owning that state through the
|
|
113
|
+
routed event, as before. An Action carries ONE function: when the author's
|
|
114
|
+
routed handler is itself a `functionCall` (e.g. `openUrl`), it wins, NO `set` is
|
|
115
|
+
added and the reducer reports `on.<event>: a function action replaces the
|
|
116
|
+
two-way set of props.<prop>; …` once per macro (never lost silently; locked
|
|
117
|
+
by `bind-time.json` `extra` `TabBar/set:author-function`). Used by: Collapsible (open), Pagination (page),
|
|
118
|
+
ButtonGroup (selected), TabBar (value), Rating (value), AlertDialog
|
|
119
|
+
cancel/confirm (open → false).
|
|
120
|
+
|
|
121
|
+
**Action semantics** (`src/dynamic.ts runAction`): resolve the function's
|
|
122
|
+
args AND the event context against the data model AS IT IS at the press,
|
|
123
|
+
then apply `set` (write `value` at `path`, relative paths against the
|
|
124
|
+
node's data scope like a binding; missing objects are created), then
|
|
125
|
+
dispatch the event. So a Collapsible's routed event carries the NEW `open`
|
|
126
|
+
(`!old`), and the write does not double-negate it.
|
|
127
|
+
|
|
128
|
+
**Natives with bindable state** (Dialog/Drawer/Popover/Toast `open`,
|
|
129
|
+
Tabs/ToggleGroup/Select/Radio/Input/… `value`, Checkbox/Switch `checked`,
|
|
130
|
+
Toggle `pressed`, Carousel `page`, Table `sort`/`selected`, ChipInput
|
|
131
|
+
`values`, DateRangePicker `start`/`end`, NumberField/TimePicker `value`):
|
|
132
|
+
when the prop is a binding, the renderer writes the user's change to that
|
|
133
|
+
path itself AND fires `change` (this is the two-way binding the macros now
|
|
134
|
+
reach through `set`).
|
|
135
|
+
|
|
136
|
+
### The bind pass (reference: `src/dynamic.ts`)
|
|
137
|
+
After expansion, per data model: `bindTree(node, data, {scope?, strings?,
|
|
138
|
+
extensions?})`:
|
|
139
|
+
|
|
140
|
+
- **Props resolve along the component's SCHEMA** (`resolveNodeProps`): a
|
|
141
|
+
binding or call anywhere resolves; arrays and SHAPED objects recurse; a
|
|
142
|
+
DATA position — a shape-less `object` or an array of them, today only
|
|
143
|
+
Table `rows` (generated `dataProps`) — is the author's data: a LITERAL
|
|
144
|
+
there is copied VERBATIM (a row `{path: "/etc/hosts"}`, a `call` column or
|
|
145
|
+
a `"$string.cancel"` cell stay data), a dynamic value AT the position
|
|
146
|
+
resolves and its result is never descended. Values a binding returns are
|
|
147
|
+
never descended or `$string`-resolved anywhere. Everything else
|
|
148
|
+
(`resolveDynamic`) resolves at any depth: bindings by JSON pointer
|
|
149
|
+
(absolute `/a/b`, or relative to the scope), calls by the BIND table,
|
|
150
|
+
`$string.<id>` through the surface's string table.
|
|
151
|
+
- STYLE values, recipe props and **`accessibility`** (A2UI's
|
|
152
|
+
`label`/`description` are DynamicStrings; the `$a11y` states too) resolve
|
|
153
|
+
the same way; an empty result is dropped.
|
|
154
|
+
- A node whose `visible` resolves falsy is dropped (with its subtree, no
|
|
155
|
+
layout); `visible` is removed. Actions stay untouched (press time).
|
|
156
|
+
- A `template` and the slots of a **ROW-SCOPED** component (catalog
|
|
157
|
+
`slotScope: "row"`, generated `rowSlotComponents`: Table) stay UNBOUND.
|
|
158
|
+
The painter binds each slot cell once per row with `bindRowSlot(slot,
|
|
159
|
+
rowsProp, rows, index, data)`: `rowsProp` = the UNBOUND `rows` value — a
|
|
160
|
+
binding gives the scope `{base: "<its pointer>/<index>"}` (relative paths
|
|
161
|
+
read the row in the model, a relative `set` writes into it); a literal or
|
|
162
|
+
a call gives `{item: rows[index]}` (relative paths read inside the row,
|
|
163
|
+
absolute ones the surface data, a relative `set` writes nothing). `index`
|
|
164
|
+
is the row's index in `rows` as given, BEFORE any local sort. A cell whose
|
|
165
|
+
`visible` resolves falsy for a row renders empty for that row.
|
|
166
|
+
|
|
167
|
+
After expansion a STYLE value may be dynamic (Progress fill `width:
|
|
168
|
+
percent{…}`); resolve it, then apply it like an authored value. Authors
|
|
169
|
+
cannot write dynamic style values (`validateStyle` refuses).
|
|
170
|
+
|
|
171
|
+
`fixtures/bind-time.json` — `cases`: 26 (every bindable macro prop bound to
|
|
172
|
+
`/<prop>`), each × THREE data models (a sample, its falsy twin, and `{}` =
|
|
173
|
+
the path MISSING, so every operand's `fallback{value, default}` takes its
|
|
174
|
+
default; `0` is TRUTHY, so the number twin exercises arithmetic, not
|
|
175
|
+
truthiness) → the bound tree and every `set` press outcome. `extra`: 5
|
|
176
|
+
hand-written cases (bound-rows and literal-rows Table slot cells with
|
|
177
|
+
`rowSlots` = every row's bound cells, DATA rows verbatim, a bound accessible
|
|
178
|
+
name, an author function meeting `$set` with the reducer's `issues`, a
|
|
179
|
+
numeric prop bound to string data). The audit cases are pinned in
|
|
180
|
+
`round1.test.ts` ("the audit's bugs"), the review fixes in
|
|
181
|
+
`round1-fixes.test.ts`.
|
|
182
|
+
|
|
183
|
+
## 2. Responsive authoring and the style whitelist
|
|
184
|
+
|
|
185
|
+
### Conditions (`catalog/style.json` `conditions`, `src/style.ts`)
|
|
186
|
+
One level deep, ONE regex grammar (`STYLE_MEDIA_PATTERN`):
|
|
187
|
+
|
|
188
|
+
- `@media (min-width: N)` / `(max-width: N)` / `(min-height: N)` /
|
|
189
|
+
`(max-height: N)` — `N` = `<number>px` or `$breakpoint.<name>`; sizes are
|
|
190
|
+
the SURFACE box (not the window); `min-*` = `size >= N`, `max-*` =
|
|
191
|
+
`size < N` (strict, so `min-width: md` and `max-width: md` never overlap —
|
|
192
|
+
React: emit `@container xui (width < Npx)`, not `max-width`).
|
|
193
|
+
- `@media (orientation: portrait|landscape)` — portrait = height ≥ width;
|
|
194
|
+
landscape when the height is unknown.
|
|
195
|
+
- `@media (hover: hover|none)` — a hover-capable pointer.
|
|
196
|
+
- `@media (prefers-reduced-motion: reduce|no-preference)` — the platform
|
|
197
|
+
setting.
|
|
198
|
+
- NOT supported: `prefers-color-scheme` (mode belongs to the theme),
|
|
199
|
+
ranges, `em`, `screen`, nesting.
|
|
200
|
+
- States `:hover`, `:focus-visible`, `:pressed` against the node's own
|
|
201
|
+
interaction state (`:focus-visible` = keyboard focus only).
|
|
202
|
+
|
|
203
|
+
Flattening (`resolveConditions`, the reference): base keys, then every
|
|
204
|
+
MATCHING `@media` block in SOURCE order (later wins; authors write
|
|
205
|
+
narrow-first), then `:hover`, `:focus-visible`, `:pressed` in that order
|
|
206
|
+
(pressed wins). A `$breakpoint` token resolves through the theme
|
|
207
|
+
(`theme.tokens.breakpoint`) BEFORE matching; an unknown token never matches
|
|
208
|
+
(and `validateStyle` reports it). `resolveStyleValues` also rewrites the
|
|
209
|
+
token inside media KEYS (`resolveConditionKey`). Locked by
|
|
210
|
+
`fixtures/style-conditions.json` (7 cases × contexts).
|
|
211
|
+
|
|
212
|
+
### Breakpoint tokens
|
|
213
|
+
`tokens.json` `breakpoint: [sm, md, lg, xl]`, px, per theme (built-ins:
|
|
214
|
+
640 / 768 / 1024 / 1280, Tailwind's). `activeBreakpoint(width, bp)` = the
|
|
215
|
+
LAST name whose px ≤ width, `null` below `sm` (= `base`).
|
|
216
|
+
|
|
217
|
+
### Responsive props
|
|
218
|
+
A prop with `responsive: true` also takes `{base, sm?, md?, lg?, xl?}`;
|
|
219
|
+
the value at a breakpoint = the nearest key at or below it (`base` under
|
|
220
|
+
the first) — `responsiveAt`. Validation checks each value and refuses
|
|
221
|
+
other keys. Today: Stack `direction gap align justify wrap`, Grid
|
|
222
|
+
`columns gap`, Drawer `side`.
|
|
223
|
+
|
|
224
|
+
- On a MACRO the expander evaluates each part's style with the `base`
|
|
225
|
+
props, then once per breakpoint the value names (ascending, each
|
|
226
|
+
inheriting the last), and every key that changed lands in an
|
|
227
|
+
`"@media (min-width: $breakpoint.<bp>)"` block of that part. Props,
|
|
228
|
+
conditions and recipe props see `base` (a responsive value cannot add or
|
|
229
|
+
remove parts). A key cannot be UNSET at a breakpoint.
|
|
230
|
+
- On a NATIVE the renderer picks `responsiveAt(value, activeBreakpoint(surfaceWidth, theme.tokens.breakpoint))`
|
|
231
|
+
at layout time (Drawer: a bottom sheet on phones, a side panel from md),
|
|
232
|
+
and queries the recipe with that RESOLVED value (`when: {side: "right"}`
|
|
233
|
+
never sees the object).
|
|
234
|
+
- An author `style` on a macro still merges over the root's, condition
|
|
235
|
+
blocks merged key by key, so a Box style override remains a second way.
|
|
236
|
+
|
|
237
|
+
### Whitelist additions (`catalog/style.json`; schema + `STYLE_KEY_NAMES` ×4)
|
|
238
|
+
|
|
239
|
+
| key | values | notes |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `overflowX`, `overflowY` | visible hidden clip scroll auto | layout |
|
|
242
|
+
| `insetBlockStart`, `insetBlockEnd` | length | layout (logical top/bottom) |
|
|
243
|
+
| `borderTopWidth` … `borderLeftWidth` | number / `$border.*` | LAYOUT EFFECT like `borderWidth` (`STYLE_LAYOUT_EFFECT_KEYS`); taffy border rect per side |
|
|
244
|
+
| `borderStyle` | solid dashed dotted | default solid; a width without a style is solid |
|
|
245
|
+
| `borderTopLeftRadius` … `borderBottomLeftRadius` | length | per corner, over `borderRadius` |
|
|
246
|
+
| `backgroundGradient` | `{angle, stops: [{color, offset 0..1}]}` (≥ 2 stops) | linear, CSS angle (0 = up, 90 = right), painted OVER `backgroundColor` (the colour shows only through transparent stops) |
|
|
247
|
+
| `fontWeight` | 300 400 500 600 700 800 | was 400–700 |
|
|
248
|
+
| `letterSpacing` | length | px |
|
|
249
|
+
| `textAlign` | left right center start end justify | start/end flip in RTL |
|
|
250
|
+
| `textDecoration` | none underline line-through | |
|
|
251
|
+
| `textTransform` | none uppercase lowercase capitalize | |
|
|
252
|
+
| `fontStyle` | normal italic | |
|
|
253
|
+
| `transition` | `$motion.<name>` ONLY | the duration opacity/colour/transform/size changes animate with; nothing animates without it; reduced motion = 0 ms |
|
|
254
|
+
| `transitionEasing` | `$ease.<name>` ONLY | cubic-bezier `[x1, y1, x2, y2]` |
|
|
255
|
+
| `transform` | `translate(Xpx, Ypx)`, `scale(N)`, `rotate(Ndeg)`, space-separated | PAINT-ONLY: never changes layout, hit-testing follows paint where the platform does |
|
|
256
|
+
| `visibility` | visible hidden | hidden keeps its box, paints nothing, leaves the a11y tree |
|
|
257
|
+
| `pointerEvents` | auto none | none = presses pass through |
|
|
258
|
+
| `userSelect` | auto none text | |
|
|
259
|
+
| `cursor` | auto default pointer text not-allowed grab grabbing move col-resize row-resize | a hint pointer platforms honour, touch ignores |
|
|
260
|
+
|
|
261
|
+
Still excluded (decided): `zIndex` (paint order = tree order, absolute
|
|
262
|
+
children LAST among siblings, overlays stack by LAYER), per-side border
|
|
263
|
+
COLOURS (gpui paints one border colour per box), `calc()`, named grid
|
|
264
|
+
lines, `gridAutoFlow`, `justifyItems`, `order`, multi-value shorthands.
|
|
265
|
+
|
|
266
|
+
CSS mapping lives in `src/css.ts` (`styleToCss`: `transition` →
|
|
267
|
+
`all <ms>ms cubic-bezier(…)`, `backgroundGradient` → `background-image:
|
|
268
|
+
linear-gradient(…)`, any non-zero width without colour → `transparent`,
|
|
269
|
+
without style → `solid`).
|
|
270
|
+
|
|
271
|
+
## 3. Catalog
|
|
272
|
+
|
|
273
|
+
82 components (46 natives, 36 macros), 81 visible.
|
|
274
|
+
|
|
275
|
+
### New components
|
|
276
|
+
|
|
277
|
+
| component | kind | why that kind / key behaviour |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| Form | native | owns field collection, submit gating and error focus |
|
|
280
|
+
| NumberField | native | steppers, clamping, locale formatting |
|
|
281
|
+
| ChipInput | native | token editing, keyboard |
|
|
282
|
+
| DateRangePicker | native | one calendar, two-step selection |
|
|
283
|
+
| TimePicker | native | list or typed `HH:mm` |
|
|
284
|
+
| FileUpload | native | drop zone, file picker, host upload callback |
|
|
285
|
+
| CodeBlock | native | tokenizer, gutter, copy |
|
|
286
|
+
| ContextMenu | native | right-click / long-press, shares DropdownMenu's menu |
|
|
287
|
+
| Toast | native | toast LAYER, timers, live region |
|
|
288
|
+
| Table | native (was a macro) | typed cells, sort, selection, windowing, slot cells |
|
|
289
|
+
| ScrollArea | macro | Box with overflowX/Y |
|
|
290
|
+
| Sidebar | macro | two columns, the column hidden below `collapseBelow` (a `max-width` block) or when `collapsed` (bound → `visible`). NOTHING replaces it on narrow screens (one slot cannot render twice without duplicating ids and state): the AUTHOR gives phones another way in — a TabBar, or a Sheet behind an AppBar menu button (`$string.menu`) holding the same destinations; the catalog description (= the prompt) says so |
|
|
291
|
+
| AppBar | macro | title/subtitle/back + `leading`/`trailing` slots spliced in place (`"$slot:name"`) |
|
|
292
|
+
| TabBar | macro | bottom destinations, `$set` value |
|
|
293
|
+
| Breadcrumb | macro | links + current, `select` event |
|
|
294
|
+
| Stepper | macro | `status` = done/current/upcoming per step (a `$recipe` discriminator) |
|
|
295
|
+
| Rating | macro | `range(max)` stars, `$set` value |
|
|
296
|
+
| Kbd, Label | macros | text styling |
|
|
297
|
+
| AlertDialog | macro over Dialog | `dismissible: false`, a template-built `footer` SLOT part (cancel/confirm), `$set` open → false |
|
|
298
|
+
| HoverCard | macro over Popover | `openOn: hover` |
|
|
299
|
+
| Sparkline | macro over Chart | `kind: sparkline`, no axes/grid/legend |
|
|
300
|
+
|
|
301
|
+
### Changed components
|
|
302
|
+
- **Table** (BREAKING): `columns: [{key, label, type?, slot?, width?, align?, sortable?}]`,
|
|
303
|
+
`rows: [object]` (bindable), `rowKey="id"`, `sort {key, direction}`
|
|
304
|
+
(bindable: bound = the host orders `rows`; unbound = the renderer sorts a
|
|
305
|
+
local copy: numbers numerically, strings by the locale collator, missing
|
|
306
|
+
last), `selectable none|single|multiple` (multiple = a checkbox column with
|
|
307
|
+
a select-all header), `selected [keys]` (bindable), `caption`,
|
|
308
|
+
`emptyText`, `striped` (the painter queries `row` with `striped: true` on
|
|
309
|
+
ODD rows), `density`, `stickyHeader`. Cell `type`: text, number (locale
|
|
310
|
+
number), date (locale date), boolean (a tick), badge (the value in a
|
|
311
|
+
Badge look), slot (the Table slot named by `slot` renders once per row
|
|
312
|
+
with the ROW as its data scope: relative bindings resolve into the row —
|
|
313
|
+
§1 "The bind pass", `bindRowSlot`). `slots: ["*"]` = any slot name;
|
|
314
|
+
`slotScope: "row"`. `rows` is DATA: literal rows are never interpreted.
|
|
315
|
+
Windowed past `WINDOW_THRESHOLD` = 50 rows.
|
|
316
|
+
- **Chart**: kinds `bar stackedBar line area pie donut sparkline`; `min`,
|
|
317
|
+
`max`, `xLabel`, `yLabel`, `showAxes`, `showGrid`, `showLegend`,
|
|
318
|
+
`showValues`; categories optional (sparkline); the width is the box's (no
|
|
319
|
+
fixed viewBox). Numbers every painter shares (`src/chart.ts`): extent
|
|
320
|
+
(`min` defaults to 0 or the lowest negative, `max` to the largest value
|
|
321
|
+
but at least 0 — for stackedBar the largest category sum of the POSITIVE
|
|
322
|
+
values up and the lowest sum of the NEGATIVE ones down; sparkline spans
|
|
323
|
+
its own min..max; computed in loops, never by spreading, so a 150k-point
|
|
324
|
+
series is fine), the accessible name `$string.chartSummary` /
|
|
325
|
+
`sparklineSummary` filled from `chartSummaryParams` /
|
|
326
|
+
`sparklineSummaryParams`,
|
|
327
|
+
`niceTicks(min, max, 5)` (Heckbert, steps 1/2/5×10ⁿ, rounded to the step's
|
|
328
|
+
decimals) for axis ticks and grid lines, colours = the series tone, else
|
|
329
|
+
`$color.chart1..8` in order (wrapping), donut hole = 0.6 × radius, the
|
|
330
|
+
legend only with 2+ series or slices, the tooltip part on hover/focus.
|
|
331
|
+
- **Image**: `fallback` icon (on error or no src; default glyph
|
|
332
|
+
`builtinIcons["Image.fallback"]`), `loading lazy|eager`, `focalX/Y` 0..1
|
|
333
|
+
(the point kept when cropped, 0.5 default).
|
|
334
|
+
- **Drawer**: `dismissible`, `dragToDismiss`, responsive `side`.
|
|
335
|
+
- **Textarea**: `autosize` from `rows` to `maxRows`.
|
|
336
|
+
- **Markdown**: `lines` truncation, `style`. **Text**: `live off|polite|assertive`.
|
|
337
|
+
- **Popover**: `openOn press|hover` (hover also opens on keyboard focus).
|
|
338
|
+
- **Select** = the combobox: `searchable` adds a filter field and fires
|
|
339
|
+
`search {query}` (debounce 150 ms); `options` is bindable so the host
|
|
340
|
+
refills it (async source); `source` names a host list; `emptyText`.
|
|
341
|
+
- **Button**: `submit` submits the enclosing Form.
|
|
342
|
+
- **DatePicker**: `firstDayOfWeek` (else the locale's, §4).
|
|
343
|
+
- **DropdownMenu** + ContextMenu `menuItem.kind`: `item | checkbox | separator | label | submenu`
|
|
344
|
+
(`checked` bindable, `items` one level deep, `shortcut`).
|
|
345
|
+
|
|
346
|
+
### Event payloads (what the renderer merges into the author's event context)
|
|
347
|
+
|
|
348
|
+
The payload is merged OVER the author's resolved `context`: on a clashing key
|
|
349
|
+
the component's payload WINS (React `hardening.test.tsx`, Rust
|
|
350
|
+
`Surface::fire`). The payload is also still sent as its own `payload` field.
|
|
351
|
+
|
|
352
|
+
| component | event | context |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| value controls (Input, Textarea, Select, Radio, ToggleGroup, Tabs, Slider, NumberField, TimePicker, DatePicker, Composer) | `change` | `{value}` (NumberField: a number) |
|
|
355
|
+
| Checkbox, Switch / Toggle | `change` | `{checked}` / `{pressed}` |
|
|
356
|
+
| Dialog, Drawer, Popover, Toast (and the macros over them) | `change` | `{open}` |
|
|
357
|
+
| DateRangePicker | `change` | `{start, end}` |
|
|
358
|
+
| ChipInput | `change` / `add` / `remove` | `{values}` / `{value}` / `{value}` |
|
|
359
|
+
| Select | `search` | `{query}` |
|
|
360
|
+
| DropdownMenu, ContextMenu | `select` | `{value}` (+ `{checked}` for a checkbox item, its NEW state) |
|
|
361
|
+
| Table | `sort` / `select` / `rowPress` | `{sort: {key, direction}}` / `{selected: [keys]}` / `{key, row}` |
|
|
362
|
+
| Form | `submit` / `invalid` / `change` | `{values: {<field name>: value}}` / `{errors: [{name, message}]}` / `{name, value}` |
|
|
363
|
+
| FileUpload | `upload` / `remove` | `{files: [{name, size, type}]}` / `{name}`; the BYTES go to the host's `onUpload(files, {nodeId, name})` callback, never through an event |
|
|
364
|
+
| Toast | `dismiss` / `action` | `{}` |
|
|
365
|
+
| Input, NumberField / Composer | `submit` | `{value}` |
|
|
366
|
+
| macros | as routed by `$on` + `$context` | e.g. Pagination `{page}`, Breadcrumb `{value}`, Rating `{value}` |
|
|
367
|
+
|
|
368
|
+
### Form
|
|
369
|
+
Collects every field beneath it that has a `name` (Input, Textarea,
|
|
370
|
+
NumberField, Checkbox, Radio, Switch, Select, ChipInput, DatePicker,
|
|
371
|
+
DateRangePicker, TimePicker, FileUpload, Slider) — nested Forms are not
|
|
372
|
+
collected by the outer one. Submit (a `submit` Button, Enter in a
|
|
373
|
+
single-line field) runs EVERY field's `checks` regardless of `validateOn`;
|
|
374
|
+
any failure: no `submit`, `invalid {errors}` fires, EVERY failing field
|
|
375
|
+
shows ALL its failed messages under it (id `<field id>.error`, linked by
|
|
376
|
+
aria-describedby / the platform equivalent), `summary: true` also lists
|
|
377
|
+
them above the fields, focus moves to the first invalid field and
|
|
378
|
+
`invalidFields` is announced. `busy` refuses submits and shows the submit
|
|
379
|
+
Button loading; `disabled` makes every field inert.
|
|
380
|
+
|
|
381
|
+
### Toast
|
|
382
|
+
A native in the TOAST layer (above dialogs), stacked bottom-centre
|
|
383
|
+
(phones) / bottom-end (wide), newest nearest the edge, max 3 visible. The
|
|
384
|
+
host queues by rendering one Toast per notice with `open` bound; the
|
|
385
|
+
renderer runs `duration` (0 = sticky; paused while hovered or focused),
|
|
386
|
+
writes `open: false`, fires `dismiss` + `change`. `error` toasts are
|
|
387
|
+
`assertive`, others `polite`; a toast never takes focus.
|
|
388
|
+
|
|
389
|
+
### CodeBlock tokenizer (`catalog/code.json`, `src/code.ts`)
|
|
390
|
+
One forward scan, first matching rule wins: line kinds → line comments →
|
|
391
|
+
block comments → strings (`\` escapes; stop at a line break unless the
|
|
392
|
+
quote is `multiline`; `keyStrings` makes a string before `:` a property) →
|
|
393
|
+
markup tags (html) → numbers (`[0-9][0-9A-Za-z_.]*`) → identifiers
|
|
394
|
+
(keyword | property before `:` with `keyIdents` | function before `(` |
|
|
395
|
+
property after `.` | type (typeNames or Capitalized) | plain) → spaces →
|
|
396
|
+
operator runs → one punctuation char → one plain char. Then split into
|
|
397
|
+
LINES and merge adjacent same-kind tokens; a line's tokens concatenate to
|
|
398
|
+
exactly its text. Kinds = the `codeToken` enum; the recipe is
|
|
399
|
+
`CodeBlock/token` with `when: {kind}`. `fixtures/code-tokens.json` (17
|
|
400
|
+
cases, all 15 languages + edge cases).
|
|
401
|
+
|
|
402
|
+
### Built-in glyphs (`core.catalog.json` `builtinIcons`)
|
|
403
|
+
`<Component>.<part>[.<variant>]` → an icons.json name, e.g.
|
|
404
|
+
`CodeBlock.copy → ui-copy`, `FileUpload.icon → upload`,
|
|
405
|
+
`Table.sortIcon.asc → ui-chevron-up`, `Toast.icon.error → ui-error`.
|
|
406
|
+
Generated as `builtinIconSlots` / `builtinIconNames` (×4). Never hardcode
|
|
407
|
+
a glyph for an owned part.
|
|
408
|
+
|
|
409
|
+
## 4. Data, locale and strings
|
|
410
|
+
|
|
411
|
+
- **`visible`** (every component, reserved key like `style`): `true |
|
|
412
|
+
false | {path} | {call}`; falsy once resolved = not rendered, no layout,
|
|
413
|
+
not in the a11y tree. The expander also sets it on parts whose `$if` was
|
|
414
|
+
bound. On a macro the author's `visible` lands on the expanded root.
|
|
415
|
+
- **Template keys**: `children: {componentId, path, key?}` → normalized
|
|
416
|
+
`template: {component, path, key?}`; `key` = a pointer relative to each
|
|
417
|
+
item whose value identifies it, so reordering keeps the item's component
|
|
418
|
+
state (focus, scroll, local input); the index when absent.
|
|
419
|
+
- **`accessibility`** is now copied by `reduceSurface` too (it was only in
|
|
420
|
+
`reduceNested`).
|
|
421
|
+
- **SurfaceSettings** (`src/types.ts`): `locale` (BCP 47, default `en-US`),
|
|
422
|
+
`strings` (overrides), `mode` (`light | dark | system`), `density`,
|
|
423
|
+
`contrast` (`normal | high | system`), `theme`.
|
|
424
|
+
- **Locale contract** (`catalog/locale.json`, `src/locale.ts`): formatting
|
|
425
|
+
is the platform's ICU (Intl / Foundation / java.text / icu4x) in the
|
|
426
|
+
SURFACE locale for `formatNumber`, `formatCurrency`, `formatDate`,
|
|
427
|
+
`pluralize`, NumberField, Table number/date cells, calendar month and
|
|
428
|
+
weekday names. What must not differ is data: `parseLocale` first maps
|
|
429
|
+
deprecated language codes (`languageAliases`: `iw`→`he`, `ji`→`yi`,
|
|
430
|
+
`in`→`id`, `jw`→`jv`, `mo`→`ro`, `tl`→`fil`; Java/Android still emit
|
|
431
|
+
them); `weekStart(locale)` (CLDR firstDay by region; a bare language takes
|
|
432
|
+
`likelyRegion` = CLDR likelySubtags for every rtl language and ~90 common
|
|
433
|
+
ones, e.g. `ur`→PK = Sunday, `ps`→AF = Saturday, `dv`→MV = Friday;
|
|
434
|
+
default Monday), `textDirection(locale)` (`rtl` languages; the surface
|
|
435
|
+
root gets `direction: rtl` unless the author set one).
|
|
436
|
+
- **RTL glyphs** (`locale.json` `rtlMirroredIcons`, `mirrorsInRtl`): under
|
|
437
|
+
rtl a painter MIRRORS exactly these directional glyphs (back, chevron
|
|
438
|
+
left/right, arrow right, send, undo, external link — semantic and Lucide
|
|
439
|
+
names): `scaleX(-1)` as the OUTERMOST transform of the glyph, after the
|
|
440
|
+
node's own `transform` (CSS `transform: scaleX(-1) <own>`; SwiftUI
|
|
441
|
+
`.scaleEffect(x: -1)` outside `.rotationEffect`; Compose `graphicsLayer {
|
|
442
|
+
scaleX = -1 }` outside), so AppBar back, Pagination/Carousel/DatePicker
|
|
443
|
+
prev/next, Breadcrumb separators, NavRow/ListRow/Collapsible chevrons and
|
|
444
|
+
submenu indicators point the reading direction (a rotated Collapsible
|
|
445
|
+
chevron still points down when open). Media transport glyphs, up/down
|
|
446
|
+
chevrons and everything else are never mirrored. Flex rows already
|
|
447
|
+
mirror their ORDER (`direction: rtl`), so prev sits on the right AND
|
|
448
|
+
points right.
|
|
449
|
+
- **Built-in strings** (`catalog/strings.json`, 52 ids): the copy no author
|
|
450
|
+
writes (search placeholder, month buttons, copy, previous/next, close,
|
|
451
|
+
confirm/cancel, `{count} selected`, `pageOf`, `ratingStar` "{value} of
|
|
452
|
+
{max}", `ratingValue`, `stepDone`/`stepCurrent`/`stepUpcoming`,
|
|
453
|
+
`chartSummary`, `sparklineSummary`, `pagination`, `breadcrumb`, …). A
|
|
454
|
+
sentence is ONE id with placeholders, never concatenated fragments (the
|
|
455
|
+
fragment `of` is gone); a macro fills one through `$fill` → `fill`. The host overrides per surface
|
|
456
|
+
(`stringTable(overrides)`); `$string.<id>` in a prop default, a macro
|
|
457
|
+
template or an authored value resolves at bind time; `{name}` placeholders
|
|
458
|
+
fill via `formatString`. `validateProps` reports an unknown id.
|
|
459
|
+
Generated as `stringIds` / `stringDefaults` (×4).
|
|
460
|
+
|
|
461
|
+
## 5. Theming
|
|
462
|
+
|
|
463
|
+
- **Tokens added** (`catalog/tokens.json`, append-only): `color.chart6..8`;
|
|
464
|
+
`control.appBar`, `tabBar`, `toast`; `shadow.xl`; `breakpoint.{sm,md,lg,xl}`
|
|
465
|
+
(px); `ease.{standard,decelerate,accelerate}` (`[x1, y1, x2, y2]`);
|
|
466
|
+
`density.{compact,comfortable}` (multipliers 0.5..2).
|
|
467
|
+
- **Elevation** = the shadow ladder `none < sm < md < lg < xl` (flat, card,
|
|
468
|
+
popover, dialog, toast). Inside a layer paint order is tree order; layers
|
|
469
|
+
(base < overlay < toast) stack — there is no zIndex.
|
|
470
|
+
- **Modes**: `resolveMode(setting, systemPrefersDark)`; `system` follows the
|
|
471
|
+
platform live.
|
|
472
|
+
- **High contrast**: a theme may carry `contrast: {light?, dark?}` partial
|
|
473
|
+
colour/shadow overlays; `applyContrast(theme)` merges them over the modes
|
|
474
|
+
when the platform asks (or `contrast: high`). Neutral ships overlays
|
|
475
|
+
(foreground, mutedForeground, border, input, ring, card/popover
|
|
476
|
+
foregrounds); the others inherit them.
|
|
477
|
+
- **Density**: `applyDensity(theme, density)` scales `control` and
|
|
478
|
+
`spacing` by `$density.<name>` and rounds to whole px, once per surface;
|
|
479
|
+
radii, type and borders do not scale. Every resolver and fixture stays
|
|
480
|
+
density-agnostic.
|
|
481
|
+
- **Recipes**: keys add `borderTop/Right/Bottom/LeftWidth`, `borderStyle`,
|
|
482
|
+
`letterSpacing`, `textDecoration`, `textTransform`, `fontStyle`,
|
|
483
|
+
`transition` (`$motion.*`), `transitionEasing` (`$ease.*`), `transform`;
|
|
484
|
+
states add `invalid` (a field whose checks failed) and `dragover`.
|
|
485
|
+
Painter-supplied `when` discriminators: `CodeBlock/token.kind`,
|
|
486
|
+
`Table/headerCell|cell.align`, `Table/row.striped`. Template-built slot
|
|
487
|
+
parts (AlertDialog `footer`) count as parts.
|
|
488
|
+
- **Built-ins**: neutral has values for every new token and recipes for
|
|
489
|
+
every new component (motion on Button, Toggle, Tabs, Switch, rows, Pill;
|
|
490
|
+
the Collapsible chevron rotates 90° when open; FileUpload's dashed
|
|
491
|
+
drop zone; Stepper statuses; Rating stars; the CodeBlock palette on
|
|
492
|
+
chart6–8 + semantic hues); playful overrides several (overshoot easing,
|
|
493
|
+
pill toasts, primary chips); exponential takes its easings from
|
|
494
|
+
`packages/design-tokens` `motion.ease`, adds `shadow.xl`,
|
|
495
|
+
`control.appBar/tabBar/toast` (scripts/generate-themes.ts) and glass
|
|
496
|
+
recipes for the new fields, menus, toast, table, code block, drop zone,
|
|
497
|
+
sidebar and tab bar (`themes/exponential.recipes.json`). The theme builder edits breakpoints and
|
|
498
|
+
density; `diffTheme` keeps `contrast`.
|
|
499
|
+
- **Geometry**: `control-geometry.json` now also locks NumberField
|
|
500
|
+
(field), ChipInput (field, `minHeight`), TimePicker and DateRangePicker
|
|
501
|
+
(trigger).
|
|
502
|
+
|
|
503
|
+
## 6. Accessibility (`catalog/a11y.json`)
|
|
504
|
+
|
|
505
|
+
`roles` = the MACHINE-READABLE vocabulary (ARIA names plus `text` = static
|
|
506
|
+
text and `hidden` = out of the accessibility tree; generated `a11yRoles`).
|
|
507
|
+
Every component has a `role` (one of `roles`: its main accessible object),
|
|
508
|
+
`notes` (prose: composite parts, states, which strings name it) and `keys`
|
|
509
|
+
(82 entries; `round1.test.ts` gates it).
|
|
510
|
+
|
|
511
|
+
**Macro parts carry their semantics on the tree.** A template part's
|
|
512
|
+
`$a11y` (`catalog/macros.json`) is evaluated like props (bound inputs emit
|
|
513
|
+
calls) into the expanded node's `accessibility`: `{role, label,
|
|
514
|
+
description, level, expanded, current (page|step|false), selected, pressed,
|
|
515
|
+
checked, valueNow, valueMin, valueMax, hidden, autoFocus}`; the author's
|
|
516
|
+
`accessibility` on a macro wins over its root's. A painter maps them to the
|
|
517
|
+
platform (ARIA role/aria-*, SwiftUI traits/`accessibilityValue`, Compose
|
|
518
|
+
`semantics { role; stateDescription; heading(); selected }`, AccessKit) and
|
|
519
|
+
`autoFocus` = focus lands there when the enclosing dialog opens. Today:
|
|
520
|
+
Card/Group (group + title), Separator, ScrollArea (region), Band/Heading
|
|
521
|
+
(heading + level), RowList/Stepper (list; steps = listitem, current=step,
|
|
522
|
+
labelled `stepDone|Current|Upcoming`), Alert (alert|status), EmptyState
|
|
523
|
+
(status), Progress (progressbar + values), Meter, Collapsible trigger
|
|
524
|
+
(button + expanded), Pagination (navigation `pagination`, status label
|
|
525
|
+
`pageOf`), ButtonGroup (pressed), Breadcrumb (navigation `breadcrumb`,
|
|
526
|
+
current=page, separators hidden), TabBar (navigation; items button +
|
|
527
|
+
current=page), Rating (stars = buttons labelled `ratingStar`, the chosen one
|
|
528
|
+
pressed; read-only = img labelled `ratingValue`), AlertDialog (alertdialog,
|
|
529
|
+
cancel autoFocus), Sidebar (navigation + main), AppBar (banner, title
|
|
530
|
+
heading 1), NavRow (link + current), Pill (button + pressed when
|
|
531
|
+
pressable). Natives own theirs (`a11y.json`). Highlights: Tabs / Radio / ToggleGroup = roving tab stop + arrows
|
|
532
|
+
(wrap) + Home/End; Select = combobox + listbox, type-ahead, searchable keeps
|
|
533
|
+
focus in the field with aria-activedescendant; DropdownMenu / ContextMenu =
|
|
534
|
+
menu semantics with ArrowRight/ArrowLeft for submenus, Shift+F10 opens a
|
|
535
|
+
ContextMenu; Accordion headers; DatePicker grid = arrows ±1 day/week,
|
|
536
|
+
PageUp/PageDown month (Shift = year), Home/End by the LOCALE week start;
|
|
537
|
+
Tooltip shows on hover AND keyboard focus; Dialog focus trap + return;
|
|
538
|
+
AlertDialog focuses cancel; Toast never steals focus.
|
|
539
|
+
|
|
540
|
+
Rules (all components): keyboard reachability in tree order, focus ring
|
|
541
|
+
only for keyboard focus, labels/descriptions/errors linked (error id
|
|
542
|
+
`<node id>.error`), hidden nodes leave the a11y tree, icon-only controls
|
|
543
|
+
take `label`, `accessibility.label/description` override, reduced motion
|
|
544
|
+
zeroes transitions and stops shimmer.
|
|
545
|
+
|
|
546
|
+
Host → surface commands (`SurfaceCommand`): `focus {id}`, `announce {text,
|
|
547
|
+
live}`, `scrollIntoView {id}`. Live regions: `Text.live`, Toast, Form's
|
|
548
|
+
`invalidFields` announcement, CodeBlock's `copied`.
|
|
549
|
+
|
|
550
|
+
## 7. Prompt (`src/prompt.ts`)
|
|
551
|
+
|
|
552
|
+
New rules teach: `visible`, template `key`, responsive props (marked `^`)
|
|
553
|
+
with `{base, sm?, md?, lg?, xl?}`, the condition grammar with
|
|
554
|
+
`$breakpoint.*`, the three state blocks, `display: "none"`, Form and
|
|
555
|
+
Sidebar, leaving built-in copy unset. Compaction to stay under budget:
|
|
556
|
+
props that mean the same everywhere (`name`, `label`, `value`, `style`,
|
|
557
|
+
`checks`, …) are described ONCE (`COMMON_PROPS`), enum-first and `false`
|
|
558
|
+
defaults are implied, only shapes the listed components use are printed.
|
|
559
|
+
Full prompt 6451 tokens (budget 6500), lite 5205, terse 4752 for 81
|
|
560
|
+
components (was 5438 for 60).
|
|
561
|
+
|
|
562
|
+
## 8. Generated constants (Swift / Kotlin / Rust, `src/catalog.generated.ts`)
|
|
563
|
+
|
|
564
|
+
New rows: `styleLayoutEffectKeys`, `coreFunctionNames`,
|
|
565
|
+
`bindFunctionNames` (the whole bind table a renderer implements),
|
|
566
|
+
`stringIds`, `stringDefaults`, `codeLanguageNames`, `builtinIconSlots`,
|
|
567
|
+
`builtinIconNames`, `weekStartRegions` + `weekStartDays`,
|
|
568
|
+
`likelyRegionLanguages` + `likelyRegions`, `languageAliases` +
|
|
569
|
+
`languageAliasTargets`, `rtlLanguages`, `rtlMirroredIcons`, `a11yRoles`,
|
|
570
|
+
`rowSlotComponents`, `dataProps` (`Component.prop`). New scalars:
|
|
571
|
+
`styleTransformPattern`, `defaultLocale`, `weekStartDefault`.
|
|
572
|
+
`styleMediaPattern` changed (the new grammar), `styleStates` is
|
|
573
|
+
`[:hover, :focus-visible, :pressed]`, `functionNames` gained 15 names,
|
|
574
|
+
`tokenGroups` gained `breakpoint`, `ease`, `density`. The Rust file also
|
|
575
|
+
embeds `STRINGS_JSON`, `LOCALE_JSON`, `CODE_JSON`, `A11Y_JSON` beside the
|
|
576
|
+
existing catalog JSON, mirrored into
|
|
577
|
+
`apps/desktop/crates/exponential-ui/src/generated/catalog.rs`. Themes
|
|
578
|
+
generated outputs carry the new token groups and `contrast`.
|
|
579
|
+
|
|
580
|
+
## 9. Per-renderer checklists
|
|
581
|
+
|
|
582
|
+
### React (`@exponential-at/ui-react`, the reference renderer)
|
|
583
|
+
1. Media: map every condition (`min/max-width|height` → `@container xui (width >= N)` / `(width < N)` with `$breakpoint` resolved from the theme; orientation via the container's aspect: emit both `(orientation: …)` container queries; `hover`/`prefers-reduced-motion` → real `@media`). States `:hover` → `:hover` (and `[data-xs~=hover]`), `:focus-visible` → `:focus-visible`, `:pressed` → `:active, [data-xs~=pressed]`, emitted after the media rules in that order.
|
|
584
|
+
2. Bind pass: run `bindTree` semantics in the binding layer (props along the schema with DATA props verbatim, styles, recipe props, `accessibility`, `$string`), drop falsy `visible`, honour template `key` as the React key; Table slot cells per row (`bindRowSlot`); map `accessibility` (role, states, `autoFocus`) to ARIA; mirror `rtlMirroredIcons` under rtl.
|
|
585
|
+
3. Actions: `runAction` order; implement `set` against the surface data model.
|
|
586
|
+
4. Natives: Form, NumberField, ChipInput, DateRangePicker, TimePicker, FileUpload (host `onUpload`), CodeBlock (`tokenizeCode` from this package), ContextMenu, Toast (layer + queue), Table (native, windowing ≥ 50), Chart kinds + `niceTicks`; Image fallback/focal point (`object-position`), Drawer flags, Textarea autosize, Markdown lines, Text live, Popover openOn, Select search.
|
|
587
|
+
5. New style keys via `styleToCss`; motion: `transition` + `transitionEasing`; reduced motion → `transition-duration: 0ms`.
|
|
588
|
+
6. Theme: `resolveMode('system')` via `matchMedia`, `applyDensity`, `applyContrast` (`prefers-contrast: more`), new recipe keys/states (`invalid`, `dragover`).
|
|
589
|
+
7. Built-in strings + glyphs from the generated tables; `textDirection` for the root.
|
|
590
|
+
|
|
591
|
+
### Rust core (`apps/desktop/crates/exponential-ui`)
|
|
592
|
+
1. `expr.rs`: dynamic inputs emit calls exactly as `src/expr.ts` (member vs operand rule, `fallback`, `concat`, `$lt`, `$sub`, `$not`, `$text` over a value object, `range(...)`, key interpolation); `TemplateChild` gains `"$slot:name"` splices and `slots` values that are template nodes (AlertDialog) — `MacroTemplate.slots` becomes `IndexMap<String, SlotRef>` (string | node), plus `$set`.
|
|
593
|
+
2. `macros.rs`: `$if`/`$any` → `visible` (and/or), `$set` → `functionCall: set` (after `event`; an author FUNCTION wins and is reported), `$a11y` → `accessibility` (author label/description merged over the root's), responsive variants → media blocks, author `visible`/`accessibility` onto the root; `expr.rs`: `$clamp`, `$fill` (always emits), `$eq` numeric-string equality. Replay `catalog-macros.json` (189 cases incl. `bound:*`/`responsive:*`) and `bind-time.json` (`cases` × 3 datasets AND `extra` with `issues` + `rowSlots`).
|
|
594
|
+
3. `reducer.rs`: `visible` (validate: boolean or dynamic), template `key`, `accessibility` on the flat path, slot names checked (`*` = any).
|
|
595
|
+
4. `validate.rs`: responsive values, objects without shape, `$string.` ids.
|
|
596
|
+
5. `style_check.rs`: the new grammar (`STYLE_MEDIA_PATTERN`), states, unknown breakpoint tokens, `transform`/`gradient` value types, the new keys; `resolveConditions` port + `style-conditions.json` replay. Layout: per-side border widths into taffy's border rect, `overflowX/Y`, `insetBlockStart/End`, `visibility: hidden` keeps the box.
|
|
597
|
+
6. `data.rs`: the WHOLE bind table (`bindFunctionNames`: core + `clamp`/`fill` + and/or/not/required; `eq` numeric-string equality), bind pass dropping falsy `visible`, props along the schema (`dataProps` verbatim), `accessibility` resolved, row-scoped slots left unbound + `bind_row_slot` (scope `item` for literal rows), `$string` resolution through the surface table (never inside data); `reducer.rs`: `visible` on the BASIC path too, the `$set` collision issue.
|
|
598
|
+
7. `theme.rs`: token groups `breakpoint`/`ease` (tuples)/`density`, `contrast`, `apply_density`, `apply_contrast`, `resolve_mode`, recipe keys/states, `resolveConditionKey`. Replay `theme-*.json`, `control-geometry.json`.
|
|
599
|
+
8. New modules to port (pure): `code.rs` (tokenizer, replay `code-tokens.json`), `locale.rs` (+ `languageAliases`, `rtlMirroredIcons`), `chart.rs` (`chart_extent` with loops and split positive/negative stacks, `nice_ticks`, the summary params), strings table. The embedded JSON constants are already generated.
|
|
600
|
+
9. FFI: expose SurfaceSettings, `SurfaceCommand`, the action outcome (`set` writes) so the natives do not re-implement the bind pass.
|
|
601
|
+
|
|
602
|
+
### gpui painter (`exponential-ui-gpui`)
|
|
603
|
+
1. Paint the new keys: per-side border widths with ONE colour, `borderStyle` dashed/dotted (custom path), per-corner radii (`corner_radii`), `backgroundGradient` (gpui linear gradient, two stops; more stops: chain quads), `transform` at paint time only (no layout), `visibility`, `opacity` unchanged, `letterSpacing`/`textTransform`/`textDecoration`/`fontStyle`/weights 300–800 in text runs, `cursor` → `CursorStyle`, `pointerEvents: none` skips hit-testing.
|
|
604
|
+
2. Motion: animate colour/opacity/transform/size between resolved styles with `transition` ms + `transitionEasing` bezier; 0 ms when the OS asks for reduced motion.
|
|
605
|
+
3. Conditions: the core flattens with the surface size, hover capability (true on desktop), reduced motion, and the node's states (`hover`, `focus-visible` = focus from keyboard, `pressed`).
|
|
606
|
+
4. Natives: the round-1 list above, menus with submenus, Toast layer, Table with windowing, CodeBlock with the core tokenizer, Chart with `nice_ticks` and the chart palette, FileUpload drop (gpui file drop events) + host callback.
|
|
607
|
+
5. Keyboard per `a11y.json`; AccessKit roles from `accessibility.role`/states where available; mirror `rtlMirroredIcons` under rtl.
|
|
608
|
+
|
|
609
|
+
### SwiftUI (iOS)
|
|
610
|
+
1. Conditions: surface size from `GeometryReader`/`onGeometryChange`, `horizontalSizeClass` is NOT the rule (use px vs `$breakpoint`), hover = iPadOS pointer (`.onHover` available), reduced motion = `accessibilityReduceMotion`.
|
|
611
|
+
2. Keys: per-side widths via an overlay `Path` (one colour), dashed via `StrokeStyle(dash:)`, per-corner radii via `UnevenRoundedRectangle`, gradients via `LinearGradient` (angle → unit points), `transform` via `.offset/.scaleEffect/.rotationEffect` (paint-only by construction), `visibility: hidden` = `.opacity(0)` + `.accessibilityHidden`, `pointerEvents: none` = `.allowsHitTesting(false)`, `cursor` ignored (pointer: `.pointerStyle` on iPadOS 17+ optional).
|
|
612
|
+
3. Motion: `.animation(.timingCurve(x1, y1, x2, y2, duration:), value:)`.
|
|
613
|
+
4. Natives: Form, NumberField (Stepper + TextField with `.number` format in the surface locale), ChipInput, DateRangePicker (`MultiDatePicker` is not a range — custom grid), TimePicker (list/menu), FileUpload (`fileImporter` + drop), CodeBlock (core tokenizer via FFI or a Swift port), ContextMenu (`.contextMenu`), Toast (overlay layer), Table (LazyVStack windowing), Charts (Swift Charts with `niceTicks` values).
|
|
614
|
+
5. Locale: `Locale(identifier:)` for formatters; week start from `catalog/locale.json` data (do NOT trust `Calendar.current`), direction from `rtlLanguages`.
|
|
615
|
+
6. VoiceOver per `a11y.json` and each node's `accessibility` (`accessibilityAddTraits` from role/states, `.isHeader` + level, `accessibilityValue` for progress, `autoFocus` via `@AccessibilityFocusState`; adjustable actions for Slider/NumberField), `AccessibilityNotification.Announcement` for `announce`, `@AccessibilityFocusState` for `focus`; `.flipsForRightToLeftLayoutDirection(true)` only on `rtlMirroredIcons`.
|
|
616
|
+
|
|
617
|
+
### Compose (Android)
|
|
618
|
+
1. Conditions: `BoxWithConstraints` / `onSizeChanged` on the SURFACE, hover = `LocalInputModeManager` / mouse presence, reduced motion = `Settings.Global.ANIMATOR_DURATION_SCALE == 0`.
|
|
619
|
+
2. Keys: per-side widths + dashes via `drawBehind` (`PathEffect.dashPathEffect`), per-corner radii `RoundedCornerShape(topStart…)`, gradients `Brush.linearGradient` (angle → offsets in the size), `transform` via `graphicsLayer` (paint-only), `visibility: hidden` = `alpha 0` + `clearAndSetSemantics {}`, `pointerEvents: none` = no pointer input, `cursor` = `pointerHoverIcon`.
|
|
620
|
+
3. Motion: `animate*AsState(tween(ms, easing = CubicBezierEasing(x1, y1, x2, y2)))`.
|
|
621
|
+
4. Natives as for SwiftUI (Material 3 `DateRangePicker`, `TimePicker`-like list, `ExposedDropdownMenuBox` for the searchable Select, `LazyColumn` for Table windowing, `DropdownMenu` + long-press for ContextMenu, `SnackbarHost` semantics for the toast layer).
|
|
622
|
+
5. Locale: `java.text`/`android.icu` formatters in the surface locale; week start from the generated table; `LayoutDirection` from `rtlLanguages`.
|
|
623
|
+
6. TalkBack per `a11y.json` and each node's `accessibility` (`semantics { role; stateDescription; heading(); selected; liveRegion }`), `View.announceForAccessibility` for `announce`, `FocusRequester` for `focus` and `autoFocus`; mirror only `rtlMirroredIcons` under `LayoutDirection.Rtl`.
|
|
624
|
+
|
|
625
|
+
## 10. Breaking changes and migration
|
|
626
|
+
|
|
627
|
+
- **Table** is a NATIVE with object columns/rows. Old surfaces
|
|
628
|
+
(`columns: ["Name"]`, `rows: [["Alice"]]`) fail validation; migrate to
|
|
629
|
+
`columns: [{key: "name", label: "Name"}]`, `rows: [{name: "Alice"}]`.
|
|
630
|
+
- **Sparkline** is a core macro: an extension may no longer define one
|
|
631
|
+
(the example extension's native is now `TrendLine`).
|
|
632
|
+
- **Style**: `":hover"` and `":focus-visible"` are valid (were refused);
|
|
633
|
+
`@media (max-width: N)` means `< N`.
|
|
634
|
+
- **Functions**: `functionNames` = the basic 14, then the 15 core names
|
|
635
|
+
(renderers must implement them; `set` is an action); `bindFunctionNames`
|
|
636
|
+
is what a bind-time resolver needs (adds and/or/not/required). `eq` now
|
|
637
|
+
equates a number with a string spelling it.
|
|
638
|
+
- **Bind pass**: Table slot cells are no longer bound against the surface
|
|
639
|
+
(the pass leaves them for per-row binding); literal Table `rows` are never
|
|
640
|
+
interpreted; `accessibility` resolves. `bind-time.json` datasets are 3 per
|
|
641
|
+
case and the file gains `extra`.
|
|
642
|
+
- **Expanded tree**: macro parts carry `accessibility` (from `$a11y`);
|
|
643
|
+
Pagination's prev/next use `lt` (disabled at the ends for any page beyond
|
|
644
|
+
them) and write `clamp`ed pages.
|
|
645
|
+
- **a11y.json**: `role` is now machine-readable (prose moved to `notes`),
|
|
646
|
+
plus `roles`.
|
|
647
|
+
- **strings.json**: `of` removed; 9 ids added. **locale.json**:
|
|
648
|
+
`languageAliases`, `rtlMirroredIcons`, ~90 likely regions.
|
|
649
|
+
- **Actions** may carry `event` AND `functionCall` (only the expander emits
|
|
650
|
+
both today).
|
|
651
|
+
- **UiNode** may carry `visible`; style values may be dynamic after
|
|
652
|
+
expansion; `template.key`; `accessibility` from the flat path.
|
|
653
|
+
- **Recipes**: `recipeParts()` lists 82 components; neutral recipes for
|
|
654
|
+
Table moved to the native's parts.
|
|
655
|
+
- **Rust**: `macros.json` now has `slots` values that are objects and
|
|
656
|
+
`$set` keys — `MacroTemplate` must accept them before the crate can load
|
|
657
|
+
the embedded table. Checked here: `cargo check -p exponential-ui --lib`
|
|
658
|
+
compiles against the regenerated mirror; `tests/generated_drift.rs`
|
|
659
|
+
still asserts 61 components (now 82) and `style_check.rs` the old media
|
|
660
|
+
pattern — both are the Rust lane's to update.
|
|
661
|
+
|
|
662
|
+
## 11. Deferred (not in round 1)
|
|
663
|
+
|
|
664
|
+
- Per-side border COLOURS (gpui limit) and `zIndex` — kept out on purpose.
|
|
665
|
+
- A DnD reorder contract for List/Table rows, column resizing and pinning.
|
|
666
|
+
- Chart: stacked area, axis formatters per locale beyond `formatNumber`,
|
|
667
|
+
log scales. Smallest next step: a `format` prop taking a basic-catalog
|
|
668
|
+
format call.
|
|
669
|
+
- Form: async (server) validation round trip; smallest next step: an
|
|
670
|
+
`errors` bindable prop on Form the host fills after `submit`.
|
|
671
|
+
- Combobox creatable entries (Select `allowCreate`).
|
|
672
|
+
- Locale-aware string pluralization of the built-in strings (only `{name}`
|
|
673
|
+
placeholders today).
|
|
674
|
+
- Sidebar's own narrow-screen navigation (a built-in drawer): one slot
|
|
675
|
+
cannot render in two places without duplicating ids and state. Smallest
|
|
676
|
+
next step: a native `Sidebar` (or a `Drawer` mode that renders inline
|
|
677
|
+
above a breakpoint) so ONE subtree moves between the column and a sheet.
|
|
678
|
+
- Multiple functions per Action (an author function AND the `$set` write):
|
|
679
|
+
needs an Action shape beyond A2UI's single `functionCall`; reported instead.
|