@snowcone-app/canvas 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,4 +11,24 @@ export interface RealtimeMockupProps {
11
11
  className?: string;
12
12
  style?: CSSProperties;
13
13
  }
14
+ /**
15
+ * How far the displayed device-pixel width may exceed the delivered mockup's
16
+ * natural width before {@link resolutionMismatchHint} fires. Renders default to
17
+ * 1000px wide, so a 1000px image inside a ~1100 device-px slot is fine (browser
18
+ * downscale slack), but stretched across 2000 device px it looks soft and the
19
+ * developer deserves to hear why.
20
+ */
21
+ export declare const RESOLUTION_MISMATCH_RATIO = 1.2;
22
+ /** Rounds a device-pixel width up to the next {@link SUGGESTED_WIDTHS} step
23
+ * (capped at the top step). */
24
+ export declare function suggestRenderWidth(devicePx: number): number;
25
+ /**
26
+ * Builds the dev-console warning shown when a delivered mockup's natural width
27
+ * is meaningfully below the width it's displayed at in DEVICE pixels (CSS width
28
+ * × `devicePixelRatio`) — the "my preview looks soft and nothing says why"
29
+ * trap: renders default to 1000px wide, which blurs on a large or high-density
30
+ * display. Returns `null` when the resolution is adequate (within
31
+ * {@link RESOLUTION_MISMATCH_RATIO}) or either measurement is unusable.
32
+ */
33
+ export declare function resolutionMismatchHint(naturalWidth: number, devicePx: number): string | null;
14
34
  export declare function RealtimeMockup({ mockupId, fallback, alt, className, style, }: RealtimeMockupProps): React.ReactElement;
