@pixel-point/toolcraft 0.0.7 → 0.0.9

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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +185 -23
  7. package/templates/runtime/contracts/component-contracts.ts +91 -36
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +4 -4
  10. package/templates/runtime/export/export.test.ts +31 -0
  11. package/templates/runtime/export/export.ts +41 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  13. package/templates/runtime/react/canvas-shell.tsx +178 -23
  14. package/templates/runtime/react/control-conditions.ts +166 -0
  15. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  16. package/templates/runtime/react/controls-panel.test.tsx +774 -17
  17. package/templates/runtime/react/controls-panel.tsx +155 -8
  18. package/templates/runtime/react/media-file.ts +19 -0
  19. package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
  20. package/templates/runtime/schema/define-toolcraft.ts +29 -3
  21. package/templates/runtime/schema/types.ts +7 -0
  22. package/templates/runtime/state/reducer.test.ts +304 -0
  23. package/templates/runtime/state/reducer.ts +148 -9
  24. package/templates/runtime/state/types.ts +10 -2
  25. package/templates/runtime/testing/performance.test.ts +1424 -56
  26. package/templates/runtime/testing/performance.ts +710 -43
  27. package/templates/starter/AGENTS.md +10 -9
  28. package/templates/starter/docs/toolcraft/README.md +1 -1
  29. package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
  30. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
  31. package/templates/starter/docs/toolcraft/component-rules.md +46 -16
  32. package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
  33. package/templates/starter/docs/toolcraft/performance.md +54 -6
  34. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  35. package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
  36. package/templates/starter/docs/toolcraft/workflow.md +2 -2
  37. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  38. package/templates/starter/e2e/performance-helpers.ts +197 -0
  39. package/templates/starter/gitignore +1 -0
  40. package/templates/starter/package.json +5 -0
  41. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  42. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  43. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  44. package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
  45. package/templates/starter/src/app/starter-acceptance.ts +740 -24
  46. package/templates/starter/src/app/starter-performance.test.ts +66 -5
  47. package/templates/ui/components/control-layout/index.tsx +8 -3
  48. package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
  49. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  50. package/templates/ui/components/controls/color/index.ts +4 -1
  51. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  52. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  53. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  54. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  55. package/templates/ui/components/controls/index.ts +3 -0
  56. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  57. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  58. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  59. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  60. package/templates/ui/components/controls/vector/index.ts +1 -0
  61. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  62. package/templates/ui/components/panel/panel-section.tsx +82 -6
@@ -138,6 +138,18 @@ function projectDocsIncludeRendererLayerInventory(): boolean {
138
138
  );
139
139
  }
140
140
 
