@snowcone-app/canvas 0.36.7 → 0.37.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.
- package/ERROR_CONTRACT.md +137 -0
- package/dist/{EditorCanvasImpl-BeeY09cA.js → EditorCanvasImpl-BRdFWZA5.js} +2 -2
- package/dist/{EditorCanvasImpl-BeeY09cA.js.map → EditorCanvasImpl-BRdFWZA5.js.map} +1 -1
- package/dist/FontAnalyzer-CEYA_6fY.js.map +1 -1
- package/dist/api/embed.d.ts +3 -0
- package/dist/components/embed-realtime/validateEmbed.d.ts +13 -0
- package/dist/design/bundle-config.d.ts +127 -0
- package/dist/design/product-config.d.ts +3 -0
- package/dist/embed-CLz3CTcX.js +1799 -0
- package/dist/embed-CLz3CTcX.js.map +1 -0
- package/dist/embed.mjs +50 -45
- package/dist/errors/editorError.d.ts +28 -1
- package/example/package.json +1 -1
- package/package.json +3 -3
- package/dist/embed-D9n0deJ3.js +0 -1588
- package/dist/embed-D9n0deJ3.js.map +0 -1
package/ERROR_CONTRACT.md
CHANGED
|
@@ -52,6 +52,12 @@ the issue key in `error.data.issue`.
|
|
|
52
52
|
| [`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). |
|
|
53
53
|
| [`E_FONT_LOAD_FAILED`](#e_font_load_failed) | throw | A custom (brand) font failed to load from its hosted URL — the binary fetch failed (CORS, 404, network) or FontFace rejected the bytes. Custom fonts NEVER fall back silently to a system face: the design would render with a different typeface/metrics everywhere it's consumed. `data` carries `family`, `url`, `weight`, `style`. Fix the URL (must serve the font file over a plain cross-origin GET) and re-register. Dev AND prod. |
|
|
54
54
|
| [`E_DISTRIBUTE_MIN_THREE`](#e_distribute_min_three) | throw | `computeDistribute()` called with fewer than 3 movable elements — distributing needs something between the two ends. Gate the action on `useAlignDistribute().canDistribute` instead of calling below the threshold (the toolbar hides its distribute buttons for exactly this reason). |
|
|
55
|
+
| [`E_BUNDLE_CONFIG_REQUIRED`](#e_bundle_config_required) | throw | `productToRenderConfig()` was handed a BUNDLE (a catalog product with a `bundle` block — several products in one mockup, ADR-0099). A bundle has no single `variantId` and its placements are role-scoped (`top.front`), so the single-product adapter cannot describe it: use `bundleToRenderConfig()` instead. Never renders the wrong thing. |
|
|
56
|
+
| [`E_NOT_A_BUNDLE`](#e_not_a_bundle) | throw | `bundleToRenderConfig()` was handed a product with no `bundle` block — `picks` / `variants` are bundle parameters the server refuses on a single product. Use `productToRenderConfig()`. |
|
|
57
|
+
| [`E_BUNDLE_ROLE_REQUIRED`](#e_bundle_role_required) | throw | A bundle placement was named WITHOUT its role (`"front"` instead of `"top.front"`): `options.placement` on `bundleToRenderConfig()`, or a design artboard on a bundle session. Two members can both have a "Front", so the bare label is never the identity — `validNames` lists the role-scoped keys. |
|
|
58
|
+
| [`E_BUNDLE_ROLE_NOT_FOUND`](#e_bundle_role_not_found) | throw | `picks` / `variants` named a role the bundle has no slot for. `validNames` lists the bundle's roles. |
|
|
59
|
+
| [`E_BUNDLE_PICK_NOT_FOUND`](#e_bundle_pick_not_found) | throw | `picks[role]` named a product that is not one of that slot's alternatives. `validNames` lists the slot's alternatives. |
|
|
60
|
+
| [`E_BUNDLE_VARIANT_NOT_FOUND`](#e_bundle_variant_not_found) | throw | A slot has no resolvable variant: `variants[role]` is not one of the picked alternative's variant gvids (`validNames`), or the alternative publishes no variant at all. Every role of a bundle session needs a variant — the server keys the render on the per-role set. |
|
|
55
61
|
| [`missing-shop`](#missing-shop) | error → throws `E_EMBED_CONFIG` | No `shop` (the publishable shop id) was passed. |
|
|
56
62
|
| [`missing-grant`](#missing-grant) | error → throws `E_EMBED_CONFIG` | No grant source (`grantUrl`/`grant`) — the session can never authorize, so no mockup ever renders. |
|
|
57
63
|
| [`empty-mockup-ids`](#empty-mockup-ids) | error → throws `E_EMBED_CONFIG` | `product.mockupIds` is empty — nothing to render. |
|
|
@@ -59,6 +65,8 @@ the issue key in `error.data.issue`.
|
|
|
59
65
|
| [`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). |
|
|
60
66
|
| [`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. |
|
|
61
67
|
| [`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. |
|
|
68
|
+
| [`bundle-role-required`](#bundle-role-required) | error → throws `E_EMBED_CONFIG` | BUNDLE session (ADR-0099): a design artboard is a bare label (`"Front"`) instead of a role-scoped key (`"top.front"`), or a bare `variantId` is set — the server refuses both (`E_BUNDLE_ROLE_REQUIRED`): two members can both have a "Front". |
|
|
69
|
+
| [`bundle-variant-required`](#bundle-variant-required) | error → throws `E_EMBED_CONFIG` | BUNDLE session (ADR-0099): a role has no entry in `product.variants` — the server keys the render on the per-role variant set, so every role needs one (`bundleToRenderConfig` fills defaults; hand-written configs must too). |
|
|
62
70
|
| [`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. |
|
|
63
71
|
|
|
64
72
|
## `EditorError` codes
|
|
@@ -289,6 +297,30 @@ the issue key in `error.data.issue`.
|
|
|
289
297
|
|
|
290
298
|
**Docs:** <https://developers.snowcone.app/embed-mode> (`error.data.docSlug`)
|
|
291
299
|
|
|
300
|
+
- `src/design/bundle-config.ts`
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
bundle ${catalogProduct.id} has no renderable placement "${options.placement}" for picks ${JSON.stringify(picks)}. Keys: [${placementKeys.join(', ')}].
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`error.data`: `code`, `productId`, `requested`, `picks`, `validNames`, `docSlug`
|
|
307
|
+
|
|
308
|
+
- `src/design/bundle-config.ts`
|
|
309
|
+
|
|
310
|
+
```text
|
|
311
|
+
bundle ${catalogProduct.id} has no renderable image placement for picks ${JSON.stringify(picks)} (a role-scoped, non-color placement with width/height). The catalog payload may be incomplete — fetch it via getProduct(), not a search hit.
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`error.data`: `code`, `productId`, `picks`, `docSlug`
|
|
315
|
+
|
|
316
|
+
- `src/design/bundle-config.ts`
|
|
317
|
+
|
|
318
|
+
```text
|
|
319
|
+
bundle ${catalogProduct.id} has no mockup scenes — nothing to render a preview into.
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`error.data`: `code`, `productId`, `docSlug`
|
|
323
|
+
|
|
292
324
|
- `src/design/product-config.ts`
|
|
293
325
|
|
|
294
326
|
```text
|
|
@@ -573,6 +605,81 @@ the issue key in `error.data.issue`.
|
|
|
573
605
|
|
|
574
606
|
`error.data`: `code`, `mode`, `count`, `min`
|
|
575
607
|
|
|
608
|
+
### `E_BUNDLE_CONFIG_REQUIRED`
|
|
609
|
+
|
|
610
|
+
**Fires when:** `productToRenderConfig()` was handed a BUNDLE (a catalog product with a `bundle` block — several products in one mockup, ADR-0099). A bundle has no single `variantId` and its placements are role-scoped (`top.front`), so the single-product adapter cannot describe it: use `bundleToRenderConfig()` instead. Never renders the wrong thing.
|
|
611
|
+
|
|
612
|
+
**Docs:** <https://developers.snowcone.app/bundles> (`error.data.docSlug`)
|
|
613
|
+
|
|
614
|
+
- `src/design/product-config.ts`
|
|
615
|
+
|
|
616
|
+
```text
|
|
617
|
+
product ${catalogProduct.id} is a bundle (roles: [${catalogProduct.bundle.members.map((m) => m.role).join(', ')}]) — it has per-role variants and role-scoped placement keys, which the single-product config cannot describe. Use bundleToRenderConfig(product, { picks?, variants?, placement?, width? }) instead.
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
`error.data`: `code`, `productId`, `validNames`, `docSlug`
|
|
621
|
+
|
|
622
|
+
### `E_NOT_A_BUNDLE`
|
|
623
|
+
|
|
624
|
+
**Fires when:** `bundleToRenderConfig()` was handed a product with no `bundle` block — `picks` / `variants` are bundle parameters the server refuses on a single product. Use `productToRenderConfig()`.
|
|
625
|
+
|
|
626
|
+
- `src/design/bundle-config.ts`
|
|
627
|
+
|
|
628
|
+
```text
|
|
629
|
+
product ${catalogProduct.id} has no bundle block — picks / variants are bundle parameters the server refuses on a single product (E_NOT_A_BUNDLE). Use productToRenderConfig() for it.
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`error.data`: `code`, `productId`, `docSlug`
|
|
633
|
+
|
|
634
|
+
### `E_BUNDLE_ROLE_REQUIRED`
|
|
635
|
+
|
|
636
|
+
**Fires when:** A bundle placement was named WITHOUT its role (`"front"` instead of `"top.front"`): `options.placement` on `bundleToRenderConfig()`, or a design artboard on a bundle session. Two members can both have a "Front", so the bare label is never the identity — `validNames` lists the role-scoped keys.
|
|
637
|
+
|
|
638
|
+
- `src/design/bundle-config.ts`
|
|
639
|
+
|
|
640
|
+
```text
|
|
641
|
+
placement "${options.placement}" names no role — on bundle ${catalogProduct.id} a placement is addressed by its role-scoped key. Keys: [${placementKeys.join(', ')}].
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
`error.data`: `code`, `productId`, `requested`, `validNames`, `docSlug`
|
|
645
|
+
|
|
646
|
+
### `E_BUNDLE_ROLE_NOT_FOUND`
|
|
647
|
+
|
|
648
|
+
**Fires when:** `picks` / `variants` named a role the bundle has no slot for. `validNames` lists the bundle's roles.
|
|
649
|
+
|
|
650
|
+
- `src/design/bundle-config.ts`
|
|
651
|
+
|
|
652
|
+
```text
|
|
653
|
+
bundle ${catalogProduct.id} has no role "${role}" (in ${field}). Roles: [${roleNames.join(', ')}].
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
`error.data`: `code`, `productId`, `field`, `role`, `validNames`, `docSlug`
|
|
657
|
+
|
|
658
|
+
### `E_BUNDLE_PICK_NOT_FOUND`
|
|
659
|
+
|
|
660
|
+
**Fires when:** `picks[role]` named a product that is not one of that slot's alternatives. `validNames` lists the slot's alternatives.
|
|
661
|
+
|
|
662
|
+
- `src/design/bundle-config.ts`
|
|
663
|
+
|
|
664
|
+
```text
|
|
665
|
+
bundle ${catalogProduct.id} role "${member.role}" has no alternative "${wanted}". Alternatives: [${alternatives.join(', ')}] — pass picks: { ${member.role}: "${alternatives[0]}" }.
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
`error.data`: `code`, `productId`, `role`, `requested`, `validNames`, `docSlug`
|
|
669
|
+
|
|
670
|
+
### `E_BUNDLE_VARIANT_NOT_FOUND`
|
|
671
|
+
|
|
672
|
+
**Fires when:** A slot has no resolvable variant: `variants[role]` is not one of the picked alternative's variant gvids (`validNames`), or the alternative publishes no variant at all. Every role of a bundle session needs a variant — the server keys the render on the per-role set.
|
|
673
|
+
|
|
674
|
+
- `src/design/bundle-config.ts`
|
|
675
|
+
|
|
676
|
+
```text
|
|
677
|
+
${accepted.length === 0 ? `bundle ${catalogProduct.id} role "${member.role}": alternative ${alt.productId} publishes no variant — `}nothing can render this slot (catalog data is incomplete).'
|
|
678
|
+
: `bundle ${catalogProduct.id} role "${member.role}" (${alt.productId}) has no variant "${requested}". Variants: [${accepted.join(', ')}] — pass variants: { ${member.role}: "${accepted[0]}" }.
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
`error.data`: `code`, `productId`, `role`, `memberProductId`, `requested`, `validNames`, `docSlug`
|
|
682
|
+
|
|
576
683
|
## Embed wiring issues (the `onIssue` channel)
|
|
577
684
|
|
|
578
685
|
Mount-time wiring problems and runtime stalls are `EmbedIssue` objects
|
|
@@ -666,6 +773,36 @@ so `Editor.Root` throws them as `E_EMBED_CONFIG` (the issue key is in
|
|
|
666
773
|
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.
|
|
667
774
|
```
|
|
668
775
|
|
|
776
|
+
### `bundle-role-required`
|
|
777
|
+
|
|
778
|
+
**Severity:** error (throws as `E_EMBED_CONFIG`)
|
|
779
|
+
|
|
780
|
+
**Fires when:** BUNDLE session (ADR-0099): a design artboard is a bare label (`"Front"`) instead of a role-scoped key (`"top.front"`), or a bare `variantId` is set — the server refuses both (`E_BUNDLE_ROLE_REQUIRED`): two members can both have a "Front".
|
|
781
|
+
|
|
782
|
+
- `src/components/embed-realtime/validateEmbed.ts`
|
|
783
|
+
|
|
784
|
+
```text
|
|
785
|
+
product ${product.productId} is a bundle, but the design's artboard(s) [${roleless.join(', ')}] name no role. On a bundle every placement is addressed by its role-scoped key (<role>.<key>)${keys.length ? ` — keys: [${keys.join(', ')}]` : ''}. Seed the design from bundleToRenderConfig(product).placement.key, not from a bare label.
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
- `src/components/embed-realtime/validateEmbed.ts`
|
|
789
|
+
|
|
790
|
+
```text
|
|
791
|
+
product ${product.productId} is a bundle, but product.variantId ("${product.variantId}") names no slot — the server refuses it (E_BUNDLE_ROLE_REQUIRED). Name the variant per role instead: product.variants = { <role>: <gvid> } (bundleToRenderConfig fills it), and leave variantId unset.
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### `bundle-variant-required`
|
|
795
|
+
|
|
796
|
+
**Severity:** error (throws as `E_EMBED_CONFIG`)
|
|
797
|
+
|
|
798
|
+
**Fires when:** BUNDLE session (ADR-0099): a role has no entry in `product.variants` — the server keys the render on the per-role variant set, so every role needs one (`bundleToRenderConfig` fills defaults; hand-written configs must too).
|
|
799
|
+
|
|
800
|
+
- `src/components/embed-realtime/validateEmbed.ts`
|
|
801
|
+
|
|
802
|
+
```text
|
|
803
|
+
product ${product.productId} is a bundle, but role(s) [${missingVariant.join(', ')}] have no entry in product.variants (${JSON.stringify(product.variants ?? {})}). Every role needs a variant — the server keys the render on the per-role set. Build the product with bundleToRenderConfig(product, { picks, variants }), which fills each slot's default.
|
|
804
|
+
```
|
|
805
|
+
|
|
669
806
|
### `slow-render`
|
|
670
807
|
|
|
671
808
|
**Severity:** warn (advisory)
|
|
@@ -3,7 +3,7 @@ import { forwardRef as j, useContext as b, useSyncExternalStore as D, useMemo as
|
|
|
3
3
|
import { S as N } from "./SnowconeCanvas-BaFR8QXs.js";
|
|
4
4
|
import { d as U } from "./ContextualToolbars-CG5Gmv_i.js";
|
|
5
5
|
import { s as V, e as k, c as q } from "./design-builder-CqgTpp9B.js";
|
|
6
|
-
import { u as z } from "./embed-
|
|
6
|
+
import { u as z } from "./embed-CLz3CTcX.js";
|
|
7
7
|
import { T as L } from "./FontAnalyzer-CEYA_6fY.js";
|
|
8
8
|
import { q as h } from "./useCommands-CQi4Pyd2.js";
|
|
9
9
|
const O = () => () => {
|
|
@@ -78,4 +78,4 @@ const ee = j(
|
|
|
78
78
|
export {
|
|
79
79
|
ee as EditorCanvasImpl
|
|
80
80
|
};
|
|
81
|
-
//# sourceMappingURL=EditorCanvasImpl-
|
|
81
|
+
//# sourceMappingURL=EditorCanvasImpl-BRdFWZA5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorCanvasImpl-BeeY09cA.js","sources":["../src/components/editor/EditorCanvasImpl.tsx"],"sourcesContent":["'use client';\n\n/**\n * The heavy half of `Editor.Canvas` (and of the legacy `<RealtimeCanvas>`),\n * extracted from RealtimeCanvas's body. This module statically imports\n * `SnowconeCanvas` — i.e. the full rendering engine — and is therefore ONLY\n * ever reached through a dynamic `import()` (D9): the embed entry's static\n * import graph stays renderer-free, mechanically enforced by the\n * import-graph CI check.\n *\n * Beyond the original wiring (seed → SnowconeCanvas, edit →\n * `serializeStateForServer` → `pushRender`), this adds the ADR-0085 §3 store\n * wiring: every canvas change lands in the Root's {@link DesignStateStore}\n * (the ONE live design state), and a {@link CanvasBridge} is registered so\n * `Editor.TextField` writes route through the editor command path (undo/redo,\n * repaint) while a canvas is mounted — the editor document stays the single\n * write authority.\n */\n\nimport React, {\n forwardRef,\n useContext,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { SnowconeCanvas } from '../embed/SnowconeCanvas.js';\nimport { ContextualToolbars } from '../ContextualToolbars.js';\nimport type { CanvasState, SnowconeCanvasHandle } from '../embed/SnowconeCanvas.js';\nimport { serializeStateForServer } from '../../rendering/serialize-for-server.js';\nimport type { KitPresetId, KitDefinition } from '../../kits/types.js';\nimport type { CanvasServices } from '../../contexts/CanvasServicesContext.js';\nimport { useEmbedContext } from '../embed-realtime/context.js';\nimport { useElementsContext } from '../../contexts/ElementsContext.js';\nimport { useCommandContext } from '../../contexts/CommandContext.js';\nimport { TextElement } from '../../core/TextElement.js';\nimport { EditorRootInternalsContext } from './RootInternals.js';\n\nexport interface EditorCanvasImplProps {\n /** Your controls (toolbar, inputs). Rendered as an overlay inside the editor\n * context so binding hooks work. */\n children?: ReactNode;\n /** Editor chrome preset. `Editor.Canvas` pins the chrome-less `'embed-only'`;\n * the legacy `<RealtimeCanvas kit>` prop passes richer presets through. */\n kit?: KitPresetId | KitDefinition;\n /** Inherit the host app's theme instead of managing its own (default true). */\n inheritTheme?: boolean;\n /** Cap the canvas height in px. */\n maxHeight?: number;\n /** Privileged-service wiring override (usually inherited from Root). */\n services?: CanvasServices;\n /** Observe the live design state (the same {@link CanvasState} the Root's\n * state store tracks). Safe to pass inline — read through a ref. */\n onChange?: (state: CanvasState) => void;\n /** Whether the wrapping div self-applies `.sc-root`. The legacy\n * `<RealtimeCanvas>` always did (it could be the only Snowcone DOM on the\n * page); under `Editor.Root` the Root applies it once and parts never\n * self-apply (ADR-0085). Re-applying is harmless (same variables), so the\n * legacy wrapper keeps `true` for bit-identical back-compat. */\n selfScope?: boolean;\n /** `data-sc-part` value for the wrapping div (the parts contract). */\n partAttr?: string;\n /** Mount the built-in floating contextual toolbar (ADR-0085 §5: with no\n * `Editor.Controls` mounted, `Editor.Canvas` defaults to the floating\n * toolbar; a registered DOCKED Controls suppresses it). Set by\n * `Editor.Canvas` only — the legacy `<RealtimeCanvas>` wrapper keeps\n * today's chrome-less behavior. @internal */\n builtinControls?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst noopSubscribe = (): (() => void) => () => {};\n\n/**\n * Runs INSIDE the canvas's editor context (the overlay slot) and registers\n * the imperative bridge sibling parts use to edit by layer name through the\n * command path. Generalizes `useTextBinding`'s write mechanics to a dynamic\n * name (a hook can't take names at call time).\n */\nfunction CanvasBindingBridge(): null {\n const { elementStore } = useElementsContext();\n const { executeElementUpdate } = useCommandContext();\n // Tolerant on purpose: the legacy <RealtimeCanvas> is supported inside a\n // bare EmbedContext (advanced consumers provide their own); only under an\n // <Editor.Root> is there a registry to bridge into.\n const internals = useContext(EditorRootInternalsContext);\n\n const latest = useRef({ elementStore, executeElementUpdate });\n latest.current = { elementStore, executeElementUpdate };\n\n const registry = internals?.registry ?? null;\n useLayoutEffect(() => {\n if (!registry) return;\n return registry.registerCanvasBridge({\n setTextByName(name: string, text: string): boolean {\n const { elementStore: store, executeElementUpdate: execute } = latest.current;\n const matches = store.getAllByName(name).filter((el): el is TextElement => el instanceof TextElement);\n if (matches.length === 0) return false;\n for (const el of matches) {\n const cloned = el.clone() as TextElement;\n cloned.setText(text);\n execute(el, cloned);\n }\n return true;\n },\n });\n }, [registry]);\n\n return null;\n}\n\nexport const EditorCanvasImpl = forwardRef<SnowconeCanvasHandle, EditorCanvasImplProps>(\n function EditorCanvasImpl(\n {\n children,\n kit = 'embed-only',\n inheritTheme = true,\n maxHeight,\n services,\n onChange,\n selfScope = false,\n partAttr,\n builtinControls = false,\n className,\n style,\n }: EditorCanvasImplProps,\n ref,\n ): React.ReactElement {\n const { placement, throttleMs, pieceGuides, pushRender, stateStore } = useEmbedContext();\n\n // Under an <Editor.Root> the editor document is mounted by the ROOT (P1\n // provider inversion, D9: state always) — SnowconeCanvas must write into\n // that document instead of mounting its own, and a REMOUNT of this part\n // must rebuild the (surviving) document from the live seed instead of\n // accreting onto the previous mount's artboards/elements. The legacy\n // <RealtimeCanvas> inside a bare EmbedContext (no Root) keeps mounting\n // its own provider, exactly as before.\n const rootInternals = useContext(EditorRootInternalsContext);\n const underRoot = rootInternals !== null;\n\n // ADR-0085 §5 (Controls placement resolution): with no Editor.Controls\n // mounted — or a `placement=\"floating\"` one — the canvas owns the\n // floating contextual toolbar; a registered DOCKED Controls suppresses\n // it. Subscribed to the part registry so the zones swap live when a\n // docked Controls mounts/unmounts.\n const partsRegistry = rootInternals?.registry ?? null;\n useSyncExternalStore(\n partsRegistry ? partsRegistry.subscribe : noopSubscribe,\n partsRegistry ? partsRegistry.getVersion : () => 0,\n partsRegistry ? partsRegistry.getVersion : () => 0,\n );\n const showBuiltinFloatingControls =\n builtinControls && underRoot && !(partsRegistry?.hasDockedControls() ?? false);\n\n // Seed from the LIVE store, not the original design (ADR-0085 §3): the\n // store starts as design.toJSON(), so a fresh mount is identical to the\n // old behavior — but edits made while this chunk was loading (e.g. a\n // TextField keystroke in the first second) survive instead of being\n // clobbered by a stale seed. Captured once per store identity; later\n // store changes flow through the editor document, not re-seeding.\n const seed = useMemo(() => stateStore.getState(), [stateStore]);\n\n // Latest-ref so a host's inline `onChange` identity never matters here\n // (SnowconeCanvas already refs ITS callback; this keeps ours symmetric).\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n\n const rootClass = selfScope ? 'sc-root' : 'sc-canvas-frame';\n\n return (\n <div className={`${rootClass}${className ? ` ${className}` : ''}`} style={style} data-sc-part={partAttr}>\n <SnowconeCanvas\n ref={ref}\n kit={kit}\n inheritTheme={inheritTheme}\n services={services}\n externalProvider={underRoot}\n resetDocumentOnMount={underRoot}\n artboards={seed.artboards}\n activeArtboard={placement}\n initialElements={seed.elements}\n layoutConfig={maxHeight ? { maxHeight } : undefined}\n overlay={\n <>\n <CanvasBindingBridge />\n {showBuiltinFloatingControls ? <ContextualToolbars mode=\"floating\" partAttr=\"controls\" /> : null}\n {children}\n </>\n }\n // #566: the product's printable-area / dead-zone overlay, fetched and\n // dims-gated by the Root — same overlay snowcone.app's editor\n // shows, so a buyer can't unknowingly edit under a camera cutout.\n pieceGuides={pieceGuides ?? undefined}\n onChange={(state) => {\n // ONE store, last-write-wins (ADR-0085 §3): the editor document is\n // the write authority while mounted; the Root store derives from\n // its onChange, then the render follows the store.\n stateStore.setState(state);\n pushRender(serializeStateForServer(state), throttleMs);\n onChangeRef.current?.(state);\n }}\n />\n </div>\n );\n },\n);\n\nexport type { CanvasState, SnowconeCanvasHandle };\n"],"names":["noopSubscribe","CanvasBindingBridge","elementStore","useElementsContext","executeElementUpdate","useCommandContext","internals","useContext","EditorRootInternalsContext","latest","useRef","registry","useLayoutEffect","name","text","store","execute","matches","el","TextElement","cloned","EditorCanvasImpl","forwardRef","children","kit","inheritTheme","maxHeight","services","onChange","selfScope","partAttr","builtinControls","className","style","ref","placement","throttleMs","pieceGuides","pushRender","stateStore","useEmbedContext","rootInternals","underRoot","partsRegistry","useSyncExternalStore","showBuiltinFloatingControls","seed","useMemo","onChangeRef","jsx","SnowconeCanvas","jsxs","Fragment","ContextualToolbars","state","serializeStateForServer","_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EditorCanvasImpl-BRdFWZA5.js","sources":["../src/components/editor/EditorCanvasImpl.tsx"],"sourcesContent":["'use client';\n\n/**\n * The heavy half of `Editor.Canvas` (and of the legacy `<RealtimeCanvas>`),\n * extracted from RealtimeCanvas's body. This module statically imports\n * `SnowconeCanvas` — i.e. the full rendering engine — and is therefore ONLY\n * ever reached through a dynamic `import()` (D9): the embed entry's static\n * import graph stays renderer-free, mechanically enforced by the\n * import-graph CI check.\n *\n * Beyond the original wiring (seed → SnowconeCanvas, edit →\n * `serializeStateForServer` → `pushRender`), this adds the ADR-0085 §3 store\n * wiring: every canvas change lands in the Root's {@link DesignStateStore}\n * (the ONE live design state), and a {@link CanvasBridge} is registered so\n * `Editor.TextField` writes route through the editor command path (undo/redo,\n * repaint) while a canvas is mounted — the editor document stays the single\n * write authority.\n */\n\nimport React, {\n forwardRef,\n useContext,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { SnowconeCanvas } from '../embed/SnowconeCanvas.js';\nimport { ContextualToolbars } from '../ContextualToolbars.js';\nimport type { CanvasState, SnowconeCanvasHandle } from '../embed/SnowconeCanvas.js';\nimport { serializeStateForServer } from '../../rendering/serialize-for-server.js';\nimport type { KitPresetId, KitDefinition } from '../../kits/types.js';\nimport type { CanvasServices } from '../../contexts/CanvasServicesContext.js';\nimport { useEmbedContext } from '../embed-realtime/context.js';\nimport { useElementsContext } from '../../contexts/ElementsContext.js';\nimport { useCommandContext } from '../../contexts/CommandContext.js';\nimport { TextElement } from '../../core/TextElement.js';\nimport { EditorRootInternalsContext } from './RootInternals.js';\n\nexport interface EditorCanvasImplProps {\n /** Your controls (toolbar, inputs). Rendered as an overlay inside the editor\n * context so binding hooks work. */\n children?: ReactNode;\n /** Editor chrome preset. `Editor.Canvas` pins the chrome-less `'embed-only'`;\n * the legacy `<RealtimeCanvas kit>` prop passes richer presets through. */\n kit?: KitPresetId | KitDefinition;\n /** Inherit the host app's theme instead of managing its own (default true). */\n inheritTheme?: boolean;\n /** Cap the canvas height in px. */\n maxHeight?: number;\n /** Privileged-service wiring override (usually inherited from Root). */\n services?: CanvasServices;\n /** Observe the live design state (the same {@link CanvasState} the Root's\n * state store tracks). Safe to pass inline — read through a ref. */\n onChange?: (state: CanvasState) => void;\n /** Whether the wrapping div self-applies `.sc-root`. The legacy\n * `<RealtimeCanvas>` always did (it could be the only Snowcone DOM on the\n * page); under `Editor.Root` the Root applies it once and parts never\n * self-apply (ADR-0085). Re-applying is harmless (same variables), so the\n * legacy wrapper keeps `true` for bit-identical back-compat. */\n selfScope?: boolean;\n /** `data-sc-part` value for the wrapping div (the parts contract). */\n partAttr?: string;\n /** Mount the built-in floating contextual toolbar (ADR-0085 §5: with no\n * `Editor.Controls` mounted, `Editor.Canvas` defaults to the floating\n * toolbar; a registered DOCKED Controls suppresses it). Set by\n * `Editor.Canvas` only — the legacy `<RealtimeCanvas>` wrapper keeps\n * today's chrome-less behavior. @internal */\n builtinControls?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\nconst noopSubscribe = (): (() => void) => () => {};\n\n/**\n * Runs INSIDE the canvas's editor context (the overlay slot) and registers\n * the imperative bridge sibling parts use to edit by layer name through the\n * command path. Generalizes `useTextBinding`'s write mechanics to a dynamic\n * name (a hook can't take names at call time).\n */\nfunction CanvasBindingBridge(): null {\n const { elementStore } = useElementsContext();\n const { executeElementUpdate } = useCommandContext();\n // Tolerant on purpose: the legacy <RealtimeCanvas> is supported inside a\n // bare EmbedContext (advanced consumers provide their own); only under an\n // <Editor.Root> is there a registry to bridge into.\n const internals = useContext(EditorRootInternalsContext);\n\n const latest = useRef({ elementStore, executeElementUpdate });\n latest.current = { elementStore, executeElementUpdate };\n\n const registry = internals?.registry ?? null;\n useLayoutEffect(() => {\n if (!registry) return;\n return registry.registerCanvasBridge({\n setTextByName(name: string, text: string): boolean {\n const { elementStore: store, executeElementUpdate: execute } = latest.current;\n const matches = store.getAllByName(name).filter((el): el is TextElement => el instanceof TextElement);\n if (matches.length === 0) return false;\n for (const el of matches) {\n const cloned = el.clone() as TextElement;\n cloned.setText(text);\n execute(el, cloned);\n }\n return true;\n },\n });\n }, [registry]);\n\n return null;\n}\n\nexport const EditorCanvasImpl = forwardRef<SnowconeCanvasHandle, EditorCanvasImplProps>(\n function EditorCanvasImpl(\n {\n children,\n kit = 'embed-only',\n inheritTheme = true,\n maxHeight,\n services,\n onChange,\n selfScope = false,\n partAttr,\n builtinControls = false,\n className,\n style,\n }: EditorCanvasImplProps,\n ref,\n ): React.ReactElement {\n const { placement, throttleMs, pieceGuides, pushRender, stateStore } = useEmbedContext();\n\n // Under an <Editor.Root> the editor document is mounted by the ROOT (P1\n // provider inversion, D9: state always) — SnowconeCanvas must write into\n // that document instead of mounting its own, and a REMOUNT of this part\n // must rebuild the (surviving) document from the live seed instead of\n // accreting onto the previous mount's artboards/elements. The legacy\n // <RealtimeCanvas> inside a bare EmbedContext (no Root) keeps mounting\n // its own provider, exactly as before.\n const rootInternals = useContext(EditorRootInternalsContext);\n const underRoot = rootInternals !== null;\n\n // ADR-0085 §5 (Controls placement resolution): with no Editor.Controls\n // mounted — or a `placement=\"floating\"` one — the canvas owns the\n // floating contextual toolbar; a registered DOCKED Controls suppresses\n // it. Subscribed to the part registry so the zones swap live when a\n // docked Controls mounts/unmounts.\n const partsRegistry = rootInternals?.registry ?? null;\n useSyncExternalStore(\n partsRegistry ? partsRegistry.subscribe : noopSubscribe,\n partsRegistry ? partsRegistry.getVersion : () => 0,\n partsRegistry ? partsRegistry.getVersion : () => 0,\n );\n const showBuiltinFloatingControls =\n builtinControls && underRoot && !(partsRegistry?.hasDockedControls() ?? false);\n\n // Seed from the LIVE store, not the original design (ADR-0085 §3): the\n // store starts as design.toJSON(), so a fresh mount is identical to the\n // old behavior — but edits made while this chunk was loading (e.g. a\n // TextField keystroke in the first second) survive instead of being\n // clobbered by a stale seed. Captured once per store identity; later\n // store changes flow through the editor document, not re-seeding.\n const seed = useMemo(() => stateStore.getState(), [stateStore]);\n\n // Latest-ref so a host's inline `onChange` identity never matters here\n // (SnowconeCanvas already refs ITS callback; this keeps ours symmetric).\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n\n const rootClass = selfScope ? 'sc-root' : 'sc-canvas-frame';\n\n return (\n <div className={`${rootClass}${className ? ` ${className}` : ''}`} style={style} data-sc-part={partAttr}>\n <SnowconeCanvas\n ref={ref}\n kit={kit}\n inheritTheme={inheritTheme}\n services={services}\n externalProvider={underRoot}\n resetDocumentOnMount={underRoot}\n artboards={seed.artboards}\n activeArtboard={placement}\n initialElements={seed.elements}\n layoutConfig={maxHeight ? { maxHeight } : undefined}\n overlay={\n <>\n <CanvasBindingBridge />\n {showBuiltinFloatingControls ? <ContextualToolbars mode=\"floating\" partAttr=\"controls\" /> : null}\n {children}\n </>\n }\n // #566: the product's printable-area / dead-zone overlay, fetched and\n // dims-gated by the Root — same overlay snowcone.app's editor\n // shows, so a buyer can't unknowingly edit under a camera cutout.\n pieceGuides={pieceGuides ?? undefined}\n onChange={(state) => {\n // ONE store, last-write-wins (ADR-0085 §3): the editor document is\n // the write authority while mounted; the Root store derives from\n // its onChange, then the render follows the store.\n stateStore.setState(state);\n pushRender(serializeStateForServer(state), throttleMs);\n onChangeRef.current?.(state);\n }}\n />\n </div>\n );\n },\n);\n\nexport type { CanvasState, SnowconeCanvasHandle };\n"],"names":["noopSubscribe","CanvasBindingBridge","elementStore","useElementsContext","executeElementUpdate","useCommandContext","internals","useContext","EditorRootInternalsContext","latest","useRef","registry","useLayoutEffect","name","text","store","execute","matches","el","TextElement","cloned","EditorCanvasImpl","forwardRef","children","kit","inheritTheme","maxHeight","services","onChange","selfScope","partAttr","builtinControls","className","style","ref","placement","throttleMs","pieceGuides","pushRender","stateStore","useEmbedContext","rootInternals","underRoot","partsRegistry","useSyncExternalStore","showBuiltinFloatingControls","seed","useMemo","onChangeRef","jsx","SnowconeCanvas","jsxs","Fragment","ContextualToolbars","state","serializeStateForServer","_a"],"mappings":"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"}
|