@snowcone-app/canvas 0.19.1 → 0.21.0

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 (86) hide show
  1. package/README.md +124 -13
  2. package/STYLING_CONTRACT.md +427 -0
  3. package/dist/{CanvasRenderer-DUc-MOeR.js → CanvasRenderer-CwiYq8qg.js} +3 -3
  4. package/dist/{CanvasRenderer-DUc-MOeR.js.map → CanvasRenderer-CwiYq8qg.js.map} +1 -1
  5. package/dist/{CanvasStateV1-Bq48qhv5.js → CanvasStateV1-BFabcAKC.js} +35 -30
  6. package/dist/{CanvasStateV1-Bq48qhv5.js.map → CanvasStateV1-BFabcAKC.js.map} +1 -1
  7. package/dist/{ContextualToolbars-CkLjHNPn.js → ContextualToolbars--vTtorNg.js} +5 -5
  8. package/dist/{ContextualToolbars-CkLjHNPn.js.map → ContextualToolbars--vTtorNg.js.map} +1 -1
  9. package/dist/{EditorCanvasImpl-_3xCnLkx.js → EditorCanvasImpl-2ZgIUVpK.js} +18 -17
  10. package/dist/{EditorCanvasImpl-_3xCnLkx.js.map → EditorCanvasImpl-2ZgIUVpK.js.map} +1 -1
  11. package/dist/{ImportManager-D--7JnvW.js → ExportManager-DK7CdI-G.js} +24758 -24758
  12. package/dist/ExportManager-DK7CdI-G.js.map +1 -0
  13. package/dist/{FontAnalyzer-DIlqmyuf.js → FontAnalyzer-BpsTYowD.js} +5 -2
  14. package/dist/FontAnalyzer-BpsTYowD.js.map +1 -0
  15. package/dist/SnowconeCanvas-DqZicdxK.js +1172 -0
  16. package/dist/SnowconeCanvas-DqZicdxK.js.map +1 -0
  17. package/dist/{MenuButton-CeoEAtzP.js → UnifiedFontService-B1PGVu8O.js} +3704 -3429
  18. package/dist/UnifiedFontService-B1PGVu8O.js.map +1 -0
  19. package/dist/advanced.mjs +13 -13
  20. package/dist/api/embed.d.ts +3 -1
  21. package/dist/api/stable.d.ts +2 -2
  22. package/dist/components/editor/EditorAdd.d.ts +2 -1
  23. package/dist/components/editor/EditorBuy.d.ts +2 -1
  24. package/dist/components/editor/EditorCanvas.d.ts +2 -1
  25. package/dist/components/editor/EditorControls.d.ts +2 -1
  26. package/dist/components/editor/EditorHistory.d.ts +2 -1
  27. package/dist/components/editor/EditorLayers.d.ts +2 -1
  28. package/dist/components/editor/EditorMenu.d.ts +2 -1
  29. package/dist/components/editor/EditorPreview.d.ts +2 -1
  30. package/dist/components/editor/EditorRoot.d.ts +2 -1
  31. package/dist/components/editor/EditorTextField.d.ts +2 -1
  32. package/dist/components/editor/useProductSelection.d.ts +2 -1
  33. package/dist/components/embed/SnowconeCanvas.d.ts +10 -2
  34. package/dist/components/embed-realtime/context.d.ts +1 -1
  35. package/dist/contexts/EditorContext.d.ts +1 -1
  36. package/dist/core/ArtboardElement.d.ts +2 -1
  37. package/dist/default-editor/SnowconeEditor.d.ts +40 -0
  38. package/dist/design/design-builder.d.ts +46 -21
  39. package/dist/design/template-fields.d.ts +32 -0
  40. package/dist/design-builder-CPF60RU2.js +367 -0
  41. package/dist/design-builder-CPF60RU2.js.map +1 -0
  42. package/dist/{EditorContext-5QpqAhjh.js → editorErrors-CK1RISFa.js} +382 -358
  43. package/dist/editorErrors-CK1RISFa.js.map +1 -0
  44. package/dist/embed-2tp6Ue90.js +1117 -0
  45. package/dist/embed-2tp6Ue90.js.map +1 -0
  46. package/dist/embed.mjs +33 -31
  47. package/dist/hooks/useArtboards.d.ts +2 -1
  48. package/dist/index.mjs +8 -8
  49. package/dist/internals.mjs +5 -5
  50. package/dist/kits/sections.d.ts +11 -7
  51. package/dist/rendering/serialize-for-server.d.ts +3 -0
  52. package/dist/state/CanvasStateV1.d.ts +12 -0
  53. package/dist/template-fields-FgImixyv.js +476 -0
  54. package/dist/template-fields-FgImixyv.js.map +1 -0
  55. package/dist/testing/assertRenders.d.ts +2 -1
  56. package/dist/testing.mjs +8 -8
  57. package/dist/testing.mjs.map +1 -1
  58. package/dist/types/index.d.ts +10 -0
  59. package/dist/{usePieceGuideBundle-DVOISsB1.js → usePieceGuideBundle-Ctnu5k2D.js} +2 -2
  60. package/dist/{usePieceGuideBundle-DVOISsB1.js.map → usePieceGuideBundle-Ctnu5k2D.js.map} +1 -1
  61. package/dist/{validation-0IscSfRb.js → validation-3YEMnKLB.js} +467 -463
  62. package/dist/validation-3YEMnKLB.js.map +1 -0
  63. package/example/README.md +179 -0
  64. package/example/index.html +12 -0
  65. package/example/package.json +24 -0
  66. package/example/src/Widget.tsx +62 -0
  67. package/example/src/main.tsx +9 -0
  68. package/example/tsconfig.json +14 -0
  69. package/example/vite.config.ts +11 -0
  70. package/llms.txt +45 -0
  71. package/package.json +11 -6
  72. package/dist/EditorContext-5QpqAhjh.js.map +0 -1
  73. package/dist/FontAnalyzer-DIlqmyuf.js.map +0 -1
  74. package/dist/ImportManager-D--7JnvW.js.map +0 -1
  75. package/dist/MenuButton-CeoEAtzP.js.map +0 -1
  76. package/dist/SnowconeCanvas-3PA17na3.js +0 -1166
  77. package/dist/SnowconeCanvas-3PA17na3.js.map +0 -1
  78. package/dist/design-builder-v4935TeS.js +0 -343
  79. package/dist/design-builder-v4935TeS.js.map +0 -1
  80. package/dist/editorErrors-DXNfVXtD.js +0 -29
  81. package/dist/editorErrors-DXNfVXtD.js.map +0 -1
  82. package/dist/embed-D2QOpJXG.js +0 -1090
  83. package/dist/embed-D2QOpJXG.js.map +0 -1
  84. package/dist/template-fields-Dr3fMItw.js +0 -329
  85. package/dist/template-fields-Dr3fMItw.js.map +0 -1
  86. package/dist/validation-0IscSfRb.js.map +0 -1
@@ -48,6 +48,7 @@ export declare function mutateNamedTextLayers(seed: DesignState): DesignState;
48
48
  * Prove a design + product render AND that the design's bindings are alive
49
49
  * (two-render contract). See the module doc for the failure catalog.
50
50
  *
51
- * @experimental ADR-0085; subject to change until the P4 gate.
51
+ * Stable since ADR-0085 P4 (the default editor is rebuilt from this public
52
+ * surface, CI-gated); additive-only from here.
52
53
  */
53
54
  export declare function assertRenders(design: RenderableDesign, product: RenderProduct, opts: AssertRendersOptions): Promise<AssertRendersResult>;
package/dist/testing.mjs CHANGED
@@ -1,11 +1,11 @@
1
- import { e as ie, g as oe, a as ae } from "./editorErrors-DXNfVXtD.js";
1
+ import { E as A, g as m } from "./editorErrors-CK1RISFa.js";
2
+ import { l as ie, m as oe, n as ae } from "./editorErrors-CK1RISFa.js";
2
3
  import { fetchMockupImage as S, isSnowconeSdkError as b, RenderSession as _, mintRealtimeGrant as R } from "@snowcone-app/sdk";
3
- import { s as I, d as A } from "./design-builder-v4935TeS.js";
4
- import { E as y, g as m } from "./EditorContext-5QpqAhjh.js";
4
+ import { s as I, d as y } from "./design-builder-CPF60RU2.js";
5
5
  import { jsx as u } from "react/jsx-runtime";
6
6
  import { useState as D } from "react";
7
7
  import { T as p } from "./ThemeContext-Rjz25x9X.js";
8
- import { E as T, A as k, g as F, I as x } from "./FontAnalyzer-DIlqmyuf.js";
8
+ import { E as T, A as k, g as F, I as x } from "./FontAnalyzer-BpsTYowD.js";
9
9
  const G = ({
10
10
  children: t,
11
11
  initialElements: n = [],
@@ -32,7 +32,7 @@ const G = ({
32
32
  }), a;
33
33
  });
