@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,47 @@
1
+ {
2
+ "$comment": "VAPP-85: the token NAMES the core catalog's macros and Box styles reference as `$<group>.<name>` (e.g. `$color.primary`, `$spacing.md`). VALUES come from a theme (VAPP-92); this file only fixes the vocabulary. `color` = the shadcn semantic set plus success/warning/info and the eight chart hues (round 1 added chart6..8); `spacing`/`radius`/`type` mirror packages/design-tokens' scales by NAME (xl2 because 2xl is not an identifier in Swift/Kotlin/Rust); `control` = the per-kind control heights the VAPP-4 spike asked for instead of px (round 1 adds `appBar`, `tabBar`, `toast`); `shadow` = the boxShadow presets, ALSO the elevation scale (none < sm < md < lg < xl: flat, card, popover, dialog, toast; paint order inside a layer stays tree order, overlays stack by layer, never by zIndex); `opacity` = the text-emphasis ladder; VAPP-92 adds `border` (the border-width ladder, px) and `motion` (durations, ms). Round 1 (docs/round-1-contract.md) adds `breakpoint` (px, the ONLY tokens a style CONDITION may reference: `\"@media (min-width: $breakpoint.md)\"`; resolved against the theme before matching), `ease` (cubic-bezier control points [x1, y1, x2, y2], referenced by `transitionEasing`) and `density` (multipliers over `control` and `spacing` for the surface density `compact`/`comfortable`; `default` = 1, never a token). Round 2 (docs/round-2-contract.md §2) adds `blur` (px radii, the ONLY values `backdropBlur` takes). EXP-1259 (EXP-1243, packages/design-tokens motion.scale) adds `scale` (unitless multipliers the PAINTERS read, never a style value): `press` = what a pressed button scales to, `popoverFrom` = what a popover or menu scales FROM while it fades in (never from 0); reduce motion = no scale. Every group is append-only: a theme is validated against these names.",
3
+ "color": [
4
+ "background", "foreground",
5
+ "card", "cardForeground",
6
+ "popover", "popoverForeground",
7
+ "primary", "primaryForeground",
8
+ "secondary", "secondaryForeground",
9
+ "muted", "mutedForeground",
10
+ "accent", "accentForeground",
11
+ "destructive", "destructiveForeground",
12
+ "success", "successForeground",
13
+ "warning", "warningForeground",
14
+ "info", "infoForeground",
15
+ "border", "input", "ring",
16
+ "chart1", "chart2", "chart3", "chart4", "chart5", "chart6", "chart7", "chart8"
17
+ ],
18
+ "spacing": ["none", "xxs", "xs", "sm", "md", "lg", "xl", "xl2", "xl3"],
19
+ "radius": ["none", "sm", "md", "lg", "xl", "xl2", "xl3", "full"],
20
+ "type": {
21
+ "size": ["xs", "sm", "base", "lg", "xl", "xl2", "xl3"],
22
+ "lineHeight": ["xs", "sm", "base", "lg", "xl", "xl2", "xl3"],
23
+ "weight": ["regular", "medium", "semibold", "bold"],
24
+ "family": ["sans", "mono"]
25
+ },
26
+ "control": [
27
+ "buttonSm", "buttonMd", "buttonLg", "buttonIcon",
28
+ "input", "textarea",
29
+ "row", "rowCompact",
30
+ "pill", "badge", "chip",
31
+ "toggle", "switch", "checkbox", "slider",
32
+ "progress", "meter",
33
+ "avatarSm", "avatarMd", "avatarLg",
34
+ "iconSm", "iconMd", "iconLg",
35
+ "hairline",
36
+ "appBar", "tabBar", "toast"
37
+ ],
38
+ "shadow": ["none", "sm", "md", "lg", "xl"],
39
+ "opacity": ["primary", "secondary", "tertiary", "quaternary", "disabled"],
40
+ "border": ["none", "hairline", "thick"],
41
+ "motion": ["fast", "standard", "slow"],
42
+ "breakpoint": ["sm", "md", "lg", "xl"],
43
+ "ease": ["standard", "decelerate", "accelerate"],
44
+ "density": ["compact", "comfortable"],
45
+ "blur": ["sm", "md", "lg", "xl"],
46
+ "scale": ["press", "popoverFrom"]
47
+ }
@@ -0,0 +1,217 @@
1
+ # Exponential UI conformance
2
+
3
+ A renderer is **Exponential UI conformant** when its runner replays every
4
+ suite of [`manifest.json`](manifest.json) (GENERATED from the fixtures,
5
+ drift-gated) and the report passes:
6
+
7
+ ```bash
8
+ bun run --filter @exponential-at/ui conformance:check path/to/report.json
9
+ # CONFORMANT my-renderer (flutter 0.3.0): <n> cases passed in <m> suites
10
+ ```
11
+
12
+ Every suite present, every case run (the manifest's `cases` count), nothing
13
+ failed or skipped. The fixtures under `../fixtures/` are the contract; the
14
+ TS reference (`../src/`) generated their `expected` values and the Rust core
15
+ replays them byte for byte.
16
+
17
+ ## The suites
18
+
19
+ | suite | fixture | a runner does, per case |
20
+ |---|---|---|
21
+ | `catalog` | `catalog-components.json` | reduce the nested node (no issues), PAINT it: no crash, no `Unknown` |
22
+ | `macros` | `catalog-macros.json` | expand: the tree equals `expanded` |
23
+ | `basic-map` | `catalog-basic-map.json` | reduce the A2UI basic surface: tree + issues equal |
24
+ | `extension` | `catalog-extension.json` | register the extension, reduce: tree + issues equal |
25
+ | `theme-resolved` | `theme-resolved.json` | load each built-in theme: equal |
26
+ | `theme-recipes` | `theme-recipes.json` | resolve the part recipe per theme × mode × state: equal |
27
+ | `theme-extends` | `theme-extends.json` | load over the parents: chain + probes equal |
28
+ | `theme-invalid` | `theme-invalid.json` | validate: the same issues, never a crash |
29
+ | `control-geometry` | `control-geometry.json` | measure YOUR painted control: every fixed key within 0.5 (with `minHeight`, height is only a floor) |
30
+ | `layout` | `layout-geometry.json` | lay out with the fixed measure: frames exact (a CSS renderer: within 1 px) |
31
+ | `overlay` | `overlay-geometry.json` | place the overlay: side, flip, position within `tolerancePx` |
32
+ | `replay` | `kitchen-sink.json` | paint the sink per built-in theme × mode: the expanded tree, no `Unknown`, paint/a11y order = pre-order |
33
+ | `host-transport` | `host-transport.json` | JSONL / SSE chunks through ONE decoder, the MCP carrier: messages + issues equal |
34
+ | `host-policy` | `host-policy.json` | the function gate, combine, URL policy, media request, source URIs, catalog negotiation |
35
+ | `host-router` | `host-router.json` | package validation; message flows → the ops, in order |
36
+ | `bind` | `bind-time.json` | expand, run the bind pass per dataset: bound tree, `set` presses, per-row slot cells |
37
+ | `style-conditions` | `style-conditions.json` | flatten the style per context |
38
+ | `code-tokens` | `code-tokens.json` | tokenize: the lines of tokens |
39
+ | `format` | `format.json` | format calls through the English fallback (`zoned`: at `offsetMinutes`), exact; display values. A platform Formatter may run too (U+202F, U+00A0 → space) |
40
+ | `template-items` | `template-items.json` | item and row keys, template instances + escaped suffixes, reduce with templates lifted (cycles reported, kept in place) |
41
+ | `text-direction` | `text-direction.json` | every node's direction and physical text alignment |
42
+ | `resizable` | `resizable.json` | panel sizes (normalize, drag, keys, extents, px → percent) within 1e-6 |
43
+ | `virtual-list` | `virtual-list.json` | the window, scrollToIndex (sectioned: data index → row), sections, the sticky header |
44
+ | `animations` | `animations.json` | timing + frames per theme within 1e-3; painted opacity = own × frame |
45
+
46
+ `manifest.json` says how each count is derived (`unit`). A renderer built on
47
+ the Rust core (the facade, `exponential-ui-ffi`) gets the pure suites through
48
+ its bindings; one on the TS package gets them from `@exponential-at/ui`;
49
+ anything else implements `catalog/host.json` and the reducer itself.
50
+
51
+ ## The report
52
+
53
+ [`report.schema.json`](report.schema.json):
54
+ `{renderer, platform, version, conformanceVersion, suites: {<id>: {cases,
55
+ passed, failed: [names], skipped?}}}`. Our runners write it to
56
+ `$EXPONENTIAL_UI_CONFORMANCE_REPORT` (default `<repo>/.conformance/<name>.json`):
57
+
58
+ | renderer | runner | command |
59
+ |---|---|---|
60
+ | `exponential-ui` (Rust core) | `apps/desktop/crates/exponential-ui/tests/conformance.rs` | `cargo test -p exponential-ui --test conformance` |
61
+ | `@exponential-at/ui-react` | `packages/exponential-ui-react/browser/suites.test.ts` | `bun run --filter @exponential-at/ui-react test:conformance` (headless Chromium) |
62
+ | `@exponential-at/ui-solid` | `packages/exponential-ui-solid/browser/suites.test.ts` | `bun run --filter @exponential-at/ui-solid test:conformance` (headless Chromium) |
63
+ | `@exponential-at/ui-element` | `packages/exponential-ui-element/browser/suites.test.ts` | `bun run --filter @exponential-at/ui-element test:conformance` (headless Chromium) |
64
+ | `exponential-ui-gpui` | `apps/desktop/crates/exponential-ui-gpui/tests/suites.rs` | `cargo test -p exponential-ui-gpui --test suites` |
65
+ | `ExponentialUI` (SwiftUI) | `packages/exponential-ui-swift/Tests/ExponentialUITests/ConformanceTests.swift` | `swift test` |
66
+ | `at.exponential:ui-compose` | `packages/exponential-ui-compose/ui-compose/src/test/…/ConformanceTest.kt` | `./gradlew :ui-compose:testDebugUnitTest` |
67
+
68
+ CI (`.github/workflows/exponential-ui.yml`) runs all six renderers plus the
69
+ core on every SDK pull request and fails unless every report is conformant;
70
+ the two non-React web renderers (Solid, the web component) must also pass
71
+ the web-parity gate against React (below).
72
+
73
+ Version 2 (round 2, `docs/round-2-contract.md` §8) added the last nine
74
+ suites: round 1's bind/style/code fixtures every runner already replayed,
75
+ and the round-2 contract fixtures.
76
+
77
+ ## With the real-font harness (VAPP-98, round 1)
78
+
79
+ Round 1 (#VAPP-98) adds the other half to this directory: `run.ts` dumps the
80
+ cases of `fixtures/conformance-cases.json` from the web and desktop renderers
81
+ with the conformance fonts and compares them with
82
+ `fixtures/conformance-baseline.json` (geometry with real text, plus a reported
83
+ pixel step). The two halves do not overlap: this manifest locks the CONTRACT
84
+ every renderer replays case by case (no fonts involved), the harness locks
85
+ what real text does to it. Once both are on master, the harness's comparison
86
+ becomes suite `real-font` here (cases = its matrix, a renderer passes when its
87
+ dump has no divergence beyond `conformance-known.json`), so
88
+ `conformance:check` stays the one verdict. Runner files are named `suites`
89
+ (`browser/suites.test.ts`, gpui `tests/suites.rs`) beside the harness's own
90
+ `conformance` ones.
91
+
92
+ ### The matrix
93
+
94
+ Every renderer runs the FULL cross product of `conformance-cases.json`
95
+ (`allCases` in `dump.ts`; the Rust, Swift and Kotlin runners build the same
96
+ keys `<fixture>/<theme>/<mode>/<width>/<direction>`), so adding a fixture, a
97
+ theme, a mode or a width there widens every runner at once:
98
+
99
+ | axis | values |
100
+ |---|---|
101
+ | fixtures | `kitchen-sink` (every component), `responsive` (the kitchen sink's responsive section, `layout-geometry-round1.json`), `dashboard` (`dashboard.json` + `dashboard.data.json`: an agent project dashboard, VAPP-103) |
102
+ | themes | the built-ins: `exponential`, `neutral`, `playful`, `orange` |
103
+ | modes | `dark`, `light` (a mode changes colours only by contract; the light cases lock that no theme mode or recipe moves a box) |
104
+ | widths | 390, 600, 900, 1280 |
105
+ | directions | `ltr`, `rtl` |
106
+
107
+ The web dump of the whole matrix takes well under a minute; the Swift and
108
+ Compose ratchets a few seconds each, so the matrix is not sampled.
109
+
110
+ ### Recording
111
+
112
+ | what | where | command |
113
+ |---|---|---|
114
+ | the web baseline | any machine with Chromium | `bun run --filter @exponential-at/ui conformance -- --write-baseline --skip-desktop` (whole matrix only) |
115
+ | the SwiftUI ratchet | macOS | `EXPONENTIAL_UI_WRITE_FIXTURES=1 swift test --filter RealFont` (`packages/exponential-ui-swift`) |
116
+ | the Compose ratchet | ubuntu (CI verifies there) | `.github/workflows/record-compose-fixtures.yml`, artifact `compose-fixtures` (`EXP_UI_WRITE_FIXTURES=1 ./gradlew :ui-compose:testDebugUnitTest --tests '*RealFontConformanceTest'`) |
117
+ | the gpui ratchet | Linux only (`tests/conformance.rs` builds there) | the same workflow's `gpui` job, artifact `gpui-ratchet` (`EXP_UI_WRITE_FIXTURES=1 cargo test -p exponential-ui-gpui --test conformance`) |
118
+
119
+ A new case without a budget fails every ratchet, so a matrix change re-records
120
+ all four in the same change.
121
+
122
+ Round 2: the dump measures LAYOUT boxes (transforms and animations off,
123
+ `LAYOUT_ONLY_CSS`), skips inactive carousel pages (`data-xui-inactive`),
124
+ and every painted part carries `data-xui-id="<node id>.<part>[.<index or
125
+ row key>]"` + `data-xui-part` (catalog/recipes.json lists the parts), so the
126
+ web and gpui dumps cover the same nodes. Nunito and Fira Code are real
127
+ faces (`fonts/`, SIL OFL 1.1). `conformance-known.json` carries the
128
+ divergence decisions (`causes`, `rules`) beside the ratchet's counts.
129
+
130
+ ## Shared browser runner
131
+
132
+ Every WEB renderer runs the manifest through ONE runner,
133
+ [`browser-suites.ts`](browser-suites.ts) `runBrowserSuites(options)`: the
134
+ pure suites through the core and the shared web layer
135
+ (`@exponential-at/ui/web`), the painted ones in headless Chromium against the
136
+ renderer's harness ([`browser-page.ts`](browser-page.ts): `bunHarness`,
137
+ `openPage`, `EXPONENTIAL_UI_CHROME` = an explicit Chromium executable). A
138
+ renderer plugs in with:
139
+
140
+ - **a harness**: a `serve.ts` that honours `PORT` and answers `/` with a page
141
+ speaking the React harness's protocol (`?view=` kitchen-sink, geometry,
142
+ overlay, catalog, conditions, tree, controls, specimen, mount, bench,
143
+ round2; `theme`/`mode`/`rtl`/`width`/`echo`; the `window.__xui*` probes);
144
+ - **its logic**: `templateItems`, `itemInstance`, `runNodeAction` (the rest
145
+ is shared).
146
+
147
+ The function returns `{report, failures}` (no vitest in the core); the
148
+ renderer's `browser/suites.test.ts` writes the report and asserts
149
+ `checkReport`, as React's does in about 30 lines.
150
+
151
+ ## Web parity
152
+
153
+ A separate, web-only gate (not a manifest suite: the natives keep the
154
+ manifest's list): [`parity.ts`](parity.ts) `runParity({reference,
155
+ candidate})` loads two web renderers side by side in one headless Chromium
156
+ (software raster, `--font-render-hinting=none`, the conformance fonts,
157
+ device scale 1, reduced motion, animations and transitions off) and compares
158
+ every case EXACTLY:
159
+
160
+ | check | what |
161
+ |---|---|
162
+ | dom | the `.xui-surface` subtrees (tag, attributes by name, text); generated ids (`useId`, Radix, Solid `cl-N`) in id attributes → `#id-N` in first-seen order |
163
+ | style | the full computed style of every `[data-xui-id]` element (and every overlay part) |
164
+ | frame | their `getBoundingClientRect`, 0 px tolerance |
165
+ | pixels | full-page screenshots, pixelmatch threshold 0 (AA excluded): 0 differing pixels; native `<video>`/`<audio>` boxes masked (the browser's own spinner) |
166
+
167
+ Cases (390): the matrix through `?view=tree` (192), every specimen at 900 px
168
+ (75), the control specimens with `data-xs` forced to hover, focus, pressed,
169
+ checked, open, disabled, invalid on every part (112), the overlay cases open
170
+ (11). The report ([`parity-report.schema.json`](parity-report.schema.json))
171
+ keeps the FIRST difference of each kind per case; differing screenshots land
172
+ in `.conformance/parity/`.
173
+
174
+ ```bash
175
+ bun run --filter @exponential-at/ui conformance:parity # React vs React, the self-check (~3 min)
176
+ bun run --filter @exponential-at/ui conformance:parity -- --candidate packages/exponential-ui-solid/harness/serve.ts
177
+ bun run --filter @exponential-at/ui conformance:parity -- --candidate packages/exponential-ui-element/harness/serve.ts
178
+ bun run --filter @exponential-at/ui parity:check .conformance/exponential-ui-parity-react-react.json
179
+ bun run --filter @exponential-at/ui-solid test:parity # Solid's gate calls runParity
180
+ bun run --filter @exponential-at/ui-element test:parity # the web component's gate, the same call
181
+ ```
182
+
183
+ **Primitive cases (FEED-131, group `primitive`).** The same gate also
184
+ compares the PRIMITIVE sets: `@exponential-at/ui-react/primitives` (Radix +
185
+ `dist/exponential-ui-react.css`) against `@exponential-at/ui-solid/primitives`
186
+ (+ `dist/exponential-ui-solid.css`, compiled from the SAME `tailwind.css`,
187
+ byte-identical: `src/primitives/styles.test.ts`). Every row of
188
+ [`primitive-specimens.json`](../fixtures/primitive-specimens.json) runs ×
189
+ [`exponential` light, `orange` dark] (`primitive:<id>:<theme>-<mode>`, plus
190
+ `…:<hover|focus>` when the row names an `interact`: the first
191
+ `[data-xui-target]` is hovered / focused on both sides) through both
192
+ harnesses' `?view=primitives&id=&theme=&mode=&width=`
193
+ ([`primitives-page.ts`](primitives-page.ts): `globalsCss` + the package's
194
+ own sheet in `<head>`, `class="dark"` on `<html>`, `data-xui-parity-root` +
195
+ `data-xui-mode` on `<body>`, the specimen in `<div data-xui-specimen
196
+ style="padding: 16px;">`). On such a page the probe compares the WHOLE
197
+ `<body>` (its attributes too: Radix's scroll lock writes them; `<script>`
198
+ and the gate's own styles skipped) and EVERY element under it, keyed by its
199
+ path (`body/div[0]/button[1]`). Specimens live in one module per lane,
200
+ `harness/primitives/{smoke,plain,controls,overlays,data,calendar}.tsx` in
201
+ BOTH packages (`SPECIMENS: Record<id, component>`); the jsdom twin is
202
+ `expectElementParity` (ui-solid `src/test/parity.ts`).
203
+
204
+ ```bash
205
+ bun run --filter @exponential-at/ui-solid test:parity:primitives # the primitive group alone (seconds)
206
+ EXPONENTIAL_UI_PARITY_GROUP=matrix,overlay bun run --filter @exponential-at/ui-solid test:parity
207
+ ```
208
+
209
+ ## Listing
210
+
211
+ Once two platforms pass, the A2UI ecosystem's renderers page gets an entry
212
+ (a pull request to google/A2UI, a person's call: it is outward-facing):
213
+
214
+ > **Exponential UI** (React, SwiftUI, Jetpack Compose, gpui): native renderers
215
+ > of the A2UI v0.9 basic catalog plus an 81-component core catalog, runtime
216
+ > themes, extension catalogs and a host API (transports, functions, bindings,
217
+ > policy). Apache-2.0. https://ui.exponential.at
@@ -0,0 +1,101 @@
1
+ // The committed web BASELINE (fixtures/conformance-baseline.json): the React
2
+ // renderer's frame dump of the whole matrix, stored compactly — per fixture
3
+ // ONE node table (the union of every case's nodes: id, component, part,
4
+ // parent, text) and per case one frame row per placed node
5
+ // `[node, x, y, w, h, lines?, lh?]`, one row per line so a layout change
6
+ // diffs as the rows it moved. `decodeBaseline` gives the plain dump back;
7
+ // the Rust gate (tests/conformance.rs) decodes the same shape.
8
+
9
+ import { BASELINE_PATH, type CaseDump, type Dump, type DumpNode } from "./dump"
10
+
11
+ export const BASELINE_FILE = BASELINE_PATH
12
+ export const BASELINE_FORMAT = `xui-conformance-baseline/1`
13
+
14
+ type NodeRow = [id: string, component: string, part: string | null, parent: string | null, text: string | null]
15
+ type FrameRow = [node: number, x: number, y: number, w: number, h: number, lines?: number, lh?: number]
16
+
17
+ export interface Baseline {
18
+ $comment: string
19
+ format: string
20
+ renderer: string
21
+ fonts: string
22
+ /** How the baseline was produced (browser version, launch flags). */
23
+ producer: string
24
+ fixtures: Record<string, { nodes: NodeRow[] }>
25
+ cases: Record<string, { width: number; height: number; frames: FrameRow[] }>
26
+ }
27
+
28
+ const fixtureOf = (key: string) => key.split(`/`)[0]
29
+
30
+ export function toBaseline(dump: Dump, producer: string): Baseline {
31
+ const fixtures: Baseline[`fixtures`] = {}
32
+ const index = new Map<string, Map<string, number>>()
33
+ const cases: Baseline[`cases`] = {}
34
+ for (const [key, c] of Object.entries(dump.cases)) {
35
+ const fx = fixtureOf(key)
36
+ fixtures[fx] ??= { nodes: [] }
37
+ const ids = index.get(fx) ?? new Map<string, number>()
38
+ index.set(fx, ids)
39
+ const frames: FrameRow[] = []
40
+ for (const n of c.nodes) {
41
+ const sig = JSON.stringify([n.id, n.component, n.part ?? null, n.parent ?? null, n.text ?? null])
42
+ let i = ids.get(sig)
43
+ if (i === undefined) {
44
+ i = fixtures[fx].nodes.length
45
+ fixtures[fx].nodes.push(JSON.parse(sig) as NodeRow)
46
+ ids.set(sig, i)
47
+ }
48
+ const row: FrameRow = [i, n.x, n.y, n.w, n.h]
49
+ if (n.lines !== undefined) row.push(n.lines, n.lh ?? 0)
50
+ frames.push(row)
51
+ }
52
+ cases[key] = { width: c.width, height: c.height, frames }
53
+ }
54
+ return {
55
+ $comment: `GENERATED by \`bun run --filter @exponential-at/ui conformance -- --write-baseline\` (packages/exponential-ui/conformance): the React renderer's frame dump of fixtures/conformance-cases.json in headless Chromium with the conformance font set. Decoded by conformance/baseline.ts and the Rust gate. Never hand-edit.`,
56
+ format: BASELINE_FORMAT,
57
+ renderer: dump.renderer,
58
+ fonts: dump.fonts,
59
+ producer,
60
+ fixtures,
61
+ cases,
62
+ }
63
+ }
64
+
65
+ export function decodeBaseline(b: Baseline): Dump {
66
+ if (b.format !== BASELINE_FORMAT) throw new Error(`baseline format ${b.format} ≠ ${BASELINE_FORMAT}`)
67
+ const cases: Record<string, CaseDump> = {}
68
+ for (const [key, c] of Object.entries(b.cases)) {
69
+ const table = b.fixtures[fixtureOf(key)].nodes
70
+ cases[key] = {
71
+ width: c.width,
72
+ height: c.height,
73
+ nodes: c.frames.map(([i, x, y, w, h, lines, lh]) => {
74
+ const [id, component, part, parent, text] = table[i]
75
+ const n: DumpNode = { id, component, x, y, w, h }
76
+ if (part !== null) n.part = part
77
+ if (parent !== null) n.parent = parent
78
+ if (text !== null) n.text = text
79
+ if (lines !== undefined) {
80
+ n.lines = lines
81
+ n.lh = lh
82
+ }
83
+ return n
84
+ }),
85
+ }
86
+ }
87
+ return { format: `xui-frame-dump/1`, renderer: b.renderer, fonts: b.fonts, cases }
88
+ }
89
+
90
+ /** The baseline as committed: the envelope pretty, one node row / frame row per line. */
91
+ export function encodeBaseline(dump: Dump, producer: string): string {
92
+ const b = toBaseline(dump, producer)
93
+ const rows = (items: unknown[], indent: string) => (items.length ? `[\n${items.map((r) => `${indent} ${JSON.stringify(r)}`).join(`,\n`)}\n${indent}]` : `[]`)
94
+ const fixtures = Object.entries(b.fixtures)
95
+ .map(([k, f]) => ` ${JSON.stringify(k)}: { "nodes": ${rows(f.nodes, ` `)} }`)
96
+ .join(`,\n`)
97
+ const cases = Object.entries(b.cases)
98
+ .map(([k, c]) => ` ${JSON.stringify(k)}: { "width": ${c.width}, "height": ${c.height}, "frames": ${rows(c.frames, ` `)} }`)
99
+ .join(`,\n`)
100
+ return `{\n "$comment": ${JSON.stringify(b.$comment)},\n "format": ${JSON.stringify(b.format)},\n "renderer": ${JSON.stringify(b.renderer)},\n "fonts": ${JSON.stringify(b.fonts)},\n "producer": ${JSON.stringify(b.producer)},\n "fixtures": {\n${fixtures}\n },\n "cases": {\n${cases}\n }\n}\n`
101
+ }
@@ -0,0 +1,146 @@
1
+ // FEED-128: the Playwright side of the shared WEB conformance tooling — one
2
+ // headless Chromium (`EXPONENTIAL_UI_CHROME` = an explicit executable, for a
3
+ // machine whose Playwright build is not installed), a renderer HARNESS served
4
+ // on a loopback port, and the page opener every painted suite uses. Node-safe
5
+ // (vitest runs the runners under Node): a harness is a `bun <serve.ts>`
6
+ // child process, never an in-process `Bun.build`.
7
+ //
8
+ // The harness protocol (packages/exponential-ui-react/harness/main.tsx is
9
+ // the reference): `<serve.ts>` honours `PORT`, answers `/` with a page whose
10
+ // module script is the bundle, and that page reads `?view=…` (kitchen-sink,
11
+ // geometry, overlay, catalog, conditions, tree, controls, specimen, mount,
12
+ // bench, round2, primitives = FEED-131's primitives-page.ts) plus
13
+ // theme/mode/rtl/width/echo and exposes the `window.__xui*` probes.
14
+
15
+ import { chromium, type Browser, type BrowserContext, type Page } from "playwright"
16
+ import { spawn, type ChildProcess } from "node:child_process"
17
+ import { createServer } from "node:net"
18
+
19
+ export type Query = Record<string, string | number | undefined>
20
+
21
+ /** A renderer's harness: `start()` serves its bundle on a loopback port. */
22
+ export interface WebHarness {
23
+ start(): Promise<RunningHarness>
24
+ }
25
+
26
+ export interface RunningHarness {
27
+ baseUrl: string
28
+ stop(): void | Promise<void>
29
+ }
30
+
31
+ /** The Chromium executable override (`EXPONENTIAL_UI_CHROME`), else
32
+ * Playwright's own build. */
33
+ export const chromeExecutable = (): string | undefined => process.env.EXPONENTIAL_UI_CHROME || undefined
34
+
35
+ export async function launchChromium(args: string[] = [], executablePath = chromeExecutable()): Promise<Browser> {
36
+ return chromium.launch({ executablePath, args })
37
+ }
38
+
39
+ /** `${baseUrl}/?<query>` (undefined values dropped). */
40
+ export function pageUrl(baseUrl: string, query: Query): string {
41
+ const q = new URLSearchParams()
42
+ for (const [k, v] of Object.entries(query)) if (v !== undefined) q.set(k, String(v))
43
+ return `${baseUrl}/?${q.toString()}`
44
+ }
45
+
46
+ /** A free loopback TCP port. */
47
+ export function freePort(): Promise<number> {
48
+ return new Promise((resolve, reject) => {
49
+ const srv = createServer()
50
+ srv.unref()
51
+ srv.on(`error`, reject)
52
+ srv.listen(0, `127.0.0.1`, () => {
53
+ const addr = srv.address()
54
+ const port = typeof addr === `object` && addr ? addr.port : 0
55
+ srv.close(() => resolve(port))
56
+ })
57
+ })
58
+ }
59
+
60
+ /** The harness of a `serve.ts` that honours `PORT`: spawned with `bun`,
61
+ * ready once `/` answers and the module script it names bundles (a 500 =
62
+ * the bundler's error, thrown). A child that dies before `stop()` (killed
63
+ * from outside on a busy machine) is respawned on the same port. */
64
+ export function bunHarness(serveScript: string, opts: { timeoutMs?: number } = {}): WebHarness {
65
+ return {
66
+ async start() {
67
+ const port = await freePort()
68
+ const baseUrl = `http://127.0.0.1:${port}`
69
+ let stderr = ``
70
+ let stopping = false
71
+ let proc: ChildProcess
72
+ const spawnChild = () => {
73
+ proc = spawn(`bun`, [serveScript], { env: { ...process.env, PORT: String(port) }, stdio: [`ignore`, `pipe`, `pipe`] })
74
+ proc.stderr?.on(`data`, (d) => (stderr += String(d)))
75
+ proc.on(`exit`, (code, signal) => {
76
+ if (stopping) return
77
+ stderr += `${serveScript} exited (${code ?? signal}); respawned\n`
78
+ spawnChild()
79
+ })
80
+ }
81
+ spawnChild()
82
+ const deadline = Date.now() + (opts.timeoutMs ?? 60_000)
83
+ for (;;) {
84
+ let ready = false
85
+ try {
86
+ const page = await fetch(`${baseUrl}/`)
87
+ if (page.ok) {
88
+ const src = /<script[^>]*src="([^"]+)"/.exec(await page.text())?.[1] ?? `/dist/main.js`
89
+ const r = await fetch(new URL(src, baseUrl))
90
+ if (r.status === 500) {
91
+ stopping = true
92
+ proc!.kill()
93
+ throw new Error(`${serveScript}: bundle failed: ${await r.text()}`)
94
+ }
95
+ ready = r.ok
96
+ }
97
+ } catch (e) {
98
+ if (String(e).includes(`bundle failed`)) throw e
99
+ }
100
+ if (ready) break
101
+ if (Date.now() > deadline) {
102
+ stopping = true
103
+ proc!.kill()
104
+ throw new Error(`${serveScript} did not start on ${baseUrl}: ${stderr}`)
105
+ }
106
+ await new Promise((r) => setTimeout(r, 150))
107
+ }
108
+ return {
109
+ baseUrl,
110
+ stop: () => {
111
+ stopping = true
112
+ proc.kill()
113
+ if (stderr.trim()) console.error(stderr)
114
+ },
115
+ }
116
+ },
117
+ }
118
+ }
119
+
120
+ export interface OpenPageOptions {
121
+ viewport?: { width: number; height: number }
122
+ /** Wait for a `.xui-surface` (default true). */
123
+ waitForSurface?: boolean
124
+ /** Extra settle time after the surface attached, ms (default 250). */
125
+ settleMs?: number
126
+ context?: Parameters<Browser[`newContext`]>[0]
127
+ }
128
+
129
+ /** A fresh context + page on the harness at `query`; throws on page errors. */
130
+ export async function openPage(browser: Browser, baseUrl: string, query: Query, viewport = { width: 1200, height: 900 }, opts: Omit<OpenPageOptions, `viewport`> = {}): Promise<Page> {
131
+ const context: BrowserContext = await browser.newContext({ viewport, deviceScaleFactor: 1, ...opts.context })
132
+ const page = await context.newPage()
133
+ const errors: string[] = []
134
+ page.on(`pageerror`, (e) => errors.push(String(e)))
135
+ page.on(`console`, (m) => {
136
+ if (m.type() === `error`) errors.push(m.text())
137
+ })
138
+ await page.goto(pageUrl(baseUrl, query))
139
+ if (opts.waitForSurface !== false) await page.waitForSelector(`.xui-surface`, { timeout: 30_000, state: `attached` })
140
+ await page.waitForTimeout(opts.settleMs ?? 250)
141
+ if (errors.length) {
142
+ await context.close()
143
+ throw new Error(`page errors: ${errors.join(`\n`)}`)
144
+ }
145
+ return page
146
+ }