@snowcone-app/canvas 0.20.0 → 0.21.1

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 (45) hide show
  1. package/README.md +129 -13
  2. package/STYLING_CONTRACT.md +427 -0
  3. package/dist/{EditorCanvasImpl-UBKdG0c0.js → EditorCanvasImpl-2ZgIUVpK.js} +4 -4
  4. package/dist/{EditorCanvasImpl-UBKdG0c0.js.map → EditorCanvasImpl-2ZgIUVpK.js.map} +1 -1
  5. package/dist/{SnowconeCanvas-B01o5LmS.js → SnowconeCanvas-DqZicdxK.js} +2 -2
  6. package/dist/{SnowconeCanvas-B01o5LmS.js.map → SnowconeCanvas-DqZicdxK.js.map} +1 -1
  7. package/dist/UnifiedFontService-B1PGVu8O.js.map +1 -1
  8. package/dist/components/editor/EditorAdd.d.ts +2 -1
  9. package/dist/components/editor/EditorBuy.d.ts +2 -1
  10. package/dist/components/editor/EditorCanvas.d.ts +2 -1
  11. package/dist/components/editor/EditorControls.d.ts +2 -1
  12. package/dist/components/editor/EditorHistory.d.ts +2 -1
  13. package/dist/components/editor/EditorLayers.d.ts +2 -1
  14. package/dist/components/editor/EditorMenu.d.ts +2 -1
  15. package/dist/components/editor/EditorPreview.d.ts +2 -1
  16. package/dist/components/editor/EditorRoot.d.ts +2 -1
  17. package/dist/components/editor/EditorTextField.d.ts +2 -1
  18. package/dist/components/editor/useProductSelection.d.ts +2 -1
  19. package/dist/components/embed-realtime/context.d.ts +1 -1
  20. package/dist/contexts/EditorContext.d.ts +1 -1
  21. package/dist/default-editor/SnowconeEditor.d.ts +2 -1
  22. package/dist/design/design-builder.d.ts +2 -2
  23. package/dist/{design-builder-DMKx9UXD.js → design-builder-CPF60RU2.js} +1 -2
  24. package/dist/design-builder-CPF60RU2.js.map +1 -0
  25. package/dist/editorErrors-CK1RISFa.js.map +1 -1
  26. package/dist/{embed-uALcVSYN.js → embed-2tp6Ue90.js} +4 -4
  27. package/dist/embed-2tp6Ue90.js.map +1 -0
  28. package/dist/embed.mjs +3 -3
  29. package/dist/index.mjs +3 -3
  30. package/dist/{template-fields-B9RagVpT.js → template-fields-FgImixyv.js} +2 -2
  31. package/dist/{template-fields-B9RagVpT.js.map → template-fields-FgImixyv.js.map} +1 -1
  32. package/dist/testing/assertRenders.d.ts +2 -1
  33. package/dist/testing.mjs +1 -1
  34. package/dist/testing.mjs.map +1 -1
  35. package/example/README.md +179 -0
  36. package/example/index.html +12 -0
  37. package/example/package.json +24 -0
  38. package/example/src/Widget.tsx +65 -0
  39. package/example/src/main.tsx +9 -0
  40. package/example/tsconfig.json +14 -0
  41. package/example/vite.config.ts +11 -0
  42. package/llms.txt +45 -0
  43. package/package.json +6 -2
  44. package/dist/design-builder-DMKx9UXD.js.map +0 -1
  45. package/dist/embed-uALcVSYN.js.map +0 -1