34
34
  return /* @__PURE__ */ u(p, { children: /* @__PURE__ */ u(
35
- y,
35
+ A,
36
36
  {
37
37
  initialArtboardConfig: o,
38
38
  children: t
@@ -44,7 +44,7 @@ const G = ({
44
44
  initialSelection: e = null,
45
45
  initialMultiSelection: r = [],
46
46
  initialArtboardConfig: i = {}
47
- }) => /* @__PURE__ */ u(p, { children: /* @__PURE__ */ u(y, { initialArtboardConfig: i, children: t }) });
47
+ }) => /* @__PURE__ */ u(p, { children: /* @__PURE__ */ u(A, { initialArtboardConfig: i, children: t }) });
48
48
  function W(t = "custom", n = {}) {
49
49
  const e = F(t);
50
50
  if (!e || !e.Component)
@@ -223,10 +223,10 @@ async function X(t, n, e) {
223
223
  }
224
224
  const v = "https://assets.test-403.invalid/denied.png";
225
225
  function Z() {
226
- return A({ name: "Front", width: 1e3, height: 1e3 }).background(v, { aspectRatio: 1, name: "art" }).text("ALLOWLIST DENIAL FIXTURE", { anchor: "top", name: "headline" });
226
+ return y({ name: "Front", width: 1e3, height: 1e3 }).background(v, { aspectRatio: 1, name: "art" }).text("ALLOWLIST DENIAL FIXTURE", { anchor: "top", name: "headline" });
227
227
  }
228
228
  function ee() {
229
- return A({ name: "Front", width: 1e3, height: 1e3 }).text("YOUR NAME", { anchor: "center", name: "name" });
229
+ return y({ name: "Front", width: 1e3, height: 1e3 }).text("YOUR NAME", { anchor: "center", name: "name" });
230
230
  }
231
231
  function te(t = "failing-grant fixture: grant endpoint unavailable") {
232
232
  return () => Promise.reject(new Error(t));
@@ -1 +1 @@
1
- {"version":3,"file":"testing.mjs","sources":["../src/testing/MockEditorProvider.tsx","../src/testing/utils.ts","../src/testing/assertRenders.ts","../src/testing/failureFixtures.ts"],"sourcesContent":["/**\n * MockEditorProvider - Lightweight mock for testing custom components\n * Compatible with Vitest (does not require Jest)\n *\n * @example\n * ```tsx\n * import { render } from '@testing-library/react';\n * import { MockEditorProvider } from '@snowcone-app/canvas/embed';\n *\n * test('my component renders', () => {\n * render(\n * <MockEditorProvider>\n * <MyCustomComponent />\n * </MockEditorProvider>\n * );\n * });\n * ```\n */\n\nimport React, { useState, ReactNode } from 'react';\nimport { EditorProvider } from '../contexts/EditorContext.js';\nimport { ThemeProvider } from '../contexts/ThemeContext.js';\nimport { ArtboardManager } from '../core/ArtboardManager.js';\nimport { ArtboardElement } from '../core/ArtboardElement.js';\nimport type { TextElement } from '../core/TextElement.js';\nimport type { ImageElement } from '../core/ImageElement.js';\nimport type { GroupElement } from '../core/GroupElement.js';\nimport type { ShapeElement } from '../core/ShapeElement.js';\n\nexport type MockEditorElement = TextElement | ImageElement | GroupElement | ShapeElement;\n\nexport interface MockEditorProviderProps {\n children: ReactNode;\n\n /**\n * Initial elements to populate the editor\n */\n initialElements?: MockEditorElement[];\n\n /**\n * Initial artboards (default: one artboard)\n */\n initialArtboards?: ArtboardElement[];\n\n /**\n * Initial selection\n */\n initialSelection?: string | null;\n\n /**\n * Initial multi-selection\n */\n initialMultiSelection?: string[];\n\n /**\n * Initial artboard configuration\n */\n initialArtboardConfig?: {\n name?: string;\n width?: number;\n height?: number;\n backgroundColor?: string;\n };\n}\n\n/**\n * MockEditorProvider - Wraps EditorProvider with test-friendly defaults\n *\n * This is a lightweight wrapper that provides the same EditorContext\n * as the real app, making it perfect for testing custom components.\n */\nexport const MockEditorProvider: React.FC<MockEditorProviderProps> = ({\n children,\n initialElements: _initialElements = [],\n initialArtboards,\n initialSelection: _initialSelection = null,\n initialMultiSelection: _initialMultiSelection = [],\n initialArtboardConfig = {},\n}) => {\n // Create artboard manager if artboards provided\n const [_artboardManager] = useState(() => {\n const manager = new ArtboardManager();\n\n if (initialArtboards && initialArtboards.length > 0) {\n // Use provided artboards\n initialArtboards.forEach(artboard => {\n manager.createArtboard({\n name: artboard.name,\n width: artboard.width,\n height: artboard.height,\n backgroundColor: artboard.backgroundColor,\n });\n });\n } else {\n // Create default artboard\n manager.createArtboard({\n name: 'Test Artboard',\n width: 1200,\n height: 1200,\n backgroundColor: '#FFFFFF',\n ...initialArtboardConfig,\n });\n }\n\n return manager;\n });\n\n // Simple wrapper that passes children through to real EditorProvider\n // The EditorProvider will handle all the state management\n // ThemeProvider is required because CanvasEditor uses useTheme()\n return (\n <ThemeProvider>\n <EditorProvider\n initialArtboardConfig={initialArtboardConfig}\n >\n {children}\n </EditorProvider>\n </ThemeProvider>\n );\n};\n\n/**\n * MockEditorProviderWithState - For tests that need to control state directly\n *\n * Use this when you need fine-grained control over the editor state.\n * Most tests should use the simpler MockEditorProvider above.\n */\nexport const MockEditorProviderWithState: React.FC<MockEditorProviderProps> = ({\n children,\n initialElements: _initialElements = [],\n initialSelection: _initialSelection = null,\n initialMultiSelection: _initialMultiSelection = [],\n initialArtboardConfig = {},\n}) => {\n return (\n <ThemeProvider>\n <EditorProvider initialArtboardConfig={initialArtboardConfig}>\n {children}\n </EditorProvider>\n </ThemeProvider>\n );\n};\n","/**\n * Testing Utilities - Helpers for creating mock elements and artboards\n * Compatible with Vitest\n */\n\nimport { TextElement } from '../core/TextElement.js';\nimport { ImageElement } from '../core/ImageElement.js';\nimport { ArtboardElement } from '../core/ArtboardElement.js';\nimport { getTransformById } from '../transforms/registry.js';\nimport type { TransformType, ImageElementConfig } from '../types/index.js';\n\n/**\n * Create a mock text element with sensible defaults\n *\n * @example\n * ```tsx\n * const element = createMockElement('custom', {\n * text: 'Hello World',\n * fontSize: 48,\n * fontColor: '#FF0000'\n * });\n * ```\n */\nexport function createMockElement(\n transformType: TransformType = 'custom',\n overrides: Partial<TextElement> = {}\n): TextElement {\n // Get the correct concrete class from registry\n const transformDef = getTransformById(transformType);\n if (!transformDef || !transformDef.Component) {\n throw new Error(`Transform type \"${transformType}\" not found in registry`);\n }\n\n // Create element using the concrete class\n const element = new transformDef.Component({\n transformType,\n text: 'Test Text',\n fontSize: 32,\n color: '#000000',\n fontFamily: 'Arial',\n x: 100,\n y: 100,\n rotation: 0,\n ...overrides,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test utility: overrides merge Partial<TextElement> properties with config, which are structurally compatible but not type-assignable\n } as any);\n\n return element as TextElement;\n}\n\n/**\n * Create a mock image element\n *\n * @example\n * ```tsx\n * const image = createMockImageElement({\n * imageUrl: 'data:image/png;base64,...',\n * width: 200,\n * height: 200\n * });\n * ```\n */\nexport function createMockImageElement(\n overrides: Partial<ImageElementConfig> = {}\n): ImageElement {\n const element = new ImageElement({\n imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',\n imageAspectRatio: 1,\n x: 100,\n y: 100,\n rotation: 0,\n ...overrides,\n });\n\n return element;\n}\n\n/**\n * Create a mock artboard\n *\n * @example\n * ```tsx\n * const artboard = createMockArtboard({\n * name: 'Front',\n * width: 1200,\n * height: 1200,\n * backgroundColor: '#FFFFFF'\n * });\n * ```\n */\nexport function createMockArtboard(\n overrides: Partial<{\n name: string;\n width: number;\n height: number;\n x: number;\n y: number;\n backgroundColor: string;\n }> = {}\n): ArtboardElement {\n const artboard = new ArtboardElement({\n name: 'Test Artboard',\n width: 1200,\n height: 1200,\n x: 0,\n y: 0,\n backgroundColor: '#FFFFFF',\n ...overrides,\n });\n\n return artboard;\n}\n\n/**\n * Wait for a condition to become true\n * Useful for async testing\n *\n * @example\n * ```tsx\n * await waitFor(() => screen.getByText('Loaded'), 5000);\n * ```\n */\nexport async function waitFor(\n condition: () => boolean,\n timeout = 5000,\n interval = 100\n): Promise<void> {\n const startTime = Date.now();\n\n while (!condition()) {\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for condition after ${timeout}ms`);\n }\n await new Promise(resolve => setTimeout(resolve, interval));\n }\n}\n\n/**\n * Wait for an export to complete\n * Polls until dataUrl is available\n *\n * @example\n * ```tsx\n * const dataUrl = await waitForExport('artboard-1', 5000);\n * expect(dataUrl).toContain('data:image/png');\n * ```\n */\nexport async function waitForExport(\n _artboardId: string,\n timeout = 5000\n): Promise<string | null> {\n let dataUrl: string | null = null;\n\n await waitFor(() => {\n // Check if export completed (this would need to be implemented based on your export state)\n // For now, just return false as a placeholder\n return false;\n }, timeout);\n\n return dataUrl;\n}\n\n/**\n * Simulate a keyboard event\n * Useful for testing keyboard shortcuts\n *\n * @example\n * ```tsx\n * simulateKeyPress('c', { meta: true }); // Cmd+C\n * ```\n */\nexport function simulateKeyPress(\n key: string,\n options: {\n ctrl?: boolean;\n shift?: boolean;\n alt?: boolean;\n meta?: boolean;\n } = {}\n): void {\n const event = new KeyboardEvent('keydown', {\n key,\n ctrlKey: options.ctrl,\n shiftKey: options.shift,\n altKey: options.alt,\n metaKey: options.meta,\n bubbles: true,\n cancelable: true,\n });\n\n document.dispatchEvent(event);\n}\n\n/**\n * Create a mock canvas element for testing\n * Returns an HTMLCanvasElement with mocked context\n *\n * @example\n * ```tsx\n * const canvas = createMockCanvas(800, 600);\n * const ctx = canvas.getContext('2d');\n * ```\n */\nexport function createMockCanvas(\n width = 800,\n height = 600\n): HTMLCanvasElement {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n\n return canvas;\n}\n\n/**\n * Assert that an element has certain properties\n * Useful for programmatic assertions\n *\n * @example\n * ```tsx\n * assertElementProperties(element, {\n * text: 'Hello',\n * fontSize: 48,\n * fontColor: '#FF0000'\n * });\n * ```\n */\nexport function assertElementProperties(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Test utility: accepts any element type and checks arbitrary properties\n element: any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Test utility: expected values can be any type\n expected: Record<string, any>\n): void {\n Object.entries(expected).forEach(([key, value]) => {\n if (element[key] !== value) {\n throw new Error(\n `Expected element.${key} to be ${value}, got ${element[key]}`\n );\n }\n });\n}\n\n/**\n * Get element bounding box for testing\n * Returns computed bounds from the element\n *\n * @example\n * ```tsx\n * const bounds = getElementBounds(element);\n * expect(bounds.width).toBeGreaterThan(0);\n * ```\n */\nexport function getElementBounds(element: TextElement | ImageElement): {\n x: number;\n y: number;\n width: number;\n height: number;\n} {\n const bbox = element.getBoundingBox();\n\n return {\n x: bbox.x,\n y: bbox.y,\n width: bbox.width,\n height: bbox.height,\n };\n}\n","/**\n * `assertRenders(design, product, opts)` — the TWO-RENDER live contract check\n * (agent-DX decision F4, ADR-0085 plan).\n *\n * WHY TWO RENDERS: \"the URL returns an image\" can't catch a DEAD BINDING — a\n * wrong layer name still renders the seed beautifully. So this renders the\n * design twice: once as seeded, once with the personalization bindings\n * mutated, and asserts the encoded image BYTES differ (mockup delivery is\n * deterministic per state, so byte-identical means pixel-identical means the\n * mutation never reached the render).\n *\n * What it proves, in order:\n * 1. the seed design renders: HTTP 200 + real raster bytes (non-blank);\n * 2. the mutated design renders the same way;\n * 3. the two renders DIFFER — the binding is alive.\n *\n * Failures are the structured error contract (E3), never prose-only:\n * - `E_ASSET_FORBIDDEN` — the shop's asset-origin allowlist denied a design\n * asset (translated from the server's 403; `data.failedAssets` names it);\n * - `E_RENDER_FAILED` — render error / timeout / non-image response;\n * - `E_BINDING_INERT` — both renders byte-identical (dead binding);\n * - `E_NO_MUTABLE_BINDING` — nothing to mutate and no `mutate` given.\n * - The SDK's `SHOP_NOT_PROPAGATED` passes through: a freshly minted shop\n * that never becomes visible is an environment fault, named as such.\n *\n * This talks to the REAL realtime render path (network). Use it in nightly /\n * acceptance jobs and agent verify loops — per-PR unit tests should mock the\n * SDK transport instead (plan: per-PR replay, nightly network-real).\n */\n\nimport {\n RenderSession,\n fetchMockupImage,\n mintRealtimeGrant,\n type MockupResult,\n type RealtimeGrant,\n type RenderProduct,\n} from '@snowcone-app/sdk';\nimport { isSnowconeSdkError } from '@snowcone-app/sdk';\nimport { serializeStateForServer } from '../rendering/serialize-for-server.js';\nimport { EditorError } from '../errors/editorError.js';\nimport type { DesignState } from '../components/editor/designStateStore.js';\n\n/** Anything `design()` produces (or a compatible shape). */\nexport interface RenderableDesign {\n toJSON(): DesignState;\n}\n\nexport interface AssertRendersOptions {\n /** Publishable shop id (= shop.id). */\n shop: string;\n /** Secret API key (sk_) for the grant; omit for the keyless publishable\n * path (sandbox/trial shops — the F1 bootstrap default). */\n apiKey?: string;\n /** Pre-fetched grant or grant factory — overrides the mint entirely. */\n grant?: RealtimeGrant | (() => Promise<RealtimeGrant>);\n /** Backend base for the keyless/sk grant mint (defaults to prod). */\n grantBase?: string;\n /** Override the realtime WS endpoint. */\n wsUrl?: string;\n /**\n * Produce the mutated state for the second render. Default: rewrite the\n * text of EVERY named text layer to a sentinel (`ASSERT_RENDERS_<name>`).\n * Throws `E_NO_MUTABLE_BINDING` if the design has no named text layer and\n * no override is given.\n */\n mutate?: (seed: DesignState) => DesignState;\n /** Per-render timeout (ms). Default 30_000. */\n timeoutMs?: number;\n /** Override fetch for the image downloads (tests). */\n fetchImpl?: typeof fetch;\n}\n\nexport interface AssertRendersResult {\n /** The seed render's mockup URL + encoded size. */\n seed: { url: string; bytes: number; kind: string };\n /** The mutated render's mockup URL + encoded size. */\n mutated: { url: string; bytes: number; kind: string };\n}\n\n/** Default mutation: every NAMED text layer gets a sentinel value. */\nexport function mutateNamedTextLayers(seed: DesignState): DesignState {\n let mutatedAny = false;\n const elements = seed.elements.map((el) => {\n const name = (el as { name?: string }).name;\n const text = (el as { text?: string }).text;\n if (!name || typeof text !== 'string') return el;\n mutatedAny = true;\n return { ...el, text: `ASSERT_RENDERS_${name.toUpperCase()}` } as typeof el;\n });\n if (!mutatedAny) {\n throw new EditorError(\n 'E_NO_MUTABLE_BINDING',\n 'this design has no NAMED text layer to mutate for the two-render check. Name one ' +\n '(design().text(\"…\", { name: \"headline\" })) or pass opts.mutate to define the mutation.',\n { docSlug: 'embed-mode' },\n );\n }\n return { ...seed, elements };\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Render one state on a FRESH session and resolve the first mockup result.\n * One session per render on purpose: a session keeps emitting trailing mockup\n * results (multi-mockup products emit per scene), so reusing it for the\n * second render could resolve the mutated phase with a stale seed emission —\n * and with content-addressed delivery a dead binding makes the two\n * indistinguishable by URL. A fresh session has no history to confuse.\n */\nasync function renderOnce(\n makeSession: () => RenderSession,\n placement: string,\n state: DesignState,\n timeoutMs: number,\n label: 'seed' | 'mutated',\n): Promise<MockupResult> {\n const session = makeSession();\n try {\n return await new Promise<MockupResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new EditorError('E_RENDER_FAILED', `the ${label} render produced no mockup within ${timeoutMs}ms.`, {\n phase: label,\n timeoutMs,\n }),\n );\n }, timeoutMs);\n session.onMockups((results) => {\n const withImage = results.find((r) => r.imageUrl);\n if (!withImage) return;\n clearTimeout(timer);\n resolve(withImage);\n });\n session.onError((message) => {\n clearTimeout(timer);\n reject(new EditorError('E_RENDER_FAILED', `the ${label} render failed: ${message}`, { phase: label, error: message }));\n });\n void session.renderState(placement, serializeStateForServer(state)).catch((err) => {\n clearTimeout(timer);\n const message = err instanceof Error ? err.message : String(err);\n reject(new EditorError('E_RENDER_FAILED', `the ${label} render failed to dispatch: ${message}`, { phase: label, error: message }));\n });\n });\n } finally {\n session.close();\n }\n}\n\n/** Fetch + validate one rendered image, translating SDK failures (asset-403 →\n * `E_ASSET_FORBIDDEN`) into the editor error contract. */\nasync function fetchRender(url: string, label: 'seed' | 'mutated', opts: AssertRendersOptions): Promise<{ bytes: Uint8Array; kind: string }> {\n try {\n const img = await fetchMockupImage(url, {\n ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),\n });\n return { bytes: img.bytes, kind: img.kind };\n } catch (err) {\n if (isSnowconeSdkError(err)) {\n if (err.code === 'ASSET_FORBIDDEN') {\n throw new EditorError(\n 'E_ASSET_FORBIDDEN',\n `the shop's asset-origin allowlist denied an asset in this design (${label} render). ` +\n `Allow the asset's origin on the shop (or host it on the shop's storage origin).`,\n { phase: label, url, failedAssets: err.data.failedAssets, docSlug: 'embed-mode' },\n );\n }\n if (err.code === 'SHOP_NOT_PROPAGATED') throw err; // already structured + named\n throw new EditorError('E_RENDER_FAILED', `the ${label} render's image fetch failed: ${err.message}`, {\n phase: label,\n url,\n ...err.data,\n });\n }\n throw err;\n }\n}\n\n/**\n * Prove a design + product render AND that the design's bindings are alive\n * (two-render contract). See the module doc for the failure catalog.\n *\n * @experimental ADR-0085; subject to change until the P4 gate.\n */\nexport async function assertRenders(\n design: RenderableDesign,\n product: RenderProduct,\n opts: AssertRendersOptions,\n): Promise<AssertRendersResult> {\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const seed = design.toJSON();\n const placement = seed.activeArtboard;\n const mutated = (opts.mutate ?? mutateNamedTextLayers)(seed);\n\n const grant = opts.grant;\n const getToken =\n typeof grant === 'function'\n ? grant\n : grant\n ? () => Promise.resolve(grant)\n : () =>\n mintRealtimeGrant({\n shop: opts.shop,\n ...(opts.apiKey ? { apiKey: opts.apiKey } : {}),\n ...(opts.grantBase ? { base: opts.grantBase } : {}),\n ...(opts.fetchImpl ? { fetch: opts.fetchImpl } : {}),\n });\n\n const makeSession = () =>\n new RenderSession({\n shop: opts.shop,\n product,\n ...(opts.wsUrl ? { wsUrl: opts.wsUrl } : {}),\n getToken,\n });\n\n const seedResult = await renderOnce(makeSession, placement, seed, timeoutMs, 'seed');\n const seedImage = await fetchRender(seedResult.imageUrl!, 'seed', opts);\n\n const mutatedResult = await renderOnce(makeSession, placement, mutated, timeoutMs, 'mutated');\n const mutatedImage = await fetchRender(mutatedResult.imageUrl!, 'mutated', opts);\n\n if (bytesEqual(seedImage.bytes, mutatedImage.bytes)) {\n throw new EditorError(\n 'E_BINDING_INERT',\n 'the mutated render is byte-identical to the seed render — the mutation never reached ' +\n 'the server, so the binding under test is DEAD. Usual causes: the mutated layer name ' +\n \"doesn't exist in the design (check design.layer() names), or opts.mutate returned the \" +\n 'seed unchanged.',\n {\n seedUrl: seedResult.imageUrl,\n mutatedUrl: mutatedResult.imageUrl,\n bytes: seedImage.bytes.byteLength,\n docSlug: 'embed-mode',\n },\n );\n }\n\n return {\n seed: { url: seedResult.imageUrl!, bytes: seedImage.bytes.byteLength, kind: seedImage.kind },\n mutated: { url: mutatedResult.imageUrl!, bytes: mutatedImage.bytes.byteLength, kind: mutatedImage.kind },\n };\n}\n","/**\n * Deterministic failure-mode fixtures (ADR-0085 plan, P2 — the magic-test-card\n * equivalent): trigger each commerce/render failure ON DEMAND so integrators\n * build their error UI against real failures instead of waiting for prod to\n * produce one.\n *\n * These are the CLIENT-ACHIEVABLE fixtures — each works against any shop\n * (including a fresh sandbox shop) with no server cooperation:\n *\n * - {@link test403Design} — asset-origin allowlist denial. References an\n * asset on an origin no shop allowlists, so the server's asset fetch is\n * denied deterministically. Surfaces as a render error (live path) or\n * `E_ASSET_FORBIDDEN` from `assertRenders`.\n * - {@link testIncompleteDesign} — Buy completeness failure. A design whose\n * only personalization layer still carries its seed value, so\n * `<Editor.Buy>` gates with `layer-seed:<name>` (and `layer:<name>` once\n * cleared). Pair with `<Editor.TextField layer={d.layer('name')} />`.\n * - {@link failingGrant} — grant failure. A grant factory that always\n * rejects; pass as `<Editor.Root grant={failingGrant()}>` to exercise the\n * \"grant fetch fails\" surface (`onRenderError` + the Preview's\n * `data-sc-part=\"preview-error\"` state).\n *\n * Server-triggered fixtures (a `test-slow` product for the timeout path, a\n * forced render-error token) need render-edge cooperation and are tracked\n * separately — they are NOT silently stubbed here.\n */\n\nimport { design } from '../design/design-builder.js';\nimport type { RealtimeGrant } from '@snowcone-app/sdk';\n\n/**\n * An origin that is deliberately on NO shop's asset-origin allowlist. The\n * `.invalid` TLD is reserved (RFC 2606) — it can never resolve, and the\n * allowlist check denies it before any fetch is attempted, so the failure is\n * deterministic and free.\n */\nexport const TEST_403_ASSET_URL = 'https://assets.test-403.invalid/denied.png';\n\n/**\n * A design whose background asset the shop's allowlist is guaranteed to deny —\n * the deterministic allowlist-denial (403) trigger.\n */\nexport function test403Design() {\n return design({ name: 'Front', width: 1000, height: 1000 })\n .background(TEST_403_ASSET_URL, { aspectRatio: 1, name: 'art' })\n .text('ALLOWLIST DENIAL FIXTURE', { anchor: 'top', name: 'headline' });\n}\n\n/**\n * A design that gates `<Editor.Buy>` out of the box: its one personalization\n * layer (`'name'`, seed `'YOUR NAME'`) is required and still equals its seed,\n * so Buy renders `data-sc-buy-state=\"incomplete\"` with\n * `data-sc-buy-missing=\"layer-seed:name\"` until a real value is typed.\n */\nexport function testIncompleteDesign() {\n return design({ name: 'Front', width: 1000, height: 1000 })\n .text('YOUR NAME', { anchor: 'center', name: 'name' });\n}\n\n/**\n * A grant factory that always rejects — the deterministic \"grant fetch\n * failed\" trigger. The rejection reason is stable for assertions.\n */\nexport function failingGrant(reason = 'failing-grant fixture: grant endpoint unavailable'): () => Promise<RealtimeGrant> {\n return () => Promise.reject(new Error(reason));\n}\n"],"names":["MockEditorProvider","children","_initialElements","initialArtboards","_initialSelection","_initialMultiSelection","initialArtboardConfig","_artboardManager","useState","manager","ArtboardManager","artboard","ThemeProvider","jsx","EditorProvider","MockEditorProviderWithState","createMockElement","transformType","overrides","transformDef","getTransformById","createMockImageElement","ImageElement","createMockArtboard","ArtboardElement","waitFor","condition","timeout","interval","startTime","resolve","waitForExport","_artboardId","dataUrl","simulateKeyPress","key","options","event","createMockCanvas","width","height","canvas","assertElementProperties","element","expected","value","getElementBounds","bbox","mutateNamedTextLayers","seed","mutatedAny","elements","el","name","text","EditorError","bytesEqual","a","b","i","renderOnce","makeSession","placement","state","timeoutMs","label","session","reject","timer","results","withImage","r","message","serializeStateForServer","err","fetchRender","url","opts","img","fetchMockupImage","isSnowconeSdkError","assertRenders","design","product","mutated","grant","getToken","mintRealtimeGrant","RenderSession","seedResult","seedImage","mutatedResult","mutatedImage","TEST_403_ASSET_URL","test403Design","testIncompleteDesign","failingGrant","reason"],"mappings":"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"}
1
+ {"version":3,"file":"testing.mjs","sources":["../src/testing/MockEditorProvider.tsx","../src/testing/utils.ts","../src/testing/assertRenders.ts","../src/testing/failureFixtures.ts"],"sourcesContent":["/**\n * MockEditorProvider - Lightweight mock for testing custom components\n * Compatible with Vitest (does not require Jest)\n *\n * @example\n * ```tsx\n * import { render } from '@testing-library/react';\n * import { MockEditorProvider } from '@snowcone-app/canvas/embed';\n *\n * test('my component renders', () => {\n * render(\n * <MockEditorProvider>\n * <MyCustomComponent />\n * </MockEditorProvider>\n * );\n * });\n * ```\n */\n\nimport React, { useState, ReactNode } from 'react';\nimport { EditorProvider } from '../contexts/EditorContext.js';\nimport { ThemeProvider } from '../contexts/ThemeContext.js';\nimport { ArtboardManager } from '../core/ArtboardManager.js';\nimport { ArtboardElement } from '../core/ArtboardElement.js';\nimport type { TextElement } from '../core/TextElement.js';\nimport type { ImageElement } from '../core/ImageElement.js';\nimport type { GroupElement } from '../core/GroupElement.js';\nimport type { ShapeElement } from '../core/ShapeElement.js';\n\nexport type MockEditorElement = TextElement | ImageElement | GroupElement | ShapeElement;\n\nexport interface MockEditorProviderProps {\n children: ReactNode;\n\n /**\n * Initial elements to populate the editor\n */\n initialElements?: MockEditorElement[];\n\n /**\n * Initial artboards (default: one artboard)\n */\n initialArtboards?: ArtboardElement[];\n\n /**\n * Initial selection\n */\n initialSelection?: string | null;\n\n /**\n * Initial multi-selection\n */\n initialMultiSelection?: string[];\n\n /**\n * Initial artboard configuration\n */\n initialArtboardConfig?: {\n name?: string;\n width?: number;\n height?: number;\n backgroundColor?: string;\n };\n}\n\n/**\n * MockEditorProvider - Wraps EditorProvider with test-friendly defaults\n *\n * This is a lightweight wrapper that provides the same EditorContext\n * as the real app, making it perfect for testing custom components.\n */\nexport const MockEditorProvider: React.FC<MockEditorProviderProps> = ({\n children,\n initialElements: _initialElements = [],\n initialArtboards,\n initialSelection: _initialSelection = null,\n initialMultiSelection: _initialMultiSelection = [],\n initialArtboardConfig = {},\n}) => {\n // Create artboard manager if artboards provided\n const [_artboardManager] = useState(() => {\n const manager = new ArtboardManager();\n\n if (initialArtboards && initialArtboards.length > 0) {\n // Use provided artboards\n initialArtboards.forEach(artboard => {\n manager.createArtboard({\n name: artboard.name,\n width: artboard.width,\n height: artboard.height,\n backgroundColor: artboard.backgroundColor,\n });\n });\n } else {\n // Create default artboard\n manager.createArtboard({\n name: 'Test Artboard',\n width: 1200,\n height: 1200,\n backgroundColor: '#FFFFFF',\n ...initialArtboardConfig,\n });\n }\n\n return manager;\n });\n\n // Simple wrapper that passes children through to real EditorProvider\n // The EditorProvider will handle all the state management\n // ThemeProvider is required because CanvasEditor uses useTheme()\n return (\n <ThemeProvider>\n <EditorProvider\n initialArtboardConfig={initialArtboardConfig}\n >\n {children}\n </EditorProvider>\n </ThemeProvider>\n );\n};\n\n/**\n * MockEditorProviderWithState - For tests that need to control state directly\n *\n * Use this when you need fine-grained control over the editor state.\n * Most tests should use the simpler MockEditorProvider above.\n */\nexport const MockEditorProviderWithState: React.FC<MockEditorProviderProps> = ({\n children,\n initialElements: _initialElements = [],\n initialSelection: _initialSelection = null,\n initialMultiSelection: _initialMultiSelection = [],\n initialArtboardConfig = {},\n}) => {\n return (\n <ThemeProvider>\n <EditorProvider initialArtboardConfig={initialArtboardConfig}>\n {children}\n </EditorProvider>\n </ThemeProvider>\n );\n};\n","/**\n * Testing Utilities - Helpers for creating mock elements and artboards\n * Compatible with Vitest\n */\n\nimport { TextElement } from '../core/TextElement.js';\nimport { ImageElement } from '../core/ImageElement.js';\nimport { ArtboardElement } from '../core/ArtboardElement.js';\nimport { getTransformById } from '../transforms/registry.js';\nimport type { TransformType, ImageElementConfig } from '../types/index.js';\n\n/**\n * Create a mock text element with sensible defaults\n *\n * @example\n * ```tsx\n * const element = createMockElement('custom', {\n * text: 'Hello World',\n * fontSize: 48,\n * fontColor: '#FF0000'\n * });\n * ```\n */\nexport function createMockElement(\n transformType: TransformType = 'custom',\n overrides: Partial<TextElement> = {}\n): TextElement {\n // Get the correct concrete class from registry\n const transformDef = getTransformById(transformType);\n if (!transformDef || !transformDef.Component) {\n throw new Error(`Transform type \"${transformType}\" not found in registry`);\n }\n\n // Create element using the concrete class\n const element = new transformDef.Component({\n transformType,\n text: 'Test Text',\n fontSize: 32,\n color: '#000000',\n fontFamily: 'Arial',\n x: 100,\n y: 100,\n rotation: 0,\n ...overrides,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test utility: overrides merge Partial<TextElement> properties with config, which are structurally compatible but not type-assignable\n } as any);\n\n return element as TextElement;\n}\n\n/**\n * Create a mock image element\n *\n * @example\n * ```tsx\n * const image = createMockImageElement({\n * imageUrl: 'data:image/png;base64,...',\n * width: 200,\n * height: 200\n * });\n * ```\n */\nexport function createMockImageElement(\n overrides: Partial<ImageElementConfig> = {}\n): ImageElement {\n const element = new ImageElement({\n imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',\n imageAspectRatio: 1,\n x: 100,\n y: 100,\n rotation: 0,\n ...overrides,\n });\n\n return element;\n}\n\n/**\n * Create a mock artboard\n *\n * @example\n * ```tsx\n * const artboard = createMockArtboard({\n * name: 'Front',\n * width: 1200,\n * height: 1200,\n * backgroundColor: '#FFFFFF'\n * });\n * ```\n */\nexport function createMockArtboard(\n overrides: Partial<{\n name: string;\n width: number;\n height: number;\n x: number;\n y: number;\n backgroundColor: string;\n }> = {}\n): ArtboardElement {\n const artboard = new ArtboardElement({\n name: 'Test Artboard',\n width: 1200,\n height: 1200,\n x: 0,\n y: 0,\n backgroundColor: '#FFFFFF',\n ...overrides,\n });\n\n return artboard;\n}\n\n/**\n * Wait for a condition to become true\n * Useful for async testing\n *\n * @example\n * ```tsx\n * await waitFor(() => screen.getByText('Loaded'), 5000);\n * ```\n */\nexport async function waitFor(\n condition: () => boolean,\n timeout = 5000,\n interval = 100\n): Promise<void> {\n const startTime = Date.now();\n\n while (!condition()) {\n if (Date.now() - startTime > timeout) {\n throw new Error(`Timeout waiting for condition after ${timeout}ms`);\n }\n await new Promise(resolve => setTimeout(resolve, interval));\n }\n}\n\n/**\n * Wait for an export to complete\n * Polls until dataUrl is available\n *\n * @example\n * ```tsx\n * const dataUrl = await waitForExport('artboard-1', 5000);\n * expect(dataUrl).toContain('data:image/png');\n * ```\n */\nexport async function waitForExport(\n _artboardId: string,\n timeout = 5000\n): Promise<string | null> {\n let dataUrl: string | null = null;\n\n await waitFor(() => {\n // Check if export completed (this would need to be implemented based on your export state)\n // For now, just return false as a placeholder\n return false;\n }, timeout);\n\n return dataUrl;\n}\n\n/**\n * Simulate a keyboard event\n * Useful for testing keyboard shortcuts\n *\n * @example\n * ```tsx\n * simulateKeyPress('c', { meta: true }); // Cmd+C\n * ```\n */\nexport function simulateKeyPress(\n key: string,\n options: {\n ctrl?: boolean;\n shift?: boolean;\n alt?: boolean;\n meta?: boolean;\n } = {}\n): void {\n const event = new KeyboardEvent('keydown', {\n key,\n ctrlKey: options.ctrl,\n shiftKey: options.shift,\n altKey: options.alt,\n metaKey: options.meta,\n bubbles: true,\n cancelable: true,\n });\n\n document.dispatchEvent(event);\n}\n\n/**\n * Create a mock canvas element for testing\n * Returns an HTMLCanvasElement with mocked context\n *\n * @example\n * ```tsx\n * const canvas = createMockCanvas(800, 600);\n * const ctx = canvas.getContext('2d');\n * ```\n */\nexport function createMockCanvas(\n width = 800,\n height = 600\n): HTMLCanvasElement {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n\n return canvas;\n}\n\n/**\n * Assert that an element has certain properties\n * Useful for programmatic assertions\n *\n * @example\n * ```tsx\n * assertElementProperties(element, {\n * text: 'Hello',\n * fontSize: 48,\n * fontColor: '#FF0000'\n * });\n * ```\n */\nexport function assertElementProperties(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Test utility: accepts any element type and checks arbitrary properties\n element: any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Test utility: expected values can be any type\n expected: Record<string, any>\n): void {\n Object.entries(expected).forEach(([key, value]) => {\n if (element[key] !== value) {\n throw new Error(\n `Expected element.${key} to be ${value}, got ${element[key]}`\n );\n }\n });\n}\n\n/**\n * Get element bounding box for testing\n * Returns computed bounds from the element\n *\n * @example\n * ```tsx\n * const bounds = getElementBounds(element);\n * expect(bounds.width).toBeGreaterThan(0);\n * ```\n */\nexport function getElementBounds(element: TextElement | ImageElement): {\n x: number;\n y: number;\n width: number;\n height: number;\n} {\n const bbox = element.getBoundingBox();\n\n return {\n x: bbox.x,\n y: bbox.y,\n width: bbox.width,\n height: bbox.height,\n };\n}\n","/**\n * `assertRenders(design, product, opts)` — the TWO-RENDER live contract check\n * (agent-DX decision F4, ADR-0085 plan).\n *\n * WHY TWO RENDERS: \"the URL returns an image\" can't catch a DEAD BINDING — a\n * wrong layer name still renders the seed beautifully. So this renders the\n * design twice: once as seeded, once with the personalization bindings\n * mutated, and asserts the encoded image BYTES differ (mockup delivery is\n * deterministic per state, so byte-identical means pixel-identical means the\n * mutation never reached the render).\n *\n * What it proves, in order:\n * 1. the seed design renders: HTTP 200 + real raster bytes (non-blank);\n * 2. the mutated design renders the same way;\n * 3. the two renders DIFFER — the binding is alive.\n *\n * Failures are the structured error contract (E3), never prose-only:\n * - `E_ASSET_FORBIDDEN` — the shop's asset-origin allowlist denied a design\n * asset (translated from the server's 403; `data.failedAssets` names it);\n * - `E_RENDER_FAILED` — render error / timeout / non-image response;\n * - `E_BINDING_INERT` — both renders byte-identical (dead binding);\n * - `E_NO_MUTABLE_BINDING` — nothing to mutate and no `mutate` given.\n * - The SDK's `SHOP_NOT_PROPAGATED` passes through: a freshly minted shop\n * that never becomes visible is an environment fault, named as such.\n *\n * This talks to the REAL realtime render path (network). Use it in nightly /\n * acceptance jobs and agent verify loops — per-PR unit tests should mock the\n * SDK transport instead (plan: per-PR replay, nightly network-real).\n */\n\nimport {\n RenderSession,\n fetchMockupImage,\n mintRealtimeGrant,\n type MockupResult,\n type RealtimeGrant,\n type RenderProduct,\n} from '@snowcone-app/sdk';\nimport { isSnowconeSdkError } from '@snowcone-app/sdk';\nimport { serializeStateForServer } from '../rendering/serialize-for-server.js';\nimport { EditorError } from '../errors/editorError.js';\nimport type { DesignState } from '../components/editor/designStateStore.js';\n\n/** Anything `design()` produces (or a compatible shape). */\nexport interface RenderableDesign {\n toJSON(): DesignState;\n}\n\nexport interface AssertRendersOptions {\n /** Publishable shop id (= shop.id). */\n shop: string;\n /** Secret API key (sk_) for the grant; omit for the keyless publishable\n * path (sandbox/trial shops — the F1 bootstrap default). */\n apiKey?: string;\n /** Pre-fetched grant or grant factory — overrides the mint entirely. */\n grant?: RealtimeGrant | (() => Promise<RealtimeGrant>);\n /** Backend base for the keyless/sk grant mint (defaults to prod). */\n grantBase?: string;\n /** Override the realtime WS endpoint. */\n wsUrl?: string;\n /**\n * Produce the mutated state for the second render. Default: rewrite the\n * text of EVERY named text layer to a sentinel (`ASSERT_RENDERS_<name>`).\n * Throws `E_NO_MUTABLE_BINDING` if the design has no named text layer and\n * no override is given.\n */\n mutate?: (seed: DesignState) => DesignState;\n /** Per-render timeout (ms). Default 30_000. */\n timeoutMs?: number;\n /** Override fetch for the image downloads (tests). */\n fetchImpl?: typeof fetch;\n}\n\nexport interface AssertRendersResult {\n /** The seed render's mockup URL + encoded size. */\n seed: { url: string; bytes: number; kind: string };\n /** The mutated render's mockup URL + encoded size. */\n mutated: { url: string; bytes: number; kind: string };\n}\n\n/** Default mutation: every NAMED text layer gets a sentinel value. */\nexport function mutateNamedTextLayers(seed: DesignState): DesignState {\n let mutatedAny = false;\n const elements = seed.elements.map((el) => {\n const name = (el as { name?: string }).name;\n const text = (el as { text?: string }).text;\n if (!name || typeof text !== 'string') return el;\n mutatedAny = true;\n return { ...el, text: `ASSERT_RENDERS_${name.toUpperCase()}` } as typeof el;\n });\n if (!mutatedAny) {\n throw new EditorError(\n 'E_NO_MUTABLE_BINDING',\n 'this design has no NAMED text layer to mutate for the two-render check. Name one ' +\n '(design().text(\"…\", { name: \"headline\" })) or pass opts.mutate to define the mutation.',\n { docSlug: 'embed-mode' },\n );\n }\n return { ...seed, elements };\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Render one state on a FRESH session and resolve the first mockup result.\n * One session per render on purpose: a session keeps emitting trailing mockup\n * results (multi-mockup products emit per scene), so reusing it for the\n * second render could resolve the mutated phase with a stale seed emission —\n * and with content-addressed delivery a dead binding makes the two\n * indistinguishable by URL. A fresh session has no history to confuse.\n */\nasync function renderOnce(\n makeSession: () => RenderSession,\n placement: string,\n state: DesignState,\n timeoutMs: number,\n label: 'seed' | 'mutated',\n): Promise<MockupResult> {\n const session = makeSession();\n try {\n return await new Promise<MockupResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new EditorError('E_RENDER_FAILED', `the ${label} render produced no mockup within ${timeoutMs}ms.`, {\n phase: label,\n timeoutMs,\n }),\n );\n }, timeoutMs);\n session.onMockups((results) => {\n const withImage = results.find((r) => r.imageUrl);\n if (!withImage) return;\n clearTimeout(timer);\n resolve(withImage);\n });\n session.onError((message) => {\n clearTimeout(timer);\n reject(new EditorError('E_RENDER_FAILED', `the ${label} render failed: ${message}`, { phase: label, error: message }));\n });\n void session.renderState(placement, serializeStateForServer(state)).catch((err) => {\n clearTimeout(timer);\n const message = err instanceof Error ? err.message : String(err);\n reject(new EditorError('E_RENDER_FAILED', `the ${label} render failed to dispatch: ${message}`, { phase: label, error: message }));\n });\n });\n } finally {\n session.close();\n }\n}\n\n/** Fetch + validate one rendered image, translating SDK failures (asset-403 →\n * `E_ASSET_FORBIDDEN`) into the editor error contract. */\nasync function fetchRender(url: string, label: 'seed' | 'mutated', opts: AssertRendersOptions): Promise<{ bytes: Uint8Array; kind: string }> {\n try {\n const img = await fetchMockupImage(url, {\n ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),\n });\n return { bytes: img.bytes, kind: img.kind };\n } catch (err) {\n if (isSnowconeSdkError(err)) {\n if (err.code === 'ASSET_FORBIDDEN') {\n throw new EditorError(\n 'E_ASSET_FORBIDDEN',\n `the shop's asset-origin allowlist denied an asset in this design (${label} render). ` +\n `Allow the asset's origin on the shop (or host it on the shop's storage origin).`,\n { phase: label, url, failedAssets: err.data.failedAssets, docSlug: 'embed-mode' },\n );\n }\n if (err.code === 'SHOP_NOT_PROPAGATED') throw err; // already structured + named\n throw new EditorError('E_RENDER_FAILED', `the ${label} render's image fetch failed: ${err.message}`, {\n phase: label,\n url,\n ...err.data,\n });\n }\n throw err;\n }\n}\n\n/**\n * Prove a design + product render AND that the design's bindings are alive\n * (two-render contract). See the module doc for the failure catalog.\n *\n * Stable since ADR-0085 P4 (the default editor is rebuilt from this public\n * surface, CI-gated); additive-only from here.\n */\nexport async function assertRenders(\n design: RenderableDesign,\n product: RenderProduct,\n opts: AssertRendersOptions,\n): Promise<AssertRendersResult> {\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const seed = design.toJSON();\n const placement = seed.activeArtboard;\n const mutated = (opts.mutate ?? mutateNamedTextLayers)(seed);\n\n const grant = opts.grant;\n const getToken =\n typeof grant === 'function'\n ? grant\n : grant\n ? () => Promise.resolve(grant)\n : () =>\n mintRealtimeGrant({\n shop: opts.shop,\n ...(opts.apiKey ? { apiKey: opts.apiKey } : {}),\n ...(opts.grantBase ? { base: opts.grantBase } : {}),\n ...(opts.fetchImpl ? { fetch: opts.fetchImpl } : {}),\n });\n\n const makeSession = () =>\n new RenderSession({\n shop: opts.shop,\n product,\n ...(opts.wsUrl ? { wsUrl: opts.wsUrl } : {}),\n getToken,\n });\n\n const seedResult = await renderOnce(makeSession, placement, seed, timeoutMs, 'seed');\n const seedImage = await fetchRender(seedResult.imageUrl!, 'seed', opts);\n\n const mutatedResult = await renderOnce(makeSession, placement, mutated, timeoutMs, 'mutated');\n const mutatedImage = await fetchRender(mutatedResult.imageUrl!, 'mutated', opts);\n\n if (bytesEqual(seedImage.bytes, mutatedImage.bytes)) {\n throw new EditorError(\n 'E_BINDING_INERT',\n 'the mutated render is byte-identical to the seed render — the mutation never reached ' +\n 'the server, so the binding under test is DEAD. Usual causes: the mutated layer name ' +\n \"doesn't exist in the design (check design.layer() names), or opts.mutate returned the \" +\n 'seed unchanged.',\n {\n seedUrl: seedResult.imageUrl,\n mutatedUrl: mutatedResult.imageUrl,\n bytes: seedImage.bytes.byteLength,\n docSlug: 'embed-mode',\n },\n );\n }\n\n return {\n seed: { url: seedResult.imageUrl!, bytes: seedImage.bytes.byteLength, kind: seedImage.kind },\n mutated: { url: mutatedResult.imageUrl!, bytes: mutatedImage.bytes.byteLength, kind: mutatedImage.kind },\n };\n}\n","/**\n * Deterministic failure-mode fixtures (ADR-0085 plan, P2 — the magic-test-card\n * equivalent): trigger each commerce/render failure ON DEMAND so integrators\n * build their error UI against real failures instead of waiting for prod to\n * produce one.\n *\n * These are the CLIENT-ACHIEVABLE fixtures — each works against any shop\n * (including a fresh sandbox shop) with no server cooperation:\n *\n * - {@link test403Design} — asset-origin allowlist denial. References an\n * asset on an origin no shop allowlists, so the server's asset fetch is\n * denied deterministically. Surfaces as a render error (live path) or\n * `E_ASSET_FORBIDDEN` from `assertRenders`.\n * - {@link testIncompleteDesign} — Buy completeness failure. A design whose\n * only personalization layer still carries its seed value, so\n * `<Editor.Buy>` gates with `layer-seed:<name>` (and `layer:<name>` once\n * cleared). Pair with `<Editor.TextField layer={d.layer('name')} />`.\n * - {@link failingGrant} — grant failure. A grant factory that always\n * rejects; pass as `<Editor.Root grant={failingGrant()}>` to exercise the\n * \"grant fetch fails\" surface (`onRenderError` + the Preview's\n * `data-sc-part=\"preview-error\"` state).\n *\n * Server-triggered fixtures (a `test-slow` product for the timeout path, a\n * forced render-error token) need render-edge cooperation and are tracked\n * separately — they are NOT silently stubbed here.\n */\n\nimport { design } from '../design/design-builder.js';\nimport type { RealtimeGrant } from '@snowcone-app/sdk';\n\n/**\n * An origin that is deliberately on NO shop's asset-origin allowlist. The\n * `.invalid` TLD is reserved (RFC 2606) — it can never resolve, and the\n * allowlist check denies it before any fetch is attempted, so the failure is\n * deterministic and free.\n */\nexport const TEST_403_ASSET_URL = 'https://assets.test-403.invalid/denied.png';\n\n/**\n * A design whose background asset the shop's allowlist is guaranteed to deny —\n * the deterministic allowlist-denial (403) trigger.\n */\nexport function test403Design() {\n return design({ name: 'Front', width: 1000, height: 1000 })\n .background(TEST_403_ASSET_URL, { aspectRatio: 1, name: 'art' })\n .text('ALLOWLIST DENIAL FIXTURE', { anchor: 'top', name: 'headline' });\n}\n\n/**\n * A design that gates `<Editor.Buy>` out of the box: its one personalization\n * layer (`'name'`, seed `'YOUR NAME'`) is required and still equals its seed,\n * so Buy renders `data-sc-buy-state=\"incomplete\"` with\n * `data-sc-buy-missing=\"layer-seed:name\"` until a real value is typed.\n */\nexport function testIncompleteDesign() {\n return design({ name: 'Front', width: 1000, height: 1000 })\n .text('YOUR NAME', { anchor: 'center', name: 'name' });\n}\n\n/**\n * A grant factory that always rejects — the deterministic \"grant fetch\n * failed\" trigger. The rejection reason is stable for assertions.\n */\nexport function failingGrant(reason = 'failing-grant fixture: grant endpoint unavailable'): () => Promise<RealtimeGrant> {\n return () => Promise.reject(new Error(reason));\n}\n"],"names":["MockEditorProvider","children","_initialElements","initialArtboards","_initialSelection","_initialMultiSelection","initialArtboardConfig","_artboardManager","useState","manager","ArtboardManager","artboard","ThemeProvider","jsx","EditorProvider","MockEditorProviderWithState","createMockElement","transformType","overrides","transformDef","getTransformById","createMockImageElement","ImageElement","createMockArtboard","ArtboardElement","waitFor","condition","timeout","interval","startTime","resolve","waitForExport","_artboardId","dataUrl","simulateKeyPress","key","options","event","createMockCanvas","width","height","canvas","assertElementProperties","element","expected","value","getElementBounds","bbox","mutateNamedTextLayers","seed","mutatedAny","elements","el","name","text","EditorError","bytesEqual","a","b","i","renderOnce","makeSession","placement","state","timeoutMs","label","session","reject","timer","results","withImage","r","message","serializeStateForServer","err","fetchRender","url","opts","img","fetchMockupImage","isSnowconeSdkError","assertRenders","design","product","mutated","grant","getToken","mintRealtimeGrant","RenderSession","seedResult","seedImage","mutatedResult","mutatedImage","TEST_403_ASSET_URL","test403Design","testIncompleteDesign","failingGrant","reason"],"mappings":"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"}
@@ -568,6 +568,15 @@ export type ClipShape = 'rectangle' | 'circle' | {
568
568
  baseHeight?: number;
569
569
  }>;
570
570
  };
571
+ /**
572
+ * Pacing role of an artboard in a multi-page template (ADR-0086 albums).
573
+ *
574
+ * Client/template metadata: auto-fill uses it to know which spreads clone as
575
+ * the page count grows (`repeating`) and which stay fixed (`cover` /
576
+ * `front-matter` / `back-matter`). It never reaches the server render wire —
577
+ * `serializeStateForServer` strips it like the element `artboard` membership tag.
578
+ */
579
+ export type ArtboardRole = 'cover' | 'front-matter' | 'repeating' | 'back-matter';
571
580
  export interface ArtboardConfig {
572
581
  id?: string;
573
582
  name?: string;
@@ -581,6 +590,7 @@ export interface ArtboardConfig {
581
590
  exportBackground?: boolean;
582
591
  elementIds?: string[];
583
592
  transformType?: 'artboard';
593
+ role?: ArtboardRole;
584
594
  clipShape?: ClipShape;
585
595
  previewBackgroundColor?: string;
586
596
  distressTexture?: ArtboardDistressTexture;
@@ -1,4 +1,4 @@
1
- import { y as r } from "./FontAnalyzer-DIlqmyuf.js";
1
+ import { y as r } from "./FontAnalyzer-BpsTYowD.js";
2
2
  import { useState as a, useEffect as d } from "react";
3
3
  const u = "https://api.snowcone.app";
4
4
  function h(e) {
@@ -58,4 +58,4 @@ export {
58
58
  l as p,
59
59
  p as u
60
60
  };
61
- //# sourceMappingURL=usePieceGuideBundle-DVOISsB1.js.map
61
+ //# sourceMappingURL=usePieceGuideBundle-Ctnu5k2D.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePieceGuideBundle-DVOISsB1.js","sources":["../src/components/embed-realtime/usePieceGuideBundle.ts"],"sourcesContent":["'use client';\n\n/**\n * usePieceGuideBundle — fetch a product's printable-area guides for the embed.\n *\n * Some products hide part of the print file under hardware (a phone case's\n * camera module, a notebook's spine). snowcone.app's own editor draws the\n * printable-area / dead-zone overlay from the product's piece guides;\n * `<RealtimeEmbed>` uses this hook so embedders get the same overlay with\n * zero config — without it, a buyer can edit artwork the mockup will never\n * show, with no signal anywhere (#566).\n *\n * The endpoint is a public read (geometry only — piece rects, safe areas,\n * authored guides; same exposure class as the public catalog). Errors degrade\n * silently to \"no guides\": the editor keeps working, the overlay just doesn't\n * appear — same posture as snowcone.app's own loader.\n *\n * @module\n */\n\nimport { useEffect, useState } from 'react';\nimport type { PieceGuidesConfig } from '../embed/SnowconeCanvas.js';\nimport type {\n PieceGuide,\n PieceGuideRendererPiece,\n PieceGuideRendererStyle,\n} from '../../rendering/PieceGuideRenderer.js';\n\n/** Backend base hosting `GET /printables/by-gvid/:gvid/piece-guides`. */\nexport const PIECE_GUIDES_API_BASE = 'https://api.snowcone.app';\n\n/** Wire shape of the piece-guides endpoint (the consumer-safe subset). */\ninterface RawPieceGuidesResponse {\n artboardWidth: number;\n artboardHeight: number;\n /** Inches→px conversion factor for `safeAreaInsetInches`. */\n templateDpi: number;\n pieces: Array<{\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n rotation?: number;\n safeAreaInsetInches?: number;\n }>;\n pieceGuides?: Record<string, PieceGuide>;\n pieceGuidesStyle?: PieceGuideRendererStyle;\n}\n\n/** A fetched bundle: the overlay config plus the artboard frame it was authored in. */\nexport interface PieceGuideBundle {\n /** The print-file frame the piece coords live in. */\n artboardWidth: number;\n artboardHeight: number;\n config: PieceGuidesConfig;\n}\n\n/** Convert the wire response into a renderer-ready bundle (inches → px, etc.). */\nexport function transformPieceGuides(raw: RawPieceGuidesResponse): PieceGuideBundle {\n const pieces: PieceGuideRendererPiece[] = raw.pieces.map((p) => ({\n id: p.id,\n x: p.x,\n y: p.y,\n width: p.width,\n height: p.height,\n rotation: typeof p.rotation === 'number' ? p.rotation : undefined,\n safeAreaInsetPx:\n typeof p.safeAreaInsetInches === 'number' && p.safeAreaInsetInches > 0\n ? p.safeAreaInsetInches * raw.templateDpi\n : undefined,\n }));\n return {\n artboardWidth: raw.artboardWidth,\n artboardHeight: raw.artboardHeight,\n config: {\n pieces,\n guides: raw.pieceGuides ?? {},\n style: raw.pieceGuidesStyle,\n },\n };\n}\n\n/**\n * Gate the bundle on the design's artboard matching the print-file frame the\n * guides were authored in — the same gate snowcone.app's editor applies. A\n * design authored at other dims would render the overlay misaligned, so it's\n * skipped (with a dev hint, since the fix — author at the placement's dims —\n * is on the embedder's side).\n */\nexport function pieceGuidesForArtboard(\n bundle: PieceGuideBundle,\n artboard: { width: number; height: number } | undefined,\n): PieceGuidesConfig | undefined {\n if (!artboard) return undefined;\n if (artboard.width !== bundle.artboardWidth || artboard.height !== bundle.artboardHeight) {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n console.warn(\n `[RealtimeEmbed] This product has printable-area guides (authored at ` +\n `${bundle.artboardWidth}×${bundle.artboardHeight}), but the design's artboard is ` +\n `${artboard.width}×${artboard.height} — the dead-zone overlay is skipped because the ` +\n `coordinates wouldn't line up. Author the design at the placement's dimensions to get it.`,\n );\n }\n return undefined;\n }\n return bundle.config;\n}\n\n/**\n * Fetch the piece-guide bundle for a variant (gvid). Returns null while\n * loading, when `gvid` is undefined, and on any error (silent degrade).\n */\nexport function usePieceGuideBundle(\n gvid: string | undefined,\n apiBase: string = PIECE_GUIDES_API_BASE,\n): PieceGuideBundle | null {\n const [bundle, setBundle] = useState<PieceGuideBundle | null>(null);\n\n useEffect(() => {\n if (!gvid) {\n setBundle(null);\n return;\n }\n let cancelled = false;\n fetch(`${apiBase}/printables/by-gvid/${encodeURIComponent(gvid)}/piece-guides`)\n .then(async (res) => {\n if (!res.ok) throw new Error(`status ${res.status}`);\n return (await res.json()) as RawPieceGuidesResponse;\n })\n .then((raw) => {\n if (!cancelled) setBundle(transformPieceGuides(raw));\n })\n .catch(() => {\n // No guides is a degraded state, not a failure — the editor keeps\n // working, the overlay just doesn't appear.\n if (!cancelled) setBundle(null);\n });\n return () => {\n cancelled = true;\n };\n }, [gvid, apiBase]);\n\n return bundle;\n}\n"],"names":["PIECE_GUIDES_API_BASE","transformPieceGuides","raw","pieces","p","pieceGuidesForArtboard","bundle","artboard","process","usePieceGuideBundle","gvid","apiBase","setBundle","useState","useEffect","cancelled","res"],"mappings":";;AA6BO,MAAMA,IAAwB;AA8B9B,SAASC,EAAqBC,GAA+C;AAClF,QAAMC,IAAoCD,EAAI,OAAO,IAAI,CAACE,OAAO;AAAA,IAC/D,IAAIA,EAAE;AAAA,IACN,GAAGA,EAAE;AAAA,IACL,GAAGA,EAAE;AAAA,IACL,OAAOA,EAAE;AAAA,IACT,QAAQA,EAAE;AAAA,IACV,UAAU,OAAOA,EAAE,YAAa,WAAWA,EAAE,WAAW;AAAA,IACxD,iBACE,OAAOA,EAAE,uBAAwB,YAAYA,EAAE,sBAAsB,IACjEA,EAAE,sBAAsBF,EAAI,cAC5B;AAAA,EAAA,EACN;AACF,SAAO;AAAA,IACL,eAAeA,EAAI;AAAA,IACnB,gBAAgBA,EAAI;AAAA,IACpB,QAAQ;AAAA,MACN,QAAAC;AAAA,MACA,QAAQD,EAAI,eAAe,CAAA;AAAA,MAC3B,OAAOA,EAAI;AAAA,IAAA;AAAA,EACb;AAEJ;AASO,SAASG,EACdC,GACAC,GAC+B;;AAC/B,MAAKA,GACL;AAAA,QAAIA,EAAS,UAAUD,EAAO,iBAAiBC,EAAS,WAAWD,EAAO,gBAAgB;AACxF,MAAI,OAAOE,IAAY,SAAeA,IAAAA,EAAQ,QAARA,gBAAAA,EAAa,cAAa,gBAC9D,QAAQ;AAAA,QACN,uEACKF,EAAO,aAAa,IAAIA,EAAO,cAAc,mCAC7CC,EAAS,KAAK,IAAIA,EAAS,MAAM;AAAA,MAAA;AAI1C;AAAA,IACF;AACA,WAAOD,EAAO;AAAA;AAChB;AAMO,SAASG,EACdC,GACAC,IAAkBX,GACO;AACzB,QAAM,CAACM,GAAQM,CAAS,IAAIC,EAAkC,IAAI;AAElE,SAAAC,EAAU,MAAM;AACd,QAAI,CAACJ,GAAM;AACT,MAAAE,EAAU,IAAI;AACd;AAAA,IACF;AACA,QAAIG,IAAY;AAChB,iBAAM,GAAGJ,CAAO,uBAAuB,mBAAmBD,CAAI,CAAC,eAAe,EAC3E,KAAK,OAAOM,MAAQ;AACnB,UAAI,CAACA,EAAI,GAAI,OAAM,IAAI,MAAM,UAAUA,EAAI,MAAM,EAAE;AACnD,aAAQ,MAAMA,EAAI,KAAA;AAAA,IACpB,CAAC,EACA,KAAK,CAACd,MAAQ;AACb,MAAKa,KAAWH,EAAUX,EAAqBC,CAAG,CAAC;AAAA,IACrD,CAAC,EACA,MAAM,MAAM;AAGX,MAAKa,KAAWH,EAAU,IAAI;AAAA,IAChC,CAAC,GACI,MAAM;AACX,MAAAG,IAAY;AAAA,IACd;AAAA,EACF,GAAG,CAACL,GAAMC,CAAO,CAAC,GAEXL;AACT;"}
1
+ {"version":3,"file":"usePieceGuideBundle-Ctnu5k2D.js","sources":["../src/components/embed-realtime/usePieceGuideBundle.ts"],"sourcesContent":["'use client';\n\n/**\n * usePieceGuideBundle — fetch a product's printable-area guides for the embed.\n *\n * Some products hide part of the print file under hardware (a phone case's\n * camera module, a notebook's spine). snowcone.app's own editor draws the\n * printable-area / dead-zone overlay from the product's piece guides;\n * `<RealtimeEmbed>` uses this hook so embedders get the same overlay with\n * zero config — without it, a buyer can edit artwork the mockup will never\n * show, with no signal anywhere (#566).\n *\n * The endpoint is a public read (geometry only — piece rects, safe areas,\n * authored guides; same exposure class as the public catalog). Errors degrade\n * silently to \"no guides\": the editor keeps working, the overlay just doesn't\n * appear — same posture as snowcone.app's own loader.\n *\n * @module\n */\n\nimport { useEffect, useState } from 'react';\nimport type { PieceGuidesConfig } from '../embed/SnowconeCanvas.js';\nimport type {\n PieceGuide,\n PieceGuideRendererPiece,\n PieceGuideRendererStyle,\n} from '../../rendering/PieceGuideRenderer.js';\n\n/** Backend base hosting `GET /printables/by-gvid/:gvid/piece-guides`. */\nexport const PIECE_GUIDES_API_BASE = 'https://api.snowcone.app';\n\n/** Wire shape of the piece-guides endpoint (the consumer-safe subset). */\ninterface RawPieceGuidesResponse {\n artboardWidth: number;\n artboardHeight: number;\n /** Inches→px conversion factor for `safeAreaInsetInches`. */\n templateDpi: number;\n pieces: Array<{\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n rotation?: number;\n safeAreaInsetInches?: number;\n }>;\n pieceGuides?: Record<string, PieceGuide>;\n pieceGuidesStyle?: PieceGuideRendererStyle;\n}\n\n/** A fetched bundle: the overlay config plus the artboard frame it was authored in. */\nexport interface PieceGuideBundle {\n /** The print-file frame the piece coords live in. */\n artboardWidth: number;\n artboardHeight: number;\n config: PieceGuidesConfig;\n}\n\n/** Convert the wire response into a renderer-ready bundle (inches → px, etc.). */\nexport function transformPieceGuides(raw: RawPieceGuidesResponse): PieceGuideBundle {\n const pieces: PieceGuideRendererPiece[] = raw.pieces.map((p) => ({\n id: p.id,\n x: p.x,\n y: p.y,\n width: p.width,\n height: p.height,\n rotation: typeof p.rotation === 'number' ? p.rotation : undefined,\n safeAreaInsetPx:\n typeof p.safeAreaInsetInches === 'number' && p.safeAreaInsetInches > 0\n ? p.safeAreaInsetInches * raw.templateDpi\n : undefined,\n }));\n return {\n artboardWidth: raw.artboardWidth,\n artboardHeight: raw.artboardHeight,\n config: {\n pieces,\n guides: raw.pieceGuides ?? {},\n style: raw.pieceGuidesStyle,\n },\n };\n}\n\n/**\n * Gate the bundle on the design's artboard matching the print-file frame the\n * guides were authored in — the same gate snowcone.app's editor applies. A\n * design authored at other dims would render the overlay misaligned, so it's\n * skipped (with a dev hint, since the fix — author at the placement's dims —\n * is on the embedder's side).\n */\nexport function pieceGuidesForArtboard(\n bundle: PieceGuideBundle,\n artboard: { width: number; height: number } | undefined,\n): PieceGuidesConfig | undefined {\n if (!artboard) return undefined;\n if (artboard.width !== bundle.artboardWidth || artboard.height !== bundle.artboardHeight) {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n console.warn(\n `[RealtimeEmbed] This product has printable-area guides (authored at ` +\n `${bundle.artboardWidth}×${bundle.artboardHeight}), but the design's artboard is ` +\n `${artboard.width}×${artboard.height} — the dead-zone overlay is skipped because the ` +\n `coordinates wouldn't line up. Author the design at the placement's dimensions to get it.`,\n );\n }\n return undefined;\n }\n return bundle.config;\n}\n\n/**\n * Fetch the piece-guide bundle for a variant (gvid). Returns null while\n * loading, when `gvid` is undefined, and on any error (silent degrade).\n */\nexport function usePieceGuideBundle(\n gvid: string | undefined,\n apiBase: string = PIECE_GUIDES_API_BASE,\n): PieceGuideBundle | null {\n const [bundle, setBundle] = useState<PieceGuideBundle | null>(null);\n\n useEffect(() => {\n if (!gvid) {\n setBundle(null);\n return;\n }\n let cancelled = false;\n fetch(`${apiBase}/printables/by-gvid/${encodeURIComponent(gvid)}/piece-guides`)\n .then(async (res) => {\n if (!res.ok) throw new Error(`status ${res.status}`);\n return (await res.json()) as RawPieceGuidesResponse;\n })\n .then((raw) => {\n if (!cancelled) setBundle(transformPieceGuides(raw));\n })\n .catch(() => {\n // No guides is a degraded state, not a failure — the editor keeps\n // working, the overlay just doesn't appear.\n if (!cancelled) setBundle(null);\n });\n return () => {\n cancelled = true;\n };\n }, [gvid, apiBase]);\n\n return bundle;\n}\n"],"names":["PIECE_GUIDES_API_BASE","transformPieceGuides","raw","pieces","p","pieceGuidesForArtboard","bundle","artboard","process","usePieceGuideBundle","gvid","apiBase","setBundle","useState","useEffect","cancelled","res"],"mappings":";;AA6BO,MAAMA,IAAwB;AA8B9B,SAASC,EAAqBC,GAA+C;AAClF,QAAMC,IAAoCD,EAAI,OAAO,IAAI,CAACE,OAAO;AAAA,IAC/D,IAAIA,EAAE;AAAA,IACN,GAAGA,EAAE;AAAA,IACL,GAAGA,EAAE;AAAA,IACL,OAAOA,EAAE;AAAA,IACT,QAAQA,EAAE;AAAA,IACV,UAAU,OAAOA,EAAE,YAAa,WAAWA,EAAE,WAAW;AAAA,IACxD,iBACE,OAAOA,EAAE,uBAAwB,YAAYA,EAAE,sBAAsB,IACjEA,EAAE,sBAAsBF,EAAI,cAC5B;AAAA,EAAA,EACN;AACF,SAAO;AAAA,IACL,eAAeA,EAAI;AAAA,IACnB,gBAAgBA,EAAI;AAAA,IACpB,QAAQ;AAAA,MACN,QAAAC;AAAA,MACA,QAAQD,EAAI,eAAe,CAAA;AAAA,MAC3B,OAAOA,EAAI;AAAA,IAAA;AAAA,EACb;AAEJ;AASO,SAASG,EACdC,GACAC,GAC+B;;AAC/B,MAAKA,GACL;AAAA,QAAIA,EAAS,UAAUD,EAAO,iBAAiBC,EAAS,WAAWD,EAAO,gBAAgB;AACxF,MAAI,OAAOE,IAAY,SAAeA,IAAAA,EAAQ,QAARA,gBAAAA,EAAa,cAAa,gBAC9D,QAAQ;AAAA,QACN,uEACKF,EAAO,aAAa,IAAIA,EAAO,cAAc,mCAC7CC,EAAS,KAAK,IAAIA,EAAS,MAAM;AAAA,MAAA;AAI1C;AAAA,IACF;AACA,WAAOD,EAAO;AAAA;AAChB;AAMO,SAASG,EACdC,GACAC,IAAkBX,GACO;AACzB,QAAM,CAACM,GAAQM,CAAS,IAAIC,EAAkC,IAAI;AAElE,SAAAC,EAAU,MAAM;AACd,QAAI,CAACJ,GAAM;AACT,MAAAE,EAAU,IAAI;AACd;AAAA,IACF;AACA,QAAIG,IAAY;AAChB,iBAAM,GAAGJ,CAAO,uBAAuB,mBAAmBD,CAAI,CAAC,eAAe,EAC3E,KAAK,OAAOM,MAAQ;AACnB,UAAI,CAACA,EAAI,GAAI,OAAM,IAAI,MAAM,UAAUA,EAAI,MAAM,EAAE;AACnD,aAAQ,MAAMA,EAAI,KAAA;AAAA,IACpB,CAAC,EACA,KAAK,CAACd,MAAQ;AACb,MAAKa,KAAWH,EAAUX,EAAqBC,CAAG,CAAC;AAAA,IACrD,CAAC,EACA,MAAM,MAAM;AAGX,MAAKa,KAAWH,EAAU,IAAI;AAAA,IAChC,CAAC,GACI,MAAM;AACX,MAAAG,IAAY;AAAA,IACd;AAAA,EACF,GAAG,CAACL,GAAMC,CAAO,CAAC,GAEXL;AACT;"}