@snowcone-app/canvas 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/STYLING_CONTRACT.md +114 -85
  2. package/dist/{EditorCanvasImpl-BH_bKy-F.js → EditorCanvasImpl-Ckg7WO2j.js} +4 -4
  3. package/dist/{EditorCanvasImpl-BH_bKy-F.js.map → EditorCanvasImpl-Ckg7WO2j.js.map} +1 -1
  4. package/dist/{SnowconeCanvas-Cx7nXlBI.js → SnowconeCanvas-D1sLcIja.js} +2 -2
  5. package/dist/{SnowconeCanvas-Cx7nXlBI.js.map → SnowconeCanvas-D1sLcIja.js.map} +1 -1
  6. package/dist/api/embed.d.ts +2 -2
  7. package/dist/design/design-builder.d.ts +67 -12
  8. package/dist/design-builder-2VUjfmdt.js +422 -0
  9. package/dist/design-builder-2VUjfmdt.js.map +1 -0
  10. package/dist/editorErrors-B2mb1sVN.js.map +1 -1
  11. package/dist/{embed-8ecUMMH-.js → embed-DxsaqZJF.js} +4 -4
  12. package/dist/{embed-8ecUMMH-.js.map → embed-DxsaqZJF.js.map} +1 -1
  13. package/dist/embed.mjs +21 -20
  14. package/dist/errors/editorError.d.ts +10 -0
  15. package/dist/hooks/bindingGuards.d.ts +16 -0
  16. package/dist/hooks/useColorBinding.d.ts +2 -1
  17. package/dist/hooks/useFontBinding.d.ts +2 -1
  18. package/dist/hooks/useImageBinding.d.ts +2 -1
  19. package/dist/hooks/useTextBinding.d.ts +2 -1
  20. package/dist/index.mjs +3 -3
  21. package/dist/template-fields-CQ6n2P4V.js +548 -0
  22. package/dist/template-fields-CQ6n2P4V.js.map +1 -0
  23. package/dist/testing/failureFixtures.d.ts +2 -2
  24. package/dist/testing.mjs +1 -1
  25. package/example/package.json +1 -1
  26. package/package.json +1 -1
  27. package/dist/design-builder-CT4DHIPe.js +0 -367
  28. package/dist/design-builder-CT4DHIPe.js.map +0 -1
  29. package/dist/template-fields-C2pSfMBh.js +0 -494
  30. package/dist/template-fields-C2pSfMBh.js.map +0 -1
@@ -51,8 +51,8 @@ export { validateEmbed, collectEmbedIssues } from '../components/embed-realtime/
51
51
  export type { ValidateEmbedInput, EmbedIssue } from '../components/embed-realtime/validateEmbed.js';
52
52
  export { productToRenderConfig } from '../design/product-config.js';
53
53
  export type { ProductRenderConfig, ProductRenderConfigOptions } from '../design/product-config.js';
54
- export { design, DesignBuilder, isLayerToken } from '../design/design-builder.js';
55
- export type { Anchor, ShapeKind, DesignArtboard, StateArtboard, FromStateOptions, AnyDesign, LayerKind, LayerToken, } from '../design/design-builder.js';
54
+ export { design, DesignBuilder, isLayerToken, isColorToken } from '../design/design-builder.js';
55
+ export type { Anchor, ShapeKind, DesignArtboard, StateArtboard, FromStateOptions, AnyDesign, LayerKind, LayerToken, ColorToken, } from '../design/design-builder.js';
56
56
  export { useEditor } from '../contexts/EditorContext.js';
57
57
  export type { EditorContextValue, EditorProviderProps } from '../contexts/EditorContext.js';
58
58
  export { useLayers } from '../hooks/useLayers.js';
