@pixel-point/toolcraft 0.0.9 → 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 (75) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +14 -6
  4. package/src/generate.test.mjs +28 -0
  5. package/templates/runtime/contracts/component-contracts.test.ts +148 -34
  6. package/templates/runtime/contracts/component-contracts.ts +79 -38
  7. package/templates/runtime/contracts/decision-contracts.test.ts +33 -1
  8. package/templates/runtime/contracts/decision-contracts.ts +19 -6
  9. package/templates/runtime/export/export.test.ts +63 -0
  10. package/templates/runtime/export/export.ts +55 -0
  11. package/templates/runtime/index.ts +1 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  13. package/templates/runtime/react/canvas-shell.tsx +31 -9
  14. package/templates/runtime/react/controls-panel.test.tsx +474 -27
  15. package/templates/runtime/react/controls-panel.tsx +71 -26
  16. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  17. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  18. package/templates/runtime/react/settings-transfer.ts +6 -1
  19. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  20. package/templates/runtime/react/timeline-panel.tsx +44 -7
  21. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  22. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  23. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  24. package/templates/runtime/schema/define-toolcraft.test.ts +225 -173
  25. package/templates/runtime/schema/define-toolcraft.ts +117 -247
  26. package/templates/runtime/schema/runtime-targets.ts +21 -0
  27. package/templates/runtime/schema/types.ts +41 -0
  28. package/templates/runtime/state/create-template-state.test.ts +156 -0
  29. package/templates/runtime/state/create-template-state.ts +38 -8
  30. package/templates/runtime/state/media-defaults.ts +105 -0
  31. package/templates/runtime/state/persistence.test.ts +58 -0
  32. package/templates/runtime/state/persistence.ts +105 -1
  33. package/templates/runtime/state/reducer.test.ts +280 -4
  34. package/templates/runtime/state/reducer.ts +195 -9
  35. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  36. package/templates/runtime/state/timeline-loop.ts +35 -0
  37. package/templates/runtime/state/types.ts +27 -0
  38. package/templates/runtime/testing/performance.test.ts +657 -2
  39. package/templates/runtime/testing/performance.ts +789 -49
  40. package/templates/starter/AGENTS.md +22 -16
  41. package/templates/starter/docs/toolcraft/README.md +8 -4
  42. package/templates/starter/docs/toolcraft/acceptance-testing.md +38 -7
  43. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  44. package/templates/starter/docs/toolcraft/assembly-workflow.md +51 -20
  45. package/templates/starter/docs/toolcraft/component-rules.md +49 -37
  46. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  47. package/templates/starter/docs/toolcraft/performance.md +30 -10
  48. package/templates/starter/docs/toolcraft/schema-reference.md +134 -33
  49. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  50. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  51. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  52. package/templates/starter/e2e/performance-helpers.ts +45 -0
  53. package/templates/starter/index.html +1 -0
  54. package/templates/starter/package.json +1 -0
  55. package/templates/starter/playwright.config.ts +1 -1
  56. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  57. package/templates/starter/scripts/run-vite-on-free-port.mjs +82 -16
  58. package/templates/starter/scripts/toolcraft-port.mjs +178 -0
  59. package/templates/starter/scripts/toolcraft-port.test.mjs +147 -0
  60. package/templates/starter/src/app/starter-acceptance.test.ts +2573 -313
  61. package/templates/starter/src/app/starter-acceptance.ts +978 -81
  62. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  63. package/templates/starter/src/app/starter-performance.ts +5 -0
  64. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  65. package/templates/starter/src/app/starter-schema.ts +6 -2
  66. package/templates/starter/vite.config.ts +58 -2
  67. package/templates/ui/components/controls/actions/actions-control.tsx +46 -3
  68. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  69. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  70. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  71. package/templates/ui/components/controls/index.ts +2 -0
  72. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  73. package/templates/ui/components/controls/select/select-control.tsx +4 -26
  74. package/templates/ui/components/controls/vector/vector-control.tsx +25 -4
  75. package/templates/ui/components/panel/panel-actions.tsx +1 -1
@@ -3,6 +3,7 @@ import {
3
3
  getToolcraftCanvasAspectRatioPreset,
4
4
  getToolcraftCanvasAspectRatioPresetBySize,
5
5
  } from "./canvas-aspect-ratio-presets";
