@pixel-point/toolcraft 0.0.8 → 0.0.11

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 (88) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +42 -5
  4. package/src/generate.test.mjs +40 -0
  5. package/src/package-json.mjs +15 -0
  6. package/src/package-json.test.mjs +14 -1
  7. package/templates/runtime/contracts/component-contracts.test.ts +251 -47
  8. package/templates/runtime/contracts/component-contracts.ts +130 -57
  9. package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +22 -9
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +916 -108
  17. package/templates/runtime/react/controls-panel.tsx +136 -30
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
  27. package/templates/runtime/schema/define-toolcraft.ts +140 -246
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +44 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +810 -21
  41. package/templates/runtime/testing/performance.ts +823 -53
  42. package/templates/starter/AGENTS.md +24 -18
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +75 -42
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +37 -11
  50. package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/gitignore +1 -0
  56. package/templates/starter/index.html +1 -0
  57. package/templates/starter/package.json +3 -0
  58. package/templates/starter/playwright.config.ts +1 -1
  59. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  60. package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
  61. package/templates/starter/scripts/toolcraft-port.mjs +280 -0
  62. package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
  63. package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
  64. package/templates/starter/src/app/starter-acceptance.ts +1453 -97
  65. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  66. package/templates/starter/src/app/starter-performance.ts +5 -0
  67. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  68. package/templates/starter/src/app/starter-schema.ts +6 -2
  69. package/templates/starter/vite.config.ts +58 -2
  70. package/templates/ui/components/control-layout/index.tsx +8 -3
  71. package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
  72. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  73. package/templates/ui/components/controls/color/index.ts +4 -1
  74. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  75. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  76. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  77. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  78. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  79. package/templates/ui/components/controls/index.ts +4 -0
  80. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  81. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  82. package/templates/ui/components/controls/select/select-control.tsx +8 -25
  83. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  84. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  85. package/templates/ui/components/controls/vector/index.ts +1 -0
  86. package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
  87. package/templates/ui/components/panel/panel-actions.tsx +1 -1
  88. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -17,6 +17,14 @@ export type VectorControlValue = {
17
17
  y: string;
18
18
  };
19
19
 
20
+ type VectorPadAxisLock = "x" | "y";
21
+
22
+ type VectorPadDragStart = {
23
+ clientX: number;
24
+ clientY: number;
25
+ value: VectorControlValue;
26
+ };
27
+
20
28
  export type VectorPadVariant =
21
29
  | "default"
22
30
  | "whiteBalance"
@@ -24,10 +32,13 @@ export type VectorPadVariant =
24
32
  | "chromaOffset"
25
33
  | "toneBias";
26
34
 
