@snowcone-app/canvas 0.22.0 → 0.26.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 (59) hide show
  1. package/ERROR_CONTRACT.md +600 -0
  2. package/README.md +2 -1
  3. package/STYLING_CONTRACT.md +173 -93
  4. package/dist/{ContextualToolbars-BDEt9ND0.js → ContextualToolbars-DUhBKVxe.js} +3 -3
  5. package/dist/{ContextualToolbars-BDEt9ND0.js.map → ContextualToolbars-DUhBKVxe.js.map} +1 -1
  6. package/dist/{EditorCanvasImpl-BH_bKy-F.js → EditorCanvasImpl-BLZV_el0.js} +6 -6
  7. package/dist/{EditorCanvasImpl-BH_bKy-F.js.map → EditorCanvasImpl-BLZV_el0.js.map} +1 -1
  8. package/dist/{ImagePanel-BwqFDKvW.js → ImagePanel-sLSjaPWS.js} +3 -3
  9. package/dist/{ImagePanel-BwqFDKvW.js.map → ImagePanel-sLSjaPWS.js.map} +1 -1
  10. package/dist/{SnowconeCanvas-Cx7nXlBI.js → SnowconeCanvas-4ef4a61J.js} +6 -6
  11. package/dist/{SnowconeCanvas-Cx7nXlBI.js.map → SnowconeCanvas-4ef4a61J.js.map} +1 -1
  12. package/dist/{UnifiedFontService-CC3CSmH5.js → UnifiedFontService-Bx2MxFfy.js} +867 -834
  13. package/dist/{UnifiedFontService-CC3CSmH5.js.map → UnifiedFontService-Bx2MxFfy.js.map} +1 -1
  14. package/dist/advanced.mjs +7 -7
  15. package/dist/api/embed.d.ts +4 -4
  16. package/dist/components/editor/EditorLoadingOverlay.d.ts +12 -0
  17. package/dist/components/editor/EditorPlacements.d.ts +14 -0
  18. package/dist/components/editor/EditorPreview.d.ts +32 -8
  19. package/dist/components/editor/EditorRoot.d.ts +12 -0
  20. package/dist/components/editor/PreviewPrismOverlay.d.ts +23 -0
  21. package/dist/components/editor/RootInternals.d.ts +31 -0
  22. package/dist/components/editor/editorAudit.d.ts +66 -0
  23. package/dist/components/editor/index.d.ts +14 -0
  24. package/dist/components/editor/usePlacements.d.ts +14 -0
  25. package/dist/components/embed-realtime/context.d.ts +11 -2
  26. package/dist/components/embed-realtime/validateEmbed.d.ts +20 -1
  27. package/dist/{custom-icons-BW6hqmkL.js → custom-icons-DeYIrTK1.js} +7 -7
  28. package/dist/{custom-icons-BW6hqmkL.js.map → custom-icons-DeYIrTK1.js.map} +1 -1
  29. package/dist/design/design-builder.d.ts +108 -15
  30. package/dist/design/product-config.d.ts +10 -0
  31. package/dist/design-builder-U7Su9MMC.js +470 -0
  32. package/dist/design-builder-U7Su9MMC.js.map +1 -0
  33. package/dist/editorErrors-B2mb1sVN.js.map +1 -1
  34. package/dist/embed-DXGydZOT.js +1586 -0
  35. package/dist/embed-DXGydZOT.js.map +1 -0
  36. package/dist/embed.mjs +34 -31
  37. package/dist/errors/editorError.d.ts +27 -1
  38. package/dist/hooks/bindingGuards.d.ts +16 -0
  39. package/dist/hooks/useColorBinding.d.ts +2 -1
  40. package/dist/hooks/useFontBinding.d.ts +2 -1
  41. package/dist/hooks/useImageBinding.d.ts +2 -1
  42. package/dist/hooks/useTextBinding.d.ts +2 -1
  43. package/dist/index.mjs +6 -6
  44. package/dist/style.css +1 -1
  45. package/dist/template-fields-DJ157D3E.js +548 -0
  46. package/dist/template-fields-DJ157D3E.js.map +1 -0
  47. package/dist/testing/failureFixtures.d.ts +2 -2
  48. package/dist/testing.mjs +1 -1
  49. package/dist/{validation-C9Fe9ahK.js → validation-DnAkXdUg.js} +7 -7
  50. package/dist/{validation-C9Fe9ahK.js.map → validation-DnAkXdUg.js.map} +1 -1
  51. package/example/package.json +1 -1
  52. package/llms.txt +3 -0
  53. package/package.json +6 -3
  54. package/dist/design-builder-CT4DHIPe.js +0 -367
  55. package/dist/design-builder-CT4DHIPe.js.map +0 -1
  56. package/dist/embed-8ecUMMH-.js +0 -1226
  57. package/dist/embed-8ecUMMH-.js.map +0 -1
  58. package/dist/template-fields-C2pSfMBh.js +0 -494
  59. package/dist/template-fields-C2pSfMBh.js.map +0 -1
