@pixel-point/toolcraft 0.0.2

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 (257) hide show
  1. package/LICENSE.md +98 -0
  2. package/README.md +41 -0
  3. package/bin/create-toolcraft-app.mjs +8 -0
  4. package/bin/toolcraft.mjs +8 -0
  5. package/package.json +24 -0
  6. package/scripts/prepare-pack.mjs +29 -0
  7. package/src/cli.mjs +392 -0
  8. package/src/cli.test.mjs +284 -0
  9. package/src/copy-recursive.mjs +86 -0
  10. package/src/generate.mjs +212 -0
  11. package/src/generate.test.mjs +322 -0
  12. package/src/import-map.mjs +14 -0
  13. package/src/package-json.mjs +80 -0
  14. package/src/package-json.test.mjs +67 -0
  15. package/src/rewrite-imports.mjs +85 -0
  16. package/src/rewrite-imports.test.mjs +58 -0
  17. package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
  18. package/templates/runtime/contracts/component-contracts.ts +1340 -0
  19. package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
  20. package/templates/runtime/contracts/decision-contracts.ts +283 -0
  21. package/templates/runtime/contracts/index.test.ts +14 -0
  22. package/templates/runtime/contracts/index.ts +3 -0
  23. package/templates/runtime/contracts/types.ts +56 -0
  24. package/templates/runtime/export/export.test.ts +203 -0
  25. package/templates/runtime/export/export.ts +132 -0
  26. package/templates/runtime/export/index.ts +1 -0
  27. package/templates/runtime/index.ts +14 -0
  28. package/templates/runtime/react/canvas-shell.test.tsx +424 -0
  29. package/templates/runtime/react/canvas-shell.tsx +408 -0
  30. package/templates/runtime/react/control-renderers.ts +31 -0
  31. package/templates/runtime/react/controls-panel.test.tsx +3736 -0
  32. package/templates/runtime/react/controls-panel.tsx +2327 -0
  33. package/templates/runtime/react/curve-geometry.test.ts +70 -0
  34. package/templates/runtime/react/index.ts +15 -0
  35. package/templates/runtime/react/layer-tree.ts +96 -0
  36. package/templates/runtime/react/layers-panel.test.tsx +487 -0
  37. package/templates/runtime/react/layers-panel.tsx +1348 -0
  38. package/templates/runtime/react/media-file.ts +82 -0
  39. package/templates/runtime/react/panel-host-config.ts +80 -0
  40. package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
  41. package/templates/runtime/react/panel-host-geometry.ts +109 -0
  42. package/templates/runtime/react/panel-host-types.ts +74 -0
  43. package/templates/runtime/react/panel-host.test.tsx +102 -0
  44. package/templates/runtime/react/panel-host.tsx +353 -0
  45. package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
  46. package/templates/runtime/react/settings-transfer.test.ts +150 -0
  47. package/templates/runtime/react/settings-transfer.ts +279 -0
  48. package/templates/runtime/react/storage-key-migration.ts +48 -0
  49. package/templates/runtime/react/theme-runtime.tsx +177 -0
  50. package/templates/runtime/react/timeline-panel.test.tsx +668 -0
  51. package/templates/runtime/react/timeline-panel.tsx +2953 -0
  52. package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
  53. package/templates/runtime/react/toolbar-panel.tsx +205 -0
  54. package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
  55. package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
  56. package/templates/runtime/react/toolcraft-app.tsx +81 -0
  57. package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
  58. package/templates/runtime/react/toolcraft-root.tsx +203 -0
  59. package/templates/runtime/react/use-toolcraft.ts +41 -0
  60. package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
  61. package/templates/runtime/schema/define-toolcraft.ts +1442 -0
  62. package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
  63. package/templates/runtime/schema/keyframe-capability.ts +51 -0
  64. package/templates/runtime/schema/runtime-targets.ts +40 -0
  65. package/templates/runtime/schema/types.ts +370 -0
  66. package/templates/runtime/state/canvas-zoom.ts +8 -0
  67. package/templates/runtime/state/create-template-state.test.ts +242 -0
  68. package/templates/runtime/state/create-template-state.ts +95 -0
  69. package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
  70. package/templates/runtime/state/keyframe-evaluation.ts +203 -0
  71. package/templates/runtime/state/persistence.test.ts +217 -0
  72. package/templates/runtime/state/persistence.ts +511 -0
  73. package/templates/runtime/state/reducer.test.ts +937 -0
  74. package/templates/runtime/state/reducer.ts +1212 -0
  75. package/templates/runtime/state/timeline-readiness.ts +43 -0
  76. package/templates/runtime/state/types.ts +242 -0
  77. package/templates/runtime/styles.css +125 -0
  78. package/templates/runtime/testing/performance.test.ts +1058 -0
  79. package/templates/runtime/testing/performance.ts +1078 -0
  80. package/templates/starter/AGENTS.md +186 -0
  81. package/templates/starter/LICENSE.md +98 -0
  82. package/templates/starter/NOTICE.md +8 -0
  83. package/templates/starter/docs/toolcraft/README.md +41 -0
  84. package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
  85. package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
  86. package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
  87. package/templates/starter/docs/toolcraft/component-rules.md +299 -0
  88. package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
  89. package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
  90. package/templates/starter/docs/toolcraft/performance.md +112 -0
  91. package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
  92. package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
  93. package/templates/starter/docs/toolcraft/workflow.md +87 -0
  94. package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
  95. package/templates/starter/e2e/app-controls.spec.ts +41 -0
  96. package/templates/starter/e2e/app-performance.spec.ts +326 -0
  97. package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
  98. package/templates/starter/e2e/performance-helpers.ts +612 -0
  99. package/templates/starter/e2e/product-observable-helpers.ts +170 -0
  100. package/templates/starter/index.html +12 -0
  101. package/templates/starter/package.json +52 -0
  102. package/templates/starter/playwright.config.ts +43 -0
  103. package/templates/starter/scripts/check-ai-skills.mjs +95 -0
  104. package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
  105. package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
  106. package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
  107. package/templates/starter/scripts/toolcraft-port.mjs +54 -0
  108. package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
  109. package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
  110. package/templates/starter/src/app/starter-acceptance.ts +2646 -0
  111. package/templates/starter/src/app/starter-performance.test.ts +1390 -0
  112. package/templates/starter/src/app/starter-performance.ts +12 -0
  113. package/templates/starter/src/app/starter-schema.test.ts +70 -0
  114. package/templates/starter/src/app/starter-schema.ts +15 -0
  115. package/templates/starter/src/main.tsx +18 -0
  116. package/templates/starter/src/router.tsx +16 -0
  117. package/templates/starter/src/routes/index.tsx +7 -0
  118. package/templates/starter/src/routes/root.tsx +19 -0
  119. package/templates/starter/src/styles.css +120 -0
  120. package/templates/starter/tsconfig.json +11 -0
  121. package/templates/starter/vite.config.ts +13 -0
  122. package/templates/ui/components/composites/accordion.tsx +73 -0
  123. package/templates/ui/components/composites/alert-dialog.tsx +190 -0
  124. package/templates/ui/components/composites/alert.tsx +74 -0
  125. package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
  126. package/templates/ui/components/composites/avatar.tsx +98 -0
  127. package/templates/ui/components/composites/badge.tsx +69 -0
  128. package/templates/ui/components/composites/breadcrumb.tsx +106 -0
  129. package/templates/ui/components/composites/card.tsx +91 -0
  130. package/templates/ui/components/composites/combobox.tsx +486 -0
  131. package/templates/ui/components/composites/command.tsx +296 -0
  132. package/templates/ui/components/composites/context-menu.tsx +247 -0
  133. package/templates/ui/components/composites/dialog.tsx +282 -0
  134. package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
  135. package/templates/ui/components/composites/empty.tsx +110 -0
  136. package/templates/ui/components/composites/hover-card.tsx +44 -0
  137. package/templates/ui/components/composites/index.ts +30 -0
  138. package/templates/ui/components/composites/menubar.tsx +214 -0
  139. package/templates/ui/components/composites/navigation-menu.tsx +167 -0
  140. package/templates/ui/components/composites/pagination.tsx +131 -0
  141. package/templates/ui/components/composites/progress.tsx +72 -0
  142. package/templates/ui/components/composites/radio-group.tsx +84 -0
  143. package/templates/ui/components/composites/resizable.tsx +42 -0
  144. package/templates/ui/components/composites/sheet.tsx +153 -0
  145. package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
  146. package/templates/ui/components/composites/sidebar.tsx +431 -0
  147. package/templates/ui/components/composites/sonner.tsx +35 -0
  148. package/templates/ui/components/composites/spinner.tsx +43 -0
  149. package/templates/ui/components/composites/table.tsx +108 -0
  150. package/templates/ui/components/composites/tabs.tsx +83 -0
  151. package/templates/ui/components/control-layout/index.tsx +437 -0
  152. package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
  153. package/templates/ui/components/controls/actions/index.ts +9 -0
  154. package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
  155. package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
  156. package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
  157. package/templates/ui/components/controls/boolean/index.ts +4 -0
  158. package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
  159. package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
  160. package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
  161. package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
  162. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
  163. package/templates/ui/components/controls/code-textarea/index.ts +4 -0
  164. package/templates/ui/components/controls/color/color-control.tsx +571 -0
  165. package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
  166. package/templates/ui/components/controls/color/index.ts +41 -0
  167. package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
  168. package/templates/ui/components/controls/color/palette-control.tsx +535 -0
  169. package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
  170. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
  171. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
  172. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
  173. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
  174. package/templates/ui/components/controls/control-types.ts +43 -0
  175. package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
  176. package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
  177. package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
  178. package/templates/ui/components/controls/curves/index.ts +6 -0
  179. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
  180. package/templates/ui/components/controls/file-drop/index.ts +5 -0
  181. package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
  182. package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
  183. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
  184. package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
  185. package/templates/ui/components/controls/font-picker/index.ts +24 -0
  186. package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
  187. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
  188. package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
  189. package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
  190. package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
  191. package/templates/ui/components/controls/gradient/index.ts +4 -0
  192. package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
  193. package/templates/ui/components/controls/image-picker/index.ts +7 -0
  194. package/templates/ui/components/controls/index.ts +192 -0
  195. package/templates/ui/components/controls/range-input/index.ts +4 -0
  196. package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
  197. package/templates/ui/components/controls/range-slider/index.ts +4 -0
  198. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
  199. package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
  200. package/templates/ui/components/controls/segmented/index.ts +8 -0
  201. package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
  202. package/templates/ui/components/controls/select/index.ts +4 -0
  203. package/templates/ui/components/controls/select/select-control.tsx +223 -0
  204. package/templates/ui/components/controls/slider/index.ts +4 -0
  205. package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
  206. package/templates/ui/components/controls/slider/slider-value.ts +56 -0
  207. package/templates/ui/components/controls/text-input/index.ts +4 -0
  208. package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
  209. package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
  210. package/templates/ui/components/controls/vector/index.ts +8 -0
  211. package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
  212. package/templates/ui/components/panel/index.ts +19 -0
  213. package/templates/ui/components/panel/panel-actions.tsx +165 -0
  214. package/templates/ui/components/panel/panel-header.tsx +61 -0
  215. package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
  216. package/templates/ui/components/panel/panel-section.tsx +168 -0
  217. package/templates/ui/components/panel/panel-surface.tsx +206 -0
  218. package/templates/ui/components/panel/panel.tsx +210 -0
  219. package/templates/ui/components/primitives/animated-loader.tsx +61 -0
  220. package/templates/ui/components/primitives/button-group.tsx +134 -0
  221. package/templates/ui/components/primitives/button.tsx +429 -0
  222. package/templates/ui/components/primitives/checkbox.tsx +62 -0
  223. package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
  224. package/templates/ui/components/primitives/field.tsx +225 -0
  225. package/templates/ui/components/primitives/index.ts +82 -0
  226. package/templates/ui/components/primitives/input-group.tsx +298 -0
  227. package/templates/ui/components/primitives/input.tsx +61 -0
  228. package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
  229. package/templates/ui/components/primitives/label.tsx +16 -0
  230. package/templates/ui/components/primitives/popover.tsx +126 -0
  231. package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
  232. package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
  233. package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
  234. package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
  235. package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
  236. package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
  237. package/templates/ui/components/primitives/select.tsx +408 -0
  238. package/templates/ui/components/primitives/selection-state.ts +31 -0
  239. package/templates/ui/components/primitives/separator.tsx +21 -0
  240. package/templates/ui/components/primitives/slider/index.ts +4 -0
  241. package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
  242. package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
  243. package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
  244. package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
  245. package/templates/ui/components/primitives/slider/slider.tsx +511 -0
  246. package/templates/ui/components/primitives/switch.tsx +35 -0
  247. package/templates/ui/components/primitives/textarea.tsx +49 -0
  248. package/templates/ui/components/primitives/toggle-group.tsx +114 -0
  249. package/templates/ui/components/primitives/toggle.tsx +46 -0
  250. package/templates/ui/components/primitives/tooltip.tsx +100 -0
  251. package/templates/ui/hooks/use-mobile.ts +21 -0
  252. package/templates/ui/index.ts +31 -0
  253. package/templates/ui/lib/control-outline.ts +3 -0
  254. package/templates/ui/lib/input-control-style.ts +131 -0
  255. package/templates/ui/lib/style-guide-color-utils.ts +111 -0
  256. package/templates/ui/lib/utils.ts +6 -0
  257. package/templates/ui/styles.css +291 -0
