@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.
- package/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +185 -23
- package/templates/runtime/contracts/component-contracts.ts +91 -36
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +4 -4
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +774 -17
- package/templates/runtime/react/controls-panel.tsx +155 -8
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
- package/templates/runtime/schema/define-toolcraft.ts +29 -3
- package/templates/runtime/schema/types.ts +7 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1424 -56
- package/templates/runtime/testing/performance.ts +710 -43
- package/templates/starter/AGENTS.md +10 -9
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
- package/templates/starter/docs/toolcraft/component-rules.md +46 -16
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +54 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +5 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
- package/templates/starter/src/app/starter-acceptance.ts +740 -24
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
- 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/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 +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +3 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- 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 +84 -8
- package/templates/ui/components/panel/panel-section.tsx +82 -6
|
@@ -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":
|
|
@@ -279,6 +309,7 @@ function VectorPadHandle({ isDragging }: { isDragging: boolean }): React.JSX.Ele
|
|
|
279
309
|
function VectorPadField({
|
|
280
310
|
name,
|
|
281
311
|
onValueChange,
|
|
312
|
+
padCoordinateMode,
|
|
282
313
|
padShape = "compact",
|
|
283
314
|
padVariant = "default",
|
|
284
315
|
x,
|
|
@@ -287,9 +318,13 @@ function VectorPadField({
|
|
|
287
318
|
const [isPointerDragging, setIsPointerDragging] = React.useState(false);
|
|
288
319
|
const normalizedX = normalizeVectorCoordinate(x);
|
|
289
320
|
const normalizedY = normalizeVectorCoordinate(y);
|
|
290
|
-
const
|
|
321
|
+
const coordinateMode =
|
|
322
|
+
padCoordinateMode ?? getDefaultPadCoordinateMode(padVariant);
|
|
323
|
+
const point = getVectorPoint(normalizedX, normalizedY, coordinateMode);
|
|
291
324
|
const valueLabel = getVectorValueLabel(normalizedX, normalizedY);
|
|
292
325
|
const vectorPadBackgroundImage = vectorPadBackgroundImages[padVariant];
|
|
326
|
+
const axisLockRef = React.useRef<VectorPadAxisLock | null>(null);
|
|
327
|
+
const dragStartRef = React.useRef<VectorPadDragStart | null>(null);
|
|
293
328
|
const liveHistoryGroupRef = React.useRef<string | null>(null);
|
|
294
329
|
const updateVector = (
|
|
295
330
|
nextX: string,
|
|
@@ -321,17 +356,51 @@ function VectorPadField({
|
|
|
321
356
|
}
|
|
322
357
|
|
|
323
358
|
function updateFromPointer(event: React.PointerEvent<HTMLButtonElement>): void {
|
|
324
|
-
const nextPoint = pointFromEvent(event);
|
|
359
|
+
const nextPoint = pointFromEvent(event, coordinateMode);
|
|
360
|
+
const nextValue: VectorControlValue = {
|
|
361
|
+
x: (nextPoint.x * 2 - 1).toFixed(2),
|
|
362
|
+
y: (nextPoint.y * 2 - 1).toFixed(2),
|
|
363
|
+
};
|
|
364
|
+
const dragStart = dragStartRef.current;
|
|
365
|
+
|
|
366
|
+
if (event.shiftKey && dragStart) {
|
|
367
|
+
if (!axisLockRef.current) {
|
|
368
|
+
const deltaX = event.clientX - dragStart.clientX;
|
|
369
|
+
const deltaY = event.clientY - dragStart.clientY;
|
|
370
|
+
|
|
371
|
+
if (Math.hypot(deltaX, deltaY) < vectorPadAxisLockThresholdPx) {
|
|
372
|
+
updateVector(
|
|
373
|
+
dragStart.value.x,
|
|
374
|
+
dragStart.value.y,
|
|
375
|
+
getLiveHistoryMeta(),
|
|
376
|
+
);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
axisLockRef.current = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
updateVector(
|
|
384
|
+
axisLockRef.current === "x" ? nextValue.x : dragStart.value.x,
|
|
385
|
+
axisLockRef.current === "y" ? nextValue.y : dragStart.value.y,
|
|
386
|
+
getLiveHistoryMeta(),
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
axisLockRef.current = null;
|
|
325
392
|
|
|
326
393
|
updateVector(
|
|
327
|
-
|
|
328
|
-
|
|
394
|
+
nextValue.x,
|
|
395
|
+
nextValue.y,
|
|
329
396
|
getLiveHistoryMeta(),
|
|
330
397
|
);
|
|
331
398
|
}
|
|
332
399
|
|
|
333
400
|
function stopPointerDrag(): void {
|
|
334
401
|
setIsPointerDragging(false);
|
|
402
|
+
axisLockRef.current = null;
|
|
403
|
+
dragStartRef.current = null;
|
|
335
404
|
liveHistoryGroupRef.current = null;
|
|
336
405
|
}
|
|
337
406
|
|
|
@@ -352,12 +421,19 @@ function VectorPadField({
|
|
|
352
421
|
"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",
|
|
353
422
|
padShape === "square" ? "aspect-square" : "h-[142px]",
|
|
354
423
|
)}
|
|
424
|
+
data-vector-pad-coordinate-mode={coordinateMode}
|
|
355
425
|
data-vector-pad-shape={padShape}
|
|
356
426
|
data-vector-pad-variant={padVariant}
|
|
357
427
|
onLostPointerCapture={stopPointerDrag}
|
|
358
428
|
onPointerCancel={stopPointerDrag}
|
|
359
429
|
onPointerDown={(event) => {
|
|
360
430
|
setIsPointerDragging(true);
|
|
431
|
+
axisLockRef.current = null;
|
|
432
|
+
dragStartRef.current = {
|
|
433
|
+
clientX: event.clientX,
|
|
434
|
+
clientY: event.clientY,
|
|
435
|
+
value: { x: normalizedX, y: normalizedY },
|
|
436
|
+
};
|
|
361
437
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
362
438
|
updateFromPointer(event);
|
|
363
439
|
}}
|
|
@@ -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,
|
|
@@ -109,6 +133,10 @@ function PanelSectionCollapsibleBody({
|
|
|
109
133
|
}): React.JSX.Element | null {
|
|
110
134
|
const [shouldRender, setShouldRender] = React.useState(!collapsed);
|
|
111
135
|
const [isVisuallyCollapsed, setIsVisuallyCollapsed] = React.useState(collapsed);
|
|
136
|
+
const suppressControlTransitions =
|
|
137
|
+
useInitialPanelSectionControlTransitionSuppression(
|
|
138
|
+
`${collapsed ? "collapsed" : "expanded"}:${shouldRender ? "mounted" : "unmounted"}`,
|
|
139
|
+
);
|
|
112
140
|
|
|
113
141
|
React.useEffect(() => {
|
|
114
142
|
if (collapsed) {
|
|
@@ -155,11 +183,59 @@ function PanelSectionCollapsibleBody({
|
|
|
155
183
|
data-slot="panel-section-collapsible-body"
|
|
156
184
|
onTransitionEnd={handleTransitionEnd}
|
|
157
185
|
>
|
|
158
|
-
<div
|
|
186
|
+
<div
|
|
187
|
+
className="min-h-0 overflow-hidden"
|
|
188
|
+
data-toolcraft-controls-mounting={
|
|
189
|
+
suppressControlTransitions ? "true" : undefined
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
{children}
|
|
193
|
+
</div>
|
|
159
194
|
</div>
|
|
160
195
|
);
|
|
161
196
|
}
|
|
162
197
|
|
|
198
|
+
function useInitialPanelSectionControlTransitionSuppression(
|
|
199
|
+
dependency: unknown,
|
|
200
|
+
): boolean {
|
|
201
|
+
const [suppressionState, setSuppressionState] = React.useState(() => ({
|
|
202
|
+
dependency,
|
|
203
|
+
isSuppressing: true,
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
React.useEffect(() => {
|
|
207
|
+
setSuppressionState({ dependency, isSuppressing: true });
|
|
208
|
+
|
|
209
|
+
if (typeof window === "undefined") {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
214
|
+
const timeout = window.setTimeout(() => {
|
|
215
|
+
setSuppressionState({ dependency, isSuppressing: false });
|
|
216
|
+
}, 0);
|
|
217
|
+
|
|
218
|
+
return () => window.clearTimeout(timeout);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let secondFrame = 0;
|
|
222
|
+
const firstFrame = window.requestAnimationFrame(() => {
|
|
223
|
+
secondFrame = window.requestAnimationFrame(() => {
|
|
224
|
+
setSuppressionState({ dependency, isSuppressing: false });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return () => {
|
|
229
|
+
window.cancelAnimationFrame(firstFrame);
|
|
230
|
+
window.cancelAnimationFrame(secondFrame);
|
|
231
|
+
};
|
|
232
|
+
}, [dependency]);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
suppressionState.dependency !== dependency || suppressionState.isSuppressing
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
163
239
|
function getPanelSectionChildKey(
|
|
164
240
|
child: React.ReactNode,
|
|
165
241
|
index: number,
|