@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
|
@@ -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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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"
|
|
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 =
|
|
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
|
|
105
|
-
|
|
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
|
-
"
|
|
115
|
-
|
|
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
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
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 {
|
|
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({
|
|
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(
|