141
+ function projectDocsIncludeRendererPipelineInventory(): boolean {
142
+ const decisionSources = readProjectDecisionSources();
143
+
144
+ return (
145
+ /Render Pipeline Inventory|rendererPipeline|render pipeline/i.test(decisionSources) &&
146
+ /pass|passes|cacheKey|cache key|invalidat/i.test(decisionSources) &&
147
+ /control-drag|viewport-zoom|viewport-drag|media-import|animation-frame|timeline-playback|interaction/i.test(
148
+ decisionSources,
149
+ )
150
+ );
151
+ }
152
+
141
153
  function projectDocsExplainRendererAlternatives(): boolean {
142
154
  const decisionSources = readProjectDecisionSources();
143
155
 
@@ -198,12 +210,24 @@ function projectDocsIncludeFixedBackgroundDecision(): boolean {
198
210
 
199
211
  function sourceUsesCpuPixelLoop(): boolean {
200
212
  const appSources = stripJsComments(readFiles(srcDir, /\.(ts|tsx)$/));
213
+ const cpuPixelMethodCallPattern =
214
+ /(?:\.(?:createImageData|getImageData|putImageData)|\[\s*["'](?:createImageData|getImageData|putImageData)["']\s*\])\s*\(/;
201
215
 
202
216
  return (
203
217
  /new\s+ImageData\s*\(/.test(appSources) ||
204
- /\.createImageData\s*\(/.test(appSources) ||
205
- /\.getImageData\s*\(/.test(appSources) ||
206
- /\.putImageData\s*\(/.test(appSources)
218
+ cpuPixelMethodCallPattern.test(appSources)
219
+ );
220
+ }
221
+
222
+ function appPerformanceHasRenderPipelinePass(kind: string): boolean {
223
+ return (starterPerformance.rendererPipeline?.passes ?? []).some(
224
+ (pass) => pass.kind === kind,
225
+ );
226
+ }
227
+
228
+ function appPerformanceHasInteractionInvalidation(interaction: string): boolean {
229
+ return (starterPerformance.rendererPipeline?.interactionInvalidation ?? []).some(
230
+ (entry) => entry.interaction === interaction,
207
231
  );
208
232
  }
209
233
 
@@ -345,6 +369,17 @@ describe("Toolcraft template app performance coverage", () => {
345
369
  ).toBe(true);
346
370
  });
347
371
 
372
+ it("requires custom renderer apps to document the render pipeline inventory", () => {
373
+ if (!starterPerformance.usesCustomRenderer) {
374
+ return;
375
+ }
376
+
377
+ expect(
378
+ projectDocsIncludeRendererPipelineInventory(),
379
+ "Custom renderer apps must write a Render Pipeline Inventory in their app spec/plan docs before implementation so expensive passes, cache keys, and invalidation rules are deliberate.",
380
+ ).toBe(true);
381
+ });
382
+
348
383
  it("requires custom renderer apps to mirror the renderer decision in typed performance config", () => {
349
384
  if (!starterPerformance.usesCustomRenderer) {
350
385
  return;
@@ -356,6 +391,17 @@ describe("Toolcraft template app performance coverage", () => {
356
391
  ).toBeDefined();
357
392
  });
358
393
 
394
+ it("requires custom renderer apps to mirror the render pipeline in typed performance config", () => {
395
+ if (!starterPerformance.usesCustomRenderer) {
396
+ return;
397
+ }
398
+
399
+ expect(
400
+ starterPerformance.rendererPipeline,
401
+ "Custom renderer apps must declare rendererPipeline in app-performance.ts; prose specs and plans are not enough.",
402
+ ).toBeDefined();
403
+ });
404
+
359
405
  it("requires custom renderer apps to mirror layer inventory in typed performance config", () => {
360
406
  if (!starterPerformance.usesCustomRenderer) {
361
407
  return;
@@ -453,7 +499,7 @@ describe("Toolcraft template app performance coverage", () => {
453
499
  );
454
500
  });
455
501
 
456
- it("requires pixel-output renderers to use WebGL or WebGPU", () => {
502
+ it("requires pixel-output renderers to use GPU or measured CPU evidence", () => {
457
503
  expect(
458
504
  validateToolcraftPerformanceCoverage(starterSchema, {
459
505
  rendererStrategy: "canvas-2d",
@@ -464,7 +510,7 @@ describe("Toolcraft template app performance coverage", () => {
464
510
  }),
465
511
  ).toEqual(
466
512
  expect.arrayContaining([
467
- 'rendererWorkload "pixel-output" must use rendererStrategy "webgl" or "webgpu", received "canvas-2d".',
513
+ 'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires measured WebGL/WebGPU evidence in rendererTechnique.whyNotAlternativeStrategies or performanceRisks.',
468
514
  ]),
469
515
  );
470
516
  });
@@ -681,6 +727,21 @@ describe("Toolcraft template app performance coverage", () => {
681
727
  sourceUsesGpuRenderer(),
682
728
  "Procedural pixel renderers must contain an actual WebGL/WebGPU code path, not only declare a GPU strategy.",
683
729
  ).toBe(true);
730
+ expect(
731
+ appPerformanceHasRenderPipelinePass("pixel-transform"),
732
+ "Procedural pixel renderers must declare a rendererPipeline pixel-transform pass so caching and invalidation are machine-checkable.",
733
+ ).toBe(true);
734
+ });
735
+
736
+ it("requires custom renderers to declare high-frequency viewport invalidation", () => {
737
+ if (!starterPerformance.usesCustomRenderer) {
738
+ return;
739
+ }
740
+
741
+ expect(
742
+ appPerformanceHasInteractionInvalidation("viewport-zoom"),
743
+ "Custom renderer apps must declare rendererPipeline interactionInvalidation for viewport-zoom so zoom can stay responsive without recomputing expensive passes.",
744
+ ).toBe(true);
684
745
  });
685
746
 
686
747
  it("requires WebGL/WebGPU renderers to keep their pipeline lifecycle outside React render", () => {
@@ -90,7 +90,7 @@ export function ControlInlineGroup({
90
90
  {...props}
91
91
  className={cn(
92
92
  "grid min-w-0",
93
- kind === "slider" ? "gap-4" : "gap-2",
93
+ kind === "slider" ? "gap-4" : "gap-x-2.5 gap-y-2",
94
94
  kind === "toggleParameter" && "items-end",
95
95
  className,
96
96
  )}
@@ -229,10 +229,13 @@ export function ControlItem({
229
229
  }: {
230
230
  allowCompoundDividers?: boolean;
231
231
  children: React.ReactNode;
232
- compoundDividerPlacement?: "both" | "bottom";
232
+ compoundDividerPlacement?: "both" | "bottom" | "top";
233
233
  flush?: boolean;
234
234
  }): React.JSX.Element {
235
- const showCompoundTopDivider = compoundDividerPlacement === "both";
235
+ const showCompoundTopDivider =
236
+ compoundDividerPlacement === "both" || compoundDividerPlacement === "top";
237
+ const showCompoundBottomDivider =
238
+ compoundDividerPlacement === "both" || compoundDividerPlacement === "bottom";
236
239
 
237
240
  return (
238
241
  <div
@@ -240,11 +243,13 @@ export function ControlItem({
240
243
  "min-w-0",
241
244
  !flush && "px-3",
242
245
  allowCompoundDividers &&
246
+ showCompoundBottomDivider &&
243
247
  "has-data-[control-section-divider=compound]:relative has-data-[control-section-divider=compound]:pb-[18px] has-data-[control-section-divider=compound]:after:absolute has-data-[control-section-divider=compound]:after:bottom-0 has-data-[control-section-divider=compound]:after:h-px has-data-[control-section-divider=compound]:after:bg-[color:color-mix(in_oklab,var(--border)_8%,transparent)]",
244
248
  allowCompoundDividers &&
245
249
  showCompoundTopDivider &&
246
250
  "has-data-[control-section-divider=compound]:pt-[18px] has-data-[control-section-divider=compound]:before:absolute has-data-[control-section-divider=compound]:before:top-0 has-data-[control-section-divider=compound]:before:h-px has-data-[control-section-divider=compound]:before:bg-[color:color-mix(in_oklab,var(--border)_8%,transparent)]",
247
251
  allowCompoundDividers &&
252
+ showCompoundBottomDivider &&
248
253
  (flush
249
254
  ? "has-data-[control-section-divider=compound]:after:inset-x-0"
250
255
  : "has-data-[control-section-divider=compound]:after:inset-x-3"),
@@ -98,11 +98,14 @@ export function ActionsControl({
98
98
  onAction,
99
99
  showLabel = true,
100
100
  }: ActionsControlProps): React.JSX.Element {
101
+ const hasSingleAction = actions.length === 1;
102
+
101
103
  return (
102
104
  <Field
103
105
  aria-label={showLabel ? undefined : name}
104
- className="h-fit min-w-0 flex-wrap items-center justify-between gap-y-2!"
105
- orientation="horizontal"
106
+ className="h-fit min-w-0 gap-2"
107
+ data-slot="actions-control"
108
+ orientation="vertical"
106
109
  >
107
110
  {showLabel ? (
108
111
  <ControlFieldLabel className="min-w-0" textClassName="min-w-0 truncate">
@@ -111,9 +114,11 @@ export function ActionsControl({
111
114
  ) : null}
112
115
  <div
113
116
  className={cn(
114
- "flex max-w-full shrink-0 flex-wrap items-center justify-start gap-1.5",
115
- showLabel && "ml-auto",
117
+ "grid max-w-full gap-1.5",
118
+ hasSingleAction ? "w-1/2 grid-cols-1" : "w-full grid-cols-2",
116
119
  )}
120
+ data-actions-count={actions.length}
121
+ data-slot="actions-control-buttons"
117
122
  >
118
123
  {actions.map((action) => {
119
124
  const actionContent = getActionContent(action);
@@ -124,6 +129,7 @@ export function ActionsControl({
124
129
  aria-label={getActionAriaLabel(action, actionContent)}
125
130
  key={actionValue}
126
131
  onClick={() => onAction?.(actionValue)}
132
+ className="w-full"
127
133
  size="sm"
128
134
  type="button"
129
135
  variant="outline"
@@ -14,12 +14,14 @@ export type CodeTextareaControlProps = {
14
14
  defaultValue?: string;
15
15
  name: string;
16
16
  onValueChange?: ControlValueChangeHandler<string>;
17
+ showLabel?: boolean;
17
18
  value: string;
18
19
  };
19
20
 
20
21
  export function CodeTextareaControl({
21
22
  name,
22
23
  onValueChange,
24
+ showLabel = true,
23
25
  value,
24
26
  }: CodeTextareaControlProps): React.JSX.Element {
25
27
  const [currentValue, setCurrentValue] = React.useState(value);
@@ -61,9 +63,11 @@ export function CodeTextareaControl({
61
63
 
62
64
  return (
63
65
  <Field className="min-w-0 gap-2">
64
- <div className="flex items-center">
65
- <ControlFieldLabel>{name}</ControlFieldLabel>
66
- </div>
66
+ {showLabel ? (
67
+ <div className="flex items-center">
68
+ <ControlFieldLabel>{name}</ControlFieldLabel>
69
+ </div>
70
+ ) : null}
67
71
  <Textarea
68
72
  aria-label={name}
69
73
  className="max-h-[calc(12lh+12px)] min-h-[84px] overflow-y-auto font-mono"
@@ -18,7 +18,10 @@ export {
18
18
  type ColorFormatMode,
19
19
  type ColorSurfaceModel,
20
20
  } from "./style-guide-color-picker-channel-utils";
21
- export { DEFAULT_COLOR_FORMAT_MODE } from "./style-guide-color-picker";
21
+ export {
22
+ DEFAULT_COLOR_FORMAT_MODE,
23
+ StyleGuideColorPicker,
24
+ } from "./style-guide-color-picker";
22
25
  export {
23
26
  getColorSurfaceSliderConfig,
24
27
  getColorSurfaceStyle,
@@ -459,9 +459,14 @@ export function useHueHandlers({
459
459
 
460
460
  const handleHuePreviewChange = useCallback(
461
461
  (nextHue: number) => {
462
- applyOptimisticColor({ h: nextHue, s: latestHsvRef.current.s, v: latestHsvRef.current.v });
462
+ const nextHex = applyOptimisticColor({
463
+ h: nextHue,
464
+ s: latestHsvRef.current.s,
465
+ v: latestHsvRef.current.v,
466
+ });
467
+ emitChange(nextHex);
463
468
  },
464
- [applyOptimisticColor, latestHsvRef],
469
+ [applyOptimisticColor, emitChange, latestHsvRef],
465
470
  );
466
471
 
467
472
  const handleHueCommit = useCallback(
@@ -170,9 +170,9 @@ function useRgbBlueHandlers({
170
170
 
171
171
  const handleSliderPreviewChange = useCallback(
172
172
  (nextValue: number) => {
173
- applyBlueChannel(nextValue);
173
+ emitChange(applyBlueChannel(nextValue));
174
174
  },
175
- [applyBlueChannel],
175
+ [applyBlueChannel, emitChange],
176
176
  );
177
177
 
178
178
  const handleSliderCommit = useCallback(