@snowcone-app/canvas 0.21.3 → 0.22.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/STYLING_CONTRACT.md +17 -7
- package/dist/{EditorCanvasImpl-M7rw1c1k.js → EditorCanvasImpl-BH_bKy-F.js} +2 -2
- package/dist/{EditorCanvasImpl-M7rw1c1k.js.map → EditorCanvasImpl-BH_bKy-F.js.map} +1 -1
- package/dist/api/embed.d.ts +3 -1
- package/dist/components/editor/EditorCanvas.d.ts +9 -0
- package/dist/components/editor/EditorRoot.d.ts +11 -0
- package/dist/components/embed-realtime/validateEmbed.d.ts +10 -4
- package/dist/design/product-config.d.ts +48 -0
- package/dist/editorErrors-B2mb1sVN.js.map +1 -1
- package/dist/{embed-QBZkgWDr.js → embed-8ecUMMH-.js} +524 -430
- package/dist/embed-8ecUMMH-.js.map +1 -0
- package/dist/embed.mjs +30 -29
- package/dist/errors/editorError.d.ts +3 -0
- package/example/package.json +1 -1
- package/package.json +1 -1
- package/dist/embed-QBZkgWDr.js.map +0 -1
package/STYLING_CONTRACT.md
CHANGED
|
@@ -184,23 +184,29 @@ stable hooks; `sc:`-prefixed utility classes are internal and NOT contract.
|
|
|
184
184
|
<input type="file">
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
### `preview` — Editor.Preview — awaiting the first mockup
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<div aria-busy="true" data-sc-part="preview" data-sc-preview-state="rendering">
|
|
191
|
+
```
|
|
192
|
+
|
|
187
193
|
### `preview` — Editor.Preview — with a rendered mockup
|
|
188
194
|
|
|
189
195
|
```html
|
|
190
|
-
<img alt=<alt> data-sc-part="preview" src=<url>>
|
|
196
|
+
<img alt=<alt> data-sc-part="preview" data-sc-preview-state="ready" src=<url>>
|
|
191
197
|
```
|
|
192
198
|
|
|
193
199
|
### `preview-error` — Editor.Preview — render/grant failure before any mockup
|
|
194
200
|
|
|
195
201
|
```html
|
|
196
|
-
<div data-sc-error=<message> data-sc-part="preview-error" role="alert">
|
|
202
|
+
<div data-sc-error=<message> data-sc-part="preview-error" data-sc-preview-state="error" role="alert">
|
|
197
203
|
```
|
|
198
204
|
|
|
199
205
|
### `root` — Editor.Root
|
|
200
206
|
|
|
201
207
|
```html
|
|
202
208
|
<div class="sc-root" data-sc-part="root">
|
|
203
|
-
<img alt=<alt> data-sc-part="preview" src=<url>>
|
|
209
|
+
<img alt=<alt> data-sc-part="preview" data-sc-preview-state="ready" src=<url>>
|
|
204
210
|
<input aria-label=<label> data-sc-layer=<layer-name> data-sc-part="textfield" placeholder=<placeholder> type="text">
|
|
205
211
|
<button data-sc-buy-missing=<reason-codes> data-sc-buy-state="incomplete" data-sc-part="buy" disabled type="button">
|
|
206
212
|
```
|
|
@@ -224,7 +230,8 @@ stable hooks; `sc:`-prefixed utility classes are internal and NOT contract.
|
|
|
224
230
|
<div>
|
|
225
231
|
…
|
|
226
232
|
<button aria-label=<label> data-sc-part="snowcone-editor-pip" type="button">
|
|
227
|
-
<
|
|
233
|
+
<div aria-busy="true" data-sc-part="preview" data-sc-preview-state="rendering">
|
|
234
|
+
<span aria-busy="true">
|
|
228
235
|
<div data-sc-part="snowcone-editor-zone" data-sc-zone-state="idle">
|
|
229
236
|
<div>
|
|
230
237
|
<div data-sc-part="controls" data-sc-placement="docked">
|
|
@@ -296,7 +303,8 @@ stable hooks; `sc:`-prefixed utility classes are internal and NOT contract.
|
|
|
296
303
|
|
|
297
304
|
```html
|
|
298
305
|
<button aria-label=<label> data-sc-part="snowcone-editor-pip" type="button">
|
|
299
|
-
<
|
|
306
|
+
<div aria-busy="true" data-sc-part="preview" data-sc-preview-state="rendering">
|
|
307
|
+
<span aria-busy="true">
|
|
300
308
|
```
|
|
301
309
|
|
|
302
310
|
### `snowcone-editor-stage` — SnowconeEditor — stage
|
|
@@ -309,7 +317,8 @@ stable hooks; `sc:`-prefixed utility classes are internal and NOT contract.
|
|
|
309
317
|
<div>
|
|
310
318
|
…
|
|
311
319
|
<button aria-label=<label> data-sc-part="snowcone-editor-pip" type="button">
|
|
312
|
-
<
|
|
320
|
+
<div aria-busy="true" data-sc-part="preview" data-sc-preview-state="rendering">
|
|
321
|
+
<span aria-busy="true">
|
|
313
322
|
```
|
|
314
323
|
|
|
315
324
|
### `snowcone-editor-swap-back` — SnowconeEditor — the "artwork" chip back to the canvas
|
|
@@ -322,7 +331,8 @@ stable hooks; `sc:`-prefixed utility classes are internal and NOT contract.
|
|
|
322
331
|
|
|
323
332
|
```html
|
|
324
333
|
<div data-sc-part="snowcone-editor-swap-stage">
|
|
325
|
-
<div aria-busy="true">
|
|
334
|
+
<div aria-busy="true" data-sc-part="preview" data-sc-preview-state="rendering">
|
|
335
|
+
<div aria-busy="true">
|
|
326
336
|
<button aria-label=<label> data-sc-part="snowcone-editor-swap-back" type="button">
|
|
327
337
|
```
|
|
328
338
|
|
|
@@ -3,7 +3,7 @@ import { forwardRef as j, useContext as E, useSyncExternalStore as D, useMemo as
|
|
|
3
3
|
import { S as N } from "./SnowconeCanvas-Cx7nXlBI.js";
|
|
4
4
|
import { d as U } from "./ContextualToolbars-BDEt9ND0.js";
|
|
5
5
|
import { s as V } from "./design-builder-CT4DHIPe.js";
|
|
6
|
-
import { u as k } from "./embed-
|
|
6
|
+
import { u as k } from "./embed-8ecUMMH-.js";
|
|
7
7
|
import { c as z, b as L } from "./editorErrors-B2mb1sVN.js";
|
|
8
8
|
import { T as O } from "./FontAnalyzer-CwIpgTGd.js";
|
|
9
9
|
import { p as h } from "./UnifiedFontService-CC3CSmH5.js";
|
|
@@ -79,4 +79,4 @@ const te = j(
|
|
|
79
79
|
export {
|
|
80
80
|
te as EditorCanvasImpl
|
|
81
81
|
};
|
|
82
|
-
//# sourceMappingURL=EditorCanvasImpl-
|
|
82
|
+
//# sourceMappingURL=EditorCanvasImpl-BH_bKy-F.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorCanvasImpl-M7rw1c1k.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-BH_bKy-F.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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"}
|
package/dist/api/embed.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export { useRealtimeMockups } from '../components/embed-realtime/useRealtimeMock
|
|
|
49
49
|
export type { UseRealtimeMockupsResult } from '../components/embed-realtime/useRealtimeMockups.js';
|
|
50
50
|
export { validateEmbed, collectEmbedIssues } from '../components/embed-realtime/validateEmbed.js';
|
|
51
51
|
export type { ValidateEmbedInput, EmbedIssue } from '../components/embed-realtime/validateEmbed.js';
|
|
52
|
+
export { productToRenderConfig } from '../design/product-config.js';
|
|
53
|
+
export type { ProductRenderConfig, ProductRenderConfigOptions } from '../design/product-config.js';
|
|
52
54
|
export { design, DesignBuilder, isLayerToken } from '../design/design-builder.js';
|
|
53
55
|
export type { Anchor, ShapeKind, DesignArtboard, StateArtboard, FromStateOptions, AnyDesign, LayerKind, LayerToken, } from '../design/design-builder.js';
|
|
54
56
|
export { useEditor } from '../contexts/EditorContext.js';
|
|
@@ -77,6 +79,6 @@ export { CanvasServicesProvider, useCanvasServices } from '../contexts/CanvasSer
|
|
|
77
79
|
export type { CanvasServices, ResolvedCanvasServices } from '../contexts/CanvasServicesContext.js';
|
|
78
80
|
export { createProxyImageSearch, createProxyGenerate } from '../services/imageSearch.js';
|
|
79
81
|
export type { ImageSearchProvider, ImageSearchResult, ImageSearchResponse, ImageSearchParams, GenerateProvider, GenerateParams, GenerateResponse, } from '../services/imageSearch.js';
|
|
80
|
-
export type { RenderProduct, RenderProductOptions, RenderProductCombination, RenderProductRequiredPlacement, MockupResult, RealtimeGrant, } from '@snowcone-app/sdk';
|
|
82
|
+
export type { CatalogProduct, RenderProduct, RenderProductOptions, RenderProductCombination, RenderProductRequiredPlacement, MockupResult, RealtimeGrant, } from '@snowcone-app/sdk';
|
|
81
83
|
export { SnowconeEditor, formatBuyPrice } from '../default-editor/SnowconeEditor.js';
|
|
82
84
|
export type { SnowconeEditorProps } from '../default-editor/SnowconeEditor.js';
|
|
@@ -16,6 +16,15 @@ export interface EditorCanvasProps {
|
|
|
16
16
|
children?: ReactNode;
|
|
17
17
|
/** Cap the canvas height in px. */
|
|
18
18
|
maxHeight?: number;
|
|
19
|
+
/**
|
|
20
|
+
* The canvas's built-in selection-anchored floating toolbar (default
|
|
21
|
+
* `true`). Pass `false` when your composition owns the contextual zone —
|
|
22
|
+
* e.g. a conditionally-rendered `<Editor.Controls placement="docked"/>` —
|
|
23
|
+
* so suppression is declared HERE, in the type surface, rather than implied
|
|
24
|
+
* by whether a docked Controls happens to be mounted this frame (a mounted
|
|
25
|
+
* docked Controls still suppresses it either way, ADR-0085 §5).
|
|
26
|
+
*/
|
|
27
|
+
floatingToolbar?: boolean;
|
|
19
28
|
className?: string;
|
|
20
29
|
style?: CSSProperties;
|
|
21
30
|
}
|
|
@@ -2,6 +2,7 @@ import { default as React, ReactNode } from 'react';
|
|
|
2
2
|
import { RenderProduct, MockupResult, RealtimeGrant } from '@snowcone-app/sdk';
|
|
3
3
|
import { AnyDesign } from '../../design/design-builder.js';
|
|
4
4
|
import { CanvasServices } from '../../contexts/CanvasServicesContext.js';
|
|
5
|
+
import { EmbedIssue } from '../embed-realtime/validateEmbed.js';
|
|
5
6
|
/**
|
|
6
7
|
* How the session authorizes. Provide exactly one — the discriminated union makes
|
|
7
8
|
* "I forgot the grant" a compile error rather than a silent runtime hang.
|
|
@@ -41,6 +42,16 @@ export type EditorRootBaseProps = {
|
|
|
41
42
|
onMockups?: (results: MockupResult[]) => void;
|
|
42
43
|
/** Called on a render/timeout error. */
|
|
43
44
|
onRenderError?: (message: string) => void;
|
|
45
|
+
/**
|
|
46
|
+
* The machine-readable advisory channel (F3's other half): every non-fatal
|
|
47
|
+
* issue — warn-severity wiring problems at mount, the runtime `slow-render`
|
|
48
|
+
* stall — is delivered here, in dev AND prod. Console output is as
|
|
49
|
+
* invisible to a headless agent as it is to production telemetry; assert on
|
|
50
|
+
* this callback instead of scraping logs. (Error-severity wiring issues
|
|
51
|
+
* don't arrive here — they throw `EditorError E_EMBED_CONFIG` into Root's
|
|
52
|
+
* boundary.)
|
|
53
|
+
*/
|
|
54
|
+
onIssue?: (issue: EmbedIssue) => void;
|
|
44
55
|
/** Override the realtime WS endpoint (defaults to the SDK's prod URL). */
|
|
45
56
|
wsUrl?: string;
|
|
46
57
|
/**
|
|
@@ -6,13 +6,19 @@ export interface ValidateEmbedInput {
|
|
|
6
6
|
design: AnyDesign;
|
|
7
7
|
hasGrant: boolean;
|
|
8
8
|
}
|
|
9
|
-
/** One wiring problem found at mount. `error`-severity
|
|
10
|
-
* (no render will ever arrive / it targets the wrong
|
|
11
|
-
* issues are advisory.
|
|
9
|
+
/** One wiring problem found at mount, or a runtime stall. `error`-severity
|
|
10
|
+
* issues break the embed (no render will ever arrive / it targets the wrong
|
|
11
|
+
* thing); `warn`-severity issues are advisory. Every issue also reaches the
|
|
12
|
+
* host through `Editor.Root`'s `onIssue` — the machine-readable channel
|
|
13
|
+
* (console.warn is as invisible to a headless agent as console.error). */
|
|
12
14
|
export interface EmbedIssue {
|
|
13
15
|
severity: 'error' | 'warn';
|
|
14
16
|
/** Stable issue key, carried into `EditorError.data.issue` in throw mode. */
|
|
15
|
-
issue: 'missing-shop' | 'missing-grant' | 'empty-mockup-ids' | 'variant-required' | 'multi-placement' | 'placement-mismatch' | 'duplicate-layer-names'
|
|
17
|
+
issue: 'missing-shop' | 'missing-grant' | 'empty-mockup-ids' | 'variant-required' | 'multi-placement' | 'placement-mismatch' | 'duplicate-layer-names'
|
|
18
|
+
/** Runtime (not mount-time): a render has been in flight past
|
|
19
|
+
* `SLOW_RENDER_HINT_MS` with no mockup and no error — the silent-hang
|
|
20
|
+
* class. The render may still arrive; this is a heads-up. */
|
|
21
|
+
| 'slow-render';
|
|
16
22
|
message: string;
|
|
17
23
|
}
|
|
18
24
|
/** Find every wiring problem. Pure — no logging, no throwing. */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CatalogProduct, RenderProduct } from '@snowcone-app/sdk';
|
|
2
|
+
export interface ProductRenderConfigOptions {
|
|
3
|
+
/** Placement label to edit (e.g. "Back"). Defaults to the product's first
|
|
4
|
+
* renderable image placement. Unknown labels throw with `validNames`. */
|
|
5
|
+
placement?: string;
|
|
6
|
+
/** Render width passthrough (`product.width`) — bump to ~2000 for a crisp
|
|
7
|
+
* full-bleed preview. `Editor.Preview` hints in dev when it's too low. */
|
|
8
|
+
width?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ProductRenderConfig {
|
|
11
|
+
/** Ready for `<Editor.Root product={…}>` — advisory fields included so
|
|
12
|
+
* wiring mistakes fail loud at mount. */
|
|
13
|
+
product: RenderProduct;
|
|
14
|
+
/** The placement being edited — seed `design({ name: label, width, height })`
|
|
15
|
+
* from exactly these values. */
|
|
16
|
+
placement: {
|
|
17
|
+
label: string;
|
|
18
|
+
/** Artboard pixel dimensions (the placement's print-file geometry). */
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
/** width / height — for sizing the host's canvas stage. */
|
|
22
|
+
aspectRatio: number;
|
|
23
|
+
};
|
|
24
|
+
/** Every editable (non-color) placement label — placement pickers. */
|
|
25
|
+
placementLabels: string[];
|
|
26
|
+
/** First mockup scene that photographs the rendered variant — the default
|
|
27
|
+
* `<Editor.Preview mockupId>`. */
|
|
28
|
+
primaryMockupId: string;
|
|
29
|
+
/** The variant the config renders against (`undefined` when no color axis
|
|
30
|
+
* gates rendering). Already set on `product`. */
|
|
31
|
+
variantId: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Derive the full embed wiring from one `getProduct()` payload.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const config = productToRenderConfig(await getProduct('BEEB77'));
|
|
38
|
+
* const seed = design({ name: config.placement.label, ...config.placement })
|
|
39
|
+
* .text('hello', { anchor: 'center', name: 'headline' });
|
|
40
|
+
*
|
|
41
|
+
* <Editor.Root shop={SHOP} product={config.product} design={seed} grantUrl="/api/realtime-grant">
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @throws EditorError `E_PRODUCT_NOT_RENDERABLE` when the product has no
|
|
45
|
+
* renderable image placement, no mockup scenes, or `options.placement`
|
|
46
|
+
* names a placement that doesn't exist (`data.validNames` lists the real ones).
|
|
47
|
+
*/
|
|
48
|
+
export declare function productToRenderConfig(catalogProduct: CatalogProduct, options?: ProductRenderConfigOptions): ProductRenderConfig;
|