@pixel-point/toolcraft 0.0.14 → 0.0.15

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 (577) hide show
  1. package/README.md +13 -2
  2. package/package.json +1 -1
  3. package/scripts/prepare-pack.mjs +2 -28
  4. package/src/cli-io.mjs +174 -0
  5. package/src/cli-package-manager-flow.test.mjs +206 -0
  6. package/src/cli.mjs +7 -495
  7. package/src/cli.test.mjs +34 -177
  8. package/src/create-command.mjs +152 -0
  9. package/src/create-options.mjs +194 -0
  10. package/src/generate-test-assertions.mjs +27 -0
  11. package/src/generate-test-doc-assertions.mjs +162 -0
  12. package/src/generate-test-file-assertions.mjs +251 -0
  13. package/src/generate-test-fs-utils.mjs +59 -0
  14. package/src/generate-test-integrity-assertions.mjs +30 -0
  15. package/src/generate-test-integrity-framework-assertions.mjs +222 -0
  16. package/src/generate-test-integrity-manifest-assertions.mjs +150 -0
  17. package/src/generate-test-integrity-ownership-assertions.mjs +127 -0
  18. package/src/generate-test-integrity-test-utils.mjs +102 -0
  19. package/src/generate-test-package-assertions.mjs +75 -0
  20. package/src/generate-test-product-fixture.mjs +84 -0
  21. package/src/generate-test-product-fixture.test.mjs +73 -0
  22. package/src/generate-test-standalone-assertions.mjs +209 -0
  23. package/src/generate-transactional.test.mjs +151 -0
  24. package/src/generate.mjs +182 -102
  25. package/src/generate.test.mjs +242 -338
  26. package/src/generated-integrity-manifest.mjs +127 -0
  27. package/src/generated-integrity-manifest.test.mjs +151 -0
  28. package/src/generated-source-policy.mjs +22 -0
  29. package/src/generated-source-policy.test.mjs +35 -0
  30. package/src/generated-target-policy.mjs +55 -0
  31. package/src/generation-transaction.mjs +135 -0
  32. package/src/generation-transaction.test.mjs +74 -0
  33. package/src/package-json.mjs +18 -1
  34. package/src/package-json.test.mjs +38 -5
  35. package/src/package-manager.mjs +2 -0
  36. package/src/package-manager.test.mjs +5 -1
  37. package/src/packaged-template-fallback.test.mjs +126 -0
  38. package/src/prepare-pack-templates.mjs +34 -0
  39. package/templates/runtime/contracts/component-contract-builders.ts +95 -0
  40. package/templates/runtime/contracts/component-contracts.choices.test.ts +293 -0
  41. package/templates/runtime/contracts/component-contracts.choices.ts +322 -0
  42. package/templates/runtime/contracts/component-contracts.inputs.test.ts +210 -0
  43. package/templates/runtime/contracts/component-contracts.inputs.ts +252 -0
  44. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +116 -0
  45. package/templates/runtime/contracts/component-contracts.media-custom.ts +144 -0
  46. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +229 -0
  47. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +76 -0
  48. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +135 -0
  49. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +148 -0
  50. package/templates/runtime/contracts/component-contracts.runtime.ts +361 -0
  51. package/templates/runtime/contracts/component-contracts.test.ts +39 -1458
  52. package/templates/runtime/contracts/component-contracts.ts +44 -1505
  53. package/templates/runtime/contracts/component-contracts.visual.test.ts +316 -0
  54. package/templates/runtime/contracts/component-contracts.visual.ts +385 -0
  55. package/templates/runtime/contracts/contract-manifest.test.ts +64 -0
  56. package/templates/runtime/contracts/decision-contracts.test.ts +29 -4
  57. package/templates/runtime/contracts/decision-contracts.ts +38 -1
  58. package/templates/runtime/index.ts +1 -0
  59. package/templates/runtime/react/{runtime-public-api.test.tsx → app-shell/runtime-public-api.test.tsx} +29 -7
  60. package/templates/runtime/react/{settings-transfer.test.ts → app-shell/settings-transfer.test.ts} +4 -4
  61. package/templates/runtime/react/{settings-transfer.ts → app-shell/settings-transfer.ts} +3 -3
  62. package/templates/runtime/react/{toolbar-panel.test.tsx → app-shell/toolbar-panel.test.tsx} +2 -2
  63. package/templates/runtime/react/{toolbar-panel.tsx → app-shell/toolbar-panel.tsx} +3 -3
  64. package/templates/runtime/react/{toolcraft-app.integration.test.tsx → app-shell/toolcraft-app.integration.test.tsx} +6 -6
  65. package/templates/runtime/react/{toolcraft-app.test.tsx → app-shell/toolcraft-app.test.tsx} +3 -3
  66. package/templates/runtime/react/{toolcraft-app.tsx → app-shell/toolcraft-app.tsx} +11 -8
  67. package/templates/runtime/react/{toolcraft-root.test.tsx → app-shell/toolcraft-root.test.tsx} +1 -1
  68. package/templates/runtime/react/{toolcraft-root.tsx → app-shell/toolcraft-root.tsx} +5 -5
  69. package/templates/runtime/react/{use-toolcraft.ts → app-shell/use-toolcraft.ts} +1 -1
  70. package/templates/runtime/react/canvas/canvas-default-media-layer.tsx +81 -0
  71. package/templates/runtime/react/canvas/canvas-media-transform.ts +33 -0
  72. package/templates/runtime/react/canvas/canvas-shell-interactions.test.tsx +156 -0
  73. package/templates/runtime/react/canvas/canvas-shell-media-rendering.test.tsx +87 -0
  74. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +102 -0
  75. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +108 -0
  76. package/templates/runtime/react/canvas/canvas-shell-upload.test.tsx +106 -0
  77. package/templates/runtime/react/canvas/canvas-shell.tsx +146 -0
  78. package/templates/runtime/react/canvas/canvas-upload-routing.ts +84 -0
  79. package/templates/runtime/react/{media-file.ts → canvas/media-file.ts} +1 -1
  80. package/templates/runtime/react/canvas/use-canvas-drop-import.ts +145 -0
  81. package/templates/runtime/react/canvas/use-canvas-viewport-interactions.ts +227 -0
  82. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +89 -0
  83. package/templates/runtime/react/{controls-panel-filedrop-reorder.test.tsx → controls-panel/__tests__/controls-panel-filedrop-reorder.test.tsx} +4 -4
  84. package/templates/runtime/react/controls-panel/__tests__/controls-panel.actions-layout.test.tsx +91 -0
  85. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +213 -0
  86. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-layout.test.tsx +366 -0
  87. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-picker-models.test.tsx +169 -0
  88. package/templates/runtime/react/controls-panel/__tests__/controls-panel.compound-layout.test.tsx +160 -0
  89. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +352 -0
  90. package/templates/runtime/react/controls-panel/__tests__/controls-panel.curves.test.tsx +226 -0
  91. package/templates/runtime/react/controls-panel/__tests__/controls-panel.floating-shell.test.tsx +13 -0
  92. package/templates/runtime/react/controls-panel/__tests__/controls-panel.font-picker.test.tsx +302 -0
  93. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +401 -0
  94. package/templates/runtime/react/controls-panel/__tests__/controls-panel.gradient-control.test.tsx +139 -0
  95. package/templates/runtime/react/controls-panel/__tests__/controls-panel.help-policy.test.tsx +103 -0
  96. package/templates/runtime/react/controls-panel/__tests__/controls-panel.image-picker.test.tsx +16 -0
  97. package/templates/runtime/react/controls-panel/__tests__/controls-panel.inline-layout.test.tsx +290 -0
  98. package/templates/runtime/react/controls-panel/__tests__/controls-panel.keyframes.test.tsx +175 -0
  99. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-files.test.tsx +121 -0
  100. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-image.test.tsx +363 -0
  101. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-multi-image.test.tsx +105 -0
  102. package/templates/runtime/react/controls-panel/__tests__/controls-panel.range-slider-editing.test.tsx +149 -0
  103. package/templates/runtime/react/controls-panel/__tests__/controls-panel.section-shell.test.tsx +138 -0
  104. package/templates/runtime/react/controls-panel/__tests__/controls-panel.select-layout.test.tsx +148 -0
  105. package/templates/runtime/react/controls-panel/__tests__/controls-panel.setup-controls.test.tsx +189 -0
  106. package/templates/runtime/react/controls-panel/__tests__/controls-panel.slider-rendering.test.tsx +317 -0
  107. package/templates/runtime/react/controls-panel/__tests__/controls-panel.state-reset.test.tsx +137 -0
  108. package/templates/runtime/react/controls-panel/__tests__/controls-panel.structure.test.tsx +66 -0
  109. package/templates/runtime/react/controls-panel/__tests__/controls-panel.text-input.test.tsx +205 -0
  110. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-interaction.test.tsx +388 -0
  111. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-layout.test.tsx +155 -0
  112. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +172 -0
  113. package/templates/runtime/react/{control-conditions.ts → controls-panel/conditions/control-conditions.ts} +26 -3
  114. package/templates/runtime/react/{control-renderers.ts → controls-panel/control-renderers.ts} +2 -2
  115. package/templates/runtime/react/controls-panel/controls-panel.tsx +250 -0
  116. package/templates/runtime/react/controls-panel/keyframes/controls-panel-keyframes.tsx +265 -0
  117. package/templates/runtime/react/controls-panel/layout/controls-panel-collapse-storage.ts +131 -0
  118. package/templates/runtime/react/controls-panel/layout/controls-panel-help.tsx +132 -0
  119. package/templates/runtime/react/controls-panel/layout/controls-panel-inline-layout.tsx +311 -0
  120. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +216 -0
  121. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +401 -0
  122. package/templates/runtime/react/controls-panel/renderers/controls-panel-action-renderer.tsx +137 -0
  123. package/templates/runtime/react/controls-panel/renderers/controls-panel-basic-renderers.tsx +421 -0
  124. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +286 -0
  125. package/templates/runtime/react/controls-panel/renderers/controls-panel-compound-renderers.tsx +366 -0
  126. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +173 -0
  127. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +23 -0
  128. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +55 -0
  129. package/templates/runtime/react/controls-panel/renderers/controls-panel-settings-transfer-renderer.tsx +45 -0
  130. package/templates/runtime/react/controls-panel/testing/controls-panel-test-utils.tsx +300 -0
  131. package/templates/runtime/react/controls-panel/values/controls-panel-aspect-ratio-values.ts +94 -0
  132. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +87 -0
  133. package/templates/runtime/react/controls-panel/values/controls-panel-color-values.ts +82 -0
  134. package/templates/runtime/react/controls-panel/values/controls-panel-font-values.ts +69 -0
  135. package/templates/runtime/react/controls-panel/values/controls-panel-spatial-values.ts +64 -0
  136. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +93 -0
  137. package/templates/runtime/react/controls-panel/values/controls-panel-value-primitives.ts +30 -0
  138. package/templates/runtime/react/controls-panel/values/controls-panel-values.ts +7 -0
  139. package/templates/runtime/react/index.ts +13 -13
  140. package/templates/runtime/react/{layer-tree.ts → layers/layer-tree.ts} +1 -1
  141. package/templates/runtime/react/layers/layers-panel-drag-drop.test.tsx +190 -0
  142. package/templates/runtime/react/layers/layers-panel-editing.test.tsx +41 -0
  143. package/templates/runtime/react/layers/layers-panel-insert-targets.test.ts +87 -0
  144. package/templates/runtime/react/layers/layers-panel-insert-targets.ts +254 -0
  145. package/templates/runtime/react/layers/layers-panel-model.ts +15 -0
  146. package/templates/runtime/react/layers/layers-panel-rendering.test.tsx +43 -0
  147. package/templates/runtime/react/layers/layers-panel-reorder.ts +148 -0
  148. package/templates/runtime/react/layers/layers-panel-row.tsx +475 -0
  149. package/templates/runtime/react/layers/layers-panel-test-utils.tsx +135 -0
  150. package/templates/runtime/react/layers/layers-panel-visibility.test.tsx +126 -0
  151. package/templates/runtime/react/layers/layers-panel.tsx +349 -0
  152. package/templates/runtime/react/layers/use-layers-panel-drag-controller.ts +327 -0
  153. package/templates/runtime/react/{panel-host-types.ts → panel-host/panel-host-types.ts} +2 -2
  154. package/templates/runtime/react/{panel-host.test.tsx → panel-host/panel-host.test.tsx} +3 -3
  155. package/templates/runtime/react/{panel-host.tsx → panel-host/panel-host.tsx} +1 -1
  156. package/templates/runtime/react/timeline/timeline-easing-editor.tsx +409 -0
  157. package/templates/runtime/react/timeline/timeline-easing-geometry.ts +42 -0
  158. package/templates/runtime/react/timeline/timeline-easing-icons.tsx +135 -0
  159. package/templates/runtime/react/timeline/timeline-easing-model.test.ts +87 -0
  160. package/templates/runtime/react/timeline/timeline-easing-model.ts +271 -0
  161. package/templates/runtime/react/timeline/timeline-easing-popover-content.tsx +227 -0
  162. package/templates/runtime/react/timeline/timeline-easing-popover-layout.ts +69 -0
  163. package/templates/runtime/react/timeline/timeline-easing-popover.tsx +60 -0
  164. package/templates/runtime/react/timeline/timeline-event-targets.ts +19 -0
  165. package/templates/runtime/react/timeline/timeline-expanded-content.tsx +299 -0
  166. package/templates/runtime/react/timeline/timeline-icon-button.tsx +51 -0
  167. package/templates/runtime/react/timeline/timeline-keyframe-row.tsx +360 -0
  168. package/templates/runtime/react/timeline/timeline-keyframes.ts +23 -0
  169. package/templates/runtime/react/timeline/timeline-panel-floating-layout.test.tsx +328 -0
  170. package/templates/runtime/react/timeline/timeline-panel-header.tsx +404 -0
  171. package/templates/runtime/react/timeline/timeline-panel-keyframes.test.tsx +90 -0
  172. package/templates/runtime/react/timeline/timeline-panel-layout.ts +30 -0
  173. package/templates/runtime/react/timeline/timeline-panel-responsive-layout.ts +182 -0
  174. package/templates/runtime/react/timeline/timeline-panel-test-utils.tsx +88 -0
  175. package/templates/runtime/react/timeline/timeline-panel-transport.test.tsx +202 -0
  176. package/templates/runtime/react/timeline/timeline-panel-upload.test.tsx +53 -0
  177. package/templates/runtime/react/timeline/timeline-panel.tsx +449 -0
  178. package/templates/runtime/react/timeline/timeline-playback-hooks.ts +254 -0
  179. package/templates/runtime/schema/assembly-contract.ts +271 -0
  180. package/templates/runtime/schema/control-schema-normalization.ts +55 -0
  181. package/templates/runtime/schema/controls-panel-actions.ts +141 -0
  182. package/templates/runtime/schema/controls-panel-layout-groups.ts +144 -0
  183. package/templates/runtime/schema/controls-panel-normalization.ts +19 -0
  184. package/templates/runtime/schema/controls-panel-section-layout.ts +140 -0
  185. package/templates/runtime/schema/controls-panel-section-titles.ts +161 -0
  186. package/templates/runtime/schema/define-toolcraft-test-utils.ts +18 -0
  187. package/templates/runtime/schema/define-toolcraft.actions.test.ts +182 -0
  188. package/templates/runtime/schema/define-toolcraft.assembly.test.ts +246 -0
  189. package/templates/runtime/schema/define-toolcraft.canvas.test.ts +150 -0
  190. package/templates/runtime/schema/define-toolcraft.control-layout.test.ts +345 -0
  191. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +304 -0
  192. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +69 -0
  193. package/templates/runtime/schema/define-toolcraft.setup-persistence.test.ts +66 -0
  194. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +116 -0
  195. package/templates/runtime/schema/define-toolcraft.setup-settings-transfer.test.ts +291 -0
  196. package/templates/runtime/schema/define-toolcraft.ts +24 -1449
  197. package/templates/runtime/schema/panels-schema-normalization.ts +49 -0
  198. package/templates/runtime/schema/runtime-section-titles.ts +1 -0
  199. package/templates/runtime/schema/runtime-setup-section.ts +282 -0
  200. package/templates/runtime/schema/schema-resolvers.ts +168 -0
  201. package/templates/runtime/schema/types.ts +17 -0
  202. package/templates/runtime/state/canvas-reducer.ts +106 -0
  203. package/templates/runtime/state/canvas-state.ts +209 -0
  204. package/templates/runtime/state/controls-reducer.ts +277 -0
  205. package/templates/runtime/state/history-patches.ts +174 -0
  206. package/templates/runtime/state/layer-state.ts +12 -0
  207. package/templates/runtime/state/layers-reducer.ts +318 -0
  208. package/templates/runtime/state/media-reducer.ts +283 -0
  209. package/templates/runtime/state/media-state.ts +105 -0
  210. package/templates/runtime/state/panels-reducer.ts +58 -0
  211. package/templates/runtime/state/persistence-merge.ts +53 -0
  212. package/templates/runtime/state/persistence-reader-canvas.ts +27 -0
  213. package/templates/runtime/state/persistence-reader-layers.ts +73 -0
  214. package/templates/runtime/state/persistence-reader-media.ts +82 -0
  215. package/templates/runtime/state/persistence-reader-panels.ts +56 -0
  216. package/templates/runtime/state/persistence-reader-primitives.ts +27 -0
  217. package/templates/runtime/state/persistence-reader-timeline.ts +145 -0
  218. package/templates/runtime/state/persistence-reader-values.ts +40 -0
  219. package/templates/runtime/state/persistence-readers.ts +72 -0
  220. package/templates/runtime/state/persistence-shared.ts +23 -0
  221. package/templates/runtime/state/persistence-snapshot.ts +57 -0
  222. package/templates/runtime/state/persistence.ts +16 -581
  223. package/templates/runtime/state/reducer-canvas-panels.test.ts +324 -0
  224. package/templates/runtime/state/reducer-controls.test.ts +158 -0
  225. package/templates/runtime/state/reducer-layers.test.ts +306 -0
  226. package/templates/runtime/state/reducer-media-import-sizing.test.ts +197 -0
  227. package/templates/runtime/state/reducer-media-reorder-history.test.ts +116 -0
  228. package/templates/runtime/state/reducer-media-reset.test.ts +249 -0
  229. package/templates/runtime/state/reducer-media-test-utils.ts +40 -0
  230. package/templates/runtime/state/reducer-media-transform.test.ts +52 -0
  231. package/templates/runtime/state/reducer-test-utils.ts +30 -0
  232. package/templates/runtime/state/reducer-timeline.test.ts +242 -0
  233. package/templates/runtime/state/reducer.ts +57 -1785
  234. package/templates/runtime/state/timeline-reducer.ts +412 -0
  235. package/templates/runtime/state/timeline-values.test.ts +45 -0
  236. package/templates/runtime/state/timeline-values.ts +45 -0
  237. package/templates/runtime/testing/performance-browser-policy.test.ts +81 -0
  238. package/templates/runtime/testing/performance-browser-policy.ts +102 -0
  239. package/templates/runtime/testing/performance-budget-validation.ts +84 -0
  240. package/templates/runtime/testing/performance-control-classification.test.ts +106 -0
  241. package/templates/runtime/testing/performance-control-classification.ts +101 -0
  242. package/templates/runtime/testing/performance-coverage-context.ts +44 -0
  243. package/templates/runtime/testing/performance-coverage-renderer-policy.ts +221 -0
  244. package/templates/runtime/testing/performance-coverage-scenario-policy.ts +147 -0
  245. package/templates/runtime/testing/performance-coverage-target-policy.ts +121 -0
  246. package/templates/runtime/testing/performance-coverage-validator.ts +34 -0
  247. package/templates/runtime/testing/performance-export-coverage.test.ts +234 -0
  248. package/templates/runtime/testing/performance-fixture-load-profile.test.ts +360 -0
  249. package/templates/runtime/testing/performance-fixture-media.test.ts +80 -0
  250. package/templates/runtime/testing/performance-fixture-stress.test.ts +314 -0
  251. package/templates/runtime/testing/performance-fixture-validation.ts +255 -0
  252. package/templates/runtime/testing/performance-fixture-values.ts +275 -0
  253. package/templates/runtime/testing/performance-fixture-workload.test.ts +140 -0
  254. package/templates/runtime/testing/performance-load-profile-validation.ts +467 -0
  255. package/templates/runtime/testing/performance-renderer-coverage.test.ts +325 -0
  256. package/templates/runtime/testing/performance-renderer-pipeline-coverage.test.ts +115 -0
  257. package/templates/runtime/testing/performance-renderer-pipeline-interactions.test.ts +201 -0
  258. package/templates/runtime/testing/performance-renderer-pipeline-passes.test.ts +247 -0
  259. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +360 -0
  260. package/templates/runtime/testing/performance-renderer-technique-validation.ts +329 -0
  261. package/templates/runtime/testing/performance-renderer-technique.test.ts +275 -0
  262. package/templates/runtime/testing/performance-renderer-validation.ts +2 -0
  263. package/templates/runtime/testing/performance-scenario-coverage.test.ts +303 -0
  264. package/templates/runtime/testing/performance-scenario-validation.ts +49 -0
  265. package/templates/runtime/testing/performance-schema-derived-workloads.test.ts +231 -0
  266. package/templates/runtime/testing/performance-schema-queries.ts +181 -0
  267. package/templates/runtime/testing/performance-test-fixtures/index.ts +4 -0
  268. package/templates/runtime/testing/performance-test-fixtures/renderer-pipelines.ts +322 -0
  269. package/templates/runtime/testing/performance-test-fixtures/scenarios.ts +172 -0
  270. package/templates/runtime/testing/performance-test-fixtures/schemas.ts +151 -0
  271. package/templates/runtime/testing/performance-test-fixtures/stress-fixtures.ts +133 -0
  272. package/templates/runtime/testing/performance-test-fixtures.ts +1 -0
  273. package/templates/runtime/testing/performance-types.ts +338 -0
  274. package/templates/runtime/testing/performance.ts +15 -2485
  275. package/templates/starter/AGENTS.md +41 -26
  276. package/templates/starter/LICENSE.md +18 -95
  277. package/templates/starter/NOTICE.md +3 -3
  278. package/templates/starter/docs/toolcraft/README.md +5 -5
  279. package/templates/starter/docs/toolcraft/acceptance-testing.md +36 -8
  280. package/templates/starter/docs/toolcraft/assembly-workflow.md +12 -15
  281. package/templates/starter/docs/toolcraft/component-rules.md +6 -2
  282. package/templates/starter/docs/toolcraft/core/layout.md +1 -0
  283. package/templates/starter/docs/toolcraft/core/performance.md +7 -4
  284. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +11 -4
  285. package/templates/starter/docs/toolcraft/core/timeline-animation.md +1 -0
  286. package/templates/starter/docs/toolcraft/custom-controls.md +8 -1
  287. package/templates/starter/docs/toolcraft/decision-contract.md +26 -22
  288. package/templates/starter/docs/toolcraft/performance.md +42 -16
  289. package/templates/starter/docs/toolcraft/schema-reference.md +34 -23
  290. package/templates/starter/docs/toolcraft/workflow.md +25 -17
  291. package/templates/starter/e2e/app-browser-acceptance-evidence.spec.ts +111 -0
  292. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +118 -0
  293. package/templates/starter/e2e/app-browser-performance-evidence.spec.ts +217 -0
  294. package/templates/starter/e2e/app-browser-performance-probe.spec.ts +322 -0
  295. package/templates/starter/e2e/app-browser-proof-session.spec.ts +196 -0
  296. package/templates/starter/e2e/app-browser-runtime-evidence.spec.ts +9 -0
  297. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +240 -0
  298. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +180 -0
  299. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +310 -0
  300. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +432 -0
  301. package/templates/starter/e2e/app-browser-shell.spec.ts +171 -0
  302. package/templates/starter/e2e/app-browser-timeline-layers-reference.spec.ts +70 -0
  303. package/templates/starter/e2e/app-performance.spec.ts +0 -491
  304. package/templates/starter/e2e/browser-acceptance-outcome-helpers.ts +97 -0
  305. package/templates/starter/e2e/browser-acceptance-transition-helpers.ts +74 -0
  306. package/templates/starter/e2e/browser-conditional-output-evidence-helpers.ts +230 -0
  307. package/templates/starter/e2e/browser-control-target-helpers.ts +109 -0
  308. package/templates/starter/e2e/browser-layer-evidence-helpers.ts +229 -0
  309. package/templates/starter/e2e/browser-proof-session.ts +392 -0
  310. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +136 -0
  311. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +290 -0
  312. package/templates/starter/e2e/browser-runtime-evidence.ts +27 -0
  313. package/templates/starter/e2e/browser-state-evidence-helpers.ts +205 -0
  314. package/templates/starter/e2e/browser-timeline-evidence-helpers.ts +319 -0
  315. package/templates/starter/e2e/canvas-handle-helpers.ts +86 -4
  316. package/templates/starter/e2e/export-artifact-helpers.ts +132 -0
  317. package/templates/starter/e2e/performance-budget-helpers.ts +159 -0
  318. package/templates/starter/e2e/performance-canvas-helpers.ts +123 -0
  319. package/templates/starter/e2e/performance-control-helpers.ts +3 -0
  320. package/templates/starter/e2e/performance-control-layout-helpers.ts +161 -0
  321. package/templates/starter/e2e/performance-fixture-helpers.ts +217 -0
  322. package/templates/starter/e2e/performance-frame-probe.ts +247 -0
  323. package/templates/starter/e2e/performance-helpers.ts +6 -854
  324. package/templates/starter/e2e/performance-interaction-measurement.ts +251 -0
  325. package/templates/starter/e2e/performance-measurement-evidence.ts +72 -0
  326. package/templates/starter/e2e/performance-output-action-helpers.spec.ts +43 -0
  327. package/templates/starter/e2e/performance-output-action-helpers.ts +109 -0
  328. package/templates/starter/e2e/performance-probe-helpers.ts +12 -0
  329. package/templates/starter/e2e/performance-render-scale-helpers.ts +55 -0
  330. package/templates/starter/e2e/performance-slider-helpers.ts +271 -0
  331. package/templates/starter/e2e/product-observable-helpers.ts +72 -17
  332. package/templates/starter/e2e/stable-outcome-helpers.ts +208 -0
  333. package/templates/starter/package.json +10 -5
  334. package/templates/starter/playwright.config.ts +5 -2
  335. package/templates/starter/scripts/check-ai-skills.mjs +14 -8
  336. package/templates/starter/scripts/check-ai-skills.test.mjs +64 -0
  337. package/templates/starter/scripts/check-toolcraft-code-health.mjs +108 -0
  338. package/templates/starter/scripts/check-toolcraft-code-health.test.mjs +213 -0
  339. package/templates/starter/scripts/check-toolcraft-docs.mjs +47 -34
  340. package/templates/starter/scripts/check-toolcraft-integrity.mjs +141 -144
  341. package/templates/starter/scripts/run-browser-performance.mjs +118 -0
  342. package/templates/starter/scripts/run-browser-performance.test.mjs +53 -0
  343. package/templates/starter/scripts/toolcraft-code-health-core.mjs +297 -0
  344. package/templates/starter/scripts/toolcraft-contract-doc-integrity.mjs +77 -0
  345. package/templates/starter/scripts/toolcraft-contract-doc-integrity.test.mjs +81 -0
  346. package/templates/starter/scripts/toolcraft-contract-manifest.json +113 -0
  347. package/templates/starter/scripts/toolcraft-contract-manifest.mjs +128 -0
  348. package/templates/starter/scripts/toolcraft-contract-manifest.test.mjs +72 -0
  349. package/templates/starter/scripts/toolcraft-integrity-manifest.mjs +172 -0
  350. package/templates/starter/scripts/toolcraft-integrity-policy.mjs +60 -0
  351. package/templates/starter/scripts/toolcraft-product-boundary-ast-utils.mjs +23 -0
  352. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +264 -0
  353. package/templates/starter/scripts/toolcraft-product-boundary-test-fixtures.mjs +16 -0
  354. package/templates/starter/scripts/toolcraft-product-boundary.mjs +243 -0
  355. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +289 -0
  356. package/templates/starter/scripts/toolcraft-product-dependency-graph.mjs +176 -0
  357. package/templates/starter/scripts/toolcraft-product-dependency-graph.test.mjs +196 -0
  358. package/templates/starter/scripts/toolcraft-product-dependency-resolution.mjs +279 -0
  359. package/templates/starter/scripts/toolcraft-product-evidence-boundary-ast.mjs +91 -0
  360. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +138 -0
  361. package/templates/starter/scripts/toolcraft-product-style-boundary.mjs +219 -0
  362. package/templates/starter/scripts/toolcraft-source-inventory.d.mts +46 -0
  363. package/templates/starter/scripts/toolcraft-source-inventory.mjs +187 -0
  364. package/templates/starter/scripts/toolcraft-source-inventory.test.mjs +132 -0
  365. package/templates/starter/scripts/toolcraft-source-ownership.mjs +167 -0
  366. package/templates/starter/scripts/toolcraft-source-ownership.test.mjs +113 -0
  367. package/templates/starter/scripts/toolcraft-static-string.mjs +104 -0
  368. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +123 -0
  369. package/templates/starter/scripts/toolcraft-verification-receipt.d.mts +19 -0
  370. package/templates/starter/scripts/toolcraft-verification-receipt.mjs +284 -0
  371. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +282 -0
  372. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.d.mts +32 -0
  373. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.mjs +88 -0
  374. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.test.mjs +87 -0
  375. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.mjs +109 -0
  376. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.test.mjs +111 -0
  377. package/templates/starter/scripts/toolcraft-workflow-routes.mjs +314 -0
  378. package/templates/starter/scripts/toolcraft-workflow-routes.test.mjs +204 -0
  379. package/templates/starter/src/app/acceptance/actions.ts +30 -0
  380. package/templates/starter/src/app/acceptance/animation-intent.ts +184 -0
  381. package/templates/starter/src/app/acceptance/canvas-handle-acceptance.ts +70 -0
  382. package/templates/starter/src/app/acceptance/color-bank-labels.ts +164 -0
  383. package/templates/starter/src/app/acceptance/conditions.ts +80 -0
  384. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +70 -0
  385. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +271 -0
  386. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +149 -0
  387. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +189 -0
  388. package/templates/starter/src/app/acceptance/control-acceptance.ts +70 -0
  389. package/templates/starter/src/app/acceptance/control-component-rules.ts +277 -0
  390. package/templates/starter/src/app/acceptance/control-labels.ts +223 -0
  391. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +107 -0
  392. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +104 -0
  393. package/templates/starter/src/app/acceptance/control-layout-model.ts +159 -0
  394. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +164 -0
  395. package/templates/starter/src/app/acceptance/control-layout.ts +45 -0
  396. package/templates/starter/src/app/acceptance/control-order.ts +123 -0
  397. package/templates/starter/src/app/acceptance/control-parts.ts +47 -0
  398. package/templates/starter/src/app/acceptance/control-section-inventory.ts +183 -0
  399. package/templates/starter/src/app/acceptance/controls.ts +27 -0
  400. package/templates/starter/src/app/acceptance/coverage.ts +81 -0
  401. package/templates/starter/src/app/acceptance/custom-controls.ts +140 -0
  402. package/templates/starter/src/app/acceptance/inline-layout.ts +192 -0
  403. package/templates/starter/src/app/acceptance/media-upload.ts +55 -0
  404. package/templates/starter/src/app/acceptance/output-background-rules.ts +135 -0
  405. package/templates/starter/src/app/acceptance/output-export-actions.test.ts +76 -0
  406. package/templates/starter/src/app/acceptance/output-export-actions.ts +34 -0
  407. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +46 -0
  408. package/templates/starter/src/app/acceptance/output-export-model.ts +146 -0
  409. package/templates/starter/src/app/acceptance/output-export.ts +45 -0
  410. package/templates/starter/src/app/acceptance/output-image-export-rules.ts +87 -0
  411. package/templates/starter/src/app/acceptance/reference-runtime-coverage.ts +96 -0
  412. package/templates/starter/src/app/acceptance/reference-runtime-feature-inventory.ts +171 -0
  413. package/templates/starter/src/app/acceptance/reference-runtime-study.ts +73 -0
  414. package/templates/starter/src/app/acceptance/reference-runtime-timeline.ts +291 -0
  415. package/templates/starter/src/app/acceptance/reference-runtime.ts +99 -0
  416. package/templates/starter/src/app/acceptance/runtime-coverage.ts +290 -0
  417. package/templates/starter/src/app/acceptance/runtime-setup.ts +117 -0
  418. package/templates/starter/src/app/acceptance/section-title-rules.ts +8 -0
  419. package/templates/starter/src/app/acceptance/sections.ts +6 -0
  420. package/templates/starter/src/app/acceptance/semantic.ts +63 -0
  421. package/templates/starter/src/app/acceptance/timeline-loop.ts +21 -0
  422. package/templates/starter/src/app/acceptance/types.ts +363 -0
  423. package/templates/starter/src/app/acceptance/validate-coverage.ts +217 -0
  424. package/templates/starter/src/app/acceptance/validation-pipeline.test.ts +56 -0
  425. package/templates/starter/src/app/acceptance/validation-pipeline.ts +60 -0
  426. package/templates/starter/src/app/acceptance/video-reference-study.ts +217 -0
  427. package/templates/starter/src/app/starter-acceptance-data.ts +21 -0
  428. package/templates/starter/src/app/starter-acceptance.animation-intent.test.ts +383 -0
  429. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +349 -0
  430. package/templates/starter/src/app/starter-acceptance.base-coverage.test.ts +47 -0
  431. package/templates/starter/src/app/starter-acceptance.canvas-handles.test.ts +71 -0
  432. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +199 -0
  433. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +244 -0
  434. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +63 -0
  435. package/templates/starter/src/app/starter-acceptance.control-label-context.test.ts +172 -0
  436. package/templates/starter/src/app/starter-acceptance.control-labels.test.ts +387 -0
  437. package/templates/starter/src/app/starter-acceptance.control-naming-rules.test.ts +215 -0
  438. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +274 -0
  439. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +275 -0
  440. package/templates/starter/src/app/starter-acceptance.curves-rules.test.ts +184 -0
  441. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +200 -0
  442. package/templates/starter/src/app/starter-acceptance.custom-control-coverage.test.ts +111 -0
  443. package/templates/starter/src/app/starter-acceptance.custom-control-fit-check.test.ts +135 -0
  444. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +74 -0
  445. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +107 -0
  446. package/templates/starter/src/app/starter-acceptance.framework-boundary.test.ts +327 -0
  447. package/templates/starter/src/app/starter-acceptance.image-video-export-settings.test.ts +328 -0
  448. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +242 -0
  449. package/templates/starter/src/app/starter-acceptance.layers.test.ts +203 -0
  450. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +110 -0
  451. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +153 -0
  452. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +97 -0
  453. package/templates/starter/src/app/starter-acceptance.persistence-coverage.test.ts +134 -0
  454. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +202 -0
  455. package/templates/starter/src/app/starter-acceptance.product-readiness-rules.test.ts +14 -0
  456. package/templates/starter/src/app/starter-acceptance.product-readiness-test-utils.ts +25 -0
  457. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +50 -0
  458. package/templates/starter/src/app/starter-acceptance.product-worklog.test.ts +27 -0
  459. package/templates/starter/src/app/starter-acceptance.range-slider-rules.test.ts +147 -0
  460. package/templates/starter/src/app/starter-acceptance.reference-custom-timeline.test.ts +197 -0
  461. package/templates/starter/src/app/starter-acceptance.reference-foundation.test.ts +304 -0
  462. package/templates/starter/src/app/starter-acceptance.reference-test-utils.ts +102 -0
  463. package/templates/starter/src/app/starter-acceptance.reference-timeline.test.ts +267 -0
  464. package/templates/starter/src/app/starter-acceptance.schema-test-utils.ts +116 -0
  465. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +373 -0
  466. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +260 -0
  467. package/templates/starter/src/app/starter-acceptance.section-titles.test.ts +180 -0
  468. package/templates/starter/src/app/starter-acceptance.setup-readiness.test.ts +205 -0
  469. package/templates/starter/src/app/starter-acceptance.slider-rules.test.ts +470 -0
  470. package/templates/starter/src/app/starter-acceptance.slider-unit-rules.test.ts +61 -0
  471. package/templates/starter/src/app/starter-acceptance.source-test-utils.ts +43 -0
  472. package/templates/starter/src/app/starter-acceptance.test-utils.ts +21 -0
  473. package/templates/starter/src/app/starter-acceptance.text-control-kind.test.ts +89 -0
  474. package/templates/starter/src/app/starter-acceptance.timeline-playback.test.ts +264 -0
  475. package/templates/starter/src/app/starter-acceptance.timeline-test-utils.ts +56 -0
  476. package/templates/starter/src/app/starter-acceptance.toggle-pair-layout.test.ts +200 -0
  477. package/templates/starter/src/app/starter-acceptance.toggle-parameter-layout.test.ts +277 -0
  478. package/templates/starter/src/app/starter-acceptance.ts +151 -4576
  479. package/templates/starter/src/app/starter-acceptance.video-export-test-utils.ts +37 -0
  480. package/templates/starter/src/app/starter-acceptance.video-reference-study.test.ts +262 -0
  481. package/templates/starter/src/app/starter-acceptance.worklog-test-utils.ts +428 -0
  482. package/templates/starter/src/app/starter-acceptance.worklog.test.ts +283 -0
  483. package/templates/starter/src/app/starter-automated-runtime-evidence.test.ts +34 -0
  484. package/templates/starter/src/app/starter-composition.tsx +7 -0
  485. package/templates/starter/src/app/starter-performance-test-utils.ts +275 -0
  486. package/templates/starter/src/app/starter-performance.budgets.test.ts +374 -0
  487. package/templates/starter/src/app/starter-performance.gates.test.ts +128 -0
  488. package/templates/starter/src/app/starter-performance.renderer-source.test.ts +136 -0
  489. package/templates/starter/src/app/starter-performance.renderer-technique.test.ts +291 -0
  490. package/templates/starter/src/app/starter-performance.scenarios.test.ts +355 -0
  491. package/templates/starter/src/app/test-evidence/browser-runtime-contract.test.ts +253 -0
  492. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +243 -0
  493. package/templates/starter/src/routes/index.tsx +11 -2
  494. package/templates/starter/vite.config.ts +8 -0
  495. package/templates/ui/components/controls/color/color-control-types.ts +38 -0
  496. package/templates/ui/components/controls/color/color-control.tsx +19 -437
  497. package/templates/ui/components/controls/color/color-footer.tsx +272 -0
  498. package/templates/ui/components/controls/color/color-model-slider.tsx +271 -0
  499. package/templates/ui/components/controls/color/color-opacity-input.tsx +94 -0
  500. package/templates/ui/components/controls/color/color-surface.tsx +194 -0
  501. package/templates/ui/components/controls/color/color-value-control.tsx +159 -0
  502. package/templates/ui/components/controls/color/color-value-utils.ts +170 -0
  503. package/templates/ui/components/controls/color/index.ts +10 -3
  504. package/templates/ui/components/controls/color/palette-control-controller.ts +407 -0
  505. package/templates/ui/components/controls/color/palette-control-layout.ts +14 -0
  506. package/templates/ui/components/controls/color/palette-control-types.ts +50 -0
  507. package/templates/ui/components/controls/color/palette-control-view.tsx +174 -0
  508. package/templates/ui/components/controls/color/palette-control.tsx +6 -544
  509. package/templates/ui/components/controls/color/style-guide-color-picker-color-utils.ts +39 -0
  510. package/templates/ui/components/controls/color/style-guide-color-picker-controller.ts +370 -0
  511. package/templates/ui/components/controls/color/style-guide-color-picker-hue-handlers.ts +66 -0
  512. package/templates/ui/components/controls/color/style-guide-color-picker-interaction-state.ts +49 -0
  513. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +2 -2
  514. package/templates/ui/components/controls/color/style-guide-color-picker-model.ts +125 -0
  515. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +10 -707
  516. package/templates/ui/components/controls/color/style-guide-color-picker-surface-drag.ts +93 -0
  517. package/templates/ui/components/controls/color/style-guide-color-picker-surface-geometry.ts +114 -0
  518. package/templates/ui/components/controls/color/style-guide-color-picker-surface-preview.ts +45 -0
  519. package/templates/ui/components/controls/color/style-guide-color-picker-types.ts +84 -0
  520. package/templates/ui/components/controls/color/style-guide-color-picker-view.tsx +69 -0
  521. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +6 -495
  522. package/templates/ui/components/controls/curves/curves-control.tsx +4 -3
  523. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +39 -515
  524. package/templates/ui/components/controls/file-drop/file-drop-empty-state.tsx +35 -0
  525. package/templates/ui/components/controls/file-drop/file-drop-file-list.tsx +160 -0
  526. package/templates/ui/components/controls/file-drop/file-drop-image-grid.tsx +175 -0
  527. package/templates/ui/components/controls/file-drop/file-drop-model.tsx +170 -0
  528. package/templates/ui/components/controls/file-drop/file-drop-single-preview.tsx +63 -0
  529. package/templates/ui/components/controls/file-drop/file-drop-types.ts +48 -0
  530. package/templates/ui/components/controls/file-drop/index.ts +2 -2
  531. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +133 -906
  532. package/templates/ui/components/controls/font-picker/font-picker-footer.tsx +116 -0
  533. package/templates/ui/components/controls/font-picker/font-picker-list.tsx +194 -0
  534. package/templates/ui/components/controls/font-picker/font-picker-popover-content.tsx +191 -0
  535. package/templates/ui/components/controls/font-picker/font-picker-value.ts +222 -0
  536. package/templates/ui/components/controls/font-picker/index.ts +4 -1
  537. package/templates/ui/components/controls/font-picker/use-font-picker-preview-pipeline.ts +163 -0
  538. package/templates/ui/components/controls/font-picker/use-font-picker-virtual-list.ts +293 -0
  539. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +2 -25
  540. package/templates/ui/components/controls/gradient/gradient-control.tsx +4 -464
  541. package/templates/ui/components/controls/gradient/gradient-stops-controller.ts +332 -0
  542. package/templates/ui/components/controls/gradient/gradient-stops-track.tsx +159 -0
  543. package/templates/ui/components/controls/vector/vector-control-types.ts +26 -0
  544. package/templates/ui/components/controls/vector/vector-control.tsx +10 -486
  545. package/templates/ui/components/controls/vector/vector-pad-field.tsx +211 -0
  546. package/templates/ui/components/controls/vector/vector-pad-geometry.ts +61 -0
  547. package/templates/ui/components/controls/vector/vector-pad-parts.tsx +72 -0
  548. package/templates/ui/components/controls/vector/vector-pad-variants.ts +20 -0
  549. package/templates/ui/components/controls/vector/vector-size-field.tsx +104 -0
  550. package/templates/ui/components/controls/vector/vector-value.ts +46 -0
  551. package/templates/ui/components/primitives/slider/slider-discrete-state.ts +181 -0
  552. package/templates/ui/components/primitives/slider/slider-marker-policy.ts +52 -0
  553. package/templates/ui/components/primitives/slider/slider-pointer-dragging.ts +87 -0
  554. package/templates/ui/components/primitives/slider/slider-runtime.ts +109 -0
  555. package/templates/ui/components/primitives/slider/slider-types.ts +21 -0
  556. package/templates/ui/components/primitives/slider/slider.tsx +2 -403
  557. package/templates/runtime/react/canvas-shell.test.tsx +0 -554
  558. package/templates/runtime/react/canvas-shell.tsx +0 -585
  559. package/templates/runtime/react/controls-panel.test.tsx +0 -5446
  560. package/templates/runtime/react/controls-panel.tsx +0 -3220
  561. package/templates/runtime/react/layers-panel.test.tsx +0 -487
  562. package/templates/runtime/react/layers-panel.tsx +0 -1348
  563. package/templates/runtime/react/timeline-panel.test.tsx +0 -751
  564. package/templates/runtime/react/timeline-panel.tsx +0 -3078
  565. package/templates/runtime/schema/define-toolcraft.test.ts +0 -1742
  566. package/templates/runtime/state/reducer.test.ts +0 -1703
  567. package/templates/runtime/testing/performance.test.ts +0 -3081
  568. package/templates/starter/e2e/app-browser-acceptance.spec.ts +0 -785
  569. package/templates/starter/src/app/starter-acceptance.test.ts +0 -10068
  570. package/templates/starter/src/app/starter-performance.test.ts +0 -1583
  571. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +0 -490
  572. /package/templates/runtime/react/{storage-key-migration.ts → app-shell/storage-key-migration.ts} +0 -0
  573. /package/templates/runtime/react/{theme-runtime.tsx → app-shell/theme-runtime.tsx} +0 -0
  574. /package/templates/runtime/react/{curve-geometry.test.ts → controls-panel/__tests__/curve-geometry.test.ts} +0 -0
  575. /package/templates/runtime/react/{panel-host-config.ts → panel-host/panel-host-config.ts} +0 -0
  576. /package/templates/runtime/react/{panel-host-geometry.test.ts → panel-host/panel-host-geometry.test.ts} +0 -0
  577. /package/templates/runtime/react/{panel-host-geometry.ts → panel-host/panel-host-geometry.ts} +0 -0
