@pixel-point/toolcraft 0.0.6 → 0.0.8

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 (48) hide show
  1. package/package.json +1 -1
  2. package/src/generate.test.mjs +1 -1
  3. package/templates/runtime/contracts/component-contracts.test.ts +123 -15
  4. package/templates/runtime/contracts/component-contracts.ts +93 -19
  5. package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
  6. package/templates/runtime/contracts/decision-contracts.ts +2 -2
  7. package/templates/runtime/export/export.test.ts +31 -0
  8. package/templates/runtime/export/export.ts +41 -0
  9. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  10. package/templates/runtime/react/canvas-shell.tsx +178 -23
  11. package/templates/runtime/react/control-conditions.ts +166 -0
  12. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  13. package/templates/runtime/react/controls-panel.test.tsx +590 -9
  14. package/templates/runtime/react/controls-panel.tsx +472 -10
  15. package/templates/runtime/react/media-file.ts +19 -0
  16. package/templates/runtime/schema/define-toolcraft.test.ts +2 -0
  17. package/templates/runtime/schema/define-toolcraft.ts +11 -3
  18. package/templates/runtime/schema/types.ts +27 -0
  19. package/templates/runtime/state/reducer.test.ts +357 -0
  20. package/templates/runtime/state/reducer.ts +195 -9
  21. package/templates/runtime/state/types.ts +12 -2
  22. package/templates/runtime/testing/performance.test.ts +1282 -48
  23. package/templates/runtime/testing/performance.ts +676 -39
  24. package/templates/starter/AGENTS.md +13 -12
  25. package/templates/starter/docs/toolcraft/README.md +4 -3
  26. package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -7
  27. package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -7
  28. package/templates/starter/docs/toolcraft/component-rules.md +20 -7
  29. package/templates/starter/docs/toolcraft/custom-controls.md +9 -5
  30. package/templates/starter/docs/toolcraft/performance.md +51 -13
  31. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  32. package/templates/starter/docs/toolcraft/schema-reference.md +11 -6
  33. package/templates/starter/docs/toolcraft/workflow.md +7 -10
  34. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  35. package/templates/starter/e2e/performance-helpers.ts +197 -0
  36. package/templates/starter/package.json +4 -1
  37. package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
  38. package/templates/starter/src/app/starter-acceptance.ts +269 -12
  39. package/templates/starter/src/app/starter-performance.test.ts +67 -6
  40. package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
  41. package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
  42. package/templates/ui/components/controls/collection-actions/index.ts +4 -0
  43. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  44. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  45. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -6
  46. package/templates/ui/components/controls/index.ts +9 -0
  47. package/templates/ui/components/panel/panel-section.tsx +53 -1
  48. package/templates/ui/index.ts +1 -0
