@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
@@ -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" {
@@ -524,6 +509,10 @@ function getControlSectionLayout(
524
509
  control: ToolcraftControlSchema,
525
510
  entries: readonly [string, ToolcraftControlSchema][],
526
511
  ): "grouped" | "standalone" {
512
+ if (isControlGatedBySameSectionControl(control, entries)) {
513
+ return "grouped";
514
+ }
515
+
527
516
  if (
528
517
  (control.type === "color" || control.type === "colorOpacity") &&
529
518
  entries.some(
@@ -539,6 +528,24 @@ function getControlSectionLayout(
539
528
  return getControlDefaultSectionLayout(control);
540
529
  }
541
530
 
531
+ function isControlGatedBySameSectionControl(
532
+ control: ToolcraftControlSchema,
533
+ entries: readonly [string, ToolcraftControlSchema][],
534
+ ): boolean {
535
+ const gateTargets = [control.visibleWhen?.target, control.disabledWhen?.target].filter(
536
+ (target): target is string => Boolean(target),
537
+ );
538
+
539
+ if (gateTargets.length === 0) {
540
+ return false;
541
+ }
542
+
543
+ return entries.some(([, entryControl]) =>
544
+ gateTargets.includes(entryControl.target) &&
545
+ getControlDefaultSectionLayout(entryControl) === "grouped",
546
+ );
547
+ }
548
+
542
549
  function createControlsRecord(
543
550
  entries: readonly [string, ToolcraftControlSchema][],
544
551
  ): Record<string, ToolcraftControlSchema> {
@@ -615,116 +622,12 @@ function isPanelActionsControl(control: ToolcraftControlSchema): boolean {
615
622
  return control.type === "panelActions";
616
623
  }
617
624
 
618
- function isSettingsTransferControl(control: ToolcraftControlSchema): boolean {
619
- return control.type === "settingsTransfer";
620
- }
621
-
622
- function isRuntimeOnlyActionControl(control: ToolcraftControlSchema): boolean {
623
- return isPanelActionsControl(control) || isSettingsTransferControl(control);
624
- }
625
-
626
- function isSettingsTransferEligibilityControl(control: ToolcraftControlSchema): boolean {
627
- return (
628
- !isRuntimeOnlyActionControl(control) &&
629
- control.target !== canvasAspectRatioTarget &&
630
- control.target !== canvasSizeControlTargets.width &&
631
- control.target !== canvasSizeControlTargets.height
632
- );
633
- }
634
-
635
- function getSettingsTransferComplexityScore({
636
- panels,
637
- }: {
638
- panels: ToolcraftAppSchema["panels"];
639
- }): {
640
- controlCount: number;
641
- score: number;
642
- sectionCount: number;
643
- } {
644
- const sections = panels.controls?.sections ?? [];
645
- let controlCount = 0;
646
- let score = 0;
647
-
648
- for (const section of sections) {
649
- for (const control of Object.values(section.controls)) {
650
- if (!isSettingsTransferEligibilityControl(control)) {
651
- continue;
652
- }
653
-
654
- controlCount += 1;
655
- score += settingsTransferHeavyControlTypes.has(control.type) ? 3 : 1;
656
- }
657
- }
658
-
659
- const sectionCount = sections.filter((section) =>
660
- Object.values(section.controls).some((control) =>
661
- isSettingsTransferEligibilityControl(control),
662
- ),
663
- ).length;
664
-
665
- score += Math.max(0, sectionCount - 4);
666
-
667
- if (
668
- panels.timeline === true ||
669
- (typeof panels.timeline === "object" && panels.timeline.enabled !== false)
670
- ) {
671
- score += 2;
672
- }
673
-
674
- if (panels.layers) {
675
- score += 2;
676
- }
677
-
678
- return { controlCount, score, sectionCount };
679
- }
680
-
681
- export function getToolcraftSettingsTransferEligibility({
682
- panels,
683
- }: {
684
- panels: ToolcraftAppSchema["panels"];
685
- }): ToolcraftSettingsTransferEligibility {
686
- const { controlCount, score, sectionCount } = getSettingsTransferComplexityScore({
687
- panels,
688
- });
689
- const reasons: ToolcraftSettingsTransferEligibilityReason[] = [];
690
-
691
- if (controlCount >= 12) {
692
- reasons.push("control-count");
693
- }
694
-
695
- if (sectionCount >= 5) {
696
- reasons.push("section-count");
697
- }
698
-
699
- if (score >= 18) {
700
- reasons.push("score");
701
- }
702
-
703
- return {
704
- controlCount,
705
- eligible: reasons.length > 0,
706
- reasons,
707
- score,
708
- sectionCount,
709
- };
710
- }
711
-
712
- function shouldAutoEnableSettingsTransfer({
713
- panels,
714
- }: {
715
- panels: ToolcraftAppSchema["panels"];
716
- }): boolean {
717
- return getToolcraftSettingsTransferEligibility({ panels }).eligible;
718
- }
719
-
720
625
  function resolveSettingsTransfer({
721
626
  controls,
722
- panels,
723
627
  persistence,
724
628
  settingsTransfer,
725
629
  }: {
726
630
  controls: ToolcraftControlsPanelSchema | undefined;
727
- panels: ToolcraftAppSchema["panels"];
728
631
  persistence: ResolvedToolcraftAppSchema["persistence"];
729
632
  settingsTransfer: ToolcraftSettingsTransferSchema | undefined;
730
633
  }): ResolvedToolcraftSettingsTransferSchema {
@@ -737,9 +640,7 @@ function resolveSettingsTransfer({
737
640
 
738
641
  return {
739
642
  appId,
740
- enabled:
741
- Boolean(controls) &&
742
- (mode === "auto" ? shouldAutoEnableSettingsTransfer({ panels }) : mode),
643
+ enabled: Boolean(controls),
743
644
  fileName: getSettingsTransferFileName({ appId, settingsTransfer }),
744
645
  mode,
745
646
  };
@@ -747,11 +648,7 @@ function resolveSettingsTransfer({
747
648
 
748
649
  function createSettingsTransferSection(
749
650
  settingsTransfer: ResolvedToolcraftSettingsTransferSchema,
750
- ): ToolcraftControlSectionSchema | null {
751
- if (!settingsTransfer.enabled) {
752
- return null;
753
- }
754
-
651
+ ): ToolcraftControlSectionSchema {
755
652
  return {
756
653
  controls: {
757
654
  settingsTransfer: {
@@ -808,51 +705,20 @@ function getCanvasAspectRatioDefaultValue(size: ToolcraftCanvasSize): {
808
705
  };
809
706
  }
810
707
 
811
- function createCanvasSizeSection({
812
- aspectRatioControl,
813
- renderScaleControl,
814
- sizeControlIds,
815
- sizeControls,
816
- }: {
817
- aspectRatioControl: ToolcraftControlSchema;
818
- renderScaleControl?: ToolcraftControlSchema;
819
- sizeControlIds: readonly string[];
820
- sizeControls: ToolcraftControlSectionSchema["controls"];
821
- }): ToolcraftControlSectionSchema {
822
- return {
823
- controls: {
824
- canvasAspectRatio: aspectRatioControl,
825
- ...sizeControls,
826
- ...(renderScaleControl ? { canvasRenderScale: renderScaleControl } : {}),
827
- },
828
- layoutGroups: getCanvasSizeLayoutGroups(sizeControlIds),
829
- title: runtimeSetupSectionTitle,
830
- };
831
- }
832
-
833
- function createCanvasRenderScaleSection(
834
- renderScaleControl: ToolcraftControlSchema,
835
- ): ToolcraftControlSectionSchema {
836
- return {
837
- controls: {
838
- canvasRenderScale: renderScaleControl,
839
- },
840
- title: runtimeSetupSectionTitle,
841
- };
842
- }
843
-
844
708
  function mergeCanvasSizeControlsIntoSettingsTransferSection({
845
709
  aspectRatioControl,
846
710
  renderScaleControl,
847
711
  settingsTransferSection,
848
712
  sizeControlIds,
849
713
  sizeControls,
714
+ timelineExtendedControl,
850
715
  }: {
851
716
  aspectRatioControl: ToolcraftControlSchema;
852
717
  renderScaleControl?: ToolcraftControlSchema;
853
718
  settingsTransferSection: ToolcraftControlSectionSchema;
854
719
  sizeControlIds: readonly string[];
855
720
  sizeControls: ToolcraftControlSectionSchema["controls"];
721
+ timelineExtendedControl?: ToolcraftControlSchema;
856
722
  }): ToolcraftControlSectionSchema {
857
723
  const canvasSizeLayoutGroups = getCanvasSizeLayoutGroups(sizeControlIds) ?? [];
858
724
 
@@ -863,6 +729,7 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
863
729
  canvasAspectRatio: aspectRatioControl,
864
730
  ...sizeControls,
865
731
  ...(renderScaleControl ? { canvasRenderScale: renderScaleControl } : {}),
732
+ ...(timelineExtendedControl ? { timelineExtended: timelineExtendedControl } : {}),
866
733
  },
867
734
  layoutGroups:
868
735
  canvasSizeLayoutGroups.length > 0 || settingsTransferSection.layoutGroups?.length
@@ -877,15 +744,34 @@ function mergeCanvasSizeControlsIntoSettingsTransferSection({
877
744
  function mergeCanvasRenderScaleIntoSettingsTransferSection({
878
745
  renderScaleControl,
879
746
  settingsTransferSection,
747
+ timelineExtendedControl,
880
748
  }: {
881
749
  renderScaleControl: ToolcraftControlSchema;
882
750
  settingsTransferSection: ToolcraftControlSectionSchema;
751
+ timelineExtendedControl?: ToolcraftControlSchema;
883
752
  }): ToolcraftControlSectionSchema {
884
753
  return {
885
754
  ...settingsTransferSection,
886
755
  controls: {
887
756
  ...settingsTransferSection.controls,
888
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,
889
775
  },
890
776
  };
891
777
  }
@@ -1410,7 +1296,7 @@ function normalizePanels({
1410
1296
  const controls = { ...panels.controls };
1411
1297
  const settingsTransferSection = createSettingsTransferSection(settingsTransfer);
1412
1298
  const renderScaleControl: ToolcraftControlSchema | undefined =
1413
- canvas.renderScale.enabled && !hasControlTarget(panels, canvasRenderScaleTarget)
1299
+ canvas.renderScale.enabled
1414
1300
  ? {
1415
1301
  defaultValue: canvas.renderScale.defaultValue,
1416
1302
  description:
@@ -1428,27 +1314,41 @@ function normalizePanels({
1428
1314
  step: canvas.renderScale.step,
1429
1315
  target: canvasRenderScaleTarget,
1430
1316
  type: "slider",
1431
- unit: "x",
1432
1317
  variant: "discrete",
1433
1318
  }
1434
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;
1435
1331
 
1436
1332
  if (!canvas.enabled || canvas.sizing.mode !== "editable-output") {
1437
- const runtimeSetupSection =
1438
- settingsTransferSection && renderScaleControl
1439
- ? mergeCanvasRenderScaleIntoSettingsTransferSection({
1440
- renderScaleControl,
1333
+ const runtimeSetupSection = renderScaleControl
1334
+ ? mergeCanvasRenderScaleIntoSettingsTransferSection({
1335
+ renderScaleControl,
1336
+ settingsTransferSection,
1337
+ timelineExtendedControl,
1338
+ })
1339
+ : timelineExtendedControl
1340
+ ? mergeTimelineExtendedIntoSettingsTransferSection({
1441
1341
  settingsTransferSection,
1342
+ timelineExtendedControl,
1442
1343
  })
1443
- : (settingsTransferSection ??
1444
- (renderScaleControl ? createCanvasRenderScaleSection(renderScaleControl) : null));
1344
+ : settingsTransferSection;
1445
1345
 
1446
1346
  return {
1447
1347
  ...normalizedPanels,
1448
1348
  controls: normalizeControlsPanelLayout({
1449
1349
  ...controls,
1450
1350
  sections: [
1451
- ...(runtimeSetupSection ? [runtimeSetupSection] : []),
1351
+ runtimeSetupSection,
1452
1352
  ...controls.sections,
1453
1353
  ],
1454
1354
  }),
@@ -1460,63 +1360,43 @@ function normalizePanels({
1460
1360
  const aspectRatioControl: ToolcraftControlSchema = {
1461
1361
  defaultValue: getCanvasAspectRatioDefaultValue(canvas.size),
1462
1362
  label: "Aspect ratio",
1363
+ orderRole: "input",
1463
1364
  performanceReason: "Aspect ratio changes output dimensions and renderer workload.",
1464
1365
  performanceRole: "workload",
1465
1366
  target: canvasAspectRatioTarget,
1466
1367
  type: "aspectRatio",
1467
1368
  };
1468
1369
 
1469
- if (!hasControlTarget(panels, canvasSizeControlTargets.width)) {
1470
- sizeControls.canvasWidth = {
1471
- defaultValue: canvas.size.width,
1472
- label: "Canvas width",
1473
- performanceReason: "Canvas width changes output dimensions and renderer workload.",
1474
- performanceRole: "workload",
1475
- target: canvasSizeControlTargets.width,
1476
- type: "text",
1477
- };
1478
- sizeControlIds.push("canvasWidth");
1479
- }
1480
-
1481
- if (!hasControlTarget(panels, canvasSizeControlTargets.height)) {
1482
- sizeControls.canvasHeight = {
1483
- defaultValue: canvas.size.height,
1484
- label: "Canvas height",
1485
- performanceReason: "Canvas height changes output dimensions and renderer workload.",
1486
- performanceRole: "workload",
1487
- target: canvasSizeControlTargets.height,
1488
- type: "text",
1489
- };
1490
- sizeControlIds.push("canvasHeight");
1491
- }
1492
-
1493
- if (Object.keys(sizeControls).length === 0 && !renderScaleControl) {
1494
- return {
1495
- ...normalizedPanels,
1496
- controls: normalizeControlsPanelLayout({
1497
- ...controls,
1498
- sections: [
1499
- ...(settingsTransferSection ? [settingsTransferSection] : []),
1500
- ...controls.sections,
1501
- ],
1502
- }),
1503
- };
1504
- }
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");
1505
1380
 
1506
- const runtimeSettingsSection = settingsTransferSection
1507
- ? mergeCanvasSizeControlsIntoSettingsTransferSection({
1508
- aspectRatioControl,
1509
- renderScaleControl,
1510
- settingsTransferSection,
1511
- sizeControlIds,
1512
- sizeControls,
1513
- })
1514
- : createCanvasSizeSection({
1515
- aspectRatioControl,
1516
- renderScaleControl,
1517
- sizeControlIds,
1518
- sizeControls,
1519
- });
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
+ });
1520
1400
 
1521
1401
  return {
1522
1402
  ...normalizedPanels,
@@ -1536,7 +1416,11 @@ function resolveTimelinePanel(
1536
1416
  timeline: ToolcraftTimelinePanelSchema | undefined,
1537
1417
  ): ResolvedToolcraftTimelinePanelSchema | undefined {
1538
1418
  if (timeline === true) {
1539
- return { enabled: true, mode: "keyframes" };
1419
+ return {
1420
+ defaultDurationSeconds: defaultTimelineDurationSeconds,
1421
+ enabled: true,
1422
+ mode: "keyframes",
1423
+ };
1540
1424
  }
1541
1425
 
1542
1426
  if (!timeline || timeline.enabled === false) {
@@ -1544,11 +1428,20 @@ function resolveTimelinePanel(
1544
1428
  }
1545
1429
 
1546
1430
  return {
1431
+ defaultDurationSeconds: normalizeTimelineDurationSeconds(timeline.defaultDurationSeconds),
1547
1432
  enabled: true,
1548
1433
  mode: timeline.mode ?? "keyframes",
1549
1434
  };
1550
1435
  }
1551
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
+
1552
1445
  function hasVisibleRuntimePanel({
1553
1446
  panels,
1554
1447
  toolbar,
@@ -1597,7 +1490,6 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
1597
1490
  const persistence = resolvePersistence(schema.persistence);
1598
1491
  const settingsTransfer = resolveSettingsTransfer({
1599
1492
  controls: schema.panels.controls,
1600
- panels: schema.panels,
1601
1493
  persistence,
1602
1494
  settingsTransfer: schema.settingsTransfer,
1603
1495
  });
@@ -1622,6 +1514,7 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
1622
1514
  zoom: schema.toolbar?.zoom ?? canvasEnabled,
1623
1515
  };
1624
1516
  const exportSchema = resolveExport(schema.export);
1517
+ const media = resolveMedia(schema.media);
1625
1518
 
1626
1519
  assertPanelPersistenceContract({ panels, persistence, toolbar });
1627
1520
 
@@ -1633,6 +1526,7 @@ export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAp
1633
1526
  }),
1634
1527
  canvas,
1635
1528
  export: exportSchema,
1529
+ media,
1636
1530
  panels,
1637
1531
  persistence,
1638
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 {