@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.
Files changed (251) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +54 -0
  3. package/README.md +360 -2
  4. package/catalog/a11y.json +668 -0
  5. package/catalog/basic-map.json +104 -0
  6. package/catalog/code.json +58 -0
  7. package/catalog/core.catalog.json +4752 -0
  8. package/catalog/core.schema.json +22667 -0
  9. package/catalog/host.json +64 -0
  10. package/catalog/layout.json +20 -0
  11. package/catalog/limits.json +9 -0
  12. package/catalog/locale.json +48 -0
  13. package/catalog/macros.json +3417 -0
  14. package/catalog/recipes.json +61 -0
  15. package/catalog/strings.json +72 -0
  16. package/catalog/style.json +128 -0
  17. package/catalog/theme.schema.json +42543 -0
  18. package/catalog/tokens.json +47 -0
  19. package/conformance/README.md +217 -0
  20. package/conformance/baseline.ts +101 -0
  21. package/conformance/browser-page.ts +146 -0
  22. package/conformance/browser-suites.ts +683 -0
  23. package/conformance/compare.ts +201 -0
  24. package/conformance/dom.ts +96 -0
  25. package/conformance/dump.ts +163 -0
  26. package/conformance/fonts.json +154 -0
  27. package/conformance/manifest.json +224 -0
  28. package/conformance/parity-compare.ts +191 -0
  29. package/conformance/parity-report.schema.json +73 -0
  30. package/conformance/parity.ts +412 -0
  31. package/conformance/pixel.ts +177 -0
  32. package/conformance/primitives-page.ts +49 -0
  33. package/conformance/report.schema.json +66 -0
  34. package/conformance/run.ts +118 -0
  35. package/dist/chunks/index-98npjvsd.js +3940 -0
  36. package/dist/chunks/index-98npjvsd.js.map +27 -0
  37. package/dist/chunks/index-gwkt14w3.js +7314 -0
  38. package/dist/chunks/index-gwkt14w3.js.map +10 -0
  39. package/dist/chunks/index-njt7yzzm.js +134 -0
  40. package/dist/chunks/index-njt7yzzm.js.map +9 -0
  41. package/dist/chunks/index-we7va6v6.js +7984 -0
  42. package/dist/chunks/index-we7va6v6.js.map +15 -0
  43. package/dist/chunks/index-xrm6f7wc.js +2563 -0
  44. package/dist/chunks/index-xrm6f7wc.js.map +10 -0
  45. package/dist/conformance.js +182749 -0
  46. package/dist/conformance.js.map +10 -0
  47. package/dist/entries/prompt.js +251 -0
  48. package/dist/entries/prompt.js.map +11 -0
  49. package/dist/entries/schema.js +1725 -0
  50. package/dist/entries/schema.js.map +11 -0
  51. package/dist/host/index.js +122 -0
  52. package/dist/host/index.js.map +9 -0
  53. package/dist/index.js +3213 -0
  54. package/dist/index.js.map +27 -0
  55. package/dist/types/src/a11y.d.ts +21 -0
  56. package/dist/types/src/animation.d.ts +76 -0
  57. package/dist/types/src/basic-map.d.ts +38 -0
  58. package/dist/types/src/builder.d.ts +50 -0
  59. package/dist/types/src/catalog.d.ts +54 -0
  60. package/dist/types/src/catalog.described.d.ts +33 -0
  61. package/dist/types/src/catalog.generated.d.ts +829 -0
  62. package/dist/types/src/chart.d.ts +46 -0
  63. package/dist/types/src/code.d.ts +44 -0
  64. package/dist/types/src/color.d.ts +20 -0
  65. package/dist/types/src/conformance.d.ts +39 -0
  66. package/dist/types/src/connector/app.d.ts +14 -0
  67. package/dist/types/src/connector/connector.d.ts +77 -0
  68. package/dist/types/src/connector/index.d.ts +4 -0
  69. package/dist/types/src/css.d.ts +29 -0
  70. package/dist/types/src/define.d.ts +66 -0
  71. package/dist/types/src/direction.d.ts +16 -0
  72. package/dist/types/src/dynamic.d.ts +159 -0
  73. package/dist/types/src/entries/prompt.d.ts +11 -0
  74. package/dist/types/src/entries/schema.d.ts +8 -0
  75. package/dist/types/src/expr.d.ts +71 -0
  76. package/dist/types/src/extension.d.ts +4 -0
  77. package/dist/types/src/format.d.ts +112 -0
  78. package/dist/types/src/geometry.d.ts +40 -0
  79. package/dist/types/src/host/contract.d.ts +188 -0
  80. package/dist/types/src/host/decoders.d.ts +61 -0
  81. package/dist/types/src/host/index.d.ts +11 -0
  82. package/dist/types/src/host/package.d.ts +58 -0
  83. package/dist/types/src/host/policy.d.ts +105 -0
  84. package/dist/types/src/host/router.d.ts +34 -0
  85. package/dist/types/src/host/runtime.d.ts +205 -0
  86. package/dist/types/src/host/sources.d.ts +15 -0
  87. package/dist/types/src/host/transports.d.ts +109 -0
  88. package/dist/types/src/index.d.ts +63 -0
  89. package/dist/types/src/layout.d.ts +20 -0
  90. package/dist/types/src/limits.d.ts +21 -0
  91. package/dist/types/src/list.d.ts +133 -0
  92. package/dist/types/src/locale.d.ts +22 -0
  93. package/dist/types/src/macros.d.ts +29 -0
  94. package/dist/types/src/overlay.d.ts +27 -0
  95. package/dist/types/src/prompt.d.ts +44 -0
  96. package/dist/types/src/recipes.d.ts +17 -0
  97. package/dist/types/src/reducer.d.ts +41 -0
  98. package/dist/types/src/resizable.d.ts +38 -0
  99. package/dist/types/src/schema.d.ts +7 -0
  100. package/dist/types/src/strings.d.ts +13 -0
  101. package/dist/types/src/style.d.ts +201 -0
  102. package/dist/types/src/test-preload.d.ts +1 -0
  103. package/dist/types/src/text-shimmer.d.ts +24 -0
  104. package/dist/types/src/theme-types.d.ts +162 -0
  105. package/dist/types/src/theme.d.ts +73 -0
  106. package/dist/types/src/themes.d.ts +22 -0
  107. package/dist/types/src/tree-guides.d.ts +17 -0
  108. package/dist/types/src/types.d.ts +314 -0
  109. package/dist/types/src/validate.d.ts +31 -0
  110. package/dist/types/src/web/base-css.d.ts +1 -0
  111. package/dist/types/src/web/box-css.d.ts +55 -0
  112. package/dist/types/src/web/coerce.d.ts +32 -0
  113. package/dist/types/src/web/data.d.ts +105 -0
  114. package/dist/types/src/web/dates.d.ts +12 -0
  115. package/dist/types/src/web/form.d.ts +44 -0
  116. package/dist/types/src/web/host-types.d.ts +119 -0
  117. package/dist/types/src/web/index.d.ts +28 -0
  118. package/dist/types/src/web/inputs.d.ts +55 -0
  119. package/dist/types/src/web/list.d.ts +13 -0
  120. package/dist/types/src/web/listbox.d.ts +14 -0
  121. package/dist/types/src/web/markdown.d.ts +96 -0
  122. package/dist/types/src/web/media.d.ts +62 -0
  123. package/dist/types/src/web/parts.d.ts +4 -0
  124. package/dist/types/src/web/primitives/alert-dialog.d.ts +10 -0
  125. package/dist/types/src/web/primitives/alert.d.ts +9 -0
  126. package/dist/types/src/web/primitives/avatar-color.d.ts +2 -0
  127. package/dist/types/src/web/primitives/avatar.d.ts +13 -0
  128. package/dist/types/src/web/primitives/badge.d.ts +10 -0
  129. package/dist/types/src/web/primitives/breadcrumb.d.ts +10 -0
  130. package/dist/types/src/web/primitives/button.d.ts +6 -0
  131. package/dist/types/src/web/primitives/calendar.d.ts +43 -0
  132. package/dist/types/src/web/primitives/card.d.ts +9 -0
  133. package/dist/types/src/web/primitives/checkbox.d.ts +5 -0
  134. package/dist/types/src/web/primitives/choice-tiles.d.ts +32 -0
  135. package/dist/types/src/web/primitives/cn.d.ts +3 -0
  136. package/dist/types/src/web/primitives/code-block.d.ts +24 -0
  137. package/dist/types/src/web/primitives/command.d.ts +134 -0
  138. package/dist/types/src/web/primitives/context-ring.d.ts +24 -0
  139. package/dist/types/src/web/primitives/dialog-arms.d.ts +6 -0
  140. package/dist/types/src/web/primitives/dialog.d.ts +24 -0
  141. package/dist/types/src/web/primitives/disclosure-header.d.ts +5 -0
  142. package/dist/types/src/web/primitives/dock.d.ts +42 -0
  143. package/dist/types/src/web/primitives/dropdown-menu.d.ts +14 -0
  144. package/dist/types/src/web/primitives/empty-state.d.ts +15 -0
  145. package/dist/types/src/web/primitives/glass-card.d.ts +4 -0
  146. package/dist/types/src/web/primitives/hover-card.d.ts +7 -0
  147. package/dist/types/src/web/primitives/icon-disc.d.ts +12 -0
  148. package/dist/types/src/web/primitives/index.d.ts +48 -0
  149. package/dist/types/src/web/primitives/input.d.ts +5 -0
  150. package/dist/types/src/web/primitives/kbd.d.ts +4 -0
  151. package/dist/types/src/web/primitives/label.d.ts +3 -0
  152. package/dist/types/src/web/primitives/menu-surface.d.ts +17 -0
  153. package/dist/types/src/web/primitives/meter.d.ts +21 -0
  154. package/dist/types/src/web/primitives/popover.d.ts +3 -0
  155. package/dist/types/src/web/primitives/progress-ring.d.ts +5 -0
  156. package/dist/types/src/web/primitives/progress.d.ts +8 -0
  157. package/dist/types/src/web/primitives/scroll-area.d.ts +49 -0
  158. package/dist/types/src/web/primitives/segmented-control.d.ts +5 -0
  159. package/dist/types/src/web/primitives/select.d.ts +47 -0
  160. package/dist/types/src/web/primitives/separator.d.ts +3 -0
  161. package/dist/types/src/web/primitives/sheet-chrome.d.ts +4 -0
  162. package/dist/types/src/web/primitives/sheet.d.ts +20 -0
  163. package/dist/types/src/web/primitives/skeleton.d.ts +3 -0
  164. package/dist/types/src/web/primitives/spinner.d.ts +3 -0
  165. package/dist/types/src/web/primitives/switch.d.ts +4 -0
  166. package/dist/types/src/web/primitives/table.d.ts +11 -0
  167. package/dist/types/src/web/primitives/tabs.d.ts +13 -0
  168. package/dist/types/src/web/primitives/textarea.d.ts +3 -0
  169. package/dist/types/src/web/primitives/toast.d.ts +163 -0
  170. package/dist/types/src/web/primitives/tooltip.d.ts +3 -0
  171. package/dist/types/src/web/primitives/truncate.d.ts +7 -0
  172. package/dist/types/src/web/primitives/typeahead-portal.d.ts +12 -0
  173. package/dist/types/src/web/share.d.ts +6 -0
  174. package/dist/types/src/web/table.d.ts +20 -0
  175. package/dist/types/src/web/theme-css.d.ts +69 -0
  176. package/dist/types/src/web/urls.d.ts +21 -0
  177. package/dist/web/index.js +2303 -0
  178. package/dist/web/index.js.map +26 -0
  179. package/dist/web/primitives/index.js +1450 -0
  180. package/dist/web/primitives/index.js.map +57 -0
  181. package/docs/components.generated.json +6238 -0
  182. package/docs/round-1-contract.md +679 -0
  183. package/docs/round-2-contract.md +476 -0
  184. package/docs/round-3-contract.md +174 -0
  185. package/docs/themes.generated.json +3503 -0
  186. package/fixtures/animations.json +2890 -0
  187. package/fixtures/bench-list.json +58 -0
  188. package/fixtures/bind-time.json +30664 -0
  189. package/fixtures/catalog-basic-map.json +1692 -0
  190. package/fixtures/catalog-components.json +9323 -0
  191. package/fixtures/catalog-extension.json +620 -0
  192. package/fixtures/catalog-macros.json +36370 -0
  193. package/fixtures/code-tokens.json +1993 -0
  194. package/fixtures/conformance-baseline.json +55544 -0
  195. package/fixtures/conformance-cases.json +49 -0
  196. package/fixtures/conformance-known.json +2123 -0
  197. package/fixtures/control-geometry.json +4200 -0
  198. package/fixtures/dashboard.data.json +74 -0
  199. package/fixtures/dashboard.json +752 -0
  200. package/fixtures/demo-surface.json +47 -0
  201. package/fixtures/format.json +870 -0
  202. package/fixtures/host-policy.json +1067 -0
  203. package/fixtures/host-router.json +1152 -0
  204. package/fixtures/host-transport.json +590 -0
  205. package/fixtures/interactions-round1.json +773 -0
  206. package/fixtures/kitchen-sink.expanded.json +10844 -0
  207. package/fixtures/kitchen-sink.json +3049 -0
  208. package/fixtures/layout-geometry-round1.json +7278 -0
  209. package/fixtures/layout-geometry.json +2072 -0
  210. package/fixtures/overlay-geometry.json +272 -0
  211. package/fixtures/primitive-specimens.json +58 -0
  212. package/fixtures/prompt-budget.json +33 -0
  213. package/fixtures/resizable.json +714 -0
  214. package/fixtures/round4-contract.json +2985 -0
  215. package/fixtures/specimens.json +9832 -0
  216. package/fixtures/style-conditions.json +336 -0
  217. package/fixtures/template-items.json +2215 -0
  218. package/fixtures/text-direction.json +309 -0
  219. package/fixtures/text-shimmer.json +96 -0
  220. package/fixtures/theme-extends.json +573 -0
  221. package/fixtures/theme-invalid.json +1296 -0
  222. package/fixtures/theme-recipes.json +58600 -0
  223. package/fixtures/theme-resolved.json +23949 -0
  224. package/fixtures/toast-stack.json +215 -0
  225. package/fixtures/tree-guides.json +224 -0
  226. package/fixtures/virtual-list.json +733 -0
  227. package/generated/ExponentialUICatalog.generated.kt +67 -0
  228. package/generated/ExponentialUICatalog.generated.swift +67 -0
  229. package/generated/ExponentialUIThemes.generated.kt +23 -0
  230. package/generated/ExponentialUIThemes.generated.swift +23 -0
  231. package/generated/catalog.descriptions.generated.rs +6 -0
  232. package/generated/catalog.generated.rs +80 -0
  233. package/generated/catalog.themes.generated.rs +34 -0
  234. package/generated/core.descriptions.json +2505 -0
  235. package/generated/core.runtime.json +3609 -0
  236. package/package.json +87 -4
  237. package/themes/exponential.recipes.json +49 -0
  238. package/themes/exponential.theme.json +755 -0
  239. package/themes/neutral.theme.json +5676 -0
  240. package/themes/orange.theme.json +43 -0
  241. package/themes/playful.theme.json +219 -0
  242. package/vendor/a2ui/LICENSE.txt +203 -0
  243. package/vendor/a2ui/README.md +27 -0
  244. package/vendor/a2ui/v0_9/basic_catalog.json +1387 -0
  245. package/vendor/a2ui/v0_9/basic_catalog_rules.txt +5 -0
  246. package/vendor/a2ui/v0_9/client_capabilities.json +97 -0
  247. package/vendor/a2ui/v0_9/client_to_server.json +104 -0
  248. package/vendor/a2ui/v0_9/common_types.json +315 -0
  249. package/vendor/a2ui/v0_9/server_to_client.json +132 -0
  250. package/vendor/json-render/LICENSE.txt +201 -0
  251. 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.