@@ -1,378 +1,54 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import {
5
- CheckIcon,
6
- MagnifyingGlassIcon,
7
- } from "@phosphor-icons/react";
8
4
 
9
5
  import { ControlFieldLabel } from "../../control-layout";
10
6
  import {
11
7
  Field,
12
8
  Input,
13
9
  Popover,
14
- PopoverContent,
15
10
  ScrollFade,
16
- Slider,
17
11
  PopoverTrigger,
18
12
  SelectTriggerButton,
19
13
  } from "../../primitives";
20
- import { cn } from "../../../lib/utils";
21
14
  import type { ControlChangeMeta, ControlValueChangeHandler } from "../control-types";
22
15
  import {
23
- filterFontPickerFonts,
24
- FONT_PICKER_FILTER_OPTIONS,
25
16
  getDefaultFontPickerFontId,
26
17
  getFontPickerFontById,
27
- resolveFontPickerFontId,
28
- type FontPickerFontCatalogEntry,
29
- type FontPickerFontFilterValue,
30
18
  } from "./font-catalog";
31
- import {
32
- queueFontPickerPreviewLoad,
33
- queueFontPickerPreviewLoadBatch,
34
- } from "./font-preview-loader";
35
19
  import { ColorOpacityControl } from "../color";
36
20
  import { StaticSelect } from "../select";
