@pixel-point/toolcraft 0.0.3 → 0.0.4

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 (28) hide show
  1. package/package.json +1 -1
  2. package/templates/runtime/contracts/component-contracts.test.ts +28 -7
  3. package/templates/runtime/contracts/component-contracts.ts +14 -7
  4. package/templates/runtime/contracts/decision-contracts.ts +1 -1
  5. package/templates/runtime/export/export.test.ts +65 -0
  6. package/templates/runtime/export/export.ts +54 -1
  7. package/templates/runtime/react/controls-panel.test.tsx +54 -6
  8. package/templates/runtime/react/controls-panel.tsx +216 -0
  9. package/templates/runtime/react/settings-transfer.test.ts +6 -0
  10. package/templates/runtime/react/settings-transfer.ts +28 -2
  11. package/templates/runtime/schema/canvas-aspect-ratio-presets.ts +50 -0
  12. package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
  13. package/templates/runtime/schema/define-toolcraft.ts +60 -2
  14. package/templates/runtime/schema/keyframe-capability.test.ts +7 -0
  15. package/templates/runtime/schema/keyframe-capability.ts +2 -2
  16. package/templates/runtime/schema/runtime-targets.ts +5 -0
  17. package/templates/runtime/state/create-template-state.test.ts +6 -0
  18. package/templates/runtime/state/reducer.test.ts +55 -0
  19. package/templates/runtime/state/reducer.ts +214 -9
  20. package/templates/starter/AGENTS.md +5 -3
  21. package/templates/starter/docs/toolcraft/acceptance-testing.md +3 -1
  22. package/templates/starter/docs/toolcraft/assembly-workflow.md +10 -3
  23. package/templates/starter/docs/toolcraft/component-rules.md +12 -5
  24. package/templates/starter/docs/toolcraft/schema-reference.md +45 -7
  25. package/templates/starter/src/app/starter-acceptance.test.ts +623 -21
  26. package/templates/starter/src/app/starter-acceptance.ts +290 -3
  27. package/templates/ui/components/control-layout/index.tsx +4 -4
  28. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -1
@@ -498,6 +498,14 @@ function getSchemaVideoExportSection(
498
498
  );
499
499
  }
500
500
 
501
+ function getSchemaImageExportSection(
502
+ schema: ResolvedToolcraftAppSchema = starterSchema,
503
+ ): ResolvedControlsSection | undefined {
504
+ return (schema.panels.controls?.sections ?? []).find(
505
+ (section) => normalizeSectionTitle(section.title) === "image export",
506
+ );
507
+ }
508
+
501
509
  function getSectionControlByTarget(
502
510
  section: ResolvedControlsSection | undefined,
503
511
  target: string,
@@ -524,6 +532,106 @@ function getControlOptionValues(control: ResolvedControl | undefined): string[]
524
532
  return control?.options?.map((option) => option.value.toLowerCase()) ?? [];
525
533
  }
526
534
 
