@snowcone-app/canvas 0.18.1 → 0.19.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.
- package/dist/{EditorCanvasImpl-Bv4yagew.js → EditorCanvasImpl-_3xCnLkx.js} +4 -4
- package/dist/{EditorCanvasImpl-Bv4yagew.js.map → EditorCanvasImpl-_3xCnLkx.js.map} +1 -1
- package/dist/EditorContext-5QpqAhjh.js.map +1 -1
- package/dist/{SnowconeCanvas-C_O-aeaQ.js → SnowconeCanvas-3PA17na3.js} +2 -2
- package/dist/{SnowconeCanvas-C_O-aeaQ.js.map → SnowconeCanvas-3PA17na3.js.map} +1 -1
- package/dist/api/embed.d.ts +2 -2
- package/dist/api/testing.d.ts +3 -0
- package/dist/components/editor/EditorBuy.d.ts +16 -5
- package/dist/components/editor/RootInternals.d.ts +5 -0
- package/dist/components/editor/commerceStore.d.ts +68 -0
- package/dist/components/editor/index.d.ts +4 -0
- package/dist/components/editor/useProductSelection.d.ts +39 -0
- package/dist/design-builder-v4935TeS.js +343 -0
- package/dist/design-builder-v4935TeS.js.map +1 -0
- package/dist/embed-D2QOpJXG.js +1090 -0
- package/dist/embed-D2QOpJXG.js.map +1 -0
- package/dist/embed.mjs +27 -25
- package/dist/embed.mjs.map +1 -1
- package/dist/errors/editorError.d.ts +15 -1
- package/dist/index.mjs +40 -39
- package/dist/index.mjs.map +1 -1
- package/dist/template-fields-Dr3fMItw.js +329 -0
- package/dist/template-fields-Dr3fMItw.js.map +1 -0
- package/dist/testing/assertRenders.d.ts +53 -0
- package/dist/testing/failureFixtures.d.ts +25 -0
- package/dist/testing.mjs +191 -80
- package/dist/testing.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/embed-Cdbjay9i.js +0 -961
- package/dist/embed-Cdbjay9i.js.map +0 -1
- package/dist/template-fields-DgoBtajp.js +0 -668
- package/dist/template-fields-DgoBtajp.js.map +0 -1
package/dist/api/embed.d.ts
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
*
|
|
35
35
|
* @packageDocumentation
|
|
36
36
|
*/
|
|
37
|
-
export { Editor, DesignStateStore } from '../components/editor/index.js';
|
|
38
|
-
export type { EditorRootProps, EditorRootBaseProps, EditorCanvasProps, EditorPreviewProps, EditorTextFieldProps, EditorBuyProps, EditorBuyPayload, BuyMissingReason, DesignState, EditorLayersProps, EditorControlsProps, EditorHistoryProps, EditorMenuProps, EditorAddProps, } from '../components/editor/index.js';
|
|
37
|
+
export { Editor, DesignStateStore, useProductSelection } from '../components/editor/index.js';
|
|
38
|
+
export type { EditorRootProps, EditorRootBaseProps, EditorCanvasProps, EditorPreviewProps, EditorTextFieldProps, EditorBuyProps, EditorBuyPayload, BuyMissingReason, DesignState, EditorLayersProps, EditorControlsProps, EditorHistoryProps, EditorMenuProps, EditorAddProps, ProductSelectionApi, ProductOptionChoice, ProductSelectionAttribute, } from '../components/editor/index.js';
|
|
39
39
|
export { EditorError, isEditorError } from '../errors/editorError.js';
|
|
40
40
|
export type { EditorErrorCode, EditorErrorData } from '../errors/editorError.js';
|
|
41
41
|
export { RealtimeEmbed } from '../components/embed-realtime/RealtimeEmbed.js';
|
package/dist/api/testing.d.ts
CHANGED
|
@@ -24,3 +24,6 @@
|
|
|
24
24
|
export { MockEditorProvider, MockEditorProviderWithState, createMockElement, createMockImageElement, createMockArtboard, waitFor, waitForExport, simulateKeyPress, createMockCanvas, assertElementProperties, getElementBounds, } from '../testing/index.js';
|
|
25
25
|
export type { MockEditorProviderProps, MockEditorElement, } from '../testing/index.js';
|
|
26
26
|
export { expectNoEditorErrors, getContainedEditorErrors, resetContainedEditorErrors, } from '../testing/editorErrors.js';
|
|
27
|
+
export { assertRenders, mutateNamedTextLayers } from '../testing/assertRenders.js';
|
|
28
|
+
export type { AssertRendersOptions, AssertRendersResult, RenderableDesign } from '../testing/assertRenders.js';
|
|
29
|
+
export { test403Design, testIncompleteDesign, failingGrant, TEST_403_ASSET_URL, } from '../testing/failureFixtures.js';
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { default as React, CSSProperties, ReactNode } from 'react';
|
|
2
|
-
import { CartDetail,
|
|
2
|
+
import { CartDetail, OptionSelection } from '@snowcone-app/sdk';
|
|
3
3
|
import { ServerRenderRequest } from '../../rendering/serialize-for-server.js';
|
|
4
4
|
import { DesignState } from './designStateStore.js';
|
|
5
5
|
/** A structured incompleteness reason — also serialized (comma-joined) into
|
|
6
6
|
* the button's `data-sc-buy-missing` attribute for agents and styling. */
|
|
7
|
-
export type BuyMissingReason = 'variant' | `layer:${string}` | `layer-seed:${string}` | 'empty-design' | 'quantity';
|
|
7
|
+
export type BuyMissingReason = `option:${string}` | 'variant' | `layer:${string}` | `layer-seed:${string}` | 'empty-design' | 'quantity';
|
|
8
8
|
export interface EditorBuyPayload {
|
|
9
9
|
productId: string;
|
|
10
|
+
/** The variant (gvid) the selection resolves to. */
|
|
10
11
|
variantId?: string;
|
|
11
12
|
quantity: number;
|
|
13
|
+
/** The variant choices at click time, e.g. `{ Color: 'Black', Size: 'M' }`. */
|
|
14
|
+
selection: OptionSelection;
|
|
12
15
|
/** SDK cart detail (variant resolution + price) when the product carries
|
|
13
16
|
* catalog `options`; `null` for option-less products. */
|
|
14
17
|
cartDetail: CartDetail | null;
|
|
@@ -22,8 +25,13 @@ export interface EditorBuyProps {
|
|
|
22
25
|
/** The final submit — the host's one line. Everything before it (variant,
|
|
23
26
|
* quantity, completeness, payload assembly) is this part's job. */
|
|
24
27
|
onAddToCart: (payload: EditorBuyPayload) => void | Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Seeds the Buy-owned quantity once on mount (integer ≥ 1; the store
|
|
30
|
+
* defaults to 1). Live reads/writes go through `useProductSelection()` —
|
|
31
|
+
* this is deliberately NOT a controlled prop, so two Buy parts under one
|
|
32
|
+
* Root can never disagree about quantity.
|
|
33
|
+
*/
|
|
34
|
+
defaultQuantity?: number;
|
|
27
35
|
/** Button label. Defaults to "Add to cart". */
|
|
28
36
|
children?: ReactNode;
|
|
29
37
|
className?: string;
|
|
@@ -31,8 +39,11 @@ export interface EditorBuyProps {
|
|
|
31
39
|
}
|
|
32
40
|
/** Pure completeness evaluation — exported for tests. @internal */
|
|
33
41
|
export declare function evaluateBuyCompleteness(input: {
|
|
34
|
-
product: RenderProduct;
|
|
35
42
|
quantity: number;
|
|
43
|
+
/** Required attribute keys still unselected (C1 — `option:<key>`). */
|
|
44
|
+
missingOptionKeys: string[];
|
|
45
|
+
/** Full-resolution validity of the selection (C1 — `variant`). */
|
|
46
|
+
selectionValid: boolean;
|
|
36
47
|
textFields: Array<{
|
|
37
48
|
name: string;
|
|
38
49
|
optional: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EditorError } from '../../errors/editorError.js';
|
|
2
2
|
import { DesignStateStore } from './designStateStore.js';
|
|
3
|
+
import { CommerceStateStore } from './commerceStore.js';
|
|
3
4
|
/** Imperative ops a mounted `Editor.Canvas` offers sibling parts. Routes
|
|
4
5
|
* TextField writes through the editor command path (undo/redo, repaint) so
|
|
5
6
|
* the editor document stays the single write authority while a canvas is
|
|
@@ -57,6 +58,10 @@ export declare class EditorPartsRegistry {
|
|
|
57
58
|
export interface EditorRootInternals {
|
|
58
59
|
/** The one live design state under this Root (ADR-0085 §3 / T2). */
|
|
59
60
|
store: DesignStateStore;
|
|
61
|
+
/** The one commerce-selection state under this Root (ADR-0085 §2 / P2):
|
|
62
|
+
* variant selection + quantity. `Editor.Buy` owns the contract; the host
|
|
63
|
+
* reads/writes it through `useProductSelection()`. */
|
|
64
|
+
commerce: CommerceStateStore;
|
|
60
65
|
registry: EditorPartsRegistry;
|
|
61
66
|
/**
|
|
62
67
|
* Handler/async-time invariant channel (ADR-0085, D11 containment): React
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Combination, OptionAttribute, OptionSelection, RenderProduct } from '@snowcone-app/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* One choice of a variant attribute as the commerce store HANDS IT OUT —
|
|
4
|
+
* always the catalog's choice OBJECT (`toOptionAttributes` normalizes bare
|
|
5
|
+
* strings to `{ label }`). A `hex` marks a color-axis choice (render it as a
|
|
6
|
+
* swatch). NOTE the currency mismatch this type exists to make honest: the
|
|
7
|
+
* SDK's `OptionAttribute` declares `choices: string[]` for its selection
|
|
8
|
+
* math, but the runtime objects flowing through this store are these.
|
|
9
|
+
*/
|
|
10
|
+
export interface ProductOptionChoice {
|
|
11
|
+
label?: string;
|
|
12
|
+
hex?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** An attribute as `useProductSelection().attributes` returns it: the SDK
|
|
16
|
+
* shape with the choices typed as the objects they actually are. The
|
|
17
|
+
* CURRENCY for `select()` / `selection` / `disabledChoices` is the choice's
|
|
18
|
+
* `label` STRING — pass `choice.label`, never the object. */
|
|
19
|
+
export interface ProductSelectionAttribute extends Omit<OptionAttribute, 'choices'> {
|
|
20
|
+
choices: ProductOptionChoice[];
|
|
21
|
+
}
|
|
22
|
+
/** The label strings of an attribute's choices (legacy attrs may carry bare
|
|
23
|
+
* strings; normalized attrs carry `{ label, hex?, … }` objects). */
|
|
24
|
+
export declare function choiceLabelsOf(attr: OptionAttribute | ProductSelectionAttribute): string[];
|
|
25
|
+
/** Derive the seed selection from the product's initial `variantId` — the
|
|
26
|
+
* combination it resolves to pins one value per variant-affecting attribute.
|
|
27
|
+
* No `variantId` (or no match) seeds an empty selection. */
|
|
28
|
+
export declare function seedSelectionOf(product: RenderProduct, combinations: Combination[]): OptionSelection;
|
|
29
|
+
export declare class CommerceStateStore {
|
|
30
|
+
private product;
|
|
31
|
+
private attrs;
|
|
32
|
+
private combos;
|
|
33
|
+
private selection;
|
|
34
|
+
private quantity;
|
|
35
|
+
private version;
|
|
36
|
+
private readonly listeners;
|
|
37
|
+
constructor(product: RenderProduct);
|
|
38
|
+
getVersion: () => number;
|
|
39
|
+
subscribe: (listener: () => void) => (() => void);
|
|
40
|
+
getAttributes(): Record<string, ProductSelectionAttribute>;
|
|
41
|
+
getSelection(): OptionSelection;
|
|
42
|
+
getQuantity(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Select a choice for a variant attribute. Strict + loud (D4/E3): an
|
|
45
|
+
* attribute key or choice that doesn't exist on the product is a wiring
|
|
46
|
+
* mistake, not user state — throws `E_UNKNOWN_OPTION` listing what IS valid.
|
|
47
|
+
*/
|
|
48
|
+
select(key: string, choice: string): void;
|
|
49
|
+
/** Quantity is USER state (completeness rule C5 gates on it) — never throws. */
|
|
50
|
+
setQuantity(quantity: number): void;
|
|
51
|
+
/** Attribute keys still required (non-optional) but unselected — Buy's
|
|
52
|
+
* `option:<key>` reason codes. */
|
|
53
|
+
missingOptionKeys(): string[];
|
|
54
|
+
/** Full selection resolution (the C1 amendment): true when every required
|
|
55
|
+
* attribute is selected AND the selection matches a valid combination. */
|
|
56
|
+
selectionValid(): boolean;
|
|
57
|
+
/** The variant (gvid) the current selection resolves to. Falls back to the
|
|
58
|
+
* product's initial `variantId` while nothing has been selected. */
|
|
59
|
+
resolvedVariantId(): string | undefined;
|
|
60
|
+
/** Price of the resolved combination, when the catalog carries one. */
|
|
61
|
+
resolvedPrice(): number | undefined;
|
|
62
|
+
/** Choices invalid given the rest of the current selection — for hosts to
|
|
63
|
+
* disable/grey out in their variant UI. */
|
|
64
|
+
disabledChoices(): Record<string, string[]>;
|
|
65
|
+
/** The combinations (for payload assembly via `createCartDetail`). */
|
|
66
|
+
getCombinations(): Combination[];
|
|
67
|
+
private bump;
|
|
68
|
+
}
|
|
@@ -36,6 +36,10 @@ export type { EditorTextFieldProps } from './EditorTextField.js';
|
|
|
36
36
|
export type { EditorBuyProps, EditorBuyPayload, BuyMissingReason } from './EditorBuy.js';
|
|
37
37
|
export type { DesignState } from './designStateStore.js';
|
|
38
38
|
export { DesignStateStore } from './designStateStore.js';
|
|
39
|
+
export { useProductSelection } from './useProductSelection.js';
|
|
40
|
+
export type { ProductSelectionApi } from './useProductSelection.js';
|
|
41
|
+
export { CommerceStateStore } from './commerceStore.js';
|
|
42
|
+
export type { ProductOptionChoice, ProductSelectionAttribute } from './commerceStore.js';
|
|
39
43
|
export type { EditorLayersProps } from './EditorLayers.js';
|
|
40
44
|
export type { EditorControlsProps } from './EditorControls.js';
|
|
41
45
|
export type { EditorHistoryProps } from './EditorHistory.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { OptionSelection } from '@snowcone-app/sdk';
|
|
2
|
+
import { ProductSelectionAttribute } from './commerceStore.js';
|
|
3
|
+
export interface ProductSelectionApi {
|
|
4
|
+
/** The product's variant attributes (normalized catalog `options.attributes`).
|
|
5
|
+
* Choices are ALWAYS `{ label, hex?, … }` OBJECTS — render `choice.label`,
|
|
6
|
+
* use `hex` for color swatches. Empty object for an option-less product. */
|
|
7
|
+
attributes: Record<string, ProductSelectionAttribute>;
|
|
8
|
+
/** Current choices BY LABEL STRING, e.g. `{ Color: 'Black', Size: 'M' }` —
|
|
9
|
+
* the label is the currency everywhere selection is read or written.
|
|
10
|
+
* Seeded from the Root product's `variantId` combination when present. */
|
|
11
|
+
selection: OptionSelection;
|
|
12
|
+
/** Select a choice by its LABEL string (`select('Color', choice.label)`).
|
|
13
|
+
* Unknown key/choice → throws `E_UNKNOWN_OPTION` listing the valid ones. */
|
|
14
|
+
select: (key: string, choice: string) => void;
|
|
15
|
+
/** Choice LABELS invalid given the rest of the selection — disable these. */
|
|
16
|
+
disabledChoices: Record<string, string[]>;
|
|
17
|
+
/** The variant (gvid) the selection resolves to; `undefined` while
|
|
18
|
+
* incomplete/invalid. This is what renders and what Buy submits. */
|
|
19
|
+
variantId: string | undefined;
|
|
20
|
+
/** Resolved combination's price, when the catalog carries one. */
|
|
21
|
+
price: number | undefined;
|
|
22
|
+
/** Buy-owned quantity (completeness rule C5 gates on it). */
|
|
23
|
+
quantity: number;
|
|
24
|
+
/** Set the Buy-owned quantity. Values that are not integers ≥ 1 are stored
|
|
25
|
+
* as-is and GATE Buy with the `quantity` reason code (user state, never a
|
|
26
|
+
* throw) — clamp in your stepper if you never want that state. */
|
|
27
|
+
setQuantity: (quantity: number) => void;
|
|
28
|
+
/** True when every required attribute is selected and the combination is
|
|
29
|
+
* valid — the C1 component of Buy's completeness. */
|
|
30
|
+
complete: boolean;
|
|
31
|
+
/** Required attribute keys still unselected (Buy's `option:<key>` codes). */
|
|
32
|
+
missing: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read/write the Buy-owned product selection under this `Editor.Root`.
|
|
36
|
+
*
|
|
37
|
+
* @experimental ADR-0085; subject to change until the P4 gate.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useProductSelection(): ProductSelectionApi;
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { g as l } from "./EditorContext-5QpqAhjh.js";
|
|
2
|
+
const k = 1;
|
|
3
|
+
function d(t) {
|
|
4
|
+
return typeof t == "number" && Number.isFinite(t) ? Math.round(t * 100) / 100 : t;
|
|
5
|
+
}
|
|
6
|
+
function p(t) {
|
|
7
|
+
return {
|
|
8
|
+
...t,
|
|
9
|
+
...t.x !== void 0 && { x: d(t.x) },
|
|
10
|
+
...t.y !== void 0 && { y: d(t.y) },
|
|
11
|
+
...t.width !== void 0 && { width: d(t.width) },
|
|
12
|
+
...t.height !== void 0 && { height: d(t.height) },
|
|
13
|
+
...t.rotation !== void 0 && { rotation: d(t.rotation) }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function x(t) {
|
|
17
|
+
var a;
|
|
18
|
+
const i = (a = t.artboards) != null && a.length ? t.artboards : [{ name: "Front", width: 800, height: 800 }], e = i.map(() => []);
|
|
19
|
+
for (const r of t.elements || []) {
|
|
20
|
+
const o = r == null ? void 0 : r.artboard, s = typeof o == "string" ? i.findIndex((n) => (n.name || "Front") === o) : -1;
|
|
21
|
+
e[s >= 0 ? s : 0].push(R(r));
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: k,
|
|
25
|
+
artboards: i.map((r, o) => ({
|
|
26
|
+
id: `artboard-${o + 1}`,
|
|
27
|
+
name: r.name || "Front",
|
|
28
|
+
x: 0,
|
|
29
|
+
y: 0,
|
|
30
|
+
width: d(r.width || 800),
|
|
31
|
+
height: d(r.height || 800),
|
|
32
|
+
backgroundColor: r.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: r.exportBackground ?? !1,
|
|
38
|
+
elements: e[o],
|
|
39
|
+
distressTexture: r.distressTexture,
|
|
40
|
+
imageMask: r.imageMask
|
|
41
|
+
}))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function R(t) {
|
|
45
|
+
const i = t.type || t.transformType;
|
|
46
|
+
if (i === "image")
|
|
47
|
+
return C(t);
|
|
48
|
+
const { artboard: e, ...a } = t;
|
|
49
|
+
return p({
|
|
50
|
+
...a,
|
|
51
|
+
type: i
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function C(t) {
|
|
55
|
+
var c;
|
|
56
|
+
const i = t.transformData || {}, e = t.masks && t.masks.length > 0, a = i.width || 0, r = i.height || 0, o = e ? a : a * (i.cropWidth ?? 1), s = e ? r : r * (i.cropHeight ?? 1), n = {
|
|
57
|
+
id: t.id,
|
|
58
|
+
type: "image",
|
|
59
|
+
x: t.x || 0,
|
|
60
|
+
y: t.y || 0,
|
|
61
|
+
width: o,
|
|
62
|
+
height: s,
|
|
63
|
+
rotation: t.rotation || 0,
|
|
64
|
+
imageUrl: t.imageUrl,
|
|
65
|
+
imageAspectRatio: t.imageAspectRatio
|
|
66
|
+
};
|
|
67
|
+
return !e && (i.cropX !== 0 || i.cropY !== 0 || i.cropWidth !== 1 || i.cropHeight !== 1) && (n.cropX = i.cropX, n.cropY = i.cropY, n.cropWidth = i.cropWidth, n.cropHeight = i.cropHeight, n.needsCropPixelConversion = !0), i.flipHorizontal && (n.flipHorizontal = i.flipHorizontal), i.flipVertical && (n.flipVertical = i.flipVertical), i.borderRadius && (n.borderRadius = i.borderRadius), t.opacity !== void 0 && (n.opacity = t.opacity), t.distressEffect && (n.distressEffect = t.distressEffect), ((c = t.masks) == null ? void 0 : c.length) > 0 && (n.masks = t.masks), t.blendMode && (n.blendMode = t.blendMode), t.knockoutParts && (n.knockoutParts = t.knockoutParts), t.stroke && (n.stroke = t.stroke), p(n);
|
|
68
|
+
}
|
|
69
|
+
const u = "center", y = Symbol.for("snowcone.canvas.layerToken");
|
|
70
|
+
function S(t) {
|
|
71
|
+
return typeof t == "object" && t !== null && t[y] === !0 && typeof t.name == "string" && (t.kind === "text" || t.kind === "image");
|
|
72
|
+
}
|
|
73
|
+
function N(t, i) {
|
|
74
|
+
return Object.freeze({ kind: t, name: i, [y]: !0 });
|
|
75
|
+
}
|
|
76
|
+
function m(t) {
|
|
77
|
+
const i = t.transformType;
|
|
78
|
+
return i === "image" ? "image" : i === "shape" || i === "group" || i === "path" ? null : "text";
|
|
79
|
+
}
|
|
80
|
+
function T(t) {
|
|
81
|
+
return t === "top-left" || t === "left" || t === "bottom-left" ? "left" : t === "top-right" || t === "right" || t === "bottom-right" ? "right" : "center";
|
|
82
|
+
}
|
|
83
|
+
function E(t) {
|
|
84
|
+
return t === "top-left" || t === "top" || t === "top-right" ? "top" : t === "bottom-left" || t === "bottom" || t === "bottom-right" ? "bottom" : "middle";
|
|
85
|
+
}
|
|
86
|
+
function f(t, i, e, a, r, o) {
|
|
87
|
+
const s = T(t), n = E(t), h = s === "left" ? i / 2 + o : s === "right" ? a - i / 2 - o : a / 2, c = n === "top" ? e / 2 + o : n === "bottom" ? r - e / 2 - o : r / 2;
|
|
88
|
+
return { x: h, y: c };
|
|
89
|
+
}
|
|
90
|
+
function H(t, i, e) {
|
|
91
|
+
const a = i / e;
|
|
92
|
+
if (t >= a) {
|
|
93
|
+
const s = e * t, n = i / s;
|
|
94
|
+
return {
|
|
95
|
+
width: s,
|
|
96
|
+
height: e,
|
|
97
|
+
cropX: (1 - n) / 2,
|
|
98
|
+
cropY: 0,
|
|
99
|
+
cropWidth: n,
|
|
100
|
+
cropHeight: 1
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const r = i / t, o = e / r;
|
|
104
|
+
return {
|
|
105
|
+
width: i,
|
|
106
|
+
height: r,
|
|
107
|
+
cropX: 0,
|
|
108
|
+
cropY: (1 - o) / 2,
|
|
109
|
+
cropWidth: 1,
|
|
110
|
+
cropHeight: o
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
class g {
|
|
114
|
+
constructor(i) {
|
|
115
|
+
if (this.els = [], this.idCounter = 0, !(i.width > 0) || !(i.height > 0))
|
|
116
|
+
throw new Error("design(): artboard width and height must be positive numbers");
|
|
117
|
+
this.artboard = {
|
|
118
|
+
name: i.name || "Front",
|
|
119
|
+
width: i.width,
|
|
120
|
+
height: i.height,
|
|
121
|
+
backgroundColor: i.backgroundColor
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Seed a builder from a previously saved canvas state — the editor shape
|
|
126
|
+
* `<SnowconeCanvas onChange>` emits (and `toJSON()` produces).
|
|
127
|
+
*
|
|
128
|
+
* This is how a saved editable template re-enters the turnkey embed path:
|
|
129
|
+
* load the template's state JSON, lift it back into a `DesignBuilder`, and
|
|
130
|
+
* hand it to `<RealtimeEmbed design={…}>`. The active artboard becomes the
|
|
131
|
+
* builder's artboard; elements tagged for other artboards are skipped
|
|
132
|
+
* (untagged elements are assumed to belong to the active one).
|
|
133
|
+
*/
|
|
134
|
+
static fromState(i) {
|
|
135
|
+
const e = i.artboards.find((r) => r.name === i.activeArtboard) ?? i.artboards[0];
|
|
136
|
+
if (!e)
|
|
137
|
+
throw new Error("DesignBuilder.fromState(): state has no artboards");
|
|
138
|
+
const a = new g({
|
|
139
|
+
name: e.name,
|
|
140
|
+
width: e.width,
|
|
141
|
+
height: e.height,
|
|
142
|
+
backgroundColor: e.backgroundColor
|
|
143
|
+
});
|
|
144
|
+
for (const r of i.elements)
|
|
145
|
+
r.artboard && r.artboard !== e.name || a.els.push({ ...r });
|
|
146
|
+
return a;
|
|
147
|
+
}
|
|
148
|
+
nextId(i) {
|
|
149
|
+
return `${i}-${++this.idCounter}`;
|
|
150
|
+
}
|
|
151
|
+
background(i, e = {}) {
|
|
152
|
+
const { width: a, height: r } = this.artboard;
|
|
153
|
+
let o;
|
|
154
|
+
if (e.aspectRatio && e.aspectRatio > 0) {
|
|
155
|
+
const n = H(e.aspectRatio, a, r);
|
|
156
|
+
o = {
|
|
157
|
+
type: "image",
|
|
158
|
+
width: n.width,
|
|
159
|
+
height: n.height,
|
|
160
|
+
cropX: n.cropX,
|
|
161
|
+
cropY: n.cropY,
|
|
162
|
+
cropWidth: n.cropWidth,
|
|
163
|
+
cropHeight: n.cropHeight,
|
|
164
|
+
flipHorizontal: !1,
|
|
165
|
+
flipVertical: !1,
|
|
166
|
+
borderRadius: 0
|
|
167
|
+
};
|
|
168
|
+
} else
|
|
169
|
+
o = {
|
|
170
|
+
type: "image",
|
|
171
|
+
width: a,
|
|
172
|
+
height: r,
|
|
173
|
+
cropX: 0,
|
|
174
|
+
cropY: 0,
|
|
175
|
+
cropWidth: 1,
|
|
176
|
+
cropHeight: 1,
|
|
177
|
+
flipHorizontal: !1,
|
|
178
|
+
flipVertical: !1,
|
|
179
|
+
borderRadius: 0
|
|
180
|
+
};
|
|
181
|
+
const s = {
|
|
182
|
+
id: this.nextId("bg"),
|
|
183
|
+
transformType: "image",
|
|
184
|
+
x: a / 2,
|
|
185
|
+
y: r / 2,
|
|
186
|
+
rotation: 0,
|
|
187
|
+
opacity: e.opacity ?? 1,
|
|
188
|
+
imageUrl: i,
|
|
189
|
+
imageAspectRatio: e.aspectRatio,
|
|
190
|
+
transformData: o,
|
|
191
|
+
name: e.name ?? "Background"
|
|
192
|
+
};
|
|
193
|
+
return this.els.unshift(s), this;
|
|
194
|
+
}
|
|
195
|
+
image(i, e = {}) {
|
|
196
|
+
const { width: a, height: r } = this.artboard, o = e.width ?? Math.round(a / 3), s = e.height ?? (e.aspectRatio ? Math.round(o / e.aspectRatio) : o), { x: n, y: h } = f(e.anchor ?? u, o, s, a, r, e.padding ?? 0), c = {
|
|
197
|
+
id: this.nextId("img"),
|
|
198
|
+
transformType: "image",
|
|
199
|
+
x: n,
|
|
200
|
+
y: h,
|
|
201
|
+
rotation: e.rotation ?? 0,
|
|
202
|
+
opacity: e.opacity ?? 1,
|
|
203
|
+
imageUrl: i,
|
|
204
|
+
imageAspectRatio: e.aspectRatio,
|
|
205
|
+
transformData: {
|
|
206
|
+
type: "image",
|
|
207
|
+
width: o,
|
|
208
|
+
height: s,
|
|
209
|
+
cropX: 0,
|
|
210
|
+
cropY: 0,
|
|
211
|
+
cropWidth: 1,
|
|
212
|
+
cropHeight: 1,
|
|
213
|
+
flipHorizontal: !1,
|
|
214
|
+
flipVertical: !1,
|
|
215
|
+
borderRadius: 0
|
|
216
|
+
},
|
|
217
|
+
...e.name ? { name: e.name } : {}
|
|
218
|
+
};
|
|
219
|
+
return this.els.push(c), this;
|
|
220
|
+
}
|
|
221
|
+
text(i, e = {}) {
|
|
222
|
+
const { width: a, height: r } = this.artboard, o = e.fontSize ?? Math.round(this.artboard.height / 12), { x: s, y: n } = f(e.anchor ?? "top", 0, o, a, r, e.padding ?? Math.round(r / 20)), h = {
|
|
223
|
+
id: this.nextId("text"),
|
|
224
|
+
transformType: "custom",
|
|
225
|
+
x: s,
|
|
226
|
+
y: n,
|
|
227
|
+
rotation: e.rotation ?? 0,
|
|
228
|
+
text: i,
|
|
229
|
+
fontSize: o,
|
|
230
|
+
fontFamily: e.fontFamily ?? "Anton",
|
|
231
|
+
color: e.color ?? "#ffffff",
|
|
232
|
+
textAlign: e.textAlign ?? "center",
|
|
233
|
+
bold: e.bold ?? !1,
|
|
234
|
+
italic: e.italic ?? !1,
|
|
235
|
+
transformData: { type: "custom" },
|
|
236
|
+
...e.name ? { name: e.name } : {}
|
|
237
|
+
};
|
|
238
|
+
return this.els.push(h), this;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Mint a {@link LayerToken} for a named layer — the only currency
|
|
242
|
+
* `Editor.TextField` accepts (ADR-0085 §1). Compile time: `name` is
|
|
243
|
+
* constrained to this design's declared layer names, kind inferred from
|
|
244
|
+
* which per-kind union it falls in. Runtime: validated EAGERLY against the
|
|
245
|
+
* built elements (nearest the mistake), so CMS/JSON-loaded designs get the
|
|
246
|
+
* same protection with `string` typing.
|
|
247
|
+
*
|
|
248
|
+
* @throws EditorError `E_LAYER_NOT_FOUND` — no layer with this name; lists
|
|
249
|
+
* the valid names per kind.
|
|
250
|
+
* @throws EditorError `E_LAYER_WRONG_KIND` — the name matches a layer that
|
|
251
|
+
* is not bindable (shape/group/path).
|
|
252
|
+
* @experimental ADR-0085; subject to change until the P4 gate.
|
|
253
|
+
*/
|
|
254
|
+
layer(i) {
|
|
255
|
+
const e = this.els.find((r) => r.name === i);
|
|
256
|
+
if (!e) {
|
|
257
|
+
const r = this.layerNamesOfKind("text"), o = this.layerNamesOfKind("image");
|
|
258
|
+
throw new l(
|
|
259
|
+
"E_LAYER_NOT_FOUND",
|
|
260
|
+
`no layer "${i}" in this design. Text layers: ${JSON.stringify(r)}; image layers: ${JSON.stringify(o)}. Fix the name, or add the layer first — e.g. design(…).text('…', { name: '${i}' }).`,
|
|
261
|
+
{ layerName: i, validNames: [...r, ...o], textNames: r, imageNames: o, docSlug: "embed-mode" }
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const a = m(e);
|
|
265
|
+
if (!a)
|
|
266
|
+
throw new l(
|
|
267
|
+
"E_LAYER_WRONG_KIND",
|
|
268
|
+
`layer "${i}" is a ${e.transformType} element — only text and image layers are bindable. Bindable layers: ${JSON.stringify([...this.layerNamesOfKind("text"), ...this.layerNamesOfKind("image")])}.`,
|
|
269
|
+
{ layerName: i, actualType: e.transformType, docSlug: "embed-mode" }
|
|
270
|
+
);
|
|
271
|
+
return N(a, i);
|
|
272
|
+
}
|
|
273
|
+
/** Names of bindable layers of one kind, in stack order. @internal */
|
|
274
|
+
layerNamesOfKind(i) {
|
|
275
|
+
return this.els.filter((e) => m(e) === i).map((e) => e.name).filter((e) => !!e);
|
|
276
|
+
}
|
|
277
|
+
/** An anchored vector shape. */
|
|
278
|
+
shape(i, e = {}) {
|
|
279
|
+
const { width: a, height: r } = this.artboard, o = e.size ?? Math.round(a / 6), s = typeof o == "number" ? o : o.width, n = typeof o == "number" ? o : o.height, { x: h, y: c } = f(e.anchor ?? u, s, n, a, r, e.padding ?? 0), b = {
|
|
280
|
+
type: "shape",
|
|
281
|
+
shapeType: i,
|
|
282
|
+
width: s,
|
|
283
|
+
height: n,
|
|
284
|
+
fillColor: e.fill ?? "#000000",
|
|
285
|
+
fillOpacity: e.opacity ?? 1,
|
|
286
|
+
...e.points !== void 0 ? { points: e.points } : {},
|
|
287
|
+
...e.sides !== void 0 ? { sides: e.sides } : {}
|
|
288
|
+
}, w = {
|
|
289
|
+
id: this.nextId("shape"),
|
|
290
|
+
transformType: "shape",
|
|
291
|
+
x: h,
|
|
292
|
+
y: c,
|
|
293
|
+
rotation: e.rotation ?? 0,
|
|
294
|
+
transformData: b,
|
|
295
|
+
...e.name ? { name: e.name } : {}
|
|
296
|
+
};
|
|
297
|
+
return this.els.push(w), this;
|
|
298
|
+
}
|
|
299
|
+
/** The composed elements, in render order (background first). Editor shape. */
|
|
300
|
+
get elements() {
|
|
301
|
+
return this.els.map((i) => ({ ...i }));
|
|
302
|
+
}
|
|
303
|
+
/** Editor-shape canvas state for `<SnowconeCanvas initialElements=… />` and
|
|
304
|
+
* `serializeStateForServer`. `activeArtboard` is the artboard name. */
|
|
305
|
+
toJSON() {
|
|
306
|
+
return {
|
|
307
|
+
elements: this.elements,
|
|
308
|
+
artboards: [{
|
|
309
|
+
name: this.artboard.name,
|
|
310
|
+
width: this.artboard.width,
|
|
311
|
+
height: this.artboard.height,
|
|
312
|
+
// A background set on a design is part of the design: flag it
|
|
313
|
+
// renderable so exports and server renders paint it, matching the
|
|
314
|
+
// editor (the export worker skips it without `exportBackground`).
|
|
315
|
+
...this.artboard.backgroundColor ? {
|
|
316
|
+
backgroundColor: this.artboard.backgroundColor,
|
|
317
|
+
exportBackground: this.artboard.backgroundColor !== "transparent"
|
|
318
|
+
} : {}
|
|
319
|
+
}],
|
|
320
|
+
activeArtboard: this.artboard.name
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** Server-render payload for `RenderSession.renderState(placement, …)`.
|
|
324
|
+
* Equivalent to `serializeStateForServer(design.toJSON())`. */
|
|
325
|
+
toServerRequest() {
|
|
326
|
+
return x(this.toJSON());
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function v(t) {
|
|
330
|
+
return new g(t);
|
|
331
|
+
}
|
|
332
|
+
function z(t) {
|
|
333
|
+
return m(t);
|
|
334
|
+
}
|
|
335
|
+
export {
|
|
336
|
+
k as C,
|
|
337
|
+
g as D,
|
|
338
|
+
v as d,
|
|
339
|
+
S as i,
|
|
340
|
+
z as l,
|
|
341
|
+
x as s
|
|
342
|
+
};
|
|
343
|
+
//# sourceMappingURL=design-builder-v4935TeS.js.map
|