37
21
  import { useMeasuredElementWidth } from "../use-measured-element-width";
38
- import { useHoverIntent } from "./use-hover-intent";
39
-
40
- export type FontPickerLetterSpacingPreset =
41
- | "tight"
42
- | "tighter"
43
- | "normal"
44
- | "wide"
45
- | "wider"
46
- | "widest";
47
-
48
- export type FontPickerLineHeightPreset =
49
- | "loose"
50
- | "none"
51
- | "normal"
52
- | "relaxed"
53
- | "snug"
54
- | "tight";
55
-
56
- export type FontPickerTextCasePreset =
57
- | "capitalize"
58
- | "lowercase"
59
- | "original"
60
- | "titleCase"
61
- | "uppercase";
62
-
63
- export type FontPickerValue = {
64
- color: string;
65
- fontId: string;
66
- fontSize: number;
67
- fontWeight: string;
68
- letterSpacing: FontPickerLetterSpacingPreset;
69
- lineHeight: FontPickerLineHeightPreset;
70
- opacity: number;
71
- textCase: FontPickerTextCasePreset;
72
- };
22
+ import {
23
+ getFontFamilyStyle,
24
+ getFontPickerWeightOptions,
25
+ getStepByValue,
26
+ getStepIndexByValue,
27
+ isFontPickerTextCase,
28
+ letterSpacingSteps,
29
+ lineHeightSteps,
30
+ minFontPickerFontSizePx,
31
+ normalizeFontPickerFontSize,
32
+ normalizeFontPickerValue,
33
+ resolveFontPickerFontWeight,
34
+ textCaseOptions,
35
+ type FontPickerInputValue,
36
+ type FontPickerValue,
37
+ } from "./font-picker-value";
38
+ import { FontPickerPopoverContent } from "./font-picker-popover-content";
39
+ import { useFontPickerPreviewPipeline } from "./use-font-picker-preview-pipeline";
40
+ import { useFontPickerVirtualList } from "./use-font-picker-virtual-list";
73
41
 
