@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,224 @@
1
+ {
2
+ "$comment": "GENERATED by `bun run --filter @exponential-at/ui generate` (VAPP-91). The Exponential UI conformance suite: a renderer is \"Exponential UI conformant\" when its runner reports every suite below with `cases` run and none failed (`bun run --filter @exponential-at/ui conformance:check <report.json>`). conformance/README.md says how to write a runner for a new platform.",
3
+ "version": 2,
4
+ "hostContractVersion": 2,
5
+ "suites": [
6
+ {
7
+ "id": "catalog",
8
+ "files": [
9
+ "fixtures/catalog-components.json"
10
+ ],
11
+ "unit": "case",
12
+ "cases": 528,
13
+ "check": "Reduce the case's nested node with the core catalog: no issues. Paint it under the default theme: no crash, no Unknown placeholder, every visible node painted."
14
+ },
15
+ {
16
+ "id": "macros",
17
+ "files": [
18
+ "fixtures/catalog-macros.json"
19
+ ],
20
+ "unit": "case",
21
+ "cases": 265,
22
+ "check": "Expand the case: the tree equals `expanded` (JSON-equal, recipes included)."
23
+ },
24
+ {
25
+ "id": "basic-map",
26
+ "files": [
27
+ "fixtures/catalog-basic-map.json"
28
+ ],
29
+ "unit": "case",
30
+ "cases": 13,
31
+ "check": "Reduce the A2UI basic surface: the tree and the issues equal the expected ones."
32
+ },
33
+ {
34
+ "id": "extension",
35
+ "files": [
36
+ "fixtures/catalog-extension.json"
37
+ ],
38
+ "unit": "case",
39
+ "cases": 4,
40
+ "check": "Register the extension; reduce each case: tree + issues equal; its natives reach the extension painter (an `Extension` leaf of that kind)."
41
+ },
42
+ {
43
+ "id": "theme-resolved",
44
+ "files": [
45
+ "fixtures/theme-resolved.json"
46
+ ],
47
+ "unit": "theme",
48
+ "cases": 4,
49
+ "check": "Load each built-in theme: the resolved theme equals the fixture."
50
+ },
51
+ {
52
+ "id": "theme-recipes",
53
+ "files": [
54
+ "fixtures/theme-recipes.json"
55
+ ],
56
+ "unit": "case",
57
+ "cases": 409,
58
+ "check": "Resolve the part's recipe for the case's theme, props, mode and states: the visual equals the fixture."
59
+ },
60
+ {
61
+ "id": "theme-extends",
62
+ "files": [
63
+ "fixtures/theme-extends.json"
64
+ ],
65
+ "unit": "case",
66
+ "cases": 3,
67
+ "check": "Load the theme over its parents: the chain and the probed styles equal the fixture."
68
+ },
69
+ {
70
+ "id": "theme-invalid",
71
+ "files": [
72
+ "fixtures/theme-invalid.json"
73
+ ],
74
+ "unit": "case",
75
+ "cases": 26,
76
+ "check": "Validate the bad theme: the same issue paths, never a crash."
77
+ },
78
+ {
79
+ "id": "control-geometry",
80
+ "files": [
81
+ "fixtures/control-geometry.json"
82
+ ],
83
+ "unit": "theme × control × case",
84
+ "cases": 324,
85
+ "check": "Measure the control under the theme: the box (height, padding, gap, border, radius) equals the fixture."
86
+ },
87
+ {
88
+ "id": "layout",
89
+ "files": [
90
+ "fixtures/layout-geometry.json"
91
+ ],
92
+ "unit": "width × direction",
93
+ "cases": 4,
94
+ "check": "Lay the surface out with the fixture's fixed measure: every node's frame equals the fixture (exact for the Rust core and native painters, within 1 px for a CSS renderer)."
95
+ },
96
+ {
97
+ "id": "overlay",
98
+ "files": [
99
+ "fixtures/overlay-geometry.json"
100
+ ],
101
+ "unit": "case",
102
+ "cases": 11,
103
+ "check": "Place the overlay: side, flip and frame equal the fixture within `tolerancePx`."
104
+ },
105
+ {
106
+ "id": "replay",
107
+ "files": [
108
+ "fixtures/kitchen-sink.json",
109
+ "fixtures/kitchen-sink.expanded.json"
110
+ ],
111
+ "unit": "theme × mode",
112
+ "cases": 8,
113
+ "check": "Paint the kitchen sink under each built-in theme and mode: the reduced tree equals kitchen-sink.expanded.json, no Unknown, the accessibility order is the pre-order of the painted nodes."
114
+ },
115
+ {
116
+ "id": "host-transport",
117
+ "files": [
118
+ "fixtures/host-transport.json"
119
+ ],
120
+ "unit": "case",
121
+ "cases": 20,
122
+ "check": "Feed the chunks through ONE decoder (JSONL / SSE), or decode the MCP result, or build the MCP action call: messages + issues equal `expected`."
123
+ },
124
+ {
125
+ "id": "host-policy",
126
+ "files": [
127
+ "fixtures/host-policy.json"
128
+ ],
129
+ "unit": "case",
130
+ "cases": 103,
131
+ "check": "Decide the function / combine / url, build the media request, parse the source, negotiate the catalog ids: each equals `expected`."
132
+ },
133
+ {
134
+ "id": "host-router",
135
+ "files": [
136
+ "fixtures/host-router.json"
137
+ ],
138
+ "unit": "package or flow",
139
+ "cases": 11,
140
+ "check": "Validate each package; for each flow build ONE router with `extensionIds`, install `packages` (issues = `installIssues`), route every step: the ops equal `expected`."
141
+ },
142
+ {
143
+ "id": "bind",
144
+ "files": [
145
+ "fixtures/bind-time.json"
146
+ ],
147
+ "unit": "case",
148
+ "cases": 53,
149
+ "check": "Expand the input; for every dataset run the bind pass: the bound tree, every `set` press and the per-row slot cells equal the fixture (reducer `issues` too for `extra`)."
150
+ },
151
+ {
152
+ "id": "style-conditions",
153
+ "files": [
154
+ "fixtures/style-conditions.json"
155
+ ],
156
+ "unit": "case",
157
+ "cases": 7,
158
+ "check": "Flatten the style for every context: equal to `expected` (key order irrelevant)."
159
+ },
160
+ {
161
+ "id": "code-tokens",
162
+ "files": [
163
+ "fixtures/code-tokens.json"
164
+ ],
165
+ "unit": "case",
166
+ "cases": 23,
167
+ "check": "Tokenize the code: the lines of tokens equal `expected`."
168
+ },
169
+ {
170
+ "id": "format",
171
+ "files": [
172
+ "fixtures/format.json"
173
+ ],
174
+ "unit": "call or value",
175
+ "cases": 86,
176
+ "check": "Resolve each format call through the English fallback (`zoned`: at the case's `offsetMinutes`): the string equals `expected` exactly; display each value in a text prop: equals `expected`. A platform Formatter may run too, at en-US / UTC (`zoned`: `timeZone`), comparing after U+202F and U+00A0 → space."
177
+ },
178
+ {
179
+ "id": "template-items",
180
+ "files": [
181
+ "fixtures/template-items.json"
182
+ ],
183
+ "unit": "case",
184
+ "cases": 26,
185
+ "check": "Key the items / rows, list the template instances (path, key, accumulated suffix), reduce the surfaces (templates lifted): equal to `expected`."
186
+ },
187
+ {
188
+ "id": "text-direction",
189
+ "files": [
190
+ "fixtures/text-direction.json"
191
+ ],
192
+ "unit": "surface direction",
193
+ "cases": 2,
194
+ "check": "Reduce the tree; every node's resolved direction and physical text alignment equal `expected`."
195
+ },
196
+ {
197
+ "id": "resizable",
198
+ "files": [
199
+ "fixtures/resizable.json"
200
+ ],
201
+ "unit": "case",
202
+ "cases": 38,
203
+ "check": "Normalize / resize / key / measure / convert: the numbers equal `expected` within 1e-6."
204
+ },
205
+ {
206
+ "id": "virtual-list",
207
+ "files": [
208
+ "fixtures/virtual-list.json"
209
+ ],
210
+ "unit": "case",
211
+ "cases": 26,
212
+ "check": "Window the extents, compute the scrollToIndex offset (sectioned: data index → row), group the sections, pin the sticky header: equal to `expected`."
213
+ },
214
+ {
215
+ "id": "animations",
216
+ "files": [
217
+ "fixtures/animations.json"
218
+ ],
219
+ "unit": "theme × animation",
220
+ "cases": 38,
221
+ "check": "Time each animation under the theme and sample its frames (and the reduced-motion frame): equal to `expected` within 1e-3; `opacity`: the painted opacity = the node's own × the frame's, within 1e-3."
222
+ }
223
+ ]
224
+ }
@@ -0,0 +1,191 @@
1
+ // FEED-128: the PURE half of the web parity check (parity.ts drives the
2
+ // browser): the serialised DOM shape, generated-id normalisation, and the
3
+ // first-difference finders for DOM, computed style and frames. No browser,
4
+ // no I/O: parity.test.ts locks them on hand-written inputs.
5
+
6
+ /** One serialised DOM node: an element (`t` tag, `a` attributes sorted by
7
+ * name, `c` children) or a text node (`x`). Comments are dropped. */
8
+ export type DomNode = { t: string; a: [string, string][]; c: DomNode[] } | { x: string }
9
+
10
+ /** The attributes whose values are element ids (space-separated lists for
11
+ * the ARIA relations). Only these are normalised. */
12
+ export const ID_ATTRIBUTES = new Set([`id`, `for`, `name`, `aria-controls`, `aria-labelledby`, `aria-describedby`, `aria-owns`, `aria-activedescendant`, `aria-errormessage`, `aria-details`, `aria-flowto`, `list`, `form`])
13
+
14
+ /** A GENERATED id token: React `useId` (`:r1:`, `«r1»`, `_r_1_`), anything
15
+ * Radix derives (`radix-…`), Solid / Kobalte `createUniqueId` (`cl-12…`). */
16
+ export const GENERATED_ID_PATTERNS: readonly RegExp[] = [/radix-/, /:r[0-9a-z]+:/, /«r[0-9a-z]+»/, /(^|[^0-9A-Za-z])_r_[0-9a-z]+_/, /(^|[^0-9A-Za-z])cl-\d+/]
17
+
18
+ export const isGeneratedId = (token: string): boolean => GENERATED_ID_PATTERNS.some((re) => re.test(token))
19
+
20
+ /** A copy of the tree with every generated id token in an id attribute
21
+ * replaced by `#id-N`, N = first-seen order (document order, attributes by
22
+ * name): the same token always maps to the same N, so a relation still
23
+ * points at its target after normalisation. */
24
+ export function normalizeIds(roots: DomNode[]): DomNode[] {
25
+ const seen = new Map<string, string>()
26
+ const map = (token: string) => {
27
+ if (!isGeneratedId(token)) return token
28
+ let out = seen.get(token)
29
+ if (!out) seen.set(token, (out = `#id-${seen.size + 1}`))
30
+ return out
31
+ }
32
+ const walk = (n: DomNode): DomNode => {
33
+ if (`x` in n) return n
34
+ return { t: n.t, a: n.a.map(([k, v]) => [k, ID_ATTRIBUTES.has(k) ? v.split(/(\s+)/).map((p) => (p.trim() ? map(p) : p)).join(``) : v]), c: n.c.map(walk) }
35
+ }
36
+ return roots.map(walk)
37
+ }
38
+
39
+ export interface DomDiff {
40
+ /** `0/div[0]/span[2]` (surface index, then element children by tag +
41
+ * index among ALL children), `@attr` / `#text` for the differing part. */
42
+ path: string
43
+ reference: string
44
+ candidate: string
45
+ }
46
+
47
+ const describe = (n: DomNode | undefined): string => (n === undefined ? `(nothing)` : `x` in n ? `#text ${JSON.stringify(n.x)}` : `<${n.t}>`)
48
+
49
+ /** The FIRST difference between two serialised trees in document order. */
50
+ export function firstDomDiff(reference: DomNode[], candidate: DomNode[]): DomDiff | undefined {
51
+ const visit = (a: DomNode | undefined, b: DomNode | undefined, path: string): DomDiff | undefined => {
52
+ if (a === undefined || b === undefined || `x` in a !== `x` in b) return { path, reference: describe(a), candidate: describe(b) }
53
+ if (`x` in a && `x` in b) return a.x === b.x ? undefined : { path: `${path}#text`, reference: a.x, candidate: b.x }
54
+ const ea = a as Extract<DomNode, { t: string }>
55
+ const eb = b as Extract<DomNode, { t: string }>
56
+ if (ea.t !== eb.t) return { path, reference: describe(ea), candidate: describe(eb) }
57
+ const names = [...new Set([...ea.a.map(([k]) => k), ...eb.a.map(([k]) => k)])].sort()
58
+ const av = new Map(ea.a)
59
+ const bv = new Map(eb.a)
60
+ for (const k of names) if (av.get(k) !== bv.get(k)) return { path: `${path}@${k}`, reference: av.has(k) ? av.get(k)! : `(absent)`, candidate: bv.has(k) ? bv.get(k)! : `(absent)` }
61
+ for (let i = 0; i < Math.max(ea.c.length, eb.c.length); i++) {
62
+ const child = ea.c[i] ?? eb.c[i]!
63
+ const d = visit(ea.c[i], eb.c[i], `${path}/${`x` in child ? `#text` : child.t}[${i}]`)
64
+ if (d) return d
65
+ }
66
+ return undefined
67
+ }
68
+ for (let i = 0; i < Math.max(reference.length, candidate.length); i++) {
69
+ const d = visit(reference[i], candidate[i], String(i))
70
+ if (d) return d
71
+ }
72
+ return undefined
73
+ }
74
+
75
+ /** One compared element: its key (`data-xui-id`, or `<owner id>/<part>` for
76
+ * an overlay part without its own id), the computed-style values in
77
+ * `StyleTable.properties` order, and its frame. */
78
+ export interface ElementProbe {
79
+ key: string
80
+ values: string[]
81
+ frame: { x: number; y: number; w: number; h: number }
82
+ }
83
+
84
+ export interface StyleTable {
85
+ properties: string[]
86
+ elements: ElementProbe[]
87
+ }
88
+
89
+ export interface StyleDiff {
90
+ id: string
91
+ property: string
92
+ reference: string
93
+ candidate: string
94
+ }
95
+
96
+ export interface FrameDiff {
97
+ id: string
98
+ key: `x` | `y` | `w` | `h` | `element`
99
+ reference: number | string
100
+ candidate: number | string
101
+ }
102
+
103
+ /** The element lists must line up key for key; the first mismatch is the
104
+ * difference (`property: "(element)"`). */
105
+ function elementMismatch(a: StyleTable, b: StyleTable): { index: number; reference: string; candidate: string } | undefined {
106
+ for (let i = 0; i < Math.max(a.elements.length, b.elements.length); i++) {
107
+ const ka = a.elements[i]?.key
108
+ const kb = b.elements[i]?.key
109
+ if (ka !== kb) return { index: i, reference: ka ?? `(nothing)`, candidate: kb ?? `(nothing)` }
110
+ }
111
+ return undefined
112
+ }
113
+
114
+ /** The FIRST computed-style difference (element order, then property order). */
115
+ export function firstStyleDiff(a: StyleTable, b: StyleTable): StyleDiff | undefined {
116
+ const props = [...new Set([...a.properties, ...b.properties])]
117
+ const ia = new Map(a.properties.map((p, i) => [p, i]))
118
+ const ib = new Map(b.properties.map((p, i) => [p, i]))
119
+ const n = Math.min(a.elements.length, b.elements.length)
120
+ const mismatch = elementMismatch(a, b)
121
+ const upto = mismatch ? mismatch.index : n
122
+ for (let i = 0; i < upto; i++) {
123
+ const ea = a.elements[i]!
124
+ const eb = b.elements[i]!
125
+ for (const p of props) {
126
+ const va = ia.has(p) ? (ea.values[ia.get(p)!] ?? `(absent)`) : `(absent)`
127
+ const vb = ib.has(p) ? (eb.values[ib.get(p)!] ?? `(absent)`) : `(absent)`
128
+ if (va !== vb) return { id: ea.key, property: p, reference: va, candidate: vb }
129
+ }
130
+ }
131
+ if (mismatch) return { id: `#${mismatch.index}`, property: `(element)`, reference: mismatch.reference, candidate: mismatch.candidate }
132
+ return undefined
133
+ }
134
+
135
+ /** The FIRST frame difference, exact (0 px tolerance). */
136
+ export function firstFrameDiff(a: StyleTable, b: StyleTable): FrameDiff | undefined {
137
+ const mismatch = elementMismatch(a, b)
138
+ const upto = mismatch ? mismatch.index : Math.min(a.elements.length, b.elements.length)
139
+ for (let i = 0; i < upto; i++) {
140
+ const fa = a.elements[i]!.frame
141
+ const fb = b.elements[i]!.frame
142
+ for (const k of [`x`, `y`, `w`, `h`] as const) if (fa[k] !== fb[k]) return { id: a.elements[i]!.key, key: k, reference: fa[k], candidate: fb[k] }
143
+ }
144
+ if (mismatch) return { id: `#${mismatch.index}`, key: `element`, reference: mismatch.reference, candidate: mismatch.candidate }
145
+ return undefined
146
+ }
147
+
148
+ /** A case id → a file-name-safe slug. */
149
+ export const caseSlug = (id: string): string => id.replace(/[^A-Za-z0-9._-]+/g, `_`)
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // The report (parity-report.schema.json)
153
+ // ---------------------------------------------------------------------------
154
+
155
+ export const PARITY_FORMAT = `xui-web-parity/1`
156
+
157
+ export interface ParityCaseResult {
158
+ case: string
159
+ ok: boolean
160
+ elapsedMs: number
161
+ /** A page error, a timeout: the case could not be compared. */
162
+ error?: string
163
+ dom?: DomDiff
164
+ style?: StyleDiff
165
+ frame?: FrameDiff
166
+ pixels?: { differing: number; size: string; referencePng?: string; candidatePng?: string; diffPng?: string }
167
+ }
168
+
169
+ export interface ParityReport {
170
+ format: typeof PARITY_FORMAT
171
+ reference: string
172
+ candidate: string
173
+ chromium: string
174
+ startedAt: string
175
+ elapsedMs: number
176
+ summary: { cases: number; passed: number; failed: number; byGroup: Record<string, { cases: number; passed: number }> }
177
+ cases: ParityCaseResult[]
178
+ }
179
+
180
+ /** Every problem of a parity report (empty = parity). */
181
+ export function checkParityReport(report: ParityReport): string[] {
182
+ const problems: string[] = []
183
+ if (report.format !== PARITY_FORMAT) problems.push(`format ${String(report.format)}, want ${PARITY_FORMAT}`)
184
+ if (!Array.isArray(report.cases) || report.cases.length === 0) problems.push(`no cases`)
185
+ for (const c of report.cases ?? []) {
186
+ if (c.ok) continue
187
+ const what = c.error ? `error ${c.error.split(`\n`)[0]}` : [c.dom && `dom ${c.dom.path}`, c.style && `style ${c.style.id} ${c.style.property}`, c.frame && `frame ${c.frame.id}.${c.frame.key}`, c.pixels?.differing && `${c.pixels.differing} px`].filter(Boolean).join(`, `)
188
+ problems.push(`${c.case}: ${what}`)
189
+ }
190
+ return problems
191
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ui.exponential.at/conformance/parity-report.schema.json",
4
+ "title": "Exponential UI web parity report",
5
+ "description": "FEED-128: what conformance/parity.ts runParity() writes: a candidate web renderer against the reference (React) in one headless Chromium, every case compared exactly on DOM (generated ids normalised), computed style, frames and pixels. parity:check passes only when every case is ok. A web-only gate, not a manifest suite.",
6
+ "type": "object",
7
+ "required": ["format", "reference", "candidate", "chromium", "startedAt", "elapsedMs", "summary", "cases"],
8
+ "properties": {
9
+ "format": { "const": "xui-web-parity/1" },
10
+ "reference": { "type": "string", "description": "The reference renderer's name (react)." },
11
+ "candidate": { "type": "string", "description": "The candidate renderer's name (solid; react for the self-check)." },
12
+ "chromium": { "type": "string", "description": "The browser build and its settings." },
13
+ "startedAt": { "type": "string", "description": "ISO 8601." },
14
+ "elapsedMs": { "type": "number" },
15
+ "summary": {
16
+ "type": "object",
17
+ "required": ["cases", "passed", "failed", "byGroup"],
18
+ "properties": {
19
+ "cases": { "type": "integer" },
20
+ "passed": { "type": "integer" },
21
+ "failed": { "type": "integer" },
22
+ "byGroup": {
23
+ "type": "object",
24
+ "description": "matrix | specimen | state | overlay | primitive",
25
+ "additionalProperties": { "type": "object", "required": ["cases", "passed"], "properties": { "cases": { "type": "integer" }, "passed": { "type": "integer" } } }
26
+ }
27
+ }
28
+ },
29
+ "cases": { "type": "array", "items": { "$ref": "#/$defs/case" } }
30
+ },
31
+ "$defs": {
32
+ "case": {
33
+ "type": "object",
34
+ "required": ["case", "ok", "elapsedMs"],
35
+ "properties": {
36
+ "case": { "type": "string", "description": "matrix:<conformance key> | specimen:<id> | state:<id>:<state> | overlay:<n> | primitive:<id>:<theme>-<mode>[:hover|focus]" },
37
+ "ok": { "type": "boolean" },
38
+ "elapsedMs": { "type": "number", "description": "Both pages opened, settled, compared." },
39
+ "error": { "type": "string", "description": "The case could not be compared (page error, timeout)." },
40
+ "dom": {
41
+ "type": "object",
42
+ "description": "The FIRST DOM difference: path = surface index / tag[child index]…, @attr or #text for the differing part.",
43
+ "required": ["path", "reference", "candidate"],
44
+ "properties": { "path": { "type": "string" }, "reference": { "type": "string" }, "candidate": { "type": "string" } }
45
+ },
46
+ "style": {
47
+ "type": "object",
48
+ "description": "The FIRST computed-style difference: id = data-xui-id (or <owner>/<part> in an overlay, #<index> + property (element) when the element lists do not line up).",
49
+ "required": ["id", "property", "reference", "candidate"],
50
+ "properties": { "id": { "type": "string" }, "property": { "type": "string" }, "reference": { "type": "string" }, "candidate": { "type": "string" } }
51
+ },
52
+ "frame": {
53
+ "type": "object",
54
+ "description": "The FIRST getBoundingClientRect difference (0 px tolerance).",
55
+ "required": ["id", "key", "reference", "candidate"],
56
+ "properties": { "id": { "type": "string" }, "key": { "enum": ["x", "y", "w", "h", "element"] }, "reference": { "type": ["number", "string"] }, "candidate": { "type": ["number", "string"] } }
57
+ },
58
+ "pixels": {
59
+ "type": "object",
60
+ "description": "Full-page screenshots through pixelmatch (threshold 0, AA excluded); a size mismatch counts the larger area. The PNGs are written only when pixels differ.",
61
+ "required": ["differing", "size"],
62
+ "properties": {
63
+ "differing": { "type": "integer" },
64
+ "size": { "type": "string" },
65
+ "referencePng": { "type": "string" },
66
+ "candidatePng": { "type": "string" },
67
+ "diffPng": { "type": "string" }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }