@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,1165 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { cwd } from "node:process";
4
+
5
+ import { describe, expect, expectTypeOf, it } from "vitest";
6
+
7
+ import {
8
+ TOOLCRAFT_COMPONENT_CONTRACTS,
9
+ getToolcraftComponentContract,
10
+ } from "./component-contracts";
11
+
12
+ describe("Toolcraft template component contracts", () => {
13
+ it("exposes decision catalog metadata for controls that agents choose between", () => {
14
+ for (const id of [
15
+ "slider",
16
+ "rangeSlider",
17
+ "select",
18
+ "segmented",
19
+ "switch",
20
+ "checkbox",
21
+ "text",
22
+ "code",
23
+ "color",
24
+ "colorOpacity",
25
+ "gradient",
26
+ "fontPicker",
27
+ "curves",
28
+ "vector",
29
+ "fileDrop",
30
+ "imagePicker",
31
+ "palette",
32
+ "actions",
33
+ "panelActions",
34
+ "customControl",
35
+ ] as const) {
36
+ const catalog = getToolcraftComponentContract(id).decisionCatalog;
37
+
38
+ expect(catalog?.strictness).toMatch(
39
+ /^(exact-owner|best-fit|custom-escape-hatch)$/,
40
+ );
41
+ expect(catalog?.ownsValueModel.length).toBeGreaterThan(0);
42
+ expect(catalog?.useWhen.length).toBeGreaterThan(0);
43
+ expect(catalog?.requiredAcceptance.length).toBeGreaterThan(0);
44
+ }
45
+ });
46
+
47
+ it("documents high-confidence owner controls and their bad substitutes", () => {
48
+ const gradient = getToolcraftComponentContract("gradient").decisionCatalog;
49
+ const fontPicker = getToolcraftComponentContract("fontPicker").decisionCatalog;
50
+ const colorOpacity =
51
+ getToolcraftComponentContract("colorOpacity").decisionCatalog;
52
+ const customControl =
53
+ getToolcraftComponentContract("customControl").decisionCatalog;
54
+
55
+ expect(gradient?.strictness).toBe("exact-owner");
56
+ expect(gradient?.useWhen.join(" ")).toMatch(
57
+ /adjustable gradient|gradient stops/i,
58
+ );
59
+ expect(gradient?.doNotReplaceWith?.join(" ")).toMatch(
60
+ /two Color controls/i,
61
+ );
62
+ expect(gradient?.requiredAcceptance.join(" ")).toMatch(
63
+ /gradientType.*angle.*stop position.*stop color.*stop opacity/i,
64
+ );
65
+
66
+ expect(fontPicker?.doNotReplaceWith?.join(" ")).toMatch(
67
+ /plain Select.*separate typography controls/i,
68
+ );
69
+ expect(colorOpacity?.doNotReplaceWith?.join(" ")).toMatch(
70
+ /separate Color.*opacity/i,
71
+ );
72
+ expect(customControl?.strictness).toBe("custom-escape-hatch");
73
+ expect(customControl?.requiredAcceptance.join(" ")).toMatch(
74
+ /rejected built-ins/i,
75
+ );
76
+ });
77
+
78
+ it("documents select as the fallback for segmented controls that exceed compact limits", () => {
79
+ const segmented = getToolcraftComponentContract("segmented").decisionCatalog;
80
+ const select = getToolcraftComponentContract("select").decisionCatalog;
81
+
82
+ expect(segmented?.strictness).toBe("best-fit");
83
+ expect(segmented?.acceptableAlternatives?.join(" ")).toMatch(/Select/i);
84
+ expect(select?.useWhen.join(" ")).toMatch(/long labels|many options/i);
85
+ expect(select?.layoutConstraints).toContain(
86
+ "Prefer compact two-column inline layout for related short Select pairs that tune one workflow or entity.",
87
+ );
88
+ expect(select?.layoutConstraints).toContain(
89
+ "Use vertical one-select-per-row layout only as a fallback when a label, selected value, or option text would clip, truncate, or lose internal padding in the compact row.",
90
+ );
91
+ expect(select?.layoutConstraints).toContain(
92
+ "If a compact Select pair falls back to vertical layout, record the fit reason in the spec or worklog.",
93
+ );
94
+ });
95
+
96
+ it("keeps UI data variants in the exported stylesheet for standalone apps", () => {
97
+ const uiStyles = readFileSync(
98
+ resolve(cwd(), "../ui/src/styles.css"),
99
+ "utf8",
100
+ );
101
+
102
+ for (const [variant, selector] of [
103
+ ["data-horizontal", "[data-orientation='horizontal']"],
104
+ ["data-vertical", "[data-orientation='vertical']"],
105
+ ["data-active", "[data-state='active']"],
106
+ ["data-checked", "[data-state='checked']"],
107
+ ] as const) {
108
+ expect(uiStyles).toContain(`@custom-variant ${variant}`);
109
+ expect(uiStyles).toContain(selector);
110
+ }
111
+ });
112
+
113
+ it("marks standalone controls as component-labeled standalone controls", () => {
114
+ for (const id of [
115
+ "palette",
116
+ "vector",
117
+ "color",
118
+ "colorOpacity",
119
+ "gradient",
120
+ "fontPicker",
121
+ "curves",
122
+ "anchorGrid",
123
+ "channelMixer",
124
+ "imagePicker",
125
+ ] as const) {
126
+ const contract = getToolcraftComponentContract(id);
127
+
128
+ expect(contract.kind).toBe("control");
129
+ expect(contract.defaultSectionLayout).toBe("standalone");
130
+ expect(contract.labelPolicy).toBe("component-owned");
131
+ }
132
+ });
133
+
134
+ it("documents vector density and section grouping rules", () => {
135
+ const contract = getToolcraftComponentContract("vector");
136
+
137
+ expect(contract.aiUsageRules).toContain(
138
+ "If the controls panel contains exactly one vector control, the runtime renders the vector pad as a square.",
139
+ );
140
+ expect(contract.aiUsageRules).toContain(
141
+ "Multiple vector controls should live in separate semantic sections unless they intentionally belong to the same entity with other related controls.",
142
+ );
143
+ expect(contract.aiUsageRules).toContain(
144
+ 'Use variant: "whiteBalance" for temperature/tint pads: X maps cool blue to warm amber, Y maps green to magenta.',
145
+ );
146
+ expect(contract.aiUsageRules).toContain(
147
+ 'Use variant: "colorBalance" for paired color-balance axes such as cyan/red and blue/yellow correction.',
148
+ );
149
+ expect(contract.aiUsageRules).toContain(
150
+ 'Use the default vector variant for spatial values such as position, offset, direction, focus, anchor, and light direction.',
151
+ );
152
+ expect(contract.aiUsageRules).toContain(
153
+ "Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output.",
154
+ );
155
+ });
156
+
157
+ it("marks grouped controls as label-required grouped controls", () => {
158
+ for (const id of [
159
+ "slider",
160
+ "rangeSlider",
161
+ "select",
162
+ "segmented",
163
+ "switch",
164
+ "checkbox",
165
+ "text",
166
+ "rangeInput",
167
+ "actions",
168
+ ] as const) {
169
+ const contract = getToolcraftComponentContract(id);
170
+
171
+ expect(contract.kind).toBe("control");
172
+ expect(contract.defaultSectionLayout).toBe("grouped");
173
+ expect(contract.labelPolicy).toBe("required");
174
+ }
175
+ });
176
+
177
+ it("documents context-only labels for binary controls", () => {
178
+ const switchContract = getToolcraftComponentContract("switch");
179
+ const checkboxContract = getToolcraftComponentContract("checkbox");
180
+
181
+ expect(switchContract.aiUsageRules).toContain(
182
+ 'Switch labels name the setting context only; do not prefix labels with "Enable" or "Disable" because the switch already communicates on/off behavior.',
183
+ );
184
+ expect(switchContract.aiUsageRules).toContain(
185
+ 'Use labels such as "CRT", "Background", "Glow", or "Loop" instead of "Enable CRT" or "Disable background".',
186
+ );
187
+ expect(switchContract.aiUsageRules).toContain(
188
+ "Two adjacent Switch controls for the same product entity must share one inline row when every visible label fits without truncation. Keep paired labels to short one- or two-word names; the runtime auto-pairs safe adjacent switches by target entity, and generated schemas should stack switches only when any label would truncate.",
189
+ );
190
+ expect(switchContract.aiUsageRules).toContain(
191
+ "When the nearest section title already names the switch context, do not duplicate that title as the visible switch label. Use label false for a visual-only toggle and keep the meaning in target/description.",
192
+ );
193
+ expect(switchContract.aiUsageRules).toContain(
194
+ "A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. Hide the switch label when the section title provides the visible context, such as Include background plus Background color inside Background.",
195
+ );
196
+ expect(checkboxContract.aiUsageRules).toContain(
197
+ 'Checkbox labels name the setting context only; do not prefix labels with "Enable" or "Disable" because the checkbox already communicates enabled/selected state.',
198
+ );
199
+ expect(checkboxContract.aiUsageRules).toContain(
200
+ 'Use labels such as "Transparent background", "Guides", or "Loop" instead of "Enable transparent background".',
201
+ );
202
+ expect(checkboxContract.aiUsageRules).toContain(
203
+ "When the nearest section title already names the checkbox context, do not duplicate that title as the visible checkbox label. Use label false for a visual-only checkbox and keep the meaning in target/description.",
204
+ );
205
+ expect(checkboxContract.aiUsageRules).toContain(
206
+ "Two adjacent Checkbox controls for the same product entity must share one inline row when every visible label fits without truncation. Keep paired labels to short one- or two-word names; the runtime auto-pairs safe adjacent checkboxes by target entity, and generated schemas should stack checkboxes only when any label would truncate.",
207
+ );
208
+ expect(checkboxContract.aiUsageRules).toContain(
209
+ "A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. Hide the checkbox label when the section title provides the visible context.",
210
+ );
211
+ });
212
+
213
+ it("documents minimal UI rules for custom controls", () => {
214
+ const contract = getToolcraftComponentContract("customControl");
215
+
216
+ expect(contract.visualComponent).toBe("CustomControlRenderer");
217
+ expect(contract.aiUsageRules).toContain(
218
+ "Custom controls must render the minimum UI needed to understand the value, context, and available actions; avoid decorative metadata and text that repeats what the section, label, or visible item already explains.",
219
+ );
220
+ expect(contract.aiUsageRules).toContain(
221
+ "Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or status that affects the product.",
222
+ );
223
+ expect(contract.aiUsageRules).toContain(
224
+ "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
225
+ );
226
+ expect(contract.aiUsageRules).toContain(
227
+ "Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
228
+ );
229
+ expect(contract.aiUsageRules).toContain(
230
+ "Custom-control action buttons must be sized for the interaction. Do not shrink destructive, reorder, upload, or primary actions below comfortable kit button/icon-button sizes just to fit more text.",
231
+ );
232
+ });
233
+
234
+ it("documents Actions as local section command groups", () => {
235
+ const contract = getToolcraftComponentContract("actions");
236
+
237
+ expect(contract.stateMode).toBe("command-only");
238
+ expect(contract.decisionCatalog?.ownsValueModel).toContain(
239
+ "entity-scoped command group",
240
+ );
241
+ expect(contract.decisionCatalog?.useWhen).toContain(
242
+ "Use Actions for section-scoped commands such as Randomize palette, Normalize weights, Sort glyphs, Clear selection, Duplicate item, or Reset current entity.",
243
+ );
244
+ expect(contract.aiUsageRules).toContain(
245
+ "Use Actions for local commands inside the current section when the command affects only the nearby entity or workflow step.",
246
+ );
247
+ expect(contract.aiUsageRules).toContain(
248
+ "Good Actions examples: Randomize palette, Normalize weights, Sort glyphs, Clear selection, Duplicate item, Reset current layer, Reset current stop, or Shuffle shades.",
249
+ );
250
+ expect(contract.aiUsageRules).toContain(
251
+ "Do not use Actions for final product delivery actions; use sticky panelActions for Export, Copy, Download, Generate, or Apply.",
252
+ );
253
+ expect(contract.aiUsageRules).toContain(
254
+ "Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
255
+ );
256
+ expect(contract.aiUsageRules).toContain(
257
+ 'For local reset-like actions, use product-specific values such as "reset-current-layer" or "reset-palette" and handle them through ToolcraftApp onPanelAction; do not use a bare "reset" value unless the action intentionally runs controls.reset.',
258
+ );
259
+ });
260
+
261
+ it("documents Palette as a constrained design-token color control", () => {
262
+ const contract = getToolcraftComponentContract("palette");
263
+
264
+ expect(contract.decisionCatalog?.ownsValueModel).toContain(
265
+ "style-guide color token",
266
+ );
267
+ expect(contract.decisionCatalog?.useWhen).toContain(
268
+ "Use Palette for token-based color choices such as brand palettes, Tailwind-like shade scales, semantic palette families, or style-guide colors.",
269
+ );
270
+ expect(contract.decisionCatalog?.doNotReplaceWith).toContain(
271
+ "Do not use Palette for gradients, free hex colors, text color inside FontPicker, or a color value that owns opacity.",
272
+ );
273
+ expect(contract.aiUsageRules).toContain(
274
+ "Use Palette only when the product value is a constrained design-token palette choice with both family and shade.",
275
+ );
276
+ expect(contract.aiUsageRules).toContain(
277
+ "Good Palette examples: brand palette family and shade, Tailwind-like token color, style-guide color scale, semantic palette family, or theme accent token.",
278
+ );
279
+ expect(contract.aiUsageRules).toContain(
280
+ "Do not use Palette for arbitrary free color picking; use Color instead.",
281
+ );
282
+ expect(contract.aiUsageRules).toContain(
283
+ "Do not use Palette when opacity belongs to the same color entity; use ColorOpacity instead.",
284
+ );
285
+ expect(contract.aiUsageRules).toContain(
286
+ "Do not use Palette for gradients or color transitions; use Gradient instead.",
287
+ );
288
+ expect(contract.aiUsageRules).toContain(
289
+ "Do not split typography color out to Palette when the text styling belongs to FontPicker.",
290
+ );
291
+ });
292
+
293
+ it("keeps toolbar and controls panels behind PanelHost with their snap defaults", () => {
294
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.requiredWrapper).toBe("PanelHost");
295
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.defaultPlacement).toBe("bottom");
296
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.snapEdges).toEqual(["top", "bottom"]);
297
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.capabilities).toContain(
298
+ "keyboardShortcuts",
299
+ );
300
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.aiUsageRules).toContain(
301
+ "Toolbar history owns Undo and Redo buttons plus runtime keyboard shortcuts.",
302
+ );
303
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.aiUsageRules).toContain(
304
+ "Do not add app-level Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z, or Ctrl+Y listeners; use toolbar history and runtime commands.",
305
+ );
306
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.toolbar.aiUsageRules).toContain(
307
+ "Undo/redo keyboard shortcuts must not fire while the user is typing into inputs, textareas, selects, or contentEditable value labels.",
308
+ );
309
+
310
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.requiredWrapper).toBe("PanelHost");
311
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.defaultPlacement).toBe("right");
312
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel.snapEdges).toEqual(["left", "right"]);
313
+ });
314
+
315
+ it("preserves canvas runtime commands and patch history", () => {
316
+ const contract = getToolcraftComponentContract("canvas");
317
+
318
+ expect(contract.kind).toBe("canvas");
319
+ expect(contract.stateMode).toBe("runtime-owned");
320
+ expect(contract.historyPolicy).toBe("patch");
321
+ expect(contract.visualComponent).toBe("CanvasShell");
322
+ expect(contract.commands).toContain("canvas.setSize");
323
+ expect(contract.commands).toContain("canvas.panBy");
324
+ expect(contract.commands).toContain("canvas.setOffset");
325
+ expect(contract.commands).toContain("media.import");
326
+ expect(contract.capabilities).toContain("editable-size");
327
+ expect(contract.aiUsageRules).toContain(
328
+ "Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
329
+ );
330
+ expect(contract.aiUsageRules).toContain(
331
+ "Use editable-output by default for generated, exportable, shader, poster, badge, wall, banner, thumbnail, and product-output apps where users should see or edit width and height.",
332
+ );
333
+ expect(contract.aiUsageRules).toContain(
334
+ "A user-provided base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output unless the prompt or reference explicitly locks output dimensions.",
335
+ );
336
+ expect(contract.aiUsageRules).toContain(
337
+ "Use fixed-output only when the product output size must not be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
338
+ );
339
+ expect(contract.aiUsageRules).toContain(
340
+ "Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and do not depend on settingsTransfer.",
341
+ );
342
+ expect(contract.aiUsageRules).toContain(
343
+ "If canvas.size is provided without an explicit sizing mode, defineToolcraft treats it as editable-output and adds Canvas width and Canvas height controls.",
344
+ );
345
+ expect(contract.aiUsageRules).toContain(
346
+ "The runtime Canvas width and Canvas height block uses the technical Setup section and renders without a visible section heading; do not add a separate Canvas section label above these fields.",
347
+ );
348
+ });
349
+
350
+ it("documents persistence as a runtime-owned policy instead of ad hoc localStorage", () => {
351
+ const contract = getToolcraftComponentContract("persistence");
352
+
353
+ expect(contract.kind).toBe("persistence");
354
+ expect(contract.stateMode).toBe("runtime-owned");
355
+ expect(contract.historyPolicy).toBe("never");
356
+ expect(contract.aiUsageRules).toContain("Do not write app state to localStorage directly.");
357
+ expect(contract.aiUsageRules).toContain(
358
+ "Use schema persistence policy for app state that should survive reload.",
359
+ );
360
+ expect(contract.aiUsageRules).toContain(
361
+ "Persistence may include values, canvas, panels, timeline, and layers; history and media blobs are not persisted.",
362
+ );
363
+ expect(contract.aiUsageRules).toContain(
364
+ 'Apps with visible runtime panels and localStorage persistence must include "panels" so dragged panel positions survive reload.',
365
+ );
366
+ expect(contract.aiUsageRules).toContain(
367
+ 'Apps with localStorage persistence must include acceptance coverage for changing a user setting, reloading the browser page, and seeing the restored value or product output.',
368
+ );
369
+ expect(contract.aiUsageRules).toContain(
370
+ "Settings import/export is a preset transfer feature for complex apps; it must not be used to hide or replace broken persistence reload behavior.",
371
+ );
372
+ });
373
+
374
+ it("documents settings transfer as a runtime-owned complex-app feature", () => {
375
+ const contract = getToolcraftComponentContract("settingsTransfer");
376
+
377
+ expect(contract.kind).toBe("settings");
378
+ expect(contract.stateMode).toBe("runtime-owned");
379
+ expect(contract.aiUsageRules).toContain(
380
+ 'Use schema settingsTransfer: "auto" for complex apps unless the prompt explicitly disables settings import/export.',
381
+ );
382
+ expect(contract.aiUsageRules).toContain(
383
+ "After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18.",
384
+ );
385
+ expect(contract.aiUsageRules).toContain(
386
+ "Do not hand-roll settings import/export through app routes, hidden file inputs, or panelActions.",
387
+ );
388
+ expect(contract.aiUsageRules).toContain(
389
+ "Settings transfer appears as the first technical Setup controls-panel section when enabled and renders without a visible section heading; it imports and exports control values, canvas size, and timeline state.",
390
+ );
391
+ expect(contract.aiUsageRules).toContain(
392
+ "A settings-transfer section with only Export Settings and Import Settings means canvas sizing is not editable-output or canvas size controls already exist elsewhere.",
393
+ );
394
+ expect(contract.aiUsageRules).toContain(
395
+ "When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Canvas width, and Canvas height in that order and renders without a visible section heading.",
396
+ );
397
+ });
398
+
399
+ it("documents slider visual variants as explicit schema choices", () => {
400
+ const slider = getToolcraftComponentContract("slider");
401
+ const rangeSlider = getToolcraftComponentContract("rangeSlider");
402
+
403
+ expect(slider.aiUsageRules).toContain(
404
+ "Slider step means numeric snapping only; it does not make the slider visually discrete by itself.",
405
+ );
406
+ expect(slider.aiUsageRules).toContain(
407
+ "Classify every stepped slider as stepped continuous or visual discrete before writing the schema.",
408
+ );
409
+ expect(slider.aiUsageRules).toContain(
410
+ 'Small semantic integer domains such as rows, cols, gaps, jitter, counts, levels, bands, passes, points, tiles, and segments must use variant: "discrete".',
411
+ );
412
+ expect(slider.aiUsageRules).toContain(
413
+ 'Finite animation step domains such as flip depth, character count, glyph steps, and frame steps must use variant: "discrete" when the marker count stays within the Toolcraft visual budget.',
414
+ );
415
+ expect(slider.aiUsageRules).toContain(
416
+ "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
417
+ );
418
+ expect(slider.aiUsageRules).toContain(
419
+ "Schema sliders render stacked at full width; do not put sliders in two-column inline layout groups.",
420
+ );
421
+ expect(slider.aiUsageRules).toContain(
422
+ "The fontPicker component is the only built-in exception with two internal footer sliders for letter spacing and line height.",
423
+ );
424
+ expect(slider.aiUsageRules).toContain(
425
+ 'Browser verification can inspect [data-slot="slider"][data-variant="discrete"] plus slider markers to prove the Toolcraft component variant rendered.',
426
+ );
427
+ expect(slider.aiUsageRules).toContain(
428
+ "Visual discrete sliders must still drag smoothly; browser performance tests should use expectToolcraftDiscreteSliderDragSmoothness for real pointer drag.",
429
+ );
430
+ expect(slider.aiUsageRules).toContain(
431
+ "Use schema disabled: true for unavailable sliders; do not recreate a disabled-looking slider with custom markup.",
432
+ );
433
+ expect(slider.aiUsageRules).toContain(
434
+ "Use disabledWhen for sliders that are only meaningful in some mode values, such as Fill level and Islands being disabled when Fill mode is Full.",
435
+ );
436
+ expect(slider.aiUsageRules).toContain(
437
+ "Do not leave a mode-dependent slider active while making the renderer ignore it; the UI must expose the unavailable state.",
438
+ );
439
+ expect(rangeSlider.aiUsageRules).toContain(
440
+ "Range slider step means numeric snapping only; it does not make the range slider visually discrete by itself.",
441
+ );
442
+ expect(rangeSlider.aiUsageRules).toContain(
443
+ "Classify every stepped range slider as stepped continuous or visual discrete before writing the schema.",
444
+ );
445
+ expect(rangeSlider.aiUsageRules).toContain(
446
+ 'Small semantic integer domains such as rows, cols, gaps, jitter, counts, levels, bands, passes, points, tiles, and segments must use variant: "discrete".',
447
+ );
448
+ expect(rangeSlider.aiUsageRules).toContain(
449
+ 'Finite animation step domains such as flip depth, character count, glyph steps, and frame steps must use variant: "discrete" when the marker count stays within the Toolcraft visual budget.',
450
+ );
451
+ expect(rangeSlider.aiUsageRules).toContain(
452
+ "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
453
+ );
454
+ expect(rangeSlider.aiUsageRules).toContain(
455
+ "RangeSlider is always a full-width two-thumb control; never place it in an inline two-column layout group with another slider or range slider.",
456
+ );
457
+ expect(rangeSlider.aiUsageRules).toContain(
458
+ "RangeSlider defaultValue must start with different lower and upper values so the two-thumb control does not collapse into a single-value slider.",
459
+ );
460
+ expect(rangeSlider.aiUsageRules).toContain(
461
+ "Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes; do not create custom parsers for RangeSlider labels.",
462
+ );
463
+ expect(rangeSlider.aiUsageRules).toContain(
464
+ "Visual discrete sliders must still drag smoothly; browser performance tests should use expectToolcraftDiscreteSliderDragSmoothness for real pointer drag.",
465
+ );
466
+ expect(rangeSlider.aiUsageRules).toContain(
467
+ "Use schema disabled: true for unavailable range sliders; do not recreate a disabled-looking range slider with custom markup.",
468
+ );
469
+ expect(rangeSlider.aiUsageRules).toContain(
470
+ "Use disabledWhen for range sliders that are only meaningful in some mode values; keep the value so it returns when the mode becomes relevant again.",
471
+ );
472
+ expect(rangeSlider.aiUsageRules).toContain(
473
+ "Do not leave a mode-dependent range slider active while making the renderer ignore it; the UI must expose the unavailable state.",
474
+ );
475
+ expect(rangeSlider.aiUsageRules).toContain(
476
+ "Acceptance must prove both rangeSlider.lower and rangeSlider.upper change the product output; testing one handle is not enough.",
477
+ );
478
+ });
479
+
480
+ it("documents compound control part coverage requirements", () => {
481
+ const color = getToolcraftComponentContract("color");
482
+ const colorOpacity = getToolcraftComponentContract("colorOpacity");
483
+ const gradient = getToolcraftComponentContract("gradient");
484
+ const fontPicker = getToolcraftComponentContract("fontPicker");
485
+ const rangeInput = getToolcraftComponentContract("rangeInput");
486
+ const palette = getToolcraftComponentContract("palette");
487
+ const curves = getToolcraftComponentContract("curves");
488
+ const anchorGrid = getToolcraftComponentContract("anchorGrid");
489
+ const channelMixer = getToolcraftComponentContract("channelMixer");
490
+ const appEntityAcceptance = getToolcraftComponentContract("appEntityAcceptance");
491
+
492
+ expect(color.aiUsageRules).toContain(
493
+ "Product-output apps always expose renderer-owned output background color as a schema color target such as appearance.background or scene.background.",
494
+ );
495
+ expect(color.aiUsageRules).toContain(
496
+ "Pair renderer-owned output background color with export.includeBackground in one Background section. Prefer an inline hidden-label toggle plus color parameter row when the section title supplies the Background context.",
497
+ );
498
+ expect(color.aiUsageRules).toContain(
499
+ "Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls only PNG alpha; it must not make live preview, workspace canvas backing, or video transparent.",
500
+ );
501
+ expect(color.aiUsageRules).toContain(
502
+ "When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
503
+ );
504
+ expect(color.aiUsageRules).toContain(
505
+ "Mixed inline rows require visible labels on both controls except for section-title-owned hidden-label toggle plus parameter rows. Color fields in other mixed rows must not be unlabeled.",
506
+ );
507
+ expect(color.aiUsageRules).toContain(
508
+ "Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
509
+ );
510
+ expect(color.aiUsageRules).toContain(
511
+ "Show visible labels for Color and ColorOpacity controls inside mixed sections that contain any non-color controls.",
512
+ );
513
+ expect(color.aiUsageRules).toContain(
514
+ "Omit visible color field labels only when the section contains color controls and no other control types.",
515
+ );
516
+ expect(gradient.aiUsageRules).toContain(
517
+ "Gradient is a compound control; acceptance must prove gradient.gradientType, gradient.angle, gradient.stops.position, gradient.stops.color, and gradient.stops.opacity all affect the product output when visible.",
518
+ );
519
+ expect(gradient.aiUsageRules).toContain(
520
+ "Keep Gradient type/angle, draggable stop track, and Stops list inside the built-in Gradient control. The full Gradient control is visually separated with content-width dividers only when it shares a section with sibling controls; do not put dividers only around the Stops list and do not rebuild it as separate schema controls.",
521
+ );
522
+ expect(gradient.aiUsageRules).toContain(
523
+ "If the renderer intentionally supports only a subset of gradient behavior, do not use the full Gradient control; use simpler controls that match the renderer behavior.",
524
+ );
525
+ expect(fontPicker.aiUsageRules).toContain(
526
+ "FontPicker owns the font preview select, virtualized font popup, category filters, search, preview loading, font-weight select, font-size input, text-case select, text color/opacity control, letter-spacing slider, and line-height slider.",
527
+ );
528
+ expect(fontPicker.aiUsageRules).toContain(
529
+ 'Do not recreate FontPicker with a plain Select plus separate sliders; use type: "fontPicker" so the popup mechanics and footer controls stay intact.',
530
+ );
531
+ expect(fontPicker.aiUsageRules).toContain(
532
+ "Any product text controlled by FontPicker must render fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, and opacity in preview and export; do not leave typography values as panel-only runtime state.",
533
+ );
534
+ expect(fontPicker.aiUsageRules).toContain(
535
+ "FontPicker is an atomic compound typography control. Do not split any owned typography part into a neighboring schema control for the same product text entity.",
536
+ );
537
+ expect(fontPicker.aiUsageRules).toContain(
538
+ "FontPicker is a compound control; acceptance must prove fontPicker.fontId, fontPicker.fontWeight, fontPicker.fontSize, fontPicker.letterSpacing, fontPicker.lineHeight, fontPicker.textCase, fontPicker.color, and fontPicker.opacity all affect the product output.",
539
+ );
540
+ expect(fontPicker.aiUsageRules).toContain(
541
+ "FontPicker acceptance must inspect the actual product text output after changing font, weight, size, letter spacing, line height, text case, color, and opacity; runtime state, select labels, or popup preview text alone are not enough.",
542
+ );
543
+ expect(colorOpacity.aiUsageRules).toContain(
544
+ 'Do not split ColorOpacity into a separate Color plus Slider/Input for opacity; use type: "colorOpacity" so the color popover and percent input stay visually connected.',
545
+ );
546
+ expect(colorOpacity.aiUsageRules).toContain(
547
+ "ColorOpacity is the only color control variant that may expose opacity in the color picker popover; plain Color popovers hide opacity controls.",
548
+ );
549
+ expect(colorOpacity.aiUsageRules).toContain(
550
+ "Do not place ColorOpacity in inline two-column layout groups. If either color control has opacity, keep the controls stacked.",
551
+ );
552
+ expect(colorOpacity.aiUsageRules).toContain(
553
+ "Only plain Color controls without opacity may render in two-column color rows.",
554
+ );
555
+ expect(colorOpacity.aiUsageRules).toContain(
556
+ "Acceptance must prove colorOpacity.hex and colorOpacity.opacity both affect the product output; testing only the swatch or only runtime state is not enough.",
557
+ );
558
+ expect(rangeInput.aiUsageRules).toContain(
559
+ "RangeInput is a compound control; acceptance must prove rangeInput.start and rangeInput.end both affect the product output.",
560
+ );
561
+ expect(palette.aiUsageRules).toContain(
562
+ "Palette is a compound control; acceptance must prove palette.family and palette.shade both affect the product output.",
563
+ );
564
+ expect(curves.aiUsageRules).toContain(
565
+ 'Use Curves for editable remapping curves. RGB/R/G/B tabs are only for color-correction or channel-specific curves; use variant: "single" for one standalone curve without channel tabs.',
566
+ );
567
+ expect(curves.aiUsageRules).toContain(
568
+ 'Use variant: "single" for a single acceleration, bend, easing, opacity, response, depth, mask, threshold, tone-response, or mapping curve. Do not create a custom curve UI just to remove RGB tabs.',
569
+ );
570
+ expect(curves.aiUsageRules).toContain(
571
+ "RGB Curves is a color-correction-specific case; do not force RGB/R/G/B tabs onto products that need only one response, bend, depth, or easing curve.",
572
+ );
573
+ expect(curves.aiUsageRules).toContain(
574
+ 'Use interpolation: "smooth" for photo/editor-like visual tone, color, and RGB curves where the curve should feel like a creative editor spline.',
575
+ );
576
+ expect(curves.aiUsageRules).toContain(
577
+ 'Use interpolation: "monotone" for depth, response, mask, opacity, threshold, and data-mapping curves where order must be preserved and overshoot is unsafe. Single curves default to monotone unless smooth is explicitly requested.',
578
+ );
579
+ expect(curves.aiUsageRules).toContain(
580
+ "Single Curves is one labeled control without internal dividers; RGB Curves is the compound variant with channel tabs and section dividers when mixed with sibling controls.",
581
+ );
582
+ expect(curves.aiUsageRules).toContain(
583
+ "RGB curves acceptance must prove curves.activeChannel and curves.points both affect the product output. Single curves acceptance proves curves.points.",
584
+ );
585
+ expect(curves.aiUsageRules).toContain(
586
+ "Curves acceptance should include an off-center control point near an edge so smooth-vs-monotone interpolation mistakes are visible in product output.",
587
+ );
588
+ expect(anchorGrid.aiUsageRules).toContain(
589
+ "AnchorGrid is a position selector; acceptance must prove anchorGrid.position changes product placement, not only selected button state.",
590
+ );
591
+ expect(channelMixer.aiUsageRules).toContain(
592
+ "ChannelMixer is RGB-specific: it renders R/G/B tabs and Red, Green, Blue sliders for an RGB channel matrix.",
593
+ );
594
+ expect(channelMixer.aiUsageRules).toContain(
595
+ "Use ChannelMixer only for RGB channel mixing, channel swapping, or color-correction matrix behavior; do not use it for arbitrary channel lists.",
596
+ );
597
+ expect(channelMixer.decisionCatalog?.useWhen.join(" ")).toMatch(
598
+ /RGB channel mixing/i,
599
+ );
600
+ expect(channelMixer.decisionCatalog?.doNotReplaceWith?.join(" ")).toMatch(
601
+ /arbitrary non-RGB channels/i,
602
+ );
603
+ expect(channelMixer.aiUsageRules).toContain(
604
+ "ChannelMixer is a compound control; acceptance must prove channelMixer.activeChannel and channelMixer.values both affect the product output.",
605
+ );
606
+ expect(appEntityAcceptance.aiUsageRules).toContain(
607
+ "Compound controls must declare controlPartCoverage for every semantic value part required by their control type.",
608
+ );
609
+ });
610
+
611
+ it("documents segmented controls as compact selector-only choices", () => {
612
+ const segmented = getToolcraftComponentContract("segmented");
613
+
614
+ expect(segmented.aiUsageRules).toContain(
615
+ "Use Segmented only for compact mode choices where every cell keeps its internal padding.",
616
+ );
617
+ expect(segmented.aiUsageRules).toContain(
618
+ "If a segmented control is too wide, first shorten option labels; if the compact labels still exceed the width budget, use Select because it has the same selection mechanics without broken cells.",
619
+ );
620
+ });
621
+
622
+ it("documents app entity acceptance as product-output verification", () => {
623
+ const contract = getToolcraftComponentContract("appEntityAcceptance");
624
+
625
+ expect(contract.kind).toBe("composition");
626
+ expect(contract.stateMode).toBe("runtime-owned");
627
+ expect(contract.capabilities).toContain("product-output-verification");
628
+ expect(contract.aiUsageRules).toContain(
629
+ "Every app entity introduced by the AI must have an acceptance test that proves its product responsibility.",
630
+ );
631
+ expect(contract.aiUsageRules).toContain(
632
+ "Compound control browser tests must explicitly exercise each required value part, not only one visible sub-control.",
633
+ );
634
+ expect(contract.aiUsageRules).toContain(
635
+ "Acceptance tests must fail when an entity is disconnected from runtime state, renderer output, export output, or command side effects.",
636
+ );
637
+ expect(contract.aiUsageRules).toContain(
638
+ "Do not accept typecheck, component existence, registered commands, runtime state mutation, renderer input objects, shader uniform presence, or signature strings as final proof.",
639
+ );
640
+ expect(contract.aiUsageRules).toContain(
641
+ "A generic canvas hash difference is not enough for workload or semantic controls; assert the intended direction of the effect.",
642
+ );
643
+ expect(contract.aiUsageRules).toContain(
644
+ "Component variants are accepted entities too; tests should fail if a non-default Toolcraft control variant falls back to the default variant or custom markup.",
645
+ );
646
+ expect(contract.aiUsageRules).toContain(
647
+ "Conditional entities require fixtures that make the condition observable.",
648
+ );
649
+ expect(contract.aiUsageRules).toContain(
650
+ "Use visibleWhen for mode-, type-, variant-, or count-exclusive sections or controls that do not belong to the current selected state.",
651
+ );
652
+ expect(contract.aiUsageRules).toContain(
653
+ "When a count/quantity control determines how many sibling controls are available, hide unavailable siblings with visibleWhen; do not render all possible controls while the renderer reads only the first N.",
654
+ );
655
+ expect(contract.aiUsageRules).toContain(
656
+ "Use disabledWhen for controls that belong to the current entity but are temporarily unavailable for the selected mode; the disabled value must be preserved.",
657
+ );
658
+ expect(contract.aiUsageRules).toContain(
659
+ "Do not leave inactive conditional controls visible and enabled while making the renderer ignore them.",
660
+ );
661
+ });
662
+
663
+ it("documents performance acceptance for custom renderer workload controls", () => {
664
+ const contract = getToolcraftComponentContract("performanceAcceptance");
665
+
666
+ expect(contract.kind).toBe("composition");
667
+ expect(contract.stateMode).toBe("runtime-owned");
668
+ expect(contract.capabilities).toContain("performance-budgets");
669
+ expect(contract.capabilities).toContain("workload-control-tests");
670
+ expect(contract.aiUsageRules).toContain(
671
+ "Custom renderers must define performance budgets for media import, preview updates, control drags, and export/copy before implementation.",
672
+ );
673
+ expect(contract.aiUsageRules).toContain(
674
+ "Controls that change renderer workload, such as Char Size, Grid Density, Matrix Scale, Sample Count, Resolution, Blur Radius, Iterations, Particle Count, or Quality, must be tested at min, default, and max values.",
675
+ );
676
+ expect(contract.aiUsageRules).toContain(
677
+ "Hash differs is not enough for workload controls; tests must assert semantic direction, for example smaller Char Size increases glyph/cell density and larger Char Size decreases it.",
678
+ );
679
+ expect(contract.aiUsageRules).toContain(
680
+ "Performance tests must use representative fixtures and the same renderer/export path as the running app, not only tiny 32px fixtures or isolated helper state.",
681
+ );
682
+ expect(contract.aiUsageRules).toContain(
683
+ "Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
684
+ );
685
+ expect(contract.aiUsageRules).toContain(
686
+ "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
687
+ );
688
+ expect(contract.aiUsageRules).toContain(
689
+ "A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
690
+ );
691
+ expect(contract.aiUsageRules).toContain(
692
+ "Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
693
+ );
694
+ expect(contract.aiUsageRules).toContain(
695
+ "Custom renderer apps must mirror the Renderer Technique Decision Matrix in typed rendererTechnique config so validation can reject contradictory renderer choices.",
696
+ );
697
+ expect(contract.aiUsageRules).toContain(
698
+ "Custom renderer specs must include a Renderer Layer Inventory and mirror it in typed rendererTechnique.layers so dense raster backgrounds cannot silently rasterize semantic foreground output.",
699
+ );
700
+ expect(contract.aiUsageRules).toContain(
701
+ "Semantic foreground output such as product lines, shapes, icons, text, object bounds, and meaningful markers should use DOM or SVG by default; dense raster backgrounds do not justify rasterizing low-count foreground geometry or text.",
702
+ );
703
+ expect(contract.aiUsageRules).toContain(
704
+ "Editing handles must be DOM/SVG overlays, excluded from export, and written through runtime state instead of being drawn into the product raster layer.",
705
+ );
706
+ expect(contract.aiUsageRules).toContain(
707
+ "Product foreground and editing handle renderer layers must declare uiSelector so browser tests can verify the visible layer exists.",
708
+ );
709
+ expect(contract.aiUsageRules).toContain(
710
+ 'productRepresentation "mixed" is valid only when rendererTechnique.layers proves at least two different content families.',
711
+ );
712
+ expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique/);
713
+ expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique\.layers/);
714
+ expect(contract.aiUsageRules.join("\n")).toMatch(/sourceRepresentation/);
715
+ expect(contract.aiUsageRules.join("\n")).toMatch(/productRepresentation/);
716
+ expect(contract.aiUsageRules.join("\n")).toMatch(/previewRenderer/);
717
+ expect(contract.aiUsageRules.join("\n")).toMatch(/exportRenderer/);
718
+ expect(contract.aiUsageRules).toContain(
719
+ "Choose renderer technique from product context, not convenience or novelty. Preserve reference renderer technology in reference-runtime-clone mode unless a concrete blocker and replacement acceptance tests are named.",
720
+ );
721
+ expect(contract.aiUsageRules).toContain(
722
+ "Do not switch renderer technology just because it seems more modern or faster. Preview and export may use different renderers only when the decision matrix explains why and export/copy remains product-quality.",
723
+ );
724
+ expect(contract.aiUsageRules).toContain(
725
+ "Choose renderer workload by product fidelity before choosing rendering technology: ASCII, glyph grids, code art, subtitles, typography, or monospace text products are text-output unless the product intentionally rasterizes them into per-pixel effects.",
726
+ );
727
+ expect(contract.aiUsageRules).toContain(
728
+ "Text-output and vector-output visible previews must preserve native output fidelity. Do not render a low-resolution offscreen canvas or texture and upscale it to the product size.",
729
+ );
730
+ expect(contract.aiUsageRules).toContain(
731
+ "Pixel-output renderers must use WebGL or WebGPU even when the scene is static.",
732
+ );
733
+ expect(contract.aiUsageRules).toContain(
734
+ "Procedural pixel renderers, shader-like effects, animated mesh gradients, and large exportable previews should use WebGL or WebGPU for pixel work instead of main-thread ImageData loops.",
735
+ );
736
+ expect(contract.aiUsageRules).toContain(
737
+ "WebGL and WebGPU renderers must initialize contexts, programs, shaders, pipelines, textures, and large buffers once, then update uniforms or stable buffers when controls change.",
738
+ );
739
+ expect(contract.aiUsageRules).toContain(
740
+ "For keyframe or playback renderers, texture upload and media decode must be keyed to source media/resource changes, not to timeline time or evaluated settings. Timeline-only updates must reuse decoded media and existing GPU resources.",
741
+ );
742
+ expect(contract.aiUsageRules).toContain(
743
+ "Do not create WebGL/WebGPU contexts, shader programs, textures, or requestAnimationFrame loops directly in the React render path.",
744
+ );
745
+ expect(contract.aiUsageRules).toContain(
746
+ "Animation loops must cancel scheduled frames during cleanup.",
747
+ );
748
+ expect(contract.aiUsageRules).toContain(
749
+ "Animated preview renderers must suspend or coalesce non-essential animation work while the user drags, pans, pinches, zooms, or centers the canvas viewport, then resume from the correct timeline or autonomous time without changing the user's play/pause state.",
750
+ );
751
+ expect(contract.aiUsageRules).toContain(
752
+ "If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless the renderer is converted to GPU rendering or the CPU path is removed.",
753
+ );
754
+ expect(contract.aiUsageRules).toContain(
755
+ "Performance matrices must declare rendererStrategy so tests can distinguish none, dom, svg, canvas-2d, webgl, and webgpu renderer paths.",
756
+ );
757
+ });
758
+
759
+ it("documents reference runtime clone mode as a tested composition contract", () => {
760
+ const contract = getToolcraftComponentContract("referenceRuntimeClone");
761
+
762
+ expect(contract.kind).toBe("composition");
763
+ expect(contract.schemaType).toBe("transferMode");
764
+ expect(contract.visualComponent).toBe("canvasContent");
765
+ expect(contract.capabilities).toContain("reference-runtime-clone");
766
+ expect(contract.capabilities).toContain("reference-timeline-inventory");
767
+ expect(contract.aiUsageRules).toContain(
768
+ 'Use transferMode: "reference-runtime-clone" when the user asks to port, clone, copy, or reproduce an existing app exactly.',
769
+ );
770
+ expect(contract.aiUsageRules).toContain(
771
+ "Preserve the reference runtime as the source of truth instead of replacing it with a new renderer or timeline model.",
772
+ );
773
+ expect(contract.aiUsageRules).toContain(
774
+ "Port requestAnimationFrame loops, refs, mutable particle/object state, connection state, spawn/update cadence, lifetime rules, pause/resume, export/copy, canvas sizing, and media lifecycle when the reference depends on them.",
775
+ );
776
+ expect(contract.aiUsageRules).toContain(
777
+ "Reference clone timeline choice is based on timeline behavior, not only on whether the reference draws a timeline-shaped UI.",
778
+ );
779
+ expect(contract.aiUsageRules).toContain(
780
+ "If the reference has Play/Pause, Restart from beginning, current time/progress, duration, loop, scrub, selected range, trim handles, or video export timing, write a Reference Timeline Inventory before choosing a timeline mode.",
781
+ );
782
+ expect(contract.aiUsageRules).toContain(
783
+ 'Use referenceTimeline.mode "toolcraft-playback" for plain transport behavior such as play/pause, restart, duration/progress, loop, scrub, or export at time.',
784
+ );
785
+ expect(contract.aiUsageRules).toContain(
786
+ "Reference clone specs must list every detected transport behavior explicitly, including pause-resume, restart, time-progress, export-at-time, playback, scrub, duration, loop, and keyframes when present.",
787
+ );
788
+ expect(contract.aiUsageRules).toContain(
789
+ 'Do not downgrade custom reference timelines to panels.timeline mode "playback". State buttons, trim handles, selected-range playback, or range export require referenceTimeline.mode "custom-reference-timeline" and dedicated acceptance.',
790
+ );
791
+ expect(contract.aiUsageRules).toContain(
792
+ "Reference clone acceptance must include referenceCoverage rows for canvas sizing, control mapping, renderer state, and any renderer loop, spawn/update cadence, pause/resume, export/copy, or media lifecycle behavior in the reference.",
793
+ );
794
+ });
795
+
796
+ it("documents concise control label rules", () => {
797
+ const contract = getToolcraftComponentContract("controlLabels");
798
+
799
+ expect(contract.kind).toBe("composition");
800
+ expect(contract.visualComponent).toBe("ControlFieldLabel");
801
+ expect(contract.aiUsageRules).toContain(
802
+ "Control labels must be short UI names, usually one to three words.",
803
+ );
804
+ expect(contract.aiUsageRules).toContain(
805
+ "Do not put explanations, formulas, units, parenthetical hints, or usage instructions in control labels.",
806
+ );
807
+ expect(contract.aiUsageRules).toContain(
808
+ "A concise property label such as Speed, Color, Size, or Opacity is allowed when the nearest visible section or group clearly names the affected product entity.",
809
+ );
810
+ expect(contract.aiUsageRules).toContain(
811
+ "When the section is generic, mixed, missing, or otherwise weak context, include the affected entity or role in the label: Pattern color, Background opacity, Wave speed, Stroke width.",
812
+ );
813
+ expect(contract.aiUsageRules).toContain(
814
+ "Acceptance validators suggest semantic replacement labels for weak generic labels; fix the schema label instead of relying on runtime fallback rewriting.",
815
+ );
816
+ expect(contract.aiUsageRules).toContain(
817
+ "Controls-panel sections should stay discrete: two to seven product controls is the normal size, and larger sections must split by product sub-entity or workflow stage.",
818
+ );
819
+ expect(contract.aiUsageRules).toContain(
820
+ "Every app-authored controls-panel body section must have a short meaningful visible title. Runtime-created setup/settings sections use the technical title Setup but render without a visible heading; sticky footer action sections use the technical title Export but render without a visible heading.",
821
+ );
822
+ expect(contract.aiUsageRules).toContain(
823
+ "Every visible controls-panel section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon; generated apps must not hand-build section headers.",
824
+ );
825
+ expect(contract.aiUsageRules).toContain(
826
+ "Controls-panel section expand and collapse uses the standard runtime height/opacity animation; generated apps must not replace it with instant custom section visibility.",
827
+ );
828
+ expect(contract.aiUsageRules).toContain(
829
+ "Ordinary controls-panel section collapsed/expanded state persists as a runtime UI preference per app. It is not undo/redo state, not settings import/export state, and Reset controls must not clear it. Runtime technical Setup/settings sections and sticky footer Export sections are not collapsible.",
830
+ );
831
+ expect(contract.aiUsageRules).toContain(
832
+ "Ordinary controls-panel body sections use 8px top spacing and 24px bottom spacing for their control content. Runtime technical Setup/settings sections use 12px top and bottom spacing to match side padding. Sticky footer action sections keep their dedicated spacing.",
833
+ );
834
+ expect(contract.aiUsageRules).toContain(
835
+ "Broad section titles such as Flow, Icon, Shapes, Scene, Text, Typography, or Motion are only valid for small cohesive groups; use specific titles such as Flow Motion, Flow Geometry, Letter Burst, Shape Colors, Logo Glow, Logo Plate, or Text Block for larger groups.",
836
+ );
837
+ expect(contract.aiUsageRules).toContain(
838
+ "Section titles in one controls panel must be unique.",
839
+ );
840
+ expect(contract.aiUsageRules).toContain(
841
+ "Bad: Grid Density (every Nth). Good: Grid Density, with Every 6th as the select option label.",
842
+ );
843
+ expect(contract.aiUsageRules).toContain(
844
+ "Use control.description for the short help tooltip shown beside visible labels. It must describe the product behavior or output affected by the control, not restate the label.",
845
+ );
846
+ expect(contract.aiUsageRules).toContain(
847
+ "Do not write label-recap descriptions such as Adjusts Opacity, Controls Speed, or Sets Background.",
848
+ );
849
+ expect(contract.aiUsageRules).toContain(
850
+ "If there is no useful product-specific explanation, omit control.description; the runtime should not show a help tooltip for that label.",
851
+ );
852
+ expect(contract.aiUsageRules).toContain(
853
+ "For compound controls such as FontPicker, do not use control.description to enumerate the control's owned fields. FontPicker descriptions must not recap font family, weight, size, case, color, opacity, letter spacing, or line height; use description only for non-obvious product scope or omit it.",
854
+ );
855
+ expect(contract.aiUsageRules).toContain(
856
+ "The runtime renders a filled Phosphor question icon beside each visible ControlFieldLabel; generated apps must not hand-build their own help icon beside built-in labels.",
857
+ );
858
+ expect(contract.capabilities).toContain("control-description-tooltip");
859
+ });
860
+
861
+ it("documents FontPicker help as product-specific scope only", () => {
862
+ const contract = getToolcraftComponentContract("fontPicker");
863
+
864
+ expect(contract.aiUsageRules).toContain(
865
+ "Do not put a help tooltip on FontPicker just to list its owned fields. If the section title and FontPicker labels already make the text target clear, omit description.",
866
+ );
867
+ });
868
+
869
+ it("documents image picker option acceptance as product behavior", () => {
870
+ const contract = getToolcraftComponentContract("imagePicker");
871
+
872
+ expect(contract.kind).toBe("control");
873
+ expect(contract.defaultSectionLayout).toBe("standalone");
874
+ expect(contract.aiUsageRules).toContain(
875
+ "Every visible ImagePicker item must be actionable in the current product context.",
876
+ );
877
+ expect(contract.aiUsageRules).toContain(
878
+ "Do not show selectable image choices that the renderer later sanitizes to a fallback or no-op.",
879
+ );
880
+ expect(contract.aiUsageRules).toContain(
881
+ "Tests must choose each visible ImagePicker item and assert the selected image, texture, gradient, or exported pixels change in the product output.",
882
+ );
883
+ expect(contract.aiUsageRules).toContain(
884
+ "Do not accept renderer data attributes, runtime target changes, or option existence as final proof that an image choice works.",
885
+ );
886
+ });
887
+
888
+ it("preserves runtime panel commands for optional layers and timeline panels", () => {
889
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.commands).toContain("layers.reorder");
890
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.capabilities).toContain("selection");
891
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
892
+ "Do not enable the layers panel for single-layer apps.",
893
+ );
894
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
895
+ "When layers are enabled, layer-specific controls should target selectedLayer.* and apply to the currently selected runtime layer.",
896
+ );
897
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
898
+ "Do not use selectedLayer.* targets when panels.layers is disabled; single-layer apps use app-specific targets.",
899
+ );
900
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
901
+ "Layer-enabled apps need layerCoverage acceptance for selection, visibility, reorder, and grouping.",
902
+ );
903
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
904
+ "Every selectedLayer.* control needs selected-layer-controls acceptance proving it edits the currently selected layer output.",
905
+ );
906
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
907
+ "Layer browser coverage must use real LayersPanel rows and buttons, not direct layers.* command dispatch.",
908
+ );
909
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel.aiUsageRules).toContain(
910
+ "Layer-enabled custom renderers need layers.interactions viewport-stability coverage around real selection, visibility, reorder or grouping, and selected-layer output checks.",
911
+ );
912
+
913
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain(
914
+ "timeline.setCurrentTime",
915
+ );
916
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.commands).toContain(
917
+ "timeline.togglePlayback",
918
+ );
919
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.capabilities).toContain("keyframes");
920
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
921
+ "Do not enable the timeline panel just because a renderer is animated.",
922
+ );
923
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
924
+ "Before choosing no timeline for any animated product, write an Animation Intent Inventory: product transport, editable keyframes, or autonomous decorative output, plus the user-facing time behaviors present or intentionally absent.",
925
+ );
926
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
927
+ 'User-requested product animation defaults to panels.timeline mode "playback" unless the spec explicitly declares autonomous decorative/self-running output with no play, pause, scrub, duration, loop, or export-at-time behavior.',
928
+ );
929
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
930
+ "Playback renderers must consume runtime timeline state; pause freezes output, scrubbing renders a deterministic frame, and the full animation cycle maps to state.timeline.durationSeconds instead of a local fixed duration.",
931
+ );
932
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
933
+ "Playback renderers may compute an initial default duration, but must not watch state.timeline.durationSeconds and dispatch timeline.setDuration back to a computed local duration. User-edited timeline duration is the source of truth after initialization or reset.",
934
+ );
935
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
936
+ "When non-looping playback reaches the end, pressing Play again restarts playback from time 0.",
937
+ );
938
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
939
+ "Intrinsic-media upload timelines must stay paused at time 0 until source media exists; clearing the last media asset must pause and reset playback.",
940
+ );
941
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
942
+ "Do not put Pause or Resume in panelActions; playback belongs to TimelinePanel transport controls.",
943
+ );
944
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
945
+ "In keyframes mode, Toolcraft infers keyframe diamonds from control type; AI must not manually pick a smaller subset of slider/vector/color-style controls.",
946
+ );
947
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
948
+ "Keyframe state stores typed control values; valueLabel is display-only and must never be parsed by renderers or tests as the source of truth.",
949
+ );
950
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
951
+ "Custom renderers with keyframes must consume evaluateToolcraftTimelineValues or useToolcraftEvaluatedValues for keyframed settings instead of raw state.values for those targets.",
952
+ );
953
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
954
+ "Every inferred keyframe-capable control must be evaluated from runtime timeline keyframes and needs acceptance proving diamond creation, row creation, keyframe updates, scrub/playback evaluation, and product output change.",
955
+ );
956
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
957
+ "Keyframe custom renderers must prove zoom, radar, and canvas viewport stability while expanding the timeline, creating keyframes, and scrubbing or playing the timeline.",
958
+ );
959
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
960
+ "Keyframe renderers must not re-decode media or re-upload source textures on timeline ticks, scrubs, playback, or evaluated setting changes.",
961
+ );
962
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
963
+ "Timeline-driven preview renderers must suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions without mutating the user's timeline play/pause state.",
964
+ );
965
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
966
+ "Use keyframeable: false only on controls that are structurally unsupported by the shared keyframe capability helper; capable controls cannot opt out to hide broken animation wiring.",
967
+ );
968
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
969
+ "Right-panel animation controls may tune renderer parameters such as mode, intensity, speed, or stagger only after animation intent is declared; they must not replace top timeline transport.",
970
+ );
971
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
972
+ "Do not replace TimelinePanel with an app-level playback, transport, or timeline panel to avoid runtime performance issues; fix the Toolcraft runtime clock/state path instead.",
973
+ );
974
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
975
+ 'Custom timeline UI is allowed only for explicit referenceTimeline.mode "custom-reference-timeline" transfers with browser-backed referenceTimelineCoverage.',
976
+ );
977
+ expect(TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel.aiUsageRules).toContain(
978
+ "If timeline verification fails, wire the renderer to runtime timeline state or remove panels.timeline.",
979
+ );
980
+ });
981
+
982
+ it("keeps panel actions documented as sticky footer command controls", () => {
983
+ const contract = getToolcraftComponentContract("panelActions");
984
+
985
+ expect(contract.stateMode).toBe("command-only");
986
+ expect(contract.commands).toEqual(["controls.apply"]);
987
+ expect(contract.aiUsageRules).toContain(
988
+ "Do not use panelActions for resetting controls; the controls panel header owns Reset controls.",
989
+ );
990
+ expect(contract.aiUsageRules).toContain(
991
+ "Handle product-specific panelActions through ToolcraftApp onPanelAction.",
992
+ );
993
+ expect(contract.aiUsageRules).toContain(
994
+ "Async product actions such as Export, Download, Copy, Generate, or Apply must return the real Promise from onPanelAction and report progress through the onPanelAction reportProgress callback.",
995
+ );
996
+ expect(contract.aiUsageRules).toContain(
997
+ "The sticky footer top accent indicator is determinate when reportProgress receives 0..1 values and falls back to pending state only when progress is unavailable.",
998
+ );
999
+ expect(contract.aiUsageRules).toContain(
1000
+ "defineToolcraft hoists panelActions into the controls panel sticky footer automatically.",
1001
+ );
1002
+ expect(contract.aiUsageRules).toContain(
1003
+ "Product-output apps must always include export in panelActions.",
1004
+ );
1005
+ expect(contract.aiUsageRules).toContain(
1006
+ "Static or still-output apps include Export PNG as the primary footer action.",
1007
+ );
1008
+ expect(contract.aiUsageRules).toContain(
1009
+ "Animated apps include Export Video as the primary footer action and Export PNG as a secondary footer action.",
1010
+ );
1011
+ expect(contract.aiUsageRules).toContain(
1012
+ 'Animated apps with Export Video must expose a separate "Video Export" controls section.',
1013
+ );
1014
+ expect(contract.aiUsageRules).toContain(
1015
+ 'The Video Export section must include format and resolution controls such as targets "export.video.format" and "export.video.resolution".',
1016
+ );
1017
+ expect(contract.aiUsageRules).toContain(
1018
+ "Use Select controls for Video Export format and resolution; do not use Segmented unless the product has a deliberately tiny fixed output menu and browser tests prove every cell keeps padding.",
1019
+ );
1020
+ expect(contract.aiUsageRules).toContain(
1021
+ "Place the Video Export section as the final controls section directly above sticky footer panelActions.",
1022
+ );
1023
+ expect(contract.aiUsageRules).toContain(
1024
+ 'Video Export format defaults to "mp4"; keep "webm" available as the baseline alternate unless the prompt/reference requires another default.',
1025
+ );
1026
+ expect(contract.aiUsageRules).toContain(
1027
+ 'Video Export resolution defaults to "current"; keep "4k" available as the high-resolution alternate.',
1028
+ );
1029
+ expect(contract.aiUsageRules).toContain(
1030
+ "Video Export format and resolution are a compact semantic pair and should use a two-column inline layout by default; use stacked rows only when labels or selected values do not fit without clipping.",
1031
+ );
1032
+ expect(contract.aiUsageRules).toContain(
1033
+ 'Baseline browser video formats are "mp4" and "webm"; MOV or ProRes require an explicit custom encoder/transcoder and dedicated acceptance plus performance coverage.',
1034
+ );
1035
+ expect(contract.aiUsageRules).toContain(
1036
+ "Video export code must choose the actual MIME/container through MediaRecorder.isTypeSupported or an equivalent encoder capability check, then fall back safely.",
1037
+ );
1038
+ expect(contract.aiUsageRules).toContain(
1039
+ "Offline video export duration must be encoded from runtime timeline timestamps. Do not rely on canvas.captureStream plus MediaRecorder wall-clock recording time as the only duration mechanism for rendered-frame export.",
1040
+ );
1041
+ expect(contract.aiUsageRules).toContain(
1042
+ 'Video resolution must control exported dimensions. Use "current" output size by default; "4K" is an export resolution target, not a hardcoded 3840x2160 canvas lock.',
1043
+ );
1044
+ expect(contract.aiUsageRules).toContain(
1045
+ "Video export browser coverage must load the exported blob metadata and prove video.duration matches the edited runtime timeline duration; blobSize/blobType checks alone are not enough.",
1046
+ );
1047
+ expect(contract.aiUsageRules).toContain(
1048
+ "Video export must report frame-based progress through reportProgress during render/encode steps. PNG export should report phase progress for render, blob, and handoff when those phases are asynchronous.",
1049
+ );
1050
+ expect(contract.aiUsageRules).toContain(
1051
+ "Product-output apps must expose user-facing Background color and Include background controls, then pass the includeBackground runtime value to createToolcraftPngExportCanvas only for PNG alpha.",
1052
+ );
1053
+ expect(contract.aiUsageRules).toContain(
1054
+ "PNG export must use createToolcraftPngExportCanvas so background transparency and retina sizing are applied consistently without making live preview, workspace canvas backing, or video transparent.",
1055
+ );
1056
+ expect(contract.aiUsageRules).toContain(
1057
+ "Video export must keep product background and use getToolcraftRetinaExportSize for retina dimensions.",
1058
+ );
1059
+ expect(contract.aiUsageRules).toContain(
1060
+ "Copy PNG can be a secondary action when clipboard output is useful, but copy does not replace export.",
1061
+ );
1062
+ expect(contract.aiUsageRules).toContain(
1063
+ "Add Copy PNG as a secondary action only when the prompt/reference includes clipboard output or the product clearly benefits from paste/share workflows.",
1064
+ );
1065
+ expect(contract.aiUsageRules).toContain(
1066
+ "Footer actions must be one compact horizontal group; do not split them into stacked full-width sections.",
1067
+ );
1068
+ expect(contract.aiUsageRules).toContain(
1069
+ "If two footer actions are needed, render secondary/outline on the left and primary on the right.",
1070
+ );
1071
+ expect(contract.aiUsageRules).toContain(
1072
+ "When an odd number of footer actions renders in two columns, the final unpaired action spans the full row width.",
1073
+ );
1074
+ });
1075
+
1076
+ it("documents file upload ownership across single-layer and multi-layer apps", () => {
1077
+ const contract = getToolcraftComponentContract("fileDrop");
1078
+
1079
+ expect(contract.commands).toEqual(["media.delete", "media.import"]);
1080
+ expect(contract.aiUsageRules).toContain(
1081
+ "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
1082
+ );
1083
+ expect(contract.aiUsageRules).toContain(
1084
+ "In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
1085
+ );
1086
+ });
1087
+
1088
+ it("documents semantic color section titles", () => {
1089
+ const contract = getToolcraftComponentContract("color");
1090
+
1091
+ expect(contract.aiUsageRules).toContain(
1092
+ "Color controls can be standalone color sections or grouped fields inside a semantic control section.",
1093
+ );
1094
+ expect(contract.aiUsageRules).toContain(
1095
+ "First identify the semantic entity the color belongs to, such as Square 1, Square 2, Background, Object, Connector, Glow, Tone Mapping, Brand, or Export.",
1096
+ );
1097
+ expect(contract.aiUsageRules).toContain(
1098
+ "Keep a color inside a section when it configures the same entity as nearby controls. Example: Square 1 (Right) contains Connections, Hover radius, and Color in one section.",
1099
+ );
1100
+ expect(contract.aiUsageRules).toContain(
1101
+ "When color belongs to the same object or effect as nearby controls, keep it inside that section and use a concise field label that is unambiguous in context, such as Color in a Square section or Symbol color in a mixed Style section.",
1102
+ );
1103
+ expect(contract.aiUsageRules).toContain(
1104
+ "The standalone default applies only to color-only sections; mixed semantic sections keep color grouped with nearby controls.",
1105
+ );
1106
+ expect(contract.aiUsageRules).toContain(
1107
+ "Never use generic Color or Colors as a generated section title. If no meaningful color role exists and the colors are just basic colors, use a neutral section title such as Appearance instead of omitting the title.",
1108
+ );
1109
+ expect(contract.aiUsageRules).toContain(
1110
+ "Do not split a grouped object section into a separate generated Color section; if the color role is unclear, ask the user before implementation.",
1111
+ );
1112
+ expect(contract.aiUsageRules).toContain(
1113
+ "Render multiple related color fields in one section with at most two colors per row.",
1114
+ );
1115
+ });
1116
+
1117
+ it("documents CodeTextarea as generic multiline text input", () => {
1118
+ const contract = getToolcraftComponentContract("code");
1119
+
1120
+ expect(contract.visualComponent).toBe("CodeTextarea");
1121
+ expect(contract.defaultSectionLayout).toBe("standalone");
1122
+ expect(contract.labelPolicy).toBe("required");
1123
+ expect(contract.aiUsageRules).toContain(
1124
+ "CodeTextarea is the multiline text input for any potentially long value, not only source code.",
1125
+ );
1126
+ expect(contract.aiUsageRules).toContain(
1127
+ "Use text for short single-line strings such as names, small numeric values, compact prompts, titles, and short tokens.",
1128
+ );
1129
+ expect(contract.aiUsageRules).toContain(
1130
+ "Use code when the user may enter long prompts, multiline text, JSON, CSS, shader code, scripts, templates, or other long structured data.",
1131
+ );
1132
+ expect(contract.aiUsageRules).toContain(
1133
+ "CodeTextarea is a content editor and applies values while typing; do not wait for blur, Enter, or Cmd/Ctrl+Enter to update runtime state.",
1134
+ );
1135
+ expect(contract.aiUsageRules).toContain(
1136
+ "CodeTextarea height is capped at 12 visible text lines; long content scrolls inside the textarea instead of making the controls panel taller.",
1137
+ );
1138
+ });
1139
+
1140
+ it("documents TextInput content and setting commit modes", () => {
1141
+ const contract = getToolcraftComponentContract("text");
1142
+
1143
+ expect(contract.visualComponent).toBe("TextInput");
1144
+ expect(contract.defaultSectionLayout).toBe("grouped");
1145
+ expect(contract.labelPolicy).toBe("required");
1146
+ expect(contract.aiUsageRules).toContain(
1147
+ 'TextInput commitMode defaults to "content": text content, prompts, names, tokens, titles, and instructions apply while typing.',
1148
+ );
1149
+ expect(contract.aiUsageRules).toContain(
1150
+ 'Use commitMode: "setting" for text inputs that edit settings such as font size, numeric-like style values, dimensions, ids, or configuration fields; setting text commits on blur or Enter.',
1151
+ );
1152
+ expect(contract.aiUsageRules).toContain(
1153
+ "Canvas width and Canvas height are runtime editable-size fields and always commit on blur or Enter like editor size fields.",
1154
+ );
1155
+ });
1156
+
1157
+ it("preserves literal contract types from the getter", () => {
1158
+ const contract = getToolcraftComponentContract("slider");
1159
+
1160
+ expectTypeOf(contract.id).toEqualTypeOf<"slider">();
1161
+ expectTypeOf(contract.kind).toEqualTypeOf<"control">();
1162
+ expectTypeOf(contract.defaultSectionLayout).toEqualTypeOf<"grouped">();
1163
+ expectTypeOf(contract.labelPolicy).toEqualTypeOf<"required">();
1164
+ });
1165
+ });