6
+ import { toolcraftTimelinePanelExtendedTarget } from "./runtime-targets";
6
7
  import type {
7
8
  ToolcraftAssemblyCapability,
8
9
  ToolcraftAssemblyCommand,
@@ -33,6 +34,7 @@ const defaultCanvasSize = {
33
34
 
34
35
  type ResolvedCanvas = ResolvedToolcraftAppSchema["canvas"];
35
36
  type ResolvedExport = ResolvedToolcraftAppSchema["export"];
37
+ type ResolvedMedia = ResolvedToolcraftAppSchema["media"];
36
38
  type ResolvedToolbar = Required<ToolcraftToolbarSchema>;
37
39
  type PanelContract = {
38
40
  capabilities?: readonly string[];
@@ -47,6 +49,9 @@ const canvasSizeControlTargets = {
47
49
  } as const;
48
50
  const canvasAspectRatioTarget = "canvas.aspectRatio";
49
51
  const canvasRenderScaleTarget = "canvas.renderScale";
52
+ const defaultTimelineDurationSeconds = 8;
53
+ const minTimelineDurationSeconds = 1;
54
+ const maxTimelineDurationSeconds = 60;
50
55
  const defaultCanvasRenderScale = {
51
56
  defaultValue: 2,
52
57
  enabled: false,
@@ -57,33 +62,6 @@ const defaultCanvasRenderScale = {
57
62
  const maxAutoInlineControlLabelLength = 18;
58
63
  const settingsTransferTarget = "runtime.settingsTransfer";
59
64
  const runtimeSetupSectionTitle = "Setup";
60
- const settingsTransferHeavyControlTypes = new Set([
61
- "channelMixer",
62
- "code",
63
- "collectionActions",
64
- "colorOpacity",
65
- "curves",
66
- "fileDrop",
67
- "fontPicker",
68
- "gradient",
69
- "imagePicker",
70
- "palette",
71
- "rangeSlider",
72
- "vector",
73
- ]);
74
-
75
- export type ToolcraftSettingsTransferEligibilityReason =
76
- | "control-count"
77
- | "score"
78
- | "section-count";
79
-
80
- export type ToolcraftSettingsTransferEligibility = {
81
- controlCount: number;
82
- eligible: boolean;
83
- reasons: ToolcraftSettingsTransferEligibilityReason[];
84
- score: number;
85
- sectionCount: number;
86
- };
87
65
 
88
66
  type ToolcraftControlActionSchema = NonNullable<
89
67
  ToolcraftControlSchema["actions"]
@@ -193,7 +171,7 @@ function resolveCanvasSizing(
193
171
  }
194
172
 
195
173
  if (canvas.upload) {
196
- return { mode: "intrinsic-media" };
174
+ return { mode: "editable-output" };
197
175
  }
198
176
 
199
177
  return { mode: "intrinsic-media" };
@@ -257,6 +235,12 @@ function resolveExport(
257
235
  };
258
236
  }
259
237
 
238
+ function resolveMedia(mediaSchema: ToolcraftAppSchema["media"]): ResolvedMedia {
239
+ return {
240
+ defaultAssets: mediaSchema?.defaultAssets ?? [],
241
+ };
242
+ }
243
+
260
244
  function getPanelDragMode(
261
245
  contract: { capabilities?: readonly string[] },
262
246
  ): ToolcraftAssemblyPanelContract["dragMode"] {
@@ -283,7 +267,12 @@ function createPanelAssemblyContract({
283
267
  ])
284
268
  : [];
285
269
  const panelCommands = enabled
286
- ? unique<ToolcraftAssemblyCommand>(["panels.setOffset", "panels.resetOffset", ...commands])
270
+ ? unique<ToolcraftAssemblyCommand>([
271
+ "panels.setOffset",
272
+ "panels.setHidden",
273
+ "panels.resetOffset",
274
+ ...commands,
275
+ ])
287
276
  : [];
288
277
 
289
278
  return {
@@ -332,7 +321,7 @@ function createToolcraftAssembly({
332
321
 
333
322
  if (canvas.upload) {
334
323
  capabilities.push("canvas.upload");
335
- commands.push("media.delete", "media.import", "media.reorder");
324
+ commands.push("media.delete", "media.import", "media.reorder", "media.transform");
336
325
  }
337
326
  }
338
327
 
@@ -479,7 +468,12 @@ function createToolcraftAssembly({
479
468
  ? (["canvas.panBy", "canvas.setOffset", "canvas.setViewport"] as const)
480
469
  : []),
481
470
  ...(canvas.upload
482
- ? (["media.delete", "media.import", "media.reorder"] as const)
471
+ ? ([
472
+ "media.delete",
473
+ "media.import",
474
+ "media.reorder",
475
+ "media.transform",
476
+ ] as const)
483
477
  : []),
484
478
  ])
485
479
  : [],
@@ -496,15 +490,6 @@ function createToolcraftAssembly({
496
490
  };
497
491
  }
498
492
 
499
- function hasControlTarget(
500
- panels: ToolcraftAppSchema["panels"],
501
- target: string,
502
- ): boolean {
503
- return (panels.controls?.sections ?? []).some((section) =>
504
- Object.values(section.controls).some((control) => control.target === target),
505
- );
506
- }
507
-
508
493
  function getControlDefaultSectionLayout(
509
494
  control: ToolcraftControlSchema,
510
495
  ): "grouped" | "standalone" {
@@ -637,116 +622,12 @@ function isPanelActionsControl(control: ToolcraftControlSchema): boolean {
637
622
  return control.type === "panelActions";
638
623
  }
639
624
 
640
- function isSettingsTransferControl(control: ToolcraftControlSchema): boolean {
641
- return control.type === "settingsTransfer";
642
- }
643
-
644
- function isRuntimeOnlyActionControl(control: ToolcraftControlSchema): boolean {
645
- return isPanelActionsControl(control) || isSettingsTransferControl(control);
646
- }
647
-
648
- function isSettingsTransferEligibilityControl(control: ToolcraftControlSchema): boolean {
649
- return (
650
- !isRuntimeOnlyActionControl(control) &&
651
- control.target !== canvasAspectRatioTarget &&
652
- control.target !== canvasSizeControlTargets.width &&
653
- control.target !== canvasSizeControlTargets.height
654
- );
655
- }
656
-
657
- function getSettingsTransferComplexityScore({
658
- panels,
659
- }: {
660
- panels: ToolcraftAppSchema["panels"];
661
- }): {
662
- controlCount: number;
663
- score: number;
664
- sectionCount: number;
665
- } {
666
- const sections = panels.controls?.sections ?? [];
667
- let controlCount = 0;
668
- let score = 0;
669
-
670
- for (const section of sections) {
671
- for (const control of Object.values(section.controls)) {
672
- if (!isSettingsTransferEligibilityControl(control)) {
673
- continue;
674
- }
675
-
676
- controlCount += 1;
677
- score += settingsTransferHeavyControlTypes.has(control.type) ? 3 : 1;
678
- }
679
- }
680
-
681
- const sectionCount = sections.filter((section) =>
682
- Object.values(section.controls).some((control) =>
683
- isSettingsTransferEligibilityControl(control),
684
- ),
685
- ).length;
686
-
687
- score += Math.max(0, sectionCount - 4);
688
-
689
- if (
690
- panels.timeline === true ||
691
- (typeof panels.timeline === "object" && panels.timeline.enabled !== false)
692
- ) {
693
- score += 2;
694
- }
695
-
696
- if (panels.layers) {
697
- score += 2;
698
- }
699
-
700
- return { controlCount, score, sectionCount };
701
- }
702
-
703
- export function getToolcraftSettingsTransferEligibility({
704
- panels,
705
- }: {
706
- panels: ToolcraftAppSchema["panels"];
707
- }): ToolcraftSettingsTransferEligibility {
708
- const { controlCount, score, sectionCount } = getSettingsTransferComplexityScore({
709
- panels,
710
- });
711
- const reasons: ToolcraftSettingsTransferEligibilityReason[] = [];
712
-
713
- if (controlCount >= 12) {
714
- reasons.push("control-count");
715
- }
716
-
717
- if (sectionCount >= 5) {
718
- reasons.push("section-count");
719
- }
720
-
721
- if (score >= 18) {
722
- reasons.push("score");
723
- }
724
-
725
- return {
726
- controlCount,
727
- eligible: reasons.length > 0,
728
- reasons,
729
- score,
730
- sectionCount,
731
- };
732
- }
733
-
734
- function shouldAutoEnableSettingsTransfer({
735
- panels,
736
- }: {
737
- panels: ToolcraftAppSchema["panels"];
738
- }): boolean {
739
- return getToolcraftSettingsTransferEligibility({ panels }).eligible;
740
- }
741
-
742
625
  function resolveSettingsTransfer({
743
626
  controls,
744
- panels,
745
627
  persistence,
746
628
  settingsTransfer,
747
629
  }: {
748
630
  controls: ToolcraftControlsPanelSchema | undefined;
749
- panels: ToolcraftAppSchema["panels"];
750
631
  persistence: ResolvedToolcraftAppSchema["persistence"];
751
632
  settingsTransfer: ToolcraftSettingsTransferSchema | undefined;
752
633
  }): ResolvedToolcraftSettingsTransferSchema {
@@ -759,9 +640,7 @@ function resolveSettingsTransfer({
759
640
 
760
641
  return {
761
642
  appId,
762
- enabled:
763
- Boolean(controls) &&
764
- (mode === "auto" ? shouldAutoEnableSettingsTransfer({ panels }) : mode),
643
+ enabled: Boolean(controls),
765
644
  fileName: getSettingsTransferFileName({ appId, settingsTransfer }),
766
645
  mode,
767
646
  };
@@ -769,11 +648,7 @@ function resolveSettingsTransfer({
769
648
 
770
649
  function createSettingsTransferSection(
771
650
  settingsTransfer: ResolvedToolcraftSettingsTransferSchema,
772
- ): ToolcraftControlSectionSchema | null {
773
- if (!settingsTransfer.enabled) {
774
- return null;
775
- }
776
-
651
+ ): ToolcraftControlSectionSchema {
777
652
  return {
778
653
  controls: {
779
654
  settingsTransfer: {
@@ -830,51 +705,20 @@ function getCanvasAspectRatioDefaultValue(size: ToolcraftCanvasSize): {
830
705
  };
831
706
  }
832
707
 
833
- function createCanvasSizeSection({
834
- aspectRatioControl,
835
- renderScaleControl,
836
- sizeControlIds,
837
- sizeControls,
838
- }: {
839
- aspectRatioControl: ToolcraftControlSchema;
840
- renderScaleControl?: ToolcraftControlSchema;
841
- sizeControlIds: readonly string[];
842
- sizeControls: ToolcraftControlSectionSchema["controls"];
843
- }): ToolcraftControlSectionSchema {
844
- return {
845
- controls: {
846
- canvasAspectRatio: aspectRatioControl,
847
- ...sizeControls,
848
- ...(renderScaleControl ? { canvasRenderScale: renderScaleControl } : {}),
849
- },
850
- layoutGroups: getCanvasSizeLayoutGroups(sizeControlIds),
851
- title: runtimeSetupSectionTitle,
852
- };
853
- }
854
-
855
- function createCanvasRenderScaleSection(
856
- renderScaleControl: ToolcraftControlSchema,
857
- ): ToolcraftControlSectionSchema {
858
- return {
859
- controls: {
860
- canvasRenderScale: renderScaleControl,
861
- },
862
- title: runtimeSetupSectionTitle,
863
- };
864
- }
865
-
866
708
  function mergeCanvasSizeControlsIntoSettingsTransferSection({
867
709
  aspectRatioControl,
868
710
  renderScaleControl,
869
711
  settingsTransferSection,
870
712
  sizeControlIds,
871
713
  sizeControls,
714
+ timelineExtendedControl,
872
715
  }: {
873
716
  aspectRatioControl: ToolcraftControlSchema;
874
717
  renderScaleControl?: ToolcraftControlSchema;
875
718
  settingsTransferSection: ToolcraftControlSectionSchema;
876
719
  sizeControlIds: readonly string[];
877
720
  sizeControls: ToolcraftControlSectionSchema["controls"];
721
+ timelineExtendedControl?: ToolcraftControlSchema;
878
722
  }): ToolcraftControlSectionSchema {
879
723
  const canvasSizeLayoutGroups = getCanvasSizeLayoutGroups(sizeControlIds) ?? [];
880
724
 
@@ -885,6 +729,7 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
885
729
  canvasAspectRatio: aspectRatioControl,
886
730
  ...sizeControls,
887
731
  ...(renderScaleControl ? { canvasRenderScale: renderScaleControl } : {}),
732
+ ...(timelineExtendedControl ? { timelineExtended: timelineExtendedControl } : {}),
888
733
  },
889
734
  layoutGroups:
890
735
  canvasSizeLayoutGroups.length > 0 || settingsTransferSection.layoutGroups?.length
@@ -899,15 +744,34 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
899
744
  function mergeCanvasRenderScaleIntoSettingsTransferSection({
900
745
  renderScaleControl,
901
746
  settingsTransferSection,
747
+ timelineExtendedControl,
902
748
  }: {
903
749
  renderScaleControl: ToolcraftControlSchema;
904
750
  settingsTransferSection: ToolcraftControlSectionSchema;
751
+ timelineExtendedControl?: ToolcraftControlSchema;
905
752
  }): ToolcraftControlSectionSchema {
906
753
  return {
907
754
  ...settingsTransferSection,
908
755
  controls: {
909
756
  ...settingsTransferSection.controls,
910
757
  canvasRenderScale: renderScaleControl,
758
+ ...(timelineExtendedControl ? { timelineExtended: timelineExtendedControl } : {}),
759
+ },
760
+ };
761
+ }
762
+
763
+ function mergeTimelineExtendedIntoSettingsTransferSection({
764
+ settingsTransferSection,
765
+ timelineExtendedControl,
766
+ }: {
767
+ settingsTransferSection: ToolcraftControlSectionSchema;
768
+ timelineExtendedControl: ToolcraftControlSchema;
769
+ }): ToolcraftControlSectionSchema {
770
+ return {
771
+ ...settingsTransferSection,
772
+ controls: {
773
+ ...settingsTransferSection.controls,
774
+ timelineExtended: timelineExtendedControl,
911
775
  },
912
776
  };
913
777
  }
@@ -1432,7 +1296,7 @@ function normalizePanels({
1432
1296
  const controls = { ...panels.controls };
1433
1297
  const settingsTransferSection = createSettingsTransferSection(settingsTransfer);
1434
1298
  const renderScaleControl: ToolcraftControlSchema | undefined =
1435
- canvas.renderScale.enabled && !hasControlTarget(panels, canvasRenderScaleTarget)
1299
+ canvas.renderScale.enabled
1436
1300
  ? {
1437
1301
  defaultValue: canvas.renderScale.defaultValue,
1438
1302
  description:
@@ -1453,23 +1317,38 @@ function normalizePanels({
1453
1317
  variant: "discrete",
1454
1318
  }
1455
1319
  : undefined;
1320
+ const timelineExtendedControl: ToolcraftControlSchema | undefined =
1321
+ normalizedTimeline?.enabled
1322
+ ? {
1323
+ defaultValue: false,
1324
+ description:
1325
+ "Shows the extended runtime timeline with scrubber, duration, loop, and keyframe controls; compact mode keeps only Play visible.",
1326
+ label: "Timeline",
1327
+ target: toolcraftTimelinePanelExtendedTarget,
1328
+ type: "switch",
1329
+ }
1330
+ : undefined;
1456
1331
 
1457
1332
  if (!canvas.enabled || canvas.sizing.mode !== "editable-output") {
1458
- const runtimeSetupSection =
1459
- settingsTransferSection && renderScaleControl
1460
- ? mergeCanvasRenderScaleIntoSettingsTransferSection({
1461
- renderScaleControl,
1333
+ const runtimeSetupSection = renderScaleControl
1334
+ ? mergeCanvasRenderScaleIntoSettingsTransferSection({
1335
+ renderScaleControl,
1336
+ settingsTransferSection,
1337
+ timelineExtendedControl,
1338
+ })
1339
+ : timelineExtendedControl
1340
+ ? mergeTimelineExtendedIntoSettingsTransferSection({
1462
1341
  settingsTransferSection,
1342
+ timelineExtendedControl,
1463
1343
  })
1464
- : (settingsTransferSection ??
1465
- (renderScaleControl ? createCanvasRenderScaleSection(renderScaleControl) : null));
1344
+ : settingsTransferSection;
1466
1345
 
1467
1346
  return {
1468
1347
  ...normalizedPanels,
1469
1348
  controls: normalizeControlsPanelLayout({
1470
1349
  ...controls,
1471
1350
  sections: [
1472
- ...(runtimeSetupSection ? [runtimeSetupSection] : []),
1351
+ runtimeSetupSection,
1473
1352
  ...controls.sections,
1474
1353
  ],
1475
1354
  }),
@@ -1488,59 +1367,36 @@ function normalizePanels({
1488
1367
  type: "aspectRatio",
1489
1368
  };
1490
1369
 
1491
- if (!hasControlTarget(panels, canvasSizeControlTargets.width)) {
1492
- sizeControls.canvasWidth = {
1493
- defaultValue: canvas.size.width,
1494
- label: "Canvas width",
1495
- orderRole: "input",
1496
- performanceReason: "Canvas width changes output dimensions and renderer workload.",
1497
- performanceRole: "workload",
1498
- target: canvasSizeControlTargets.width,
1499
- type: "text",
1500
- };
1501
- sizeControlIds.push("canvasWidth");
1502
- }
1503
-
1504
- if (!hasControlTarget(panels, canvasSizeControlTargets.height)) {
1505
- sizeControls.canvasHeight = {
1506
- defaultValue: canvas.size.height,
1507
- label: "Canvas height",
1508
- orderRole: "input",
1509
- performanceReason: "Canvas height changes output dimensions and renderer workload.",
1510
- performanceRole: "workload",
1511
- target: canvasSizeControlTargets.height,
1512
- type: "text",
1513
- };
1514
- sizeControlIds.push("canvasHeight");
1515
- }
1516
-
1517
- if (Object.keys(sizeControls).length === 0 && !renderScaleControl) {
1518
- return {
1519
- ...normalizedPanels,
1520
- controls: normalizeControlsPanelLayout({
1521
- ...controls,
1522
- sections: [
1523
- ...(settingsTransferSection ? [settingsTransferSection] : []),
1524
- ...controls.sections,
1525
- ],
1526
- }),
1527
- };
1528
- }
1370
+ sizeControls.canvasWidth = {
1371
+ defaultValue: canvas.size.width,
1372
+ label: "Canvas width",
1373
+ orderRole: "input",
1374
+ performanceReason: "Canvas width changes output dimensions and renderer workload.",
1375
+ performanceRole: "workload",
1376
+ target: canvasSizeControlTargets.width,
1377
+ type: "text",
1378
+ };
1379
+ sizeControlIds.push("canvasWidth");
1529
1380
 
1530
- const runtimeSettingsSection = settingsTransferSection
1531
- ? mergeCanvasSizeControlsIntoSettingsTransferSection({
1532
- aspectRatioControl,
1533
- renderScaleControl,
1534
- settingsTransferSection,
1535
- sizeControlIds,
1536
- sizeControls,
1537
- })
1538
- : createCanvasSizeSection({
1539
- aspectRatioControl,
1540
- renderScaleControl,
1541
- sizeControlIds,
1542
- sizeControls,
1543
- });
1381
+ sizeControls.canvasHeight = {
1382
+ defaultValue: canvas.size.height,
1383
+ label: "Canvas height",
1384
+ orderRole: "input",
1385
+ performanceReason: "Canvas height changes output dimensions and renderer workload.",
1386
+ performanceRole: "workload",
1387
+ target: canvasSizeControlTargets.height,
1388
+ type: "text",
1389
+ };
1390
+ sizeControlIds.push("canvasHeight");
1391
+
1392
+ const runtimeSettingsSection = mergeCanvasSizeControlsIntoSettingsTransferSection({
1393
+ aspectRatioControl,
1394
+ renderScaleControl,
1395
+ settingsTransferSection,
1396
+ sizeControlIds,
1397
+ sizeControls,
1398
+ timelineExtendedControl,
1399
+ });
1544
1400
 
1545
1401
  return {
1546
1402
  ...normalizedPanels,
@@ -1560,7 +1416,11 @@ function resolveTimelinePanel(
1560
1416
  timeline: ToolcraftTimelinePanelSchema | undefined,
1561
1417
  ): ResolvedToolcraftTimelinePanelSchema | undefined {
1562
1418
  if (timeline === true) {
1563
- return { enabled: true, mode: "keyframes" };
1419
+ return {
1420
+ defaultDurationSeconds: defaultTimelineDurationSeconds,
1421
+ enabled: true,
1422
+ mode: "keyframes",
1423
+ };
1564
1424
  }
1565
1425
 
1566
1426
  if (!timeline || timeline.enabled === false) {
@@ -1568,11 +1428,20 @@ function resolveTimelinePanel(
1568
1428
  }
1569
1429
 
1570
1430
  return {
1431
+ defaultDurationSeconds: normalizeTimelineDurationSeconds(timeline.defaultDurationSeconds),
1571
1432
  enabled: true,
1572
1433
  mode: timeline.mode ?? "keyframes",
1573
1434
  };
1574
1435
  }
1575
1436
 
1437
+ function normalizeTimelineDurationSeconds(value: unknown): number {
1438
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1439
+ return defaultTimelineDurationSeconds;
1440
+ }
1441
+
1442
+ return Math.min(maxTimelineDurationSeconds, Math.max(minTimelineDurationSeconds, value));
1443
+ }
1444
+
1576
1445
  function hasVisibleRuntimePanel({
1577
1446
  panels,
1578
1447
  toolbar,
@@ -1621,7 +1490,6 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
1621
1490
  const persistence = resolvePersistence(schema.persistence);
1622
1491
  const settingsTransfer = resolveSettingsTransfer({
1623
1492
  controls: schema.panels.controls,
1624
- panels: schema.panels,
1625
1493
  persistence,
1626
1494
  settingsTransfer: schema.settingsTransfer,
1627
1495
  });
@@ -1646,6 +1514,7 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
1646
1514
  zoom: schema.toolbar?.zoom ?? canvasEnabled,
1647
1515
  };
1648
1516
  const exportSchema = resolveExport(schema.export);
1517
+ const media = resolveMedia(schema.media);
1649
1518
 
1650
1519
  assertPanelPersistenceContract({ panels, persistence, toolbar });
1651
1520
 
@@ -1657,6 +1526,7 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
1657
1526
  }),
1658
1527
  canvas,
1659
1528
  export: exportSchema,
1529
+ media,
1660
1530
  panels,
1661
1531
  persistence,
1662
1532
  settingsTransfer,
@@ -1,8 +1,14 @@
1
+ export const toolcraftTimelinePanelVisibleTarget = "panels.timeline.visible";
2
+ export const toolcraftTimelinePanelExtendedTarget = "panels.timeline.extended";
3
+
1
4
  export const toolcraftRuntimeOwnedTargets = [
2
5
  "canvas.aspectRatio",
3
6
  "canvas.renderScale",
4
7
  "canvas.size.width",
5
8
  "canvas.size.height",
9
+ "runtime.settingsTransfer",
10
+ toolcraftTimelinePanelExtendedTarget,
11
+ toolcraftTimelinePanelVisibleTarget,
6
12
  ] as const;
7
13
 
8
14
  export const toolcraftReservedTargets = [
@@ -33,6 +39,21 @@ export function isToolcraftCanvasAspectRatioTarget(target: string): boolean {
33
39
  return target === "canvas.aspectRatio";
34
40
  }
35
41
 
42
+ export function isToolcraftTimelinePanelVisibleTarget(target: string): boolean {
43
+ return target === toolcraftTimelinePanelVisibleTarget;
44
+ }
45
+
46
+ export function isToolcraftTimelinePanelExtendedTarget(target: string): boolean {
47
+ return target === toolcraftTimelinePanelExtendedTarget;
48
+ }
49
+
50
+ export function isToolcraftTimelinePanelRuntimeTarget(target: string): boolean {
51
+ return (
52
+ isToolcraftTimelinePanelExtendedTarget(target) ||
53
+ isToolcraftTimelinePanelVisibleTarget(target)
54
+ );
55
+ }
56
+
36
57
  export function isToolcraftReservedTarget(
37
58
  target: string,
38
59
  ): target is ToolcraftReservedTarget {
@@ -110,7 +110,9 @@ export type ToolcraftAssemblyCommand =
110
110
  | "media.delete"
111
111
  | "media.import"
112
112
  | "media.reorder"
113
+ | "media.transform"
113
114
  | "panels.resetOffset"
115
+ | "panels.setHidden"
114
116
  | "panels.setOffset"
115
117
  | "timeline.changeKeyframeEasing"
116
118
  | "timeline.deleteControlKeyframes"
@@ -180,11 +182,13 @@ export type ToolcraftTimelineMode = "keyframes" | "playback";
180
182
  export type ToolcraftTimelinePanelSchema =
181
183
  | boolean
182
184
  | {
185
+ defaultDurationSeconds?: number;
183
186
  enabled?: boolean;
184
187
  mode?: ToolcraftTimelineMode;
185
188
  };
186
189
 
187
190
  export type ResolvedToolcraftTimelinePanelSchema = {
191
+ defaultDurationSeconds: number;
188
192
  enabled: boolean;
189
193
  mode: ToolcraftTimelineMode;
190
194
  };
@@ -192,6 +196,7 @@ export type ResolvedToolcraftTimelinePanelSchema = {
192
196
  export type ToolcraftPersistableStateSlice =
193
197
  | "canvas"
194
198
  | "layers"
199
+ | "media"
195
200
  | "panels"
196
201
  | "timeline"
197
202
  | "values";
@@ -247,6 +252,7 @@ export type ToolcraftActionSchema = {
247
252
  | "export"
248
253
  | "rotate-ccw"
249
254
  | "shuffle"
255
+ | "upload-simple"
250
256
  | "wand-sparkles";
251
257
  label?: string;
252
258
  value: string;
@@ -276,6 +282,39 @@ export type ToolcraftControlPerformanceRole =
276
282
 
277
283
  export type ToolcraftFileDropAssetKind = "file" | "image";
278
284
 
285
+ export type ToolcraftMediaPositionSchema = {
286
+ x: number;
287
+ y: number;
288
+ };
289
+
290
+ export type ToolcraftMediaTransformSchema = {
291
+ flipHorizontal?: boolean;
292
+ flipVertical?: boolean;
293
+ rotationDeg?: 0 | 90 | 180 | 270;
294
+ };
295
+
296
+ export type ToolcraftDefaultMediaAssetSchema = {
297
+ assetKind?: ToolcraftFileDropAssetKind;
298
+ dataUrl: string;
299
+ fileName: string;
300
+ id?: string;
301
+ layerId?: string;
302
+ layerName?: string;
303
+ mimeType?: string;
304
+ position?: ToolcraftMediaPositionSchema;
305
+ size?: ToolcraftCanvasSize;
306
+ sourceTarget?: string;
307
+ transform?: ToolcraftMediaTransformSchema;
308
+ };
309
+
310
+ export type ToolcraftMediaSchema = {
311
+ defaultAssets?: readonly ToolcraftDefaultMediaAssetSchema[];
312
+ };
313
+
314
+ export type ResolvedToolcraftMediaSchema = {
315
+ defaultAssets: readonly ToolcraftDefaultMediaAssetSchema[];
316
+ };
317
+
279
318
  export type ToolcraftControlConditionSchema = {
280
319
  equals?: unknown;
281
320
  greaterThan?: number;
@@ -401,6 +440,7 @@ export type ResolvedToolcraftPanelsSchema = {
401
440
  export type ToolcraftAppSchema = {
402
441
  canvas: ToolcraftCanvasSchema;
403
442
  export?: ToolcraftExportSchema;
443
+ media?: ToolcraftMediaSchema;
404
444
  panels: ToolcraftPanelsSchema;
405
445
  persistence?: ToolcraftPersistenceSchema;
406
446
  settingsTransfer?: ToolcraftSettingsTransferSchema;
@@ -415,6 +455,7 @@ export type ResolvedToolcraftAppSchema = {
415
455
  sizeSource: ToolcraftCanvasSizeSource;
416
456
  };
417
457
  export: ResolvedToolcraftExportSchema;
458
+ media: ResolvedToolcraftMediaSchema;
418
459
  panels: ResolvedToolcraftPanelsSchema;
419
460
  persistence: ResolvedToolcraftPersistenceSchema;
420
461
  settingsTransfer: ResolvedToolcraftSettingsTransferSchema;