@snowcone-app/canvas 0.22.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ERROR_CONTRACT.md +600 -0
- package/README.md +2 -1
- package/STYLING_CONTRACT.md +173 -93
- package/dist/{ContextualToolbars-BDEt9ND0.js → ContextualToolbars-DUhBKVxe.js} +3 -3
- package/dist/{ContextualToolbars-BDEt9ND0.js.map → ContextualToolbars-DUhBKVxe.js.map} +1 -1
- package/dist/{EditorCanvasImpl-BH_bKy-F.js → EditorCanvasImpl-BLZV_el0.js} +6 -6
- package/dist/{EditorCanvasImpl-BH_bKy-F.js.map → EditorCanvasImpl-BLZV_el0.js.map} +1 -1
- package/dist/{ImagePanel-BwqFDKvW.js → ImagePanel-sLSjaPWS.js} +3 -3
- package/dist/{ImagePanel-BwqFDKvW.js.map → ImagePanel-sLSjaPWS.js.map} +1 -1
- package/dist/{SnowconeCanvas-Cx7nXlBI.js → SnowconeCanvas-4ef4a61J.js} +6 -6
- package/dist/{SnowconeCanvas-Cx7nXlBI.js.map → SnowconeCanvas-4ef4a61J.js.map} +1 -1
- package/dist/{UnifiedFontService-CC3CSmH5.js → UnifiedFontService-Bx2MxFfy.js} +867 -834
- package/dist/{UnifiedFontService-CC3CSmH5.js.map → UnifiedFontService-Bx2MxFfy.js.map} +1 -1
- package/dist/advanced.mjs +7 -7
- package/dist/api/embed.d.ts +4 -4
- package/dist/components/editor/EditorLoadingOverlay.d.ts +12 -0
- package/dist/components/editor/EditorPlacements.d.ts +14 -0
- package/dist/components/editor/EditorPreview.d.ts +32 -8
- package/dist/components/editor/EditorRoot.d.ts +12 -0
- package/dist/components/editor/PreviewPrismOverlay.d.ts +23 -0
- package/dist/components/editor/RootInternals.d.ts +31 -0
- package/dist/components/editor/editorAudit.d.ts +66 -0
- package/dist/components/editor/index.d.ts +14 -0
- package/dist/components/editor/usePlacements.d.ts +14 -0
- package/dist/components/embed-realtime/context.d.ts +11 -2
- package/dist/components/embed-realtime/validateEmbed.d.ts +20 -1
- package/dist/{custom-icons-BW6hqmkL.js → custom-icons-DeYIrTK1.js} +7 -7
- package/dist/{custom-icons-BW6hqmkL.js.map → custom-icons-DeYIrTK1.js.map} +1 -1
- package/dist/design/design-builder.d.ts +108 -15
- package/dist/design/product-config.d.ts +10 -0
- package/dist/design-builder-U7Su9MMC.js +470 -0
- package/dist/design-builder-U7Su9MMC.js.map +1 -0
- package/dist/editorErrors-B2mb1sVN.js.map +1 -1
- package/dist/embed-DXGydZOT.js +1586 -0
- package/dist/embed-DXGydZOT.js.map +1 -0
- package/dist/embed.mjs +34 -31
- package/dist/errors/editorError.d.ts +27 -1
- package/dist/hooks/bindingGuards.d.ts +16 -0
- package/dist/hooks/useColorBinding.d.ts +2 -1
- package/dist/hooks/useFontBinding.d.ts +2 -1
- package/dist/hooks/useImageBinding.d.ts +2 -1
- package/dist/hooks/useTextBinding.d.ts +2 -1
- package/dist/index.mjs +6 -6
- package/dist/style.css +1 -1
- package/dist/template-fields-DJ157D3E.js +548 -0
- package/dist/template-fields-DJ157D3E.js.map +1 -0
- package/dist/testing/failureFixtures.d.ts +2 -2
- package/dist/testing.mjs +1 -1
- package/dist/{validation-C9Fe9ahK.js → validation-DnAkXdUg.js} +7 -7
- package/dist/{validation-C9Fe9ahK.js.map → validation-DnAkXdUg.js.map} +1 -1
- package/example/package.json +1 -1
- package/llms.txt +3 -0
- package/package.json +6 -3
- package/dist/design-builder-CT4DHIPe.js +0 -367
- package/dist/design-builder-CT4DHIPe.js.map +0 -1
- package/dist/embed-8ecUMMH-.js +0 -1226
- package/dist/embed-8ecUMMH-.js.map +0 -1
- package/dist/template-fields-C2pSfMBh.js +0 -494
- package/dist/template-fields-C2pSfMBh.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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,12 +177,21 @@ 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> {
|
|
145
|
-
|
|
180
|
+
export declare class DesignBuilder<out TText extends string = string, out TImage extends string = string, out TColor extends string = string> {
|
|
181
|
+
/** The CURRENT authoring artboard — anchor math and new elements target it.
|
|
182
|
+
* Reassigned by {@link DesignBuilder.artboard}; the initially-active
|
|
183
|
+
* placement stays {@link activeName}. */
|
|
184
|
+
private current;
|
|
185
|
+
/** The placement that is active when the design mounts — always the FIRST
|
|
186
|
+
* artboard (or the saved state's active one), never moved by `.artboard()`. */
|
|
187
|
+
private readonly activeName;
|
|
146
188
|
private readonly els;
|
|
189
|
+
/** Declared color roles, name → normalized uppercase `#RRGGBB`. */
|
|
190
|
+
private readonly colorRoles;
|
|
147
191
|
private idCounter;
|
|
148
|
-
/** Every artboard of a multi-
|
|
149
|
-
* unset for single-artboard
|
|
192
|
+
/** Every artboard of a multi-artboard design — set by `fromState({ artboards:
|
|
193
|
+
* 'all' })` or the first `.artboard()` call; unset for single-artboard
|
|
194
|
+
* builders. */
|
|
150
195
|
private allArtboards?;
|
|
151
196
|
constructor(artboard: DesignArtboard);
|
|
152
197
|
/**
|
|
@@ -170,6 +215,37 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
|
|
|
170
215
|
activeArtboard: string;
|
|
171
216
|
}, options?: FromStateOptions): DesignBuilder;
|
|
172
217
|
private nextId;
|
|
218
|
+
/** A wire-shape snapshot of an authoring artboard, applying the same
|
|
219
|
+
* background-export rule everywhere (a design background is part of the
|
|
220
|
+
* design — exports and server renders paint it). */
|
|
221
|
+
private static snapshotArtboard;
|
|
222
|
+
/**
|
|
223
|
+
* Open a NEW artboard (placement) and make it the authoring context: layer
|
|
224
|
+
* calls after this target the new artboard — anchor math uses its
|
|
225
|
+
* dimensions, and the elements carry its membership tag (the public
|
|
226
|
+
* `artboard` field, PR #482). The FIRST artboard (from `design({ name })`)
|
|
227
|
+
* remains the initially-active placement when the design mounts; switch at
|
|
228
|
+
* runtime with `usePlacements()` / `<Editor.Placements/>` (ADR-0088).
|
|
229
|
+
*
|
|
230
|
+
* `label` must be the product's placement label (case-sensitive) — the same
|
|
231
|
+
* artboard-name-is-placement-label rule as `design({ name })`.
|
|
232
|
+
*
|
|
233
|
+
* ```ts
|
|
234
|
+
* const seed = design({ name: 'Front', width: 1480, height: 2328 })
|
|
235
|
+
* .text('YOUR NAME', { anchor: 'top', name: 'headline' })
|
|
236
|
+
* .artboard('Back', { width: 1480, height: 2328 })
|
|
237
|
+
* .text('EST. 2026', { anchor: 'center', name: 'backline' });
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @throws EditorError `E_DUPLICATE_ARTBOARD` — the design already has an
|
|
241
|
+
* artboard with this label.
|
|
242
|
+
*/
|
|
243
|
+
artboard(label: string, dims: {
|
|
244
|
+
width: number;
|
|
245
|
+
height: number;
|
|
246
|
+
backgroundColor?: string | ColorToken;
|
|
247
|
+
role?: ArtboardRole;
|
|
248
|
+
}): this;
|
|
173
249
|
/** Full-bleed background image (fills the whole artboard). Cover-fit (no
|
|
174
250
|
* distortion) when `aspectRatio` is supplied. Inserted at the bottom of the
|
|
175
251
|
* stack regardless of call order. The default layer name `'Background'`
|
|
@@ -177,19 +253,19 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
|
|
|
177
253
|
* not one union with text — ADR-0085 §1). */
|
|
178
254
|
background<const N extends string>(imageUrl: string, opts: BackgroundOptions & {
|
|
179
255
|
name: N;
|
|
180
|
-
}): DesignBuilder<TText, TImage | N>;
|
|
181
|
-
background(imageUrl: string, opts?: BackgroundOptions): DesignBuilder<TText, TImage | 'Background'>;
|
|
256
|
+
}): DesignBuilder<TText, TImage | N, TColor>;
|
|
257
|
+
background(imageUrl: string, opts?: BackgroundOptions): DesignBuilder<TText, TImage | 'Background', TColor>;
|
|
182
258
|
/** An anchored image element (not full-bleed). */
|
|
183
259
|
image<const N extends string>(imageUrl: string, opts: ImageOptions & {
|
|
184
260
|
name: N;
|
|
185
|
-
}): DesignBuilder<TText, TImage | N>;
|
|
186
|
-
image(imageUrl: string, opts?: ImageOptions): DesignBuilder<TText, TImage>;
|
|
261
|
+
}): DesignBuilder<TText, TImage | N, TColor>;
|
|
262
|
+
image(imageUrl: string, opts?: ImageOptions): DesignBuilder<TText, TImage, TColor>;
|
|
187
263
|
/** An anchored text layer. Horizontal centring is exact (textAlign); vertical
|
|
188
264
|
* anchoring uses the font size as the line-height estimate. */
|
|
189
265
|
text<const N extends string>(text: string, opts: TextOptions & {
|
|
190
266
|
name: N;
|
|
191
|
-
}): DesignBuilder<TText | N, TImage>;
|
|
192
|
-
text(text: string, opts?: TextOptions): DesignBuilder<TText, TImage>;
|
|
267
|
+
}): DesignBuilder<TText | N, TImage, TColor>;
|
|
268
|
+
text(text: string, opts?: TextOptions): DesignBuilder<TText, TImage, TColor>;
|
|
193
269
|
/**
|
|
194
270
|
* Mint a {@link LayerToken} for a named layer — the only currency
|
|
195
271
|
* `Editor.TextField` accepts (ADR-0085 §1). Compile time: `name` is
|
|
@@ -204,6 +280,20 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
|
|
|
204
280
|
* is not bindable (shape/group/path).
|
|
205
281
|
*/
|
|
206
282
|
layer<N extends TText | TImage>(name: N): LayerToken<InferKind<N, TText, TImage>, N>;
|
|
283
|
+
/**
|
|
284
|
+
* Mint a {@link ColorToken} for a declared color role. Compile time: `name`
|
|
285
|
+
* is constrained to the roles declared in `design({ colors })`. Runtime:
|
|
286
|
+
* validated against the declared roles (CMS/JSON-built designs degrade to
|
|
287
|
+
* `string` typing and rely on this throw).
|
|
288
|
+
*
|
|
289
|
+
* Use the token wherever the design paints the role (`fill`, `color`) AND
|
|
290
|
+
* as the `useColorBinding` argument — one source of truth, no hand-synced
|
|
291
|
+
* hex pairs.
|
|
292
|
+
*
|
|
293
|
+
* @throws EditorError `E_COLOR_ROLE_NOT_FOUND` — no role with this name;
|
|
294
|
+
* lists the declared roles.
|
|
295
|
+
*/
|
|
296
|
+
color<N extends TColor>(name: N): ColorToken<N>;
|
|
207
297
|
/** Names of bindable layers of one kind, in stack order. @internal */
|
|
208
298
|
layerNamesOfKind(kind: LayerKind): string[];
|
|
209
299
|
/** An anchored vector shape. */
|
|
@@ -236,7 +326,10 @@ export declare class DesignBuilder<out TText extends string = string, out TImage
|
|
|
236
326
|
* // …or hand d.toJSON() to <SnowconeCanvas/> for interactive editing — the
|
|
237
327
|
* // `name`s let you bind layers with useImageBinding('art') / useTextBinding('headline').
|
|
238
328
|
*/
|
|
239
|
-
export declare function design(artboard: DesignArtboard
|
|
329
|
+
export declare function design<const C extends Record<string, string>>(artboard: DesignArtboard & {
|
|
330
|
+
colors: C;
|
|
331
|
+
}): DesignBuilder<never, never, Extract<keyof C, string>>;
|
|
332
|
+
export declare function design(artboard: DesignArtboard): DesignBuilder<never, never, never>;
|
|
240
333
|
/**
|
|
241
334
|
* Resolve a layer's kind against a live element/config list — the mount-time
|
|
242
335
|
* revalidation `Editor.TextField` runs (the token is a validated claim; the
|
|
@@ -23,6 +23,16 @@ export interface ProductRenderConfig {
|
|
|
23
23
|
};
|
|
24
24
|
/** Every editable (non-color) placement label — placement pickers. */
|
|
25
25
|
placementLabels: string[];
|
|
26
|
+
/** Geometry of EVERY renderable image placement, in catalog order — seed a
|
|
27
|
+
* multi-placement design mechanically (ADR-0088):
|
|
28
|
+
* `config.placements.reduce((d, p) => d.artboard(p.label, p), …)`. The
|
|
29
|
+
* entry matching `placement` is the same object shape. */
|
|
30
|
+
placements: Array<{
|
|
31
|
+
label: string;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
aspectRatio: number;
|
|
35
|
+
}>;
|
|
26
36
|
/** First mockup scene that photographs the rendered variant — the default
|
|
27
37
|
* `<Editor.Preview mockupId>`. */
|
|
28
38
|
primaryMockupId: string;
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { g as l } from "./editorErrors-B2mb1sVN.js";
|
|
2
|
+
const x = 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(A(i));
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: x,
|
|
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 A(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 z(e) {
|
|
71
|
+
return typeof e == "object" && e !== null && e[y] === !0 && typeof e.name == "string" && (e.kind === "text" || e.kind === "image");
|
|
72
|
+
}
|
|
73
|
+
function C(e, t) {
|
|
74
|
+
return Object.freeze({ kind: e, name: t, [y]: !0 });
|
|
75
|
+
}
|
|
76
|
+
const w = Symbol.for("snowcone.canvas.colorToken");
|
|
77
|
+
function E(e) {
|
|
78
|
+
return typeof e == "object" && e !== null && e[w] === !0 && typeof e.name == "string" && typeof e.value == "string";
|
|
79
|
+
}
|
|
80
|
+
function T(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 m(e) {
|
|
88
|
+
return E(e) ? e.value : e;
|
|
89
|
+
}
|
|
90
|
+
function g(e) {
|
|
91
|
+
const t = e.transformType;
|
|
92
|
+
return t === "image" ? "image" : t === "shape" || t === "group" || t === "path" ? null : "text";
|
|
93
|
+
}
|
|
94
|
+
function v(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 _(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 u(e, t, r, n, i, o) {
|
|
101
|
+
const s = v(e), a = _(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 $(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 f {
|
|
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.current = {
|
|
149
|
+
name: t.name || "Front",
|
|
150
|
+
width: t.width,
|
|
151
|
+
height: t.height,
|
|
152
|
+
backgroundColor: m(t.backgroundColor),
|
|
153
|
+
role: t.role
|
|
154
|
+
}, this.activeName = this.current.name;
|
|
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 f({
|
|
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
|
+
/** A wire-shape snapshot of an authoring artboard, applying the same
|
|
196
|
+
* background-export rule everywhere (a design background is part of the
|
|
197
|
+
* design — exports and server renders paint it). */
|
|
198
|
+
static snapshotArtboard(t) {
|
|
199
|
+
return {
|
|
200
|
+
name: t.name,
|
|
201
|
+
width: t.width,
|
|
202
|
+
height: t.height,
|
|
203
|
+
...t.backgroundColor ? {
|
|
204
|
+
backgroundColor: t.backgroundColor,
|
|
205
|
+
exportBackground: t.backgroundColor !== "transparent"
|
|
206
|
+
} : {},
|
|
207
|
+
...t.role ? { role: t.role } : {}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Open a NEW artboard (placement) and make it the authoring context: layer
|
|
212
|
+
* calls after this target the new artboard — anchor math uses its
|
|
213
|
+
* dimensions, and the elements carry its membership tag (the public
|
|
214
|
+
* `artboard` field, PR #482). The FIRST artboard (from `design({ name })`)
|
|
215
|
+
* remains the initially-active placement when the design mounts; switch at
|
|
216
|
+
* runtime with `usePlacements()` / `<Editor.Placements/>` (ADR-0088).
|
|
217
|
+
*
|
|
218
|
+
* `label` must be the product's placement label (case-sensitive) — the same
|
|
219
|
+
* artboard-name-is-placement-label rule as `design({ name })`.
|
|
220
|
+
*
|
|
221
|
+
* ```ts
|
|
222
|
+
* const seed = design({ name: 'Front', width: 1480, height: 2328 })
|
|
223
|
+
* .text('YOUR NAME', { anchor: 'top', name: 'headline' })
|
|
224
|
+
* .artboard('Back', { width: 1480, height: 2328 })
|
|
225
|
+
* .text('EST. 2026', { anchor: 'center', name: 'backline' });
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @throws EditorError `E_DUPLICATE_ARTBOARD` — the design already has an
|
|
229
|
+
* artboard with this label.
|
|
230
|
+
*/
|
|
231
|
+
artboard(t, r) {
|
|
232
|
+
if (!(r.width > 0) || !(r.height > 0))
|
|
233
|
+
throw new Error("design().artboard(): width and height must be positive numbers");
|
|
234
|
+
if (this.allArtboards || (this.allArtboards = [f.snapshotArtboard(this.current)]), this.allArtboards.some((n) => n.name === t))
|
|
235
|
+
throw new l(
|
|
236
|
+
"E_DUPLICATE_ARTBOARD",
|
|
237
|
+
`the design already has an artboard "${t}" — artboard names are placement labels and must be unique. Existing: ${JSON.stringify(this.allArtboards.map((n) => n.name))}.`,
|
|
238
|
+
{ artboardName: t, validNames: this.allArtboards.map((n) => n.name), docSlug: "embed-mode" }
|
|
239
|
+
);
|
|
240
|
+
for (const n of this.els)
|
|
241
|
+
n.artboard || (n.artboard = this.current.name);
|
|
242
|
+
return this.current = {
|
|
243
|
+
name: t,
|
|
244
|
+
width: r.width,
|
|
245
|
+
height: r.height,
|
|
246
|
+
backgroundColor: m(r.backgroundColor),
|
|
247
|
+
role: r.role
|
|
248
|
+
}, this.allArtboards.push(f.snapshotArtboard(this.current)), this;
|
|
249
|
+
}
|
|
250
|
+
background(t, r = {}) {
|
|
251
|
+
const { width: n, height: i } = this.current;
|
|
252
|
+
let o;
|
|
253
|
+
if (r.aspectRatio && r.aspectRatio > 0) {
|
|
254
|
+
const a = $(r.aspectRatio, n, i);
|
|
255
|
+
o = {
|
|
256
|
+
type: "image",
|
|
257
|
+
width: a.width,
|
|
258
|
+
height: a.height,
|
|
259
|
+
cropX: a.cropX,
|
|
260
|
+
cropY: a.cropY,
|
|
261
|
+
cropWidth: a.cropWidth,
|
|
262
|
+
cropHeight: a.cropHeight,
|
|
263
|
+
flipHorizontal: !1,
|
|
264
|
+
flipVertical: !1,
|
|
265
|
+
borderRadius: 0
|
|
266
|
+
};
|
|
267
|
+
} else
|
|
268
|
+
o = {
|
|
269
|
+
type: "image",
|
|
270
|
+
width: n,
|
|
271
|
+
height: i,
|
|
272
|
+
cropX: 0,
|
|
273
|
+
cropY: 0,
|
|
274
|
+
cropWidth: 1,
|
|
275
|
+
cropHeight: 1,
|
|
276
|
+
flipHorizontal: !1,
|
|
277
|
+
flipVertical: !1,
|
|
278
|
+
borderRadius: 0
|
|
279
|
+
};
|
|
280
|
+
const s = {
|
|
281
|
+
id: this.nextId("bg"),
|
|
282
|
+
transformType: "image",
|
|
283
|
+
x: n / 2,
|
|
284
|
+
y: i / 2,
|
|
285
|
+
rotation: 0,
|
|
286
|
+
opacity: r.opacity ?? 1,
|
|
287
|
+
imageUrl: t,
|
|
288
|
+
imageAspectRatio: r.aspectRatio,
|
|
289
|
+
transformData: o,
|
|
290
|
+
name: r.name ?? "Background"
|
|
291
|
+
};
|
|
292
|
+
return this.els.unshift(s), this;
|
|
293
|
+
}
|
|
294
|
+
image(t, r = {}) {
|
|
295
|
+
const { width: n, height: i } = this.current, o = r.width ?? Math.round(n / 3), s = r.height ?? (r.aspectRatio ? Math.round(o / r.aspectRatio) : o), { x: a, y: h } = u(r.anchor ?? p, o, s, n, i, r.padding ?? 0), c = {
|
|
296
|
+
id: this.nextId("img"),
|
|
297
|
+
transformType: "image",
|
|
298
|
+
x: a,
|
|
299
|
+
y: h,
|
|
300
|
+
rotation: r.rotation ?? 0,
|
|
301
|
+
opacity: r.opacity ?? 1,
|
|
302
|
+
imageUrl: t,
|
|
303
|
+
imageAspectRatio: r.aspectRatio,
|
|
304
|
+
transformData: {
|
|
305
|
+
type: "image",
|
|
306
|
+
width: o,
|
|
307
|
+
height: s,
|
|
308
|
+
cropX: 0,
|
|
309
|
+
cropY: 0,
|
|
310
|
+
cropWidth: 1,
|
|
311
|
+
cropHeight: 1,
|
|
312
|
+
flipHorizontal: !1,
|
|
313
|
+
flipVertical: !1,
|
|
314
|
+
borderRadius: 0
|
|
315
|
+
},
|
|
316
|
+
...r.name ? { name: r.name } : {}
|
|
317
|
+
};
|
|
318
|
+
return this.els.push(c), this;
|
|
319
|
+
}
|
|
320
|
+
text(t, r = {}) {
|
|
321
|
+
const { width: n, height: i } = this.current, o = r.fontSize ?? Math.round(this.current.height / 12), { x: s, y: a } = u(r.anchor ?? "top", 0, o, n, i, r.padding ?? Math.round(i / 20)), h = {
|
|
322
|
+
id: this.nextId("text"),
|
|
323
|
+
transformType: "custom",
|
|
324
|
+
x: s,
|
|
325
|
+
y: a,
|
|
326
|
+
rotation: r.rotation ?? 0,
|
|
327
|
+
text: t,
|
|
328
|
+
fontSize: o,
|
|
329
|
+
fontFamily: r.fontFamily ?? "Anton",
|
|
330
|
+
color: m(r.color) ?? "#ffffff",
|
|
331
|
+
textAlign: r.textAlign ?? "center",
|
|
332
|
+
bold: r.bold ?? !1,
|
|
333
|
+
italic: r.italic ?? !1,
|
|
334
|
+
transformData: { type: "custom" },
|
|
335
|
+
...r.name ? { name: r.name } : {}
|
|
336
|
+
};
|
|
337
|
+
return this.els.push(h), this;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Mint a {@link LayerToken} for a named layer — the only currency
|
|
341
|
+
* `Editor.TextField` accepts (ADR-0085 §1). Compile time: `name` is
|
|
342
|
+
* constrained to this design's declared layer names, kind inferred from
|
|
343
|
+
* which per-kind union it falls in. Runtime: validated EAGERLY against the
|
|
344
|
+
* built elements (nearest the mistake), so CMS/JSON-loaded designs get the
|
|
345
|
+
* same protection with `string` typing.
|
|
346
|
+
*
|
|
347
|
+
* @throws EditorError `E_LAYER_NOT_FOUND` — no layer with this name; lists
|
|
348
|
+
* the valid names per kind.
|
|
349
|
+
* @throws EditorError `E_LAYER_WRONG_KIND` — the name matches a layer that
|
|
350
|
+
* is not bindable (shape/group/path).
|
|
351
|
+
*/
|
|
352
|
+
layer(t) {
|
|
353
|
+
const r = this.els.find((i) => i.name === t);
|
|
354
|
+
if (!r) {
|
|
355
|
+
const i = this.layerNamesOfKind("text"), o = this.layerNamesOfKind("image");
|
|
356
|
+
throw new l(
|
|
357
|
+
"E_LAYER_NOT_FOUND",
|
|
358
|
+
`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}' }).`,
|
|
359
|
+
{ layerName: t, validNames: [...i, ...o], textNames: i, imageNames: o, docSlug: "embed-mode" }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const n = g(r);
|
|
363
|
+
if (!n)
|
|
364
|
+
throw new l(
|
|
365
|
+
"E_LAYER_WRONG_KIND",
|
|
366
|
+
`layer "${t}" is a ${r.transformType} element — only text and image layers are bindable. Bindable layers: ${JSON.stringify([...this.layerNamesOfKind("text"), ...this.layerNamesOfKind("image")])}.`,
|
|
367
|
+
{ layerName: t, actualType: r.transformType, docSlug: "embed-mode" }
|
|
368
|
+
);
|
|
369
|
+
return C(n, t);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Mint a {@link ColorToken} for a declared color role. Compile time: `name`
|
|
373
|
+
* is constrained to the roles declared in `design({ colors })`. Runtime:
|
|
374
|
+
* validated against the declared roles (CMS/JSON-built designs degrade to
|
|
375
|
+
* `string` typing and rely on this throw).
|
|
376
|
+
*
|
|
377
|
+
* Use the token wherever the design paints the role (`fill`, `color`) AND
|
|
378
|
+
* as the `useColorBinding` argument — one source of truth, no hand-synced
|
|
379
|
+
* hex pairs.
|
|
380
|
+
*
|
|
381
|
+
* @throws EditorError `E_COLOR_ROLE_NOT_FOUND` — no role with this name;
|
|
382
|
+
* lists the declared roles.
|
|
383
|
+
*/
|
|
384
|
+
color(t) {
|
|
385
|
+
const r = this.colorRoles.get(t);
|
|
386
|
+
if (r === void 0)
|
|
387
|
+
throw new l(
|
|
388
|
+
"E_COLOR_ROLE_NOT_FOUND",
|
|
389
|
+
`no color role "${t}" in this design. Declared roles: ${JSON.stringify([...this.colorRoles.keys()])}. Declare roles up front — design({ …, colors: { ${t}: '#FF2D8A' } }).`,
|
|
390
|
+
{ roleName: t, validNames: [...this.colorRoles.keys()], docSlug: "embed-mode" }
|
|
391
|
+
);
|
|
392
|
+
return T(t, r);
|
|
393
|
+
}
|
|
394
|
+
/** Names of bindable layers of one kind, in stack order. @internal */
|
|
395
|
+
layerNamesOfKind(t) {
|
|
396
|
+
return this.els.filter((r) => g(r) === t).map((r) => r.name).filter((r) => !!r);
|
|
397
|
+
}
|
|
398
|
+
/** An anchored vector shape. */
|
|
399
|
+
shape(t, r = {}) {
|
|
400
|
+
const { width: n, height: i } = this.current, 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 } = u(r.anchor ?? p, s, a, n, i, r.padding ?? 0), R = {
|
|
401
|
+
type: "shape",
|
|
402
|
+
shapeType: t,
|
|
403
|
+
width: s,
|
|
404
|
+
height: a,
|
|
405
|
+
fillColor: m(r.fill) ?? "#000000",
|
|
406
|
+
fillOpacity: r.opacity ?? 1,
|
|
407
|
+
...r.points !== void 0 ? { points: r.points } : {},
|
|
408
|
+
...r.sides !== void 0 ? { sides: r.sides } : {}
|
|
409
|
+
}, k = {
|
|
410
|
+
id: this.nextId("shape"),
|
|
411
|
+
transformType: "shape",
|
|
412
|
+
x: h,
|
|
413
|
+
y: c,
|
|
414
|
+
rotation: r.rotation ?? 0,
|
|
415
|
+
transformData: R,
|
|
416
|
+
...r.name ? { name: r.name } : {}
|
|
417
|
+
};
|
|
418
|
+
return this.els.push(k), this;
|
|
419
|
+
}
|
|
420
|
+
/** The composed elements, in render order (background first). Editor shape. */
|
|
421
|
+
get elements() {
|
|
422
|
+
return this.els.map((t) => ({ ...t }));
|
|
423
|
+
}
|
|
424
|
+
/** Editor-shape canvas state for `<SnowconeCanvas initialElements=… />` and
|
|
425
|
+
* `serializeStateForServer`. `activeArtboard` is the artboard name. A
|
|
426
|
+
* builder loaded with `fromState(…, { artboards: 'all' })` emits every
|
|
427
|
+
* page (artboard `role`s and element membership tags included). */
|
|
428
|
+
toJSON() {
|
|
429
|
+
return this.allArtboards ? {
|
|
430
|
+
// Elements added via builder methods since the last context switch are
|
|
431
|
+
// untagged — they belong to the CURRENT authoring artboard; stamp them
|
|
432
|
+
// so the multi-artboard state groups them correctly.
|
|
433
|
+
elements: this.els.map(
|
|
434
|
+
(t) => t.artboard ? { ...t } : { ...t, artboard: this.current.name }
|
|
435
|
+
),
|
|
436
|
+
artboards: this.allArtboards.map((t) => ({ ...t })),
|
|
437
|
+
// The initially-active placement (first artboard / saved state's
|
|
438
|
+
// active) — NOT the last `.artboard()` authoring context.
|
|
439
|
+
activeArtboard: this.activeName
|
|
440
|
+
} : {
|
|
441
|
+
elements: this.elements,
|
|
442
|
+
// A background set on a design is part of the design: flag it renderable
|
|
443
|
+
// so exports and server renders paint it, matching the editor (the
|
|
444
|
+
// export worker skips it without `exportBackground`) — see snapshotArtboard.
|
|
445
|
+
artboards: [f.snapshotArtboard(this.current)],
|
|
446
|
+
activeArtboard: this.current.name
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/** Server-render payload for `RenderSession.renderState(placement, …)`.
|
|
450
|
+
* Equivalent to `serializeStateForServer(design.toJSON())`. */
|
|
451
|
+
toServerRequest() {
|
|
452
|
+
return O(this.toJSON());
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function L(e) {
|
|
456
|
+
return new f(e);
|
|
457
|
+
}
|
|
458
|
+
function F(e) {
|
|
459
|
+
return g(e);
|
|
460
|
+
}
|
|
461
|
+
export {
|
|
462
|
+
x as C,
|
|
463
|
+
f as D,
|
|
464
|
+
E as a,
|
|
465
|
+
L as d,
|
|
466
|
+
z as i,
|
|
467
|
+
F as l,
|
|
468
|
+
O as s
|
|
469
|
+
};
|
|
470
|
+
//# sourceMappingURL=design-builder-U7Su9MMC.js.map
|