@pixel-point/toolcraft 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +185 -23
  7. package/templates/runtime/contracts/component-contracts.ts +91 -36
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +4 -4
  10. package/templates/runtime/export/export.test.ts +31 -0
  11. package/templates/runtime/export/export.ts +41 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  13. package/templates/runtime/react/canvas-shell.tsx +178 -23
  14. package/templates/runtime/react/control-conditions.ts +166 -0
  15. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  16. package/templates/runtime/react/controls-panel.test.tsx +774 -17
  17. package/templates/runtime/react/controls-panel.tsx +155 -8
  18. package/templates/runtime/react/media-file.ts +19 -0
  19. package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
  20. package/templates/runtime/schema/define-toolcraft.ts +29 -3
  21. package/templates/runtime/schema/types.ts +7 -0
  22. package/templates/runtime/state/reducer.test.ts +304 -0
  23. package/templates/runtime/state/reducer.ts +148 -9
  24. package/templates/runtime/state/types.ts +10 -2
  25. package/templates/runtime/testing/performance.test.ts +1424 -56
  26. package/templates/runtime/testing/performance.ts +710 -43
  27. package/templates/starter/AGENTS.md +10 -9
  28. package/templates/starter/docs/toolcraft/README.md +1 -1
  29. package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
  30. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
  31. package/templates/starter/docs/toolcraft/component-rules.md +46 -16
  32. package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
  33. package/templates/starter/docs/toolcraft/performance.md +54 -6
  34. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  35. package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
  36. package/templates/starter/docs/toolcraft/workflow.md +2 -2
  37. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  38. package/templates/starter/e2e/performance-helpers.ts +197 -0
  39. package/templates/starter/gitignore +1 -0
  40. package/templates/starter/package.json +5 -0
  41. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  42. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  43. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  44. package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
  45. package/templates/starter/src/app/starter-acceptance.ts +740 -24
  46. package/templates/starter/src/app/starter-performance.test.ts +66 -5
  47. package/templates/ui/components/control-layout/index.tsx +8 -3
  48. package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
  49. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  50. package/templates/ui/components/controls/color/index.ts +4 -1
  51. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  52. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  53. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  54. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  55. package/templates/ui/components/controls/index.ts +3 -0
  56. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  57. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  58. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  59. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  60. package/templates/ui/components/controls/vector/index.ts +1 -0
  61. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  62. package/templates/ui/components/panel/panel-section.tsx +82 -6
@@ -47,6 +47,7 @@ import {
47
47
  type GradientStop,
48
48
  type GradientType,
49
49
  type ImagePickerItem,
50
+ type VectorPadCoordinateMode,
50
51
  type VectorPadVariant,
51
52
  } from "@repo/ui";
52
53
 
@@ -70,7 +71,11 @@ import type {
70
71
  ToolcraftPanelState,
71
72
  ToolcraftState,
72
73
  } from "../state/types";
73
- import { readImportedImageFile } from "./media-file";
74
+ import {
75
+ isToolcraftImageFile,
76
+ readImportedFile,
77
+ readImportedImageFile,
78
+ } from "./media-file";
74
79
  import { PanelContainer } from "./panel-host";
75
80
  import type { PanelPlacement, PanelStateChange } from "./panel-host-types";
76
81
  import type { ToolcraftControlRendererMap } from "./control-renderers";
@@ -417,6 +422,17 @@ function asVectorPadVariant(value: string | undefined): VectorPadVariant {
417
422
  return "default";
418
423
  }
419
424
 
425
+ function asVectorPadCoordinateMode(
426
+ value: string | undefined,
427
+ padVariant: VectorPadVariant,
428
+ ): VectorPadCoordinateMode {
429
+ if (value === "cartesian" || value === "screen") {
430
+ return value;
431
+ }
432
+
433
+ return padVariant === "default" ? "screen" : "cartesian";
434
+ }
435
+
420
436
  function asCurveInterpolation(value: string | undefined): CurveInterpolation | undefined {
421
437
  return value === "monotone" || value === "smooth" ? value : undefined;
422
438
  }
@@ -585,6 +601,8 @@ function getCollectionItemDefaultValue(control: ToolcraftControlSchema): unknown
585
601
  return { hex: "#C1FF00" };
586
602
  case "colorOpacity":
587
603
  return { hex: "#C1FF00", opacity: 100 };