@@ -538,7 +538,7 @@ function makeBackgroundSection() {
538
538
  includeBackground: {
539
539
  defaultValue: true,
540
540
  description:
541
- "Controls PNG background transparency while preview and video keep the background.",
541
+ "Controls preview and PNG background visibility while video keeps the background.",
542
542
  label: "Include",
543
543
  target: "export.includeBackground",
544
544
  type: "switch",
@@ -734,13 +734,6 @@ function getProductRuntimeImplementationSource(): string {
734
734
  )}`;
735
735
  }
736
736
 
737
- function removePngExportCanvasHelperCalls(source: string): string {
738
- return source.replace(
739
- /\bcreateToolcraftPngExportCanvas\s*\(\s*\{[\s\S]*?\}\s*\)/g,
740
- "createToolcraftPngExportCanvas({})",
741
- );
742
- }
743
-
744
737
  function getSchemaBackgroundControlTargets(
745
738
  controlTypes: ReadonlySet<string>,
746
739
  ): string[] {
@@ -813,6 +806,101 @@ describe("Toolcraft template app acceptance coverage", () => {
813
806
  expect(validateToolcraftAcceptanceCoverage(starterSchema, starterAcceptance)).toEqual([]);
814
807
  });
815
808
 
809
+ it("requires fileDrop acceptance to prove upload, clear, and reset lifecycle", () => {
810
+ const fileDropSchema = defineToolcraft({
811
+ canvas: { enabled: true },
812
+ panels: {
813
+ controls: {
814
+ sections: [
815
+ {
816
+ controls: {
817
+ source: {
818
+ defaultValue: null,
819
+ label: "Source image",
820
+ target: "media.source",
821
+ type: "fileDrop",
822
+ },
823
+ },
824
+ title: "Source",
825
+ },
826
+ ],
827
+ title: "Controls",
828
+ },
829
+ },
830
+ });
831
+
832
+ expect(
833
+ validateToolcraftAcceptanceCoverage(fileDropSchema, [
834
+ {
835
+ automated: true,
836
+ automatedTestName: "source image upload and clear update media",
837
+ browser: true,
838
+ browserTestName: "browser: source image upload and clear update media",
839
+ componentType: "fileDrop",
840
+ evidence: "media-lifecycle",
841
+ expectedObservable:
842
+ "Uploading a source image changes the media preview and Clear removes it.",
843
+ fixture: "source image fixture",
844
+ id: "media.source",
845
+ kind: "control",
846
+ target: "media.source",
847
+ userAction: "Upload a source image, then clear the preview.",
848
+ },
849
+ ]),
850
+ ).toEqual(
851
+ expect.arrayContaining([
852
+ "Source / source (media.source) fileDrop acceptance must prove upload/import, clear/remove, and section or global reset remove source media.",
853
+ ]),
854
+ );
855
+ });
856
+
857
+ it("accepts fileDrop lifecycle coverage that includes reset", () => {
858
+ const fileDropSchema = defineToolcraft({
859
+ canvas: { enabled: true },
860
+ panels: {
861
+ controls: {
862
+ sections: [
863
+ {
864
+ controls: {
865
+ source: {
866
+ defaultValue: null,
867
+ label: "Source image",
868
+ target: "media.source",
869
+ type: "fileDrop",
870
+ },
871
+ },
872
+ title: "Source",
873
+ },
874
+ ],
875
+ title: "Controls",
876
+ },
877
+ },
878
+ });
879
+
880
+ const errors = validateToolcraftAcceptanceCoverage(fileDropSchema, [
881
+ {
882
+ automated: true,
883
+ automatedTestName: "source image upload clear and reset update media",
884
+ browser: true,
885
+ browserTestName: "browser: source image upload clear and reset update media",
886
+ componentType: "fileDrop",
887
+ evidence: "media-lifecycle",
888
+ expectedObservable:
889
+ "Uploading a source image changes the media preview; Clear, section reset, and global Reset controls remove the source media and restore the empty upload state.",
890
+ fixture: "source image fixture",
891
+ id: "media.source",
892
+ kind: "control",
893
+ target: "media.source",
894
+ userAction:
895
+ "Upload a source image, clear it, upload again, then use section reset and Reset controls.",
896
+ },
897
+ ]);
898
+
899
+ expect(
900
+ errors.filter((error) => error.includes("fileDrop acceptance")),
901
+ ).toEqual([]);
902
+ });
903
+
816
904
  it("rejects fake video duration browser coverage that falls back to expected duration", () => {
817
905
  const fakeCoverage = `
818
906
  async function getVideoDuration(page, buffer, mimeType) {
@@ -1629,9 +1717,259 @@ describe("Toolcraft template app acceptance coverage", () => {
1629
1717
  ).toEqual([
1630
1718
  "Glyphs / glyphRamp (glyph.ramp) builtInFitCheck.checkedBuiltIns contains unknown built-in controls: imaginaryPicker.",
1631
1719
  'Glyphs / glyphRamp (glyph.ramp) builtInFitCheck.closestBuiltIn must be one of the checked built-ins or "none".',
1720
+ "Glyphs / glyphRamp (glyph.ramp) builtInFitCheck.checkedBuiltIns must include collectionActions when the custom control owns a growable, removable, selectable, or reorderable runtime item set.",
1721
+ "Glyphs / glyphRamp (glyph.ramp) builtInFitCheck.checkedBuiltIns must include actions when the custom control exposes local command buttons such as add, remove, delete, duplicate, sort, normalize, or clear.",
1632
1722
  ]);
1633
1723
  });
1634
1724
 
1725
+ it("requires collection-like custom controls to compare actions and collectionActions", () => {
1726
+ const schema = defineToolcraft({
1727
+ canvas: { enabled: true },
1728
+ panels: {
1729
+ controls: {
1730
+ sections: [
1731
+ {
1732
+ controls: {
1733
+ maskEditor: {
1734
+ defaultValue: { items: [], selectedId: null },
1735
+ label: "Mask shapes",
1736
+ orderRole: "spatial",
1737
+ target: "masks",
1738
+ type: "maskEditor",
1739
+ } as never,
1740
+ },
1741
+ title: "Masks",
1742
+ },
1743
+ ],
1744
+ title: "Controls",
1745
+ },
1746
+ },
1747
+ });
1748
+
1749
+ expect(
1750
+ validateToolcraftAcceptanceCoverage(schema, [
1751
+ {
1752
+ automated: true,
1753
+ automatedTestName: "mask editor changes output",
1754
+ browser: true,
1755
+ browserTestName: "browser: mask editor changes output",
1756
+ builtInFitCheck: {
1757
+ checkedBuiltIns: ["select", "vector"],
1758
+ closestBuiltIn: "vector",
1759
+ productObservable:
1760
+ "Adding, selecting, deleting, dragging, and resizing masks changes the composited output.",
1761
+ whyInsufficient:
1762
+ "Vector can edit one point, but this product needs multiple shape commands, selected item state, deletion, drag, and resize handles.",
1763
+ },
1764
+ componentType: "maskEditor",
1765
+ customControlCoverage: [
1766
+ "built-in-gap",
1767
+ "kit-primitives",
1768
+ "minimal-ui",
1769
+ "product-output",
1770
+ "runtime-state",
1771
+ ],
1772
+ evidence: "product-output",
1773
+ expectedObservable:
1774
+ "Adding rectangle, circle, and triangle masks creates selected editable shapes.",
1775
+ fixture: "uploaded image with masks",
1776
+ id: "masks",
1777
+ kind: "control",
1778
+ target: "masks",
1779
+ userAction:
1780
+ "Add each mask shape, select a mask in the list, delete one mask, drag a canvas mask handle, and resize a selected mask.",
1781
+ },
1782
+ ]),
1783
+ ).toEqual([
1784
+ "Masks / maskEditor (masks) builtInFitCheck.checkedBuiltIns must include collectionActions when the custom control owns a growable, removable, selectable, or reorderable runtime item set.",
1785
+ "Masks / maskEditor (masks) builtInFitCheck.checkedBuiltIns must include actions when the custom control exposes local command buttons such as add, remove, delete, duplicate, sort, normalize, or clear.",
1786
+ ]);
1787
+ });
1788
+
1789
+ it("detects collection-like custom controls from the value model instead of entity names", () => {
1790
+ const schema = defineToolcraft({
1791
+ canvas: { enabled: true },
1792
+ panels: {
1793
+ controls: {
1794
+ sections: [
1795
+ {
1796
+ controls: {
1797
+ patternSet: {
1798
+ defaultValue: { entries: [], selectedId: null },
1799
+ label: "Pattern set",
1800
+ orderRole: "style",
1801
+ target: "pattern.set",
1802
+ type: "patternSetEditor",
1803
+ } as never,
1804
+ },
1805
+ title: "Pattern",
1806
+ },
1807
+ ],
1808
+ title: "Controls",
1809
+ },
1810
+ },
1811
+ });
1812
+
1813
+ expect(
1814
+ validateToolcraftAcceptanceCoverage(schema, [
1815
+ {
1816
+ automated: true,
1817
+ automatedTestName: "pattern set changes output",
1818
+ browser: true,
1819
+ browserTestName: "browser: pattern set changes output",
1820
+ builtInFitCheck: {
1821
+ checkedBuiltIns: ["select", "vector"],
1822
+ closestBuiltIn: "select",
1823
+ productObservable:
1824
+ "Editing the chosen pattern entry changes the rendered pattern output.",
1825
+ whyInsufficient:
1826
+ "Select can choose one preset, but this product needs runtime entry state with selected item editing.",
1827
+ },
1828
+ componentType: "patternSetEditor",
1829
+ customControlCoverage: [
1830
+ "built-in-gap",
1831
+ "kit-primitives",
1832
+ "minimal-ui",
1833
+ "product-output",
1834
+ "runtime-state",
1835
+ ],
1836
+ evidence: "product-output",
1837
+ expectedObservable:
1838
+ "Editing the pattern set changes the rendered product output.",
1839
+ fixture: "pattern set fixture",
1840
+ id: "pattern.set",
1841
+ kind: "control",
1842
+ target: "pattern.set",
1843
+ userAction: "Edit the selected entry.",
1844
+ },
1845
+ ]),
1846
+ ).toEqual([
1847
+ "Pattern / patternSet (pattern.set) builtInFitCheck.checkedBuiltIns must include collectionActions when the custom control owns a growable, removable, selectable, or reorderable runtime item set.",
1848
+ ]);
1849
+ });
1850
+
1851
+ it("rejects custom controls justified only by visual chrome", () => {
1852
+ const schema = defineToolcraft({
1853
+ canvas: { enabled: true },
1854
+ panels: {
1855
+ controls: {
1856
+ sections: [
1857
+ {
1858
+ controls: {
1859
+ shapeButtons: {
1860
+ defaultValue: "rect",
1861
+ label: "Shape",
1862
+ orderRole: "style",
1863
+ target: "shape.kind",
1864
+ type: "shapeButtons",
1865
+ } as never,
1866
+ },
1867
+ title: "Shape",
1868
+ },
1869
+ ],
1870
+ title: "Controls",
1871
+ },
1872
+ },
1873
+ });
1874
+
1875
+ expect(
1876
+ validateToolcraftAcceptanceCoverage(schema, [
1877
+ {
1878
+ automated: true,
1879
+ automatedTestName: "shape buttons change output",
1880
+ browser: true,
1881
+ browserTestName: "browser: shape buttons change output",
1882
+ builtInFitCheck: {
1883
+ checkedBuiltIns: ["actions", "segmented", "select"],
1884
+ closestBuiltIn: "segmented",
1885
+ productObservable:
1886
+ "Choosing a shape changes the rendered product geometry.",
1887
+ whyInsufficient:
1888
+ "The custom control uses icon buttons and a compact visual layout.",
1889
+ },
1890
+ componentType: "shapeButtons",
1891
+ customControlCoverage: [
1892
+ "built-in-gap",
1893
+ "kit-primitives",
1894
+ "minimal-ui",
1895
+ "product-output",
1896
+ "runtime-state",
1897
+ ],
1898
+ evidence: "product-output",
1899
+ expectedObservable:
1900
+ "Choosing rectangle, circle, or triangle changes the rendered output.",
1901
+ fixture: "shape fixture",
1902
+ id: "shape.kind",
1903
+ kind: "control",
1904
+ target: "shape.kind",
1905
+ userAction: "Choose each shape icon button.",
1906
+ },
1907
+ ]),
1908
+ ).toEqual([
1909
+ "Shape / shapeButtons (shape.kind) builtInFitCheck.whyInsufficient cannot justify a custom control only with icons, layout, styling, or custom buttons; name the product interaction or value model that built-ins cannot express.",
1910
+ ]);
1911
+ });
1912
+
1913
+ it("does not treat numeric tuple custom values as collection owners", () => {
1914
+ const schema = defineToolcraft({
1915
+ canvas: { enabled: true },
1916
+ panels: {
1917
+ controls: {
1918
+ sections: [
1919
+ {
1920
+ controls: {
1921
+ focalPoint: {
1922
+ defaultValue: [0.5, 0.5],
1923
+ label: "Focal point",
1924
+ orderRole: "spatial",
1925
+ target: "focal.point",
1926
+ type: "focalPointPad",
1927
+ } as never,
1928
+ },
1929
+ title: "Focus",
1930
+ },
1931
+ ],
1932
+ title: "Controls",
1933
+ },
1934
+ },
1935
+ });
1936
+
1937
+ expect(
1938
+ validateToolcraftAcceptanceCoverage(schema, [
1939
+ {
1940
+ automated: true,
1941
+ automatedTestName: "focal point changes output",
1942
+ browser: true,
1943
+ browserTestName: "browser: focal point changes output",
1944
+ builtInFitCheck: {
1945
+ checkedBuiltIns: ["vector"],
1946
+ closestBuiltIn: "vector",
1947
+ productObservable:
1948
+ "Dragging the focal point changes the rendered focus position.",
1949
+ whyInsufficient:
1950
+ "Vector edits x/y, but this product needs a canvas hit target with validation tied to the rendered focal point.",
1951
+ },
1952
+ componentType: "focalPointPad",
1953
+ customControlCoverage: [
1954
+ "built-in-gap",
1955
+ "kit-primitives",
1956
+ "minimal-ui",
1957
+ "product-output",
1958
+ "runtime-state",
1959
+ ],
1960
+ evidence: "product-output",
1961
+ expectedObservable:
1962
+ "Dragging the focal point changes the rendered product output.",
1963
+ fixture: "focus fixture",
1964
+ id: "focal.point",
1965
+ kind: "control",
1966
+ target: "focal.point",
1967
+ userAction: "Drag the focal point.",
1968
+ },
1969
+ ]),
1970
+ ).toEqual([]);
1971
+ });
1972
+
1635
1973
  it("accepts custom controls with explicit custom control coverage", () => {
1636
1974
  const schema = defineToolcraft({
1637
1975
  canvas: { enabled: true },
@@ -1664,7 +2002,7 @@ describe("Toolcraft template app acceptance coverage", () => {
1664
2002
  browser: true,
1665
2003
  browserTestName: "browser: glyph ramp changes output",
1666
2004
  builtInFitCheck: {
1667
- checkedBuiltIns: ["fileDrop", "imagePicker", "select"],
2005
+ checkedBuiltIns: ["actions", "collectionActions", "fileDrop", "imagePicker", "select"],
1668
2006
  closestBuiltIn: "fileDrop",
1669
2007
  productObservable:
1670
2008
  "Ordering uploaded glyphs changes the rendered glyph ramp output.",
@@ -2334,9 +2672,9 @@ describe("Toolcraft template app acceptance coverage", () => {
2334
2672
  "PNG export must pass includeBackground from runtime state to createToolcraftPngExportCanvas; do not hardcode PNG transparency or background inclusion in schema only.",
2335
2673
  ).toMatch(/\bcreateToolcraftPngExportCanvas\s*\(\s*\{[\s\S]*\bincludeBackground\s*:/);
2336
2674
  expect(
2337
- removePngExportCanvasHelperCalls(productRuntimeImplementationSource),
2338
- "export.includeBackground may only be read by the PNG export helper. Live preview, workspace canvas backing, and video renderers must not become transparent when Include background is off.",
2339
- ).not.toMatch(/\bexport\.includeBackground\b/);
2675
+ productRuntimeImplementationSource,
2676
+ "Live preview must read include-background state through shouldIncludeToolcraftPreviewBackground(state) so turning Include off hides the product preview background without affecting video export.",
2677
+ ).toMatch(/\bshouldIncludeToolcraftPreviewBackground\b/);
2340
2678
  expect(
2341
2679
  backgroundColorTargets.length,
2342
2680
  "Every product app with Export PNG must expose a user-facing background color control.",
@@ -2346,13 +2684,18 @@ describe("Toolcraft template app acceptance coverage", () => {
2346
2684
  'Every product app with Export PNG must expose export.includeBackground in the required "Background" section as a Switch labeled "Include".',
2347
2685
  ).toBeGreaterThan(0);
2348
2686
 
2349
- for (const target of [...backgroundColorTargets, ...backgroundToggleTargets]) {
2687
+ for (const target of backgroundColorTargets) {
2350
2688
  expect(
2351
2689
  productRuntimeImplementationSource,
2352
2690
  `Runtime renderer/export code must read ${target}; declaring the control in schema is not enough.`,
2353
2691
  ).toContain(target);
2354
2692
  }
2355
2693
 
2694
+ expect(
2695
+ productRuntimeImplementationSource,
2696
+ `Runtime renderer/export code must read ${backgroundToggleTargets.join(", ")} through shouldIncludeToolcraftPreviewBackground(state); declaring the control in schema is not enough.`,
2697
+ ).toMatch(/\bshouldIncludeToolcraftPreviewBackground\b|\bexport\.includeBackground\b/);
2698
+
2356
2699
  const imageExportSection = getSchemaImageExportSection();
2357
2700
  const imageFormatControl = getSectionControlByTarget(
2358
2701
  imageExportSection,
@@ -2788,9 +3131,9 @@ describe("Toolcraft template app acceptance coverage", () => {
2788
3131
  {
2789
3132
  ...makeControlAcceptance("export.includeBackground", "switch"),
2790
3133
  expectedObservable:
2791
- "Turning Include background off makes PNG output transparent while live preview, workspace canvas backing, and video output keep the product background.",
3134
+ "Turning Include off makes PNG output transparent, hides the live preview product background, and video output keeps the product background.",
2792
3135
  userAction:
2793
- "Toggle Include background off, export PNG, then verify the PNG has alpha while the live preview canvas still preserves the background.",
3136
+ "Toggle Include off, verify preview has no product background, export PNG with alpha, and verify video export keeps the background.",
2794
3137
  },
2795
3138
  {
2796
3139
  actionCoverage: ["export.png"],
@@ -2818,7 +3161,7 @@ describe("Toolcraft template app acceptance coverage", () => {
2818
3161
  );
2819
3162
  });
2820
3163
 
2821
- it("requires include-background acceptance to keep live preview and canvas backing visible", () => {
3164
+ it("requires include-background acceptance to hide preview background and keep video background", () => {
2822
3165
  const schemaWithBackgroundControls = defineToolcraft({
2823
3166
  canvas: {
2824
3167
  enabled: true,
@@ -2875,7 +3218,7 @@ describe("Toolcraft template app acceptance coverage", () => {
2875
3218
  ).toEqual(
2876
3219
  expect.arrayContaining([
2877
3220
  expect.stringContaining(
2878
- "controls PNG background inclusion and acceptance must prove disabling it makes PNG output transparent while live preview, workspace canvas backing, and video output keep the product background.",
3221
+ "controls background inclusion and acceptance must prove disabling it makes PNG output transparent, hides the live preview product background, and keeps video output with the product background.",
2879
3222
  ),
2880
3223
  ]),
2881
3224
  );
@@ -2933,7 +3276,7 @@ describe("Toolcraft template app acceptance coverage", () => {
2933
3276
  kind: "control",
2934
3277
  target: "actions.output",
2935
3278
  userAction:
2936
- "Toggle Include background off, export PNG, then verify the PNG has alpha while the live preview canvas still preserves the background.",
3279
+ "Toggle Include off, verify preview has no product background, export PNG with alpha, and verify video export keeps the background.",
2937
3280
  },
2938
3281
  ]),
2939
3282
  ).toEqual(
@@ -3540,7 +3883,7 @@ describe("Toolcraft template app acceptance coverage", () => {
3540
3883
  includeBackground: {
3541
3884
  defaultValue: true,
3542
3885
  description:
3543
- "Controls PNG background transparency while preview and video keep the background.",
3886
+ "Controls preview and PNG background visibility while video keeps the background.",
3544
3887
  label: "Include",
3545
3888
  target: "export.includeBackground",
3546
3889
  type: "switch",
@@ -6052,6 +6395,173 @@ describe("Toolcraft template app acceptance coverage", () => {
6052
6395
  );
6053
6396
  });
6054
6397
 
6398
+ it("rejects visible labels for palette variation color banks", () => {
6399
+ const schemaWithLabeledPaletteBank = defineToolcraft({
6400
+ canvas: { enabled: true },
6401
+ panels: {
6402
+ controls: {
6403
+ sections: [
6404
+ {
6405
+ controls: {
6406
+ accent1: {
6407
+ defaultValue: { hex: "#9CE6FF" },
6408
+ label: "Color 1",
6409
+ target: "palette.accent1",
6410
+ type: "color",
6411
+ },
6412
+ accent2: {
6413
+ defaultValue: { hex: "#FF7A90" },
6414
+ label: "Color 2",
6415
+ target: "palette.accent2",
6416
+ type: "color",
6417
+ },
6418
+ spread: {
6419
+ defaultValue: 34,
6420
+ label: "Spread",
6421
+ max: 100,
6422
+ min: 0,
6423
+ target: "palette.spread",
6424
+ type: "slider",
6425
+ unit: "%",
6426
+ },
6427
+ },
6428
+ title: "Accent Shades",
6429
+ },
6430
+ ],
6431
+ title: "Controls",
6432
+ },
6433
+ },
6434
+ });
6435
+
6436
+ expect(
6437
+ validateToolcraftAcceptanceCoverage(schemaWithLabeledPaletteBank, [
6438
+ makeControlAcceptance("palette.accent1", "color"),
6439
+ makeControlAcceptance("palette.accent2", "color"),
6440
+ makeControlAcceptance("palette.spread", "slider"),
6441
+ ]),
6442
+ ).toEqual(
6443
+ expect.arrayContaining([
6444
+ 'Accent Shades / accent1 uses visible label "Color 1" for a palette variation color. When colors only add variety to one shared palette, set label: false or use collectionActions with unlabeled items. Keep visible labels only when each color edits a distinct user-facing entity such as Fill, Stroke, Background, Connector, or Object color.',
6445
+ 'Accent Shades / accent2 uses visible label "Color 2" for a palette variation color. When colors only add variety to one shared palette, set label: false or use collectionActions with unlabeled items. Keep visible labels only when each color edits a distinct user-facing entity such as Fill, Stroke, Background, Connector, or Object color.',
6446
+ ]),
6447
+ );
6448
+ });
6449
+
6450
+ it("rejects mixed label visibility inside one palette variation color group", () => {
6451
+ const schemaWithMixedPaletteBankLabels = defineToolcraft({
6452
+ canvas: { enabled: true },
6453
+ panels: {
6454
+ controls: {
6455
+ sections: [
6456
+ {
6457
+ controls: {
6458
+ accent1: {
6459
+ defaultValue: { hex: "#9CE6FF" },
6460
+ label: false,
6461
+ target: "palette.accent1",
6462
+ type: "color",
6463
+ },
6464
+ accent2: {
6465
+ defaultValue: { hex: "#FF7A90" },
6466
+ label: "Color 2",
6467
+ target: "palette.accent2",
6468
+ type: "color",
6469
+ },
6470
+ accent3: {
6471
+ defaultValue: { hex: "#FFD166" },
6472
+ label: false,
6473
+ target: "palette.accent3",
6474
+ type: "color",
6475
+ },
6476
+ },
6477
+ title: "Accent Shades",
6478
+ },
6479
+ ],
6480
+ title: "Controls",
6481
+ },
6482
+ },
6483
+ });
6484
+
6485
+ expect(
6486
+ validateToolcraftAcceptanceCoverage(schemaWithMixedPaletteBankLabels, [
6487
+ makeControlAcceptance("palette.accent1", "color"),
6488
+ makeControlAcceptance("palette.accent2", "color"),
6489
+ makeControlAcceptance("palette.accent3", "color"),
6490
+ ]),
6491
+ ).toEqual(
6492
+ expect.arrayContaining([
6493
+ "Accent Shades mixes labeled and unlabeled color items in one palette variation group. Decide label visibility for the whole group: omit all per-item labels when colors only add variety, or label every item only when each color has a distinct user-facing role.",
6494
+ 'Accent Shades / accent2 uses visible label "Color 2" for a palette variation color. When colors only add variety to one shared palette, set label: false or use collectionActions with unlabeled items. Keep visible labels only when each color edits a distinct user-facing entity such as Fill, Stroke, Background, Connector, or Object color.',
6495
+ ]),
6496
+ );
6497
+ });
6498
+
6499
+ it("allows unlabeled palette variation colors and labeled distinct color roles", () => {
6500
+ const schemaWithUsefulColorLabels = defineToolcraft({
6501
+ canvas: { enabled: true },
6502
+ panels: {
6503
+ controls: {
6504
+ sections: [
6505
+ {
6506
+ controls: {
6507
+ accent1: {
6508
+ defaultValue: { hex: "#9CE6FF" },
6509
+ label: false,
6510
+ target: "palette.accent1",
6511
+ type: "color",
6512
+ },
6513
+ accent2: {
6514
+ defaultValue: { hex: "#FF7A90" },
6515
+ label: false,
6516
+ target: "palette.accent2",
6517
+ type: "color",
6518
+ },
6519
+ spread: {
6520
+ defaultValue: 34,
6521
+ label: "Spread",
6522
+ max: 100,
6523
+ min: 0,
6524
+ target: "palette.spread",
6525
+ type: "slider",
6526
+ unit: "%",
6527
+ },
6528
+ },
6529
+ title: "Accent Shades",
6530
+ },
6531
+ {
6532
+ controls: {
6533
+ fill: {
6534
+ defaultValue: { hex: "#FFFFFF" },
6535
+ label: "Fill",
6536
+ target: "object.fill",
6537
+ type: "color",
6538
+ },
6539
+ stroke: {
6540
+ defaultValue: { hex: "#111111" },
6541
+ label: "Stroke",
6542
+ target: "object.stroke",
6543
+ type: "color",
6544
+ },
6545
+ },
6546
+ title: "Object Colors",
6547
+ },
6548
+ ],
6549
+ title: "Controls",
6550
+ },
6551
+ },
6552
+ });
6553
+
6554
+ expect(
6555
+ validateToolcraftAcceptanceCoverage(schemaWithUsefulColorLabels, [
6556
+ makeControlAcceptance("palette.accent1", "color"),
6557
+ makeControlAcceptance("palette.accent2", "color"),
6558
+ makeControlAcceptance("palette.spread", "slider"),
6559
+ makeControlAcceptance("object.fill", "color"),
6560
+ makeControlAcceptance("object.stroke", "color"),
6561
+ ]),
6562
+ ).toEqual([]);
6563
+ });
6564
+
6055
6565
  it("requires mode selectors to appear before dependent controls", () => {
6056
6566
  const schemaWithLateModeSelector = {
6057
6567
  ...starterSchema,