@@ -12,8 +12,18 @@ export interface DesignArtboard {
12
12
  width: number;
13
13
  height: number;
14
14
  /** Optional solid background fill behind all elements. Rendered everywhere
15
- * the design is — editor, exports, and server renders (realtime mockups). */
16
- backgroundColor?: string;
15
+ * the design is — editor, exports, and server renders (realtime mockups).
16
+ * NOTE: the background is not an element, so it is NOT repaintable by
17
+ * `useColorBinding` — designs that want a rebindable backdrop paint a
18
+ * full-bleed `shape('rectangle', { fill: … })` instead. */
19
+ backgroundColor?: string | ColorToken;
20
+ /** Named color ROLES for value-matched rebinding (`useColorBinding`).
21
+ * Mint tokens with `design.color('role')`; use them as `fill`/`color`
22
+ * values so role and element can never drift. Validated at mint:
23
+ * duplicate values across roles throw `E_COLOR_ROLE_COLLISION` (a
24
+ * value-matched binding can't tell colliding roles apart), non-hex
25
+ * values throw `E_COLOR_ROLE_INVALID`. */
26
+ colors?: Record<string, string>;
17
27
  /** Pacing role in a multi-page template (ADR-0086 albums). Template
18
28
  * metadata only — never sent on the server render wire. */
19
29
  role?: ArtboardRole;
@@ -71,7 +81,8 @@ export interface TextOptions extends NamedOption {
71
81
  anchor?: Anchor;
72
82
  fontSize?: number;
73
83
  fontFamily?: string;
74
- color?: string;
84
+ /** Hex string, or a `design.color('role')` token (resolved at build). */
85
+ color?: string | ColorToken;
75
86
  /** Horizontal text alignment. Named `textAlign` to match the canvas editor's
76
87
  * element field (not `align`), so the builder and the editor read the same. */
77
88
  textAlign?: 'left' | 'center' | 'right';
@@ -87,7 +98,8 @@ export interface ShapeOptions extends NamedOption {
87
98
  width: number;
88
99
  height: number;
89
100
  };
90
- fill?: string;
101
+ /** Hex string, or a `design.color('role')` token (resolved at build). */
102
+ fill?: string | ColorToken;
91
103
  rotation?: number;
92
104
  opacity?: number;
93
105
  /** star points / polygon sides. */
@@ -122,6 +134,30 @@ export interface LayerToken<K extends LayerKind = LayerKind, N extends string =
122
134
  }
123
135
  /** Runtime check used by parts before trusting a `layer` prop. */
124
136
  export declare function isLayerToken(value: unknown): value is LayerToken;
137
+ declare const COLOR_TOKEN_BRAND: unique symbol;
138
+ /**
139
+ * A validated claim that color role `N` exists in the design that minted it,
140
+ * carrying the role's (normalized) hex value. `useColorBinding` accepts it in
141
+ * place of a bare hex, so the chip and the painted elements can never drift —
142
+ * and `design({ colors })` rejects role-value collisions at mint time, which a
143
+ * value-matched binding cannot survive (one role's repaint would capture the
144
+ * other role's elements). Mint with {@link DesignBuilder.color}; not
145
+ * constructible by hand (branded).
146
+ */
147
+ export interface ColorToken<N extends string = string> {
148
+ readonly name: N;
149
+ /** Normalized uppercase `#RRGGBB`. */
150
+ readonly value: string;
151
+ /** @internal */
152
+ readonly [COLOR_TOKEN_BRAND]: true;
153
+ }
154
+ /** Runtime check used by `useColorBinding` (and the builder's value slots)
155
+ * before trusting a color value. */
156
+ export declare function isColorToken(value: unknown): value is ColorToken;
157
+ /** Normalize `#RGB`/`#RRGGBB` (any case) to uppercase `#RRGGBB` — the same
158
+ * normalization `useColorBinding` matches with. Returns null when the value
159
+ * is not a parsable hex color. */
160
+ export declare function normalizeRoleHex(color: string): string | null;
125
161
  /** The token kind inferred for name `N` against the builder's accumulated
126
162
  * per-kind name unions. A dynamically-loaded design (both unions degraded to
127
163
  * `string`) yields the full `'text' | 'image'` union — typing degrades, the
@@ -141,9 +177,11 @@ type InferKind<N extends string, TText extends string, TImage extends string> =
141
177
  * API, runtime-validated instead (the degradation is deliberate and LOUD in
142
178
  * docs; never block the dynamic case).
143
179
  */
144
- export declare class DesignBuilder<out TText extends string = string, out TImage extends string = string> {
180
+ export declare class DesignBuilder<out TText extends string = string, out TImage extends string = string, out TColor extends string = string> {
145
181
  private readonly artboard;
146
182
  private readonly els;
183
+ /** Declared color roles, name → normalized uppercase `#RRGGBB`. */
184
+ private readonly colorRoles;
147
185
  private idCounter;
148
186
  /** Every artboard of a multi-page state (fromState `{ artboards: 'all' }`);
149
187
  * unset for single-artboard builders. */
@@ -177,19 +215,19 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
177
215
  * not one union with text — ADR-0085 §1). */
178
216
  background<const N extends string>(imageUrl: string, opts: BackgroundOptions & {
179
217
  name: N;
180
- }): DesignBuilder<TText, TImage | N>;
181
- background(imageUrl: string, opts?: BackgroundOptions): DesignBuilder<TText, TImage | 'Background'>;
218
+ }): DesignBuilder<TText, TImage | N, TColor>;
219
+ background(imageUrl: string, opts?: BackgroundOptions): DesignBuilder<TText, TImage | 'Background', TColor>;
182
220
  /** An anchored image element (not full-bleed). */