@@ -0,0 +1,785 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { expect, test } from "@playwright/test";
6
+
7
+ import {
8
+ getRequiredToolcraftControlPartCoverage,
9
+ starterAcceptance,
10
+ starterTransferMode,
11
+ } from "../src/app/starter-acceptance";
12
+ import { starterSchema } from "../src/app/starter-schema";
13
+ import { expectNoForbiddenCanvasUi } from "./canvas-handle-helpers";
14
+ import { expectToolcraftSegmentedControlCellsPreservePadding } from "./performance-helpers";
15
+ import {
16
+ expectToolcraftProductObservableToChange,
17
+ getToolcraftProductObservableSnapshot,
18
+ } from "./product-observable-helpers";
19
+
20
+ const currentFileName = basename(fileURLToPath(import.meta.url));
21
+ const e2eDir = dirname(fileURLToPath(import.meta.url));
22
+
23
+ type BrowserTestSource = {
24
+ fileName: string;
25
+ source: string;
26
+ };
27
+
28
+ function escapeRegExp(value: string): string {
29
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ }
31
+
32
+ function stripJsComments(source: string): string {
33
+ return source
34
+ .replace(/\/\*[\s\S]*?\*\//g, "")
35
+ .replace(/(^|[^:])\/\/.*$/gm, "$1");
36
+ }
37
+
38
+ function hasRealLayerRowInteraction(source: string): boolean {
39
+ return /data-layer-id|data-template-layer-name|selectLayerByName\s*\(|layerRowByName\s*\(|getByRole\s*\(\s*(["'`])option\1/i.test(
40
+ source,
41
+ );
42
+ }
43
+
44
+ function hasRealLayerVisibilityInteraction(source: string): boolean {
45
+ return /toggleLayerVisibilityByName\s*\(|getByRole\s*\([\s\S]*?(Hide|Show|Disable|Enable).*layer|aria-label[\s\S]*?(Hide|Show|Disable|Enable)/i.test(
46
+ source,
47
+ );
48
+ }
49
+
50
+ function hasRealLayerDragInteraction(source: string): boolean {
51
+ return /\.dragTo\s*\(|page\.mouse\.(?:down|move|up)\s*\(|dragLayer(?:Before|After|ToGroup|ByName)?\s*\(/i.test(
52
+ source,
53
+ );
54
+ }
55
+
56
+ function hasLayerGroupTarget(source: string): boolean {
57
+ return /data-template-layer-kind[\s\S]*group|groupLayerByName\s*\(|dragLayerToGroup\s*\(|getByRole\s*\(\s*(["'`])option\1[\s\S]*Group/i.test(
58
+ source,
59
+ );
60
+ }
61
+
62
+ function readSiblingBrowserTestSources(): BrowserTestSource[] {
63
+ return readdirSync(e2eDir)
64
+ .filter((fileName) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(fileName))
65
+ .filter((fileName) => fileName !== currentFileName)
66
+ .map((fileName) => ({
67
+ fileName,
68
+ source: stripJsComments(readFileSync(join(e2eDir, fileName), "utf8")),
69
+ }));
70
+ }
71
+
72
+ function findNamedBrowserTestSource(
73
+ sources: readonly BrowserTestSource[],
74
+ testName: string,
75
+ ): string | undefined {
76
+ const testStartPattern = new RegExp(
77
+ `(?:test|it)(?:\\.[\\w]+)?\\(\\s*(["'\`])${escapeRegExp(testName)}\\1`,
78
+ );
79
+ const nextTestPattern = /\n\s*(?:test|it)(?:\.[\w]+)?\(\s*["'`]/;
80
+
81
+ for (const { source } of sources) {
82
+ const match = testStartPattern.exec(source);
83
+ if (!match) {
84
+ continue;
85
+ }
86
+
87
+ const startIndex = match.index;
88
+ const afterStart = source.slice(startIndex + 1);
89
+ const nextMatchIndex = afterStart.search(nextTestPattern);
90
+
91
+ return source.slice(
92
+ startIndex,
93
+ nextMatchIndex === -1 ? undefined : startIndex + 1 + nextMatchIndex,
94
+ );
95
+ }
96
+
97
+ return undefined;
98
+ }
99
+
100
+ function getCanvasHandleEntries() {
101
+ return starterAcceptance.filter((entry) => entry.kind === "canvas-handle");
102
+ }
103
+
104
+ function getDiscreteSliderControls() {
105
+ return (starterSchema.panels.controls?.sections ?? []).flatMap((section) =>
106
+ Object.entries(section.controls)
107
+ .filter(
108
+ ([, control]) =>
109
+ (control.type === "slider" || control.type === "rangeSlider") &&
110
+ control.variant === "discrete",
111
+ )
112
+ .map(([id, control]) => ({
113
+ control,
114
+ shouldRenderMarkers: shouldInlineDiscreteSliderRenderMarkers(section, id, control),
115
+ })),
116
+ );
117
+ }
118
+
119
+ function getStepPositionCount(control: {
120
+ max?: number;
121
+ min?: number;
122
+ step?: number;
123
+ }): number | undefined {
124
+ if (
125
+ typeof control.step !== "number" ||
126
+ typeof control.min !== "number" ||
127
+ typeof control.max !== "number" ||
128
+ !Number.isFinite(control.step) ||
129
+ !Number.isFinite(control.min) ||
130
+ !Number.isFinite(control.max) ||
131
+ control.step <= 0 ||
132
+ control.max <= control.min
133
+ ) {
134
+ return undefined;
135
+ }
136
+
137
+ const rawStepCount = (control.max - control.min) / control.step;
138
+ const roundedStepCount = Math.round(rawStepCount);
139
+ const intervalCount =
140
+ Math.abs(rawStepCount - roundedStepCount) < Number.EPSILON * 100
141
+ ? roundedStepCount
142
+ : Math.floor(rawStepCount) + 1;
143
+
144
+ return Math.max(2, intervalCount + 1);
145
+ }
146
+
147
+ function isSliderLikeControl(control: { type?: string } | undefined): boolean {
148
+ return control?.type === "slider" || control?.type === "rangeSlider";
149
+ }
150
+
151
+ function shouldInlineDiscreteSliderRenderMarkers(
152
+ section: NonNullable<typeof starterSchema.panels.controls>["sections"][number],
153
+ id: string,
154
+ control: { max?: number; min?: number; step?: number },
155
+ ): boolean {
156
+ const positionCount = getStepPositionCount(control);
157
+
158
+ if (!positionCount || positionCount <= 20) {
159
+ return true;
160
+ }
161
+
162
+ const inlineSliderGroup = section.layoutGroups?.find(
163
+ (layoutGroup) =>
164
+ layoutGroup.layout === "inline" &&
165
+ layoutGroup.columns === 2 &&
166
+ layoutGroup.controls.length === 2 &&
167
+ layoutGroup.controls.includes(id) &&
168
+ layoutGroup.controls.every((controlId) =>
169
+ isSliderLikeControl(section.controls[controlId]),
170
+ ),
171
+ );
172
+
173
+ return !inlineSliderGroup;
174
+ }
175
+
176
+ function getSegmentedControls() {
177
+ return (starterSchema.panels.controls?.sections ?? []).flatMap((section) =>
178
+ Object.values(section.controls).filter((control) => control.type === "segmented"),
179
+ );
180
+ }
181
+
182
+ function getCompoundPartControls() {
183
+ return (starterSchema.panels.controls?.sections ?? []).flatMap((section) =>
184
+ Object.values(section.controls).filter(
185
+ (control) => getRequiredToolcraftControlPartCoverage(control).length > 0,
186
+ ),
187
+ );
188
+ }
189
+
190
+ function getTimelineCoverageEntries(coverage: "keyframes" | "playback") {
191
+ return starterAcceptance.filter((entry) => entry.timelineCoverage === coverage);
192
+ }
193
+
194
+ function getLayerCoverageEntries() {
195
+ return starterAcceptance.filter((entry) => entry.layerCoverage);
196
+ }
197
+
198
+ function getReferenceCoverageEntry(coverage: string) {
199
+ return starterAcceptance.find((entry) => entry.referenceCoverage === coverage);
200
+ }
201
+
202
+ function requiresProductObservableProof(entry: (typeof starterAcceptance)[number]): boolean {
203
+ return (
204
+ entry.evidence === "product-output" ||
205
+ entry.evidence === "rendered-pixels" ||
206
+ entry.evidence === "timeline-output"
207
+ );
208
+ }
209
+
210
+ function hasProductObservableHelper(source: string): boolean {
211
+ return /expectToolcraftProductObservableToChange\s*\(|getToolcraftProductObservableSnapshot\s*\(/.test(
212
+ source,
213
+ );
214
+ }
215
+
216
+ test("browser acceptance matrix points at real Playwright tests", () => {
217
+ const browserTestSources = readSiblingBrowserTestSources();
218
+
219
+ for (const entry of starterAcceptance) {
220
+ if (!entry.browser) {
221
+ continue;
222
+ }
223
+
224
+ expect(
225
+ Boolean(findNamedBrowserTestSource(browserTestSources, entry.browserTestName)),
226
+ `${entry.id} must be backed by a Playwright test named "${entry.browserTestName}".`,
227
+ ).toBe(true);
228
+ }
229
+ });
230
+
231
+ test("browser product-output rows use the shared product observable helper", () => {
232
+ const browserTestSources = readSiblingBrowserTestSources();
233
+
234
+ for (const entry of starterAcceptance) {
235
+ if (!entry.browser || !requiresProductObservableProof(entry)) {
236
+ continue;
237
+ }
238
+
239
+ const browserTestSource = findNamedBrowserTestSource(
240
+ browserTestSources,
241
+ entry.browserTestName,
242
+ );
243
+
244
+ expect(
245
+ browserTestSource,
246
+ `${entry.id} must be backed by browser test "${entry.browserTestName}".`,
247
+ ).toBeDefined();
248
+
249
+ if (!browserTestSource) {
250
+ continue;
251
+ }
252
+
253
+ expect(
254
+ hasProductObservableHelper(browserTestSource),
255
+ `${entry.id} must use expectToolcraftProductObservableToChange or getToolcraftProductObservableSnapshot so the test proves real product output changed.`,
256
+ ).toBe(true);
257
+ }
258
+ });
259
+
260
+ test("browser renders the Toolcraft template shell instead of a reference iframe shell", async ({
261
+ page,
262
+ }) => {
263
+ await page.goto("/");
264
+
265
+ await expect(page.locator('[data-slot="toolcraft-runtime-app"]')).toBeVisible();
266
+
267
+ if (starterSchema.assembly.surfaces.canvas.enabled) {
268
+ await expect(page.getByRole("application", { name: "Canvas viewport" })).toBeVisible();
269
+ }
270
+
271
+ const nonCanvasIframeCount = await page.evaluate(
272
+ () =>
273
+ Array.from(document.querySelectorAll("iframe")).filter(
274
+ (frame) => !frame.closest("[data-toolcraft-canvas-slot]"),
275
+ ).length,
276
+ );
277
+
278
+ expect(
279
+ nonCanvasIframeCount,
280
+ "Reference iframes may not replace the Toolcraft shell. Preserve reference output inside ToolcraftApp canvasContent.",
281
+ ).toBe(0);
282
+ });
283
+
284
+ test("browser preserves the Toolcraft canvas backing surface", async ({ page }) => {
285
+ if (!starterSchema.assembly.surfaces.canvas.enabled) {
286
+ return;
287
+ }
288
+
289
+ await page.goto("/");
290
+
291
+ const canvasViewport = page.getByRole("application", { name: "Canvas viewport" });
292
+
293
+ await expect(canvasViewport).toBeVisible();
294
+
295
+ const backgroundColor = await canvasViewport.evaluate((element) =>
296
+ window.getComputedStyle(element).backgroundColor,
297
+ );
298
+
299
+ expect(
300
+ backgroundColor,
301
+ "The runtime CanvasShell backing must stay visible. Product renderers may customize their own output background, but they must not hide or make the workspace shell transparent.",
302
+ ).not.toMatch(/^(?:transparent|rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\))$/i);
303
+ });
304
+
305
+ test("browser canvas contains product output without app UI controls or CTA copy", async ({
306
+ page,
307
+ }) => {
308
+ if (!starterSchema.assembly.surfaces.canvas.enabled) {
309
+ return;
310
+ }
311
+
312
+ await page.goto("/");
313
+ await expect(page.getByRole("application", { name: "Canvas viewport" })).toBeVisible();
314
+ await expectNoForbiddenCanvasUi(page);
315
+ });
316
+
317
+ test("product observable helper catches changed and unchanged output", async ({ page }) => {
318
+ await page.setContent(`
319
+ <div data-toolcraft-product-output>Before</div>
320
+ <button type="button" id="change-output">Change output</button>
321
+ `);
322
+
323
+ const snapshot = await getToolcraftProductObservableSnapshot(page);
324
+
325
+ expect(
326
+ snapshot,
327
+ "The product observable helper should read marked product output.",
328
+ ).toContain("Before");
329
+
330
+ await expectToolcraftProductObservableToChange(page, async () => {
331
+ await page.locator("#change-output").evaluate((button) => {
332
+ button.previousElementSibling!.textContent = "After";
333
+ });
334
+ });
335
+
336
+ await expect(
337
+ expectToolcraftProductObservableToChange(page, async () => {}, {
338
+ timeoutMs: 100,
339
+ }),
340
+ ).rejects.toThrow(/Product output should change/);
341
+ });
342
+
343
+ test("canvas no-UI helper rejects unclassified canvas text", async ({ page }) => {
344
+ await page.setContent(`
345
+ <div data-toolcraft-canvas-world>
346
+ <div>Click to upload an image</div>
347
+ </div>
348
+ `);
349
+
350
+ await expect(expectNoForbiddenCanvasUi(page)).rejects.toThrow(
351
+ /Canvas text must be product output/,
352
+ );
353
+
354
+ await page.setContent(`
355
+ <div data-toolcraft-canvas-world>
356
+ <div data-toolcraft-product-output>ASCII output</div>
357
+ </div>
358
+ `);
359
+
360
+ await expectNoForbiddenCanvasUi(page);
361
+ });
362
+
363
+ test("browser canvas handle entries use handle helpers and no forbidden canvas UI check", () => {
364
+ const handleEntries = getCanvasHandleEntries();
365
+ if (handleEntries.length === 0) {
366
+ return;
367
+ }
368
+
369
+ const browserTestSources = readSiblingBrowserTestSources();
370
+
371
+ for (const entry of handleEntries) {
372
+ const browserTestSource = findNamedBrowserTestSource(
373
+ browserTestSources,
374
+ entry.browserTestName,
375
+ );
376
+
377
+ expect(
378
+ browserTestSource,
379
+ `${entry.id} must be backed by a Playwright test named "${entry.browserTestName}".`,
380
+ ).toBeDefined();
381
+
382
+ if (!browserTestSource) {
383
+ continue;
384
+ }
385
+
386
+ expect(
387
+ browserTestSource,
388
+ `${entry.id} must drag its declared canvas handle "${entry.canvasHandle?.testId}".`,
389
+ ).toMatch(
390
+ new RegExp(
391
+ `dragCanvasHandle\\s*\\([\\s\\S]*?(["'\`])${escapeRegExp(
392
+ entry.canvasHandle?.testId ?? "",
393
+ )}\\1`,
394
+ ),
395
+ );
396
+
397
+ expect(
398
+ browserTestSource,
399
+ `${entry.id} must verify canvas contains no forbidden app UI.`,
400
+ ).toMatch(/expectNoForbiddenCanvasUi\s*\(/);
401
+
402
+ expect(
403
+ browserTestSource,
404
+ `${entry.id} must verify canvas handles stay in the Toolcraft visual language.`,
405
+ ).toMatch(/expectCanvasHandlesUseToolcraftVisualLanguage\s*\(/);
406
+
407
+ expect(
408
+ browserTestSource,
409
+ `${entry.id} must verify the handle is excluded from export or copied output.`,
410
+ ).toContain(entry.canvasHandle?.exportCleanTestName ?? "");
411
+
412
+ expect(
413
+ browserTestSource,
414
+ `${entry.id} must use expectExportExcludesCanvasHandles for export-clean coverage.`,
415
+ ).toMatch(/expectExportExcludesCanvasHandles\s*\(/);
416
+ }
417
+ });
418
+
419
+ test("browser discrete slider entries verify Toolcraft variant and markers", () => {
420
+ const discreteControls = getDiscreteSliderControls();
421
+ if (discreteControls.length === 0) {
422
+ return;
423
+ }
424
+
425
+ const browserTestSources = readSiblingBrowserTestSources();
426
+
427
+ for (const { control, shouldRenderMarkers } of discreteControls) {
428
+ const entry = starterAcceptance.find(
429
+ (acceptanceEntry) =>
430
+ acceptanceEntry.kind === "control" && acceptanceEntry.target === control.target,
431
+ );
432
+
433
+ expect(
434
+ entry,
435
+ `${control.target} must have acceptance coverage before its discrete slider browser test can be checked.`,
436
+ ).toBeDefined();
437
+
438
+ if (!entry) {
439
+ continue;
440
+ }
441
+
442
+ const browserTestSource = findNamedBrowserTestSource(
443
+ browserTestSources,
444
+ entry.browserTestName,
445
+ );
446
+
447
+ expect(
448
+ browserTestSource,
449
+ `${control.target} must be backed by browser test "${entry.browserTestName}".`,
450
+ ).toBeDefined();
451
+
452
+ if (!browserTestSource) {
453
+ continue;
454
+ }
455
+
456
+ expect(
457
+ browserTestSource,
458
+ `${control.target} discrete browser test must assert the Toolcraft discrete variant.`,
459
+ ).toMatch(/data-variant/);
460
+
461
+ expect(
462
+ browserTestSource,
463
+ `${control.target} discrete browser test must assert the expected discrete variant value.`,
464
+ ).toMatch(/discrete/);
465
+
466
+ if (shouldRenderMarkers) {
467
+ expect(
468
+ browserTestSource,
469
+ `${control.target} discrete browser test must assert hover markers render.`,
470
+ ).toMatch(/slider-marker/);
471
+ } else {
472
+ expect(
473
+ browserTestSource,
474
+ `${control.target} half-width over-budget discrete browser test must assert markers are intentionally hidden.`,
475
+ ).toMatch(/expectMarkers\s*:\s*false|toHaveCount\s*\(\s*0\s*\)/);
476
+ }
477
+
478
+ expect(
479
+ browserTestSource,
480
+ `${control.target} discrete browser test must verify smooth drag with the Toolcraft helper.`,
481
+ ).toMatch(/expectToolcraftDiscreteSliderDragSmoothness\s*\(/);
482
+ }
483
+ });
484
+
485
+ test("browser segmented entries verify cell padding and no label collisions", () => {
486
+ const segmentedControls = getSegmentedControls();
487
+ if (segmentedControls.length === 0) {
488
+ return;
489
+ }
490
+
491
+ const browserTestSources = readSiblingBrowserTestSources();
492
+
493
+ for (const control of segmentedControls) {
494
+ const entry = starterAcceptance.find(
495
+ (acceptanceEntry) =>
496
+ acceptanceEntry.kind === "control" && acceptanceEntry.target === control.target,
497
+ );
498
+
499
+ expect(
500
+ entry,
501
+ `${control.target} must have acceptance coverage before its segmented browser test can be checked.`,
502
+ ).toBeDefined();
503
+
504
+ if (!entry) {
505
+ continue;
506
+ }
507
+
508
+ const browserTestSource = findNamedBrowserTestSource(
509
+ browserTestSources,
510
+ entry.browserTestName,
511
+ );
512
+
513
+ expect(
514
+ browserTestSource,
515
+ `${control.target} must be backed by browser test "${entry.browserTestName}".`,
516
+ ).toBeDefined();
517
+
518
+ if (!browserTestSource) {
519
+ continue;
520
+ }
521
+
522
+ expect(
523
+ browserTestSource,
524
+ `${control.target} segmented browser test must verify cell padding and label collisions.`,
525
+ ).toMatch(/expectToolcraftSegmentedControlCellsPreservePadding\s*\(/);
526
+ }
527
+ });
528
+
529
+ test("browser compound control entries name every required value part", () => {
530
+ const compoundControls = getCompoundPartControls();
531
+ if (compoundControls.length === 0) {
532
+ return;
533
+ }
534
+
535
+ const browserTestSources = readSiblingBrowserTestSources();
536
+
537
+ for (const control of compoundControls) {
538
+ const requiredParts = getRequiredToolcraftControlPartCoverage(control);
539
+ const entry = starterAcceptance.find(
540
+ (acceptanceEntry) =>
541
+ acceptanceEntry.kind === "control" && acceptanceEntry.target === control.target,
542
+ );
543
+
544
+ expect(
545
+ entry,
546
+ `${control.target} must have acceptance coverage before its compound browser test can be checked.`,
547
+ ).toBeDefined();
548
+
549
+ if (!entry) {
550
+ continue;
551
+ }
552
+
553
+ expect(
554
+ entry.controlPartCoverage === "all-visible-parts" ||
555
+ requiredParts.every((part) => entry.controlPartCoverage?.includes(part)),
556
+ `${control.target} acceptance must declare controlPartCoverage for ${requiredParts.join(", ")}.`,
557
+ ).toBe(true);
558
+
559
+ const browserTestSource = findNamedBrowserTestSource(
560
+ browserTestSources,
561
+ entry.browserTestName,
562
+ );
563
+
564
+ expect(
565
+ browserTestSource,
566
+ `${control.target} must be backed by browser test "${entry.browserTestName}".`,
567
+ ).toBeDefined();
568
+
569
+ if (!browserTestSource) {
570
+ continue;
571
+ }
572
+
573
+ for (const part of requiredParts) {
574
+ expect(
575
+ browserTestSource,
576
+ `${control.target} browser test must explicitly exercise value part "${part}".`,
577
+ ).toContain(part);
578
+ }
579
+ }
580
+ });
581
+
582
+ test("segmented layout helper catches paddingless or colliding cells", async ({ page }) => {
583
+ await page.setContent(`
584
+ <div data-slot="field">FX Preset
585
+ <div data-slot="toggle-group" style="display:flex;width:360px;">
586
+ <button data-slot="toggle-group-item" style="box-sizing:border-box;width:120px;padding:0 12px;">One</button>
587
+ <button data-slot="toggle-group-item" style="box-sizing:border-box;width:120px;padding:0 12px;">Two</button>
588
+ <button data-slot="toggle-group-item" style="box-sizing:border-box;width:120px;padding:0 12px;">Off</button>
589
+ </div>
590
+ </div>
591
+ `);
592
+
593
+ await expectToolcraftSegmentedControlCellsPreservePadding(page, "FX Preset");
594
+
595
+ await page.setContent(`
596
+ <div data-slot="field">FX Preset
597
+ <div data-slot="toggle-group" style="display:flex;width:180px;">
598
+ <button data-slot="toggle-group-item" style="box-sizing:border-box;width:60px;padding:0;">Full Stack</button>
599
+ <button data-slot="toggle-group-item" style="box-sizing:border-box;width:60px;padding:0;">RGB Split</button>
600
+ <button data-slot="toggle-group-item" style="box-sizing:border-box;width:60px;padding:0;">Lines</button>
601
+ </div>
602
+ </div>
603
+ `);
604
+
605
+ await expect(
606
+ expectToolcraftSegmentedControlCellsPreservePadding(page, "FX Preset"),
607
+ ).rejects.toThrow(/must preserve cell padding/);
608
+ });
609
+
610
+ test("browser timeline coverage verifies the concrete timeline mode behavior", () => {
611
+ const browserTestSources = readSiblingBrowserTestSources();
612
+
613
+ for (const entry of getTimelineCoverageEntries("playback")) {
614
+ const browserTestSource = findNamedBrowserTestSource(
615
+ browserTestSources,
616
+ entry.browserTestName,
617
+ );
618
+
619
+ expect(
620
+ browserTestSource,
621
+ `${entry.id} must be backed by browser test "${entry.browserTestName}".`,
622
+ ).toBeDefined();
623
+
624
+ if (!browserTestSource) {
625
+ continue;
626
+ }
627
+
628
+ expect(browserTestSource, `${entry.id} must test pause/play transport.`).toMatch(
629
+ /Pause playback[\s\S]*Play playback|Play playback[\s\S]*Pause playback/,
630
+ );
631
+ expect(browserTestSource, `${entry.id} must test loop transport state.`).toMatch(
632
+ /Disable loop[\s\S]*Enable loop|Enable loop[\s\S]*Disable loop/,
633
+ );
634
+ }
635
+
636
+ for (const entry of getTimelineCoverageEntries("keyframes")) {
637
+ const browserTestSource = findNamedBrowserTestSource(
638
+ browserTestSources,
639
+ entry.browserTestName,
640
+ );
641
+
642
+ expect(
643
+ browserTestSource,
644
+ `${entry.id} must be backed by browser test "${entry.browserTestName}".`,
645
+ ).toBeDefined();
646
+
647
+ if (!browserTestSource) {
648
+ continue;
649
+ }
650
+
651
+ expect(browserTestSource, `${entry.id} must open the expanded keyframe editor.`).toMatch(
652
+ /Expand timeline panel|timeline\.setExpanded|timeline-expanded/,
653
+ );
654
+ expect(browserTestSource, `${entry.id} must create or update keyframe rows.`).toMatch(
655
+ /Add .* keyframe|Disable .* keyframes|timeline-keyframe-row/,
656
+ );
657
+ expect(
658
+ hasProductObservableHelper(browserTestSource),
659
+ `${entry.id} must prove rendered keyframe output through the shared product observable helper.`,
660
+ ).toBe(true);
661
+ }
662
+ });
663
+
664
+ test("browser layer coverage verifies concrete layer behavior", () => {
665
+ const layerEntries = getLayerCoverageEntries();
666
+ if (layerEntries.length === 0) {
667
+ return;
668
+ }
669
+
670
+ const browserTestSources = readSiblingBrowserTestSources();
671
+
672
+ for (const entry of layerEntries) {
673
+ const browserTestSource = findNamedBrowserTestSource(
674
+ browserTestSources,
675
+ entry.browserTestName,
676
+ );
677
+
678
+ expect(
679
+ browserTestSource,
680
+ `${entry.id} must be backed by browser test "${entry.browserTestName}".`,
681
+ ).toBeDefined();
682
+
683
+ if (!browserTestSource) {
684
+ continue;
685
+ }
686
+
687
+ switch (entry.layerCoverage) {
688
+ case "selection":
689
+ expect(
690
+ hasRealLayerRowInteraction(browserTestSource),
691
+ `${entry.id} must select a real LayersPanel row, not dispatch layers.select directly.`,
692
+ ).toBe(true);
693
+ break;
694
+ case "visibility":
695
+ expect(
696
+ hasRealLayerRowInteraction(browserTestSource),
697
+ `${entry.id} must locate the real layer row before toggling visibility.`,
698
+ ).toBe(true);
699
+ expect(
700
+ hasRealLayerVisibilityInteraction(browserTestSource),
701
+ `${entry.id} must toggle a real layer visibility button.`,
702
+ ).toBe(true);
703
+ break;
704
+ case "reorder":
705
+ expect(
706
+ hasRealLayerRowInteraction(browserTestSource),
707
+ `${entry.id} must locate real layer rows before reorder.`,
708
+ ).toBe(true);
709
+ expect(
710
+ hasRealLayerDragInteraction(browserTestSource),
711
+ `${entry.id} must drag real layer rows instead of dispatching layers.reorder.`,
712
+ ).toBe(true);
713
+ break;
714
+ case "grouping":
715
+ expect(
716
+ hasRealLayerRowInteraction(browserTestSource),
717
+ `${entry.id} must locate real layer rows before grouping.`,
718
+ ).toBe(true);
719
+ expect(
720
+ hasRealLayerDragInteraction(browserTestSource),
721
+ `${entry.id} must drag a real layer row into a group.`,
722
+ ).toBe(true);
723
+ expect(
724
+ hasLayerGroupTarget(browserTestSource),
725
+ `${entry.id} must use a real group row as the drop target.`,
726
+ ).toBe(true);
727
+ break;
728
+ case "selected-layer-controls":
729
+ expect(
730
+ hasRealLayerRowInteraction(browserTestSource),
731
+ `${entry.id} must prove controls edit the selected layer output.`,
732
+ ).toBe(true);
733
+ expect(
734
+ hasProductObservableHelper(browserTestSource),
735
+ `${entry.id} must assert a product output or rendered-pixel change.`,
736
+ ).toBe(true);
737
+ break;
738
+ case "media-lifecycle":
739
+ expect(browserTestSource, `${entry.id} must test layer media lifecycle.`).toMatch(
740
+ /media\.import|media\.delete|upload|delete|remove/i,
741
+ );
742
+ break;
743
+ }
744
+ }
745
+ });
746
+
747
+ test("browser reference-runtime-clone coverage proves reference parity behavior", () => {
748
+ if (starterTransferMode.mode !== "reference-runtime-clone") {
749
+ return;
750
+ }
751
+
752
+ const browserTestSources = readSiblingBrowserTestSources();
753
+
754
+ for (const coverage of starterTransferMode.behaviorCoverage) {
755
+ const entry = getReferenceCoverageEntry(coverage);
756
+
757
+ expect(
758
+ entry,
759
+ `reference-runtime-clone behavior "${coverage}" must have an acceptance entry.`,
760
+ ).toBeDefined();
761
+
762
+ if (!entry) {
763
+ continue;
764
+ }
765
+
766
+ const browserTestSource = findNamedBrowserTestSource(
767
+ browserTestSources,
768
+ entry.browserTestName,
769
+ );
770
+
771
+ expect(
772
+ browserTestSource,
773
+ `${entry.id} must be backed by browser test "${entry.browserTestName}".`,
774
+ ).toBeDefined();
775
+
776
+ if (!browserTestSource) {
777
+ continue;
778
+ }
779
+
780
+ expect(
781
+ browserTestSource,
782
+ `${entry.id} must compare against reference runtime behavior, not only assert that Toolcraft state changed.`,
783
+ ).toMatch(/reference|baseline|parity|sourceOfTruth|legacy|cadence|lifetime|spawn/i);
784
+ }
785
+ });