@exponential_app/eui 0.0.0-stage → 0.2.1

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 (274) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +54 -0
  3. package/README.md +377 -2
  4. package/catalog/a11y.json +705 -0
  5. package/catalog/basic-map.json +104 -0
  6. package/catalog/code.json +58 -0
  7. package/catalog/core.catalog.json +5190 -0
  8. package/catalog/core.schema.json +24542 -0
  9. package/catalog/host.json +95 -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 +4152 -0
  14. package/catalog/recipes.json +64 -0
  15. package/catalog/strings.json +84 -0
  16. package/catalog/style.json +180 -0
  17. package/catalog/theme.schema.json +47754 -0
  18. package/catalog/tokens.json +47 -0
  19. package/conformance/README.md +218 -0
  20. package/conformance/baseline.ts +101 -0
  21. package/conformance/browser-page.ts +146 -0
  22. package/conformance/browser-suites.ts +699 -0
  23. package/conformance/compare.ts +201 -0
  24. package/conformance/dom.ts +97 -0
  25. package/conformance/dump.ts +163 -0
  26. package/conformance/fonts.json +157 -0
  27. package/conformance/manifest.json +233 -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/conformance/voice-wave-child.ts +102 -0
  36. package/dist/chunks/index-5fmaa0xq.js +8384 -0
  37. package/dist/chunks/index-5fmaa0xq.js.map +10 -0
  38. package/dist/chunks/index-62yzr94z.js +2784 -0
  39. package/dist/chunks/index-62yzr94z.js.map +10 -0
  40. package/dist/chunks/index-bg2jsgjq.js +8541 -0
  41. package/dist/chunks/index-bg2jsgjq.js.map +15 -0
  42. package/dist/chunks/index-fy7y2nz8.js +186 -0
  43. package/dist/chunks/index-fy7y2nz8.js.map +9 -0
  44. package/dist/chunks/index-gtk2gwsb.js +4555 -0
  45. package/dist/chunks/index-gtk2gwsb.js.map +31 -0
  46. package/dist/chunks/index-t6qz1770.js +95 -0
  47. package/dist/chunks/index-t6qz1770.js.map +11 -0
  48. package/dist/conformance.js +230462 -0
  49. package/dist/conformance.js.map +10 -0
  50. package/dist/entries/prompt.js +251 -0
  51. package/dist/entries/prompt.js.map +11 -0
  52. package/dist/entries/schema.js +1725 -0
  53. package/dist/entries/schema.js.map +11 -0
  54. package/dist/host/index.js +194 -0
  55. package/dist/host/index.js.map +9 -0
  56. package/dist/index.js +3851 -0
  57. package/dist/index.js.map +30 -0
  58. package/dist/types/src/a11y.d.ts +21 -0
  59. package/dist/types/src/animation.d.ts +76 -0
  60. package/dist/types/src/basic-map.d.ts +38 -0
  61. package/dist/types/src/builder.d.ts +50 -0
  62. package/dist/types/src/catalog.d.ts +54 -0
  63. package/dist/types/src/catalog.described.d.ts +33 -0
  64. package/dist/types/src/catalog.generated.d.ts +904 -0
  65. package/dist/types/src/chart.d.ts +46 -0
  66. package/dist/types/src/code.d.ts +44 -0
  67. package/dist/types/src/color.d.ts +20 -0
  68. package/dist/types/src/conformance.d.ts +39 -0
  69. package/dist/types/src/connector/app.d.ts +14 -0
  70. package/dist/types/src/connector/connector.d.ts +77 -0
  71. package/dist/types/src/connector/index.d.ts +4 -0
  72. package/dist/types/src/css.d.ts +27 -0
  73. package/dist/types/src/define.d.ts +66 -0
  74. package/dist/types/src/direction.d.ts +16 -0
  75. package/dist/types/src/dynamic.d.ts +161 -0
  76. package/dist/types/src/entries/prompt.d.ts +11 -0
  77. package/dist/types/src/entries/schema.d.ts +8 -0
  78. package/dist/types/src/expr.d.ts +71 -0
  79. package/dist/types/src/extension.d.ts +4 -0
  80. package/dist/types/src/format.d.ts +112 -0
  81. package/dist/types/src/geometry.d.ts +40 -0
  82. package/dist/types/src/host/contract.d.ts +225 -0
  83. package/dist/types/src/host/decoders.d.ts +61 -0
  84. package/dist/types/src/host/index.d.ts +15 -0
  85. package/dist/types/src/host/package.d.ts +102 -0
  86. package/dist/types/src/host/policy.d.ts +107 -0
  87. package/dist/types/src/host/record-store.d.ts +103 -0
  88. package/dist/types/src/host/records.d.ts +45 -0
  89. package/dist/types/src/host/router.d.ts +38 -0
  90. package/dist/types/src/host/runtime.d.ts +213 -0
  91. package/dist/types/src/host/signature.d.ts +52 -0
  92. package/dist/types/src/host/sources.d.ts +22 -0
  93. package/dist/types/src/host/tools.d.ts +42 -0
  94. package/dist/types/src/host/transports.d.ts +109 -0
  95. package/dist/types/src/index.d.ts +70 -0
  96. package/dist/types/src/layout.d.ts +20 -0
  97. package/dist/types/src/limits.d.ts +21 -0
  98. package/dist/types/src/list.d.ts +133 -0
  99. package/dist/types/src/locale.d.ts +22 -0
  100. package/dist/types/src/macros.d.ts +29 -0
  101. package/dist/types/src/overlay.d.ts +27 -0
  102. package/dist/types/src/prompt.d.ts +44 -0
  103. package/dist/types/src/qr-code.d.ts +30 -0
  104. package/dist/types/src/recipes.d.ts +17 -0
  105. package/dist/types/src/reducer.d.ts +41 -0
  106. package/dist/types/src/resizable.d.ts +38 -0
  107. package/dist/types/src/schema.d.ts +7 -0
  108. package/dist/types/src/strings.d.ts +13 -0
  109. package/dist/types/src/style.d.ts +201 -0
  110. package/dist/types/src/test-preload.d.ts +1 -0
  111. package/dist/types/src/text-clusters.d.ts +11 -0
  112. package/dist/types/src/text-shimmer.d.ts +24 -0
  113. package/dist/types/src/theme-types.d.ts +162 -0
  114. package/dist/types/src/theme.d.ts +75 -0
  115. package/dist/types/src/themes.d.ts +22 -0
  116. package/dist/types/src/tree-guides.d.ts +17 -0
  117. package/dist/types/src/types.d.ts +320 -0
  118. package/dist/types/src/validate.d.ts +31 -0
  119. package/dist/types/src/voice-wave.d.ts +181 -0
  120. package/dist/types/src/web/base-css.d.ts +1 -0
  121. package/dist/types/src/web/box-css.d.ts +63 -0
  122. package/dist/types/src/web/coerce.d.ts +32 -0
  123. package/dist/types/src/web/custom-icons.generated.d.ts +4 -0
  124. package/dist/types/src/web/data.d.ts +107 -0
  125. package/dist/types/src/web/dates.d.ts +12 -0
  126. package/dist/types/src/web/form.d.ts +44 -0
  127. package/dist/types/src/web/host-types.d.ts +126 -0
  128. package/dist/types/src/web/index.d.ts +35 -0
  129. package/dist/types/src/web/inputs.d.ts +55 -0
  130. package/dist/types/src/web/list.d.ts +16 -0
  131. package/dist/types/src/web/listbox.d.ts +14 -0
  132. package/dist/types/src/web/markdown.d.ts +96 -0
  133. package/dist/types/src/web/media.d.ts +63 -0
  134. package/dist/types/src/web/parts.d.ts +4 -0
  135. package/dist/types/src/web/primitives/alert-dialog.d.ts +10 -0
  136. package/dist/types/src/web/primitives/alert.d.ts +9 -0
  137. package/dist/types/src/web/primitives/avatar-color.d.ts +2 -0
  138. package/dist/types/src/web/primitives/avatar.d.ts +13 -0
  139. package/dist/types/src/web/primitives/badge.d.ts +10 -0
  140. package/dist/types/src/web/primitives/breadcrumb.d.ts +10 -0
  141. package/dist/types/src/web/primitives/button.d.ts +6 -0
  142. package/dist/types/src/web/primitives/calendar.d.ts +43 -0
  143. package/dist/types/src/web/primitives/card.d.ts +9 -0
  144. package/dist/types/src/web/primitives/checkbox.d.ts +5 -0
  145. package/dist/types/src/web/primitives/choice-tiles.d.ts +32 -0
  146. package/dist/types/src/web/primitives/cn.d.ts +3 -0
  147. package/dist/types/src/web/primitives/code-block.d.ts +24 -0
  148. package/dist/types/src/web/primitives/command.d.ts +134 -0
  149. package/dist/types/src/web/primitives/context-ring.d.ts +24 -0
  150. package/dist/types/src/web/primitives/dialog-arms.d.ts +6 -0
  151. package/dist/types/src/web/primitives/dialog.d.ts +24 -0
  152. package/dist/types/src/web/primitives/disclosure-header.d.ts +5 -0
  153. package/dist/types/src/web/primitives/dock.d.ts +42 -0
  154. package/dist/types/src/web/primitives/dropdown-menu.d.ts +14 -0
  155. package/dist/types/src/web/primitives/empty-state.d.ts +15 -0
  156. package/dist/types/src/web/primitives/glass-card.d.ts +4 -0
  157. package/dist/types/src/web/primitives/hover-card.d.ts +7 -0
  158. package/dist/types/src/web/primitives/icon-disc.d.ts +12 -0
  159. package/dist/types/src/web/primitives/index.d.ts +50 -0
  160. package/dist/types/src/web/primitives/input.d.ts +5 -0
  161. package/dist/types/src/web/primitives/kbd.d.ts +4 -0
  162. package/dist/types/src/web/primitives/label.d.ts +3 -0
  163. package/dist/types/src/web/primitives/measure.d.ts +16 -0
  164. package/dist/types/src/web/primitives/menu-surface.d.ts +17 -0
  165. package/dist/types/src/web/primitives/meter.d.ts +21 -0
  166. package/dist/types/src/web/primitives/popover.d.ts +3 -0
  167. package/dist/types/src/web/primitives/progress-ring.d.ts +5 -0
  168. package/dist/types/src/web/primitives/progress.d.ts +8 -0
  169. package/dist/types/src/web/primitives/scroll-area.d.ts +49 -0
  170. package/dist/types/src/web/primitives/segmented-control.d.ts +5 -0
  171. package/dist/types/src/web/primitives/select.d.ts +47 -0
  172. package/dist/types/src/web/primitives/separator.d.ts +3 -0
  173. package/dist/types/src/web/primitives/sheet-chrome.d.ts +4 -0
  174. package/dist/types/src/web/primitives/sheet.d.ts +20 -0
  175. package/dist/types/src/web/primitives/skeleton.d.ts +3 -0
  176. package/dist/types/src/web/primitives/spinner.d.ts +3 -0
  177. package/dist/types/src/web/primitives/switch.d.ts +4 -0
  178. package/dist/types/src/web/primitives/table.d.ts +11 -0
  179. package/dist/types/src/web/primitives/tabs.d.ts +13 -0
  180. package/dist/types/src/web/primitives/textarea.d.ts +3 -0
  181. package/dist/types/src/web/primitives/toast.d.ts +163 -0
  182. package/dist/types/src/web/primitives/tooltip.d.ts +3 -0
  183. package/dist/types/src/web/primitives/truncate.d.ts +7 -0
  184. package/dist/types/src/web/primitives/typeahead-portal.d.ts +12 -0
  185. package/dist/types/src/web/primitives/voice-wave.d.ts +50 -0
  186. package/dist/types/src/web/round5.d.ts +107 -0
  187. package/dist/types/src/web/share.d.ts +6 -0
  188. package/dist/types/src/web/table.d.ts +20 -0
  189. package/dist/types/src/web/theme-css.d.ts +69 -0
  190. package/dist/types/src/web/urls.d.ts +21 -0
  191. package/dist/web/index.js +2565 -0
  192. package/dist/web/index.js.map +28 -0
  193. package/dist/web/primitives/index.js +1476 -0
  194. package/dist/web/primitives/index.js.map +57 -0
  195. package/docs/components.generated.json +6783 -0
  196. package/docs/package-format.md +182 -0
  197. package/docs/round-1-contract.md +679 -0
  198. package/docs/round-2-contract.md +476 -0
  199. package/docs/round-3-contract.md +174 -0
  200. package/docs/round-5-contract.md +268 -0
  201. package/docs/themes.generated.json +3647 -0
  202. package/fixtures/animations.json +2890 -0
  203. package/fixtures/bench-list.json +58 -0
  204. package/fixtures/bind-time.json +54734 -0
  205. package/fixtures/catalog-basic-map.json +1692 -0
  206. package/fixtures/catalog-components.json +10293 -0
  207. package/fixtures/catalog-extension.json +620 -0
  208. package/fixtures/catalog-macros.json +51805 -0
  209. package/fixtures/code-tokens.json +1993 -0
  210. package/fixtures/conformance-baseline.json +59016 -0
  211. package/fixtures/conformance-cases.json +49 -0
  212. package/fixtures/conformance-known.json +2111 -0
  213. package/fixtures/control-geometry.json +5129 -0
  214. package/fixtures/dashboard.data.json +74 -0
  215. package/fixtures/dashboard.json +781 -0
  216. package/fixtures/demo-surface.json +47 -0
  217. package/fixtures/format.json +870 -0
  218. package/fixtures/host-package.json +1262 -0
  219. package/fixtures/host-policy.json +1209 -0
  220. package/fixtures/host-router.json +2034 -0
  221. package/fixtures/host-transport.json +590 -0
  222. package/fixtures/interactions-round1.json +1058 -0
  223. package/fixtures/kitchen-sink.expanded.json +11972 -0
  224. package/fixtures/kitchen-sink.json +3140 -0
  225. package/fixtures/layout-geometry-round1.json +7278 -0
  226. package/fixtures/layout-geometry.json +2072 -0
  227. package/fixtures/overlay-geometry.json +272 -0
  228. package/fixtures/pointer.json +89 -0
  229. package/fixtures/primitive-specimens.json +59 -0
  230. package/fixtures/prompt-budget.json +33 -0
  231. package/fixtures/qr-code.json +480 -0
  232. package/fixtures/resizable.json +714 -0
  233. package/fixtures/round4-contract.json +2985 -0
  234. package/fixtures/specimens.json +11168 -0
  235. package/fixtures/style-conditions.json +336 -0
  236. package/fixtures/template-items.json +2215 -0
  237. package/fixtures/text-clusters.json +65 -0
  238. package/fixtures/text-direction.json +309 -0
  239. package/fixtures/text-shimmer.json +96 -0
  240. package/fixtures/theme-extends.json +573 -0
  241. package/fixtures/theme-invalid.json +1300 -0
  242. package/fixtures/theme-recipes.json +61762 -0
  243. package/fixtures/theme-resolved.json +25969 -0
  244. package/fixtures/toast-stack.json +215 -0
  245. package/fixtures/tree-guides.json +224 -0
  246. package/fixtures/virtual-list.json +733 -0
  247. package/fixtures/voice-wave.json +19236 -0
  248. package/generated/ExponentialUICatalog.generated.kt +67 -0
  249. package/generated/ExponentialUICatalog.generated.swift +67 -0
  250. package/generated/ExponentialUIThemes.generated.kt +23 -0
  251. package/generated/ExponentialUIThemes.generated.swift +23 -0
  252. package/generated/catalog.descriptions.generated.rs +6 -0
  253. package/generated/catalog.generated.rs +80 -0
  254. package/generated/catalog.themes.generated.rs +34 -0
  255. package/generated/core.descriptions.json +2726 -0
  256. package/generated/core.runtime.json +3944 -0
  257. package/package.json +87 -4
  258. package/themes/exponential.recipes.json +49 -0
  259. package/themes/exponential.theme.json +768 -0
  260. package/themes/neutral.theme.json +6178 -0
  261. package/themes/orange.theme.json +43 -0
  262. package/themes/playful.theme.json +219 -0
  263. package/vendor/a2ui/LICENSE.txt +203 -0
  264. package/vendor/a2ui/README.md +27 -0
  265. package/vendor/a2ui/v0_9/basic_catalog.json +1387 -0
  266. package/vendor/a2ui/v0_9/basic_catalog_rules.txt +5 -0
  267. package/vendor/a2ui/v0_9/client_capabilities.json +97 -0
  268. package/vendor/a2ui/v0_9/client_to_server.json +104 -0
  269. package/vendor/a2ui/v0_9/common_types.json +315 -0
  270. package/vendor/a2ui/v0_9/server_to_client.json +132 -0
  271. package/vendor/json-render/LICENSE.txt +201 -0
  272. package/vendor/json-render/README.md +13 -0
  273. package/vendor/nayuki-qr/LICENSE.txt +20 -0
  274. package/vendor/nayuki-qr/README.md +10 -0
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ui.exponential.at/conformance/report.schema.json",
4
+ "title": "Exponential UI conformance report",
5
+ "description": "VAPP-91: what a renderer's conformance runner writes. checkReport() / conformance:check decide conformance against conformance/manifest.json.",
6
+ "type": "object",
7
+ "required": [
8
+ "renderer",
9
+ "platform",
10
+ "version",
11
+ "conformanceVersion",
12
+ "suites"
13
+ ],
14
+ "properties": {
15
+ "renderer": {
16
+ "type": "string",
17
+ "description": "The renderer's package name, e.g. @exponential_app/eui-react, exponential-ui-gpui, ExponentialUI, at.exponential:eui-compose."
18
+ },
19
+ "platform": {
20
+ "type": "string",
21
+ "description": "web | desktop | ios | macos | android | \u2026"
22
+ },
23
+ "version": {
24
+ "type": "string",
25
+ "description": "The renderer's version."
26
+ },
27
+ "conformanceVersion": {
28
+ "type": "integer"
29
+ },
30
+ "suites": {
31
+ "type": "object",
32
+ "additionalProperties": {
33
+ "type": "object",
34
+ "required": [
35
+ "cases",
36
+ "passed",
37
+ "failed"
38
+ ],
39
+ "properties": {
40
+ "cases": {
41
+ "type": "integer",
42
+ "minimum": 0
43
+ },
44
+ "passed": {
45
+ "type": "integer",
46
+ "minimum": 0
47
+ },
48
+ "failed": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ },
53
+ "description": "The failed cases' names (and a short reason)."
54
+ },
55
+ "skipped": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "string"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "additionalProperties": false
66
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bun
2
+ // `bun run --filter @exponential_app/eui conformance` — the renderer
3
+ // conformance harness, everything this machine can run:
4
+ // 1. the WEB dump: the React renderer in headless Chromium with the
5
+ // conformance fonts (packages/exponential-ui-react/scripts/conformance-web.ts),
6
+ // compared with the committed baseline (fixtures/conformance-baseline.json)
7
+ // 2. the DESKTOP dump: the gpui painter headless with gpui's own Linux text
8
+ // system and the same font files (cargo example `conformance_dump` of
9
+ // apps/desktop/crates/eui-gpui), compared with the baseline
10
+ // 3. (`--pixels`) the perceptual step: the web surface PNG next to the
11
+ // desktop window's capture (Xvfb), a pixel-difference ratio + a global
12
+ // SSIM per case — REPORTED, never gating
13
+ // and prints the tables, the divergence origins grouped across cases and
14
+ // writes the dumps + a JSON report to `--out` (default $TMPDIR/xui-conformance).
15
+ //
16
+ // --only <substring> cases whose key contains it
17
+ // --web <dump.json> reuse a web dump --skip-web
18
+ // --desktop <dump> reuse a desktop dump --skip-desktop
19
+ // --write-baseline rewrite the baseline from the web dump (whole matrix)
20
+ // --details every case's divergence list (default: grouped)
21
+ // --pixels [substring] the perceptual step (default: kitchen-sink/*/dark/900/ltr
22
+ // and responsive/*/dark/390/rtl)
23
+
24
+ import { spawnSync } from "node:child_process"
25
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
26
+ import { tmpdir } from "node:os"
27
+ import { join } from "node:path"
28
+ import { decodeBaseline, type Baseline } from "./baseline"
29
+ import { compareDumps, formatCase, formatTable, groupFindings, type Report } from "./compare"
30
+ import { allCases, BASELINE_PATH, MANIFEST_PATH, type ConformanceManifest, type Dump } from "./dump"
31
+
32
+ const repoRoot = join(import.meta.dir, `..`, `..`, `..`)
33
+ const argv = process.argv.slice(2)
34
+ const flag = (name: string) => argv.includes(`--${name}`)
35
+ const arg = (name: string) => {
36
+ const i = argv.indexOf(`--${name}`)
37
+ return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith(`--`) ? argv[i + 1] : undefined
38
+ }
39
+ const outDir = arg(`out`) ?? join(tmpdir(), `xui-conformance`)
40
+ mkdirSync(outDir, { recursive: true })
41
+ const manifest = JSON.parse(readFileSync(join(repoRoot, MANIFEST_PATH), `utf8`)) as ConformanceManifest
42
+ const only = arg(`only`)
43
+ const keys = allCases(manifest)
44
+ .map((c) => c.key)
45
+ .filter((k) => !only || k.includes(only))
46
+ const tol = manifest.tolerance
47
+
48
+ function run(cmd: string, args: string[], cwd: string): boolean {
49
+ console.error(`$ ${cmd} ${args.join(` `)}`)
50
+ const r = spawnSync(cmd, args, { cwd, stdio: [`ignore`, `inherit`, `inherit`], env: process.env })
51
+ if (r.error) console.error(String(r.error))
52
+ return r.status === 0
53
+ }
54
+
55
+ const readDump = (path: string) => JSON.parse(readFileSync(path, `utf8`)) as Dump
56
+ const baselinePath = join(repoRoot, BASELINE_PATH)
57
+ const loadBaseline = () => (existsSync(baselinePath) ? decodeBaseline(JSON.parse(readFileSync(baselinePath, `utf8`)) as Baseline) : null)
58
+
59
+ // 1. web
60
+ let web: Dump | null = null
61
+ const webPath = arg(`web`) ?? join(outDir, `web.json`)
62
+ if (!flag(`skip-web`)) {
63
+ if (!arg(`web`)) {
64
+ const args = [`scripts/conformance-web.ts`, `--out`, webPath]
65
+ if (only) args.push(`--only`, only)
66
+ if (flag(`write-baseline`)) args.push(`--write-baseline`)
67
+ if (!run(`bun`, args, join(repoRoot, `packages/exponential-ui-react`))) console.error(`web dump FAILED (is Chromium installed? bunx playwright install chromium)`)
68
+ }
69
+ if (existsSync(webPath)) web = readDump(webPath)
70
+ }
71
+ const baseline = loadBaseline()
72
+ const reports: Record<string, Report> = {}
73
+ if (!baseline) console.error(`no baseline at ${BASELINE_PATH}: run with --write-baseline`)
74
+ if (web && baseline) {
75
+ reports.web = compareDumps(baseline, web, tol, keys)
76
+ console.log(`\n# web vs the committed baseline\n`)
77
+ console.log(formatTable(reports.web))
78
+ }
79
+
80
+ // 2. desktop
81
+ const desktopPath = arg(`desktop`) ?? join(outDir, `desktop.json`)
82
+ if (!flag(`skip-desktop`)) {
83
+ if (!arg(`desktop`)) {
84
+ const args = [`run`, `-q`, `-p`, `eui-gpui`, `--example`, `conformance_dump`, `--`, `--out`, desktopPath]
85
+ if (only) args.push(`--only`, only)
86
+ if (process.platform !== `linux` && process.platform !== `freebsd`) console.error(`desktop dump: Linux/FreeBSD only for now — skipped`)
87
+ else if (!run(`cargo`, args, join(repoRoot, `apps/desktop`))) console.error(`desktop dump FAILED`)
88
+ }
89
+ if (existsSync(desktopPath) && baseline) {
90
+ reports.desktop = compareDumps(baseline, readDump(desktopPath), tol, keys)
91
+ console.log(`\n# desktop (gpui, headless) vs the web baseline\n`)
92
+ console.log(formatTable(reports.desktop))
93
+ const findings = groupFindings(reports.desktop)
94
+ console.log(`\n## divergence origins across cases (${findings.length}; Δ = desktop − web)\n`)
95
+ for (const f of findings.slice(0, flag(`details`) ? findings.length : 80))
96
+ console.log(` ${String(f.cases).padStart(3)}× ${f.kinds.padEnd(13)} ${f.id} (${f.component}) Δw ${f.dw[0]}…${f.dw[1]} Δh ${f.dh[0]}…${f.dh[1]}${f.text ? ` "${f.text.slice(0, 32)}"` : ``}\n e.g. ${f.example}`)
97
+ if (!flag(`details`) && findings.length > 80) console.log(` … ${findings.length - 80} more (--details)`)
98
+ if (flag(`details`)) for (const c of reports.desktop.cases) console.log(`\n${formatCase(c)}`)
99
+ }
100
+ }
101
+
102
+ // 3. pixels
103
+ if (flag(`pixels`)) {
104
+ const sel = arg(`pixels`)
105
+ const pixelKeys = keys.filter((k) => (sel ? k.includes(sel) : /^(kitchen-sink\/[^/]+\/dark\/900\/ltr|responsive\/[^/]+\/dark\/390\/rtl)$/.test(k)))
106
+ const { pixelStep } = await import(`./pixel`)
107
+ const rows = await pixelStep(repoRoot, outDir, pixelKeys)
108
+ console.log(`\n# perceptual (reported, never gating)\n`)
109
+ for (const r of rows) console.log(` ${r.key.padEnd(44)} ${r.note ?? `diff ${(r.diffRatio! * 100).toFixed(2)}% SSIM ${r.ssim!.toFixed(4)} (${r.size}) ${r.files}`}`)
110
+ }
111
+
112
+ writeFileSync(join(outDir, `report.json`), JSON.stringify(reports, null, 1))
113
+ console.log(`\ndumps + report.json in ${outDir}`)
114
+ const webBad = reports.web ? reports.web.cases.some((c) => c.diffs.length || c.onlyRef.length || c.onlyCand.length) || reports.web.missingCases.length > 0 : false
115
+ if (webBad) {
116
+ console.log(`\nthe WEB dump drifted from the baseline (see above): fix the renderer, or rewrite the baseline with --write-baseline when the change is intended`)
117
+ process.exitCode = 1
118
+ }
@@ -0,0 +1,102 @@
1
+ // NEW-77: THE CHILD RULE of VoiceWave (catalog/style.json `voiceWave`)
2
+ // measured on a WEB painter in headless Chromium: every `child` case of
3
+ // fixtures/voice-wave.json is painted (the native through the harness's
4
+ // `?view=mount`, the primitive through `?view=primitives&id=plain/voice-wave`,
5
+ // whose first five waves are these cases in order) and the DRAWN child box
6
+ // (getBoundingClientRect, transforms included, relative to the wave's root)
7
+ // must sit within 0.5 px of `expected.child`. The renderers' browser tests
8
+ // (voice-wave-child.test.ts) call these.
9
+
10
+ import { readFileSync } from "node:fs"
11
+ import { join } from "node:path"
12
+ import type { Page } from "playwright"
13
+ import type { NestedNode } from "../src/index"
14
+
15
+ export interface VoiceWaveChildCase {
16
+ name: string
17
+ input: { size: number; activity: string; level?: number; bands?: number[]; child: { width: number; height: number } }
18
+ expected: { x: number; y: number; width: number; height: number; scale: number }
19
+ }
20
+
21
+ export interface DrawnBox {
22
+ x: number
23
+ y: number
24
+ width: number
25
+ height: number
26
+ }
27
+
28
+ /** The px edge → the size enum (the neutral theme's `VoiceWave/root`, the
29
+ * primitives' VOICE_WAVE_SIZE_PX). */
30
+ const SIZE_NAME: Record<number, string> = { 32: `sm`, 56: `md`, 160: `lg`, 200: `xl` }
31
+
32
+ export const VOICE_WAVE_CHILD_TOLERANCE = 0.5
33
+
34
+ export function voiceWaveChildCases(): VoiceWaveChildCase[] {
35
+ const fixture = JSON.parse(readFileSync(join(import.meta.dirname, `..`, `fixtures`, `voice-wave.json`), `utf8`)) as { cases: { name: string; input: VoiceWaveChildCase[`input`] & { child?: unknown }; expected: { child?: VoiceWaveChildCase[`expected`] } }[] }
36
+ return fixture.cases.filter((c) => c.input.child && c.expected.child).map((c) => ({ name: c.name, input: c.input, expected: c.expected.child! }))
37
+ }
38
+
39
+ /** One VoiceWave per case (`vw<i>`), its one child a Box of the case's
40
+ * child size (`vw<i>-c`), in a wrapping row. */
41
+ export function voiceWaveChildTree(cases = voiceWaveChildCases()): NestedNode {
42
+ return {
43
+ id: `root`,
44
+ component: `Box`,
45
+ style: { display: `flex`, flexDirection: `row`, flexWrap: `wrap`, alignItems: `center`, gap: 24, padding: 24 },
46
+ children: cases.map((c, i) => {
47
+ const size = SIZE_NAME[c.input.size]
48
+ if (!size) throw new Error(`${c.name}: no size enum for ${c.input.size} px`)
49
+ return {
50
+ id: `vw${i}`,
51
+ component: `VoiceWave`,
52
+ props: { activity: c.input.activity, level: c.input.level ?? 0, ...(c.input.bands ? { bands: c.input.bands } : {}), size },
53
+ children: [{ id: `vw${i}-c`, component: `Box`, style: { width: c.input.child.width, height: c.input.child.height } }],
54
+ }
55
+ }),
56
+ } as NestedNode
57
+ }
58
+
59
+ /** `true`, or what is off (x, y, width, height beyond the tolerance). */
60
+ export function checkVoiceWaveChild(expected: VoiceWaveChildCase[`expected`], drawn: DrawnBox | null): true | string {
61
+ if (!drawn) return `child not painted`
62
+ const off = ([`x`, `y`, `width`, `height`] as const).filter((k) => !(Math.abs(drawn[k] - expected[k]) <= VOICE_WAVE_CHILD_TOLERANCE))
63
+ return off.length === 0 || off.map((k) => `${k} ${expected[k]}≠${drawn[k].toFixed(2)}`).join(`, `)
64
+ }
65
+
66
+ /** The drawn child of each [root selector, child selector] pair, relative
67
+ * to its root (null = not found), after two frames (the measure lands
68
+ * before the first paint; the frames let any re-measure settle). */
69
+ export function drawnChildBoxes(page: Page, pairs: [string, string][]): Promise<(DrawnBox | null)[]> {
70
+ return page.evaluate(async (pairs) => {
71
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
72
+ return pairs.map(([rootSel, childSel]) => {
73
+ const root = document.querySelector(rootSel)
74
+ const child = document.querySelector(childSel)
75
+ if (!root || !child) return null
76
+ const r = root.getBoundingClientRect()
77
+ const c = child.getBoundingClientRect()
78
+ return { x: c.left - r.left, y: c.top - r.top, width: c.width, height: c.height }
79
+ })
80
+ }, pairs)
81
+ }
82
+
83
+ /** The native (a page at `?view=mount`): mounts the tree, measures. */
84
+ export async function measureNativeVoiceWaveChildren(page: Page, cases = voiceWaveChildCases()): Promise<{ name: string; verdict: true | string }[]> {
85
+ await page.waitForFunction(() => typeof (window as unknown as { __xuiMount?: unknown }).__xuiMount === `function`)
86
+ await page.evaluate((tree) => (window as unknown as { __xuiMount: (t: unknown, o?: unknown) => Promise<void> }).__xuiMount(tree, { theme: `neutral` }), voiceWaveChildTree(cases))
87
+ const boxes = await drawnChildBoxes(
88
+ page,
89
+ cases.map((_, i) => [`[data-xui-id="vw${i}"]`, `[data-xui-id="vw${i}-c"]`])
90
+ )
91
+ return cases.map((c, i) => ({ name: c.name, verdict: checkVoiceWaveChild(c.expected, boxes[i]!) }))
92
+ }
93
+
94
+ /** The primitive (a page at `?view=primitives&id=plain/voice-wave`). */
95
+ export async function measurePrimitiveVoiceWaveChildren(page: Page, cases = voiceWaveChildCases()): Promise<{ name: string; verdict: true | string }[]> {
96
+ await page.waitForSelector(`[data-voice-case="${cases.length - 1}"] [data-slot="voice-wave"]`, { state: `attached` })
97
+ const boxes = await drawnChildBoxes(
98
+ page,
99
+ cases.map((_, i) => [`[data-voice-case="${i}"] [data-slot="voice-wave"]`, `[data-voice-case="${i}"] [data-voice-child]`])
100
+ )
101
+ return cases.map((c, i) => ({ name: c.name, verdict: checkVoiceWaveChild(c.expected, boxes[i]!) }))
102
+ }