package/dist/embed.mjs CHANGED
@@ -1,189 +1,210 @@
1
- import { a as K, e as z } from "./validation-C857Ptxn.js";
2
- import { b as pe, d as ve, f as be } from "./validation-C857Ptxn.js";
3
- import { y as T } from "./HybridHistoryManager-DANuzx7G.js";
4
- import { jsx as b, Fragment as J } from "react/jsx-runtime";
5
- import { createContext as W, useContext as H, useMemo as E, useRef as C, useState as q, useEffect as M } from "react";
6
- import { assertVariantSelected as Y, RenderSession as Q } from "@snowcone-app/sdk";
7
- import { S as X, s as Z } from "./template-fields-C8Tj67ek.js";
8
- import { D as Ee, T as $e, d as Se, r as ke, a as Re, e as Te, f as we, g as xe, w as Ie } from "./template-fields-C8Tj67ek.js";
9
- const V = W(void 0);
10
- function B() {
11
- const e = H(V);
1
+ import { a as W, e as H } from "./validation-C857Ptxn.js";
2
+ import { b as we, d as ye, f as Se } from "./validation-C857Ptxn.js";
3
+ import { y as b } from "./HybridHistoryManager-DANuzx7G.js";
4
+ import { jsx as E, Fragment as z } from "react/jsx-runtime";
5
+ import { createContext as J, useContext as Y, useMemo as S, useRef as x, useState as q, useEffect as B } from "react";
6
+ import { assertVariantSelected as Q, RenderSession as X } from "@snowcone-app/sdk";
7
+ import { S as Z, s as ee } from "./template-fields-C8Tj67ek.js";
8
+ import { D as $e, T as Re, d as Te, r as Ie, a as xe, e as Me, f as Ue, g as Ce, w as Ne } from "./template-fields-C8Tj67ek.js";
9
+ const L = J(void 0);
10
+ function _() {
11
+ const e = Y(L);
12
12
  if (!e)
13
13
  throw new Error(
14
14
  "useRealtimeMockups / <RealtimeCanvas> / <RealtimeMockup> must be rendered inside <RealtimeEmbed>. Wrap them: <RealtimeEmbed shop=… product=… design=… grantUrl=…>…</RealtimeEmbed>"
15
15
  );
16
16
  return e;
17
17
  }
18
- const p = "[RealtimeEmbed]";
19
- function ee(e) {
20
- var a, i;
21
- if ((a = e == null ? void 0 : e.requiredPlacements) != null && a.length)
18
+ const v = "[RealtimeEmbed]";
19
+ function ne(e) {
20
+ var o, l;
21
+ if ((o = e == null ? void 0 : e.requiredPlacements) != null && o.length)
22
22
  return e.requiredPlacements.filter((t) => !t.autoFilledByVariant).map((t) => t.label);
23
23
  const n = new Set(
24
- (((i = e == null ? void 0 : e.options) == null ? void 0 : i.combinations) ?? []).map((t) => t.variantId).filter((t) => typeof t == "string" && t !== "")
24
+ (((l = e == null ? void 0 : e.options) == null ? void 0 : l.combinations) ?? []).map((t) => t.variantId).filter((t) => typeof t == "string" && t !== "")
25
25
  );
26
26
  return !!(e != null && e.variantId) || n.size > 1 ? [] : (e == null ? void 0 : e.placements) ?? [];
27
27
  }
28
- function ne({ shop: e, product: n, design: s, hasGrant: a }) {
29
- var v, o;
30
- if (e || console.error(`${p} \`shop\` is required (your publishable shop id).`), a || console.error(
31
- `${p} no grant source — pass \`grantUrl\` (a proxy to mintRealtimeGrant) or \`grant\`. Without it the session can never authorize and no mockup renders.`
32
- ), (v = n == null ? void 0 : n.mockupIds) != null && v.length || console.error(
33
- `${p} product.mockupIds is empty — pass the catalog scene codes (product.mockups[].id) you want rendered, e.g. ["FV1qjO"].`
28
+ function te({ shop: e, product: n, design: s, hasGrant: o }) {
29
+ var u, a;
30
+ if (e || console.error(`${v} \`shop\` is required (your publishable shop id).`), o || console.error(
31
+ `${v} no grant source — pass \`grantUrl\` (a proxy to mintRealtimeGrant) or \`grant\`. Without it the session can never authorize and no mockup renders.`
32
+ ), (u = n == null ? void 0 : n.mockupIds) != null && u.length || console.error(
33
+ `${v} product.mockupIds is empty — pass the catalog scene codes (product.mockups[].id) you want rendered, e.g. ["FV1qjO"].`
34
34
  ), n != null && n.options)
35
35
  try {
36
- Y(n);
36
+ Q(n);
37
37
  } catch (r) {
38
- console.error(`${p} ${r instanceof Error ? r.message : String(r)}`);
38
+ console.error(`${v} ${r instanceof Error ? r.message : String(r)}`);
39
39
  }
40
- const i = s.toJSON(), t = i.activeArtboard, d = ee(n);
41
- d.length > 1 && console.error(
42
- `${p} product ${n.productId} requires placements [${d.join(", ")}] but <RealtimeEmbed> renders a single placement ("${t}"). The render will time out waiting for the others. This product needs the eject path — see https://developers.snowcone.app/embed-mode#eject.`
43
- ), (o = n == null ? void 0 : n.placements) != null && o.length && !n.placements.includes(t) && console.warn(
44
- `${p} the design's placement "${t}" is not in product.placements [${n.placements.join(", ")}]. The canvas artboard name and renderState placement must equal a real product placement label, or the server renders a blank mockup. Fix the \`name\` passed to design({ name }).`
40
+ const l = s.toJSON(), t = l.activeArtboard, f = ne(n);
41
+ f.length > 1 && console.error(
42
+ `${v} product ${n.productId} requires placements [${f.join(", ")}] but <RealtimeEmbed> renders a single placement ("${t}"). The render will time out waiting for the others. This product needs the eject path — see https://developers.snowcone.app/embed-mode#eject.`
43
+ ), (a = n == null ? void 0 : n.placements) != null && a.length && !n.placements.includes(t) && console.warn(
44
+ `${v} the design's placement "${t}" is not in product.placements [${n.placements.join(", ")}]. The canvas artboard name and renderState placement must equal a real product placement label, or the server renders a blank mockup. Fix the \`name\` passed to design({ name }).`
45
45
  );
46
- const c = i.elements.map((r) => r.name).filter((r) => !!r), h = c.filter((r, l) => c.indexOf(r) !== l);
46
+ const c = l.elements.map((r) => r.name).filter((r) => !!r), h = c.filter((r, i) => c.indexOf(r) !== i);
47
47
  h.length && console.warn(
48
- `${p} duplicate layer name(s) [${[...new Set(h)].join(", ")}] — useTextBinding(name) / useImageBinding(name) bind to the FIRST match, so a duplicate name makes a control edit the wrong layer. Keep names unique.`
48
+ `${v} duplicate layer name(s) [${[...new Set(h)].join(", ")}] — useTextBinding(name) / useImageBinding(name) bind to the FIRST match, so a duplicate name makes a control edit the wrong layer. Keep names unique.`
49
49
  );
50
50
  }
51
- function te(e) {
51
+ function re(e) {
52
52
  if ("grantUrl" in e && e.grantUrl)
53
53
  return { grantUrl: e.grantUrl };
54
54
  const n = e.grant;
55
55
  return typeof n == "function" ? { getToken: n } : { getToken: () => Promise.resolve(n) };
56
56
  }
57
- const _ = 8e3;
58
- function re(e) {
59
- return `[RealtimeEmbed] No mockup after ${Math.round(_ / 1e3)}s. Common causes: a variant product missing \`variantId\` (pass \`product.options\` so the embed can flag it up front), a multi-placement product (the server waits for every required placement but <RealtimeEmbed> sends only "${e}" — pass \`product.requiredPlacements\` so this is flagged at mount; such products need the eject path), an unreachable or non-public asset URL in the design, or the placement "${e}" not existing on the product. The render may still arrive — this is a heads-up, not a failure.`;
57
+ const V = 8e3;
58
+ function se(e) {
59
+ return `[RealtimeEmbed] No mockup after ${Math.round(V / 1e3)}s. Common causes: a variant product missing \`variantId\` (pass \`product.options\` so the embed can flag it up front), a multi-placement product (the server waits for every required placement but <RealtimeEmbed> sends only "${e}" — pass \`product.requiredPlacements\` so this is flagged at mount; such products need the eject path), an unreachable or non-public asset URL in the design, or the placement "${e}" not existing on the product. The render may still arrive — this is a heads-up, not a failure.`;
60
60
  }
61
- function me(e) {
62
- const { shop: n, product: s, design: a, throttleMs: i = 200, onMockups: t, onRenderError: d, wsUrl: c, services: h, children: v } = e, o = E(() => a.toJSON().activeArtboard, [a]), r = C(t), l = C(d);
63
- r.current = t, l.current = d;
64
- const [N, D] = q([]), [$, y] = q(!0), [j, S] = q(null), w = !!("grantUrl" in e && e.grantUrl || "grant" in e && e.grant), x = te(e), L = "grantUrl" in x ? x.grantUrl : "getToken", I = `${s.productId}|${s.mockupIds.join(",")}|${s.variantId ?? ""}`, k = E(
61
+ function ge(e) {
62
+ const { shop: n, product: s, design: o, throttleMs: l = 200, onMockups: t, onRenderError: f, wsUrl: c, services: h, children: u } = e, a = S(() => o.toJSON().activeArtboard, [o]), r = x(t), i = x(f);
63
+ r.current = t, i.current = f;
64
+ const [w, k] = q([]), [$, y] = q(!0), [j, R] = q(null), M = !!("grantUrl" in e && e.grantUrl || "grant" in e && e.grant), U = re(e), G = "grantUrl" in U ? U.grantUrl : "getToken", C = `${s.productId}|${s.mockupIds.join(",")}|${s.variantId ?? ""}`, T = S(
65
65
  () => {
66
66
  try {
67
- return { session: new Q({ shop: n, product: s, wsUrl: c, ...x }) };
67
+ return { session: new X({ shop: n, product: s, wsUrl: c, ...U }) };
68
68
  } catch (m) {
69
69
  return { error: m instanceof Error ? m.message : String(m) };
70
70
  }
71
71
  },
72
72
  // eslint-disable-next-line react-hooks/exhaustive-deps
73
- [n, I, o, c, L]
74
- ), g = "session" in k ? k.session : null, R = "error" in k ? k.error : null, P = C(null);
75
- M(() => {
73
+ [n, C, a, c, G]
74
+ ), g = "session" in T ? T.session : null, I = "error" in T ? T.error : null, P = x(null);
75
+ B(() => {
76
76
  var m;
77
- if (typeof T < "u" && ((m = T.env) == null ? void 0 : m.NODE_ENV) !== "production") {
78
- const u = `${n}|${I}|${o}|${w}`;
79
- if (P.current === u) return;
80
- P.current = u, ne({ shop: n, product: s, design: a, hasGrant: w });
77
+ if (typeof b < "u" && ((m = b.env) == null ? void 0 : m.NODE_ENV) !== "production") {
78
+ const d = `${n}|${C}|${a}|${M}`;
79
+ if (P.current === d) return;
80
+ P.current = d, te({ shop: n, product: s, design: o, hasGrant: M });
81
81
  }
82
- }, [n, I, o, w]);
83
- const U = E(() => (m, u = i) => {
84
- g && (S(null), y(!0), g.renderState(o, m, u).catch((f) => {
85
- var A;
86
- const O = f instanceof Error ? f.message : String(f);
87
- S(O), y(!1), (A = l.current) == null || A.call(l, O);
82
+ }, [n, C, a, M]);
83
+ const N = S(() => (m, d = l) => {
84
+ g && (R(null), y(!0), g.renderState(a, m, d).catch((p) => {
85
+ var F;
86
+ const D = p instanceof Error ? p.message : String(p);
87
+ R(D), y(!1), (F = i.current) == null || F.call(i, D);
88
88
  }));
89
- }, [g, o, i]);
90
- M(() => {
89
+ }, [g, a, l]);
90
+ B(() => {
91
91
  var m;
92
- if (R) {
93
- S(R), y(!1), (m = l.current) == null || m.call(l, R);
92
+ if (I) {
93
+ R(I), y(!1), (m = i.current) == null || m.call(i, I);
94
94
  return;
95
95
  }
96
96
  if (g)
97
- return g.onMockups((u) => {
98
- var f;
99
- D(u), y(!1), (f = r.current) == null || f.call(r, u);
100
- }), g.onError((u) => {
101
- var f;
102
- S(u), y(!1), (f = l.current) == null || f.call(l, u);
103
- }), U(a.toServerRequest()), () => g.close();
104
- }, [g, R]), M(() => {
105
- var u;
106
- if (!$ || typeof T > "u" || ((u = T.env) == null ? void 0 : u.NODE_ENV) === "production") return;
107
- const m = setTimeout(() => console.warn(re(o)), _);
97
+ return g.onMockups((d) => {
98
+ var p;
99
+ k(d), y(!1), (p = r.current) == null || p.call(r, d);
100
+ }), g.onError((d) => {
101
+ var p;
102
+ R(d), y(!1), (p = i.current) == null || p.call(i, d);
103
+ }), N(o.toServerRequest()), () => g.close();
104
+ }, [g, I]), B(() => {
105
+ var d;
106
+ if (!$ || typeof b > "u" || ((d = b.env) == null ? void 0 : d.NODE_ENV) === "production") return;
107
+ const m = setTimeout(() => console.warn(se(a)), V);
108
108
  return () => clearTimeout(m);
109
- }, [$, o]);
110
- const G = E(
109
+ }, [$, a]);
110
+ const K = S(
111
111
  () => ({
112
112
  session: g,
113
- design: a,
114
- placement: o,
113
+ design: o,
114
+ placement: a,
115
115
  product: s,
116
- mockups: N,
116
+ mockups: w,
117
117
  isRendering: $,
118
118
  error: j,
119
- throttleMs: i,
120
- pushRender: U
119
+ throttleMs: l,
120
+ pushRender: N
121
121
  }),
122
- [g, a, o, s, N, $, j, i, U]
123
- ), F = /* @__PURE__ */ b(V.Provider, { value: G, children: v });
124
- return h ? /* @__PURE__ */ b(K, { services: h, children: F }) : F;
122
+ [g, o, a, s, w, $, j, l, N]
123
+ ), A = /* @__PURE__ */ E(L.Provider, { value: K, children: u });
124
+ return h ? /* @__PURE__ */ E(W, { services: h, children: A }) : A;
125
125
  }
126
- function ue({
126
+ function he({
127
127
  children: e,
128
128
  kit: n = "embed-only",
129
129
  inheritTheme: s = !0,
130
- maxHeight: a,
131
- services: i,
130
+ maxHeight: o,
131
+ services: l,
132
132
  className: t,
133
- style: d
133
+ style: f
134
134
  }) {
135
- const { design: c, placement: h, throttleMs: v, pushRender: o } = B(), r = E(() => c.toJSON(), [c]);
136
- return /* @__PURE__ */ b("div", { className: `app-modern${t ? ` ${t}` : ""}`, style: d, children: /* @__PURE__ */ b(
137
- X,
135
+ const { design: c, placement: h, throttleMs: u, pushRender: a } = _(), r = S(() => c.toJSON(), [c]);
136
+ return /* @__PURE__ */ E("div", { className: `app-modern${t ? ` ${t}` : ""}`, style: f, children: /* @__PURE__ */ E(
137
+ Z,
138
138
  {
139
139
  kit: n,
140
140
  inheritTheme: s,
141
- services: i,
141
+ services: l,
142
142
  artboards: r.artboards,
143
143
  activeArtboard: h,
144
144
  initialElements: r.elements,
145
- layoutConfig: a ? { maxHeight: a } : void 0,
145
+ layoutConfig: o ? { maxHeight: o } : void 0,
146
146
  overlay: e,
147
- onChange: (l) => o(Z(l), v)
147
+ onChange: (i) => a(ee(i), u)
148
148
  }
149
149
  ) });
150
150
  }
151
- function de({
151
+ const ae = 1.2, O = [1200, 1400, 1600, 1800, 2e3, 2500, 3e3, 4e3];
152
+ function oe(e) {
153
+ for (const n of O)
154
+ if (n >= e) return n;
155
+ return O[O.length - 1];
156
+ }
157
+ function ie(e, n) {
158
+ if (!(e > 0) || !(n > 0) || n <= e * ae) return null;
159
+ const s = oe(n);
160
+ return `[RealtimeMockup] mockup rendered at ${e}px but displayed at ~${n} device px — the preview will look soft. Pass \`product.width\` (e.g. ${s}) on <RealtimeEmbed> for a crisp preview.`;
161
+ }
162
+ function pe({
152
163
  mockupId: e,
153
164
  fallback: n = null,
154
165
  alt: s = "Live product mockup",
155
- className: a,
156
- style: i
166
+ className: o,
167
+ style: l
157
168
  }) {
158
- const { mockups: t } = B(), d = e ? t.find((c) => c.mockupId === e && c.imageUrl) : t.find((c) => c.imageUrl);
159
- return d != null && d.imageUrl ? (
169
+ const { mockups: t } = _(), f = x(/* @__PURE__ */ new Set()), c = e ? t.find((u) => u.mockupId === e && u.imageUrl) : t.find((u) => u.imageUrl);
170
+ if (!(c != null && c.imageUrl))
171
+ return /* @__PURE__ */ E(z, { children: n });
172
+ const h = (u) => {
173
+ var k;
174
+ if (typeof b > "u" || ((k = b.env) == null ? void 0 : k.NODE_ENV) === "production" || typeof window > "u" || f.current.has(c.mockupId)) return;
175
+ const a = u.currentTarget, r = a.getBoundingClientRect().width;
176
+ if (r <= 0) return;
177
+ const i = Math.round(r * (window.devicePixelRatio || 1)), w = ie(a.naturalWidth, i);
178
+ w && (f.current.add(c.mockupId), console.warn(w));
179
+ };
180
+ return (
160
181
  // eslint-disable-next-line @next/next/no-img-element
161
- /* @__PURE__ */ b("img", { src: d.imageUrl, alt: s, className: a, style: i })
162
- ) : /* @__PURE__ */ b(J, { children: n });
182
+ /* @__PURE__ */ E("img", { src: c.imageUrl, alt: s, className: o, style: l, onLoad: h })
183
+ );
163
184
  }
164
- function ge() {
165
- const { mockups: e, isRendering: n, error: s } = B();
185
+ function ve() {
186
+ const { mockups: e, isRendering: n, error: s } = _();
166
187
  return { mockups: e, isRendering: n, error: s };
167
188
  }
168
- z();
189
+ H();
169
190
  export {
170
- K as CanvasServicesProvider,
171
- Ee as DesignBuilder,
172
- ue as RealtimeCanvas,
173
- me as RealtimeEmbed,
174
- de as RealtimeMockup,
175
- $e as TEMPLATE_FIELDS_KEY,
176
- pe as createProxyGenerate,
177
- ve as createProxyImageSearch,
178
- Se as design,
179
- ke as readTemplateFields,
180
- be as useCanvasServices,
181
- Re as useColorBinding,
182
- Te as useElementByName,
183
- we as useImageBinding,
184
- ge as useRealtimeMockups,
185
- xe as useTextBinding,
186
- ne as validateEmbed,
187
- Ie as withTemplateFields
191
+ W as CanvasServicesProvider,
192
+ $e as DesignBuilder,
193
+ he as RealtimeCanvas,
194
+ ge as RealtimeEmbed,
195
+ pe as RealtimeMockup,
196
+ Re as TEMPLATE_FIELDS_KEY,
197
+ we as createProxyGenerate,
198
+ ye as createProxyImageSearch,
199
+ Te as design,
200
+ Ie as readTemplateFields,
201
+ Se as useCanvasServices,
202
+ xe as useColorBinding,
203
+ Me as useElementByName,
204
+ Ue as useImageBinding,
205
+ ve as useRealtimeMockups,
206
+ Ce as useTextBinding,
207
+ te as validateEmbed,
208
+ Ne as withTemplateFields
188
209
  };
189
210
  //# sourceMappingURL=embed.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"embed.mjs","sources":["../src/components/embed-realtime/context.ts","../src/components/embed-realtime/validateEmbed.ts","../src/components/embed-realtime/RealtimeEmbed.tsx","../src/components/embed-realtime/RealtimeCanvas.tsx","../src/components/embed-realtime/RealtimeMockup.tsx","../src/components/embed-realtime/useRealtimeMockups.ts","../src/api/embed.ts"],"sourcesContent":["'use client';\n\n/**\n * Shared context for the realtime embed primitives. `<RealtimeEmbed>` owns the\n * single {@link RenderSession} and the live mockup state; `<RealtimeCanvas>` and\n * `<RealtimeMockup>` read it from here so the editor and the preview can be laid\n * out freely yet can never desync (they share one session, one design, one\n * placement).\n */\n\nimport { createContext, useContext } from 'react';\nimport type { RenderSession, RenderProduct, RenderState, MockupResult } from '@snowcone-app/sdk';\nimport type { DesignBuilder } from '../../design/design-builder.js';\n\nexport interface EmbedContextValue {\n /** The single session the editor pushes renders to. `null` if the session\n * failed to construct (a config fail-fast, surfaced via `error`). Escape\n * hatch for advanced use. */\n session: RenderSession | null;\n /** The design the embed was seeded with — single source of truth for both the\n * canvas seed and the seed render. */\n design: DesignBuilder;\n /** The placement name (= the design's artboard name). Both the canvas artboard\n * and every `renderState` call use this; derived once so it can't drift. */\n placement: string;\n /** The product being rendered against. */\n product: RenderProduct;\n /** Latest mockups from the session. Reactive — updates re-render consumers. */\n mockups: MockupResult[];\n /** True between a render dispatch and the next mockup / error. */\n isRendering: boolean;\n /** Last render error message, or null. */\n error: string | null;\n /** Default edit→render throttle (ms). */\n throttleMs: number;\n /** Dispatch a server render for the embed's placement. Sets `isRendering`. */\n pushRender: (state: RenderState, throttleMs?: number) => void;\n}\n\nexport const EmbedContext = createContext<EmbedContextValue | undefined>(undefined);\n\n/**\n * Read the embed context. Throws a clear error when called outside\n * `<RealtimeEmbed>` — the most common wiring mistake.\n */\nexport function useEmbedContext(): EmbedContextValue {\n const ctx = useContext(EmbedContext);\n if (!ctx) {\n throw new Error(\n 'useRealtimeMockups / <RealtimeCanvas> / <RealtimeMockup> must be rendered ' +\n 'inside <RealtimeEmbed>. Wrap them: ' +\n '<RealtimeEmbed shop=… product=… design=… grantUrl=…>…</RealtimeEmbed>'\n );\n }\n return ctx;\n}\n","'use client';\n\n/**\n * Dev-only assertions for the realtime embed. Catches the silent-break wiring\n * mistakes that otherwise produce a blank canvas or a mockup that never renders:\n *\n * - no grant source (the session can't authorize → silent hang)\n * - a variant product with no `variantId` (→ ~30s render hang), AND the\n * can't-even-tell case: neither `variantId` nor `options` passed\n * - empty `mockupIds` (nothing to render)\n * - the design's placement isn't one of the product's placements (renderState\n * targets an artboard the product doesn't have → silent blank mockup)\n * - a multi-placement product (the embed renders ONE placement, so the server\n * waits on the rest and the render times out ~15s in — needs the eject path)\n * - duplicate layer names (binding hooks resolve to the wrong element)\n *\n * Advisory by design: it logs `console.warn` / `console.error` and never throws,\n * so a partly-wired embed still mounts and renders what it can. (The hard\n * fail-fast for a missing required `variantId` lives in the session itself.)\n * Tree-shaken out of production builds via the `NODE_ENV` guard at the call site.\n */\n\nimport { assertVariantSelected, type RenderProduct } from '@snowcone-app/sdk';\nimport type { DesignBuilder } from '../../design/design-builder.js';\n\nexport interface ValidateEmbedInput {\n shop: string;\n product: RenderProduct;\n design: DesignBuilder;\n hasGrant: boolean;\n}\n\nconst TAG = '[RealtimeEmbed]';\n\n/**\n * The placement labels this product demands a canvas_state for. Prefers the\n * catalog's `requiredPlacements` contract; variant-auto-filled (color)\n * placements don't count — `variantId` satisfies them (the missing-variantId\n * case has its own guard above). Falls back to the bare `placements` label\n * list ONLY when the product has no variant in play: a label list can't say\n * which entries are auto-filled colors, but without a variant none are.\n */\nfunction requiredPlacementLabels(product: RenderProduct): string[] {\n if (product?.requiredPlacements?.length) {\n return product.requiredPlacements.filter((p) => !p.autoFilledByVariant).map((p) => p.label);\n }\n const variantIds = new Set(\n (product?.options?.combinations ?? [])\n .map((c) => c.variantId)\n .filter((id): id is string => typeof id === 'string' && id !== '')\n );\n const hasVariantAxis = Boolean(product?.variantId) || variantIds.size > 1;\n return hasVariantAxis ? [] : (product?.placements ?? []);\n}\n\nexport function validateEmbed({ shop, product, design, hasGrant }: ValidateEmbedInput): void {\n if (!shop) {\n console.error(`${TAG} \\`shop\\` is required (your publishable shop id).`);\n }\n\n if (!hasGrant) {\n console.error(\n `${TAG} no grant source — pass \\`grantUrl\\` (a proxy to mintRealtimeGrant) ` +\n 'or `grant`. Without it the session can never authorize and no mockup renders.'\n );\n }\n\n if (!product?.mockupIds?.length) {\n console.error(\n `${TAG} product.mockupIds is empty — pass the catalog scene codes ` +\n '(product.mockups[].id) you want rendered, e.g. [\"FV1qjO\"].'\n );\n }\n\n // Variant axis: a product with a color/size option REQUIRES a variantId or the\n // render hangs ~30s waiting for a color blob that never comes. When `options`\n // (the catalog combinations) is passed we can detect the axis and fail fast\n // before the session's constructor throws. When `options` is NOT passed we\n // can't see the axis — so we DON'T warn here (a no-`options` product might\n // genuinely have no variants; warning would false-fire on every valid\n // single-variant embed). That stall is instead caught at runtime by\n // `<RealtimeEmbed>`'s slow-render backstop (`slowRenderHint`), which fires only\n // when a render actually hangs — no false positives, and it covers every\n // silent-hang cause, not just this one.\n if (product?.options) {\n try {\n assertVariantSelected(product);\n } catch (err) {\n console.error(`${TAG} ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n const json = design.toJSON();\n const placement = json.activeArtboard;\n\n // <RealtimeEmbed> renders exactly ONE placement — the design's artboard. A\n // product that requires several can never complete through it: the canvas\n // mounts perfectly, then the server waits on the missing canvas_states until\n // the ~15s \"Timed out waiting for all placements\" error. Fully knowable at\n // mount from the catalog data already on the product, so say so now.\n const required = requiredPlacementLabels(product);\n if (required.length > 1) {\n console.error(\n `${TAG} product ${product.productId} requires placements ` +\n `[${required.join(', ')}] but <RealtimeEmbed> renders a single ` +\n `placement (\"${placement}\"). The render will time out waiting for the ` +\n 'others. This product needs the eject path — see ' +\n 'https://developers.snowcone.app/embed-mode#eject.'\n );\n }\n\n if (product?.placements?.length && !product.placements.includes(placement)) {\n console.warn(\n `${TAG} the design's placement \"${placement}\" is not in ` +\n `product.placements [${product.placements.join(', ')}]. The canvas ` +\n 'artboard name and renderState placement must equal a real product ' +\n 'placement label, or the server renders a blank mockup. Fix the ' +\n '`name` passed to design({ name }).'\n );\n }\n\n const names = json.elements.map((el) => (el as { name?: string }).name).filter((n): n is string => Boolean(n));\n const dupes = names.filter((n, i) => names.indexOf(n) !== i);\n if (dupes.length) {\n console.warn(\n `${TAG} duplicate layer name(s) [${[...new Set(dupes)].join(', ')}] — ` +\n 'useTextBinding(name) / useImageBinding(name) bind to the FIRST match, ' +\n 'so a duplicate name makes a control edit the wrong layer. Keep names unique.'\n );\n }\n}\n","'use client';\n\n/**\n * `<RealtimeEmbed>` — the one place the required pieces live.\n *\n * It owns a single {@link RenderSession}, seeds the first render from the\n * `design`, tracks the live mockups, and hands all of it to `<RealtimeCanvas>`\n * and `<RealtimeMockup>` through context. Every prop that an embed needs to work\n * is REQUIRED here, so a coding agent can't ship a half-wired embed:\n *\n * @example\n * ```tsx\n * const d = design({ name: 'Front', width: 1480, height: 2328 })\n * .background(artUrl, { aspectRatio: 1480 / 2328, name: 'art' })\n * .text('YOUR NAME', { anchor: 'top', name: 'headline' });\n *\n * <RealtimeEmbed\n * shop=\"YOUR_SHOP_ID\"\n * grantUrl=\"/api/realtime/grant\"\n * // variantId is REQUIRED for a product with a color/variant axis (BEEB77 has\n * // a Frame color) — omit it and the render hangs. Pass `options` too so a\n * // wrong/missing variantId fails fast instead of timing out. Read both from\n * // getProduct(productId) (its options.combinations[].variantId).\n * product={{ productId: 'BEEB77', mockupIds: ['FV1qjO'], variantId: 'Pv1sLC', placements: ['Front'] }}\n * design={d}\n * >\n * <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>\n * <RealtimeCanvas /> {/* editor *\\/}\n * <RealtimeMockup /> {/* live preview *\\/}\n * </div>\n * </RealtimeEmbed>\n * ```\n */\n\nimport React, { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport {\n RenderSession,\n type RenderProduct,\n type RenderState,\n type MockupResult,\n type RealtimeGrant,\n} from '@snowcone-app/sdk';\nimport type { DesignBuilder } from '../../design/design-builder.js';\nimport { CanvasServicesProvider, type CanvasServices } from '../../contexts/CanvasServicesContext.js';\nimport { EmbedContext, type EmbedContextValue } from './context.js';\nimport { validateEmbed } from './validateEmbed.js';\n\n/**\n * How the session authorizes. Provide exactly one — the discriminated union makes\n * \"I forgot the grant\" a compile error rather than a silent runtime hang.\n *\n * Member order is load-bearing: when NEITHER prop is supplied (the common\n * mistake) TS reports the missing property of the LAST union member, so the\n * `grantUrl` arm is kept last on purpose — the error then reads \"Property\n * 'grantUrl' is missing\", pointing at the recommended prop instead of `grant`.\n * Don't reorder these.\n */\nexport type GrantSource =\n | {\n /** Full control over fetching the grant: a token or a token factory. */\n grant: RealtimeGrant | (() => Promise<RealtimeGrant>);\n grantUrl?: never;\n }\n | {\n /** Your endpoint returning `{ token, expiresAt }` for `POST { shop }` —\n * a same-origin proxy to `mintRealtimeGrant`. Recommended. */\n grantUrl: string;\n grant?: never;\n };\n\nexport type RealtimeEmbedProps = GrantSource & {\n /** Publishable shop id (= shop.id, like a Stripe pk_). Billing target. */\n shop: string;\n /** Product to render against (catalog ids). `mockupIds` are scene codes\n * (product.mockups[].id), NOT placement names. Pass `placements`, `options`\n * + `requiredPlacements` so wiring mistakes (including a multi-placement\n * product, which the embed can't render) fail loud at mount instead of\n * hanging. */\n product: RenderProduct;\n /** Single source of truth — seeds the canvas AND the first render. Build it\n * with `design({ name, width, height })…`. Its artboard `name` is the\n * placement every render targets. */\n design: DesignBuilder;\n /** Edit→render throttle in ms (default 200). */\n throttleMs?: number;\n /** Called whenever new mockups arrive. */\n onMockups?: (results: MockupResult[]) => void;\n /** Called on a render/timeout error. */\n onRenderError?: (message: string) => void;\n /** Override the realtime WS endpoint (defaults to the SDK's prod URL). */\n wsUrl?: string;\n /**\n * Privileged-service wiring for the editor chrome (the Image Browser's\n * stock-image search and AI generation tabs). Same pattern as `grantUrl`:\n * host these URLs on your server; your vendor keys live there. Flows to\n * every `<RealtimeCanvas>` inside this embed. Tabs without a configured\n * service do not render.\n *\n * @example\n * ```tsx\n * <RealtimeEmbed\n * services={{ imageSearchUrl: '/api/image-search', generateUrl: '/api/generate-image' }}\n * …\n * />\n * ```\n */\n services?: CanvasServices;\n children?: ReactNode;\n};\n\nfunction toGetToken(props: RealtimeEmbedProps): { grantUrl: string } | { getToken: () => Promise<RealtimeGrant> } {\n if ('grantUrl' in props && props.grantUrl) {\n return { grantUrl: props.grantUrl };\n }\n const grant = (props as { grant: RealtimeGrant | (() => Promise<RealtimeGrant>) }).grant;\n if (typeof grant === 'function') return { getToken: grant };\n return { getToken: () => Promise.resolve(grant) };\n}\n\n/**\n * How long a render may be in flight before the embed assumes something is wrong\n * and surfaces a diagnostic. A healthy render returns in ~2s; the session's own\n * watchdog is ~30s. 8s sits in between — long enough not to false-fire on a\n * genuinely slow render, short enough to spare a first-timer the full 30s of\n * silence.\n */\nexport const SLOW_RENDER_HINT_MS = 8000;\n\n/**\n * The dev-console message shown when a render hasn't produced a mockup in\n * {@link SLOW_RENDER_HINT_MS}. A general backstop for the whole silent-hang\n * class — not just the missing-`variantId` trap — so a stuck embed always says\n * *something* actionable instead of hanging quietly.\n */\nexport function slowRenderHint(placement: string): string {\n return (\n `[RealtimeEmbed] No mockup after ${Math.round(SLOW_RENDER_HINT_MS / 1000)}s. ` +\n `Common causes: a variant product missing \\`variantId\\` (pass \\`product.options\\` ` +\n `so the embed can flag it up front), a multi-placement product (the server waits ` +\n `for every required placement but <RealtimeEmbed> sends only \"${placement}\" — pass ` +\n `\\`product.requiredPlacements\\` so this is flagged at mount; such products need ` +\n `the eject path), an unreachable or non-public asset URL in the design, or the ` +\n `placement \"${placement}\" not existing on the product. ` +\n `The render may still arrive — this is a heads-up, not a failure.`\n );\n}\n\nexport function RealtimeEmbed(props: RealtimeEmbedProps): React.ReactElement {\n const { shop, product, design, throttleMs = 200, onMockups, onRenderError, wsUrl, services, children } = props;\n\n const placement = useMemo(() => design.toJSON().activeArtboard, [design]);\n\n // Keep the latest callbacks reachable without re-creating the session.\n const onMockupsRef = useRef(onMockups);\n const onRenderErrorRef = useRef(onRenderError);\n onMockupsRef.current = onMockups;\n onRenderErrorRef.current = onRenderError;\n\n const [mockups, setMockups] = useState<MockupResult[]>([]);\n const [isRendering, setIsRendering] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n const hasGrant = Boolean(('grantUrl' in props && props.grantUrl) || ('grant' in props && props.grant));\n\n // One session per (shop, product, placement, auth). Key it on the product's\n // identity-BEARING fields, not the object reference — so an embedder passing\n // an inline `product={{…}}` (the natural way to write it) doesn't rebuild the\n // session and reconnect on every render. A real change (different product,\n // mockups, or variant) still re-keys it. `options`/`placements` are advisory\n // (fail-fast validation) so they don't need to force a rebuild.\n const auth = toGetToken(props);\n const authKey = 'grantUrl' in auth ? auth.grantUrl : 'getToken';\n const productKey = `${product.productId}|${product.mockupIds.join(',')}|${product.variantId ?? ''}`;\n // RenderSession's constructor fail-fasts (e.g. a variant product with no\n // variantId) by THROWING. Catch it here so a config mistake surfaces through\n // `onRenderError` + the error state instead of throwing during render and\n // white-screening the whole host page.\n const sessionResult = useMemo<{ session: RenderSession } | { error: string }>(\n () => {\n try {\n return { session: new RenderSession({ shop, product, wsUrl, ...auth }) };\n } catch (err) {\n return { error: err instanceof Error ? err.message : String(err) };\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [shop, productKey, placement, wsUrl, authKey]\n );\n const session = 'session' in sessionResult ? sessionResult.session : null;\n const constructError = 'error' in sessionResult ? sessionResult.error : null;\n\n // Dev-only wiring guard (tree-shaken from production builds). Run it in an\n // effect keyed on the meaningful inputs so warnings fire once per change, not\n // on every render (validateEmbed used to flood the console). The ref-based\n // dedup survives StrictMode's deliberate unmount/remount cycle, which would\n // otherwise print every diagnostic 2-3×. The `process` reference is\n // typeof-guarded on the same line — non-Node bundlers don't define it.\n const validatedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n const key = `${shop}|${productKey}|${placement}|${hasGrant}`;\n if (validatedKeyRef.current === key) return;\n validatedKeyRef.current = key;\n validateEmbed({ shop, product, design, hasGrant });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [shop, productKey, placement, hasGrant]);\n\n const pushRender = useMemo(() => {\n return (state: RenderState, throttle = throttleMs) => {\n if (!session) return;\n setError(null);\n setIsRendering(true);\n void session.renderState(placement, state, throttle).catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n setError(message);\n setIsRendering(false);\n onRenderErrorRef.current?.(message);\n });\n };\n }, [session, placement, throttleMs]);\n\n useEffect(() => {\n // Construction fail-fast (caught above) — surface it instead of rendering.\n if (constructError) {\n setError(constructError);\n setIsRendering(false);\n onRenderErrorRef.current?.(constructError);\n return;\n }\n if (!session) return;\n\n session.onMockups((results) => {\n setMockups(results);\n setIsRendering(false);\n onMockupsRef.current?.(results);\n });\n session.onError((message) => {\n setError(message);\n setIsRendering(false);\n onRenderErrorRef.current?.(message);\n });\n\n // Seed the first mockup from the design before any edit, so a freshly\n // mounted embed shows a real mockup immediately (no duplicate seed state —\n // the design is the single source).\n pushRender(design.toServerRequest());\n\n return () => session.close();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [session, constructError]);\n\n // Runtime silent-hang backstop. While a render is in flight, arm a timer; if no\n // mockup (or error) lands within SLOW_RENDER_HINT_MS, log an actionable hint.\n // Unlike the mount-time wiring guard this fires ONLY on an actual stall, so it\n // never nags a working embed — and it covers every silent-hang cause, not just\n // missing variantId. Dev-only (the `process` ref is typeof-guarded on the same\n // line); cleared the moment `isRendering` flips false.\n useEffect(() => {\n if (!isRendering) return;\n if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production') return;\n const timer = setTimeout(() => console.warn(slowRenderHint(placement)), SLOW_RENDER_HINT_MS);\n return () => clearTimeout(timer);\n }, [isRendering, placement]);\n\n const value = useMemo<EmbedContextValue>(\n () => ({\n session,\n design,\n placement,\n product,\n mockups,\n isRendering,\n error,\n throttleMs,\n pushRender,\n }),\n [session, design, placement, product, mockups, isRendering, error, throttleMs, pushRender]\n );\n\n const body = <EmbedContext.Provider value={value}>{children}</EmbedContext.Provider>;\n\n // Only mount a services provider when configured — `<RealtimeCanvas>`'s\n // inner SnowconeCanvas inherits it through context.\n if (services) {\n return <CanvasServicesProvider services={services}>{body}</CanvasServicesProvider>;\n }\n return body;\n}\n","'use client';\n\n/**\n * `<RealtimeCanvas>` — the interactive editor half of a realtime embed.\n *\n * Mounts a `<SnowconeCanvas>` seeded from the embed's `design`, and forwards\n * every edit to the shared {@link RenderSession} as a server render (the\n * recommended ship-state path — no pixel upload). All the load-bearing wiring an\n * embedder used to copy by hand is internal here: the `app-modern` class,\n * `inheritTheme`, edit→`serializeStateForServer`→`renderState`, and the\n * artboard/placement match.\n *\n * Defaults to the chrome-less `kit=\"embed-only\"` — bring your own controls as\n * `children` (they render as an overlay INSIDE the canvas's editor context, so\n * `useTextBinding(name)` / `useImageBinding(name)` resolve against the live\n * elements). For a full in-canvas editor (select layer, font size, add element)\n * pass `kit=\"compact-customizer\"` or `kit=\"pro-studio\"` — renders still flow\n * automatically. See the {@link RealtimeCanvasProps.kit} docs.\n *\n * Must be rendered inside `<RealtimeEmbed>`.\n */\n\nimport React, { useMemo, type CSSProperties, type ReactNode } from 'react';\nimport { SnowconeCanvas } 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 './context.js';\n\nexport interface RealtimeCanvasProps {\n /** Your controls (toolbar, inputs). Rendered as an overlay inside the editor\n * context so binding hooks work. */\n children?: ReactNode;\n /**\n * Editor chrome to mount. The default `'embed-only'` is a chrome-less canvas —\n * the host supplies every control as `children` (text inputs, image swap)\n * through `useTextBinding` / `useImageBinding`. That's the right call for a\n * fill-in-the-blanks personalizer, but it gives the shopper NO way to select a\n * layer and change its font size, add an element, or reorder layers.\n *\n * To ship a real editor through the turnkey realtime path, pass a richer\n * preset — `'compact-customizer'` (a trimmed toolbar) or `'pro-studio'` (full\n * panels). Every edit still flows to the shared {@link RenderSession} as a\n * server render automatically; you get add-element / font-size / layers for\n * free without ejecting to a raw `SnowconeCanvas` + `RenderSession`.\n *\n * Pair a chrome'd kit with `children` only when your overlay adds to — not\n * duplicates — the kit's controls, or you'll get double chrome.\n *\n * @default 'embed-only'\n */\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 /**\n * Privileged-service wiring for the editor chrome (Image Browser search /\n * AI tabs). Usually set once on `<RealtimeEmbed services={…}>` and inherited;\n * pass it here only to override for this canvas. Host the URLs on your\n * server — your vendor keys live there.\n */\n services?: CanvasServices;\n className?: string;\n style?: CSSProperties;\n}\n\nexport function RealtimeCanvas({\n children,\n kit = 'embed-only',\n inheritTheme = true,\n maxHeight,\n services,\n className,\n style,\n}: RealtimeCanvasProps): React.ReactElement {\n const { design, placement, throttleMs, pushRender } = useEmbedContext();\n\n // The design is the single source for the canvas seed — same object that\n // seeded the first render, so the editor and the preview start identical.\n const seed = useMemo(() => design.toJSON(), [design]);\n\n return (\n <div className={`app-modern${className ? ` ${className}` : ''}`} style={style}>\n <SnowconeCanvas\n kit={kit}\n inheritTheme={inheritTheme}\n services={services}\n artboards={seed.artboards}\n activeArtboard={placement}\n initialElements={seed.elements}\n layoutConfig={maxHeight ? { maxHeight } : undefined}\n overlay={children}\n onChange={(state) => pushRender(serializeStateForServer(state), throttleMs)}\n />\n </div>\n );\n}\n","'use client';\n\n/**\n * `<RealtimeMockup>` — the live preview half of a realtime embed.\n *\n * Reads the shared session's mockups from context and renders the rendered\n * image, updating as the shopper edits. Because it shares one session with\n * `<RealtimeCanvas>`, the preview can never desync from the editor and needs no\n * wiring of its own. Place it anywhere inside `<RealtimeEmbed>`.\n */\n\nimport React, { type CSSProperties, type ReactNode } from 'react';\nimport { useEmbedContext } from './context.js';\n\nexport interface RealtimeMockupProps {\n /** Which mockup scene to show (a `product.mockupIds` entry). Defaults to the\n * first mockup that has rendered. */\n mockupId?: string;\n /** Shown before the first mockup arrives / while a render is in flight with no\n * prior image. Defaults to nothing. */\n fallback?: ReactNode;\n /** Alt text for the rendered image. */\n alt?: string;\n className?: string;\n style?: CSSProperties;\n}\n\nexport function RealtimeMockup({\n mockupId,\n fallback = null,\n alt = 'Live product mockup',\n className,\n style,\n}: RealtimeMockupProps): React.ReactElement {\n const { mockups } = useEmbedContext();\n\n const result = mockupId\n ? mockups.find((m) => m.mockupId === mockupId && m.imageUrl)\n : mockups.find((m) => m.imageUrl);\n\n if (!result?.imageUrl) {\n return <>{fallback}</>;\n }\n\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={result.imageUrl} alt={alt} className={className} style={style} />\n );\n}\n","'use client';\n\nimport type { MockupResult } from '@snowcone-app/sdk';\nimport { useEmbedContext } from './context.js';\n\nexport interface UseRealtimeMockupsResult {\n /** All mockups from the latest render (one per requested mockupId). */\n mockups: MockupResult[];\n /** True between a render dispatch and the next mockup / error. */\n isRendering: boolean;\n /** Last render/timeout error message, or null. */\n error: string | null;\n}\n\n/**\n * Read the live mockups for a custom preview UI. Reactive: re-renders when new\n * mockups arrive. Must be called inside `<RealtimeEmbed>`.\n *\n * For the common case (just show the rendered image) use `<RealtimeMockup>`\n * instead — this hook is for building your own gallery / loading treatment.\n */\nexport function useRealtimeMockups(): UseRealtimeMockupsResult {\n const { mockups, isRendering, error } = useEmbedContext();\n return { mockups, isRendering, error };\n}\n","/**\n * @snowcone-app/canvas/embed — Realtime embed primitives (Tier 2)\n *\n * The turnkey way to drop a live, server-rendered product editor into your app.\n * Three components that share one session so the editor and the preview can't\n * desync, plus the `design()` builder that seeds both:\n *\n * ```tsx\n * import { RealtimeEmbed, RealtimeCanvas, RealtimeMockup, design }\n * from '@snowcone-app/canvas/embed';\n * import '@snowcone-app/canvas/style.css';\n *\n * const d = design({ name: 'Front', width: 1480, height: 2328 })\n * .background(artUrl, { aspectRatio: 1480 / 2328, name: 'art' })\n * .text('YOUR NAME', { anchor: 'top', name: 'headline' });\n *\n * <RealtimeEmbed\n * shop=\"YOUR_SHOP_ID\"\n * grantUrl=\"/api/realtime/grant\"\n * // variantId is REQUIRED when the product has a color/variant axis (BEEB77\n * // does) — omit it and the render hangs. Read it + options from getProduct().\n * product={{ productId: 'BEEB77', mockupIds: ['FV1qjO'], variantId: 'Pv1sLC', placements: ['Front'] }}\n * design={d}\n * >\n * <RealtimeCanvas />\n * <RealtimeMockup />\n * </RealtimeEmbed>\n * ```\n *\n * Everything an embed needs is REQUIRED on `<RealtimeEmbed>` — there's no\n * silently-optional prop to forget. For full manual control (your own session,\n * custom layout, multi-placement, color variants) drop to `<SnowconeCanvas>` +\n * `RenderSession` directly; see the docs' \"eject\" section.\n *\n * @packageDocumentation\n */\n\n// Register icons on module load (the editor + overlay controls use them).\nimport { ensureIconsRegistered } from '../icons/registry.js';\nensureIconsRegistered();\n\n// === Embed primitives ===\nexport { RealtimeEmbed } from '../components/embed-realtime/RealtimeEmbed.js';\nexport type { RealtimeEmbedProps, GrantSource } from '../components/embed-realtime/RealtimeEmbed.js';\n\nexport { RealtimeCanvas } from '../components/embed-realtime/RealtimeCanvas.js';\nexport type { RealtimeCanvasProps } from '../components/embed-realtime/RealtimeCanvas.js';\n\nexport { RealtimeMockup } from '../components/embed-realtime/RealtimeMockup.js';\nexport type { RealtimeMockupProps } from '../components/embed-realtime/RealtimeMockup.js';\n\nexport { useRealtimeMockups } from '../components/embed-realtime/useRealtimeMockups.js';\nexport type { UseRealtimeMockupsResult } from '../components/embed-realtime/useRealtimeMockups.js';\n\nexport { validateEmbed } from '../components/embed-realtime/validateEmbed.js';\nexport type { ValidateEmbedInput } from '../components/embed-realtime/validateEmbed.js';\n\n// === Design builder — the single source of truth for an embed ===\nexport { design, DesignBuilder } from '../design/design-builder.js';\nexport type { Anchor, ShapeKind, DesignArtboard } from '../design/design-builder.js';\n\n// === Binding hooks (build your own controls inside <RealtimeCanvas>) ===\nexport { useTextBinding } from '../hooks/useTextBinding.js';\nexport { useImageBinding } from '../hooks/useImageBinding.js';\nexport { useColorBinding } from '../hooks/useColorBinding.js';\nexport type { ColorBindingResult } from '../hooks/useColorBinding.js';\nexport { useElementByName } from '../hooks/useElementByName.js';\n\n// === Editable templates (saved state + buyer-editable field manifest) ===\nexport {\n readTemplateFields,\n withTemplateFields,\n TEMPLATE_FIELDS_KEY,\n} from '../design/template-fields.js';\nexport type {\n TemplateField,\n TemplateTextField,\n TemplateImageField,\n TemplateColorField,\n} from '../design/template-fields.js';\n\n// === Privileged services (Image Browser search / AI generation) ===\n// Same pattern as `grantUrl`: host the proxy routes on your server, your\n// vendor keys live there. Wire via `services` on <RealtimeEmbed>.\nexport { CanvasServicesProvider, useCanvasServices } from '../contexts/CanvasServicesContext.js';\nexport type { CanvasServices, ResolvedCanvasServices } from '../contexts/CanvasServicesContext.js';\nexport { createProxyImageSearch, createProxyGenerate } from '../services/imageSearch.js';\nexport type {\n ImageSearchProvider,\n ImageSearchResult,\n ImageSearchResponse,\n ImageSearchParams,\n GenerateProvider,\n GenerateParams,\n GenerateResponse,\n} from '../services/imageSearch.js';\n\n// === Re-exported SDK types embedders touch ===\nexport type {\n RenderProduct,\n RenderProductOptions,\n RenderProductCombination,\n RenderProductRequiredPlacement,\n MockupResult,\n RealtimeGrant,\n} from '@snowcone-app/sdk';\n"],"names":["EmbedContext","createContext","useEmbedContext","ctx","useContext","TAG","requiredPlacementLabels","product","_a","p","variantIds","_b","c","id","validateEmbed","shop","design","hasGrant","assertVariantSelected","err","json","placement","required","names","el","n","dupes","i","toGetToken","props","grant","SLOW_RENDER_HINT_MS","slowRenderHint","RealtimeEmbed","throttleMs","onMockups","onRenderError","wsUrl","services","children","useMemo","onMockupsRef","useRef","onRenderErrorRef","mockups","setMockups","useState","isRendering","setIsRendering","error","setError","auth","authKey","productKey","sessionResult","RenderSession","session","constructError","validatedKeyRef","useEffect","process","key","pushRender","state","throttle","message","results","timer","value","body","jsx","CanvasServicesProvider","RealtimeCanvas","kit","inheritTheme","maxHeight","className","style","seed","SnowconeCanvas","serializeStateForServer","RealtimeMockup","mockupId","fallback","alt","result","m","useRealtimeMockups","ensureIconsRegistered"],"mappings":";;;;;;;;AAuCO,MAAMA,IAAeC,EAA6C,MAAS;AAM3E,SAASC,IAAqC;AACnD,QAAMC,IAAMC,EAAWJ,CAAY;AACnC,MAAI,CAACG;AACH,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAKJ,SAAOA;AACT;ACvBA,MAAME,IAAM;AAUZ,SAASC,GAAwBC,GAAkC;;AACjE,OAAIC,IAAAD,KAAA,gBAAAA,EAAS,uBAAT,QAAAC,EAA6B;AAC/B,WAAOD,EAAQ,mBAAmB,OAAO,CAACE,MAAM,CAACA,EAAE,mBAAmB,EAAE,IAAI,CAACA,MAAMA,EAAE,KAAK;AAE5F,QAAMC,IAAa,IAAI;AAAA,OACpBC,IAAAJ,KAAA,gBAAAA,EAAS,YAAT,gBAAAI,EAAkB,iBAAgB,CAAA,GAChC,IAAI,CAACC,MAAMA,EAAE,SAAS,EACtB,OAAO,CAACC,MAAqB,OAAOA,KAAO,YAAYA,MAAO,EAAE;AAAA,EAAA;AAGrE,SADuB,GAAQN,KAAA,QAAAA,EAAS,cAAcG,EAAW,OAAO,IAChD,CAAA,KAAMH,KAAA,gBAAAA,EAAS,eAAc,CAAA;AACvD;AAEO,SAASO,GAAc,EAAE,MAAAC,GAAM,SAAAR,GAAS,QAAAS,GAAQ,UAAAC,KAAsC;;AA6B3F,MA5BKF,KACH,QAAQ,MAAM,GAAGV,CAAG,mDAAmD,GAGpEY,KACH,QAAQ;AAAA,IACN,GAAGZ,CAAG;AAAA,EAAA,IAKLG,IAAAD,KAAA,gBAAAA,EAAS,cAAT,QAAAC,EAAoB,UACvB,QAAQ;AAAA,IACN,GAAGH,CAAG;AAAA,EAAA,GAeNE,KAAA,QAAAA,EAAS;AACX,QAAI;AACF,MAAAW,EAAsBX,CAAO;AAAA,IAC/B,SAASY,GAAK;AACZ,cAAQ,MAAM,GAAGd,CAAG,IAAIc,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,CAAC,EAAE;AAAA,IAC5E;AAGF,QAAMC,IAAOJ,EAAO,OAAA,GACdK,IAAYD,EAAK,gBAOjBE,IAAWhB,GAAwBC,CAAO;AAChD,EAAIe,EAAS,SAAS,KACpB,QAAQ;AAAA,IACN,GAAGjB,CAAG,YAAYE,EAAQ,SAAS,yBAC7Be,EAAS,KAAK,IAAI,CAAC,sDACRD,CAAS;AAAA,EAAA,IAM1BV,IAAAJ,KAAA,gBAAAA,EAAS,eAAT,QAAAI,EAAqB,UAAU,CAACJ,EAAQ,WAAW,SAASc,CAAS,KACvE,QAAQ;AAAA,IACN,GAAGhB,CAAG,4BAA4BgB,CAAS,mCAClBd,EAAQ,WAAW,KAAK,IAAI,CAAC;AAAA,EAAA;AAO1D,QAAMgB,IAAQH,EAAK,SAAS,IAAI,CAACI,MAAQA,EAAyB,IAAI,EAAE,OAAO,CAACC,MAAmB,EAAQA,CAAE,GACvGC,IAAQH,EAAM,OAAO,CAACE,GAAGE,MAAMJ,EAAM,QAAQE,CAAC,MAAME,CAAC;AAC3D,EAAID,EAAM,UACR,QAAQ;AAAA,IACN,GAAGrB,CAAG,6BAA6B,CAAC,GAAG,IAAI,IAAIqB,CAAK,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,EAAA;AAKvE;ACpBA,SAASE,GAAWC,GAA8F;AAChH,MAAI,cAAcA,KAASA,EAAM;AAC/B,WAAO,EAAE,UAAUA,EAAM,SAAA;AAE3B,QAAMC,IAASD,EAAoE;AACnF,SAAI,OAAOC,KAAU,aAAmB,EAAE,UAAUA,EAAA,IAC7C,EAAE,UAAU,MAAM,QAAQ,QAAQA,CAAK,EAAA;AAChD;AASO,MAAMC,IAAsB;AAQ5B,SAASC,GAAeX,GAA2B;AACxD,SACE,mCAAmC,KAAK,MAAMU,IAAsB,GAAI,CAAC,oOAGTV,CAAS,oLAG3DA,CAAS;AAG3B;AAEO,SAASY,GAAcJ,GAA+C;AAC3E,QAAM,EAAE,MAAAd,GAAM,SAAAR,GAAS,QAAAS,GAAQ,YAAAkB,IAAa,KAAK,WAAAC,GAAW,eAAAC,GAAe,OAAAC,GAAO,UAAAC,GAAU,UAAAC,EAAA,IAAaV,GAEnGR,IAAYmB,EAAQ,MAAMxB,EAAO,SAAS,gBAAgB,CAACA,CAAM,CAAC,GAGlEyB,IAAeC,EAAOP,CAAS,GAC/BQ,IAAmBD,EAAON,CAAa;AAC7C,EAAAK,EAAa,UAAUN,GACvBQ,EAAiB,UAAUP;AAE3B,QAAM,CAACQ,GAASC,CAAU,IAAIC,EAAyB,CAAA,CAAE,GACnD,CAACC,GAAaC,CAAc,IAAIF,EAAS,EAAI,GAC7C,CAACG,GAAOC,CAAQ,IAAIJ,EAAwB,IAAI,GAEhD7B,IAAW,GAAS,cAAcY,KAASA,EAAM,YAAc,WAAWA,KAASA,EAAM,QAQzFsB,IAAOvB,GAAWC,CAAK,GACvBuB,IAAU,cAAcD,IAAOA,EAAK,WAAW,YAC/CE,IAAa,GAAG9C,EAAQ,SAAS,IAAIA,EAAQ,UAAU,KAAK,GAAG,CAAC,IAAIA,EAAQ,aAAa,EAAE,IAK3F+C,IAAgBd;AAAA,IACpB,MAAM;AACJ,UAAI;AACF,eAAO,EAAE,SAAS,IAAIe,EAAc,EAAE,MAAAxC,GAAM,SAAAR,GAAS,OAAA8B,GAAO,GAAGc,EAAA,CAAM,EAAA;AAAA,MACvE,SAAShC,GAAK;AACZ,eAAO,EAAE,OAAOA,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,EAAA;AAAA,MACjE;AAAA,IACF;AAAA;AAAA,IAEA,CAACJ,GAAMsC,GAAYhC,GAAWgB,GAAOe,CAAO;AAAA,EAAA,GAExCI,IAAU,aAAaF,IAAgBA,EAAc,UAAU,MAC/DG,IAAiB,WAAWH,IAAgBA,EAAc,QAAQ,MAQlEI,IAAkBhB,EAAsB,IAAI;AAClD,EAAAiB,EAAU,MAAM;;AACd,QAAI,OAAOC,IAAY,SAAeA,IAAAA,EAAQ,QAARA,gBAAAA,EAAa,cAAa,cAAc;AAC5E,YAAMC,IAAM,GAAG9C,CAAI,IAAIsC,CAAU,IAAIhC,CAAS,IAAIJ,CAAQ;AAC1D,UAAIyC,EAAgB,YAAYG,EAAK;AACrC,MAAAH,EAAgB,UAAUG,GAC1B/C,GAAc,EAAE,MAAAC,GAAM,SAAAR,GAAS,QAAAS,GAAQ,UAAAC,GAAU;AAAA,IACnD;AAAA,EAEF,GAAG,CAACF,GAAMsC,GAAYhC,GAAWJ,CAAQ,CAAC;AAE1C,QAAM6C,IAAatB,EAAQ,MAClB,CAACuB,GAAoBC,IAAW9B,MAAe;AACpD,IAAKsB,MACLN,EAAS,IAAI,GACbF,EAAe,EAAI,GACdQ,EAAQ,YAAYnC,GAAW0C,GAAOC,CAAQ,EAAE,MAAM,CAAC7C,MAAQ;;AAClE,YAAM8C,IAAU9C,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG;AAC/D,MAAA+B,EAASe,CAAO,GAChBjB,EAAe,EAAK,IACpBxC,IAAAmC,EAAiB,YAAjB,QAAAnC,EAAA,KAAAmC,GAA2BsB;AAAA,IAC7B,CAAC;AAAA,EACH,GACC,CAACT,GAASnC,GAAWa,CAAU,CAAC;AAEnC,EAAAyB,EAAU,MAAM;;AAEd,QAAIF,GAAgB;AAClB,MAAAP,EAASO,CAAc,GACvBT,EAAe,EAAK,IACpBxC,IAAAmC,EAAiB,YAAjB,QAAAnC,EAAA,KAAAmC,GAA2Bc;AAC3B;AAAA,IACF;AACA,QAAKD;AAEL,aAAAA,EAAQ,UAAU,CAACU,MAAY;;AAC7B,QAAArB,EAAWqB,CAAO,GAClBlB,EAAe,EAAK,IACpBxC,IAAAiC,EAAa,YAAb,QAAAjC,EAAA,KAAAiC,GAAuByB;AAAA,MACzB,CAAC,GACDV,EAAQ,QAAQ,CAACS,MAAY;;AAC3B,QAAAf,EAASe,CAAO,GAChBjB,EAAe,EAAK,IACpBxC,IAAAmC,EAAiB,YAAjB,QAAAnC,EAAA,KAAAmC,GAA2BsB;AAAA,MAC7B,CAAC,GAKDH,EAAW9C,EAAO,iBAAiB,GAE5B,MAAMwC,EAAQ,MAAA;AAAA,EAEvB,GAAG,CAACA,GAASC,CAAc,CAAC,GAQ5BE,EAAU,MAAM;;AAEd,QADI,CAACZ,KACD,OAAOa,IAAY,SAAeA,IAAAA,EAAQ,QAARA,gBAAAA,EAAa,cAAa,aAAc;AAC9E,UAAMO,IAAQ,WAAW,MAAM,QAAQ,KAAKnC,GAAeX,CAAS,CAAC,GAAGU,CAAmB;AAC3F,WAAO,MAAM,aAAaoC,CAAK;AAAA,EACjC,GAAG,CAACpB,GAAa1B,CAAS,CAAC;AAE3B,QAAM+C,IAAQ5B;AAAA,IACZ,OAAO;AAAA,MACL,SAAAgB;AAAA,MACA,QAAAxC;AAAA,MACA,WAAAK;AAAA,MACA,SAAAd;AAAA,MACA,SAAAqC;AAAA,MACA,aAAAG;AAAA,MACA,OAAAE;AAAA,MACA,YAAAf;AAAA,MACA,YAAA4B;AAAA,IAAA;AAAA,IAEF,CAACN,GAASxC,GAAQK,GAAWd,GAASqC,GAASG,GAAaE,GAAOf,GAAY4B,CAAU;AAAA,EAAA,GAGrFO,IAAO,gBAAAC,EAACtE,EAAa,UAAb,EAAsB,OAAAoE,GAAe,UAAA7B,GAAS;AAI5D,SAAID,IACK,gBAAAgC,EAACC,GAAA,EAAuB,UAAAjC,GAAqB,UAAA+B,EAAA,CAAK,IAEpDA;AACT;AC7NO,SAASG,GAAe;AAAA,EAC7B,UAAAjC;AAAA,EACA,KAAAkC,IAAM;AAAA,EACN,cAAAC,IAAe;AAAA,EACf,WAAAC;AAAA,EACA,UAAArC;AAAA,EACA,WAAAsC;AAAA,EACA,OAAAC;AACF,GAA4C;AAC1C,QAAM,EAAE,QAAA7D,GAAQ,WAAAK,GAAW,YAAAa,GAAY,YAAA4B,EAAA,IAAe5D,EAAA,GAIhD4E,IAAOtC,EAAQ,MAAMxB,EAAO,UAAU,CAACA,CAAM,CAAC;AAEpD,SACE,gBAAAsD,EAAC,OAAA,EAAI,WAAW,aAAaM,IAAY,IAAIA,CAAS,KAAK,EAAE,IAAI,OAAAC,GAC/D,UAAA,gBAAAP;AAAA,IAACS;AAAA,IAAA;AAAA,MACC,KAAAN;AAAA,MACA,cAAAC;AAAA,MACA,UAAApC;AAAA,MACA,WAAWwC,EAAK;AAAA,MAChB,gBAAgBzD;AAAA,MAChB,iBAAiByD,EAAK;AAAA,MACtB,cAAcH,IAAY,EAAE,WAAAA,EAAA,IAAc;AAAA,MAC1C,SAASpC;AAAA,MACT,UAAU,CAACwB,MAAUD,EAAWkB,EAAwBjB,CAAK,GAAG7B,CAAU;AAAA,IAAA;AAAA,EAAA,GAE9E;AAEJ;ACtEO,SAAS+C,GAAe;AAAA,EAC7B,UAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,KAAAC,IAAM;AAAA,EACN,WAAAR;AAAA,EACA,OAAAC;AACF,GAA4C;AAC1C,QAAM,EAAE,SAAAjC,EAAA,IAAY1C,EAAA,GAEdmF,IAASH,IACXtC,EAAQ,KAAK,CAAC0C,MAAMA,EAAE,aAAaJ,KAAYI,EAAE,QAAQ,IACzD1C,EAAQ,KAAK,CAAC0C,MAAMA,EAAE,QAAQ;AAElC,SAAKD,KAAA,QAAAA,EAAQ;AAAA;AAAA,sBAMV,OAAA,EAAI,KAAKA,EAAO,UAAU,KAAAD,GAAU,WAAAR,GAAsB,OAAAC,EAAA,CAAc;AAAA,6BAL/D,UAAAM,EAAA,CAAS;AAOvB;AC3BO,SAASI,KAA+C;AAC7D,QAAM,EAAE,SAAA3C,GAAS,aAAAG,GAAa,OAAAE,EAAA,IAAU/C,EAAA;AACxC,SAAO,EAAE,SAAA0C,GAAS,aAAAG,GAAa,OAAAE,EAAA;AACjC;ACeAuC,EAAA;"}
1
+ {"version":3,"file":"embed.mjs","sources":["../src/components/embed-realtime/context.ts","../src/components/embed-realtime/validateEmbed.ts","../src/components/embed-realtime/RealtimeEmbed.tsx","../src/components/embed-realtime/RealtimeCanvas.tsx","../src/components/embed-realtime/RealtimeMockup.tsx","../src/components/embed-realtime/useRealtimeMockups.ts","../src/api/embed.ts"],"sourcesContent":["'use client';\n\n/**\n * Shared context for the realtime embed primitives. `<RealtimeEmbed>` owns the\n * single {@link RenderSession} and the live mockup state; `<RealtimeCanvas>` and\n * `<RealtimeMockup>` read it from here so the editor and the preview can be laid\n * out freely yet can never desync (they share one session, one design, one\n * placement).\n */\n\nimport { createContext, useContext } from 'react';\nimport type { RenderSession, RenderProduct, RenderState, MockupResult } from '@snowcone-app/sdk';\nimport type { DesignBuilder } from '../../design/design-builder.js';\n\nexport interface EmbedContextValue {\n /** The single session the editor pushes renders to. `null` if the session\n * failed to construct (a config fail-fast, surfaced via `error`). Escape\n * hatch for advanced use. */\n session: RenderSession | null;\n /** The design the embed was seeded with — single source of truth for both the\n * canvas seed and the seed render. */\n design: DesignBuilder;\n /** The placement name (= the design's artboard name). Both the canvas artboard\n * and every `renderState` call use this; derived once so it can't drift. */\n placement: string;\n /** The product being rendered against. */\n product: RenderProduct;\n /** Latest mockups from the session. Reactive — updates re-render consumers. */\n mockups: MockupResult[];\n /** True between a render dispatch and the next mockup / error. */\n isRendering: boolean;\n /** Last render error message, or null. */\n error: string | null;\n /** Default edit→render throttle (ms). */\n throttleMs: number;\n /** Dispatch a server render for the embed's placement. Sets `isRendering`. */\n pushRender: (state: RenderState, throttleMs?: number) => void;\n}\n\nexport const EmbedContext = createContext<EmbedContextValue | undefined>(undefined);\n\n/**\n * Read the embed context. Throws a clear error when called outside\n * `<RealtimeEmbed>` — the most common wiring mistake.\n */\nexport function useEmbedContext(): EmbedContextValue {\n const ctx = useContext(EmbedContext);\n if (!ctx) {\n throw new Error(\n 'useRealtimeMockups / <RealtimeCanvas> / <RealtimeMockup> must be rendered ' +\n 'inside <RealtimeEmbed>. Wrap them: ' +\n '<RealtimeEmbed shop=… product=… design=… grantUrl=…>…</RealtimeEmbed>'\n );\n }\n return ctx;\n}\n","'use client';\n\n/**\n * Dev-only assertions for the realtime embed. Catches the silent-break wiring\n * mistakes that otherwise produce a blank canvas or a mockup that never renders:\n *\n * - no grant source (the session can't authorize → silent hang)\n * - a variant product with no `variantId` (→ ~30s render hang), AND the\n * can't-even-tell case: neither `variantId` nor `options` passed\n * - empty `mockupIds` (nothing to render)\n * - the design's placement isn't one of the product's placements (renderState\n * targets an artboard the product doesn't have → silent blank mockup)\n * - a multi-placement product (the embed renders ONE placement, so the server\n * waits on the rest and the render times out ~15s in — needs the eject path)\n * - duplicate layer names (binding hooks resolve to the wrong element)\n *\n * Advisory by design: it logs `console.warn` / `console.error` and never throws,\n * so a partly-wired embed still mounts and renders what it can. (The hard\n * fail-fast for a missing required `variantId` lives in the session itself.)\n * Tree-shaken out of production builds via the `NODE_ENV` guard at the call site.\n */\n\nimport { assertVariantSelected, type RenderProduct } from '@snowcone-app/sdk';\nimport type { DesignBuilder } from '../../design/design-builder.js';\n\nexport interface ValidateEmbedInput {\n shop: string;\n product: RenderProduct;\n design: DesignBuilder;\n hasGrant: boolean;\n}\n\nconst TAG = '[RealtimeEmbed]';\n\n/**\n * The placement labels this product demands a canvas_state for. Prefers the\n * catalog's `requiredPlacements` contract; variant-auto-filled (color)\n * placements don't count — `variantId` satisfies them (the missing-variantId\n * case has its own guard above). Falls back to the bare `placements` label\n * list ONLY when the product has no variant in play: a label list can't say\n * which entries are auto-filled colors, but without a variant none are.\n */\nfunction requiredPlacementLabels(product: RenderProduct): string[] {\n if (product?.requiredPlacements?.length) {\n return product.requiredPlacements.filter((p) => !p.autoFilledByVariant).map((p) => p.label);\n }\n const variantIds = new Set(\n (product?.options?.combinations ?? [])\n .map((c) => c.variantId)\n .filter((id): id is string => typeof id === 'string' && id !== '')\n );\n const hasVariantAxis = Boolean(product?.variantId) || variantIds.size > 1;\n return hasVariantAxis ? [] : (product?.placements ?? []);\n}\n\nexport function validateEmbed({ shop, product, design, hasGrant }: ValidateEmbedInput): void {\n if (!shop) {\n console.error(`${TAG} \\`shop\\` is required (your publishable shop id).`);\n }\n\n if (!hasGrant) {\n console.error(\n `${TAG} no grant source — pass \\`grantUrl\\` (a proxy to mintRealtimeGrant) ` +\n 'or `grant`. Without it the session can never authorize and no mockup renders.'\n );\n }\n\n if (!product?.mockupIds?.length) {\n console.error(\n `${TAG} product.mockupIds is empty — pass the catalog scene codes ` +\n '(product.mockups[].id) you want rendered, e.g. [\"FV1qjO\"].'\n );\n }\n\n // Variant axis: a product with a color/size option REQUIRES a variantId or the\n // render hangs ~30s waiting for a color blob that never comes. When `options`\n // (the catalog combinations) is passed we can detect the axis and fail fast\n // before the session's constructor throws. When `options` is NOT passed we\n // can't see the axis — so we DON'T warn here (a no-`options` product might\n // genuinely have no variants; warning would false-fire on every valid\n // single-variant embed). That stall is instead caught at runtime by\n // `<RealtimeEmbed>`'s slow-render backstop (`slowRenderHint`), which fires only\n // when a render actually hangs — no false positives, and it covers every\n // silent-hang cause, not just this one.\n if (product?.options) {\n try {\n assertVariantSelected(product);\n } catch (err) {\n console.error(`${TAG} ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n const json = design.toJSON();\n const placement = json.activeArtboard;\n\n // <RealtimeEmbed> renders exactly ONE placement — the design's artboard. A\n // product that requires several can never complete through it: the canvas\n // mounts perfectly, then the server waits on the missing canvas_states until\n // the ~15s \"Timed out waiting for all placements\" error. Fully knowable at\n // mount from the catalog data already on the product, so say so now.\n const required = requiredPlacementLabels(product);\n if (required.length > 1) {\n console.error(\n `${TAG} product ${product.productId} requires placements ` +\n `[${required.join(', ')}] but <RealtimeEmbed> renders a single ` +\n `placement (\"${placement}\"). The render will time out waiting for the ` +\n 'others. This product needs the eject path — see ' +\n 'https://developers.snowcone.app/embed-mode#eject.'\n );\n }\n\n if (product?.placements?.length && !product.placements.includes(placement)) {\n console.warn(\n `${TAG} the design's placement \"${placement}\" is not in ` +\n `product.placements [${product.placements.join(', ')}]. The canvas ` +\n 'artboard name and renderState placement must equal a real product ' +\n 'placement label, or the server renders a blank mockup. Fix the ' +\n '`name` passed to design({ name }).'\n );\n }\n\n const names = json.elements.map((el) => (el as { name?: string }).name).filter((n): n is string => Boolean(n));\n const dupes = names.filter((n, i) => names.indexOf(n) !== i);\n if (dupes.length) {\n console.warn(\n `${TAG} duplicate layer name(s) [${[...new Set(dupes)].join(', ')}] — ` +\n 'useTextBinding(name) / useImageBinding(name) bind to the FIRST match, ' +\n 'so a duplicate name makes a control edit the wrong layer. Keep names unique.'\n );\n }\n}\n","'use client';\n\n/**\n * `<RealtimeEmbed>` — the one place the required pieces live.\n *\n * It owns a single {@link RenderSession}, seeds the first render from the\n * `design`, tracks the live mockups, and hands all of it to `<RealtimeCanvas>`\n * and `<RealtimeMockup>` through context. Every prop that an embed needs to work\n * is REQUIRED here, so a coding agent can't ship a half-wired embed:\n *\n * @example\n * ```tsx\n * const d = design({ name: 'Front', width: 1480, height: 2328 })\n * .background(artUrl, { aspectRatio: 1480 / 2328, name: 'art' })\n * .text('YOUR NAME', { anchor: 'top', name: 'headline' });\n *\n * <RealtimeEmbed\n * shop=\"YOUR_SHOP_ID\"\n * grantUrl=\"/api/realtime/grant\"\n * // variantId is REQUIRED for a product with a color/variant axis (BEEB77 has\n * // a Frame color) — omit it and the render hangs. Pass `options` too so a\n * // wrong/missing variantId fails fast instead of timing out. Read both from\n * // getProduct(productId) (its options.combinations[].variantId).\n * product={{ productId: 'BEEB77', mockupIds: ['FV1qjO'], variantId: 'Pv1sLC', placements: ['Front'] }}\n * design={d}\n * >\n * <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>\n * <RealtimeCanvas /> {/* editor *\\/}\n * <RealtimeMockup /> {/* live preview *\\/}\n * </div>\n * </RealtimeEmbed>\n * ```\n */\n\nimport React, { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport {\n RenderSession,\n type RenderProduct,\n type RenderState,\n type MockupResult,\n type RealtimeGrant,\n} from '@snowcone-app/sdk';\nimport type { DesignBuilder } from '../../design/design-builder.js';\nimport { CanvasServicesProvider, type CanvasServices } from '../../contexts/CanvasServicesContext.js';\nimport { EmbedContext, type EmbedContextValue } from './context.js';\nimport { validateEmbed } from './validateEmbed.js';\n\n/**\n * How the session authorizes. Provide exactly one — the discriminated union makes\n * \"I forgot the grant\" a compile error rather than a silent runtime hang.\n *\n * Member order is load-bearing: when NEITHER prop is supplied (the common\n * mistake) TS reports the missing property of the LAST union member, so the\n * `grantUrl` arm is kept last on purpose — the error then reads \"Property\n * 'grantUrl' is missing\", pointing at the recommended prop instead of `grant`.\n * Don't reorder these.\n */\nexport type GrantSource =\n | {\n /** Full control over fetching the grant: a token or a token factory. */\n grant: RealtimeGrant | (() => Promise<RealtimeGrant>);\n grantUrl?: never;\n }\n | {\n /** Your endpoint returning `{ token, expiresAt }` for `POST { shop }` —\n * a same-origin proxy to `mintRealtimeGrant`. Recommended. */\n grantUrl: string;\n grant?: never;\n };\n\nexport type RealtimeEmbedProps = GrantSource & {\n /** Publishable shop id (= shop.id, like a Stripe pk_). Billing target. */\n shop: string;\n /** Product to render against (catalog ids). `mockupIds` are scene codes\n * (product.mockups[].id), NOT placement names. Pass `placements`, `options`\n * + `requiredPlacements` so wiring mistakes (including a multi-placement\n * product, which the embed can't render) fail loud at mount instead of\n * hanging. */\n product: RenderProduct;\n /** Single source of truth — seeds the canvas AND the first render. Build it\n * with `design({ name, width, height })…`. Its artboard `name` is the\n * placement every render targets. */\n design: DesignBuilder;\n /** Edit→render throttle in ms (default 200). */\n throttleMs?: number;\n /** Called whenever new mockups arrive. */\n onMockups?: (results: MockupResult[]) => void;\n /** Called on a render/timeout error. */\n onRenderError?: (message: string) => void;\n /** Override the realtime WS endpoint (defaults to the SDK's prod URL). */\n wsUrl?: string;\n /**\n * Privileged-service wiring for the editor chrome (the Image Browser's\n * stock-image search and AI generation tabs). Same pattern as `grantUrl`:\n * host these URLs on your server; your vendor keys live there. Flows to\n * every `<RealtimeCanvas>` inside this embed. Tabs without a configured\n * service do not render.\n *\n * @example\n * ```tsx\n * <RealtimeEmbed\n * services={{ imageSearchUrl: '/api/image-search', generateUrl: '/api/generate-image' }}\n * …\n * />\n * ```\n */\n services?: CanvasServices;\n children?: ReactNode;\n};\n\nfunction toGetToken(props: RealtimeEmbedProps): { grantUrl: string } | { getToken: () => Promise<RealtimeGrant> } {\n if ('grantUrl' in props && props.grantUrl) {\n return { grantUrl: props.grantUrl };\n }\n const grant = (props as { grant: RealtimeGrant | (() => Promise<RealtimeGrant>) }).grant;\n if (typeof grant === 'function') return { getToken: grant };\n return { getToken: () => Promise.resolve(grant) };\n}\n\n/**\n * How long a render may be in flight before the embed assumes something is wrong\n * and surfaces a diagnostic. A healthy render returns in ~2s; the session's own\n * watchdog is ~30s. 8s sits in between — long enough not to false-fire on a\n * genuinely slow render, short enough to spare a first-timer the full 30s of\n * silence.\n */\nexport const SLOW_RENDER_HINT_MS = 8000;\n\n/**\n * The dev-console message shown when a render hasn't produced a mockup in\n * {@link SLOW_RENDER_HINT_MS}. A general backstop for the whole silent-hang\n * class — not just the missing-`variantId` trap — so a stuck embed always says\n * *something* actionable instead of hanging quietly.\n */\nexport function slowRenderHint(placement: string): string {\n return (\n `[RealtimeEmbed] No mockup after ${Math.round(SLOW_RENDER_HINT_MS / 1000)}s. ` +\n `Common causes: a variant product missing \\`variantId\\` (pass \\`product.options\\` ` +\n `so the embed can flag it up front), a multi-placement product (the server waits ` +\n `for every required placement but <RealtimeEmbed> sends only \"${placement}\" — pass ` +\n `\\`product.requiredPlacements\\` so this is flagged at mount; such products need ` +\n `the eject path), an unreachable or non-public asset URL in the design, or the ` +\n `placement \"${placement}\" not existing on the product. ` +\n `The render may still arrive — this is a heads-up, not a failure.`\n );\n}\n\nexport function RealtimeEmbed(props: RealtimeEmbedProps): React.ReactElement {\n const { shop, product, design, throttleMs = 200, onMockups, onRenderError, wsUrl, services, children } = props;\n\n const placement = useMemo(() => design.toJSON().activeArtboard, [design]);\n\n // Keep the latest callbacks reachable without re-creating the session.\n const onMockupsRef = useRef(onMockups);\n const onRenderErrorRef = useRef(onRenderError);\n onMockupsRef.current = onMockups;\n onRenderErrorRef.current = onRenderError;\n\n const [mockups, setMockups] = useState<MockupResult[]>([]);\n const [isRendering, setIsRendering] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n const hasGrant = Boolean(('grantUrl' in props && props.grantUrl) || ('grant' in props && props.grant));\n\n // One session per (shop, product, placement, auth). Key it on the product's\n // identity-BEARING fields, not the object reference — so an embedder passing\n // an inline `product={{…}}` (the natural way to write it) doesn't rebuild the\n // session and reconnect on every render. A real change (different product,\n // mockups, or variant) still re-keys it. `options`/`placements` are advisory\n // (fail-fast validation) so they don't need to force a rebuild.\n const auth = toGetToken(props);\n const authKey = 'grantUrl' in auth ? auth.grantUrl : 'getToken';\n const productKey = `${product.productId}|${product.mockupIds.join(',')}|${product.variantId ?? ''}`;\n // RenderSession's constructor fail-fasts (e.g. a variant product with no\n // variantId) by THROWING. Catch it here so a config mistake surfaces through\n // `onRenderError` + the error state instead of throwing during render and\n // white-screening the whole host page.\n const sessionResult = useMemo<{ session: RenderSession } | { error: string }>(\n () => {\n try {\n return { session: new RenderSession({ shop, product, wsUrl, ...auth }) };\n } catch (err) {\n return { error: err instanceof Error ? err.message : String(err) };\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [shop, productKey, placement, wsUrl, authKey]\n );\n const session = 'session' in sessionResult ? sessionResult.session : null;\n const constructError = 'error' in sessionResult ? sessionResult.error : null;\n\n // Dev-only wiring guard (tree-shaken from production builds). Run it in an\n // effect keyed on the meaningful inputs so warnings fire once per change, not\n // on every render (validateEmbed used to flood the console). The ref-based\n // dedup survives StrictMode's deliberate unmount/remount cycle, which would\n // otherwise print every diagnostic 2-3×. The `process` reference is\n // typeof-guarded on the same line — non-Node bundlers don't define it.\n const validatedKeyRef = useRef<string | null>(null);\n useEffect(() => {\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n const key = `${shop}|${productKey}|${placement}|${hasGrant}`;\n if (validatedKeyRef.current === key) return;\n validatedKeyRef.current = key;\n validateEmbed({ shop, product, design, hasGrant });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [shop, productKey, placement, hasGrant]);\n\n const pushRender = useMemo(() => {\n return (state: RenderState, throttle = throttleMs) => {\n if (!session) return;\n setError(null);\n setIsRendering(true);\n void session.renderState(placement, state, throttle).catch((err) => {\n const message = err instanceof Error ? err.message : String(err);\n setError(message);\n setIsRendering(false);\n onRenderErrorRef.current?.(message);\n });\n };\n }, [session, placement, throttleMs]);\n\n useEffect(() => {\n // Construction fail-fast (caught above) — surface it instead of rendering.\n if (constructError) {\n setError(constructError);\n setIsRendering(false);\n onRenderErrorRef.current?.(constructError);\n return;\n }\n if (!session) return;\n\n session.onMockups((results) => {\n setMockups(results);\n setIsRendering(false);\n onMockupsRef.current?.(results);\n });\n session.onError((message) => {\n setError(message);\n setIsRendering(false);\n onRenderErrorRef.current?.(message);\n });\n\n // Seed the first mockup from the design before any edit, so a freshly\n // mounted embed shows a real mockup immediately (no duplicate seed state —\n // the design is the single source).\n pushRender(design.toServerRequest());\n\n return () => session.close();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [session, constructError]);\n\n // Runtime silent-hang backstop. While a render is in flight, arm a timer; if no\n // mockup (or error) lands within SLOW_RENDER_HINT_MS, log an actionable hint.\n // Unlike the mount-time wiring guard this fires ONLY on an actual stall, so it\n // never nags a working embed — and it covers every silent-hang cause, not just\n // missing variantId. Dev-only (the `process` ref is typeof-guarded on the same\n // line); cleared the moment `isRendering` flips false.\n useEffect(() => {\n if (!isRendering) return;\n if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production') return;\n const timer = setTimeout(() => console.warn(slowRenderHint(placement)), SLOW_RENDER_HINT_MS);\n return () => clearTimeout(timer);\n }, [isRendering, placement]);\n\n const value = useMemo<EmbedContextValue>(\n () => ({\n session,\n design,\n placement,\n product,\n mockups,\n isRendering,\n error,\n throttleMs,\n pushRender,\n }),\n [session, design, placement, product, mockups, isRendering, error, throttleMs, pushRender]\n );\n\n const body = <EmbedContext.Provider value={value}>{children}</EmbedContext.Provider>;\n\n // Only mount a services provider when configured — `<RealtimeCanvas>`'s\n // inner SnowconeCanvas inherits it through context.\n if (services) {\n return <CanvasServicesProvider services={services}>{body}</CanvasServicesProvider>;\n }\n return body;\n}\n","'use client';\n\n/**\n * `<RealtimeCanvas>` — the interactive editor half of a realtime embed.\n *\n * Mounts a `<SnowconeCanvas>` seeded from the embed's `design`, and forwards\n * every edit to the shared {@link RenderSession} as a server render (the\n * recommended ship-state path — no pixel upload). All the load-bearing wiring an\n * embedder used to copy by hand is internal here: the `app-modern` class,\n * `inheritTheme`, edit→`serializeStateForServer`→`renderState`, and the\n * artboard/placement match.\n *\n * Defaults to the chrome-less `kit=\"embed-only\"` — bring your own controls as\n * `children` (they render as an overlay INSIDE the canvas's editor context, so\n * `useTextBinding(name)` / `useImageBinding(name)` resolve against the live\n * elements). For a full in-canvas editor (select layer, font size, add element)\n * pass `kit=\"compact-customizer\"` or `kit=\"pro-studio\"` — renders still flow\n * automatically. See the {@link RealtimeCanvasProps.kit} docs.\n *\n * Must be rendered inside `<RealtimeEmbed>`.\n */\n\nimport React, { useMemo, type CSSProperties, type ReactNode } from 'react';\nimport { SnowconeCanvas } 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 './context.js';\n\nexport interface RealtimeCanvasProps {\n /** Your controls (toolbar, inputs). Rendered as an overlay inside the editor\n * context so binding hooks work. */\n children?: ReactNode;\n /**\n * Editor chrome to mount. The default `'embed-only'` is a chrome-less canvas —\n * the host supplies every control as `children` (text inputs, image swap)\n * through `useTextBinding` / `useImageBinding`. That's the right call for a\n * fill-in-the-blanks personalizer, but it gives the shopper NO way to select a\n * layer and change its font size, add an element, or reorder layers.\n *\n * To ship a real editor through the turnkey realtime path, pass a richer\n * preset — `'compact-customizer'` (a trimmed toolbar) or `'pro-studio'` (full\n * panels). Every edit still flows to the shared {@link RenderSession} as a\n * server render automatically; you get add-element / font-size / layers for\n * free without ejecting to a raw `SnowconeCanvas` + `RenderSession`.\n *\n * Pair a chrome'd kit with `children` only when your overlay adds to — not\n * duplicates — the kit's controls, or you'll get double chrome.\n *\n * @default 'embed-only'\n */\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 /**\n * Privileged-service wiring for the editor chrome (Image Browser search /\n * AI tabs). Usually set once on `<RealtimeEmbed services={…}>` and inherited;\n * pass it here only to override for this canvas. Host the URLs on your\n * server — your vendor keys live there.\n */\n services?: CanvasServices;\n className?: string;\n style?: CSSProperties;\n}\n\nexport function RealtimeCanvas({\n children,\n kit = 'embed-only',\n inheritTheme = true,\n maxHeight,\n services,\n className,\n style,\n}: RealtimeCanvasProps): React.ReactElement {\n const { design, placement, throttleMs, pushRender } = useEmbedContext();\n\n // The design is the single source for the canvas seed — same object that\n // seeded the first render, so the editor and the preview start identical.\n const seed = useMemo(() => design.toJSON(), [design]);\n\n return (\n <div className={`app-modern${className ? ` ${className}` : ''}`} style={style}>\n <SnowconeCanvas\n kit={kit}\n inheritTheme={inheritTheme}\n services={services}\n artboards={seed.artboards}\n activeArtboard={placement}\n initialElements={seed.elements}\n layoutConfig={maxHeight ? { maxHeight } : undefined}\n overlay={children}\n onChange={(state) => pushRender(serializeStateForServer(state), throttleMs)}\n />\n </div>\n );\n}\n","'use client';\n\n/**\n * `<RealtimeMockup>` — the live preview half of a realtime embed.\n *\n * Reads the shared session's mockups from context and renders the rendered\n * image, updating as the shopper edits. Because it shares one session with\n * `<RealtimeCanvas>`, the preview can never desync from the editor and needs no\n * wiring of its own. Place it anywhere inside `<RealtimeEmbed>`.\n */\n\nimport React, { useRef, type CSSProperties, type ReactNode, type SyntheticEvent } from 'react';\nimport { useEmbedContext } from './context.js';\n\nexport interface RealtimeMockupProps {\n /** Which mockup scene to show (a `product.mockupIds` entry). Defaults to the\n * first mockup that has rendered. */\n mockupId?: string;\n /** Shown before the first mockup arrives / while a render is in flight with no\n * prior image. Defaults to nothing. */\n fallback?: ReactNode;\n /** Alt text for the rendered image. */\n alt?: string;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * How far the displayed device-pixel width may exceed the delivered mockup's\n * natural width before {@link resolutionMismatchHint} fires. Renders default to\n * 1000px wide, so a 1000px image inside a ~1100 device-px slot is fine (browser\n * downscale slack), but stretched across 2000 device px it looks soft and the\n * developer deserves to hear why.\n */\nexport const RESOLUTION_MISMATCH_RATIO = 1.2;\n\n/**\n * The render-width ladder a mismatch suggestion rounds UP to. Mirrors the\n * `product.width` guidance (\"bump to ~2000 for a crisp preview\") with sensible\n * steps — suggesting the exact device-px count would invite over-precise\n * configs that re-trigger on the next viewport.\n */\nconst SUGGESTED_WIDTHS = [1200, 1400, 1600, 1800, 2000, 2500, 3000, 4000] as const;\n\n/** Rounds a device-pixel width up to the next {@link SUGGESTED_WIDTHS} step\n * (capped at the top step). */\nexport function suggestRenderWidth(devicePx: number): number {\n for (const step of SUGGESTED_WIDTHS) {\n if (step >= devicePx) return step;\n }\n return SUGGESTED_WIDTHS[SUGGESTED_WIDTHS.length - 1];\n}\n\n/**\n * Builds the dev-console warning shown when a delivered mockup's natural width\n * is meaningfully below the width it's displayed at in DEVICE pixels (CSS width\n * × `devicePixelRatio`) — the \"my preview looks soft and nothing says why\"\n * trap: renders default to 1000px wide, which blurs on a large or high-density\n * display. Returns `null` when the resolution is adequate (within\n * {@link RESOLUTION_MISMATCH_RATIO}) or either measurement is unusable.\n */\nexport function resolutionMismatchHint(naturalWidth: number, devicePx: number): string | null {\n if (!(naturalWidth > 0) || !(devicePx > 0)) return null;\n if (devicePx <= naturalWidth * RESOLUTION_MISMATCH_RATIO) return null;\n const suggestion = suggestRenderWidth(devicePx);\n return (\n `[RealtimeMockup] mockup rendered at ${naturalWidth}px but displayed at ` +\n `~${devicePx} device px — the preview will look soft. Pass ` +\n `\\`product.width\\` (e.g. ${suggestion}) on <RealtimeEmbed> for a crisp preview.`\n );\n}\n\nexport function RealtimeMockup({\n mockupId,\n fallback = null,\n alt = 'Live product mockup',\n className,\n style,\n}: RealtimeMockupProps): React.ReactElement {\n const { mockups } = useEmbedContext();\n\n // Mockup ids already warned about this mount — re-renders and repeated loads\n // of the same scene stay quiet; a different scene gets its own one warning.\n const warnedMockupIdsRef = useRef<Set<string>>(new Set());\n\n const result = mockupId\n ? mockups.find((m) => m.mockupId === mockupId && m.imageUrl)\n : mockups.find((m) => m.imageUrl);\n\n if (!result?.imageUrl) {\n return <>{fallback}</>;\n }\n\n // Dev-only resolution check (the `process` reference is typeof-guarded on the\n // same line — non-Node bundlers don't define it). Runs on load because only a\n // decoded image knows its natural width; skips SSR (no `window`) and\n // unmeasurable (zero-width) elements.\n const handleLoad = (event: SyntheticEvent<HTMLImageElement>) => {\n if (typeof process === 'undefined' || process.env?.NODE_ENV === 'production') return;\n if (typeof window === 'undefined') return;\n if (warnedMockupIdsRef.current.has(result.mockupId)) return;\n const img = event.currentTarget;\n const cssWidth = img.getBoundingClientRect().width;\n if (cssWidth <= 0) return;\n const devicePx = Math.round(cssWidth * (window.devicePixelRatio || 1));\n const hint = resolutionMismatchHint(img.naturalWidth, devicePx);\n if (hint) {\n warnedMockupIdsRef.current.add(result.mockupId);\n console.warn(hint);\n }\n };\n\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={result.imageUrl} alt={alt} className={className} style={style} onLoad={handleLoad} />\n );\n}\n","'use client';\n\nimport type { MockupResult } from '@snowcone-app/sdk';\nimport { useEmbedContext } from './context.js';\n\nexport interface UseRealtimeMockupsResult {\n /** All mockups from the latest render (one per requested mockupId). */\n mockups: MockupResult[];\n /** True between a render dispatch and the next mockup / error. */\n isRendering: boolean;\n /** Last render/timeout error message, or null. */\n error: string | null;\n}\n\n/**\n * Read the live mockups for a custom preview UI. Reactive: re-renders when new\n * mockups arrive. Must be called inside `<RealtimeEmbed>`.\n *\n * For the common case (just show the rendered image) use `<RealtimeMockup>`\n * instead — this hook is for building your own gallery / loading treatment.\n */\nexport function useRealtimeMockups(): UseRealtimeMockupsResult {\n const { mockups, isRendering, error } = useEmbedContext();\n return { mockups, isRendering, error };\n}\n","/**\n * @snowcone-app/canvas/embed — Realtime embed primitives (Tier 2)\n *\n * The turnkey way to drop a live, server-rendered product editor into your app.\n * Three components that share one session so the editor and the preview can't\n * desync, plus the `design()` builder that seeds both:\n *\n * ```tsx\n * import { RealtimeEmbed, RealtimeCanvas, RealtimeMockup, design }\n * from '@snowcone-app/canvas/embed';\n * import '@snowcone-app/canvas/style.css';\n *\n * const d = design({ name: 'Front', width: 1480, height: 2328 })\n * .background(artUrl, { aspectRatio: 1480 / 2328, name: 'art' })\n * .text('YOUR NAME', { anchor: 'top', name: 'headline' });\n *\n * <RealtimeEmbed\n * shop=\"YOUR_SHOP_ID\"\n * grantUrl=\"/api/realtime/grant\"\n * // variantId is REQUIRED when the product has a color/variant axis (BEEB77\n * // does) — omit it and the render hangs. Read it + options from getProduct().\n * product={{ productId: 'BEEB77', mockupIds: ['FV1qjO'], variantId: 'Pv1sLC', placements: ['Front'] }}\n * design={d}\n * >\n * <RealtimeCanvas />\n * <RealtimeMockup />\n * </RealtimeEmbed>\n * ```\n *\n * Everything an embed needs is REQUIRED on `<RealtimeEmbed>` — there's no\n * silently-optional prop to forget. For full manual control (your own session,\n * custom layout, multi-placement, color variants) drop to `<SnowconeCanvas>` +\n * `RenderSession` directly; see the docs' \"eject\" section.\n *\n * @packageDocumentation\n */\n\n// Register icons on module load (the editor + overlay controls use them).\nimport { ensureIconsRegistered } from '../icons/registry.js';\nensureIconsRegistered();\n\n// === Embed primitives ===\nexport { RealtimeEmbed } from '../components/embed-realtime/RealtimeEmbed.js';\nexport type { RealtimeEmbedProps, GrantSource } from '../components/embed-realtime/RealtimeEmbed.js';\n\nexport { RealtimeCanvas } from '../components/embed-realtime/RealtimeCanvas.js';\nexport type { RealtimeCanvasProps } from '../components/embed-realtime/RealtimeCanvas.js';\n\nexport { RealtimeMockup } from '../components/embed-realtime/RealtimeMockup.js';\nexport type { RealtimeMockupProps } from '../components/embed-realtime/RealtimeMockup.js';\n\nexport { useRealtimeMockups } from '../components/embed-realtime/useRealtimeMockups.js';\nexport type { UseRealtimeMockupsResult } from '../components/embed-realtime/useRealtimeMockups.js';\n\nexport { validateEmbed } from '../components/embed-realtime/validateEmbed.js';\nexport type { ValidateEmbedInput } from '../components/embed-realtime/validateEmbed.js';\n\n// === Design builder — the single source of truth for an embed ===\nexport { design, DesignBuilder } from '../design/design-builder.js';\nexport type { Anchor, ShapeKind, DesignArtboard } from '../design/design-builder.js';\n\n// === Binding hooks (build your own controls inside <RealtimeCanvas>) ===\nexport { useTextBinding } from '../hooks/useTextBinding.js';\nexport { useImageBinding } from '../hooks/useImageBinding.js';\nexport { useColorBinding } from '../hooks/useColorBinding.js';\nexport type { ColorBindingResult } from '../hooks/useColorBinding.js';\nexport { useElementByName } from '../hooks/useElementByName.js';\n\n// === Editable templates (saved state + buyer-editable field manifest) ===\nexport {\n readTemplateFields,\n withTemplateFields,\n TEMPLATE_FIELDS_KEY,\n} from '../design/template-fields.js';\nexport type {\n TemplateField,\n TemplateTextField,\n TemplateImageField,\n TemplateColorField,\n} from '../design/template-fields.js';\n\n// === Privileged services (Image Browser search / AI generation) ===\n// Same pattern as `grantUrl`: host the proxy routes on your server, your\n// vendor keys live there. Wire via `services` on <RealtimeEmbed>.\nexport { CanvasServicesProvider, useCanvasServices } from '../contexts/CanvasServicesContext.js';\nexport type { CanvasServices, ResolvedCanvasServices } from '../contexts/CanvasServicesContext.js';\nexport { createProxyImageSearch, createProxyGenerate } from '../services/imageSearch.js';\nexport type {\n ImageSearchProvider,\n ImageSearchResult,\n ImageSearchResponse,\n ImageSearchParams,\n GenerateProvider,\n GenerateParams,\n GenerateResponse,\n} from '../services/imageSearch.js';\n\n// === Re-exported SDK types embedders touch ===\nexport type {\n RenderProduct,\n RenderProductOptions,\n RenderProductCombination,\n RenderProductRequiredPlacement,\n MockupResult,\n RealtimeGrant,\n} from '@snowcone-app/sdk';\n"],"names":["EmbedContext","createContext","useEmbedContext","ctx","useContext","TAG","requiredPlacementLabels","product","_a","p","variantIds","_b","c","id","validateEmbed","shop","design","hasGrant","assertVariantSelected","err","json","placement","required","names","el","n","dupes","toGetToken","props","grant","SLOW_RENDER_HINT_MS","slowRenderHint","RealtimeEmbed","throttleMs","onMockups","onRenderError","wsUrl","services","children","useMemo","onMockupsRef","useRef","onRenderErrorRef","mockups","setMockups","useState","isRendering","setIsRendering","error","setError","auth","authKey","productKey","sessionResult","RenderSession","session","constructError","validatedKeyRef","useEffect","process","key","pushRender","state","throttle","message","results","timer","value","body","jsx","CanvasServicesProvider","RealtimeCanvas","kit","inheritTheme","maxHeight","className","style","seed","SnowconeCanvas","serializeStateForServer","RESOLUTION_MISMATCH_RATIO","SUGGESTED_WIDTHS","suggestRenderWidth","devicePx","step","resolutionMismatchHint","naturalWidth","suggestion","RealtimeMockup","mockupId","fallback","alt","warnedMockupIdsRef","result","m","handleLoad","event","img","cssWidth","hint","useRealtimeMockups","ensureIconsRegistered"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/canvas",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Self-contained canvas editor component for e-commerce product customization",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",