604
+ case "fontPicker":
605
+ return asFontPickerValue(control.itemControl?.defaultValue);
588
606
  case "checkbox":
589
607
  case "switch":
590
608
  return false;
@@ -1148,6 +1166,15 @@ function hasSliderControl(
1148
1166
  );
1149
1167
  }
1150
1168
 
1169
+ function hasSegmentedControl(
1170
+ controlsById: Record<string, ToolcraftControlSchema>,
1171
+ layoutGroup: ToolcraftControlLayoutGroupSchema,
1172
+ ): boolean {
1173
+ return layoutGroup.controls.some(
1174
+ (controlId) => controlsById[controlId]?.type === "segmented",
1175
+ );
1176
+ }
1177
+
1151
1178
  function hasSectionedCompoundControl(
1152
1179
  controlsById: Record<string, ToolcraftControlSchema>,
1153
1180
  layoutGroup: ToolcraftControlLayoutGroupSchema,
@@ -1208,6 +1235,22 @@ function isToggleParameterLayoutGroup(
1208
1235
  return booleanControlCount === 1 && parameterControlCount === 1;
1209
1236
  }
1210
1237
 
1238
+ function shouldHideToggleParameterControlLabel({
1239
+ control,
1240
+ controlsById,
1241
+ layoutGroup,
1242
+ }: {
1243
+ control: ToolcraftControlSchema;
1244
+ controlsById: Record<string, ToolcraftControlSchema>;
1245
+ layoutGroup: ToolcraftControlLayoutGroupSchema | undefined;
1246
+ }): boolean {
1247
+ return Boolean(
1248
+ layoutGroup &&
1249
+ isToggleParameterLayoutGroup(controlsById, layoutGroup) &&
1250
+ !isBooleanControl(control),
1251
+ );
1252
+ }
1253
+
1211
1254
  function getControlTargetEntityKey(control: ToolcraftControlSchema): string | null {
1212
1255
  const parts = control.target.split(".");
1213
1256
 
@@ -1403,6 +1446,7 @@ function getInlineLayoutGroupByControlId({
1403
1446
  if (
1404
1447
  hasColorOpacityControl(controlsById, layoutGroup) ||
1405
1448
  hasSliderControl(controlsById, layoutGroup) ||
1449
+ hasSegmentedControl(controlsById, layoutGroup) ||
1406
1450
  hasSectionedCompoundControl(controlsById, layoutGroup) ||
1407
1451
  hasUnsafeInlineSwitchLabels(controlsById, layoutGroup)
1408
1452
  ) {
@@ -1790,7 +1834,9 @@ export function ControlsPanel({
1790
1834
  }
1791
1835
 
1792
1836
  function isColorSectionTitle(sectionTitle: string | undefined): boolean {
1793
- return /\b(colou?rs?|palette|palettes)\b/i.test(sectionTitle ?? "");
1837
+ return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
1838
+ sectionTitle ?? "",
1839
+ );
1794
1840
  }
1795
1841
 
1796
1842
  function isSequentialColorLabel(label: string): boolean {
@@ -1885,6 +1931,10 @@ export function ControlsPanel({
1885
1931
  providerKey: string;
1886
1932
  sectionTitle: string | undefined;
1887
1933
  }): React.ReactNode {
1934
+ if (control.label === false) {
1935
+ return children;
1936
+ }
1937
+
1888
1938
  const help = getControlHelpText({ control, label, sectionTitle });
1889
1939
 
1890
1940
  if (!help) {
@@ -2221,6 +2271,16 @@ export function ControlsPanel({
2221
2271
  showLabel={itemControl?.label !== false}
2222
2272
  />
2223
2273
  );
2274
+ case "fontPicker":
2275
+ return (
2276
+ <FontPicker
2277
+ defaultValue={asFontPickerValue(itemControl?.defaultValue)}
2278
+ key={key}
2279
+ name={itemName || "Font"}
2280
+ onValueChange={update}
2281
+ value={asFontPickerValue(item)}
2282
+ />
2283
+ );
2224
2284
  case "text":
2225
2285
  return (
2226
2286
  <TextInput
@@ -2386,13 +2446,23 @@ export function ControlsPanel({
2386
2446
  }
2387
2447
 
2388
2448
  const [id, rawControl] = group.entry;
2389
- const isInInlineLayoutGroup = inlineLayoutGroupByControlId.has(id);
2449
+ const inlineLayoutGroup = inlineLayoutGroupByControlId.get(id);
2450
+ const isInInlineLayoutGroup = Boolean(inlineLayoutGroup);
2390
2451
  const disabled = isControlDisabled(rawControl);
2391
- const control =
2452
+ const resolvedControl =
2392
2453
  disabled === Boolean(rawControl.disabled)
2393
2454
  ? rawControl
2394
2455
  : { ...rawControl, disabled };
2395
- const name = getControlName(id, control.label);
2456
+ const shouldHideLabel = shouldHideToggleParameterControlLabel({
2457
+ control: resolvedControl,
2458
+ controlsById: visibleControls,
2459
+ layoutGroup: inlineLayoutGroup,
2460
+ });
2461
+ const control =
2462
+ shouldHideLabel && resolvedControl.label !== false
2463
+ ? { ...resolvedControl, label: false }
2464
+ : resolvedControl;
2465
+ const name = getControlName(id, resolvedControl.label);
2396
2466
  const value = getControlValue(control);
2397
2467
  const usesHeaderKeyframeAction = control.target === headerKeyframeTarget;
2398
2468
  const commitWithLabel =
@@ -2504,6 +2574,7 @@ export function ControlsPanel({
2504
2574
  key={id}
2505
2575
  name={name}
2506
2576
  onValueChange={commit}
2577
+ showLabel={control.label !== false}
2507
2578
  value={asString(value)}
2508
2579
  />
2509
2580
  ),
@@ -2590,15 +2661,51 @@ export function ControlsPanel({
2590
2661
  }
2591
2662
 
2592
2663
  case "fileDrop": {
2593
- const previewMediaAssets = state.schema.panels.layers ? [] : state.mediaAssets;
2664
+ const assetKind = control.assetKind === "file" ? "file" : "image";
2665
+ const previewMediaAssets = state.schema.panels.layers
2666
+ ? []
2667
+ : state.mediaAssets.filter(
2668
+ (asset) =>
2669
+ asset.sourceTarget === undefined ||
2670
+ asset.sourceTarget === control.target,
2671
+ );
2594
2672
  const previewMediaAsset = previewMediaAssets[0];
2595
2673
  const previews = previewMediaAssets.map((asset): FileDropPreview => ({
2596
2674
  alt: asset.fileName,
2675
+ assetKind: asset.assetKind ?? "image",
2676
+ fileName: asset.fileName,
2597
2677
  id: asset.id,
2598
2678
  size: asset.size,
2599
2679
  src: asset.dataUrl,
2600
2680
  }));
2681
+ const previewMediaIds = previewMediaAssets.map((asset) => asset.id);
2601
2682
  const importFile = (file: File, replaceExisting: boolean): void => {
2683
+ if (assetKind === "file") {
2684
+ void readImportedFile(file).then((importedFile) => {
2685
+ if (!importedFile) {
2686
+ return;
2687
+ }
2688
+
2689
+ dispatchCommand({
2690
+ asset: {
2691
+ assetKind: "file",
2692
+ dataUrl: importedFile.dataUrl,
2693
+ fileName: file.name,
2694
+ mimeType: file.type || "application/octet-stream",
2695
+ position: { x: 0, y: 0 },
2696
+ sourceTarget: control.target,
2697
+ },
2698
+ replaceExisting,
2699
+ type: "media.import",
2700
+ });
2701
+ });
2702
+ return;
2703
+ }
2704
+
2705
+ if (!isToolcraftImageFile(file)) {
2706
+ return;
2707
+ }
2708
+
2602
2709
  void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
2603
2710
  if (!importedImage) {
2604
2711
  return;
@@ -2606,11 +2713,13 @@ export function ControlsPanel({
2606
2713
 
2607
2714
  dispatchCommand({
2608
2715
  asset: {
2716
+ assetKind: "image",
2609
2717
  dataUrl: importedImage.dataUrl,
2610
2718
  fileName: file.name,
2611
2719
  mimeType: file.type || "image/*",
2612
2720
  position: { x: 0, y: 0 },
2613
2721
  size: importedImage.size,
2722
+ sourceTarget: control.target,
2614
2723
  },
2615
2724
  replaceExisting,
2616
2725
  type: "media.import",
@@ -2620,7 +2729,11 @@ export function ControlsPanel({
2620
2729
 
2621
2730
  return (
2622
2731
  <FileDrop
2623
- accept={control.accept ?? "PNG, JPEG, GIF, SVG, WebP"}
2732
+ accept={
2733
+ control.accept ??
2734
+ (assetKind === "file" ? "" : "PNG, JPEG, GIF, SVG, WebP")
2735
+ }
2736
+ assetKind={assetKind}
2624
2737
  multiple={control.multiple}
2625
2738
  key={id}
2626
2739
  onClear={
@@ -2647,6 +2760,32 @@ export function ControlsPanel({
2647
2760
  type: "media.delete",
2648
2761
  });
2649
2762
  }}
2763
+ onPreviewReorder={(orderedPreviews) => {
2764
+ const orderedPreviewIds = orderedPreviews.flatMap((item) =>
2765
+ item.id ? [item.id] : [],
2766
+ );
2767
+
2768
+ if (orderedPreviewIds.length !== previewMediaIds.length) {
2769
+ return;
2770
+ }
2771
+
2772
+ const previewIdSet = new Set(previewMediaIds);
2773
+ let orderedPreviewIndex = 0;
2774
+ const mediaIds = state.mediaAssets.map((asset) => {
2775
+ if (!previewIdSet.has(asset.id)) {
2776
+ return asset.id;
2777
+ }
2778
+
2779
+ const nextId = orderedPreviewIds[orderedPreviewIndex];
2780
+ orderedPreviewIndex += 1;
2781
+ return nextId ?? asset.id;
2782
+ });
2783
+
2784
+ dispatchCommand({
2785
+ mediaIds,
2786
+ type: "media.reorder",
2787
+ });
2788
+ }}
2650
2789
  preview={
2651
2790
  previewMediaAsset
2652
2791
  ? {
@@ -2777,6 +2916,7 @@ export function ControlsPanel({
2777
2916
  key={id}
2778
2917
  name={name}
2779
2918
  onValueChange={commit}
2919
+ showLabel={control.label !== false}
2780
2920
  start={rangeValue.start}
2781
2921
  />
2782
2922
  ),
@@ -2843,6 +2983,7 @@ export function ControlsPanel({
2843
2983
  name={name}
2844
2984
  onValueChange={commit}
2845
2985
  options={control.options ?? []}
2986
+ showLabel={control.label !== false}
2846
2987
  value={asString(value, control.options?.[0]?.value ?? "")}
2847
2988
  />
2848
2989
  );
@@ -2922,6 +3063,7 @@ export function ControlsPanel({
2922
3063
  key={id}
2923
3064
  name={name}
2924
3065
  onValueChange={commit}
3066
+ showLabel={control.label !== false}
2925
3067
  value={asString(value)}
2926
3068
  />
2927
3069
  ),
@@ -2934,6 +3076,7 @@ export function ControlsPanel({
2934
3076
 
2935
3077
  case "vector": {
2936
3078
  const vectorValue = asVectorValue(value);
3079
+ const padVariant = asVectorPadVariant(control.variant);
2937
3080
 
2938
3081
  return withKeyframeLabelAction({
2939
3082
  children: (
@@ -2942,8 +3085,12 @@ export function ControlsPanel({
2942
3085
  key={id}
2943
3086
  name={name}
2944
3087
  onValueChange={commit}
3088
+ padCoordinateMode={asVectorPadCoordinateMode(
3089
+ control.coordinateMode,
3090
+ padVariant,
3091
+ )}
2945
3092
  padShape={vectorPadShape}
2946
- padVariant={asVectorPadVariant(control.variant)}
3093
+ padVariant={padVariant}
2947
3094
  x={vectorValue.x}
2948
3095
  xLabel={control.xLabel}
2949
3096
  y={vectorValue.y}
@@ -5,6 +5,10 @@ export type ToolcraftImportedImageFile = {
5
5
  size: ToolcraftCanvasSize;
6
6
  };
7
7
 
8
+ export type ToolcraftImportedFile = {
9
+ dataUrl: string;
10
+ };
11
+
8
12
  function readFileDataUrl(file: File): Promise<string | null> {
9
13
  return new Promise((resolve) => {
10
14
  const reader = new FileReader();
@@ -65,6 +69,21 @@ function readImageDataUrlSize(dataUrl: string): Promise<ToolcraftCanvasSize | nu
65
69
  });
66
70
  }
67
71
 
72
+ export function isToolcraftImageFile(file: File): boolean {
73
+ return (
74
+ file.type.startsWith("image/") ||
75
+ /\.(avif|gif|heic|heif|jpe?g|png|svg|tiff?|webp)$/i.test(file.name)
76
+ );
77
+ }
78
+
79
+ export async function readImportedFile(
80
+ file: File,
81
+ ): Promise<ToolcraftImportedFile | null> {
82
+ const dataUrl = await readFileDataUrl(file);
83
+
84
+ return dataUrl ? { dataUrl } : null;
85
+ }
86
+
68
87
  export async function readImportedImageFile(
69
88
  file: File,
70
89
  fallbackSize: ToolcraftCanvasSize,
@@ -539,6 +539,7 @@ describe("defineToolcraft", () => {
539
539
  "history.undo",
540
540
  "media.delete",
541
541
  "media.import",
542
+ "media.reorder",
542
543
  "layers.reorder",
543
544
  "timeline.setCurrentTime",
544
545
  "canvas.center",
@@ -836,7 +837,6 @@ describe("defineToolcraft", () => {
836
837
  step: 0.25,
837
838
  target: "canvas.renderScale",
838
839
  type: "slider",
839
- unit: "x",
840
840
  variant: "discrete",
841
841
  });
842
842
  });
@@ -1396,6 +1396,51 @@ describe("defineToolcraft", () => {
1396
1396
  ]);
1397
1397
  });
1398
1398
 
1399
+ it("keeps mode-gated standalone controls inside their semantic section", () => {
1400
+ const app = defineToolcraft({
1401
+ canvas: { enabled: true },
1402
+ panels: {
1403
+ controls: {
1404
+ sections: [
1405
+ {
1406
+ controls: {
1407
+ sourceMode: {
1408
+ defaultValue: "preset",
1409
+ label: "Source",
1410
+ options: [
1411
+ { label: "Preset", value: "preset" },
1412
+ { label: "Image", value: "image" },
1413
+ ],
1414
+ target: "source.mode",
1415
+ type: "segmented",
1416
+ },
1417
+ sourceUpload: {
1418
+ accept: "image/*",
1419
+ assetKind: "image",
1420
+ defaultValue: null,
1421
+ label: "Image",
1422
+ target: "source.upload",
1423
+ type: "fileDrop",
1424
+ visibleWhen: { equals: "image", target: "source.mode" },
1425
+ },
1426
+ },
1427
+ title: "Source",
1428
+ },
1429
+ ],
1430
+ title: "Controls",
1431
+ },
1432
+ },
1433
+ });
1434
+
1435
+ expect(app.panels.controls?.sections).toHaveLength(1);
1436
+ expect(app.panels.controls?.sections[0]?.title).toBe("Source");
1437
+ expect(app.panels.controls?.sections[0]?.layout).toBeUndefined();
1438
+ expect(Object.keys(app.panels.controls?.sections[0]?.controls ?? {})).toEqual([
1439
+ "sourceMode",
1440
+ "sourceUpload",
1441
+ ]);
1442
+ });
1443
+
1399
1444
  it("preserves controls panel action sections and render metadata", () => {
1400
1445
  const app = defineToolcraft({
1401
1446
  canvas: { enabled: true },
@@ -332,7 +332,7 @@ function createToolcraftAssembly({
332
332
 
333
333
  if (canvas.upload) {
334
334
  capabilities.push("canvas.upload");
335
- commands.push("media.delete", "media.import");
335
+ commands.push("media.delete", "media.import", "media.reorder");
336
336
  }
337
337
  }
338
338
 
@@ -478,7 +478,9 @@ function createToolcraftAssembly({
478
478
  ...(canvas.draggable
479
479
  ? (["canvas.panBy", "canvas.setOffset", "canvas.setViewport"] as const)
480
480
  : []),
481
- ...(canvas.upload ? (["media.delete", "media.import"] as const) : []),
481
+ ...(canvas.upload
482
+ ? (["media.delete", "media.import", "media.reorder"] as const)
483
+ : []),
482
484
  ])
483
485
  : [],
484
486
  enabled: canvas.enabled,
@@ -522,6 +524,10 @@ function getControlSectionLayout(
522
524
  control: ToolcraftControlSchema,
523
525
  entries: readonly [string, ToolcraftControlSchema][],
524
526
  ): "grouped" | "standalone" {
527
+ if (isControlGatedBySameSectionControl(control, entries)) {
528
+ return "grouped";
529
+ }
530
+
525
531
  if (
526
532
  (control.type === "color" || control.type === "colorOpacity") &&
527
533
  entries.some(
@@ -537,6 +543,24 @@ function getControlSectionLayout(
537
543
  return getControlDefaultSectionLayout(control);
538
544
  }
539
545
 
546
+ function isControlGatedBySameSectionControl(
547
+ control: ToolcraftControlSchema,
548
+ entries: readonly [string, ToolcraftControlSchema][],
549
+ ): boolean {
550
+ const gateTargets = [control.visibleWhen?.target, control.disabledWhen?.target].filter(
551
+ (target): target is string => Boolean(target),
552
+ );
553
+
554
+ if (gateTargets.length === 0) {
555
+ return false;
556
+ }
557
+
558
+ return entries.some(([, entryControl]) =>
559
+ gateTargets.includes(entryControl.target) &&
560
+ getControlDefaultSectionLayout(entryControl) === "grouped",
561
+ );
562
+ }
563
+
540
564
  function createControlsRecord(
541
565
  entries: readonly [string, ToolcraftControlSchema][],
542
566
  ): Record<string, ToolcraftControlSchema> {
@@ -1426,7 +1450,6 @@ function normalizePanels({
1426
1450
  step: canvas.renderScale.step,
1427
1451
  target: canvasRenderScaleTarget,
1428
1452
  type: "slider",
1429
- unit: "x",
1430
1453
  variant: "discrete",
1431
1454
  }
1432
1455
  : undefined;
@@ -1458,6 +1481,7 @@ function normalizePanels({
1458
1481
  const aspectRatioControl: ToolcraftControlSchema = {
1459
1482
  defaultValue: getCanvasAspectRatioDefaultValue(canvas.size),
1460
1483
  label: "Aspect ratio",
1484
+ orderRole: "input",
1461
1485
  performanceReason: "Aspect ratio changes output dimensions and renderer workload.",
1462
1486
  performanceRole: "workload",
1463
1487
  target: canvasAspectRatioTarget,
@@ -1468,6 +1492,7 @@ function normalizePanels({
1468
1492
  sizeControls.canvasWidth = {
1469
1493
  defaultValue: canvas.size.width,
1470
1494
  label: "Canvas width",
1495
+ orderRole: "input",
1471
1496
  performanceReason: "Canvas width changes output dimensions and renderer workload.",
1472
1497
  performanceRole: "workload",
1473
1498
  target: canvasSizeControlTargets.width,
@@ -1480,6 +1505,7 @@ function normalizePanels({
1480
1505
  sizeControls.canvasHeight = {
1481
1506
  defaultValue: canvas.size.height,
1482
1507
  label: "Canvas height",
1508
+ orderRole: "input",
1483
1509
  performanceReason: "Canvas height changes output dimensions and renderer workload.",
1484
1510
  performanceRole: "workload",
1485
1511
  target: canvasSizeControlTargets.height,
@@ -109,6 +109,7 @@ export type ToolcraftAssemblyCommand =
109
109
  | "layers.toggleVisibility"
110
110
  | "media.delete"
111
111
  | "media.import"
112
+ | "media.reorder"
112
113
  | "panels.resetOffset"
113
114
  | "panels.setOffset"
114
115
  | "timeline.changeKeyframeEasing"
@@ -273,6 +274,8 @@ export type ToolcraftControlPerformanceRole =
273
274
  | "responsiveness"
274
275
  | "workload";
275
276
 
277
+ export type ToolcraftFileDropAssetKind = "file" | "image";
278
+
276
279
  export type ToolcraftControlConditionSchema = {
277
280
  equals?: unknown;
278
281
  greaterThan?: number;
@@ -320,11 +323,15 @@ export type ToolcraftFontPickerValueSchema = {
320
323
 
321
324
  export type ToolcraftCurveInterpolation = "monotone" | "smooth";
322
325
 
326
+ export type ToolcraftVectorCoordinateMode = "cartesian" | "screen";
327
+
323
328
  export type ToolcraftControlSchema = {
324
329
  accept?: string;
325
330
  actions?: readonly (ToolcraftActionSchema | string)[];
331
+ assetKind?: ToolcraftFileDropAssetKind;
326
332
  addLabel?: string;
327
333
  commitMode?: "content" | "setting";
334
+ coordinateMode?: ToolcraftVectorCoordinateMode;
328
335
  defaultValue?: unknown;
329
336
  description?: string;
330
337
  disabled?: boolean;