74
42
  export type FontPickerControlProps = {
75
- defaultValue?: Partial<FontPickerValue> | string;
43
+ defaultValue?: FontPickerInputValue;
76
44
  disabled?: boolean;
77
45
  name: string;
78
46
  onPreviewChange?: (nextFontId: string | null) => void;
79
47
  onValueChange?: ControlValueChangeHandler<FontPickerValue>;
80
48
  searchPlaceholder?: string;
81
- value?: Partial<FontPickerValue> | string;
49
+ value?: FontPickerInputValue;
82
50
  };
83
51
 
84
- const fontItemHeightPx = 36;
85
- const fontItemGapPx = 1;
86
- const fontVirtualItemStepPx = fontItemHeightPx + fontItemGapPx;
87
- const fontListOverscanItems = 6;
88
- const fontListHeightWithFooterPx = 240;
89
- const fontPreloadBufferAheadItems = 60;
90
- const fontPreloadBufferBehindItems = 30;
91
- const defaultFontPickerFontSizePx = 16;
92
- const defaultFontPickerColor = "#FFFFFF";
93
- const defaultFontPickerOpacity = 100;
94
- const minFontPickerFontSizePx = 1;
95
-
96
- const menuItemInteractionClassName =
97
- "hover:bg-[color:color-mix(in_oklab,var(--muted-foreground)_10%,transparent)] hover:text-[color:var(--foreground)] focus:bg-[color:color-mix(in_oklab,var(--muted-foreground)_10%,transparent)] focus:text-[color:var(--foreground)]";
98
-
99
- const letterSpacingSteps: Array<{
100
- label: string;
101
- numericValue: number;
102
- value: FontPickerLetterSpacingPreset;
103
- }> = [
104
- { label: "Tighter", numericValue: -0.05, value: "tighter" },
105
- { label: "Tight", numericValue: -0.025, value: "tight" },
106
- { label: "Normal", numericValue: 0, value: "normal" },
107
- { label: "Wide", numericValue: 0.025, value: "wide" },
108
- { label: "Wider", numericValue: 0.05, value: "wider" },
109
- { label: "Widest", numericValue: 0.1, value: "widest" },
110
- ];
111
-
112
- const lineHeightSteps: Array<{
113
- label: string;
114
- numericValue: number;
115
- value: FontPickerLineHeightPreset;
116
- }> = [
117
- { label: "None", numericValue: 1, value: "none" },
118
- { label: "Tight", numericValue: 1.25, value: "tight" },
119
- { label: "Snug", numericValue: 1.375, value: "snug" },
120
- { label: "Normal", numericValue: 1.5, value: "normal" },
121
- { label: "Relaxed", numericValue: 1.625, value: "relaxed" },
122
- { label: "Loose", numericValue: 2, value: "loose" },
123
- ];
124
-
125
- const textCaseOptions: Array<{
126
- label: string;
127
- value: FontPickerTextCasePreset;
128
- }> = [
129
- { label: "As typed", value: "original" },
130
- { label: "Uppercase", value: "uppercase" },
131
- { label: "Lowercase", value: "lowercase" },
132
- { label: "Capitalize", value: "capitalize" },
133
- { label: "Title Case", value: "titleCase" },
134
- ];
135
-
136
- function isFontPickerTextCase(value: unknown): value is FontPickerTextCasePreset {
137
- return textCaseOptions.some((option) => option.value === value);
138
- }
139
-
140
- function getStepByValue<Value extends string>(
141
- steps: readonly { numericValue: number; value: Value }[],
142
- value: Value,
143
- fallbackValue: Value,
144
- ): { numericValue: number; value: Value } {
145
- const step = steps.find((item) => item.value === value);
146
- if (step) {
147
- return step;
148
- }
149
-
150
- return steps.find((item) => item.value === fallbackValue) ?? steps[0]!;
151
- }
152
-
153
- function getStepIndexByValue<Value extends string>(
154
- steps: readonly { value: Value }[],
155
- value: Value,
156
- fallbackValue: Value,
157
- ): number {
158
- const stepIndex = steps.findIndex((item) => item.value === value);
159
- if (stepIndex >= 0) {
160
- return stepIndex;
161
- }
162
-
163
- const fallbackIndex = steps.findIndex((item) => item.value === fallbackValue);
164
- return fallbackIndex >= 0 ? fallbackIndex : 0;
165
- }
166
-
167
- function normalizeFontPickerValue(
168
- value: FontPickerControlProps["value"],
169
- ): FontPickerValue {
170
- const fontId = resolveFontPickerFontId(
171
- typeof value === "string" ? value : value?.fontId,
172
- );
173
- const font = getFontPickerFontById(fontId);
174
-
175
- if (typeof value === "string") {
176
- return {
177
- color: defaultFontPickerColor,
178
- fontId,
179
- fontSize: defaultFontPickerFontSizePx,
180
- fontWeight: resolveFontPickerFontWeight(font),
181
- letterSpacing: "normal",
182
- lineHeight: "normal",
183
- opacity: defaultFontPickerOpacity,
184
- textCase: "original",
185
- };
186
- }
187
-
188
- return {
189
- color: normalizeFontPickerColor(value?.color),
190
- fontId,
191
- fontSize: normalizeFontPickerFontSize(value?.fontSize),
192
- fontWeight: resolveFontPickerFontWeight(font, value?.fontWeight),
193
- letterSpacing: value?.letterSpacing ?? "normal",
194
- lineHeight: value?.lineHeight ?? "normal",
195
- opacity: normalizeFontPickerOpacity(value?.opacity),
196
- textCase: isFontPickerTextCase(value?.textCase) ? value.textCase : "original",
197
- };
198
- }
199
-
200
- function normalizeFontPickerColor(value: unknown): string {
201
- return typeof value === "string" && value.trim() ? value : defaultFontPickerColor;
202
- }
203
-
204
- function normalizeFontPickerOpacity(value: unknown): number {
205
- const nextOpacity =
206
- typeof value === "number" ? value : Number.parseFloat(String(value ?? ""));
207
-
208
- return Number.isFinite(nextOpacity)
209
- ? Math.min(100, Math.max(0, Math.round(nextOpacity)))
210
- : defaultFontPickerOpacity;
211
- }
212
-
213
- function getFontPickerWeightOptions(
214
- font: FontPickerFontCatalogEntry | null,
215
- ): string[] {
216
- const weights = font?.weights.length ? font.weights : ["400"];
217
-
218
- return Array.from(new Set(weights.map((weight) => String(weight)))).sort(
219
- (left, right) => Number(left) - Number(right),
220
- );
221
- }
222
-
223
- function resolveFontPickerFontWeight(
224
- font: FontPickerFontCatalogEntry | null,
225
- weight?: string,
226
- ): string {
227
- const weights = getFontPickerWeightOptions(font);
228
- const requestedWeight = typeof weight === "string" ? weight : undefined;
229
-
230
- if (requestedWeight && weights.includes(requestedWeight)) {
231
- return requestedWeight;
232
- }
233
-
234
- if (weights.includes("400")) {
235
- return "400";
236
- }
237
-
238
- const requestedNumericWeight = Number(requestedWeight ?? 400);
239
- if (Number.isFinite(requestedNumericWeight)) {
240
- return weights.reduce((closest, current) => {
241
- return Math.abs(Number(current) - requestedNumericWeight) <
242
- Math.abs(Number(closest) - requestedNumericWeight)
243
- ? current
244
- : closest;
245
- }, weights[0] ?? "400");
246
- }
247
-
248
- return weights[0] ?? "400";
249
- }
250
-
251
- function normalizeFontPickerFontSize(value: unknown): number {
252
- if (typeof value !== "number" || !Number.isFinite(value)) {
253
- return defaultFontPickerFontSizePx;
254
- }
255
-
256
- return Math.max(minFontPickerFontSizePx, Math.round(value));
257
- }
258
-
259
- function getFontFamilyStyle(font: FontPickerFontCatalogEntry | null): React.CSSProperties | undefined {
260
- return font
261
- ? {
262
- fontFamily: `"${font.family}", ui-sans-serif, system-ui, sans-serif`,
263
- }
264
- : undefined;
265
- }
266
-
267
- function LetterSpacingIcon(): React.JSX.Element {
268
- return (
269
- <svg
270
- aria-hidden
271
- className="size-4 shrink-0"
272
- data-slot="font-picker-footer-icon"
273
- fill="none"
274
- height="16"
275
- viewBox="0 0 16 16"
276
- width="16"
277
- xmlns="http://www.w3.org/2000/svg"
278
- >
279
- <rect fill="#D9D9D9" height="16" width="1" />
280
- <rect fill="#D9D9D9" height="16" width="1" x="15" />
281
- <path
282
- d="M5.18182 13H4L7.41818 3H8.58182L12 13H10.8182L8.03636 4.58203H7.96364L5.18182 13ZM5.61818 9.09375H10.3818V10.168H5.61818V9.09375Z"
283
- fill="white"
284
- />
285
- </svg>
286
- );
287
- }
288
-
289
- function LineHeightIcon(): React.JSX.Element {
290
- return (
291
- <svg
292
- aria-hidden
293
- className="size-4 shrink-0"
294
- data-slot="font-picker-footer-icon"
295
- fill="none"
296
- height="16"
297
- viewBox="0 0 16 16"
298
- width="16"
299
- xmlns="http://www.w3.org/2000/svg"
300
- >
301
- <rect
302
- fill="#D9D9D9"
303
- height="16"
304
- transform="rotate(90 16 0)"
305
- width="1"
306
- x="16"
307
- />
308
- <rect
309
- fill="#D9D9D9"
310
- height="16"
311
- transform="rotate(90 16 15)"
312
- width="1"
313
- x="16"
314
- y="15"
315
- />
316
- <path
317
- d="M5.18182 13H4L7.41818 3H8.58182L12 13H10.8182L8.03636 4.58203H7.96364L5.18182 13ZM5.61818 9.09375H10.3818V10.168H5.61818V9.09375Z"
318
- fill="white"
319
- />
320
- </svg>
321
- );
322
- }
323
-
324
- function FontPickerFooterControl({
325
- disabled,
326
- icon,
327
- onValueChange,
328
- steps,
329
- title,
330
- valueIndex,
331
- }: {
332
- disabled: boolean;
333
- icon: React.ReactNode;
334
- onValueChange: (nextValue: number) => void;
335
- steps: readonly unknown[];
336
- title: string;
337
- valueIndex: number;
338
- }): React.JSX.Element {
339
- const markerValues = steps.map((_, index) => index);
340
- const min = 0;
341
- const max = Math.max(0, markerValues.length - 1);
342
- const currentValue = Math.min(max, Math.max(min, Math.round(valueIndex)));
343
-
344
- return (
345
- <div
346
- className="flex min-w-0 flex-1 items-center gap-2"
347
- data-slot="font-picker-footer-control"
348
- >
349
- {icon}
350
- <div className="min-w-0 flex-1" data-slot="font-picker-footer-slider">
351
- <Slider
352
- className="[&_[data-slot=slider-range]]:transition-none [&_[data-slot=slider-thumb]]:transition-none"
353
- disabled={disabled}
354
- getAriaLabel={() => title}
355
- markerValues={markerValues}
356
- max={max}
357
- min={min}
358
- onValueChange={(nextValue) => {
359
- const resolvedValue = Array.isArray(nextValue) ? nextValue[0] : nextValue;
360
-
361
- if (typeof resolvedValue === "number") {
362
- onValueChange(Math.min(max, Math.max(min, Math.round(resolvedValue))));
363
- }
364
- }}
365
- showFill
366
- snapValues={markerValues}
367
- step={1}
368
- value={[currentValue]}
369
- variant="discrete"
370
- />
371
- </div>
372
- </div>
373
- );
374
- }
375
-
376
52
  export function FontPickerControl({
377
53
  defaultValue,
378
54
  disabled = false,
@@ -383,22 +59,6 @@ export function FontPickerControl({
383
59
  value,
384
60
  }: FontPickerControlProps): React.JSX.Element {
385
61
  const [open, setOpen] = React.useState(false);
386
- const [query, setQuery] = React.useState("");
387
- const [category, setCategory] =
388
- React.useState<FontPickerFontFilterValue>("sans-serif");
389
- const [scrollTop, setScrollTop] = React.useState(0);
390
- const [viewportHeight, setViewportHeight] = React.useState(0);
391
- const [scrollViewportElement, setScrollViewportElement] =
392
- React.useState<HTMLDivElement | null>(null);
393
- const searchInputRef = React.useRef<HTMLInputElement>(null);
394
- const scrollViewportRef = React.useRef<HTMLDivElement | null>(null);
395
- const previousScrollTopRef = React.useRef(0);
396
- const scrollDirectionRef = React.useRef<"backward" | "forward">("forward");
397
- const shouldScrollSelectedOnOpenRef = React.useRef(false);
398
- const scrollFrameRef = React.useRef<number | null>(null);
399
- const previewFrameRef = React.useRef<number | null>(null);
400
- const pendingPreviewFontIdRef = React.useRef<string | null>(null);
401
- const lastEmittedPreviewFontIdRef = React.useRef<string | null>(null);
402
62
  const triggerRef = React.useRef<HTMLButtonElement | null>(null);
403
63
  const familyWeightRowRef = React.useRef<HTMLDivElement | null>(null);
404
64
  const familyWeightRowWidth = useMeasuredElementWidth(familyWeightRowRef);
@@ -408,52 +68,42 @@ export function FontPickerControl({
408
68
  String(normalizedValue.fontSize),
409
69
  );
410
70
  const selectedFont = getFontPickerFontById(normalizedValue.fontId);
411
- const filteredFonts = React.useMemo(
412
- () => filterFontPickerFonts(query, category),
413
- [category, query],
414
- );
415
- const selectedFontIndex = React.useMemo(() => {
416
- if (!selectedFont) {
417
- return -1;
418
- }
419
-
420
- return filteredFonts.findIndex((font) => font.id === selectedFont.id);
421
- }, [filteredFonts, selectedFont]);
422
- const resolvedViewportHeight =
423
- viewportHeight > 0 ? viewportHeight : fontListHeightWithFooterPx;
424
- const selectedFontTop = selectedFontIndex >= 0 ? selectedFontIndex * fontVirtualItemStepPx : 0;
425
- const selectedFontBottom = selectedFontTop + fontItemHeightPx;
426
- const selectedFontVisible =
427
- selectedFontIndex >= 0 &&
428
- selectedFontBottom > scrollTop &&
429
- selectedFontTop < scrollTop + resolvedViewportHeight;
430
- const pinnedSelectedRowSide =
431
- selectedFont && selectedFontIndex >= 0 && !selectedFontVisible
432
- ? selectedFontBottom <= scrollTop
433
- ? "top"
434
- : "bottom"
435
- : null;
436
- const visibleItemCount = Math.max(
437
- 1,
438
- Math.ceil(resolvedViewportHeight / fontVirtualItemStepPx),
439
- );
440
- const virtualStartIndex = Math.max(
441
- 0,
442
- Math.floor(scrollTop / fontVirtualItemStepPx) - fontListOverscanItems,
443
- );
444
- const virtualEndIndex = Math.min(
445
- filteredFonts.length,
446
- virtualStartIndex + visibleItemCount + fontListOverscanItems * 2,
447
- );
448
- const visibleFonts = React.useMemo(
449
- () => filteredFonts.slice(virtualStartIndex, virtualEndIndex),
450
- [filteredFonts, virtualEndIndex, virtualStartIndex],
451
- );
452
- const topSpacerHeight = virtualStartIndex * fontVirtualItemStepPx;
453
- const bottomSpacerHeight = Math.max(
454
- 0,
455
- (filteredFonts.length - virtualEndIndex) * fontVirtualItemStepPx,
456
- );
71
+ const {
72
+ attachScrollViewport,
73
+ bottomSpacerHeight,
74
+ cancelOpenSelectedScroll,
75
+ category,
76
+ filteredFonts,
77
+ pinnedSelectedRowSide,
78
+ prepareForOpen,
79
+ query,
80
+ scrollToFontIndex,
81
+ searchInputRef,
82
+ selectedFontIndex,
83
+ setCategoryWithReset,
84
+ setQueryWithReset,
85
+ topSpacerHeight,
86
+ virtualEndIndex,
87
+ virtualStartIndex,
88
+ visibleFonts,
89
+ } = useFontPickerVirtualList({
90
+ open,
91
+ selectedFont,
92
+ });
93
+ const {
94
+ cancelHoverPreviewIntent,
95
+ clearHoverPreview,
96
+ emitPreviewChange,
97
+ handleHoverPreview,
98
+ scheduleHoverPreviewIntent,
99
+ warmFontPreview,
100
+ } = useFontPickerPreviewPipeline({
101
+ disabled,
102
+ onPreviewChange,
103
+ open,
104
+ selectedFont,
105
+ visibleFonts,
106
+ });
457
107
  const emitChange = React.useCallback(
458
108
  (nextValue: FontPickerValue, meta?: ControlChangeMeta) => {
459
109
  onValueChange?.(nextValue, meta);
@@ -484,157 +134,17 @@ export function FontPickerControl({
484
134
  }
485
135
  }
486
136
 
487
- const emitPreviewImmediately = React.useCallback(
488
- (nextFontId: string | null) => {
489
- if (disabled) {
490
- return;
491
- }
492
-
493
- if (nextFontId !== null && lastEmittedPreviewFontIdRef.current === nextFontId) {
494
- return;
495
- }
496
-
497
- lastEmittedPreviewFontIdRef.current = nextFontId;
498
- onPreviewChange?.(nextFontId);
499
- },
500
- [disabled, onPreviewChange],
501
- );
502
-
503
- const cancelScheduledPreview = React.useCallback(() => {
504
- if (previewFrameRef.current !== null) {
505
- window.cancelAnimationFrame(previewFrameRef.current);
506
- previewFrameRef.current = null;
507
- }
508
-
509
- pendingPreviewFontIdRef.current = null;
510
- }, []);
511
-
512
- const emitPreviewChange = React.useCallback(
513
- (nextFontId: string | null, options?: { immediate?: boolean }) => {
514
- if (disabled) {
515
- return;
516
- }
517
-
518
- if (nextFontId === null || options?.immediate) {
519
- cancelScheduledPreview();
520
- emitPreviewImmediately(nextFontId);
521
- return;
522
- }
523
-
524
- pendingPreviewFontIdRef.current = nextFontId;
525
- if (previewFrameRef.current !== null) {
526
- return;
527
- }
528
-
529
- previewFrameRef.current = window.requestAnimationFrame(() => {
530
- previewFrameRef.current = null;
531
- const scheduledFontId = pendingPreviewFontIdRef.current;
532
- pendingPreviewFontIdRef.current = null;
533
-
534
- if (scheduledFontId) {
535
- emitPreviewImmediately(scheduledFontId);
536
- }
537
- });
538
- },
539
- [cancelScheduledPreview, disabled, emitPreviewImmediately],
540
- );
541
-
542
- const resetViewportScroll = React.useCallback(() => {
543
- const viewportElement = scrollViewportRef.current;
544
- if (viewportElement) {
545
- viewportElement.scrollTop = 0;
546
- }
547
-
548
- previousScrollTopRef.current = 0;
549
- scrollDirectionRef.current = "forward";
550
- setScrollTop(0);
551
- }, []);
552
-
553
- const scrollToFontIndex = React.useCallback((index: number) => {
554
- if (index < 0 || !scrollViewportRef.current) {
555
- return;
556
- }
557
-
558
- const nextScrollTop = index * fontVirtualItemStepPx;
559
- scrollDirectionRef.current =
560
- nextScrollTop >= scrollViewportRef.current.scrollTop
561
- ? "forward"
562
- : "backward";
563
- previousScrollTopRef.current = nextScrollTop;
564
- scrollViewportRef.current.scrollTop = nextScrollTop;
565
- setScrollTop(nextScrollTop);
566
- }, []);
567
-
568
- const warmFontPreview = React.useCallback(
569
- (
570
- fontEntry: FontPickerFontCatalogEntry | null | undefined,
571
- priority: "high" | "normal" = "normal",
572
- ) => {
573
- if (!fontEntry) {
574
- return;
575
- }
576
-
577
- queueFontPickerPreviewLoad(fontEntry, { priority });
578
- },
579
- [],
580
- );
581
-
582
- const queueBufferedPreload = React.useCallback(
583
- (direction: "backward" | "forward") => {
584
- if (!filteredFonts.length) {
585
- return;
586
- }
587
-
588
- const backwardItems =
589
- direction === "forward"
590
- ? fontPreloadBufferBehindItems
591
- : fontPreloadBufferAheadItems;
592
- const forwardItems =
593
- direction === "forward"
594
- ? fontPreloadBufferAheadItems
595
- : fontPreloadBufferBehindItems;
596
- const preloadStart = Math.max(0, virtualStartIndex - backwardItems);
597
- const preloadEnd = Math.min(filteredFonts.length, virtualEndIndex + forwardItems);
598
-
599
- if (preloadStart >= preloadEnd) {
600
- return;
601
- }
602
-
603
- queueFontPickerPreviewLoadBatch(filteredFonts.slice(preloadStart, preloadEnd), {
604
- priority: "normal",
605
- });
606
- },
607
- [filteredFonts, virtualEndIndex, virtualStartIndex],
608
- );
609
-
610
- const handleHoverPreview = React.useCallback(
611
- (font: FontPickerFontCatalogEntry) => {
612
- warmFontPreview(font, "high");
613
- emitPreviewChange(font.id);
614
- },
615
- [emitPreviewChange, warmFontPreview],
616
- );
617
-
618
- const {
619
- cancelIntent: cancelHoverPreviewIntent,
620
- scheduleIntent: scheduleHoverPreviewIntent,
621
- } = useHoverIntent({ onIntent: handleHoverPreview });
622
-
623
137
  const handleOpenChange = React.useCallback(
624
138
  (nextOpen: boolean) => {
625
139
  if (nextOpen) {
626
- shouldScrollSelectedOnOpenRef.current = true;
627
- setQuery("");
628
- setCategory(selectedFont?.category ?? "sans-serif");
629
- resetViewportScroll();
140
+ prepareForOpen(selectedFont?.category ?? "sans-serif");
630
141
  }
631
142
 
632
143
  setOpen(nextOpen);
633
144
 
634
145
  if (!nextOpen) {
635
- shouldScrollSelectedOnOpenRef.current = false;
636
- cancelHoverPreviewIntent();
637
- emitPreviewChange(null, { immediate: true });
146
+ cancelOpenSelectedScroll();
147
+ clearHoverPreview();
638
148
  window.requestAnimationFrame(() => {
639
149
  if (document.activeElement === triggerRef.current) {
640
150
  triggerRef.current?.blur();
@@ -643,127 +153,13 @@ export function FontPickerControl({
643
153
  }
644
154
  },
645
155
  [
646
- cancelHoverPreviewIntent,
647
- emitPreviewChange,
648
- resetViewportScroll,
156
+ cancelOpenSelectedScroll,
157
+ clearHoverPreview,
158
+ prepareForOpen,
649
159
  selectedFont?.category,
650
160
  ],
651
161
  );
652
162
 
653
- React.useEffect(() => {
654
- if (selectedFont) {
655
- warmFontPreview(selectedFont, "high");
656
- }
657
- }, [selectedFont, warmFontPreview]);
658
-
659
- React.useEffect(() => {
660
- if (!open) {
661
- return;
662
- }
663
-
664
- if (selectedFont) {
665
- warmFontPreview(selectedFont, "high");
666
- }
667
-
668
- queueFontPickerPreviewLoadBatch(visibleFonts, { priority: "high" });
669
- }, [open, selectedFont, visibleFonts, warmFontPreview]);
670
-
671
- React.useEffect(() => {
672
- if (!open) {
673
- return;
674
- }
675
-
676
- queueBufferedPreload(scrollDirectionRef.current);
677
- }, [open, queueBufferedPreload, scrollTop]);
678
-
679
- React.useEffect(() => {
680
- if (!open) {
681
- return undefined;
682
- }
683
-
684
- const frame = window.requestAnimationFrame(() => {
685
- searchInputRef.current?.focus({ preventScroll: true });
686
- });
687
-
688
- return () => window.cancelAnimationFrame(frame);
689
- }, [open]);
690
-
691
- React.useEffect(() => {
692
- if (!open || !shouldScrollSelectedOnOpenRef.current || selectedFontIndex < 0) {
693
- return undefined;
694
- }
695
-
696
- const frame = window.requestAnimationFrame(() => {
697
- shouldScrollSelectedOnOpenRef.current = false;
698
- scrollToFontIndex(selectedFontIndex);
699
- });
700
-
701
- return () => window.cancelAnimationFrame(frame);
702
- }, [open, scrollToFontIndex, selectedFontIndex]);
703
-
704
- React.useEffect(() => {
705
- if (!open || !scrollViewportElement) {
706
- return undefined;
707
- }
708
-
709
- const viewportElement = scrollViewportElement;
710
- const syncMetrics = () => {
711
- setViewportHeight(
712
- viewportElement.clientHeight > 0
713
- ? viewportElement.clientHeight
714
- : fontListHeightWithFooterPx,
715
- );
716
- setScrollTop(viewportElement.scrollTop);
717
- };
718
- const handleScroll = () => {
719
- if (scrollFrameRef.current !== null) {
720
- window.cancelAnimationFrame(scrollFrameRef.current);
721
- }
722
-
723
- scrollFrameRef.current = window.requestAnimationFrame(() => {
724
- scrollFrameRef.current = null;
725
- const nextScrollTop = viewportElement.scrollTop;
726
- scrollDirectionRef.current =
727
- nextScrollTop >= previousScrollTopRef.current ? "forward" : "backward";
728
- previousScrollTopRef.current = nextScrollTop;
729
- setScrollTop(nextScrollTop);
730
- });
731
- };
732
-
733
- syncMetrics();
734
- viewportElement.addEventListener("scroll", handleScroll, { passive: true });
735
- window.addEventListener("resize", syncMetrics);
736
-
737
- return () => {
738
- viewportElement.removeEventListener("scroll", handleScroll);
739
- window.removeEventListener("resize", syncMetrics);
740
-
741
- if (scrollFrameRef.current !== null) {
742
- window.cancelAnimationFrame(scrollFrameRef.current);
743
- scrollFrameRef.current = null;
744
- }
745
- };
746
- }, [open, scrollViewportElement]);
747
-
748
- React.useEffect(() => {
749
- return () => {
750
- cancelHoverPreviewIntent();
751
- cancelScheduledPreview();
752
- onPreviewChange?.(null);
753
- };
754
- }, [cancelHoverPreviewIntent, cancelScheduledPreview, onPreviewChange]);
755
-
756
- const attachScrollViewport = React.useCallback(
757
- (node: HTMLDivElement | null) => {
758
- scrollViewportRef.current = node;
759
- setScrollViewportElement(node);
760
- },
761
- [],
762
- );
763
- const clearHoverPreview = React.useCallback(() => {
764
- cancelHoverPreviewIntent();
765
- emitPreviewChange(null, { immediate: true });
766
- }, [cancelHoverPreviewIntent, emitPreviewChange]);
767
163
  const selectedFamily = selectedFont?.family ?? getFontPickerFontById(getDefaultFontPickerFontId())?.family ?? "Inter";
768
164
  const fontWeightOptions = getFontPickerWeightOptions(selectedFont);
769
165
  const selectedFontPreviewStyle = selectedFont
@@ -846,245 +242,76 @@ export function FontPickerControl({
846
242
  </ScrollFade>
847
243
  </span>
848
244
  </PopoverTrigger>
849
- <PopoverContent
850
- align="start"
851
- className="w-(--anchor-width) gap-0 overflow-hidden rounded-lg border border-[color:color-mix(in_oklab,var(--border)_20%,transparent)] bg-[color:color-mix(in_oklab,var(--popover)_88%,transparent)] p-0 text-[color:var(--popover-foreground)] shadow-sm backdrop-blur-[12.5px]"
852
- finalFocus={false}
853
- sideOffset={6}
854
- style={
855
- familyWeightRowWidth
856
- ? { width: familyWeightRowWidth }
857
- : undefined
858
- }
859
- >
860
- <div>
861
- <div className="border-b border-[color:color-mix(in_oklab,var(--muted-foreground)_20%,transparent)]">
862
- <div className="relative h-10">
863
- <MagnifyingGlassIcon
864
- aria-hidden
865
- className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-[color:var(--muted-foreground)]"
866
- />
867
- <Input
868
- ref={searchInputRef}
869
- className="h-10 border-none bg-transparent pl-[34px] text-[13px] font-normal focus-visible:bg-transparent focus-visible:ring-0"
870
- name="font-search"
871
- onChange={(event) => {
872
- resetViewportScroll();
873
- setQuery(event.target.value);
874
- }}
875
- placeholder={searchPlaceholder}
876
- type="text"
877
- value={query}
878
- />
879
- </div>
880
- </div>
881
- <div className="relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-[color:color-mix(in_oklab,var(--muted-foreground)_20%,transparent)]">
882
- <div className="flex h-10 w-full items-center justify-between overflow-x-hidden px-3">
883
- {FONT_PICKER_FILTER_OPTIONS.map((option) => {
884
- const active = category === option.value;
885
-
886
- return (
887
- <button
888
- className={cn(
889
- "relative z-10 h-10 shrink-0 px-0 text-xs font-normal leading-none tracking-normal text-[color:var(--muted-foreground)] transition-colors after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-[color:var(--foreground)] after:opacity-0 after:transition-opacity after:duration-200 after:ease-in-out after:content-[''] hover:text-[color:color-mix(in_oklab,var(--foreground)_80%,transparent)]",
890
- active &&
891
- "text-[color:var(--foreground)] after:opacity-100",
892
- )}
893
- data-state={active ? "active" : "inactive"}
894
- key={option.value}
895
- onClick={() => {
896
- resetViewportScroll();
897
- setCategory(option.value);
898
- }}
899
- type="button"
900
- >
901
- {option.label}
902
- </button>
903
- );
904
- })}
905
- </div>
906
- </div>
907
- </div>
908
- <div
909
- className="relative isolate pb-1"
910
- onMouseLeave={clearHoverPreview}
911
- >
912
- <div className="px-1 pt-1">
913
- <div className="relative h-60">
914
- <ScrollFade
915
- className="toolcraft-scrollbar h-full"
916
- containerClassName="h-full"
917
- data-slot="font-picker-list-viewport"
918
- height={24}
919
- preset="default"
920
- showOppositeSide
921
- side="bottom"
922
- viewportRef={attachScrollViewport}
923
- watch={[
924
- filteredFonts.length,
925
- query,
926
- category,
927
- virtualStartIndex,
928
- virtualEndIndex,
929
- ]}
930
- >
931
- {visibleFonts.length ? (
932
- <div
933
- className="flex flex-col gap-px"
934
- data-slot="font-picker-list"
935
- >
936
- {topSpacerHeight > 0 ? (
937
- <div
938
- aria-hidden
939
- style={{ height: `${topSpacerHeight}px` }}
940
- />
941
- ) : null}
942
- {visibleFonts.map((font) => {
943
- const selected = font.id === normalizedValue.fontId;
944
-
945
- return (
946
- <button
947
- className={cn(
948
- "flex min-h-9 w-full items-center justify-between gap-3 rounded-sm px-2.5 text-left text-sm font-normal text-[color:color-mix(in_oklab,var(--foreground)_85%,transparent)] outline-none",
949
- menuItemInteractionClassName,
950
- selected &&
951
- "bg-[color:color-mix(in_oklab,var(--muted-foreground)_5%,transparent)] font-medium text-[color:var(--foreground)] hover:bg-[color:color-mix(in_oklab,var(--muted-foreground)_5%,transparent)] focus:bg-[color:color-mix(in_oklab,var(--muted-foreground)_5%,transparent)]",
952
- )}
953
- key={font.id}
954
- onBlur={clearHoverPreview}
955
- onClick={() => {
956
- cancelHoverPreviewIntent();
957
- warmFontPreview(font, "high");
958
- emitPreviewChange(null, { immediate: true });
959
- emitChange({
960
- ...normalizedValue,
961
- fontId: font.id,
962
- fontWeight: resolveFontPickerFontWeight(
963
- font,
964
- normalizedValue.fontWeight,
965
- ),
966
- });
967
- }}
968
- onFocus={() => {
969
- cancelHoverPreviewIntent();
970
- handleHoverPreview(font);
971
- }}
972
- onMouseEnter={() =>
973
- scheduleHoverPreviewIntent(font)
974
- }
975
- type="button"
976
- >
977
- <span
978
- className="min-w-0 truncate text-sm"
979
- style={getFontFamilyStyle(font)}
980
- >
981
- {font.family}
982
- </span>
983
- {selected ? (
984
- <CheckIcon
985
- aria-hidden
986
- className="size-3.5 shrink-0 text-[color:var(--foreground)]"
987
- weight="bold"
988
- />
989
- ) : null}
990
- </button>
991
- );
992
- })}
993
- {bottomSpacerHeight > 0 ? (
994
- <div
995
- aria-hidden
996
- style={{ height: `${bottomSpacerHeight}px` }}
997
- />
998
- ) : null}
999
- </div>
1000
- ) : null}
1001
- </ScrollFade>
1002
- {!visibleFonts.length ? (
1003
- <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
1004
- <div className="rounded-sm px-2 text-center text-xs text-[color:var(--muted-foreground)]">
1005
- No fonts match your search.
1006
- </div>
1007
- </div>
1008
- ) : null}
1009
- </div>
1010
- </div>
1011
- {pinnedSelectedRowSide ? (
1012
- <div
1013
- className={cn(
1014
- "absolute inset-x-0 z-20 overflow-hidden bg-[color:color-mix(in_oklab,var(--popover)_90%,transparent)]",
1015
- pinnedSelectedRowSide === "top"
1016
- ? "top-0 border-b border-[color:color-mix(in_oklab,var(--foreground)_10%,transparent)]"
1017
- : "bottom-0 border-t border-[color:color-mix(in_oklab,var(--foreground)_10%,transparent)]",
1018
- )}
1019
- >
1020
- <button
1021
- aria-label={`Jump to selected font ${selectedFont?.family ?? ""}`}
1022
- className={cn(
1023
- "flex min-h-9 w-full items-center justify-between px-[14px] text-left text-sm font-medium text-[color:var(--foreground)] outline-none",
1024
- menuItemInteractionClassName,
1025
- )}
1026
- data-side={pinnedSelectedRowSide}
1027
- data-slot="selected-font-jump-row"
1028
- onClick={() => scrollToFontIndex(selectedFontIndex)}
1029
- onMouseDown={(event) => event.preventDefault()}
1030
- type="button"
1031
- >
1032
- <span
1033
- className="min-w-0 flex-1 truncate text-sm"
1034
- style={selectedFontPreviewStyle}
1035
- >
1036
- {selectedFont?.family ?? ""}
1037
- </span>
1038
- <CheckIcon
1039
- aria-hidden
1040
- className="size-3.5 shrink-0"
1041
- weight="bold"
1042
- />
1043
- </button>
1044
- </div>
1045
- ) : null}
1046
- </div>
1047
- <div className="flex h-11 shrink-0 items-center gap-5 border-t border-[color:color-mix(in_oklab,var(--muted-foreground)_20%,transparent)] px-3.5">
1048
- <FontPickerFooterControl
1049
- disabled={disabled}
1050
- icon={<LetterSpacingIcon />}
1051
- onValueChange={(nextIndex) => {
1052
- const nextStep =
1053
- letterSpacingSteps[nextIndex] ?? letterSpacingStep;
1054
-
1055
- emitChange(
1056
- {
1057
- ...normalizedValue,
1058
- letterSpacing: nextStep.value,
1059
- },
1060
- { history: "merge" },
1061
- );
1062
- }}
1063
- steps={letterSpacingSteps}
1064
- title="Letter spacing"
1065
- valueIndex={letterSpacingStepIndex}
1066
- />
1067
- <FontPickerFooterControl
1068
- disabled={disabled}
1069
- icon={<LineHeightIcon />}
1070
- onValueChange={(nextIndex) => {
1071
- const nextStep =
1072
- lineHeightSteps[nextIndex] ?? lineHeightStep;
1073
-
1074
- emitChange(
1075
- {
1076
- ...normalizedValue,
1077
- lineHeight: nextStep.value,
1078
- },
1079
- { history: "merge" },
1080
- );
1081
- }}
1082
- steps={lineHeightSteps}
1083
- title="Line height"
1084
- valueIndex={lineHeightStepIndex}
1085
- />
1086
- </div>
1087
- </PopoverContent>
245
+ <FontPickerPopoverContent
246
+ attachScrollViewport={attachScrollViewport}
247
+ bottomSpacerHeight={bottomSpacerHeight}
248
+ category={category}
249
+ clearHoverPreview={clearHoverPreview}
250
+ disabled={disabled}
251
+ filteredFontCount={filteredFonts.length}
252
+ lineHeightSteps={lineHeightSteps}
253
+ lineHeightValueIndex={lineHeightStepIndex}
254
+ letterSpacingSteps={letterSpacingSteps}
255
+ letterSpacingValueIndex={letterSpacingStepIndex}
256
+ onCategoryChange={setCategoryWithReset}
257
+ onFontBlur={clearHoverPreview}
258
+ onFontFocus={(font) => {
259
+ cancelHoverPreviewIntent();
260
+ handleHoverPreview(font);
261
+ }}
262
+ onFontMouseEnter={(font) => scheduleHoverPreviewIntent(font)}
263
+ onFontSelect={(font) => {
264
+ cancelHoverPreviewIntent();
265
+ warmFontPreview(font, "high");
266
+ emitPreviewChange(null, { immediate: true });
267
+ emitChange({
268
+ ...normalizedValue,
269
+ fontId: font.id,
270
+ fontWeight: resolveFontPickerFontWeight(
271
+ font,
272
+ normalizedValue.fontWeight,
273
+ ),
274
+ });
275
+ }}
276
+ onLetterSpacingValueChange={(nextIndex) => {
277
+ const nextStep =
278
+ letterSpacingSteps[nextIndex] ?? letterSpacingStep;
279
+
280
+ emitChange(
281
+ {
282
+ ...normalizedValue,
283
+ letterSpacing: nextStep.value,
284
+ },
285
+ { history: "merge" },
286
+ );
287
+ }}
288
+ onLineHeightValueChange={(nextIndex) => {
289
+ const nextStep = lineHeightSteps[nextIndex] ?? lineHeightStep;
290
+
291
+ emitChange(
292
+ {
293
+ ...normalizedValue,
294
+ lineHeight: nextStep.value,
295
+ },
296
+ { history: "merge" },
297
+ );
298
+ }}
299
+ onQueryChange={setQueryWithReset}
300
+ pinnedSelectedRowSide={pinnedSelectedRowSide}
301
+ popoverWidth={familyWeightRowWidth || undefined}
302
+ query={query}
303
+ scrollToFontIndex={scrollToFontIndex}
304
+ searchInputRef={searchInputRef}
305
+ searchPlaceholder={searchPlaceholder}
306
+ selectedFont={selectedFont}
307
+ selectedFontId={normalizedValue.fontId}
308
+ selectedFontIndex={selectedFontIndex}
309
+ selectedFontPreviewStyle={selectedFontPreviewStyle}
310
+ topSpacerHeight={topSpacerHeight}
311
+ virtualEndIndex={virtualEndIndex}
312
+ virtualStartIndex={virtualStartIndex}
313
+ visibleFonts={visibleFonts}
314
+ />
1088
315
  </Popover>
1089
316
  </div>
1090
317
  <div