@@ -0,0 +1,179 @@
1
+ <!-- Assembled by scripts/assemble-example.mjs from the repo's canonical
2
+ fixture (packages/canvas/fixtures/no-canvas-widget) plus this standalone
3
+ scaffolding, so the example builds against the PUBLISHED package. -->
4
+
5
+ # Read me first (you are inside the npm tarball)
6
+
7
+ This directory is the canonical `Editor.*` integration, shipped inside
8
+ `@snowcone-app/canvas` so an agent in a host repo can onboard without any
9
+ network fetch:
10
+
11
+ - **`src/Widget.tsx` is the integration to copy** — `Editor.Root` +
12
+ `Editor.Preview` + `Editor.TextField` + `Editor.Buy`; the design-editor
13
+ chunk never loads in this composition.
14
+ - `npm install && npm run build` here typechecks + builds it against the
15
+ published package. `npm run dev` serves it — rendering live needs a real
16
+ shop key and a grant route (see the docs link below).
17
+ - **`../STYLING_CONTRACT.md`** is the generated restyling contract: every
18
+ `data-sc-part` value, the DOM each part renders, and every `.sc-root`
19
+ theming token — restyle without reading package source.
20
+ - `../llms.txt` indexes everything agent-facing in this tarball. The full
21
+ hosted docs are one fetch: <https://developers.snowcone.app/llms-full.txt>.
22
+
23
+ One caveat on the protocol below: it was written for contributors inside the
24
+ snowcone monorepo — its `fixtures/<your-name>` paths and `FakeRenderSession`
25
+ test harness are repo-internal, not shipped. In YOUR app the protocol is:
26
+ copy `src/Widget.tsx`, change the three things it marks, and verify with
27
+ `assertRenders` + `expectNoEditorErrors` from `@snowcone-app/canvas/testing`.
28
+
29
+ ---
30
+
31
+ # Personalization widget — the canonical `Editor.*` example (copy me)
32
+
33
+ This directory is a **template, not just an exhibit**. The fastest path to your
34
+ own widget — measured at **zero failed iterations** in a cold-start agent run —
35
+ is: copy this folder, change three things, write one test, run one command.
36
+
37
+ ## The protocol
38
+
39
+ 1. **Copy `src/Widget.tsx`** to `fixtures/<your-name>/src/Widget.tsx` (the
40
+ other files here — `index.html`, `vite.config.ts`, `src/main.tsx` — are
41
+ this fixture's CI plumbing; you don't need them unless you want a runnable
42
+ page).
43
+ 2. **Change three things in `src/Widget.tsx`:**
44
+ - the `design(…)` — your artboard size + layers (name every layer you want
45
+ to bind: `.text('…', { name: 'headline' })`);
46
+ - the `product` — your catalog ids (`productId`, `mockupIds`; get them from
47
+ `getProduct()` in `@snowcone-app/sdk` — the ids here are demo values).
48
+ A product with a COLOR variant axis also needs `variantId`, or the render
49
+ hangs (pass `options` too so it fails fast instead);
50
+ - the parts — `<Editor.TextField layer={d.layer('…')}/>` per bindable
51
+ layer (`optional` ⇒ doesn't gate Buy), `<Editor.Preview/>`,
52
+ `<Editor.Buy onAddToCart={…}/>`.
53
+ 3. **Write your test** at `src/components/editor/__tests__/<your-name>.test.tsx`,
54
+ copying the pattern in `canonicalExample.test.tsx` (it imports this widget).
55
+ 4. **Run `pnpm -C packages/canvas verify`** — typecheck + tests + the
56
+ bundle-isolation gate. Green = your integration is proven, no browser needed.
57
+
58
+ `index.html` / `vite.config.ts` / `src/main.tsx` here are load-bearing only for
59
+ THIS canonical fixture (the CI import-graph check builds it). Your copy doesn't
60
+ need them unless you also want a runnable page — the test is the proof.
61
+
62
+ ## The test-harness contract (what the canonical test uses and why)
63
+
64
+ ```ts
65
+ import { FakeRenderSession, mountFlushed } from './testHarness.js';
66
+ import { expectNoEditorErrors } from '../../../testing/editorErrors.js';
67
+ ```
68
+
69
+ - **Two `vi.mock` blocks are required**, copy them verbatim:
70
+ - `@snowcone-app/sdk` → swaps `RenderSession` for `FakeRenderSession` (no
71
+ network; you drive mockups by hand);
72
+ - `@iconify/react` → the embed entry registers icons at module load, and the
73
+ registry's async timers leak across happy-dom tests without the stub.
74
+ - `FakeRenderSession.reset()` in `beforeEach`; `FakeRenderSession.last` is the
75
+ session your Root constructed.
76
+ - `session.renderState` is a spy — every render push, with the serialized
77
+ state as call arg `[1]` (assert your personalization landed there).
78
+ - `session.emitMockups([{ mockupId, imageUrl }])` simulates the server
79
+ answering. `mockupId` must be one of your `product.mockupIds` when your
80
+ Preview pins `mockupId=…`; an un-pinned Preview shows the first result.
81
+ - `mountFlushed(<YourWidget/>)` renders and flushes effects + any settled
82
+ lazy chunks; returns `{ container, unmount }`.
83
+ - `expectNoEditorErrors()` in `afterEach` — REQUIRED. Root's boundary contains
84
+ composition errors behind an error card in prod; this is what makes a
85
+ contained error fail your test instead of passing green.
86
+
87
+ ## Selector vocabulary (testing or driving the widget)
88
+
89
+ | Selector | Means |
90
+ |---|---|
91
+ | `[data-sc-part="preview"]` | the live mockup `<img>` |
92
+ | `[data-sc-part="preview-error"]` | the Preview's render/grant-failure surface (`role="alert"`, message in `data-sc-error`) — shown when a render fails before any mockup exists; a stale mockup beats a blank, so later errors keep the image |
93
+ | `[data-sc-part="textfield"]` | a bound text input; `aria-label` defaults to the **layer name**, so `input[aria-label="headline"]` targets one field |
94
+ | `[data-sc-part="buy"]` | the add-to-cart button |
95
+ | `[data-sc-buy-state]` | `ready` / `incomplete` / `busy` |
96
+ | `[data-sc-buy-missing]` | comma-joined reasons while incomplete, in a STABLE evaluation order you can assert on: `option:<key>`… (variant axes unselected, attribute order) → `variant` (complete selection matching no combination) → `layer:<name>` / `layer-seed:<name>` (per mounted field, mount order; `layer-seed` = still the placeholder) → `empty-design` → `quantity` |
97
+ | `[data-sc-part="error"]` | Root's contained-error card (`[data-sc-part="error-retry"]` when retryable) |
98
+
99
+ Multiple `<Editor.Preview/>` AND multiple `<Editor.TextField/>` under one Root
100
+ are valid (one field per named layer). One `<Editor.Canvas/>` max.
101
+
102
+ ## Variant selection + quantity (Buy-owned)
103
+
104
+ `Editor.Buy` owns variant selection and quantity; YOU render the variant UI
105
+ with `useProductSelection()` (same barrel). `select(key, choice)` re-resolves
106
+ the variant and the live preview follows — no remount, no session rebuild.
107
+ `<Editor.Buy defaultQuantity={2}/>` seeds quantity; `setQuantity` changes it
108
+ live. Don't drive variant switching through the Root `product.variantId` prop
109
+ — that re-keys the whole render session (initial `variantId` on a color
110
+ product is still required, it seeds the selection).
111
+
112
+ A variant product needs an `options` block on the Root `product` — pass your
113
+ catalog product's `options` verbatim; the shape is:
114
+
115
+ ```ts
116
+ const product = {
117
+ productId: 'BEEB77',
118
+ mockupIds: ['FV1qjO'],
119
+ placements: ['Front'],
120
+ variantId: 'gv-black-s', // initial selection (REQUIRED for color axes)
121
+ options: {
122
+ attributes: {
123
+ Color: { choices: [{ label: 'Black', hex: '#000' }, { label: 'White', hex: '#fff' }] },
124
+ Size: { choices: [{ label: 'S' }, { label: 'M' }] }, // no hex = non-color axis
125
+ },
126
+ combinations: [
127
+ { variantId: 'gv-black-s', Color: 'Black', Size: 'S', price: 1900 },
128
+ { variantId: 'gv-black-m', Color: 'Black', Size: 'M', price: 2100 },
129
+ ],
130
+ },
131
+ };
132
+ ```
133
+
134
+ **The currency rule:** `attributes[key].choices` are always `{ label, hex? }`
135
+ OBJECTS (use `hex` for swatches), but `select()`, `selection`, and
136
+ `disabledChoices` all speak the choice's **`label` string** — pass
137
+ `choice.label`, never the object. The hook's full surface:
138
+ `{ attributes, selection, select, disabledChoices, variantId, price,
139
+ quantity, setQuantity, complete, missing }`.
140
+
141
+ ```tsx
142
+ function VariantPicker() {
143
+ const { attributes, selection, select, disabledChoices, price, quantity, setQuantity } =
144
+ useProductSelection();
145
+ return (
146
+ <>
147
+ {Object.entries(attributes).map(([key, attr]) => (
148
+ <div key={key}>
149
+ {attr.choices.map(({ label = '', hex }) => (
150
+ <button
151
+ key={label}
152
+ style={hex ? { background: hex } : undefined}
153
+ disabled={disabledChoices[key]?.includes(label)}
154
+ aria-pressed={selection[key] === label}
155
+ onClick={() => select(key, label)}
156
+ >
157
+ {label}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ ))}
162
+ <button onClick={() => setQuantity(quantity + 1)}>qty {quantity} +</button>
163
+ </>
164
+ );
165
+ }
166
+ ```
167
+
168
+ ## Failure-mode fixtures (build your error UI against real failures)
169
+
170
+ From `@snowcone-app/canvas/testing`: `test403Design()` (asset-origin
171
+ allowlist denial — deterministic 403), `testIncompleteDesign()` (Buy gates
172
+ out of the box), `failingGrant()` (grant fetch rejects). For live contract
173
+ checks, `assertRenders(design, product, { shop })` renders twice (seed +
174
+ mutated bindings) and throws structured errors (`E_BINDING_INERT`,
175
+ `E_ASSET_FORBIDDEN`, `E_RENDER_FAILED`) — network-real, so nightly/acceptance
176
+ jobs, not per-PR unit tests.
177
+
178
+ All `Editor.*` parts are stable (ADR-0085 — the default editor is rebuilt from
179
+ them, CI-gated); the public surface is additive-only.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Personalization widget — no-canvas Editor.* composition</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "snowcone-personalization-widget-example",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "description": "The canonical @snowcone-app/canvas Editor.* example: a no-canvas personalization widget (live preview + bound text field + smart add-to-cart).",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "typecheck": "tsc --noEmit",
10
+ "build": "tsc --noEmit && vite build"
11
+ },
12
+ "dependencies": {
13
+ "@snowcone-app/canvas": "^0.21.1",
14
+ "react": "^19.1.0",
15
+ "react-dom": "^19.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.2.2",
19
+ "@types/react-dom": "^19.2.2",
20
+ "@vitejs/plugin-react": "^4.7.0",
21
+ "typescript": "^5.9.3",
22
+ "vite": "^5.4.21"
23
+ }
24
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * THE canonical personalization example (ADR-0085 / plan D7): a no-canvas
3
+ * merch widget — live product preview, ONE personalized text layer, smart
4
+ * add-to-cart — composed from the public `Editor.*` parts. The full canvas
5
+ * editor never loads; CI asserts this build's static import graph excludes
6
+ * the rendering engine (`scripts/check-import-graph.mjs`).
7
+ *
8
+ * This fixture is also the P0.5 hard-gate exhibit: the same public surface
9
+ * (`Editor.Root` + parts) serves both this composition and the full
10
+ * canvas-mounting editor (see the dev app's scrolling editor page).
11
+ */
12
+
13
+ import React from 'react';
14
+ import { Editor, design, type EditorBuyPayload } from '@snowcone-app/canvas/embed';
15
+
16
+ const SHOP_KEY = import.meta.env.VITE_SNOWCONE_SHOP ?? 'demo-shop';
17
+
18
+ // The design is the single source of truth: it seeds the first render AND
19
+ // mints the typed layer tokens the TextField binds with.
20
+ const nameDrop = design({ name: 'Front', width: 1480, height: 2328 })
21
+ .background('https://dqMwU8Jct3.storage.snowcone.app/demo/demo-008.jpg', {
22
+ aspectRatio: 1480 / 2328,
23
+ name: 'art',
24
+ })
25
+ .text('YOUR NAME', { anchor: 'top', name: 'name' });
26
+
27
+ const product = {
28
+ productId: 'BEEB77',
29
+ mockupIds: ['FV1qjO'],
30
+ placements: ['Front'],
31
+ // BEEB77 has a COLOR axis (Frame) — a color product REQUIRES variantId, or
32
+ // a live render times out. Read real ids from the catalog with getProduct().
33
+ variantId: 'Pv1sLC',
34
+ };
35
+
36
+ export function PersonalizationWidget({
37
+ onAddToCart,
38
+ }: {
39
+ /** Your cart handler. NOTE the payload's `designState` is the EDITOR shape
40
+ * (flat `elements` + artboard metadata) — persist it as-is. It is NOT the
41
+ * server render shape (`artboards[].elements`) the session pushes;
42
+ * `payload.serverRequest` carries that one if you need it. */
43
+ onAddToCart?: (payload: EditorBuyPayload) => void;
44
+ } = {}): React.ReactElement {
45
+ return (
46
+ <Editor.Root shop={SHOP_KEY} product={product} design={nameDrop} grantUrl="/api/realtime/grant">
47
+ <div style={{ display: 'grid', gap: 12, maxWidth: 320 }}>
48
+ <Editor.Preview alt="Your personalized tee" fallback={<div aria-busy="true">Rendering…</div>} />
49
+ <Editor.TextField layer={nameDrop.layer('name')} placeholder="Your name" />
50
+ <Editor.Buy
51
+ onAddToCart={
52
+ onAddToCart ??
53
+ (({ productId, quantity, designState }) => {
54
+ // The host's one line: persist the designState (the
55
+ // re-renderable source of truth) with the cart line.
56
+ console.log('add-to-cart', { productId, quantity, designState });
57
+ })
58
+ }
59
+ >
60
+ Add to cart · $24
61
+ </Editor.Buy>
62
+ </div>
63
+ </Editor.Root>
64
+ );
65
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { PersonalizationWidget } from './Widget.js';
4
+
5
+ createRoot(document.getElementById('root')!).render(
6
+ <React.StrictMode>
7
+ <PersonalizationWidget />
8
+ </React.StrictMode>,
9
+ );
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ // Standalone build of the canonical example against the PUBLISHED
5
+ // @snowcone-app/canvas (the in-repo fixture variant of this file resolves the
6
+ // package to workspace source instead; scripts/assemble-example.mjs swaps in
7
+ // this one when the example ships in the tarball or runs in the
8
+ // published-pins CI gate).
9
+ export default defineConfig({
10
+ plugins: [react()],
11
+ });
package/llms.txt ADDED
@@ -0,0 +1,45 @@
1
+ # @snowcone-app/canvas — agent index (ships inside the npm tarball)
2
+
3
+ Zero-network onboarding: everything listed here is included in this package.
4
+ Read these before fetching anything.
5
+
6
+ - example/ — THE canonical integration (a no-canvas personalization widget:
7
+ Editor.Root + Editor.Preview + Editor.TextField + Editor.Buy; the design
8
+ editor chunk never loads). Copy example/src/Widget.tsx into your app;
9
+ example/README.md has the copy protocol, the data-sc-* selector vocabulary,
10
+ variant selection + quantity via useProductSelection(), and the
11
+ failure-mode fixtures.
12
+ - STYLING_CONTRACT.md — GENERATED restyling contract: every data-sc-part
13
+ value, the DOM each part renders, and every .sc-root theming token.
14
+ Restyle the parts without reading package source.
15
+ - README.md — package overview: the canonical example first, then the
16
+ standalone design editor (SnowconeCanvas).
17
+
18
+ Entry points:
19
+
20
+ - @snowcone-app/canvas/embed — the Editor.* parts and SnowconeEditor (the
21
+ turnkey default composed from those same parts), the design() builder and
22
+ typed layer tokens, useProductSelection(), the turnkey
23
+ RealtimeEmbed/RealtimeCanvas/RealtimeMockup trio, and the chrome-building
24
+ hooks (useEditor, useLayers, useCommands, ...).
25
+ - @snowcone-app/canvas — SnowconeCanvas, the standalone design editor.
26
+ - @snowcone-app/canvas/testing — assertRenders (two-render binding-liveness
27
+ check), expectNoEditorErrors (REQUIRED in every integration test), and the
28
+ deterministic failure fixtures (test403Design, testIncompleteDesign,
29
+ failingGrant).
30
+ - @snowcone-app/canvas/style.css — the required stylesheet. Fully namespaced
31
+ (.sc-root scope); it cannot collide with host CSS.
32
+
33
+ Contracts to rely on:
34
+
35
+ - All Editor.* throws are structured: EditorError with a stable `code` +
36
+ `error.data` (valid names, offending values, docSlug). Assert on codes,
37
+ never message prose.
38
+ - Buy incompleteness is state, not an error: [data-sc-buy-state="incomplete"]
39
+ with machine-readable reason codes in [data-sc-buy-missing].
40
+ - The public embed surface is additive-only (CI-pinned).
41
+
42
+ Hosted docs (one fetch, the source of truth):
43
+ https://developers.snowcone.app/llms-full.txt
44
+ The primitives guide (grant-route framework variants, catalog ids, CMS/typed
45
+ layer notes): https://developers.snowcone.app/primitives
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/canvas",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "description": "Self-contained canvas editor component for e-commerce product customization",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -41,7 +41,10 @@
41
41
  },
42
42
  "files": [
43
43
  "dist",
44
- "README.md"
44
+ "README.md",
45
+ "llms.txt",
46
+ "STYLING_CONTRACT.md",
47
+ "example"
45
48
  ],
46
49
  "sideEffects": [
47
50
  "**/*.css",
@@ -185,6 +188,7 @@
185
188
  "typecheck": "tsc --noEmit -p tsconfig.json",
186
189
  "typecheck:verify": "tsc --noEmit -p tsconfig.verify.json",
187
190
  "generate": "node scripts/generate.js",
191
+ "gen:styling-contract": "STYLING_CONTRACT_WRITE=1 vitest run src/components/editor/__tests__/stylingContract.test.tsx",
188
192
  "test": "pnpm run typecheck:verify && pnpm run test:unit && pnpm run test:component && pnpm run test:ssr && pnpm run check:graph",
189
193
  "check:graph": "node scripts/check-import-graph.mjs",
190
194
  "verify": "pnpm run typecheck && pnpm run typecheck:verify && pnpm run test:unit && pnpm run test:component && pnpm run check:graph",
@@ -1 +0,0 @@
1
- {"version":3,"file":"design-builder-DMKx9UXD.js","sources":["../src/rendering/serialize-for-server.ts","../src/design/design-builder.ts"],"sourcesContent":["/**\n * Serialize canvas state (from onChange/toJSON) to the format the export worker expects.\n *\n * This bridges the gap between SnowconeCanvas.onChange() output (element configs with\n * transformType and nested transformData) and the export worker's expected format\n * (elements with 'type' field and flattened image properties).\n *\n * Use this when sending canvas state to a server-side renderer that runs the export\n * worker bundle. The client-side serializeForWorkerExport() requires live element\n * instances; this function works on plain JSON from onChange/toJSON.\n */\n\n/**\n * Wire-schema version of the server-render `canvas_state` payload (ADR-0079).\n *\n * This is a PUBLIC contract: third parties send `ServerRenderRequest` (produced\n * by {@link serializeStateForServer}) to Snowcone's realtime renderer. Bump the\n * MAJOR whenever the element/artboard shape changes incompatibly so a renderer\n * can reject states it cannot render instead of silently mis-rendering. Servers\n * treat a MISSING `schemaVersion` as `1` — legacy clients that predate the\n * stamp keep working.\n */\nexport const CANVAS_STATE_SCHEMA_VERSION = 1;\n\n// NOTE: this is a `type`, not an `interface`, ON PURPOSE. The SDK's\n// `RenderSession.renderState(placement, state)` takes a `RenderState` that has a\n// string index signature (`[key: string]: unknown`). A named `interface` does\n// NOT get TypeScript's implicit index signature, so an `interface` value is not\n// assignable to an indexed type — passing `serializeStateForServer()`'s result\n// straight into `renderState()` (the headline realtime flow) fails with TS2345\n// \"Index signature for type 'string' is missing\". A `type` alias DOES get the\n// implicit signature, so the flow type-checks with no cast. Do not convert this\n// back to `interface`. Pinned by @snowcone-app/contracts\n// (src/realtime-getting-started.contract.ts).\nexport type ServerRenderRequest = {\n /** Wire-schema version — see {@link CANVAS_STATE_SCHEMA_VERSION}. */\n schemaVersion: number;\n artboards: Array<{\n id: string;\n name: string;\n x: number;\n y: number;\n width: number;\n height: number;\n backgroundColor: string;\n exportBackground: boolean;\n elements: any[];\n distressTexture?: any;\n imageMask?: any;\n }>;\n};\n\n/**\n * Quantize a geometry value to a stable precision (0.01px).\n *\n * `SnowconeCanvas.onChange` fires on the render loop and reports element\n * geometry with sub-pixel float drift (e.g. `height: 355.9609375`) even when the\n * design is idle. Serialized verbatim, every frame produces a *different* wire\n * state, so a `onChange → renderState` wiring streams renders continuously while\n * the user does nothing — silently burning the realtime render budget. Rounding\n * geometry to a precision well below a pixel collapses that drift so identical\n * idle frames serialize identically (and the SDK's send-dedupe drops them).\n */\nfunction q(n: unknown): unknown {\n return typeof n === 'number' && Number.isFinite(n) ? Math.round(n * 100) / 100 : n;\n}\n\n/** Return a shallow copy with geometry fields quantized (see {@link q}). */\nfunction quantizeGeometry<T extends Record<string, any>>(el: T): T {\n return {\n ...el,\n ...(el.x !== undefined && { x: q(el.x) }),\n ...(el.y !== undefined && { y: q(el.y) }),\n ...(el.width !== undefined && { width: q(el.width) }),\n ...(el.height !== undefined && { height: q(el.height) }),\n ...(el.rotation !== undefined && { rotation: q(el.rotation) }),\n };\n}\n\n/**\n * Convert onChange canvas state to the server render format.\n *\n * Emits EVERY artboard in the state, each holding its own elements: elements\n * are grouped by their `artboard` tag (the artboard NAME `SnowconeCanvas.onChange`\n * stamps on each element). Elements without a tag — legacy saved states — land\n * on the first artboard, which also keeps single-artboard output byte-identical\n * to before. The tag itself is client-side metadata and is stripped from the\n * wire elements, so the per-placement `{schemaVersion, artboards: [...]}` shape\n * the server consumes is unchanged.\n *\n * For a multi-placement product, slice the result per placement:\n *\n * ```ts\n * const wire = serializeStateForServer(state);\n * for (const ab of wire.artboards) {\n * session.renderState(ab.name, { schemaVersion: wire.schemaVersion, artboards: [ab] });\n * }\n * ```\n *\n * @param state - The state from SnowconeCanvas onChange callback\n * @returns ServerRenderRequest ready to send via sendCanvasState()\n */\nexport function serializeStateForServer(state: {\n elements?: any[];\n artboards?: Array<{\n name?: string;\n width?: number;\n height?: number;\n backgroundColor?: string;\n exportBackground?: boolean;\n clipShape?: any;\n distressTexture?: any;\n imageMask?: any;\n /** Pacing role (multi-page templates, ADR-0086) — client/template\n * metadata, stripped from the wire like the element `artboard` tag. */\n role?: string;\n }>;\n activeArtboard?: string;\n}): ServerRenderRequest {\n const srcArtboards = state.artboards?.length\n ? state.artboards\n : [{ name: 'Front', width: 800, height: 800 }];\n\n // Group elements by their artboard tag (artboard NAME, stamped by\n // SnowconeCanvas.onChange). Untagged or unknown-tagged elements fall back to\n // the first artboard — today's behavior for legacy flat states.\n const elementsByArtboard = srcArtboards.map<any[]>(() => []);\n for (const el of state.elements || []) {\n const tag = el?.artboard;\n const idx = typeof tag === 'string'\n ? srcArtboards.findIndex((ab) => (ab.name || 'Front') === tag)\n : -1;\n elementsByArtboard[idx >= 0 ? idx : 0].push(serializeElement(el));\n }\n\n // Wire artboards are built field-by-field: template metadata on the source\n // artboards (the `role` pacing tag, ADR-0086) never reaches the server —\n // same rule as the element `artboard` membership tag below.\n return {\n schemaVersion: CANVAS_STATE_SCHEMA_VERSION,\n artboards: srcArtboards.map((srcArtboard, i) => ({\n id: `artboard-${i + 1}`,\n name: srcArtboard.name || 'Front',\n x: 0,\n y: 0,\n width: q(srcArtboard.width || 800) as number,\n height: q(srcArtboard.height || 800) as number,\n backgroundColor: srcArtboard.backgroundColor || 'transparent',\n // Pass the artboard's flag through — the server worker only paints the\n // background when this is true. Hardcoding `false` here silently dropped\n // explicitly-set artboard backgrounds from every realtime mockup while\n // the editor kept showing them (WYSIWYG drift).\n exportBackground: srcArtboard.exportBackground ?? false,\n elements: elementsByArtboard[i],\n distressTexture: srcArtboard.distressTexture,\n imageMask: srcArtboard.imageMask,\n })),\n };\n}\n\n/**\n * Serialize a single element from toJSON() format to export worker format.\n */\nfunction serializeElement(el: any): any {\n const type = el.type || el.transformType;\n\n if (type === 'image') {\n return serializeImageElement(el);\n }\n\n // Text, shape, group, path elements: map transformType → type, keep everything else\n // Text, shape, group, path elements: pass through as-is with type mapped.\n // Builtin mask URLs (builtin-mask:*) are kept as-is — the server worker\n // generates them procedurally via generateMaskBitmap().\n // Geometry is quantized (see `q`) so idle render-loop float drift doesn't\n // produce a new wire state every frame.\n // The `artboard` membership tag is CLIENT-side metadata (it already decided\n // which wire artboard this element belongs to) — strip it so the server\n // payload shape stays unchanged.\n const { artboard: _artboard, ...rest } = el;\n return quantizeGeometry({\n ...rest,\n type,\n });\n}\n\n/**\n * Serialize an image element, flattening transformData to top-level fields\n * as expected by SerializedImageElement and renderImageElement.\n */\nfunction serializeImageElement(el: any): any {\n const td = el.transformData || {};\n const hasMasks = el.masks && el.masks.length > 0;\n\n // Visible dimensions depend on crop and masks\n const fullWidth = td.width || 0;\n const fullHeight = td.height || 0;\n const visibleWidth = hasMasks ? fullWidth : fullWidth * (td.cropWidth ?? 1);\n const visibleHeight = hasMasks ? fullHeight : fullHeight * (td.cropHeight ?? 1);\n\n const serialized: any = {\n id: el.id,\n type: 'image' as const,\n x: el.x || 0,\n y: el.y || 0,\n width: visibleWidth,\n height: visibleHeight,\n rotation: el.rotation || 0,\n imageUrl: el.imageUrl,\n imageAspectRatio: el.imageAspectRatio,\n };\n\n // Crop (only if actually cropped and no masks)\n const isCropped = !hasMasks && (td.cropX !== 0 || td.cropY !== 0 || td.cropWidth !== 1 || td.cropHeight !== 1);\n if (isCropped) {\n // Note: crop values stay as fractions here — the server's Chrome tab\n // will convert to pixels using the actual bitmap dimensions after fetch.\n // The client doesn't have the bitmap dimensions available in onChange state.\n serialized.cropX = td.cropX;\n serialized.cropY = td.cropY;\n serialized.cropWidth = td.cropWidth;\n serialized.cropHeight = td.cropHeight;\n serialized.needsCropPixelConversion = true; // Flag for server to convert\n }\n\n // Flip and border radius\n if (td.flipHorizontal) serialized.flipHorizontal = td.flipHorizontal;\n if (td.flipVertical) serialized.flipVertical = td.flipVertical;\n if (td.borderRadius) serialized.borderRadius = td.borderRadius;\n if (el.opacity !== undefined) serialized.opacity = el.opacity;\n\n // Effects\n if (el.distressEffect) serialized.distressEffect = el.distressEffect;\n if (el.masks?.length > 0) serialized.masks = el.masks;\n if (el.blendMode) serialized.blendMode = el.blendMode;\n if (el.knockoutParts) serialized.knockoutParts = el.knockoutParts;\n if (el.stroke) serialized.stroke = el.stroke;\n\n // Quantize geometry so idle render-loop float drift doesn't churn the wire state.\n return quantizeGeometry(serialized);\n}\n","/**\n * Declarative design builder — compose a multi-element design by *intent*\n * (full-bleed background, anchored text, anchored shapes) and never by raw\n * coordinates.\n *\n * WHY THIS EXISTS\n * ---------------\n * The canvas/render contract positions every element by its CENTER point\n * (`x, y` is the centre, not the top-left — see CLAUDE.md). That convention is\n * correct for the interactive editor (rotation/scale pivot around centre) but is\n * a foot-gun for anyone hand-constructing a `canvas_state`: the natural\n * \"fill the placement\" attempt (`x:0, y:0, width:full`) centres the element on\n * the artboard's top-left CORNER, so it renders into the top-left quadrant. An\n * expert integrator hit exactly this and mis-diagnosed it as a server bug.\n *\n * This builder makes that class of mistake impossible: you describe *what* you\n * want (a background that fills, a title anchored top-centre, a star bottom-left)\n * and it emits a correct editor-shape canvas state. The centre-coordinate maths\n * lives here, once, instead of in every integrator's head.\n *\n * THREE RENDER TARGETS, ONE DESIGN\n * --------------------------------\n * The builder emits the EDITOR shape (`transformType` + nested `transformData`,\n * centre coords), which is exactly what both consumers already accept:\n * - `design.toJSON()` → `<SnowconeCanvas initialElements=… />` (interactive)\n * - `design.toServerRequest()` → `session.renderState(placement, …)` (realtime)\n * - (single asset on a placement → use `buildMockupUrl` in the SDK instead)\n *\n * So a developer can author a design headlessly, render it live, AND hand the\n * same design to the editor for the end user to drag/scale/rotate from there.\n */\n\nimport type {\n AnyElementConfig,\n ArtboardRole,\n ImageElementConfig,\n CustomElementConfig,\n ShapeElementConfig,\n ShapeTransformData,\n} from '../types/index.js';\nimport { serializeStateForServer, type ServerRenderRequest } from '../rendering/serialize-for-server.js';\nimport { EditorError } from '../errors/editorError.js';\n\n/** A nine-point anchor: where the element sits relative to the artboard. */\nexport type Anchor =\n | 'top-left' | 'top' | 'top-right'\n | 'left' | 'center' | 'right'\n | 'bottom-left' | 'bottom' | 'bottom-right';\n\nexport type ShapeKind =\n | 'rectangle' | 'circle' | 'ellipse' | 'triangle' | 'polygon' | 'star' | 'line';\n\n/** The artboard a design is composed on — one print placement. `width`/`height`\n * are the placement's pixel dimensions (e.g. 1480×2328 for a phone-case Front).\n * `name` MUST match the placement label you render (case-sensitive). */\nexport interface DesignArtboard {\n /** Placement label (case-sensitive — e.g. 'Front'). Defaults to 'Front'. */\n name?: string;\n width: number;\n height: number;\n /** Optional solid background fill behind all elements. Rendered everywhere\n * the design is — editor, exports, and server renders (realtime mockups). */\n backgroundColor?: string;\n /** Pacing role in a multi-page template (ADR-0086 albums). Template\n * metadata only — never sent on the server render wire. */\n role?: ArtboardRole;\n}\n\n/** One artboard of a saved multi-artboard state, as {@link DesignBuilder.fromState}\n * accepts and `toJSON()` emits. */\nexport interface StateArtboard {\n name: string;\n width: number;\n height: number;\n backgroundColor?: string;\n exportBackground?: boolean;\n /** Pacing role in a multi-page template (ADR-0086 albums). */\n role?: ArtboardRole;\n}\n\nexport interface FromStateOptions {\n /**\n * Which artboards to load:\n * - `'active'` (default) — today's behavior: the active artboard becomes the\n * builder's artboard; elements tagged for other artboards are skipped.\n * - `'all'` — multi-page templates (ADR-0086): every artboard and its\n * elements are kept, with `artboard` membership tags preserved, so\n * `toJSON()` / `toServerRequest()` stay multi-artboard. Template fields\n * scope per page via `TemplateField.artboard`.\n */\n artboards?: 'active' | 'all';\n}\n\n/** A stable layer name. Set it so you can bind the layer in an interactive\n * `<SnowconeCanvas/>` editor — `useImageBinding(name)` / `useTextBinding(name)`\n * find the element by `name`. Keep names unique per design. */\ntype NamedOption = { name?: string };\n\nexport interface BackgroundOptions extends NamedOption {\n /** Source image aspect ratio (w/h). When given, the image is COVER-fit (filled\n * + centre-cropped) with no distortion. When omitted, the image fills the\n * artboard box directly (correct when it's already the placement's aspect,\n * e.g. an AI image generated at the placement size). */\n aspectRatio?: number;\n opacity?: number;\n}\n\nexport interface ImageOptions extends NamedOption {\n anchor?: Anchor;\n /** Display width in artboard px. Defaults to a third of the artboard width. */\n width?: number;\n /** Display height in px. Defaults to `width / aspectRatio` (or square). */\n height?: number;\n aspectRatio?: number;\n opacity?: number;\n rotation?: number;\n /** Inset from the artboard edge for non-centre anchors, in px. Default 0. */\n padding?: number;\n}\n\nexport interface TextOptions extends NamedOption {\n anchor?: Anchor;\n fontSize?: number;\n fontFamily?: string;\n color?: string;\n /** Horizontal text alignment. Named `textAlign` to match the canvas editor's\n * element field (not `align`), so the builder and the editor read the same. */\n textAlign?: 'left' | 'center' | 'right';\n bold?: boolean;\n italic?: boolean;\n rotation?: number;\n padding?: number;\n}\n\nexport interface ShapeOptions extends NamedOption {\n anchor?: Anchor;\n /** Square size, or explicit box. Default 200×200. */\n size?: number | { width: number; height: number };\n fill?: string;\n rotation?: number;\n opacity?: number;\n /** star points / polygon sides. */\n points?: number;\n sides?: number;\n padding?: number;\n}\n\nconst DEFAULT_ANCHOR: Anchor = 'center';\n\n// ── Typed layer tokens (ADR-0085 §1 / T1) ────────────────────────────────────\n\n/** The bindable layer kinds. Shapes are not bindable in v1. */\nexport type LayerKind = 'text' | 'image';\n\n/**\n * Any design builder, regardless of its compile-time layer-name unions.\n * CONSUMERS of a builder (props, context) must use this — `DesignBuilder`\n * (= `DesignBuilder<string, string>`) is not a supertype of a statically\n * typed builder because `layer()`'s name constraint narrows with the design.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- variance escape: `any` here means \"accept every name union\", checked at mint/mount instead\nexport type AnyDesign = DesignBuilder<any, any>;\n\ndeclare const LAYER_TOKEN_BRAND: unique symbol;\n/** Runtime marker carried by every minted token (survives serialization\n * boundaries within one realm; `Symbol.for` so duplicated module instances\n * still agree). */\nconst LAYER_TOKEN_MARKER = Symbol.for('snowcone.canvas.layerToken');\n\n/**\n * A validated claim that layer `N` of kind `K` exists in the design that\n * minted it. The ONLY currency `Editor.TextField` (and future `ImageField`)\n * accepts — bare strings are rejected (`E_LAYER_TOKEN_REQUIRED`). Mint with\n * {@link DesignBuilder.layer}; not constructible by hand (branded).\n *\n * @experimental ADR-0085; subject to change until the P4 gate.\n */\nexport interface LayerToken<K extends LayerKind = LayerKind, N extends string = string> {\n readonly kind: K;\n readonly name: N;\n /** @internal */\n readonly [LAYER_TOKEN_BRAND]: true;\n}\n\n/** Runtime check used by parts before trusting a `layer` prop. */\nexport function isLayerToken(value: unknown): value is LayerToken {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[LAYER_TOKEN_MARKER] === true &&\n typeof (value as { name?: unknown }).name === 'string' &&\n ((value as { kind?: unknown }).kind === 'text' || (value as { kind?: unknown }).kind === 'image')\n );\n}\n\nfunction mintLayerToken(kind: LayerKind, name: string): LayerToken {\n return Object.freeze({ kind, name, [LAYER_TOKEN_MARKER]: true }) as unknown as LayerToken;\n}\n\n/** The token kind inferred for name `N` against the builder's accumulated\n * per-kind name unions. A dynamically-loaded design (both unions degraded to\n * `string`) yields the full `'text' | 'image'` union — typing degrades, the\n * runtime validation in `layer()` is the net that still fires (0G#2). */\ntype InferKind<N extends string, TText extends string, TImage extends string> =\n [N] extends [TText] ? ([N] extends [TImage] ? LayerKind : 'text') : 'image';\n\n/** Kind of an element config, for token validation. `null` = not bindable. */\nfunction elementLayerKind(el: AnyElementConfig): LayerKind | null {\n const t = (el as { transformType?: string }).transformType;\n if (t === 'image') return 'image';\n if (t === 'shape' || t === 'group' || t === 'path') return null;\n // Every text transform ('custom', 'circle', 'arch', …) is a text layer.\n return 'text';\n}\n\nfunction hPart(a: Anchor): 'left' | 'center' | 'right' {\n if (a === 'top-left' || a === 'left' || a === 'bottom-left') return 'left';\n if (a === 'top-right' || a === 'right' || a === 'bottom-right') return 'right';\n return 'center';\n}\n\nfunction vPart(a: Anchor): 'top' | 'middle' | 'bottom' {\n if (a === 'top-left' || a === 'top' || a === 'top-right') return 'top';\n if (a === 'bottom-left' || a === 'bottom' || a === 'bottom-right') return 'bottom';\n return 'middle';\n}\n\n/**\n * Resolve an element's CENTRE (x, y) so its bounding box sits at `anchor` on the\n * artboard, inset by `padding`. This is the whole point of the builder: a\n * `center` anchor on a full-bleed element returns the artboard centre, never\n * (0, 0).\n */\nfunction anchorToCenter(\n anchor: Anchor,\n elW: number,\n elH: number,\n artW: number,\n artH: number,\n padding: number,\n): { x: number; y: number } {\n const h = hPart(anchor);\n const v = vPart(anchor);\n const x = h === 'left' ? elW / 2 + padding\n : h === 'right' ? artW - elW / 2 - padding\n : artW / 2;\n const y = v === 'top' ? elH / 2 + padding\n : v === 'bottom' ? artH - elH / 2 - padding\n : artH / 2;\n return { x, y };\n}\n\n/** Compute the centred cover-crop (fractions, 0–1) for a source of aspect `ar`\n * filling an `artW×artH` box, plus the full (uncropped) display size the editor\n * stores in `transformData`. Mirrors the editor's cover behaviour but needs only\n * the aspect ratio, not the pixel dimensions. */\nfunction coverCrop(ar: number, artW: number, artH: number) {\n const artAR = artW / artH;\n if (ar >= artAR) {\n // Source is wider than the box → crop the sides.\n const fullW = artH * ar;\n const cropWidth = artW / fullW; // = artAR / ar\n return {\n width: fullW, height: artH,\n cropX: (1 - cropWidth) / 2, cropY: 0, cropWidth, cropHeight: 1,\n };\n }\n // Source is taller than the box → crop top/bottom.\n const fullH = artW / ar;\n const cropHeight = artH / fullH; // = ar / artAR\n return {\n width: artW, height: fullH,\n cropX: 0, cropY: (1 - cropHeight) / 2, cropWidth: 1, cropHeight,\n };\n}\n\n/**\n * A composed design. Add elements by intent, then emit it for the editor or the\n * render path. Methods are chainable.\n *\n * The two type parameters accumulate the design's NAMED layer literals per\n * kind (ADR-0085 §1): naming a layer in `.text()` / `.image()` /\n * `.background()` widens the corresponding union, so `design.layer(\"naem\")`\n * is a compile error listing the valid names. Designs loaded from JSON\n * ({@link DesignBuilder.fromState}) degrade both unions to `string` — same\n * API, runtime-validated instead (the degradation is deliberate and LOUD in\n * docs; never block the dynamic case).\n */\nexport class DesignBuilder<out TText extends string = string, out TImage extends string = string> {\n private readonly artboard: Required<Pick<DesignArtboard, 'name' | 'width' | 'height'>> & {\n backgroundColor?: string;\n role?: ArtboardRole;\n };\n private readonly els: AnyElementConfig[] = [];\n private idCounter = 0;\n /** Every artboard of a multi-page state (fromState `{ artboards: 'all' }`);\n * unset for single-artboard builders. */\n private allArtboards?: StateArtboard[];\n\n constructor(artboard: DesignArtboard) {\n if (!(artboard.width > 0) || !(artboard.height > 0)) {\n throw new Error('design(): artboard width and height must be positive numbers');\n }\n this.artboard = {\n name: artboard.name || 'Front',\n width: artboard.width,\n height: artboard.height,\n backgroundColor: artboard.backgroundColor,\n role: artboard.role,\n };\n }\n\n /**\n * Seed a builder from a previously saved canvas state — the editor shape\n * `<SnowconeCanvas onChange>` emits (and `toJSON()` produces).\n *\n * This is how a saved editable template re-enters the turnkey embed path:\n * load the template's state JSON, lift it back into a `DesignBuilder`, and\n * hand it to `<RealtimeEmbed design={…}>`. By default the active artboard\n * becomes the builder's artboard; elements tagged for other artboards are\n * skipped (untagged elements are assumed to belong to the active one).\n *\n * Pass `{ artboards: 'all' }` to load a multi-page template (ADR-0086)\n * whole: every artboard (with its pacing `role`) and every element are\n * kept, membership tags preserved, so `toJSON()` / `toServerRequest()`\n * emit all pages and `TemplateField.artboard` can scope fields per page.\n */\n static fromState(\n state: {\n elements: AnyElementConfig[];\n artboards: StateArtboard[];\n activeArtboard: string;\n },\n options: FromStateOptions = {},\n ): DesignBuilder {\n const artboard =\n state.artboards.find((a) => a.name === state.activeArtboard) ?? state.artboards[0];\n if (!artboard) {\n throw new Error('DesignBuilder.fromState(): state has no artboards');\n }\n const builder = new DesignBuilder({\n name: artboard.name,\n width: artboard.width,\n height: artboard.height,\n backgroundColor: artboard.backgroundColor,\n role: artboard.role,\n });\n if (options.artboards === 'all') {\n builder.allArtboards = state.artboards.map((a) => ({ ...a }));\n for (const el of state.elements) {\n // Untagged elements belong to the active artboard (same assumption as\n // the active-only mode) — stamp them so the multi-artboard state keeps\n // them there instead of collapsing onto the first artboard.\n builder.els.push(el.artboard ? { ...el } : { ...el, artboard: artboard.name });\n }\n return builder;\n }\n for (const el of state.elements) {\n if (el.artboard && el.artboard !== artboard.name) continue;\n builder.els.push({ ...el });\n }\n return builder;\n }\n\n private nextId(prefix: string): string {\n return `${prefix}-${++this.idCounter}`;\n }\n\n /** Full-bleed background image (fills the whole artboard). Cover-fit (no\n * distortion) when `aspectRatio` is supplied. Inserted at the bottom of the\n * stack regardless of call order. The default layer name `'Background'`\n * joins the IMAGE-layer token union (auto-injected names are deliberately\n * not one union with text — ADR-0085 §1). */\n background<const N extends string>(\n imageUrl: string,\n opts: BackgroundOptions & { name: N },\n ): DesignBuilder<TText, TImage | N>;\n background(imageUrl: string, opts?: BackgroundOptions): DesignBuilder<TText, TImage | 'Background'>;\n background(imageUrl: string, opts: BackgroundOptions = {}): AnyDesign {\n const { width: artW, height: artH } = this.artboard;\n let td: ImageElementConfig['transformData'];\n if (opts.aspectRatio && opts.aspectRatio > 0) {\n const c = coverCrop(opts.aspectRatio, artW, artH);\n td = {\n type: 'image', width: c.width, height: c.height,\n cropX: c.cropX, cropY: c.cropY, cropWidth: c.cropWidth, cropHeight: c.cropHeight,\n flipHorizontal: false, flipVertical: false, borderRadius: 0,\n };\n } else {\n td = {\n type: 'image', width: artW, height: artH,\n cropX: 0, cropY: 0, cropWidth: 1, cropHeight: 1,\n flipHorizontal: false, flipVertical: false, borderRadius: 0,\n };\n }\n const el: ImageElementConfig = {\n id: this.nextId('bg'),\n transformType: 'image',\n x: artW / 2,\n y: artH / 2,\n rotation: 0,\n opacity: opts.opacity ?? 1,\n imageUrl,\n imageAspectRatio: opts.aspectRatio,\n transformData: td,\n name: opts.name ?? 'Background',\n };\n // Background goes underneath everything else.\n this.els.unshift(el);\n return this;\n }\n\n /** An anchored image element (not full-bleed). */\n image<const N extends string>(\n imageUrl: string,\n opts: ImageOptions & { name: N },\n ): DesignBuilder<TText, TImage | N>;\n image(imageUrl: string, opts?: ImageOptions): DesignBuilder<TText, TImage>;\n image(imageUrl: string, opts: ImageOptions = {}): AnyDesign {\n const { width: artW, height: artH } = this.artboard;\n const w = opts.width ?? Math.round(artW / 3);\n const h = opts.height ?? (opts.aspectRatio ? Math.round(w / opts.aspectRatio) : w);\n const { x, y } = anchorToCenter(opts.anchor ?? DEFAULT_ANCHOR, w, h, artW, artH, opts.padding ?? 0);\n const el: ImageElementConfig = {\n id: this.nextId('img'),\n transformType: 'image',\n x, y,\n rotation: opts.rotation ?? 0,\n opacity: opts.opacity ?? 1,\n imageUrl,\n imageAspectRatio: opts.aspectRatio,\n transformData: {\n type: 'image', width: w, height: h,\n cropX: 0, cropY: 0, cropWidth: 1, cropHeight: 1,\n flipHorizontal: false, flipVertical: false, borderRadius: 0,\n },\n ...(opts.name ? { name: opts.name } : {}),\n };\n this.els.push(el);\n return this;\n }\n\n /** An anchored text layer. Horizontal centring is exact (textAlign); vertical\n * anchoring uses the font size as the line-height estimate. */\n text<const N extends string>(\n text: string,\n opts: TextOptions & { name: N },\n ): DesignBuilder<TText | N, TImage>;\n text(text: string, opts?: TextOptions): DesignBuilder<TText, TImage>;\n text(text: string, opts: TextOptions = {}): AnyDesign {\n const { width: artW, height: artH } = this.artboard;\n const fontSize = opts.fontSize ?? Math.round(this.artboard.height / 12);\n // Text width is metric-dependent (unknown headless); horizontal placement is\n // handled by textAlign, so anchor only drives the vertical centre + the\n // horizontal *reference* point. Use a zero-width box horizontally so the\n // centre sits exactly on the anchor's x line.\n const { x, y } = anchorToCenter(opts.anchor ?? 'top', 0, fontSize, artW, artH, opts.padding ?? Math.round(artH / 20));\n const el: CustomElementConfig = {\n id: this.nextId('text'),\n transformType: 'custom',\n x, y,\n rotation: opts.rotation ?? 0,\n text,\n fontSize,\n fontFamily: opts.fontFamily ?? 'Anton',\n color: opts.color ?? '#ffffff',\n textAlign: opts.textAlign ?? 'center',\n bold: opts.bold ?? false,\n italic: opts.italic ?? false,\n transformData: { type: 'custom' },\n ...(opts.name ? { name: opts.name } : {}),\n };\n this.els.push(el);\n return this;\n }\n\n /**\n * Mint a {@link LayerToken} for a named layer — the only currency\n * `Editor.TextField` accepts (ADR-0085 §1). Compile time: `name` is\n * constrained to this design's declared layer names, kind inferred from\n * which per-kind union it falls in. Runtime: validated EAGERLY against the\n * built elements (nearest the mistake), so CMS/JSON-loaded designs get the\n * same protection with `string` typing.\n *\n * @throws EditorError `E_LAYER_NOT_FOUND` — no layer with this name; lists\n * the valid names per kind.\n * @throws EditorError `E_LAYER_WRONG_KIND` — the name matches a layer that\n * is not bindable (shape/group/path).\n * @experimental ADR-0085; subject to change until the P4 gate.\n */\n layer<N extends TText | TImage>(name: N): LayerToken<InferKind<N, TText, TImage>, N> {\n const el = this.els.find((e) => (e as { name?: string }).name === name);\n if (!el) {\n const textNames = this.layerNamesOfKind('text');\n const imageNames = this.layerNamesOfKind('image');\n throw new EditorError(\n 'E_LAYER_NOT_FOUND',\n `no layer \"${name}\" in this design. Text layers: ${JSON.stringify(textNames)}; ` +\n `image layers: ${JSON.stringify(imageNames)}. Fix the name, or add the layer ` +\n `first — e.g. design(…).text('…', { name: '${name}' }).`,\n { layerName: name, validNames: [...textNames, ...imageNames], textNames, imageNames, docSlug: 'embed-mode' },\n );\n }\n const kind = elementLayerKind(el);\n if (!kind) {\n throw new EditorError(\n 'E_LAYER_WRONG_KIND',\n `layer \"${name}\" is a ${(el as { transformType?: string }).transformType} element — ` +\n `only text and image layers are bindable. Bindable layers: ` +\n `${JSON.stringify([...this.layerNamesOfKind('text'), ...this.layerNamesOfKind('image')])}.`,\n { layerName: name, actualType: (el as { transformType?: string }).transformType, docSlug: 'embed-mode' },\n );\n }\n return mintLayerToken(kind, name) as LayerToken<InferKind<N, TText, TImage>, N>;\n }\n\n /** Names of bindable layers of one kind, in stack order. @internal */\n layerNamesOfKind(kind: LayerKind): string[] {\n return this.els\n .filter((e) => elementLayerKind(e) === kind)\n .map((e) => (e as { name?: string }).name)\n .filter((n): n is string => Boolean(n));\n }\n\n /** An anchored vector shape. */\n shape(kind: ShapeKind, opts: ShapeOptions = {}): this {\n const { width: artW, height: artH } = this.artboard;\n const size = opts.size ?? Math.round(artW / 6);\n const w = typeof size === 'number' ? size : size.width;\n const h = typeof size === 'number' ? size : size.height;\n const { x, y } = anchorToCenter(opts.anchor ?? DEFAULT_ANCHOR, w, h, artW, artH, opts.padding ?? 0);\n const transformData: ShapeTransformData = {\n type: 'shape',\n shapeType: kind,\n width: w,\n height: h,\n fillColor: opts.fill ?? '#000000',\n fillOpacity: opts.opacity ?? 1,\n ...(opts.points !== undefined ? { points: opts.points } : {}),\n ...(opts.sides !== undefined ? { sides: opts.sides } : {}),\n };\n const el: ShapeElementConfig = {\n id: this.nextId('shape'),\n transformType: 'shape',\n x, y,\n rotation: opts.rotation ?? 0,\n transformData,\n ...(opts.name ? { name: opts.name } : {}),\n };\n this.els.push(el);\n return this;\n }\n\n /** The composed elements, in render order (background first). Editor shape. */\n get elements(): AnyElementConfig[] {\n return this.els.map((e) => ({ ...e }));\n }\n\n /** Editor-shape canvas state for `<SnowconeCanvas initialElements=… />` and\n * `serializeStateForServer`. `activeArtboard` is the artboard name. A\n * builder loaded with `fromState(…, { artboards: 'all' })` emits every\n * page (artboard `role`s and element membership tags included). */\n toJSON(): {\n elements: AnyElementConfig[];\n artboards: StateArtboard[];\n activeArtboard: string;\n } {\n if (this.allArtboards) {\n return {\n // Elements added via builder methods after fromState are untagged —\n // they belong to the builder's (active) artboard; stamp them so the\n // multi-artboard state groups them correctly.\n elements: this.els.map((e) =>\n e.artboard ? { ...e } : { ...e, artboard: this.artboard.name },\n ),\n artboards: this.allArtboards.map((a) => ({ ...a })),\n activeArtboard: this.artboard.name,\n };\n }\n return {\n elements: this.elements,\n artboards: [{\n name: this.artboard.name,\n width: this.artboard.width,\n height: this.artboard.height,\n // A background set on a design is part of the design: flag it\n // renderable so exports and server renders paint it, matching the\n // editor (the export worker skips it without `exportBackground`).\n ...(this.artboard.backgroundColor\n ? {\n backgroundColor: this.artboard.backgroundColor,\n exportBackground: this.artboard.backgroundColor !== 'transparent',\n }\n : {}),\n ...(this.artboard.role ? { role: this.artboard.role } : {}),\n }],\n activeArtboard: this.artboard.name,\n };\n }\n\n /** Server-render payload for `RenderSession.renderState(placement, …)`.\n * Equivalent to `serializeStateForServer(design.toJSON())`. */\n toServerRequest(): ServerRenderRequest {\n return serializeStateForServer(this.toJSON());\n }\n}\n\n/**\n * Start a design on an artboard (one print placement).\n *\n * @example\n * const d = design({ name: 'Front', width: 1480, height: 2328 })\n * .background(aiImageUrl, { aspectRatio: 1480 / 2328, name: 'art' })\n * .text('YOUR NAME', { anchor: 'top', name: 'headline' })\n * .shape('star', { anchor: 'bottom-left', fill: '#000' });\n *\n * session.renderState('Front', d.toServerRequest()); // live\n * // …or hand d.toJSON() to <SnowconeCanvas/> for interactive editing — the\n * // `name`s let you bind layers with useImageBinding('art') / useTextBinding('headline').\n */\nexport function design(artboard: DesignArtboard): DesignBuilder<never, never> {\n return new DesignBuilder<never, never>(artboard);\n}\n\n/**\n * Resolve a layer's kind against a live element/config list — the mount-time\n * revalidation `Editor.TextField` runs (the token is a validated claim; the\n * mounted Root's design state is the authority). @internal\n */\nexport function layerKindOfConfig(el: AnyElementConfig): LayerKind | null {\n return elementLayerKind(el);\n}\n"],"names":["CANVAS_STATE_SCHEMA_VERSION","q","n","quantizeGeometry","el","serializeStateForServer","state","srcArtboards","_a","elementsByArtboard","tag","idx","ab","serializeElement","srcArtboard","type","serializeImageElement","_artboard","rest","td","hasMasks","fullWidth","fullHeight","visibleWidth","visibleHeight","serialized","DEFAULT_ANCHOR","LAYER_TOKEN_MARKER","isLayerToken","value","mintLayerToken","kind","name","elementLayerKind","t","hPart","a","vPart","anchorToCenter","anchor","elW","elH","artW","artH","padding","h","v","x","y","coverCrop","ar","artAR","fullW","cropWidth","fullH","cropHeight","DesignBuilder","artboard","options","builder","prefix","imageUrl","opts","c","w","text","fontSize","e","textNames","imageNames","EditorError","size","transformData","design","layerKindOfConfig"],"mappings":"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"}