183
221
  image<const N extends string>(imageUrl: string, opts: ImageOptions & {
184
222
  name: N;
185
- }): DesignBuilder<TText, TImage | N>;
186
- image(imageUrl: string, opts?: ImageOptions): DesignBuilder<TText, TImage>;
223
+ }): DesignBuilder<TText, TImage | N, TColor>;
224
+ image(imageUrl: string, opts?: ImageOptions): DesignBuilder<TText, TImage, TColor>;
187
225
  /** An anchored text layer. Horizontal centring is exact (textAlign); vertical
188
226
  * anchoring uses the font size as the line-height estimate. */
189
227
  text<const N extends string>(text: string, opts: TextOptions & {
190
228
  name: N;
191
- }): DesignBuilder<TText | N, TImage>;
192
- text(text: string, opts?: TextOptions): DesignBuilder<TText, TImage>;
229
+ }): DesignBuilder<TText | N, TImage, TColor>;
230
+ text(text: string, opts?: TextOptions): DesignBuilder<TText, TImage, TColor>;
193
231
  /**
194
232
  * Mint a {@link LayerToken} for a named layer — the only currency
195
233
  * `Editor.TextField` accepts (ADR-0085 §1). Compile time: `name` is
@@ -204,6 +242,20 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
204
242
  * is not bindable (shape/group/path).
205
243
  */
206
244
  layer<N extends TText | TImage>(name: N): LayerToken<InferKind<N, TText, TImage>, N>;
245
+ /**
246
+ * Mint a {@link ColorToken} for a declared color role. Compile time: `name`
247
+ * is constrained to the roles declared in `design({ colors })`. Runtime:
248
+ * validated against the declared roles (CMS/JSON-built designs degrade to
249
+ * `string` typing and rely on this throw).
250
+ *
251
+ * Use the token wherever the design paints the role (`fill`, `color`) AND
252
+ * as the `useColorBinding` argument — one source of truth, no hand-synced
253
+ * hex pairs.
254
+ *
255
+ * @throws EditorError `E_COLOR_ROLE_NOT_FOUND` — no role with this name;
256
+ * lists the declared roles.
257
+ */
258
+ color<N extends TColor>(name: N): ColorToken<N>;
207
259
  /** Names of bindable layers of one kind, in stack order. @internal */
208
260
  layerNamesOfKind(kind: LayerKind): string[];
209
261
  /** An anchored vector shape. */
@@ -236,7 +288,10 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
236
288
  * // …or hand d.toJSON() to <SnowconeCanvas/> for interactive editing — the
237
289
  * // `name`s let you bind layers with useImageBinding('art') / useTextBinding('headline').
238
290
  */
