@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
@@ -1,4 +1,5 @@
1
1
  import {
2
+ act,
2
3
  cleanup,
3
4
  fireEvent,
4
5
  render,
@@ -6,7 +7,7 @@ import {
6
7
  waitFor,
7
8
  } from "@testing-library/react";
8
9
  import * as React from "react";
9
- import { afterEach, beforeAll, describe, expect, it } from "vitest";
10
+ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
10
11
 
11
12
  import { defineToolcraft } from "../schema/define-toolcraft";
12
13
  import type { ToolcraftCustomControlRendererProps } from "./control-renderers";
@@ -27,9 +28,17 @@ beforeAll(() => {
27
28
  }),
28
29
  writable: true,
29
30
  });
31
+ window.requestAnimationFrame ??= ((callback: FrameRequestCallback) =>
32
+ window.setTimeout(
33
+ () => callback(performance.now()),
34
+ 16,
35
+ )) as typeof window.requestAnimationFrame;
36
+ window.cancelAnimationFrame ??= ((handle: number) =>
37
+ window.clearTimeout(handle)) as typeof window.cancelAnimationFrame;
30
38
  });
31
39
 
32
40
  afterEach(() => {
41
+ vi.useRealTimers();
33
42
  cleanup();
34
43
  document.documentElement.classList.remove("dark");
35
44
  document.documentElement.style.colorScheme = "";
@@ -113,6 +122,16 @@ function CustomValueProbe(): React.JSX.Element {
113
122
  );
114
123
  }
115
124
 
125
+ function TimelineTimeProbe(): React.JSX.Element {
126
+ const { state } = useToolcraft();
127
+
128
+ return (
129
+ <span data-testid="timeline-time">
130
+ {state.timeline.currentTimeSeconds.toFixed(3)}
131
+ </span>
132
+ );
133
+ }
134
+
116
135
  describe("ToolcraftApp", () => {
117
136
  it("renders enabled runtime surfaces from the app schema", () => {
118
137
  const { container } = render(<ToolcraftApp schema={createSchema()} />);
@@ -122,7 +141,11 @@ describe("ToolcraftApp", () => {
122
141
  ).toBeTruthy();
123
142
  expect(screen.getByText("Controls")).toBeTruthy();
124
143
  expect(screen.getByText("Layers")).toBeTruthy();
125
- expect(screen.getByText("Dur:")).toBeTruthy();
144
+ expect(screen.getByRole("button", { name: "Pause playback" })).toBeTruthy();
145
+ expect(
146
+ container.querySelector('[data-toolcraft-timeline-panel-variant="compact"]'),
147
+ ).toBeTruthy();
148
+ expect(screen.queryByText("Dur:")).toBeNull();
126
149
  expect(screen.queryByText("Duration:")).toBeNull();
127
150
  expect(screen.getByRole("button", { name: "Undo" })).toBeTruthy();
128
151
  expect(screen.getByRole("button", { name: "Redo" })).toBeTruthy();
@@ -145,6 +168,92 @@ describe("ToolcraftApp", () => {
145
168
  expect(container.querySelector('[data-panel-type="toolbar"]')).toBeTruthy();
146
169
  });
147
170
 
171
+ it("switches the timeline panel between compact and extended through the runtime setup toggle", () => {
172
+ const { container } = render(<ToolcraftApp schema={createSchema()} />);
173
+
174
+ expect(container.querySelector('[data-panel-type="timeline"]')).toBeTruthy();
175
+ expect(
176
+ container.querySelector('[data-toolcraft-timeline-panel-variant="compact"]'),
177
+ ).toBeTruthy();
178
+ expect(screen.getByRole("button", { name: "Pause playback" })).toBeTruthy();
179
+ expect(screen.queryByText("Dur:")).toBeNull();
180
+
181
+ const timelineField = screen.getByText("Timeline").closest('[role="group"]');
182
+ const timelineSwitch = timelineField?.querySelector<HTMLElement>('[role="switch"]');
183
+
184
+ expect(timelineSwitch?.getAttribute("aria-checked")).toBe("false");
185
+
186
+ fireEvent.click(timelineSwitch as HTMLElement);
187
+
188
+ expect(container.querySelector('[data-panel-type="timeline"]')).toBeTruthy();
189
+ expect(
190
+ container.querySelector('[data-toolcraft-timeline-panel-variant="extended"]'),
191
+ ).toBeTruthy();
192
+ expect(container.querySelector('[data-toolcraft-timeline-panel-hidden="true"]')).toBeNull();
193
+ expect(screen.getByText("Dur:")).toBeTruthy();
194
+ expect(screen.getByText(/\d+(?:\.\d+)? \/ 8s/)).toBeTruthy();
195
+ expect(timelineSwitch?.getAttribute("aria-checked")).toBe("true");
196
+ });
197
+
198
+ it("keeps timeline playback running while the panel is hidden", () => {
199
+ vi.useFakeTimers();
200
+ const schema = defineToolcraft({
201
+ canvas: {
202
+ enabled: true,
203
+ size: { height: 180, unit: "px", width: 320 },
204
+ },
205
+ panels: {
206
+ controls: {
207
+ sections: [],
208
+ title: "Controls",
209
+ },
210
+ timeline: { mode: "playback" },
211
+ },
212
+ persistence: {
213
+ include: ["panels", "timeline"],
214
+ key: "toolcraft:timeline-hidden-test:state:v1",
215
+ storage: "localStorage",
216
+ version: 1,
217
+ },
218
+ });
219
+
220
+ window.localStorage.setItem(
221
+ "toolcraft:timeline-hidden-test:state:v1",
222
+ JSON.stringify({
223
+ state: {
224
+ panels: {
225
+ timeline: { hidden: true },
226
+ },
227
+ timeline: {
228
+ currentTimeSeconds: 0,
229
+ durationSeconds: 8,
230
+ expanded: false,
231
+ isLooping: true,
232
+ isPlaying: true,
233
+ },
234
+ },
235
+ version: 1,
236
+ }),
237
+ );
238
+
239
+ const { container } = render(
240
+ <ToolcraftApp canvasContent={<TimelineTimeProbe />} schema={schema} />,
241
+ );
242
+
243
+ expect(
244
+ container.querySelector('[data-toolcraft-timeline-panel-hidden="true"]'),
245
+ ).toBeTruthy();
246
+ expect(screen.getByTestId("timeline-time").textContent).toBe("0.000");
247
+
248
+ act(() => {
249
+ vi.advanceTimersByTime(200);
250
+ });
251
+
252
+ expect(Number(screen.getByTestId("timeline-time").textContent)).toBeGreaterThan(0);
253
+
254
+ vi.useRealTimers();
255
+ });
256
+
148
257
  it("does not render disabled optional panels or toolbar features", () => {
149
258
  const schema = defineToolcraft({
150
259
  canvas: { enabled: true },
@@ -272,7 +381,7 @@ describe("ToolcraftApp", () => {
272
381
  footer: {
273
382
  actions: [
274
383
  {
275
- icon: "export",
384
+ icon: "upload-simple",
276
385
  label: "Export image",
277
386
  value: "export",
278
387
  variant: "default",
@@ -13,6 +13,7 @@ import { ToolcraftRoot } from "./toolcraft-root";
13
13
  import { LayersPanel } from "./layers-panel";
14
14
  import { TimelinePanel } from "./timeline-panel";
15
15
  import { ToolbarPanel } from "./toolbar-panel";
16
+ import { useToolcraft } from "./use-toolcraft";
16
17
 
17
18
  export type ToolcraftAppProps = {
18
19
  canvasContent?: React.ReactNode;
@@ -30,52 +31,70 @@ function cn(...classNames: Array<string | false | null | undefined>): string {
30
31
  return classNames.filter(Boolean).join(" ");
31
32
  }
32
33
 
33
- export function ToolcraftApp({
34
+ function ToolcraftAppContent({
34
35
  canvasContent,
35
36
  className,
36
37
  controlRenderers,
37
38
  onPanelAction,
38
39
  renderDefaultCanvasMedia = true,
39
- schema,
40
40
  style,
41
- }: ToolcraftAppProps): React.JSX.Element {
42
- const surfaces = schema.assembly.surfaces;
41
+ }: Omit<ToolcraftAppProps, "schema">): React.JSX.Element {
42
+ const { state } = useToolcraft();
43
+ const surfaces = state.schema.assembly.surfaces;
44
+ const timelinePanelHidden = state.panels.timeline.hidden === true;
45
+ const timelinePanelVariant =
46
+ state.panels.timeline.extended === true ? "extended" : "compact";
47
+
48
+ return (
49
+ <div
50
+ className={cn(
51
+ "relative min-h-[640px] w-full overflow-hidden bg-[color:var(--background)]",
52
+ className,
53
+ )}
54
+ data-slot="toolcraft-runtime-app"
55
+ style={{
56
+ ...style,
57
+ minWidth: toolcraftMinAppWidthPx,
58
+ }}
59
+ >
60
+ {surfaces.canvas.enabled ? (
61
+ <CanvasShell renderDefaultMedia={renderDefaultCanvasMedia}>
62
+ {canvasContent}
63
+ </CanvasShell>
64
+ ) : null}
65
+ {surfaces.panels.layers?.enabled ? (
66
+ <LayersPanel panelPlacement="floating" />
67
+ ) : null}
68
+ {surfaces.panels.controls?.enabled ? (
69
+ <ControlsPanel
70
+ controlRenderers={controlRenderers}
71
+ onPanelAction={onPanelAction}
72
+ panelPlacement="floating"
73
+ />
74
+ ) : null}
75
+ {surfaces.panels.timeline?.enabled ? (
76
+ <div
77
+ data-toolcraft-timeline-panel-hidden={timelinePanelHidden ? "true" : undefined}
78
+ data-toolcraft-timeline-panel-variant={timelinePanelVariant}
79
+ hidden={timelinePanelHidden}
80
+ >
81
+ <TimelinePanel panelPlacement="floating" variant={timelinePanelVariant} />
82
+ </div>
83
+ ) : null}
84
+ {surfaces.panels.toolbar.enabled ? (
85
+ <ToolbarPanel panelPlacement="floating" />
86
+ ) : null}
87
+ </div>
88
+ );
89
+ }
43
90
 
91
+ export function ToolcraftApp({
92
+ schema,
93
+ ...props
94
+ }: ToolcraftAppProps): React.JSX.Element {
44
95
  return (
45
96
  <ToolcraftRoot schema={schema}>
46
- <div
47
- className={cn(
48
- "relative min-h-[640px] w-full overflow-hidden bg-[color:var(--background)]",
49
- className,
50
- )}
51
- data-slot="toolcraft-runtime-app"
52
- style={{
53
- ...style,
54
- minWidth: toolcraftMinAppWidthPx,
55
- }}
56
- >
57
- {surfaces.canvas.enabled ? (
58
- <CanvasShell renderDefaultMedia={renderDefaultCanvasMedia}>
59
- {canvasContent}
60
- </CanvasShell>
61
- ) : null}
62
- {surfaces.panels.layers?.enabled ? (
63
- <LayersPanel panelPlacement="floating" />
64
- ) : null}
65
- {surfaces.panels.controls?.enabled ? (
66
- <ControlsPanel
67
- controlRenderers={controlRenderers}
68
- onPanelAction={onPanelAction}
69
- panelPlacement="floating"
70
- />
71
- ) : null}
72
- {surfaces.panels.timeline?.enabled ? (
73
- <TimelinePanel panelPlacement="floating" />
74
- ) : null}
75
- {surfaces.panels.toolbar.enabled ? (
76
- <ToolbarPanel panelPlacement="floating" />
77
- ) : null}
78
- </div>
97
+ <ToolcraftAppContent {...props} />
79
98
  </ToolcraftRoot>
80
99
  );
81
100
  }