@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
|
@@ -295,6 +295,15 @@ function browserTestsAssertNativePreviewResolution(): boolean {
|
|
|
295
295
|
);
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
function browserPerfContractRequiresRenderScaleBackingPixels(): boolean {
|
|
299
|
+
const browserTestSources = readBrowserTestSources();
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
/scenarioUsesRenderScaleFixture/.test(browserTestSources) &&
|
|
303
|
+
/expectToolcraftCanvasBackingPixelsForRenderScale/.test(browserTestSources)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
298
307
|
function sourceUsesAnimationFrameWithoutCleanup(): boolean {
|
|
299
308
|
const appSources = stripJsComments(readFiles(srcDir, /\.(ts|tsx)$/));
|
|
300
309
|
|
|
@@ -308,25 +317,36 @@ function sourceUsesDirectStorageApi(): boolean {
|
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
describe("Toolcraft template app performance coverage", () => {
|
|
311
|
-
it("publishes a sequential browser performance
|
|
320
|
+
it("publishes a sequential Playwright browser performance fallback", () => {
|
|
312
321
|
const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8")) as {
|
|
313
322
|
scripts?: Record<string, string>;
|
|
314
323
|
};
|
|
315
324
|
|
|
316
325
|
expect(
|
|
317
326
|
packageJson.scripts?.["test:browser:perf"],
|
|
318
|
-
"Generated apps must expose a sequential
|
|
327
|
+
"Generated apps must expose a sequential Playwright fallback so perf budgets can be measured when no agent browser is available.",
|
|
319
328
|
).toBe(
|
|
320
329
|
'playwright install chromium && playwright test e2e/app-performance.spec.ts --workers=1 && playwright test --grep "browser perf:" --workers=1 --pass-with-no-tests',
|
|
321
330
|
);
|
|
322
331
|
expect(packageJson.scripts?.["verify:quick"]).toBe("pnpm ai:check && pnpm test");
|
|
323
332
|
expect(packageJson.scripts?.["verify:ui"]).toBe("pnpm test:browser");
|
|
324
333
|
expect(packageJson.scripts?.["verify:perf"]).toBe("pnpm test:browser:perf");
|
|
334
|
+
expect(packageJson.scripts?.["verify:perf:playwright"]).toBe(
|
|
335
|
+
"pnpm test:browser:perf",
|
|
336
|
+
);
|
|
325
337
|
expect(packageJson.scripts?.["verify:final"]).toBe(
|
|
326
338
|
"pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
|
|
327
339
|
);
|
|
328
340
|
});
|
|
329
341
|
|
|
342
|
+
it("declares agent browser as the preferred performance runner", () => {
|
|
343
|
+
expect(starterPerformance.browserCheckPolicy).toEqual({
|
|
344
|
+
fallbackRunner: "playwright",
|
|
345
|
+
fallbackWhen: ["agent-browser-unavailable", "ci"],
|
|
346
|
+
preferredRunner: "agent-browser",
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
330
350
|
it("requires valid performance coverage for declared workload scenarios", () => {
|
|
331
351
|
expect(validateToolcraftPerformanceCoverage(starterSchema, starterPerformance)).toEqual([]);
|
|
332
352
|
});
|
|
@@ -510,7 +530,7 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
510
530
|
}),
|
|
511
531
|
).toEqual(
|
|
512
532
|
expect.arrayContaining([
|
|
513
|
-
'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires
|
|
533
|
+
'rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "canvas-2d". Keeping a CPU renderer requires rendererTechnique.measuredAlternativeEvidence for WebGL/WebGPU stress comparison.',
|
|
514
534
|
]),
|
|
515
535
|
);
|
|
516
536
|
});
|
|
@@ -710,6 +730,13 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
710
730
|
).toBe(true);
|
|
711
731
|
});
|
|
712
732
|
|
|
733
|
+
it("requires render scale scenarios to prove backing canvas pixels in browser performance tests", () => {
|
|
734
|
+
expect(
|
|
735
|
+
browserPerfContractRequiresRenderScaleBackingPixels(),
|
|
736
|
+
"Browser performance contract must require renderScale scenarios to assert backing canvas pixels, not only state or labels.",
|
|
737
|
+
).toBe(true);
|
|
738
|
+
});
|
|
739
|
+
|
|
713
740
|
it("requires procedural pixel-loop renderers to use a GPU strategy", () => {
|
|
714
741
|
if (!sourceUsesCpuPixelLoop()) {
|
|
715
742
|
return;
|
|
@@ -822,7 +849,7 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
822
849
|
}
|
|
823
850
|
});
|
|
824
851
|
|
|
825
|
-
it("requires each browser performance scenario to point at a Playwright test", () => {
|
|
852
|
+
it("requires each browser performance scenario to point at a fallback Playwright test", () => {
|
|
826
853
|
const browserTestSources = readBrowserTestSources();
|
|
827
854
|
|
|
828
855
|
for (const scenario of starterPerformance.scenarios) {
|
|
@@ -832,7 +859,7 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
832
859
|
|
|
833
860
|
expect(
|
|
834
861
|
browserTestSources,
|
|
835
|
-
`${scenario.id} must be backed by a Playwright test named "${scenario.browserTestName}".`,
|
|
862
|
+
`${scenario.id} must be backed by a fallback Playwright test named "${scenario.browserTestName}".`,
|
|
836
863
|
).toContain(scenario.browserTestName);
|
|
837
864
|
}
|
|
838
865
|
});
|
|
@@ -1262,8 +1289,6 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
1262
1289
|
}),
|
|
1263
1290
|
).toEqual(
|
|
1264
1291
|
expect.arrayContaining([
|
|
1265
|
-
"canvas.size.width must have a performance scenario because every visible control can affect app responsiveness.",
|
|
1266
|
-
"canvas.size.height must have a performance scenario because every visible control can affect app responsiveness.",
|
|
1267
1292
|
"shader.gradient must have a performance scenario because every visible control can affect app responsiveness.",
|
|
1268
1293
|
"shader.mode must have a performance scenario because every visible control can affect app responsiveness.",
|
|
1269
1294
|
]),
|
|
@@ -1418,6 +1443,85 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
1418
1443
|
);
|
|
1419
1444
|
});
|
|
1420
1445
|
|
|
1446
|
+
it("does not let load-profile smooth targets bypass required interaction budgets", () => {
|
|
1447
|
+
const schema = defineToolcraft({
|
|
1448
|
+
canvas: { enabled: true },
|
|
1449
|
+
panels: {
|
|
1450
|
+
controls: {
|
|
1451
|
+
sections: [
|
|
1452
|
+
{
|
|
1453
|
+
controls: {
|
|
1454
|
+
density: {
|
|
1455
|
+
defaultValue: 4,
|
|
1456
|
+
label: "Density",
|
|
1457
|
+
max: 12,
|
|
1458
|
+
min: 1,
|
|
1459
|
+
performanceReason: "Density changes rendered output size.",
|
|
1460
|
+
performanceRole: "workload",
|
|
1461
|
+
target: "render.density",
|
|
1462
|
+
type: "slider",
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
title: "Render",
|
|
1466
|
+
},
|
|
1467
|
+
],
|
|
1468
|
+
title: "Controls",
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
expect(
|
|
1474
|
+
validateToolcraftPerformanceCoverage(schema, {
|
|
1475
|
+
rendererStrategy: "none",
|
|
1476
|
+
rendererWorkload: "none",
|
|
1477
|
+
scenarios: [
|
|
1478
|
+
{
|
|
1479
|
+
automated: true,
|
|
1480
|
+
automatedTestName: "perf: density drag stays responsive",
|
|
1481
|
+
browser: true,
|
|
1482
|
+
browserTestName: "browser perf: density drag stays responsive",
|
|
1483
|
+
budget: { maxFrameGapMs: 80 },
|
|
1484
|
+
controlLabel: "Density",
|
|
1485
|
+
expectedObservable: "Dragging Density updates product output without blocking the UI.",
|
|
1486
|
+
fixture: "density smooth target fixture",
|
|
1487
|
+
id: "density-drag",
|
|
1488
|
+
interaction: "control-drag",
|
|
1489
|
+
stressFixture: {
|
|
1490
|
+
kind: "max-value",
|
|
1491
|
+
loadProfile: {
|
|
1492
|
+
degradationStepPercent: 10,
|
|
1493
|
+
evidence: [
|
|
1494
|
+
{
|
|
1495
|
+
attemptedTarget: 12,
|
|
1496
|
+
decision: "Keep 12 as experimental and guarantee smoothness through 11.",
|
|
1497
|
+
measuredResult: "At density 12, maxFrameGapMs 148 exceeded the 80ms budget.",
|
|
1498
|
+
optimizationAttempted:
|
|
1499
|
+
"Cached expensive render inputs and coalesced preview work to requestAnimationFrame.",
|
|
1500
|
+
result: "failed",
|
|
1501
|
+
scenarioId: "density-drag",
|
|
1502
|
+
},
|
|
1503
|
+
],
|
|
1504
|
+
hardLimit: 12,
|
|
1505
|
+
metric: "numeric-max",
|
|
1506
|
+
smoothTarget: 11,
|
|
1507
|
+
smoothTargetRatio: 0.9,
|
|
1508
|
+
target: "render.density",
|
|
1509
|
+
userFacingRange: "experimental-above-smooth",
|
|
1510
|
+
},
|
|
1511
|
+
reason: "Density 11 is the measured smooth target after hard-limit testing.",
|
|
1512
|
+
value: 11,
|
|
1513
|
+
},
|
|
1514
|
+
target: "render.density",
|
|
1515
|
+
values: { default: 4, max: 12, min: 1 },
|
|
1516
|
+
workload: true,
|
|
1517
|
+
},
|
|
1518
|
+
],
|
|
1519
|
+
usesCustomRenderer: false,
|
|
1520
|
+
workloadTargets: ["render.density"],
|
|
1521
|
+
}),
|
|
1522
|
+
).toContain("density-drag control-drag scenario must declare maxInteractionMs.");
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1421
1525
|
it("requires real browser interaction metadata for control performance scenarios", () => {
|
|
1422
1526
|
expect(
|
|
1423
1527
|
validateToolcraftPerformanceCoverage(starterSchema, {
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
} from "@repo/toolcraft-runtime";
|
|
5
5
|
|
|
6
6
|
export const starterPerformance: ToolcraftPerformanceConfig = defineToolcraftPerformance({
|
|
7
|
+
browserCheckPolicy: {
|
|
8
|
+
fallbackRunner: "playwright",
|
|
9
|
+
fallbackWhen: ["agent-browser-unavailable", "ci"],
|
|
10
|
+
preferredRunner: "agent-browser",
|
|
11
|
+
},
|
|
7
12
|
rendererStrategy: "none",
|
|
8
13
|
rendererWorkload: "none",
|
|
9
14
|
scenarios: [],
|
|
@@ -7,9 +7,25 @@ describe("starterSchema", () => {
|
|
|
7
7
|
it("publishes the base Toolcraft template app contract for AI assembly", () => {
|
|
8
8
|
expect(starterSchema.canvas.draggable).toBe(true);
|
|
9
9
|
expect(starterSchema.canvas.enabled).toBe(true);
|
|
10
|
-
expect(starterSchema.canvas.sizing).toEqual({ mode: "
|
|
10
|
+
expect(starterSchema.canvas.sizing).toEqual({ mode: "editable-output" });
|
|
11
11
|
expect(starterSchema.canvas.upload).toBe(true);
|
|
12
|
-
expect(starterSchema.panels.controls).
|
|
12
|
+
expect(starterSchema.panels.controls?.sections[0]?.title).toBe("Setup");
|
|
13
|
+
expect(starterSchema.panels.controls?.sections[0]?.controls.settingsTransfer).toMatchObject({
|
|
14
|
+
target: "runtime.settingsTransfer",
|
|
15
|
+
type: "settingsTransfer",
|
|
16
|
+
});
|
|
17
|
+
expect(starterSchema.panels.controls?.sections[0]?.controls.canvasAspectRatio).toMatchObject({
|
|
18
|
+
target: "canvas.aspectRatio",
|
|
19
|
+
type: "aspectRatio",
|
|
20
|
+
});
|
|
21
|
+
expect(starterSchema.panels.controls?.sections[0]?.controls.canvasWidth).toMatchObject({
|
|
22
|
+
target: "canvas.size.width",
|
|
23
|
+
type: "text",
|
|
24
|
+
});
|
|
25
|
+
expect(starterSchema.panels.controls?.sections[0]?.controls.canvasHeight).toMatchObject({
|
|
26
|
+
target: "canvas.size.height",
|
|
27
|
+
type: "text",
|
|
28
|
+
});
|
|
13
29
|
expect(starterSchema.panels.layers).toBeUndefined();
|
|
14
30
|
expect(starterSchema.panels.timeline).toBeUndefined();
|
|
15
31
|
expect(starterSchema.toolbar).toEqual({
|
|
@@ -20,38 +36,47 @@ describe("starterSchema", () => {
|
|
|
20
36
|
});
|
|
21
37
|
expect(starterSchema.assembly.components).toEqual([
|
|
22
38
|
"canvas",
|
|
39
|
+
"controlsPanel",
|
|
23
40
|
"toolbar",
|
|
24
41
|
]);
|
|
25
42
|
expect(starterSchema.assembly.capabilities).toEqual(
|
|
26
43
|
expect.arrayContaining([
|
|
27
44
|
"canvas.draggable",
|
|
45
|
+
"canvas.editableSize",
|
|
28
46
|
"canvas.upload",
|
|
47
|
+
"controls.defaults",
|
|
48
|
+
"controls.panel",
|
|
29
49
|
"toolbar.history",
|
|
30
50
|
"toolbar.radar",
|
|
31
51
|
"toolbar.theme",
|
|
32
52
|
"toolbar.zoom",
|
|
33
53
|
]),
|
|
34
54
|
);
|
|
35
|
-
expect(starterSchema.assembly.capabilities).not.toContain("controls.defaults");
|
|
36
55
|
expect(starterSchema.assembly.capabilities).not.toContain("timeline.playback");
|
|
37
56
|
expect(starterSchema.assembly.capabilities).not.toContain("timeline.keyframes");
|
|
38
57
|
expect(starterSchema.assembly.commands).toEqual(
|
|
39
58
|
expect.arrayContaining([
|
|
40
59
|
"canvas.center",
|
|
60
|
+
"canvas.setSize",
|
|
41
61
|
"canvas.setViewport",
|
|
42
62
|
"canvas.zoomIn",
|
|
63
|
+
"controls.reset",
|
|
64
|
+
"controls.setValue",
|
|
43
65
|
"history.undo",
|
|
44
66
|
"media.delete",
|
|
45
67
|
"media.import",
|
|
46
68
|
]),
|
|
47
69
|
);
|
|
48
|
-
expect(starterSchema.assembly.commands).not.toContain("controls.reset");
|
|
49
|
-
expect(starterSchema.assembly.commands).not.toContain("controls.setValue");
|
|
50
70
|
expect(starterSchema.assembly.commands).not.toContain("timeline.setCurrentTime");
|
|
51
71
|
});
|
|
52
72
|
|
|
53
|
-
it("starts without product-specific panels or controls", () => {
|
|
54
|
-
|
|
73
|
+
it("starts with runtime setup but without product-specific panels or controls", () => {
|
|
74
|
+
const productSections =
|
|
75
|
+
starterSchema.panels.controls?.sections.filter((section) => section.title !== "Setup") ??
|
|
76
|
+
[];
|
|
77
|
+
|
|
78
|
+
expect(starterSchema.panels.controls?.sections[0]?.title).toBe("Setup");
|
|
79
|
+
expect(productSections).toEqual([]);
|
|
55
80
|
expect(starterSchema.panels.layers).toBeUndefined();
|
|
56
81
|
expect(starterSchema.panels.timeline).toBeUndefined();
|
|
57
82
|
});
|
|
@@ -3,10 +3,14 @@ import { defineToolcraft } from "@repo/toolcraft-runtime";
|
|
|
3
3
|
export const starterSchema = defineToolcraft({
|
|
4
4
|
canvas: {
|
|
5
5
|
enabled: true,
|
|
6
|
-
sizing: { mode: "intrinsic-media" },
|
|
7
6
|
upload: true,
|
|
8
7
|
},
|
|
9
|
-
panels: {
|
|
8
|
+
panels: {
|
|
9
|
+
controls: {
|
|
10
|
+
sections: [],
|
|
11
|
+
title: "Controls",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
10
14
|
toolbar: {
|
|
11
15
|
history: true,
|
|
12
16
|
radar: true,
|
|
@@ -1,10 +1,66 @@
|
|
|
1
1
|
import tailwindcss from "@tailwindcss/vite";
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
+
import path from "node:path";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { defineConfig } from "vite";
|
|
7
|
+
import { defineConfig, type Plugin } from "vite";
|
|
8
|
+
|
|
9
|
+
const rootDir = fileURLToPath(new URL(".", import.meta.url));
|
|
10
|
+
const toolcraftServerIdentityPath = "/.toolcraft/server-identity.json";
|
|
11
|
+
|
|
12
|
+
function readHtmlAppTitle(source: string): string | null {
|
|
13
|
+
const appTitleMeta = source
|
|
14
|
+
.match(/<meta\b[^>]*>/gi)
|
|
15
|
+
?.find((tag) => tag.match(/\bname\s*=\s*["']toolcraft-app-title["']/i));
|
|
16
|
+
|
|
17
|
+
return appTitleMeta?.match(/\bcontent\s*=\s*["']([^"']*)["']/i)?.[1] ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function createToolcraftServerIdentity() {
|
|
21
|
+
const indexSource = await fs.readFile(path.join(rootDir, "index.html"), "utf8");
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
appTitle: readHtmlAppTitle(indexSource),
|
|
25
|
+
root: await fs.realpath(rootDir),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toolcraftServerIdentityPlugin(): Plugin {
|
|
30
|
+
function handleIdentityRequest(
|
|
31
|
+
request: IncomingMessage,
|
|
32
|
+
response: ServerResponse,
|
|
33
|
+
next: (error?: unknown) => void,
|
|
34
|
+
) {
|
|
35
|
+
const requestUrl = new URL(request.url ?? "/", "http://localhost");
|
|
36
|
+
|
|
37
|
+
if (requestUrl.pathname !== toolcraftServerIdentityPath) {
|
|
38
|
+
next();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
createToolcraftServerIdentity()
|
|
43
|
+
.then((identity) => {
|
|
44
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
45
|
+
response.setHeader("cache-control", "no-store");
|
|
46
|
+
response.end(JSON.stringify(identity));
|
|
47
|
+
})
|
|
48
|
+
.catch(next);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: "toolcraft-server-identity",
|
|
53
|
+
configurePreviewServer(server) {
|
|
54
|
+
server.middlewares.use(handleIdentityRequest);
|
|
55
|
+
},
|
|
56
|
+
configureServer(server) {
|
|
57
|
+
server.middlewares.use(handleIdentityRequest);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
5
61
|
|
|
6
62
|
export default defineConfig({
|
|
7
|
-
plugins: [tailwindcss(), react()],
|
|
63
|
+
plugins: [toolcraftServerIdentityPlugin(), tailwindcss(), react()],
|
|
8
64
|
resolve: {
|
|
9
65
|
alias: {
|
|
10
66
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
@@ -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"),
|
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ReactElement, ReactNode } from "react";
|
|
4
4
|
import {
|
|
5
|
+
ArrowClockwiseIcon,
|
|
5
6
|
ArrowCounterClockwiseIcon,
|
|
6
7
|
CheckIcon,
|
|
7
8
|
CopySimpleIcon,
|
|
8
9
|
DownloadSimpleIcon,
|
|
9
10
|
EraserIcon,
|
|
10
11
|
ExportIcon,
|
|
12
|
+
FlipHorizontalIcon,
|
|
13
|
+
FlipVerticalIcon,
|
|
11
14
|
MagicWandIcon,
|
|
12
15
|
ShuffleIcon,
|
|
16
|
+
UploadSimpleIcon,
|
|
13
17
|
} from "@phosphor-icons/react";
|
|
14
18
|
|
|
15
19
|
import { Button, Field } from "../../primitives";
|
|
16
20
|
import { ControlFieldLabel } from "../../control-layout";
|
|
21
|
+
import { cn } from "../../../lib/utils";
|
|
17
22
|
|
|
18
23
|
export type ActionControlIconName =
|
|
19
24
|
| "check"
|
|
@@ -22,8 +27,12 @@ export type ActionControlIconName =
|
|
|
22
27
|
| "download-simple"
|
|
23
28
|
| "eraser"
|
|
24
29
|
| "export"
|
|
30
|
+
| "flip-horizontal"
|
|
31
|
+
| "flip-vertical"
|
|
32
|
+
| "rotate-cw"
|
|
25
33
|
| "rotate-ccw"
|
|
26
34
|
| "shuffle"
|
|
35
|
+
| "upload-simple"
|
|
27
36
|
| "wand-sparkles";
|
|
28
37
|
|
|
29
38
|
const actionIconComponents = {
|
|
@@ -33,12 +42,17 @@ const actionIconComponents = {
|
|
|
33
42
|
"download-simple": DownloadSimpleIcon,
|
|
34
43
|
eraser: EraserIcon,
|
|
35
44
|
export: ExportIcon,
|
|
45
|
+
"flip-horizontal": FlipHorizontalIcon,
|
|
46
|
+
"flip-vertical": FlipVerticalIcon,
|
|
47
|
+
"rotate-cw": ArrowClockwiseIcon,
|
|
36
48
|
"rotate-ccw": ArrowCounterClockwiseIcon,
|
|
37
49
|
shuffle: ShuffleIcon,
|
|
50
|
+
"upload-simple": UploadSimpleIcon,
|
|
38
51
|
"wand-sparkles": MagicWandIcon,
|
|
39
52
|
} as const;
|
|
40
53
|
|
|
41
54
|
export type ActionControlObjectOption = {
|
|
55
|
+
ariaLabel?: string;
|
|
42
56
|
children?: ReactNode;
|
|
43
57
|
label?: string;
|
|
44
58
|
value: string;
|
|
@@ -51,6 +65,12 @@ function getActionValue(action: ActionControlOption): string {
|
|
|
51
65
|
return typeof action === "string" ? action : action.value;
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
function getActionLabel(action: ActionControlOption): string {
|
|
69
|
+
return typeof action === "string"
|
|
70
|
+
? action
|
|
71
|
+
: (action.ariaLabel ?? action.label ?? action.value);
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
function getActionContent(action: ActionControlOption): ReactNode {
|
|
55
75
|
if (typeof action === "string") {
|
|
56
76
|
return action;
|
|
@@ -77,6 +97,10 @@ function getActionAriaLabel(
|
|
|
77
97
|
action: ActionControlOption,
|
|
78
98
|
content: ReactNode,
|
|
79
99
|
): string | undefined {
|
|
100
|
+
if (typeof action !== "string" && action.ariaLabel) {
|
|
101
|
+
return action.ariaLabel;
|
|
102
|
+
}
|
|
103
|
+
|
|
80
104
|
if (typeof content === "string") {
|
|
81
105
|
return undefined;
|
|
82
106
|
}
|
|
@@ -86,23 +110,41 @@ function getActionAriaLabel(
|
|
|
86
110
|
|
|
87
111
|
export type ActionsControlProps = {
|
|
88
112
|
actions: readonly ActionControlOption[];
|
|
113
|
+
buttonColumns?: 1 | 2 | 3 | 4;
|
|
89
114
|
name: string;
|
|
90
115
|
onAction?: (value: string) => void;
|
|
116
|
+
showActionLabels?: boolean;
|
|
91
117
|
showLabel?: boolean;
|
|
92
118
|
};
|
|
93
119
|
|
|
94
120
|
export function ActionsControl({
|
|
95
121
|
actions,
|
|
122
|
+
buttonColumns,
|
|
96
123
|
name,
|
|
97
124
|
onAction,
|
|
125
|
+
showActionLabels = true,
|
|
98
126
|
showLabel = true,
|
|
99
127
|
}: ActionsControlProps): React.JSX.Element {
|
|
128
|
+
const hasSingleAction = actions.length === 1;
|
|
129
|
+
const columnClass =
|
|
130
|
+
buttonColumns === 4
|
|
131
|
+
? "w-full grid-cols-4"
|
|
132
|
+
: buttonColumns === 3
|
|
133
|
+
? "w-full grid-cols-3"
|
|
134
|
+
: buttonColumns === 2
|
|
135
|
+
? "w-full grid-cols-2"
|
|
136
|
+
: buttonColumns === 1
|
|
137
|
+
? "w-full grid-cols-1"
|
|
138
|
+
: hasSingleAction
|
|
139
|
+
? "w-1/2 grid-cols-1"
|
|
140
|
+
: "w-full grid-cols-2";
|
|
141
|
+
|
|
100
142
|
return (
|
|
101
143
|
<Field
|
|
102
144
|
aria-label={showLabel ? undefined : name}
|
|
103
|
-
className="h-fit min-w-0
|
|
145
|
+
className="h-fit min-w-0 gap-2"
|
|
104
146
|
data-slot="actions-control"
|
|
105
|
-
orientation="
|
|
147
|
+
orientation="vertical"
|
|
106
148
|
>
|
|
107
149
|
{showLabel ? (
|
|
108
150
|
<ControlFieldLabel className="min-w-0" textClassName="min-w-0 truncate">
|
|
@@ -110,24 +152,33 @@ export function ActionsControl({
|
|
|
110
152
|
</ControlFieldLabel>
|
|
111
153
|
) : null}
|
|
112
154
|
<div
|
|
113
|
-
className=
|
|
155
|
+
className={cn(
|
|
156
|
+
"grid max-w-full gap-1.5",
|
|
157
|
+
columnClass,
|
|
158
|
+
)}
|
|
159
|
+
data-actions-count={actions.length}
|
|
160
|
+
data-actions-columns={buttonColumns ?? (hasSingleAction ? 1 : 2)}
|
|
114
161
|
data-slot="actions-control-buttons"
|
|
115
162
|
>
|
|
116
163
|
{actions.map((action) => {
|
|
117
164
|
const actionContent = getActionContent(action);
|
|
165
|
+
const actionLabel = getActionLabel(action);
|
|
118
166
|
const actionValue = getActionValue(action);
|
|
119
167
|
|
|
120
168
|
return (
|
|
121
169
|
<Button
|
|
122
|
-
aria-label={
|
|
170
|
+
aria-label={
|
|
171
|
+
showActionLabels ? getActionAriaLabel(action, actionContent) : actionLabel
|
|
172
|
+
}
|
|
123
173
|
key={actionValue}
|
|
124
174
|
onClick={() => onAction?.(actionValue)}
|
|
175
|
+
className="w-full"
|
|
125
176
|
size="sm"
|
|
126
177
|
type="button"
|
|
127
178
|
variant="outline"
|
|
128
179
|
>
|
|
129
180
|
{getActionIcon(action)}
|
|
130
|
-
{actionContent}
|
|
181
|
+
{showActionLabels ? actionContent : null}
|
|
131
182
|
</Button>
|
|
132
183
|
);
|
|
133
184
|
})}
|
|
@@ -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,
|