@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.
- package/README.md +42 -9
- package/package.json +1 -1
- package/src/generate.mjs +42 -5
- package/src/generate.test.mjs +40 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +251 -47
- package/templates/runtime/contracts/component-contracts.ts +130 -57
- package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
- package/templates/runtime/contracts/decision-contracts.ts +22 -9
- package/templates/runtime/export/export.test.ts +63 -0
- package/templates/runtime/export/export.ts +55 -0
- package/templates/runtime/index.ts +1 -0
- package/templates/runtime/react/canvas-shell.test.tsx +58 -4
- package/templates/runtime/react/canvas-shell.tsx +31 -9
- package/templates/runtime/react/controls-panel.test.tsx +916 -108
- package/templates/runtime/react/controls-panel.tsx +136 -30
- package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
- package/templates/runtime/react/settings-transfer.test.ts +4 -0
- package/templates/runtime/react/settings-transfer.ts +6 -1
- package/templates/runtime/react/timeline-panel.test.tsx +14 -0
- package/templates/runtime/react/timeline-panel.tsx +44 -7
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
- package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
- package/templates/runtime/react/toolcraft-app.tsx +56 -37
- package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
- package/templates/runtime/schema/define-toolcraft.ts +140 -246
- package/templates/runtime/schema/runtime-targets.ts +21 -0
- package/templates/runtime/schema/types.ts +44 -0
- package/templates/runtime/state/create-template-state.test.ts +156 -0
- package/templates/runtime/state/create-template-state.ts +38 -8
- package/templates/runtime/state/media-defaults.ts +105 -0
- package/templates/runtime/state/persistence.test.ts +58 -0
- package/templates/runtime/state/persistence.ts +105 -1
- package/templates/runtime/state/reducer.test.ts +280 -4
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/timeline-loop.test.ts +71 -0
- package/templates/runtime/state/timeline-loop.ts +35 -0
- package/templates/runtime/state/types.ts +27 -0
- package/templates/runtime/testing/performance.test.ts +810 -21
- package/templates/runtime/testing/performance.ts +823 -53
- package/templates/starter/AGENTS.md +24 -18
- package/templates/starter/docs/toolcraft/README.md +8 -4
- package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
- package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
- package/templates/starter/docs/toolcraft/component-rules.md +75 -42
- package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
- package/templates/starter/docs/toolcraft/performance.md +37 -11
- package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
- package/templates/starter/docs/toolcraft/workflow.md +5 -2
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
- package/templates/starter/e2e/app-performance.spec.ts +55 -2
- package/templates/starter/e2e/performance-helpers.ts +45 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/index.html +1 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/playwright.config.ts +1 -1
- package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
- package/templates/starter/scripts/toolcraft-port.mjs +280 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
- package/templates/starter/src/app/starter-acceptance.ts +1453 -97
- package/templates/starter/src/app/starter-performance.test.ts +111 -7
- package/templates/starter/src/app/starter-performance.ts +5 -0
- package/templates/starter/src/app/starter-schema.test.ts +32 -7
- package/templates/starter/src/app/starter-schema.ts +6 -2
- package/templates/starter/vite.config.ts +58 -2
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/palette-control.tsx +34 -4
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
- package/templates/ui/components/controls/file-drop/index.ts +6 -1
- package/templates/ui/components/controls/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
- package/templates/ui/components/controls/select/select-control.tsx +8 -25
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
- package/templates/ui/components/panel/panel-actions.tsx +1 -1
- 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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
289
|
-
const normalizedY =
|
|
290
|
-
const
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,
|