35
+ export type VectorPadCoordinateMode = "cartesian" | "screen";
36
+
27
37
  export type VectorControlProps = VectorControlValue & {
28
38
  defaultValue?: Partial<VectorControlValue>;
29
39
  name: string;
30
40
  onValueChange?: ControlValueChangeHandler<VectorControlValue>;
41
+ padCoordinateMode?: VectorPadCoordinateMode;
31
42
  padShape?: "compact" | "square";
32
43
  padVariant?: VectorPadVariant;
33
44
  xLabel?: string;
@@ -38,10 +49,20 @@ function clamp(value: number, min = 0, max = 1): number {
38
49
  return Math.min(max, Math.max(min, value));
39
50
  }
40
51
 
41
- function pointFromEvent(event: React.PointerEvent<HTMLElement>): CurvePoint {
52
+ function getDefaultPadCoordinateMode(
53
+ padVariant: VectorPadVariant,
54
+ ): VectorPadCoordinateMode {
55
+ return padVariant === "default" ? "screen" : "cartesian";
56
+ }
57
+
58
+ function pointFromEvent(
59
+ event: React.PointerEvent<HTMLElement>,
60
+ coordinateMode: VectorPadCoordinateMode,
61
+ ): CurvePoint {
42
62
  const rect = event.currentTarget.getBoundingClientRect();
43
63
  const x = clamp((event.clientX - rect.left) / rect.width);
44
- const y = clamp(1 - (event.clientY - rect.top) / rect.height);
64
+ const yRatio = clamp((event.clientY - rect.top) / rect.height);
65
+ const y = coordinateMode === "screen" ? yRatio : 1 - yRatio;
45
66
 
46
67
  return { x, y };
47
68
  }
@@ -54,6 +75,8 @@ type VectorPadStyle = React.CSSProperties & {
54
75
  "--xy-pad-y": string;
55
76
  };
56
77
 
78
+ const vectorPadAxisLockThresholdPx = 2;
79
+
57
80
  const whiteBalancePadBackgroundImage =
58
81
  "linear-gradient(90deg,color-mix(in oklab, #2f7cff 72%, transparent),color-mix(in oklab, var(--foreground) 8%, transparent) 50%,color-mix(in oklab, #ff9a22 78%, transparent)),linear-gradient(180deg,color-mix(in oklab, #d84f9a 68%, transparent),transparent 49%,color-mix(in oklab, #42ba62 72%, transparent)),radial-gradient(circle at 50% 50%,color-mix(in oklab, var(--foreground) 7%, transparent),color-mix(in oklab, var(--background) 14%, transparent) 62%)";
59
82
 
@@ -73,13 +96,20 @@ const vectorPadBackgroundImages: Partial<Record<VectorPadVariant, string>> = {
73
96
  whiteBalance: whiteBalancePadBackgroundImage,
74
97
  };
75
98
 
76
- function getVectorPoint(x: string, y: string): VectorPadStyle {
99
+ function getVectorPoint(
100
+ x: string,
101
+ y: string,
102
+ coordinateMode: VectorPadCoordinateMode,
103
+ ): VectorPadStyle {
77
104
  const parsedX = Number.parseFloat(x);
78
105
  const parsedY = Number.parseFloat(y);
79
106
  const clampedX = Number.isFinite(parsedX) ? clamp(parsedX, -1, 1) : 0;
80
107
  const clampedY = Number.isFinite(parsedY) ? clamp(parsedY, -1, 1) : 0;
81
108
  const xPosition = `${(clampedX + 1) * 50}%`;
82
- const yPosition = `${(1 - (clampedY + 1) / 2) * 100}%`;
109
+ const yPosition =
110
+ coordinateMode === "screen"
111
+ ? `${((clampedY + 1) / 2) * 100}%`
112
+ : `${(1 - (clampedY + 1) / 2) * 100}%`;
83
113
 
84
114
  return {
85
115
  "--xy-pad-display-x":
@@ -93,13 +123,25 @@ function getVectorPoint(x: string, y: string): VectorPadStyle {
93
123
  }
94
124
 
95
125
  function getVectorValueLabel(x: string, y: string): string {
96
- return `${x}, ${y}`;
126
+ return `${formatVectorPadCoordinate(x)}, ${formatVectorPadCoordinate(y)}`;
97
127
  }
98
128
 
99
129
  function normalizeVectorCoordinate(value: string | undefined): string {
100
130
  return typeof value === "string" && value.trim() ? value : "0.00";
101
131
  }
102
132
 
133
+ function formatVectorPadCoordinate(value: string | undefined): string {
134
+ const parsedValue = Number.parseFloat(normalizeVectorCoordinate(value));
135
+
136
+ if (!Number.isFinite(parsedValue)) {
137
+ return "0.00";
138
+ }
139
+
140
+ const roundedValue = Math.abs(parsedValue) < 0.005 ? 0 : clamp(parsedValue, -1, 1);
141
+
142
+ return roundedValue.toFixed(2);
143
+ }
144
+
103
145
  function parseVectorValueDraft(value: string): VectorControlValue | null {
104
146
  const matches = value.match(/[+-]?(?:\d+(?:[.,]\d+)?|[.,]\d+)/g);
105
147
 
@@ -277,20 +319,28 @@ function VectorPadHandle({ isDragging }: { isDragging: boolean }): React.JSX.Ele
277
319
  }
278
320
 
279
321
  function VectorPadField({
322
+ defaultValue,
280
323
  name,
281
324
  onValueChange,
325
+ padCoordinateMode,
282
326
  padShape = "compact",
283
327
  padVariant = "default",
284
328
  x,
285
329
  y,
286
330
  }: VectorControlProps): React.JSX.Element {
287
331
  const [isPointerDragging, setIsPointerDragging] = React.useState(false);
288
- const normalizedX = normalizeVectorCoordinate(x);
289
- const normalizedY = normalizeVectorCoordinate(y);
290
- const point = getVectorPoint(normalizedX, normalizedY);
332
+ const normalizedX = formatVectorPadCoordinate(x);
333
+ const normalizedY = formatVectorPadCoordinate(y);
334
+ const coordinateMode =
335
+ padCoordinateMode ?? getDefaultPadCoordinateMode(padVariant);
336
+ const point = getVectorPoint(normalizedX, normalizedY, coordinateMode);
291
337
  const valueLabel = getVectorValueLabel(normalizedX, normalizedY);
292
338
  const vectorPadBackgroundImage = vectorPadBackgroundImages[padVariant];
339
+ const axisLockRef = React.useRef<VectorPadAxisLock | null>(null);
340
+ const dragStartRef = React.useRef<VectorPadDragStart | null>(null);
293
341
  const liveHistoryGroupRef = React.useRef<string | null>(null);
342
+ const resetX = formatVectorPadCoordinate(defaultValue?.x);
343
+ const resetY = formatVectorPadCoordinate(defaultValue?.y);
294
344
  const updateVector = (
295
345
  nextX: string,
296
346
  nextY: string,
@@ -321,17 +371,51 @@ function VectorPadField({
321
371
  }
322
372
 
323
373
  function updateFromPointer(event: React.PointerEvent<HTMLButtonElement>): void {
324
- const nextPoint = pointFromEvent(event);
374
+ const nextPoint = pointFromEvent(event, coordinateMode);
375
+ const nextValue: VectorControlValue = {
376
+ x: (nextPoint.x * 2 - 1).toFixed(2),
377
+ y: (nextPoint.y * 2 - 1).toFixed(2),
378
+ };
379
+ const dragStart = dragStartRef.current;
380
+
381
+ if (event.shiftKey && dragStart) {
382
+ if (!axisLockRef.current) {
383
+ const deltaX = event.clientX - dragStart.clientX;
384
+ const deltaY = event.clientY - dragStart.clientY;
385
+
386
+ if (Math.hypot(deltaX, deltaY) < vectorPadAxisLockThresholdPx) {
387
+ updateVector(
388
+ dragStart.value.x,
389
+ dragStart.value.y,
390
+ getLiveHistoryMeta(),
391
+ );
392
+ return;
393
+ }
394
+
395
+ axisLockRef.current = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y";
396
+ }
397
+
398
+ updateVector(
399
+ axisLockRef.current === "x" ? nextValue.x : dragStart.value.x,
400
+ axisLockRef.current === "y" ? nextValue.y : dragStart.value.y,
401
+ getLiveHistoryMeta(),
402
+ );
403
+ return;
404
+ }
405
+
406
+ axisLockRef.current = null;
325
407
 
326
408
  updateVector(
327
- (nextPoint.x * 2 - 1).toFixed(2),
328
- (nextPoint.y * 2 - 1).toFixed(2),
409
+ nextValue.x,
410
+ nextValue.y,
329
411
  getLiveHistoryMeta(),
330
412
  );
331
413
  }
332
414
 
333
415
  function stopPointerDrag(): void {
334
416
  setIsPointerDragging(false);
417
+ axisLockRef.current = null;
418
+ dragStartRef.current = null;
335
419
  liveHistoryGroupRef.current = null;
336
420
  }
337
421
 
@@ -349,15 +433,28 @@ function VectorPadField({
349
433
  <button
350
434
  aria-label={`${name} X/Y pad`}
351
435
  className={cn(
352
- "relative w-full cursor-default! touch-none overflow-hidden rounded-[calc(var(--radius)+2px)] bg-[linear-gradient(180deg,color-mix(in_oklab,var(--foreground)_4%,transparent),color-mix(in_oklab,var(--foreground)_1%,transparent))] focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--foreground)_12%,transparent)] focus-visible:outline-none",
436
+ "relative w-full cursor-default! touch-none overflow-hidden rounded-[calc(var(--radius)+2px)] bg-[linear-gradient(180deg,color-mix(in_oklab,var(--foreground)_4%,transparent),color-mix(in_oklab,var(--foreground)_1%,transparent))] select-none focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--foreground)_12%,transparent)] focus-visible:outline-none",
353
437
  padShape === "square" ? "aspect-square" : "h-[142px]",
354
438
  )}
439
+ data-vector-pad-coordinate-mode={coordinateMode}
355
440
  data-vector-pad-shape={padShape}
356
441
  data-vector-pad-variant={padVariant}
442
+ onDoubleClick={(event) => {
443
+ event.preventDefault();
444
+ stopPointerDrag();
445
+ updateVector(resetX, resetY);
446
+ }}
357
447
  onLostPointerCapture={stopPointerDrag}
358
448
  onPointerCancel={stopPointerDrag}
359
449
  onPointerDown={(event) => {
450
+ event.preventDefault();
360
451
  setIsPointerDragging(true);
452
+ axisLockRef.current = null;
453
+ dragStartRef.current = {
454
+ clientX: event.clientX,
455
+ clientY: event.clientY,
456
+ value: { x: normalizedX, y: normalizedY },
457
+ };
361
458
  event.currentTarget.setPointerCapture(event.pointerId);
362
459
  updateFromPointer(event);
363
460
  }}
@@ -85,7 +85,7 @@ function getPanelActionIcon(action: PanelActionOption): ReactNode {
85
85
 
86
86
  const Icon = panelActionIconComponents[action.icon];
87
87
 
88
- return <Icon data-icon="inline-start" />;
88
+ return <Icon data-icon="inline-start" data-icon-name={action.icon} />;
89
89
  }
90
90
 
91
91
  function getPanelActionAriaLabel(
@@ -53,11 +53,11 @@ export function PanelSection({
53
53
  {childArray.map((child, index) => (
54
54
  <ControlItem
55
55
  allowCompoundDividers={shouldRenderInnerDividers}
56
- compoundDividerPlacement={
57
- shouldRenderInnerDividers && childArray.length > 1 && index === 0
58
- ? "bottom"
59
- : "both"
60
- }
56
+ compoundDividerPlacement={getCompoundDividerPlacement({
57
+ childCount: childArray.length,
58
+ index,
59
+ shouldRenderInnerDividers,
60
+ })}
61
61
  flush={flush || isActionSection}
62
62
  key={getPanelSectionChildKey(child, index)}
63
63
  >
@@ -100,6 +100,30 @@ export function PanelSection({
100
100
  );
101
101
  }
102
102
 
103
+ function getCompoundDividerPlacement({
104
+ childCount,
105
+ index,
106
+ shouldRenderInnerDividers,
107
+ }: {
108
+ childCount: number;
109
+ index: number;
110
+ shouldRenderInnerDividers: boolean;
111
+ }): "both" | "bottom" | "top" {
112
+ if (!shouldRenderInnerDividers || childCount <= 1) {
113
+ return "both";
114
+ }
115
+
116
+ if (index === 0) {
117
+ return "bottom";
118
+ }
119
+
120
+ if (index === childCount - 1) {
121
+ return "top";
122
+ }
123
+
124
+ return "both";
125
+ }
126
+
103
127
  function PanelSectionCollapsibleBody({
104
128
  collapsed,
105
129
  children,