@@ -0,0 +1,600 @@
1
+ # `Editor.*` error contract
2
+
3
+ <!-- GENERATED — do not edit. Derived from the EditorError/EmbedIssue source
4
+ of truth by packages/canvas/scripts/gen-error-contract.mjs.
5
+ Regenerate: pnpm -C packages/canvas gen:error-contract -->
6
+
7
+ Every composition/runtime violation in the `Editor.*` parts throws an
8
+ `EditorError`: a stable machine-readable `code` plus a structured
9
+ `error.data` payload (always including `code`, usually `docSlug` and the
10
+ valid alternatives). Throws fire dev AND prod and are contained by
11
+ `Editor.Root`'s error boundary. **Assert on `code` + `data` fields, never
12
+ on message prose** — messages are rendered from the data and may be
13
+ reworded. Every message names the violated contract and the corrective
14
+ code, so the fix is in the message itself. `${…}` in the templates below
15
+ marks runtime facts.
16
+
17
+ Advisory wiring problems (severity `warn`) never throw: they reach the
18
+ host through `Editor.Root`'s `onIssue` channel as `EmbedIssue` objects —
19
+ see [Embed wiring issues](#embed-wiring-issues-the-onissue-channel).
20
+ Error-severity wiring issues escalate to a thrown `E_EMBED_CONFIG` with
21
+ the issue key in `error.data.issue`.
22
+
23
+ ## Code index
24
+
25
+ | code | severity | fires when |
26
+ |---|---|---|
27
+ | [`E_OUTSIDE_ROOT`](#e_outside_root) | throw | An `Editor.*` part mounted outside `<Editor.Root>`. |
28
+ | [`E_LAYER_NOT_FOUND`](#e_layer_not_found) | throw | A layer token (or its name) matches no layer in the mounted design. |
29
+ | [`E_LAYER_WRONG_KIND`](#e_layer_wrong_kind) | throw | The layer exists but is not the kind the part binds (e.g. an image layer passed to `Editor.TextField`). |
30
+ | [`E_LAYER_TOKEN_REQUIRED`](#e_layer_token_required) | throw | `Editor.TextField` received a bare string (or other non-token) — layers bind by `design.layer("name")` token only (ADR-0085 §1). |
31
+ | [`E_BOUND_LAYER_MUTATION`](#e_bound_layer_mutation) | throw | Programmatic rename/delete of a layer a mounted TextField is bound to. |
32
+ | [`E_DUPLICATE_CANVAS`](#e_duplicate_canvas) | throw | Two `Editor.Canvas` parts mounted under one Root. |
33
+ | [`E_REQUIRES_CANVAS`](#e_requires_canvas) | throw | A part that needs a mounted `Editor.Canvas` found none under this Root. |
34
+ | [`E_BUY_NO_PRODUCT`](#e_buy_no_product) | throw | `Editor.Buy` mounted without product context. |
35
+ | [`E_CHUNK_LOAD_FAILED`](#e_chunk_load_failed) | throw | The lazy renderer chunk failed to load (network/CDN). Retryable. |
36
+ | [`E_EMBED_CONFIG`](#e_embed_config) | throw | Embed wiring invalid at mount (missing grant/mockupIds, variant or placement mismatch) — the throw-mode upgrade of `validateEmbed` (F3). |
37
+ | [`E_PRODUCT_NOT_RENDERABLE`](#e_product_not_renderable) | throw | `productToRenderConfig`: the catalog product has no renderable image placement / no mockup scenes, or the requested placement doesn't exist. |
38
+ | [`E_DUPLICATE_ARTBOARD`](#e_duplicate_artboard) | throw | `design().artboard()`: an artboard with this label already exists in the design (artboard names are placement labels — unique by contract). |
39
+ | [`E_UNKNOWN_PLACEMENT`](#e_unknown_placement) | throw | `usePlacements().select()`: the label names no artboard in the design (ADR-0088). Lists the valid placement labels. |
40
+ | [`E_UNKNOWN_OPTION`](#e_unknown_option) | throw | `select()` named an option key/choice that doesn't exist on the product (Buy-owned selection, ADR-0085 §2 / P2). |
41
+ | [`E_COLOR_ROLE_COLLISION`](#e_color_role_collision) | throw | `design({ colors })`: two color roles share one (normalized) hex value. `useColorBinding` matches by VALUE, so colliding roles would capture each other's elements — rejected at mint time instead of failing subtly. |
42
+ | [`E_COLOR_ROLE_INVALID`](#e_color_role_invalid) | throw | `design({ colors })`: a role's value is not a `#RGB`/`#RRGGBB` hex (value-matched bindings normalize hex; other syntaxes can't bind). |
43
+ | [`E_COLOR_ROLE_NOT_FOUND`](#e_color_role_not_found) | throw | `design.color(name)`: no color role with this name — lists the design's declared roles. |
44
+ | [`E_PREVIEW_NO_LOADER`](#e_preview_no_loader) | throw | `Editor.Preview loadingOverlay` got `false`/an unknown value. A mockup preview ALWAYS has a render-in-flight treatment (a server render takes real time, and the loader paints OVER the still-visible previous mockup) — keep the default, pass a variant/custom element, or declare `"host-owned"` with your own indicator. Silence-then-pop-in is a composition mistake, not a preference. |
45
+ | [`E_ASSET_FORBIDDEN`](#e_asset_forbidden) | throw | `assertRenders`: the shop's asset-origin allowlist denied a design asset (translated from the server's 403 — F4). |
46
+ | [`E_RENDER_FAILED`](#e_render_failed) | throw | `assertRenders`: the render failed or produced no usable image (F4). |
47
+ | [`E_BINDING_INERT`](#e_binding_inert) | throw | `assertRenders`: the mutated render is byte-identical to the seed render — the binding under test is dead (the two-render contract, F4). |
48
+ | [`E_NO_MUTABLE_BINDING`](#e_no_mutable_binding) | throw | `assertRenders`: the design has no named text layer to mutate and no `mutate` override was given (F4). |
49
+ | [`E_AUDIT_NO_ROOT`](#e_audit_no_root) | throw | `auditEditor()` called with no `<Editor.Root>` mounted — the imperative audit reads the most-recent mounted Root's registry (#10). |
50
+ | [`missing-shop`](#missing-shop) | error → throws `E_EMBED_CONFIG` | No `shop` (the publishable shop id) was passed. |
51
+ | [`missing-grant`](#missing-grant) | error → throws `E_EMBED_CONFIG` | No grant source (`grantUrl`/`grant`) — the session can never authorize, so no mockup ever renders. |
52
+ | [`empty-mockup-ids`](#empty-mockup-ids) | error → throws `E_EMBED_CONFIG` | `product.mockupIds` is empty — nothing to render. |
53
+ | [`variant-required`](#variant-required) | error → throws `E_EMBED_CONFIG` | The product has a variant axis but no `variantId` — the render would hang ~30s waiting on a color blob that never comes. |
54
+ | [`multi-placement`](#multi-placement) | error → throws `E_EMBED_CONFIG` | The product requires several placements but this embed renders ONE — the server times out waiting on the rest (needs the eject path). |
55
+ | [`placement-mismatch`](#placement-mismatch) | error → throws `E_EMBED_CONFIG` | The design's placement isn't one of the product's placements — the server renders a blank mockup. |
56
+ | [`duplicate-layer-names`](#duplicate-layer-names) | warn (advisory) | Two layers share a name — name-bound hooks bind the FIRST match, so a control may edit the wrong layer. |
57
+ | [`slow-render`](#slow-render) | warn (advisory) | Runtime (not mount-time): a render has been in flight past `SLOW_RENDER_HINT_MS` with no mockup and no error — the silent-hang class. The render may still arrive; this is a heads-up. |
58
+
59
+ ## `EditorError` codes
60
+
61
+ ### `E_OUTSIDE_ROOT`
62
+
63
+ **Fires when:** An `Editor.*` part mounted outside `<Editor.Root>`.
64
+
65
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
66
+
67
+ - `src/components/editor/RootInternals.ts`
68
+
69
+ ```text
70
+ <${partName}> must be rendered inside <Editor.Root>. Wrap it: <Editor.Root shop={…} product={…} design={…} grantUrl={…}><${partName}/></Editor.Root>
71
+ ```
72
+
73
+ `error.data`: `code`, `part`, `docSlug`
74
+
75
+ ### `E_LAYER_NOT_FOUND`
76
+
77
+ **Fires when:** A layer token (or its name) matches no layer in the mounted design.
78
+
79
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
80
+
81
+ - `src/components/editor/EditorTextField.tsx`
82
+
83
+ ```text
84
+ no layer "${layer.name}" in this Root's design — the token was minted from a different design than the one passed to <Editor.Root design={…}>. Bindable layers here: ${JSON.stringify(stateStore.bindableLayerNames())}.
85
+ ```
86
+
87
+ `error.data`: `code`, `layerName`, `validNames`, `docSlug`
88
+
89
+ - `src/components/editor/EditorTextField.tsx`
90
+
91
+ ```text
92
+ layer "${layer.name}" no longer exists in the mounted canvas — it was removed or renamed after this <Editor.TextField> bound to it. Bound layers must not be renamed or deleted while a field is mounted (ADR-0085 §3).
93
+ ```
94
+
95
+ `error.data`: `code`, `layerName`, `docSlug`
96
+
97
+ - `src/components/editor/designStateStore.ts`
98
+
99
+ ```text
100
+ no layer "${name}" in the current design state. Bindable layers: ${JSON.stringify(this.bindableLayerNames())}.
101
+ ```
102
+
103
+ `error.data`: `code`, `layerName`, `validNames`, `docSlug`
104
+
105
+ - `src/design/design-builder.ts`
106
+
107
+ ```text
108
+ no layer "${name}" in this design. Text layers: ${JSON.stringify(textNames)}; image layers: ${JSON.stringify(imageNames)}. Fix the name, or add the layer first — e.g. design(…).text('…', { name: '${name}' }).
109
+ ```
110
+
111
+ `error.data`: `code`, `layerName`, `validNames`, `textNames`, `imageNames`, `docSlug`
112
+
113
+ - `src/hooks/bindingGuards.ts`
114
+
115
+ ```text
116
+ no layer "${name}" in this Root's document — the token was minted from a different design than the one passed to <Editor.Root design={…}>, or the layer was deleted. Bindable layers here: ${JSON.stringify(named.map((e) => e.name))}.
117
+ ```
118
+
119
+ `error.data`: `code`, `layerName`, `validNames`, `docSlug`
120
+
121
+ ### `E_LAYER_WRONG_KIND`
122
+
123
+ **Fires when:** The layer exists but is not the kind the part binds (e.g. an image layer passed to `Editor.TextField`).
124
+
125
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
126
+
127
+ - `src/components/editor/EditorTextField.tsx`
128
+
129
+ ```text
130
+ layer "${layer.name}" is ${kindNow === null ? 'not a bindable kind' : `an ${kindNow} layer`} — <Editor.TextField> binds text layers only.
131
+ ```
132
+
133
+ `error.data`: `code`, `layerName`, `actualKind`, `docSlug`
134
+
135
+ - `src/design/design-builder.ts`
136
+
137
+ ```text
138
+ layer "${name}" is a ${(el as { transformType?: string }).transformType} element — only text and image layers are bindable. Bindable layers: ${JSON.stringify([...this.layerNamesOfKind('text'), ...this.layerNamesOfKind('image')])}.
139
+ ```
140
+
141
+ `error.data`: `code`, `layerName`, `actualType`, `docSlug`
142
+
143
+ - `src/hooks/bindingGuards.ts`
144
+
145
+ ```text
146
+ layer "${name}" is a ${layerOrName.kind} token — this hook binds ${expectedKind} layers only (use ${HOOK_BY_KIND[layerOrName.kind]}).
147
+ ```
148
+
149
+ `error.data`: `code`, `layerName`, `actualKind`, `expectedKind`, `docSlug`
150
+
151
+ - `src/hooks/bindingGuards.ts`
152
+
153
+ ```text
154
+ layer "${name}" is ${actual === 'unbindable' ? 'not a bindable kind' : `an ${actual} layer`} — this hook binds ${expectedKind} layers only.
155
+ ```
156
+
157
+ `error.data`: `code`, `layerName`, `actualKind`, `expectedKind`, `docSlug`
158
+
159
+ ### `E_LAYER_TOKEN_REQUIRED`
160
+
161
+ **Fires when:** `Editor.TextField` received a bare string (or other non-token) — layers bind by `design.layer("name")` token only (ADR-0085 §1).
162
+
163
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
164
+
165
+ - `src/components/editor/EditorTextField.tsx`
166
+
167
+ ```text
168
+ the `layer` prop must be a token minted by the design — pass layer={design.layer("name")}, not ${typeof layer === 'string' ? `the bare string "${layer}"` : 'a plain value'}. Tokens validate the name against the design's layers at mint time.
169
+ ```
170
+
171
+ `error.data`: `code`, `received`, `docSlug`
172
+
173
+ ### `E_BOUND_LAYER_MUTATION`
174
+
175
+ **Fires when:** Programmatic rename/delete of a layer a mounted TextField is bound to.
176
+
177
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
178
+
179
+ - `src/hooks/useCommandHistory.ts`
180
+
181
+ ```text
182
+ cannot ${operation} layer "${name}" — a mounted <${bound.part} layer={design.layer("${name}")}> is bound to it. Unmount that field first, or keep the layer and set its text through the field / its binding instead.
183
+ ```
184
+
185
+ `error.data`: `code`, `layer`, `part`, `operation`, `docSlug`
186
+
187
+ ### `E_DUPLICATE_CANVAS`
188
+
189
+ **Fires when:** Two `Editor.Canvas` parts mounted under one Root.
190
+
191
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
192
+
193
+ - `src/components/editor/RootInternals.ts`
194
+
195
+ ```text
196
+ two <Editor.Canvas> parts are mounted under one <Editor.Root>. A Root owns exactly one editable artboard — remove one Canvas, or mount a second <Editor.Root> for a second editor. (Multiple <Editor.Preview> parts ARE valid.)
197
+ ```
198
+
199
+ `error.data`: `code`, `docSlug`
200
+
201
+ ### `E_REQUIRES_CANVAS`
202
+
203
+ **Fires when:** A part that needs a mounted `Editor.Canvas` found none under this Root.
204
+
205
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
206
+
207
+ - `src/components/editor/RootInternals.ts`
208
+
209
+ ```text
210
+ ${parts.join(', ')} require${parts.length === 1 ? 's' : ''} an <Editor.Canvas> under the same <Editor.Root> — they operate on the editable artboard (selection, editor commands). Mount <Editor.Canvas/>, or remove the part. Canvas-free parts: Editor.Preview, Editor.Buy, Editor.TextField, Editor.Layers.
211
+ ```
212
+
213
+ `error.data`: `code`, `parts`, `docSlug`
214
+
215
+ ### `E_BUY_NO_PRODUCT`
216
+
217
+ **Fires when:** `Editor.Buy` mounted without product context.
218
+
219
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
220
+
221
+ - `src/components/editor/EditorBuy.tsx`
222
+
223
+ ```text
224
+ <Editor.Buy> has no product context — pass `product` (with `productId`) on <Editor.Root product={…}>. Buy assembles its cart payload from the Root product and the live design state.
225
+ ```
226
+
227
+ `error.data`: `code`, `docSlug`
228
+
229
+ ### `E_CHUNK_LOAD_FAILED`
230
+
231
+ **Fires when:** The lazy renderer chunk failed to load (network/CDN). Retryable.
232
+
233
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
234
+
235
+ - `src/components/editor/EditorCanvas.tsx`
236
+
237
+ ```text
238
+ the editor's rendering chunk failed to load (${err instanceof Error ? err.message : String(err)}). This is a network/CDN failure, not a composition mistake — retry, or check the deployment serves the @snowcone-app/canvas lazy chunks.
239
+ ```
240
+
241
+ `error.data`: `code`, `cause`, `docSlug`
242
+
243
+ - `src/components/editor/EditorControls.tsx`
244
+
245
+ ```text
246
+ the editor's controls chunk failed to load (${err instanceof Error ? err.message : String(err)}). This is a network/CDN failure, not a composition mistake — retry, or check the deployment serves the @snowcone-app/canvas lazy chunks.
247
+ ```
248
+
249
+ `error.data`: `code`, `cause`, `docSlug`
250
+
251
+ - `src/components/editor/LazyImageBrowser.tsx`
252
+
253
+ ```text
254
+ the editor's image-browser chunk failed to load (${err instanceof Error ? err.message : String(err)}). This is a network/CDN failure, not a composition mistake — retry, or check the deployment serves the @snowcone-app/canvas lazy chunks.
255
+ ```
256
+
257
+ `error.data`: `code`, `cause`, `docSlug`
258
+
259
+ ### `E_EMBED_CONFIG`
260
+
261
+ **Fires when:** Embed wiring invalid at mount (missing grant/mockupIds, variant or placement mismatch) — the throw-mode upgrade of `validateEmbed` (F3).
262
+
263
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
264
+
265
+ - `src/components/editor/EditorRoot.tsx`
266
+
267
+ ```text
268
+ ${fatal.message}
269
+ ```
270
+
271
+ `error.data`: `code`, `issue`, `docSlug`
272
+
273
+ ### `E_PRODUCT_NOT_RENDERABLE`
274
+
275
+ **Fires when:** `productToRenderConfig`: the catalog product has no renderable image placement / no mockup scenes, or the requested placement doesn't exist.
276
+
277
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
278
+
279
+ - `src/design/product-config.ts`
280
+
281
+ ```text
282
+ product ${catalogProduct.id} has no renderable placement "${options.placement}". Renderable image placements: [${renderableLabels.join(', ')}].
283
+ ```
284
+
285
+ `error.data`: `code`, `productId`, `requested`, `validNames`, `docSlug`
286
+
287
+ - `src/design/product-config.ts`
288
+
289
+ ```text
290
+ product ${catalogProduct.id} has no renderable image placement (a non-color placement with width/height). The catalog payload may be incomplete — fetch it via getProduct(), not a search hit.
291
+ ```
292
+
293
+ `error.data`: `code`, `productId`, `docSlug`
294
+
295
+ - `src/design/product-config.ts`
296
+
297
+ ```text
298
+ product ${catalogProduct.id} has no mockup scenes — nothing to render a preview into.
299
+ ```
300
+
301
+ `error.data`: `code`, `productId`, `docSlug`
302
+
303
+ ### `E_DUPLICATE_ARTBOARD`
304
+
305
+ **Fires when:** `design().artboard()`: an artboard with this label already exists in the design (artboard names are placement labels — unique by contract).
306
+
307
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
308
+
309
+ - `src/design/design-builder.ts`
310
+
311
+ ```text
312
+ the design already has an artboard "${label}" — artboard names are placement labels and must be unique. Existing: ${JSON.stringify(this.allArtboards.map((a) => a.name))}.
313
+ ```
314
+
315
+ `error.data`: `code`, `artboardName`, `validNames`, `docSlug`
316
+
317
+ ### `E_UNKNOWN_PLACEMENT`
318
+
319
+ **Fires when:** `usePlacements().select()`: the label names no artboard in the design (ADR-0088). Lists the valid placement labels.
320
+
321
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
322
+
323
+ - `src/components/editor/EditorRoot.tsx`
324
+
325
+ ```text
326
+ no placement "${label}" in this design — its artboards are ${JSON.stringify(placements)}. Add it to the seed with design(…).artboard('${label}', { width, height }) (ADR-0088).
327
+ ```
328
+
329
+ `error.data`: `code`, `placement`, `validNames`, `docSlug`
330
+
331
+ ### `E_UNKNOWN_OPTION`
332
+
333
+ **Fires when:** `select()` named an option key/choice that doesn't exist on the product (Buy-owned selection, ADR-0085 §2 / P2).
334
+
335
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
336
+
337
+ - `src/components/editor/commerceStore.ts`
338
+
339
+ ```text
340
+ product ${this.product.productId} has no option "${key}". Valid option keys: ${JSON.stringify(Object.keys(this.attrs))}.
341
+ ```
342
+
343
+ `error.data`: `code`, `key`, `validKeys`, `docSlug`
344
+
345
+ - `src/components/editor/commerceStore.ts`
346
+
347
+ ```text
348
+ "${choice}" is not a choice of option "${key}" on product ${this.product.productId}. Valid choices: ${JSON.stringify(labels)}.
349
+ ```
350
+
351
+ `error.data`: `code`, `key`, `choice`, `validChoices`, `docSlug`
352
+
353
+ ### `E_COLOR_ROLE_COLLISION`
354
+
355
+ **Fires when:** `design({ colors })`: two color roles share one (normalized) hex value. `useColorBinding` matches by VALUE, so colliding roles would capture each other's elements — rejected at mint time instead of failing subtly.
356
+
357
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
358
+
359
+ - `src/design/design-builder.ts`
360
+
361
+ ```text
362
+ color roles "${collision[0]}" and "${roleName}" both normalize to ${value}. useColorBinding matches elements by VALUE, so colliding roles would repaint each other's elements — give every role a distinct hex.
363
+ ```
364
+
365
+ `error.data`: `code`, `roles`, `value`, `docSlug`
366
+
367
+ ### `E_COLOR_ROLE_INVALID`
368
+
369
+ **Fires when:** `design({ colors })`: a role's value is not a `#RGB`/`#RRGGBB` hex (value-matched bindings normalize hex; other syntaxes can't bind).
370
+
371
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
372
+
373
+ - `src/design/design-builder.ts`
374
+
375
+ ```text
376
+ color role "${roleName}" is ${JSON.stringify(raw)} — roles must be #RGB/#RRGGBB hex (useColorBinding matches by normalized hex value; other syntaxes can't bind).
377
+ ```
378
+
379
+ `error.data`: `code`, `roleName`, `value`, `docSlug`
380
+
381
+ ### `E_COLOR_ROLE_NOT_FOUND`
382
+
383
+ **Fires when:** `design.color(name)`: no color role with this name — lists the design's declared roles.
384
+
385
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
386
+
387
+ - `src/design/design-builder.ts`
388
+
389
+ ```text
390
+ no color role "${name}" in this design. Declared roles: ${JSON.stringify([...this.colorRoles.keys()])}. Declare roles up front — design({ …, colors: { ${name}: '#FF2D8A' } }).
391
+ ```
392
+
393
+ `error.data`: `code`, `roleName`, `validNames`, `docSlug`
394
+
395
+ ### `E_PREVIEW_NO_LOADER`
396
+
397
+ **Fires when:** `Editor.Preview loadingOverlay` got `false`/an unknown value. A mockup preview ALWAYS has a render-in-flight treatment (a server render takes real time, and the loader paints OVER the still-visible previous mockup) — keep the default, pass a variant/custom element, or declare `"host-owned"` with your own indicator. Silence-then-pop-in is a composition mistake, not a preference.
398
+
399
+ **Docs:** <https://developers.snowcone.app/preview-loading-overlay> (`error.data.docSlug`)
400
+
401
+ - `src/components/editor/EditorPreview.tsx`
402
+
403
+ ```text
404
+ Editor.Preview: invalid loadingOverlay value ${JSON.stringify(value)}. A mockup preview always has a render-in-flight treatment (the loader paints over the still-visible previous mockup). Omit the prop for the default rainbow overlay, pass "dark" | "light", pass a custom ReactElement, or declare "host-owned" if the host renders its own indicator off data-sc-preview-state / <Editor.LoadingOverlay>.
405
+ ```
406
+
407
+ `error.data`: `code`, `docSlug`
408
+
409
+ ### `E_ASSET_FORBIDDEN`
410
+
411
+ **Fires when:** `assertRenders`: the shop's asset-origin allowlist denied a design asset (translated from the server's 403 — F4).
412
+
413
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
414
+
415
+ - `src/testing/assertRenders.ts`
416
+
417
+ ```text
418
+ the shop's asset-origin allowlist denied an asset in this design (${label} render). Allow the asset's origin on the shop (or host it on the shop's storage origin).
419
+ ```
420
+
421
+ `error.data`: `code`, `phase`, `url`, `failedAssets`, `docSlug`
422
+
423
+ ### `E_RENDER_FAILED`
424
+
425
+ **Fires when:** `assertRenders`: the render failed or produced no usable image (F4).
426
+
427
+ - `src/testing/assertRenders.ts`
428
+
429
+ ```text
430
+ the ${label} render produced no mockup within ${timeoutMs}ms.
431
+ ```
432
+
433
+ `error.data`: `code`, `phase`, `timeoutMs`
434
+
435
+ - `src/testing/assertRenders.ts`
436
+
437
+ ```text
438
+ the ${label} render failed: ${message}
439
+ ```
440
+
441
+ `error.data`: `code`, `phase`, `error`
442
+
443
+ - `src/testing/assertRenders.ts`
444
+
445
+ ```text
446
+ the ${label} render failed to dispatch: ${message}
447
+ ```
448
+
449
+ `error.data`: `code`, `phase`, `error`
450
+
451
+ - `src/testing/assertRenders.ts`
452
+
453
+ ```text
454
+ the ${label} render's image fetch failed: ${err.message}
455
+ ```
456
+
457
+ `error.data`: `code`, `phase`, `url`, `...err.data`
458
+
459
+ ### `E_BINDING_INERT`
460
+
461
+ **Fires when:** `assertRenders`: the mutated render is byte-identical to the seed render — the binding under test is dead (the two-render contract, F4).
462
+
463
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
464
+
465
+ - `src/testing/assertRenders.ts`
466
+
467
+ ```text
468
+ the mutated render is byte-identical to the seed render — the mutation never reached the server, so the binding under test is DEAD. Usual causes: the mutated layer name doesn't exist in the design (check design.layer() names), or opts.mutate returned the seed unchanged.
469
+ ```
470
+
471
+ `error.data`: `code`, `seedUrl`, `mutatedUrl`, `bytes`, `docSlug`
472
+
473
+ ### `E_NO_MUTABLE_BINDING`
474
+
475
+ **Fires when:** `assertRenders`: the design has no named text layer to mutate and no `mutate` override was given (F4).
476
+
477
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
478
+
479
+ - `src/testing/assertRenders.ts`
480
+
481
+ ```text
482
+ this design has no NAMED text layer to mutate for the two-render check. Name one (design().text("…", { name: "headline" })) or pass opts.mutate to define the mutation.
483
+ ```
484
+
485
+ `error.data`: `code`, `docSlug`
486
+
487
+ ### `E_AUDIT_NO_ROOT`
488
+
489
+ **Fires when:** `auditEditor()` called with no `<Editor.Root>` mounted — the imperative audit reads the most-recent mounted Root's registry (#10).
490
+
491
+ **Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
492
+
493
+ - `src/components/editor/editorAudit.ts`
494
+
495
+ ```text
496
+ auditEditor() found no mounted <Editor.Root>. Mount a Root first — or, to audit at mount time, pass onAudit on the Root: <Editor.Root onAudit={(report) => …}>.
497
+ ```
498
+
499
+ `error.data`: `code`, `docSlug`
500
+
501
+ ## Embed wiring issues (the `onIssue` channel)
502
+
503
+ Mount-time wiring problems and runtime stalls are `EmbedIssue` objects
504
+ (`{ severity, issue, message }`) delivered to `Editor.Root`'s `onIssue`
505
+ prop in every env. `warn`-severity issues are advisory — the embed still
506
+ mounts and renders what it can. `error`-severity issues break the embed,
507
+ so `Editor.Root` throws them as `E_EMBED_CONFIG` (the issue key is in
508
+ `error.data.issue`).
509
+
510
+ ### `missing-shop`
511
+
512
+ **Severity:** error (throws as `E_EMBED_CONFIG`)
513
+
514
+ **Fires when:** No `shop` (the publishable shop id) was passed.
515
+
516
+ - `src/components/embed-realtime/validateEmbed.ts`
517
+
518
+ ```text
519
+ `shop` is required (your publishable shop id).
520
+ ```
521
+
522
+ ### `missing-grant`
523
+
524
+ **Severity:** error (throws as `E_EMBED_CONFIG`)
525
+
526
+ **Fires when:** No grant source (`grantUrl`/`grant`) — the session can never authorize, so no mockup ever renders.
527
+
528
+ - `src/components/embed-realtime/validateEmbed.ts`
529
+
530
+ ```text
531
+ no grant source — pass `grantUrl` (a proxy to mintRealtimeGrant) or `grant`. Without it the session can never authorize and no mockup renders.
532
+ ```
533
+
534
+ ### `empty-mockup-ids`
535
+
536
+ **Severity:** error (throws as `E_EMBED_CONFIG`)
537
+
538
+ **Fires when:** `product.mockupIds` is empty — nothing to render.
539
+
540
+ - `src/components/embed-realtime/validateEmbed.ts`
541
+
542
+ ```text
543
+ product.mockupIds is empty — pass the catalog scene codes (product.mockups[].id) you want rendered, e.g. ["FV1qjO"].
544
+ ```
545
+
546
+ ### `variant-required`
547
+
548
+ **Severity:** error (throws as `E_EMBED_CONFIG`)
549
+
550
+ **Fires when:** The product has a variant axis but no `variantId` — the render would hang ~30s waiting on a color blob that never comes.
551
+
552
+ - `src/components/embed-realtime/validateEmbed.ts`
553
+
554
+ ```text
555
+ ${err instanceof Error ? err.message : String(err)}
556
+ ```
557
+
558
+ ### `multi-placement`
559
+
560
+ **Severity:** error (throws as `E_EMBED_CONFIG`)
561
+
562
+ **Fires when:** The product requires several placements but this embed renders ONE — the server times out waiting on the rest (needs the eject path).
563
+
564
+ - `src/components/embed-realtime/validateEmbed.ts`
565
+
566
+ ```text
567
+ product ${product.productId} requires placements [${required.join(', ')}] but the design only has artboards [${artboardNames.join(', ')}] — missing [${missing.join(', ')}]. The render would time out waiting for the missing canvas_states. Add each missing placement to the seed: design(…).artboard('${missing[0]}', { width, height })… (ADR-0088), or use the eject path — see https://developers.snowcone.app/embed-mode#eject.
568
+ ```
569
+
570
+ ### `placement-mismatch`
571
+
572
+ **Severity:** error (throws as `E_EMBED_CONFIG`)
573
+
574
+ **Fires when:** The design's placement isn't one of the product's placements — the server renders a blank mockup.
575
+
576
+ - `src/components/embed-realtime/validateEmbed.ts`
577
+
578
+ ```text
579
+ the design's artboard(s) [${foreign.join(', ')}] are not in product.placements [${product.placements!.join(', ')}]. Every artboard name must equal a real product placement label (renderState targets placements by that name), or the server renders a blank mockup. Fix the `name` passed to design({ name }) / design().artboard(name, …).
580
+ ```
581
+
582
+ ### `duplicate-layer-names`
583
+
584
+ **Severity:** warn (advisory)
585
+
586
+ **Fires when:** Two layers share a name — name-bound hooks bind the FIRST match, so a control may edit the wrong layer.
587
+
588
+ - `src/components/embed-realtime/validateEmbed.ts`
589
+
590
+ ```text
591
+ duplicate layer name(s) [${[...new Set(dupes)].join(', ')}] — useTextBinding(name) / useImageBinding(name) bind to the FIRST match, so a duplicate name makes a control edit the wrong layer. Keep names unique.
592
+ ```
593
+
594
+ ### `slow-render`
595
+
596
+ **Severity:** warn (advisory)
597
+
598
+ **Fires when:** Runtime (not mount-time): a render has been in flight past `SLOW_RENDER_HINT_MS` with no mockup and no error — the silent-hang class. The render may still arrive; this is a heads-up.
599
+
600
+ - `src/components/editor/EditorRoot.tsx`
package/README.md CHANGED
@@ -135,7 +135,8 @@ Every part renders minimal DOM tagged `data-sc-part` and themes through
135
135
  `.sc-root` CSS custom properties — the full generated reference is
136
136
  [`STYLING_CONTRACT.md`](./STYLING_CONTRACT.md). Composition mistakes throw
137
137
  structured `EditorError`s (stable `code` + `error.data`) in dev AND prod,
138
- contained by the Root's error boundary. Test with
138
+ contained by the Root's error boundary — the full generated code catalog is
139
+ [`ERROR_CONTRACT.md`](./ERROR_CONTRACT.md). Test with
139
140
  `@snowcone-app/canvas/testing`: `assertRenders` proves a design's bindings
140
141
  are alive; `expectNoEditorErrors()` makes contained errors fail your tests.
141
142