535
+ function makeBackgroundSection() {
536
+ return {
537
+ controls: {
538
+ includeBackground: {
539
+ defaultValue: true,
540
+ description:
541
+ "Controls PNG background transparency while preview and video keep the background.",
542
+ label: "Include",
543
+ target: "export.includeBackground",
544
+ type: "switch",
545
+ },
546
+ background: {
547
+ defaultValue: "#0F0F0F",
548
+ label: false,
549
+ target: "appearance.background",
550
+ type: "color",
551
+ },
552
+ },
553
+ layoutGroups: [
554
+ {
555
+ columns: 2,
556
+ controls: ["includeBackground", "background"],
557
+ layout: "inline",
558
+ },
559
+ ],
560
+ title: "Background",
561
+ } as const;
562
+ }
563
+
564
+ function makeImageExportSection() {
565
+ return {
566
+ controls: {
567
+ imageFormat: {
568
+ defaultValue: "png",
569
+ label: "Format",
570
+ options: [
571
+ { label: "PNG", value: "png" },
572
+ { label: "JPG", value: "jpg" },
573
+ ],
574
+ target: "export.image.format",
575
+ type: "select",
576
+ },
577
+ imageResolution: {
578
+ defaultValue: "4k",
579
+ label: "Resolution",
580
+ options: [
581
+ { label: "2K", value: "2k" },
582
+ { label: "4K", value: "4k" },
583
+ { label: "8K", value: "8k" },
584
+ ],
585
+ target: "export.image.resolution",
586
+ type: "select",
587
+ },
588
+ },
589
+ layoutGroups: [
590
+ {
591
+ columns: 2,
592
+ controls: ["imageFormat", "imageResolution"],
593
+ layout: "inline",
594
+ },
595
+ ],
596
+ title: "Image Export",
597
+ } as const;
598
+ }
599
+
600
+ function makeVideoExportSection() {
601
+ return {
602
+ controls: {
603
+ videoFormat: {
604
+ defaultValue: "mp4",
605
+ label: "Format",
606
+ options: [
607
+ { label: "MP4", value: "mp4" },
608
+ { label: "WebM", value: "webm" },
609
+ ],
610
+ target: "export.video.format",
611
+ type: "select",
612
+ },
613
+ videoResolution: {
614
+ defaultValue: "current",
615
+ label: "Resolution",
616
+ options: [
617
+ { label: "Current", value: "current" },
618
+ { label: "4K", value: "4k" },
619
+ ],
620
+ target: "export.video.resolution",
621
+ type: "select",
622
+ },
623
+ },
624
+ layoutGroups: [
625
+ {
626
+ columns: 2,
627
+ controls: ["videoFormat", "videoResolution"],
628
+ layout: "inline",
629
+ },
630
+ ],
631
+ title: "Video Export",
632
+ } as const;
633
+ }
634
+
527
635
  function sourceHasVideoCapabilityCheck(source: string): boolean {
528
636
  return /\bMediaRecorder\.isTypeSupported\b|\bVideoEncoder\b|\bffmpeg\b|\bFFmpeg\b|\btranscoder\b|\bencoder\b/i.test(
529
637
  source,
@@ -574,6 +682,47 @@ function sourceHasCustomMovOrProResEncoder(source: string): boolean {
574
682
  );
575
683
  }
576
684
 
685
+ function sourceHasImageExportDimensionCoverage(source: string): boolean {
686
+ const hasImageDecoder =
687
+ /\bcreateImageBitmap\b/.test(source) ||
688
+ /\bnew\s+Image\s*\(/.test(source) ||
689
+ /\bHTMLImageElement\b/.test(source);
690
+ const hasDimensionRead =
691
+ /\b(?:bitmap|image|img|png|exportedImage|decodedImage)\.(?:width|naturalWidth|videoWidth)\b/i.test(
692
+ source,
693
+ ) &&
694
+ /\b(?:bitmap|image|img|png|exportedImage|decodedImage)\.(?:height|naturalHeight|videoHeight)\b/i.test(
695
+ source,
696
+ );
697
+ const hasResolutionPreset =
698
+ /\b(?:2k|4k|8k|2048|4096|8192)\b/i.test(source) &&
699
+ /\b(?:export\.image\.resolution|image resolution|resolution)\b/i.test(source);
700
+
701
+ return hasImageDecoder && hasDimensionRead && hasResolutionPreset;
702
+ }
703
+
704
+ function sourcePassesImageResolutionToPngExport(source: string): boolean {
705
+ if (
706
+ /\bcreateToolcraftPngExportCanvas\s*\(\s*\{[\s\S]*\bresolution\s*:[\s\S]{0,320}\bexport\.image\.resolution\b/.test(
707
+ source,
708
+ )
709
+ ) {
710
+ return true;
711
+ }
712
+
713
+ const runtimeResolutionNames = Array.from(
714
+ source.matchAll(
715
+ /\b(?:const|let)\s+([A-Za-z_$][A-Za-z0-9_$]*(?:Resolution|resolution)[A-Za-z0-9_$]*)\s*=[\s\S]{0,220}\bexport\.image\.resolution\b/g,
716
+ ),
717
+ ).map((match) => match[1]);
718
+
719
+ return runtimeResolutionNames.some((name) =>
720
+ new RegExp(
721
+ `\\bcreateToolcraftPngExportCanvas\\s*\\(\\s*\\{[\\s\\S]*\\bresolution\\s*:\\s*${name}\\b`,
722
+ ).test(source),
723
+ );
724
+ }
725
+
577
726
  function getProductImplementationSource(): string {
578
727
  return `${readSourceTree(routesDir)}\n${readSourceTree(appDir)}`;
579
728
  }
@@ -2194,7 +2343,7 @@ describe("Toolcraft template app acceptance coverage", () => {
2194
2343
  ).toBeGreaterThan(0);
2195
2344
  expect(
2196
2345
  backgroundToggleTargets.length,
2197
- "Every product app with Export PNG must expose a user-facing Include background / Transparent background control.",
2346
+ 'Every product app with Export PNG must expose export.includeBackground in the required "Background" section as a Switch labeled "Include".',
2198
2347
  ).toBeGreaterThan(0);
2199
2348
 
2200
2349
  for (const target of [...backgroundColorTargets, ...backgroundToggleTargets]) {
@@ -2204,6 +2353,93 @@ describe("Toolcraft template app acceptance coverage", () => {
2204
2353
  ).toContain(target);
2205
2354
  }
2206
2355
 
2356
+ const imageExportSection = getSchemaImageExportSection();
2357
+ const imageFormatControl = getSectionControlByTarget(
2358
+ imageExportSection,
2359
+ "export.image.format",
2360
+ );
2361
+ const imageResolutionControl = getSectionControlByTarget(
2362
+ imageExportSection,
2363
+ "export.image.resolution",
2364
+ );
2365
+ const imageFormatOptionValues = getControlOptionValues(imageFormatControl);
2366
+ const imageResolutionOptionValues = getControlOptionValues(imageResolutionControl);
2367
+ const imageFormatControlId = getSectionControlIdByTarget(
2368
+ imageExportSection,
2369
+ "export.image.format",
2370
+ );
2371
+ const imageResolutionControlId = getSectionControlIdByTarget(
2372
+ imageExportSection,
2373
+ "export.image.resolution",
2374
+ );
2375
+ const imageExportHasInlinePair =
2376
+ imageFormatControlId === undefined || imageResolutionControlId === undefined
2377
+ ? false
2378
+ : imageExportSection?.layoutGroups?.some(
2379
+ (group) =>
2380
+ group.layout === "inline" &&
2381
+ group.columns === 2 &&
2382
+ group.controls.includes(imageFormatControlId) &&
2383
+ group.controls.includes(imageResolutionControlId),
2384
+ ) === true;
2385
+
2386
+ expect(
2387
+ imageExportSection,
2388
+ 'Apps with Export PNG must expose image settings in a separate controls section titled "Image Export".',
2389
+ ).toBeDefined();
2390
+ expect(
2391
+ imageFormatControl,
2392
+ 'The separate "Image Export" section must include a format control with target "export.image.format".',
2393
+ ).toBeDefined();
2394
+ expect(
2395
+ imageFormatControl?.type,
2396
+ "Image Export format must use the same Select/dropdown structure as Video Export.",
2397
+ ).toBe("select");
2398
+ expect(
2399
+ imageFormatOptionValues,
2400
+ 'Image format options must include "png" and "jpg".',
2401
+ ).toEqual(expect.arrayContaining(["png", "jpg"]));
2402
+ expect(
2403
+ imageFormatControl?.defaultValue,
2404
+ 'Image format must default to "png".',
2405
+ ).toBe("png");
2406
+ expect(
2407
+ imageResolutionControl,
2408
+ 'The separate "Image Export" section must include a resolution control with target "export.image.resolution".',
2409
+ ).toBeDefined();
2410
+ expect(
2411
+ imageResolutionControl?.type,
2412
+ "Image Export resolution must use the same Select/dropdown structure as Video Export.",
2413
+ ).toBe("select");
2414
+ expect(
2415
+ imageResolutionOptionValues,
2416
+ 'Image resolution options must include "2k", "4k", and "8k".',
2417
+ ).toEqual(expect.arrayContaining(["2k", "4k", "8k"]));
2418
+ expect(
2419
+ imageResolutionControl?.defaultValue,
2420
+ 'Image resolution must default to "4k".',
2421
+ ).toBe("4k");
2422
+ expect(
2423
+ imageExportHasInlinePair,
2424
+ "Image Export format and resolution must render as a compact inline pair.",
2425
+ ).toBe(true);
2426
+ expect(
2427
+ productRuntimeImplementationSource,
2428
+ 'Image export implementation must read "export.image.format" from runtime state; declaring the control is not enough.',
2429
+ ).toContain("export.image.format");
2430
+ expect(
2431
+ productRuntimeImplementationSource,
2432
+ 'Image export implementation must read "export.image.resolution" from runtime state; declaring the control is not enough.',
2433
+ ).toContain("export.image.resolution");
2434
+ expect(
2435
+ sourcePassesImageResolutionToPngExport(productImplementationSource),
2436
+ "Image export must pass the selected image resolution to createToolcraftPngExportCanvas so 2K/4K/8K change the actual exported pixel dimensions.",
2437
+ ).toBe(true);
2438
+ expect(
2439
+ sourceHasImageExportDimensionCoverage(browserTestSources),
2440
+ "Image export browser coverage must decode the exported image and assert actual width/height for selected 2K/4K/8K resolution. Blob size or a clicked button alone does not prove export dimensions.",
2441
+ ).toBe(true);
2442
+
2207
2443
  if (!schemaHasAnimatedProductOutput()) {
2208
2444
  return;
2209
2445
  }
@@ -2434,13 +2670,13 @@ describe("Toolcraft template app acceptance coverage", () => {
2434
2670
  ).toEqual(
2435
2671
  expect.arrayContaining([
2436
2672
  expect.stringContaining("must expose a user-facing background color control"),
2437
- expect.stringContaining("must expose a user-facing Include background / Transparent background control"),
2673
+ expect.stringContaining('must expose export.includeBackground inside the required "Background" section'),
2438
2674
  ]),
2439
2675
  );
2440
2676
  });
2441
2677
 
2442
- it("accepts png export apps that wire background controls into the schema", () => {
2443
- const schemaWithBackgroundControls = defineToolcraft({
2678
+ it("rejects legacy output sections that mix background controls with export actions", () => {
2679
+ const schemaWithLegacyBackgroundControls = defineToolcraft({
2444
2680
  canvas: {
2445
2681
  enabled: true,
2446
2682
  sizing: { mode: "editable-output" },
@@ -2481,6 +2717,72 @@ describe("Toolcraft template app acceptance coverage", () => {
2481
2717
  },
2482
2718
  },
2483
2719
  });
2720
+
2721
+ expect(
2722
+ validateToolcraftAcceptanceCoverage(schemaWithLegacyBackgroundControls, [
2723
+ makeControlAcceptance("appearance.background", "color"),
2724
+ makeControlAcceptance("export.includeBackground", "switch"),
2725
+ {
2726
+ actionCoverage: ["export.png"],
2727
+ automated: true,
2728
+ automatedTestName: "exports png output",
2729
+ browser: true,
2730
+ browserTestName: "browser: exports png output",
2731
+ componentType: "panelActions",
2732
+ evidence: "exported-bytes",
2733
+ expectedObservable: "Export PNG creates output bytes.",
2734
+ fixture: "export fixture",
2735
+ id: "actions.output",
2736
+ kind: "control",
2737
+ target: "actions.output",
2738
+ userAction: "Click Export PNG.",
2739
+ },
2740
+ ]),
2741
+ ).toEqual(
2742
+ expect.arrayContaining([
2743
+ expect.stringContaining('separate controls section titled "Background"'),
2744
+ expect.stringContaining(
2745
+ 'The "Background" section must contain export.includeBackground as the Include switch.',
2746
+ ),
2747
+ expect.stringContaining(
2748
+ 'The "Background" section must contain the renderer-owned background color control',
2749
+ ),
2750
+ ]),
2751
+ );
2752
+ });
2753
+
2754
+ it("accepts png export apps that wire background controls into the schema", () => {
2755
+ const schemaWithBackgroundControls = defineToolcraft({
2756
+ canvas: {
2757
+ enabled: true,
2758
+ sizing: { mode: "editable-output" },
2759
+ },
2760
+ panels: {
2761
+ controls: {
2762
+ sections: [
2763
+ makeBackgroundSection(),
2764
+ makeImageExportSection(),
2765
+ {
2766
+ controls: {
2767
+ outputActions: {
2768
+ actions: [
2769
+ {
2770
+ icon: "export",
2771
+ label: "Export PNG",
2772
+ value: "export.png",
2773
+ },
2774
+ ],
2775
+ target: "actions.output",
2776
+ type: "panelActions",
2777
+ },
2778
+ },
2779
+ title: "Output",
2780
+ },
2781
+ ],
2782
+ title: "Controls",
2783
+ },
2784
+ },
2785
+ });
2484
2786
  const errors = validateToolcraftAcceptanceCoverage(schemaWithBackgroundControls, [
2485
2787
  makeControlAcceptance("appearance.background", "color"),
2486
2788
  {
@@ -2511,7 +2813,7 @@ describe("Toolcraft template app acceptance coverage", () => {
2511
2813
  expect(errors).not.toEqual(
2512
2814
  expect.arrayContaining([
2513
2815
  expect.stringContaining("must expose a user-facing background color control"),
2514
- expect.stringContaining("must expose a user-facing Include background / Transparent background control"),
2816
+ expect.stringContaining('must expose export.includeBackground inside the required "Background" section'),
2515
2817
  ]),
2516
2818
  );
2517
2819
  });
@@ -2525,20 +2827,10 @@ describe("Toolcraft template app acceptance coverage", () => {
2525
2827
  panels: {
2526
2828
  controls: {
2527
2829
  sections: [
2830
+ makeBackgroundSection(),
2831
+ makeImageExportSection(),
2528
2832
  {
2529
2833
  controls: {
2530
- background: {
2531
- defaultValue: "#ffffff",
2532
- label: "Background",
2533
- target: "appearance.background",
2534
- type: "color",
2535
- },
2536
- includeBackground: {
2537
- defaultValue: true,
2538
- label: "Include background",
2539
- target: "export.includeBackground",
2540
- type: "switch",
2541
- },
2542
2834
  outputActions: {
2543
2835
  actions: [
2544
2836
  {
@@ -2589,6 +2881,314 @@ describe("Toolcraft template app acceptance coverage", () => {
2589
2881
  );
2590
2882
  });
2591
2883
 
2884
+ it("requires still-output apps to expose image export format and resolution settings", () => {
2885
+ const schemaWithoutImageExportSettings = defineToolcraft({
2886
+ canvas: {
2887
+ enabled: true,
2888
+ sizing: { mode: "editable-output" },
2889
+ },
2890
+ panels: {
2891
+ controls: {
2892
+ sections: [
2893
+ makeBackgroundSection(),
2894
+ {
2895
+ actionGroup: "secondary",
2896
+ controls: {
2897
+ outputActions: {
2898
+ actions: [
2899
+ {
2900
+ icon: "export",
2901
+ label: "Export PNG",
2902
+ value: "export.png",
2903
+ },
2904
+ ],
2905
+ target: "actions.output",
2906
+ type: "panelActions",
2907
+ },
2908
+ },
2909
+ title: "Export",
2910
+ },
2911
+ ],
2912
+ title: "Controls",
2913
+ },
2914
+ },
2915
+ });
2916
+
2917
+ expect(
2918
+ validateToolcraftAcceptanceCoverage(schemaWithoutImageExportSettings, [
2919
+ makeControlAcceptance("appearance.background", "color"),
2920
+ makeControlAcceptance("export.includeBackground", "switch"),
2921
+ {
2922
+ actionCoverage: ["export.png"],
2923
+ automated: true,
2924
+ automatedTestName: "exports image output",
2925
+ browser: true,
2926
+ browserTestName: "browser: exports image output",
2927
+ componentType: "panelActions",
2928
+ evidence: "exported-bytes",
2929
+ expectedObservable:
2930
+ "Export PNG creates output bytes and reads image format, image resolution, background color, and include-background state.",
2931
+ fixture: "export fixture",
2932
+ id: "actions.output",
2933
+ kind: "control",
2934
+ target: "actions.output",
2935
+ userAction:
2936
+ "Toggle Include background off, export PNG, then verify the PNG has alpha while the live preview canvas still preserves the background.",
2937
+ },
2938
+ ]),
2939
+ ).toEqual(
2940
+ expect.arrayContaining([
2941
+ 'Apps with Export PNG must expose image export settings in a separate controls section titled "Image Export" directly above sticky footer export actions or directly before "Video Export" when video export also exists.',
2942
+ 'The separate "Image Export" section must include a format control with target "export.image.format".',
2943
+ 'The separate "Image Export" section must include a resolution control with target "export.image.resolution".',
2944
+ ]),
2945
+ );
2946
+ });
2947
+
2948
+ it("accepts still-output apps with image export settings", () => {
2949
+ const schemaWithImageExportSettings = defineToolcraft({
2950
+ canvas: {
2951
+ enabled: true,
2952
+ sizing: { mode: "editable-output" },
2953
+ },
2954
+ panels: {
2955
+ controls: {
2956
+ sections: [
2957
+ makeBackgroundSection(),
2958
+ makeImageExportSection(),
2959
+ {
2960
+ actionGroup: "secondary",
2961
+ controls: {
2962
+ outputActions: {
2963
+ actions: [
2964
+ {
2965
+ icon: "export",
2966
+ label: "Export PNG",
2967
+ value: "export.png",
2968
+ },
2969
+ ],
2970
+ target: "actions.output",
2971
+ type: "panelActions",
2972
+ },
2973
+ },
2974
+ title: "Export",
2975
+ },
2976
+ ],
2977
+ title: "Controls",
2978
+ },
2979
+ },
2980
+ });
2981
+ const imageFormatAcceptance = makeControlAcceptance("export.image.format", "select");
2982
+ imageFormatAcceptance.optionCoverage = ["png", "jpg"];
2983
+ imageFormatAcceptance.expectedObservable =
2984
+ "PNG and JPG choices change the exported image MIME/file extension.";
2985
+ imageFormatAcceptance.userAction =
2986
+ "Choose PNG and JPG, export the image, and verify the blob type or file extension changes.";
2987
+ const imageResolutionAcceptance = makeControlAcceptance(
2988
+ "export.image.resolution",
2989
+ "select",
2990
+ );
2991
+ imageResolutionAcceptance.optionCoverage = ["2k", "4k", "8k"];
2992
+ imageResolutionAcceptance.expectedObservable =
2993
+ "Resolution choices change the actual exported image dimensions.";
2994
+ imageResolutionAcceptance.userAction =
2995
+ "Choose 2K and 8K, export each image, decode it, and compare actual pixel width/height.";
2996
+
2997
+ expect(
2998
+ validateToolcraftAcceptanceCoverage(schemaWithImageExportSettings, [
2999
+ makeControlAcceptance("appearance.background", "color"),
3000
+ makeControlAcceptance("export.includeBackground", "switch"),
3001
+ imageFormatAcceptance,
3002
+ imageResolutionAcceptance,
3003
+ {
3004
+ actionCoverage: ["export.png"],
3005
+ automated: true,
3006
+ automatedTestName: "exports image output",
3007
+ browser: true,
3008
+ browserTestName: "browser: exports image output",
3009
+ componentType: "panelActions",
3010
+ evidence: "exported-bytes",
3011
+ expectedObservable:
3012
+ "Export PNG creates output bytes and reads image format, image resolution, background color, and include-background state. JPG changes file type; 8K changes exported pixel dimensions to an 8192px long edge.",
3013
+ fixture: "export fixture",
3014
+ id: "actions.output",
3015
+ kind: "control",
3016
+ target: "actions.output",
3017
+ userAction:
3018
+ "Set format to JPG and resolution to 8K, then export and decode the output image to verify file type and long-edge dimensions.",
3019
+ },
3020
+ ]),
3021
+ ).not.toEqual(
3022
+ expect.arrayContaining([
3023
+ expect.stringContaining("Image Export"),
3024
+ expect.stringContaining("export.image.format"),
3025
+ expect.stringContaining("export.image.resolution"),
3026
+ ]),
3027
+ );
3028
+ });
3029
+
3030
+ it("requires animated apps with PNG and video actions to expose Image Export before Video Export", () => {
3031
+ const schemaWithoutImageSettings = defineToolcraft({
3032
+ canvas: {
3033
+ enabled: true,
3034
+ sizing: { mode: "editable-output" },
3035
+ },
3036
+ panels: {
3037
+ controls: {
3038
+ sections: [
3039
+ makeBackgroundSection(),
3040
+ makeVideoExportSection(),
3041
+ {
3042
+ actionGroup: "secondary",
3043
+ controls: {
3044
+ outputActions: {
3045
+ actions: [
3046
+ {
3047
+ icon: "download",
3048
+ label: "Export Video",
3049
+ value: "export.video",
3050
+ },
3051
+ {
3052
+ icon: "export",
3053
+ label: "Export PNG",
3054
+ value: "export.png",
3055
+ },
3056
+ ],
3057
+ target: "actions.output",
3058
+ type: "panelActions",
3059
+ },
3060
+ },
3061
+ title: "Export",
3062
+ },
3063
+ ],
3064
+ title: "Controls",
3065
+ },
3066
+ timeline: { enabled: true, mode: "playback" },
3067
+ },
3068
+ });
3069
+
3070
+ expect(
3071
+ validateToolcraftAcceptanceCoverage(schemaWithoutImageSettings, [
3072
+ makeControlAcceptance("appearance.background", "color"),
3073
+ makeControlAcceptance("export.includeBackground", "switch"),
3074
+ makeControlAcceptance("export.video.format", "select"),
3075
+ makeControlAcceptance("export.video.resolution", "select"),
3076
+ {
3077
+ actionCoverage: ["export.video", "export.png"],
3078
+ automated: true,
3079
+ automatedTestName: "exports animated and still output",
3080
+ browser: true,
3081
+ browserTestName: "browser: exports video and image output",
3082
+ componentType: "panelActions",
3083
+ evidence: "exported-bytes",
3084
+ expectedObservable:
3085
+ "Export Video and Export PNG both create output bytes and read their runtime export settings.",
3086
+ fixture: "animated export fixture",
3087
+ id: "actions.output",
3088
+ kind: "control",
3089
+ target: "actions.output",
3090
+ userAction:
3091
+ "Export video and PNG from the same timeline state, then verify both files exist.",
3092
+ },
3093
+ ]),
3094
+ ).toEqual(
3095
+ expect.arrayContaining([
3096
+ 'Apps with Export PNG must expose image export settings in a separate controls section titled "Image Export" directly above sticky footer export actions or directly before "Video Export" when video export also exists.',
3097
+ 'The separate "Image Export" section must include a format control with target "export.image.format".',
3098
+ 'The separate "Image Export" section must include a resolution control with target "export.image.resolution".',
3099
+ ]),
3100
+ );
3101
+ });
3102
+
3103
+ it("accepts animated apps with Image Export immediately before Video Export", () => {
3104
+ const schemaWithDualExportSettings = defineToolcraft({
3105
+ canvas: {
3106
+ enabled: true,
3107
+ sizing: { mode: "editable-output" },
3108
+ },
3109
+ panels: {
3110
+ controls: {
3111
+ sections: [
3112
+ makeBackgroundSection(),
3113
+ makeImageExportSection(),
3114
+ makeVideoExportSection(),
3115
+ {
3116
+ actionGroup: "secondary",
3117
+ controls: {
3118
+ outputActions: {
3119
+ actions: [
3120
+ {
3121
+ icon: "download",
3122
+ label: "Export Video",
3123
+ value: "export.video",
3124
+ },
3125
+ {
3126
+ icon: "export",
3127
+ label: "Export PNG",
3128
+ value: "export.png",
3129
+ },
3130
+ ],
3131
+ target: "actions.output",
3132
+ type: "panelActions",
3133
+ },
3134
+ },
3135
+ title: "Export",
3136
+ },
3137
+ ],
3138
+ title: "Controls",
3139
+ },
3140
+ timeline: { enabled: true, mode: "playback" },
3141
+ },
3142
+ });
3143
+ const imageFormatAcceptance = makeControlAcceptance("export.image.format", "select");
3144
+ imageFormatAcceptance.optionCoverage = ["png", "jpg"];
3145
+ const imageResolutionAcceptance = makeControlAcceptance(
3146
+ "export.image.resolution",
3147
+ "select",
3148
+ );
3149
+ imageResolutionAcceptance.optionCoverage = ["2k", "4k", "8k"];
3150
+ const videoFormatAcceptance = makeControlAcceptance("export.video.format", "select");
3151
+ videoFormatAcceptance.optionCoverage = ["mp4", "webm"];
3152
+ const videoResolutionAcceptance = makeControlAcceptance(
3153
+ "export.video.resolution",
3154
+ "select",
3155
+ );
3156
+ videoResolutionAcceptance.optionCoverage = ["current", "4k"];
3157
+
3158
+ expect(
3159
+ validateToolcraftAcceptanceCoverage(schemaWithDualExportSettings, [
3160
+ makeControlAcceptance("appearance.background", "color"),
3161
+ makeControlAcceptance("export.includeBackground", "switch"),
3162
+ imageFormatAcceptance,
3163
+ imageResolutionAcceptance,
3164
+ videoFormatAcceptance,
3165
+ videoResolutionAcceptance,
3166
+ {
3167
+ actionCoverage: ["export.video", "export.png"],
3168
+ automated: true,
3169
+ automatedTestName: "exports animated and still output",
3170
+ browser: true,
3171
+ browserTestName: "browser: exports video and image output",
3172
+ componentType: "panelActions",
3173
+ evidence: "exported-bytes",
3174
+ expectedObservable:
3175
+ "Export Video and Export PNG both create output bytes and read their runtime export settings.",
3176
+ fixture: "animated export fixture",
3177
+ id: "actions.output",
3178
+ kind: "control",
3179
+ target: "actions.output",
3180
+ userAction:
3181
+ "Choose JPG and 8K, choose MP4 and Current, then export PNG and video and verify both outputs use their selected settings.",
3182
+ },
3183
+ ]),
3184
+ ).not.toEqual(
3185
+ expect.arrayContaining([
3186
+ expect.stringContaining("Image Export"),
3187
+ expect.stringContaining("Video Export"),
3188
+ ]),
3189
+ );
3190
+ });
3191
+
2592
3192
  it("requires disabledWhen controls to reference a real target and prove disabled behavior", () => {
2593
3193
  const schemaWithDisabledDependency = defineToolcraft({
2594
3194
  canvas: { enabled: true },
@@ -2923,13 +3523,13 @@ describe("Toolcraft template app acceptance coverage", () => {
2923
3523
  ).toEqual(
2924
3524
  expect.arrayContaining([
2925
3525
  expect.stringContaining(
2926
- 'export.includeBackground) toggle label "Background" duplicates section title "Background". Use label false for a visual-only toggle or rename the toggle to a more specific setting.',
3526
+ 'export.includeBackground) toggle label "Background" duplicates section title "Background". Use a shorter contextual label such as "Include" or rename the toggle to a more specific setting.',
2927
3527
  ),
2928
3528
  ]),
2929
3529
  );
2930
3530
  });
2931
3531
 
2932
- it("allows hidden-label toggle plus one related parameter in a section-owned inline row", () => {
3532
+ it("allows short Include toggle plus an unlabeled background color in the required row", () => {
2933
3533
  const schema = defineToolcraft({
2934
3534
  canvas: { enabled: true },
2935
3535
  panels: {
@@ -2941,7 +3541,7 @@ describe("Toolcraft template app acceptance coverage", () => {
2941
3541
  defaultValue: true,
2942
3542
  description:
2943
3543
  "Controls PNG background transparency while preview and video keep the background.",
2944
- label: false,
3544
+ label: "Include",
2945
3545
  target: "export.includeBackground",
2946
3546
  type: "switch",
2947
3547
  },
@@ -2976,6 +3576,8 @@ describe("Toolcraft template app acceptance coverage", () => {
2976
3576
  expect.arrayContaining([
2977
3577
  expect.stringContaining("duplicates section title"),
2978
3578
  expect.stringContaining("too long for a two-column toggle row"),
3579
+ expect.stringContaining("must use the short visible label"),
3580
+ expect.stringContaining("must use label false"),
2979
3581
  ]),
2980
3582
  );
2981
3583
  });
@@ -5284,7 +5886,7 @@ describe("Toolcraft template app acceptance coverage", () => {
5284
5886
  defaultValue: "uppercase",
5285
5887
  label: "Case",
5286
5888
  options: [
5287
- { label: "Original", value: "original" },
5889
+ { label: "As typed", value: "original" },
5288
5890
  { label: "Uppercase", value: "uppercase" },
5289
5891
  ],
5290
5892
  orderRole: "primary",