239
- export declare function design(artboard: DesignArtboard): DesignBuilder<never, never>;
291
+ export declare function design<const C extends Record<string, string>>(artboard: DesignArtboard & {
292
+ colors: C;
293
+ }): DesignBuilder<never, never, Extract<keyof C, string>>;
294
+ export declare function design(artboard: DesignArtboard): DesignBuilder<never, never, never>;
240
295
  /**
241
296
  * Resolve a layer's kind against a live element/config list — the mount-time
242
297
  * revalidation `Editor.TextField` runs (the token is a validated claim; the
@@ -0,0 +1,422 @@
1
+ import { g as l } from "./editorErrors-B2mb1sVN.js";
2
+ const k = 1;
3
+ function d(e) {
4
+ return typeof e == "number" && Number.isFinite(e) ? Math.round(e * 100) / 100 : e;
5
+ }
6
+ function b(e) {
7
+ return {
8
+ ...e,
9
+ ...e.x !== void 0 && { x: d(e.x) },
10
+ ...e.y !== void 0 && { y: d(e.y) },
11
+ ...e.width !== void 0 && { width: d(e.width) },
12
+ ...e.height !== void 0 && { height: d(e.height) },
13
+ ...e.rotation !== void 0 && { rotation: d(e.rotation) }
14
+ };
15
+ }
16
+ function O(e) {
17
+ var n;
18
+ const t = (n = e.artboards) != null && n.length ? e.artboards : [{ name: "Front", width: 800, height: 800 }], r = t.map(() => []);
19
+ for (const i of e.elements || []) {
20
+ const o = i == null ? void 0 : i.artboard, s = typeof o == "string" ? t.findIndex((a) => (a.name || "Front") === o) : -1;
21
+ r[s >= 0 ? s : 0].push(C(i));
22
+ }
23
+ return {
24
+ schemaVersion: k,
25
+ artboards: t.map((i, o) => ({
26
+ id: `artboard-${o + 1}`,
27
+ name: i.name || "Front",
28
+ x: 0,
29
+ y: 0,
30
+ width: d(i.width || 800),
31
+ height: d(i.height || 800),
32
+ backgroundColor: i.backgroundColor || "transparent",
33
+ // Pass the artboard's flag through — the server worker only paints the
34
+ // background when this is true. Hardcoding `false` here silently dropped
35
+ // explicitly-set artboard backgrounds from every realtime mockup while
36
+ // the editor kept showing them (WYSIWYG drift).
37
+ exportBackground: i.exportBackground ?? !1,
38
+ elements: r[o],
39
+ distressTexture: i.distressTexture,
40
+ imageMask: i.imageMask
41
+ }))
42
+ };
43
+ }
44
+ function C(e) {
45
+ const t = e.type || e.transformType;
46
+ if (t === "image")
47
+ return N(e);
48
+ const { artboard: r, ...n } = e;
49
+ return b({
50
+ ...n,
51
+ type: t
52
+ });
53
+ }
54
+ function N(e) {
55
+ var c;
56
+ const t = e.transformData || {}, r = e.masks && e.masks.length > 0, n = t.width || 0, i = t.height || 0, o = r ? n : n * (t.cropWidth ?? 1), s = r ? i : i * (t.cropHeight ?? 1), a = {
57
+ id: e.id,
58
+ type: "image",
59
+ x: e.x || 0,
60
+ y: e.y || 0,
61
+ width: o,
62
+ height: s,
63
+ rotation: e.rotation || 0,
64
+ imageUrl: e.imageUrl,
65
+ imageAspectRatio: e.imageAspectRatio
66
+ };
67
+ return !r && (t.cropX !== 0 || t.cropY !== 0 || t.cropWidth !== 1 || t.cropHeight !== 1) && (a.cropX = t.cropX, a.cropY = t.cropY, a.cropWidth = t.cropWidth, a.cropHeight = t.cropHeight, a.needsCropPixelConversion = !0), t.flipHorizontal && (a.flipHorizontal = t.flipHorizontal), t.flipVertical && (a.flipVertical = t.flipVertical), t.borderRadius && (a.borderRadius = t.borderRadius), e.opacity !== void 0 && (a.opacity = e.opacity), e.distressEffect && (a.distressEffect = e.distressEffect), ((c = e.masks) == null ? void 0 : c.length) > 0 && (a.masks = e.masks), e.blendMode && (a.blendMode = e.blendMode), e.knockoutParts && (a.knockoutParts = e.knockoutParts), e.stroke && (a.stroke = e.stroke), b(a);
68
+ }
69
+ const p = "center", y = Symbol.for("snowcone.canvas.layerToken");
70
+ function $(e) {
71
+ return typeof e == "object" && e !== null && e[y] === !0 && typeof e.name == "string" && (e.kind === "text" || e.kind === "image");
72
+ }
73
+ function E(e, t) {
74
+ return Object.freeze({ kind: e, name: t, [y]: !0 });
75
+ }
76
+ const w = Symbol.for("snowcone.canvas.colorToken");
77
+ function T(e) {
78
+ return typeof e == "object" && e !== null && e[w] === !0 && typeof e.name == "string" && typeof e.value == "string";
79
+ }
80
+ function A(e, t) {
81
+ return Object.freeze({ name: e, value: t, [w]: !0 });
82
+ }
83
+ function S(e) {
84
+ let t = e.trim().toUpperCase();
85
+ return /^#[0-9A-F]{3}$/.test(t) && (t = "#" + t[1] + t[1] + t[2] + t[2] + t[3] + t[3]), /^#[0-9A-F]{6}$/.test(t) ? t : null;
86
+ }
87
+ function f(e) {
88
+ return T(e) ? e.value : e;
89
+ }
90
+ function u(e) {
91
+ const t = e.transformType;
92
+ return t === "image" ? "image" : t === "shape" || t === "group" || t === "path" ? null : "text";
93
+ }
94
+ function _(e) {
95
+ return e === "top-left" || e === "left" || e === "bottom-left" ? "left" : e === "top-right" || e === "right" || e === "bottom-right" ? "right" : "center";
96
+ }
97
+ function v(e) {
98
+ return e === "top-left" || e === "top" || e === "top-right" ? "top" : e === "bottom-left" || e === "bottom" || e === "bottom-right" ? "bottom" : "middle";
99
+ }
100
+ function m(e, t, r, n, i, o) {
101
+ const s = _(e), a = v(e), h = s === "left" ? t / 2 + o : s === "right" ? n - t / 2 - o : n / 2, c = a === "top" ? r / 2 + o : a === "bottom" ? i - r / 2 - o : i / 2;
102
+ return { x: h, y: c };
103
+ }
104
+ function H(e, t, r) {
105
+ const n = t / r;
106
+ if (e >= n) {
107
+ const s = r * e, a = t / s;
108
+ return {
109
+ width: s,
110
+ height: r,
111
+ cropX: (1 - a) / 2,
112
+ cropY: 0,
113
+ cropWidth: a,
114
+ cropHeight: 1
115
+ };
116
+ }
117
+ const i = t / e, o = r / i;
118
+ return {
119
+ width: t,
120
+ height: i,
121
+ cropX: 0,
122
+ cropY: (1 - o) / 2,
123
+ cropWidth: 1,
124
+ cropHeight: o
125
+ };
126
+ }
127
+ class g {
128
+ constructor(t) {
129
+ if (this.els = [], this.colorRoles = /* @__PURE__ */ new Map(), this.idCounter = 0, !(t.width > 0) || !(t.height > 0))
130
+ throw new Error("design(): artboard width and height must be positive numbers");
131
+ for (const [r, n] of Object.entries(t.colors ?? {})) {
132
+ const i = S(n);
133
+ if (i === null)
134
+ throw new l(
135
+ "E_COLOR_ROLE_INVALID",
136
+ `color role "${r}" is ${JSON.stringify(n)} — roles must be #RGB/#RRGGBB hex (useColorBinding matches by normalized hex value; other syntaxes can't bind).`,
137
+ { roleName: r, value: n, docSlug: "embed-mode" }
138
+ );
139
+ const o = [...this.colorRoles.entries()].find(([, s]) => s === i);
140
+ if (o)
141
+ throw new l(
142
+ "E_COLOR_ROLE_COLLISION",
143
+ `color roles "${o[0]}" and "${r}" both normalize to ${i}. useColorBinding matches elements by VALUE, so colliding roles would repaint each other's elements — give every role a distinct hex.`,
144
+ { roles: [o[0], r], value: i, docSlug: "embed-mode" }
145
+ );
146
+ this.colorRoles.set(r, i);
147
+ }
148
+ this.artboard = {
149
+ name: t.name || "Front",
150
+ width: t.width,
151
+ height: t.height,
152
+ backgroundColor: f(t.backgroundColor),
153
+ role: t.role
154
+ };
155
+ }
156
+ /**
157
+ * Seed a builder from a previously saved canvas state — the editor shape
158
+ * `<SnowconeCanvas onChange>` emits (and `toJSON()` produces).
159
+ *
160
+ * This is how a saved editable template re-enters the turnkey embed path:
161
+ * load the template's state JSON, lift it back into a `DesignBuilder`, and
162
+ * hand it to `<RealtimeEmbed design={…}>`. By default the active artboard
163
+ * becomes the builder's artboard; elements tagged for other artboards are
164
+ * skipped (untagged elements are assumed to belong to the active one).
165
+ *
166
+ * Pass `{ artboards: 'all' }` to load a multi-page template (ADR-0086)
167
+ * whole: every artboard (with its pacing `role`) and every element are
168
+ * kept, membership tags preserved, so `toJSON()` / `toServerRequest()`
169
+ * emit all pages and `TemplateField.artboard` can scope fields per page.
170
+ */
171
+ static fromState(t, r = {}) {
172
+ const n = t.artboards.find((o) => o.name === t.activeArtboard) ?? t.artboards[0];
173
+ if (!n)
174
+ throw new Error("DesignBuilder.fromState(): state has no artboards");
175
+ const i = new g({
176
+ name: n.name,
177
+ width: n.width,
178
+ height: n.height,
179
+ backgroundColor: n.backgroundColor,
180
+ role: n.role
181
+ });
182
+ if (r.artboards === "all") {
183
+ i.allArtboards = t.artboards.map((o) => ({ ...o }));
184
+ for (const o of t.elements)
185
+ i.els.push(o.artboard ? { ...o } : { ...o, artboard: n.name });
186
+ return i;
187
+ }
188
+ for (const o of t.elements)
189
+ o.artboard && o.artboard !== n.name || i.els.push({ ...o });
190
+ return i;
191
+ }
192
+ nextId(t) {
193
+ return `${t}-${++this.idCounter}`;
194
+ }
195
+ background(t, r = {}) {
196
+ const { width: n, height: i } = this.artboard;
197
+ let o;
198
+ if (r.aspectRatio && r.aspectRatio > 0) {
199
+ const a = H(r.aspectRatio, n, i);
200
+ o = {
201
+ type: "image",
202
+ width: a.width,
203
+ height: a.height,
204
+ cropX: a.cropX,
205
+ cropY: a.cropY,
206
+ cropWidth: a.cropWidth,
207
+ cropHeight: a.cropHeight,
208
+ flipHorizontal: !1,
209
+ flipVertical: !1,
210
+ borderRadius: 0
211
+ };
212
+ } else
213
+ o = {
214
+ type: "image",
215
+ width: n,
216
+ height: i,
217
+ cropX: 0,
218
+ cropY: 0,
219
+ cropWidth: 1,
220
+ cropHeight: 1,
221
+ flipHorizontal: !1,
222
+ flipVertical: !1,
223
+ borderRadius: 0
224
+ };
225
+ const s = {
226
+ id: this.nextId("bg"),
227
+ transformType: "image",
228
+ x: n / 2,
229
+ y: i / 2,
230
+ rotation: 0,
231
+ opacity: r.opacity ?? 1,
232
+ imageUrl: t,
233
+ imageAspectRatio: r.aspectRatio,
234
+ transformData: o,
235
+ name: r.name ?? "Background"
236
+ };
237
+ return this.els.unshift(s), this;
238
+ }
239
+ image(t, r = {}) {
240
+ const { width: n, height: i } = this.artboard, o = r.width ?? Math.round(n / 3), s = r.height ?? (r.aspectRatio ? Math.round(o / r.aspectRatio) : o), { x: a, y: h } = m(r.anchor ?? p, o, s, n, i, r.padding ?? 0), c = {
241
+ id: this.nextId("img"),
242
+ transformType: "image",
243
+ x: a,
244
+ y: h,
245
+ rotation: r.rotation ?? 0,
246
+ opacity: r.opacity ?? 1,
247
+ imageUrl: t,
248
+ imageAspectRatio: r.aspectRatio,
249
+ transformData: {
250
+ type: "image",
251
+ width: o,
252
+ height: s,
253
+ cropX: 0,
254
+ cropY: 0,
255
+ cropWidth: 1,
256
+ cropHeight: 1,
257
+ flipHorizontal: !1,
258
+ flipVertical: !1,
259
+ borderRadius: 0
260
+ },
261
+ ...r.name ? { name: r.name } : {}
262
+ };
263
+ return this.els.push(c), this;
264
+ }
265
+ text(t, r = {}) {
266
+ const { width: n, height: i } = this.artboard, o = r.fontSize ?? Math.round(this.artboard.height / 12), { x: s, y: a } = m(r.anchor ?? "top", 0, o, n, i, r.padding ?? Math.round(i / 20)), h = {
267
+ id: this.nextId("text"),
268
+ transformType: "custom",
269
+ x: s,
270
+ y: a,
271
+ rotation: r.rotation ?? 0,
272
+ text: t,
273
+ fontSize: o,
274
+ fontFamily: r.fontFamily ?? "Anton",
275
+ color: f(r.color) ?? "#ffffff",
276
+ textAlign: r.textAlign ?? "center",
277
+ bold: r.bold ?? !1,
278
+ italic: r.italic ?? !1,
279
+ transformData: { type: "custom" },
280
+ ...r.name ? { name: r.name } : {}
281
+ };
282
+ return this.els.push(h), this;
283
+ }
284
+ /**
285
+ * Mint a {@link LayerToken} for a named layer — the only currency
286
+ * `Editor.TextField` accepts (ADR-0085 §1). Compile time: `name` is
287
+ * constrained to this design's declared layer names, kind inferred from
288
+ * which per-kind union it falls in. Runtime: validated EAGERLY against the
289
+ * built elements (nearest the mistake), so CMS/JSON-loaded designs get the
290
+ * same protection with `string` typing.
291
+ *
292
+ * @throws EditorError `E_LAYER_NOT_FOUND` — no layer with this name; lists
293
+ * the valid names per kind.
294
+ * @throws EditorError `E_LAYER_WRONG_KIND` — the name matches a layer that
295
+ * is not bindable (shape/group/path).
296
+ */
297
+ layer(t) {
298
+ const r = this.els.find((i) => i.name === t);
299
+ if (!r) {
300
+ const i = this.layerNamesOfKind("text"), o = this.layerNamesOfKind("image");
301
+ throw new l(
302
+ "E_LAYER_NOT_FOUND",
303
+ `no layer "${t}" in this design. Text layers: ${JSON.stringify(i)}; image layers: ${JSON.stringify(o)}. Fix the name, or add the layer first — e.g. design(…).text('…', { name: '${t}' }).`,
304
+ { layerName: t, validNames: [...i, ...o], textNames: i, imageNames: o, docSlug: "embed-mode" }
305
+ );
306
+ }
307
+ const n = u(r);
308
+ if (!n)
309
+ throw new l(
310
+ "E_LAYER_WRONG_KIND",
311
+ `layer "${t}" is a ${r.transformType} element — only text and image layers are bindable. Bindable layers: ${JSON.stringify([...this.layerNamesOfKind("text"), ...this.layerNamesOfKind("image")])}.`,
312
+ { layerName: t, actualType: r.transformType, docSlug: "embed-mode" }
313
+ );
314
+ return E(n, t);
315
+ }
316
+ /**
317
+ * Mint a {@link ColorToken} for a declared color role. Compile time: `name`
318
+ * is constrained to the roles declared in `design({ colors })`. Runtime:
319
+ * validated against the declared roles (CMS/JSON-built designs degrade to
320
+ * `string` typing and rely on this throw).
321
+ *
322
+ * Use the token wherever the design paints the role (`fill`, `color`) AND
323
+ * as the `useColorBinding` argument — one source of truth, no hand-synced
324
+ * hex pairs.
325
+ *
326
+ * @throws EditorError `E_COLOR_ROLE_NOT_FOUND` — no role with this name;
327
+ * lists the declared roles.
328
+ */
329
+ color(t) {
330
+ const r = this.colorRoles.get(t);
331
+ if (r === void 0)
332
+ throw new l(
333
+ "E_COLOR_ROLE_NOT_FOUND",
334
+ `no color role "${t}" in this design. Declared roles: ${JSON.stringify([...this.colorRoles.keys()])}. Declare roles up front — design({ …, colors: { ${t}: '#FF2D8A' } }).`,
335
+ { roleName: t, validNames: [...this.colorRoles.keys()], docSlug: "embed-mode" }
336
+ );
337
+ return A(t, r);
338
+ }
339
+ /** Names of bindable layers of one kind, in stack order. @internal */
340
+ layerNamesOfKind(t) {
341
+ return this.els.filter((r) => u(r) === t).map((r) => r.name).filter((r) => !!r);
342
+ }
343
+ /** An anchored vector shape. */
344
+ shape(t, r = {}) {
345
+ const { width: n, height: i } = this.artboard, o = r.size ?? Math.round(n / 6), s = typeof o == "number" ? o : o.width, a = typeof o == "number" ? o : o.height, { x: h, y: c } = m(r.anchor ?? p, s, a, n, i, r.padding ?? 0), R = {
346
+ type: "shape",
347
+ shapeType: t,
348
+ width: s,
349
+ height: a,
350
+ fillColor: f(r.fill) ?? "#000000",
351
+ fillOpacity: r.opacity ?? 1,
352
+ ...r.points !== void 0 ? { points: r.points } : {},
353
+ ...r.sides !== void 0 ? { sides: r.sides } : {}
354
+ }, x = {
355
+ id: this.nextId("shape"),
356
+ transformType: "shape",
357
+ x: h,
358
+ y: c,
359
+ rotation: r.rotation ?? 0,
360
+ transformData: R,
361
+ ...r.name ? { name: r.name } : {}
362
+ };
363
+ return this.els.push(x), this;
364
+ }
365
+ /** The composed elements, in render order (background first). Editor shape. */
366
+ get elements() {
367
+ return this.els.map((t) => ({ ...t }));
368
+ }
369
+ /** Editor-shape canvas state for `<SnowconeCanvas initialElements=… />` and
370
+ * `serializeStateForServer`. `activeArtboard` is the artboard name. A
371
+ * builder loaded with `fromState(…, { artboards: 'all' })` emits every
372
+ * page (artboard `role`s and element membership tags included). */
373
+ toJSON() {
374
+ return this.allArtboards ? {
375
+ // Elements added via builder methods after fromState are untagged —
376
+ // they belong to the builder's (active) artboard; stamp them so the
377
+ // multi-artboard state groups them correctly.
378
+ elements: this.els.map(
379
+ (t) => t.artboard ? { ...t } : { ...t, artboard: this.artboard.name }
380
+ ),
381
+ artboards: this.allArtboards.map((t) => ({ ...t })),
382
+ activeArtboard: this.artboard.name
383
+ } : {
384
+ elements: this.elements,
385
+ artboards: [{
386
+ name: this.artboard.name,
387
+ width: this.artboard.width,
388
+ height: this.artboard.height,
389
+ // A background set on a design is part of the design: flag it
390
+ // renderable so exports and server renders paint it, matching the
391
+ // editor (the export worker skips it without `exportBackground`).
392
+ ...this.artboard.backgroundColor ? {
393
+ backgroundColor: this.artboard.backgroundColor,
394
+ exportBackground: this.artboard.backgroundColor !== "transparent"
395
+ } : {},
396
+ ...this.artboard.role ? { role: this.artboard.role } : {}
397
+ }],
398
+ activeArtboard: this.artboard.name
399
+ };
400
+ }
401
+ /** Server-render payload for `RenderSession.renderState(placement, …)`.
402
+ * Equivalent to `serializeStateForServer(design.toJSON())`. */
403
+ toServerRequest() {
404
+ return O(this.toJSON());
405
+ }
406
+ }
407
+ function L(e) {
408
+ return new g(e);
409
+ }
410
+ function F(e) {
411
+ return u(e);
412
+ }
413
+ export {
414
+ k as C,
415
+ g as D,
416
+ T as a,
417
+ L as d,
418
+ $ as i,
419
+ F as l,
420
+ O as s
421
+ };
422
+ //# sourceMappingURL=design-builder-2VUjfmdt.js.map