@snowcone-app/canvas 0.19.1 → 0.21.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/README.md +124 -13
- package/STYLING_CONTRACT.md +427 -0
- package/dist/{CanvasRenderer-DUc-MOeR.js → CanvasRenderer-CwiYq8qg.js} +3 -3
- package/dist/{CanvasRenderer-DUc-MOeR.js.map → CanvasRenderer-CwiYq8qg.js.map} +1 -1
- package/dist/{CanvasStateV1-Bq48qhv5.js → CanvasStateV1-BFabcAKC.js} +35 -30
- package/dist/{CanvasStateV1-Bq48qhv5.js.map → CanvasStateV1-BFabcAKC.js.map} +1 -1
- package/dist/{ContextualToolbars-CkLjHNPn.js → ContextualToolbars--vTtorNg.js} +5 -5
- package/dist/{ContextualToolbars-CkLjHNPn.js.map → ContextualToolbars--vTtorNg.js.map} +1 -1
- package/dist/{EditorCanvasImpl-_3xCnLkx.js → EditorCanvasImpl-2ZgIUVpK.js} +18 -17
- package/dist/{EditorCanvasImpl-_3xCnLkx.js.map → EditorCanvasImpl-2ZgIUVpK.js.map} +1 -1
- package/dist/{ImportManager-D--7JnvW.js → ExportManager-DK7CdI-G.js} +24758 -24758
- package/dist/ExportManager-DK7CdI-G.js.map +1 -0
- package/dist/{FontAnalyzer-DIlqmyuf.js → FontAnalyzer-BpsTYowD.js} +5 -2
- package/dist/FontAnalyzer-BpsTYowD.js.map +1 -0
- package/dist/SnowconeCanvas-DqZicdxK.js +1172 -0
- package/dist/SnowconeCanvas-DqZicdxK.js.map +1 -0
- package/dist/{MenuButton-CeoEAtzP.js → UnifiedFontService-B1PGVu8O.js} +3704 -3429
- package/dist/UnifiedFontService-B1PGVu8O.js.map +1 -0
- package/dist/advanced.mjs +13 -13
- package/dist/api/embed.d.ts +3 -1
- package/dist/api/stable.d.ts +2 -2
- package/dist/components/editor/EditorAdd.d.ts +2 -1
- package/dist/components/editor/EditorBuy.d.ts +2 -1
- package/dist/components/editor/EditorCanvas.d.ts +2 -1
- package/dist/components/editor/EditorControls.d.ts +2 -1
- package/dist/components/editor/EditorHistory.d.ts +2 -1
- package/dist/components/editor/EditorLayers.d.ts +2 -1
- package/dist/components/editor/EditorMenu.d.ts +2 -1
- package/dist/components/editor/EditorPreview.d.ts +2 -1
- package/dist/components/editor/EditorRoot.d.ts +2 -1
- package/dist/components/editor/EditorTextField.d.ts +2 -1
- package/dist/components/editor/useProductSelection.d.ts +2 -1
- package/dist/components/embed/SnowconeCanvas.d.ts +10 -2
- package/dist/components/embed-realtime/context.d.ts +1 -1
- package/dist/contexts/EditorContext.d.ts +1 -1
- package/dist/core/ArtboardElement.d.ts +2 -1
- package/dist/default-editor/SnowconeEditor.d.ts +40 -0
- package/dist/design/design-builder.d.ts +46 -21
- package/dist/design/template-fields.d.ts +32 -0
- package/dist/design-builder-CPF60RU2.js +367 -0
- package/dist/design-builder-CPF60RU2.js.map +1 -0
- package/dist/{EditorContext-5QpqAhjh.js → editorErrors-CK1RISFa.js} +382 -358
- package/dist/editorErrors-CK1RISFa.js.map +1 -0
- package/dist/embed-2tp6Ue90.js +1117 -0
- package/dist/embed-2tp6Ue90.js.map +1 -0
- package/dist/embed.mjs +33 -31
- package/dist/hooks/useArtboards.d.ts +2 -1
- package/dist/index.mjs +8 -8
- package/dist/internals.mjs +5 -5
- package/dist/kits/sections.d.ts +11 -7
- package/dist/rendering/serialize-for-server.d.ts +3 -0
- package/dist/state/CanvasStateV1.d.ts +12 -0
- package/dist/template-fields-FgImixyv.js +476 -0
- package/dist/template-fields-FgImixyv.js.map +1 -0
- package/dist/testing/assertRenders.d.ts +2 -1
- package/dist/testing.mjs +8 -8
- package/dist/testing.mjs.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/{usePieceGuideBundle-DVOISsB1.js → usePieceGuideBundle-Ctnu5k2D.js} +2 -2
- package/dist/{usePieceGuideBundle-DVOISsB1.js.map → usePieceGuideBundle-Ctnu5k2D.js.map} +1 -1
- package/dist/{validation-0IscSfRb.js → validation-3YEMnKLB.js} +467 -463
- package/dist/validation-3YEMnKLB.js.map +1 -0
- package/example/README.md +179 -0
- package/example/index.html +12 -0
- package/example/package.json +24 -0
- package/example/src/Widget.tsx +62 -0
- package/example/src/main.tsx +9 -0
- package/example/tsconfig.json +14 -0
- package/example/vite.config.ts +11 -0
- package/llms.txt +45 -0
- package/package.json +11 -6
- package/dist/EditorContext-5QpqAhjh.js.map +0 -1
- package/dist/FontAnalyzer-DIlqmyuf.js.map +0 -1
- package/dist/ImportManager-D--7JnvW.js.map +0 -1
- package/dist/MenuButton-CeoEAtzP.js.map +0 -1
- package/dist/SnowconeCanvas-3PA17na3.js +0 -1166
- package/dist/SnowconeCanvas-3PA17na3.js.map +0 -1
- package/dist/design-builder-v4935TeS.js +0 -343
- package/dist/design-builder-v4935TeS.js.map +0 -1
- package/dist/editorErrors-DXNfVXtD.js +0 -29
- package/dist/editorErrors-DXNfVXtD.js.map +0 -1
- package/dist/embed-D2QOpJXG.js +0 -1090
- package/dist/embed-D2QOpJXG.js.map +0 -1
- package/dist/template-fields-Dr3fMItw.js +0 -329
- package/dist/template-fields-Dr3fMItw.js.map +0 -1
- package/dist/validation-0IscSfRb.js.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!-- Assembled by scripts/assemble-example.mjs from the repo's canonical
|
|
2
|
+
fixture (packages/canvas/fixtures/no-canvas-widget) plus this standalone
|
|
3
|
+
scaffolding, so the example builds against the PUBLISHED package. -->
|
|
4
|
+
|
|
5
|
+
# Read me first (you are inside the npm tarball)
|
|
6
|
+
|
|
7
|
+
This directory is the canonical `Editor.*` integration, shipped inside
|
|
8
|
+
`@snowcone-app/canvas` so an agent in a host repo can onboard without any
|
|
9
|
+
network fetch:
|
|
10
|
+
|
|
11
|
+
- **`src/Widget.tsx` is the integration to copy** — `Editor.Root` +
|
|
12
|
+
`Editor.Preview` + `Editor.TextField` + `Editor.Buy`; the design-editor
|
|
13
|
+
chunk never loads in this composition.
|
|
14
|
+
- `npm install && npm run build` here typechecks + builds it against the
|
|
15
|
+
published package. `npm run dev` serves it — rendering live needs a real
|
|
16
|
+
shop key and a grant route (see the docs link below).
|
|
17
|
+
- **`../STYLING_CONTRACT.md`** is the generated restyling contract: every
|
|
18
|
+
`data-sc-part` value, the DOM each part renders, and every `.sc-root`
|
|
19
|
+
theming token — restyle without reading package source.
|
|
20
|
+
- `../llms.txt` indexes everything agent-facing in this tarball. The full
|
|
21
|
+
hosted docs are one fetch: <https://developers.snowcone.app/llms-full.txt>.
|
|
22
|
+
|
|
23
|
+
One caveat on the protocol below: it was written for contributors inside the
|
|
24
|
+
snowcone monorepo — its `fixtures/<your-name>` paths and `FakeRenderSession`
|
|
25
|
+
test harness are repo-internal, not shipped. In YOUR app the protocol is:
|
|
26
|
+
copy `src/Widget.tsx`, change the three things it marks, and verify with
|
|
27
|
+
`assertRenders` + `expectNoEditorErrors` from `@snowcone-app/canvas/testing`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Personalization widget — the canonical `Editor.*` example (copy me)
|
|
32
|
+
|
|
33
|
+
This directory is a **template, not just an exhibit**. The fastest path to your
|
|
34
|
+
own widget — measured at **zero failed iterations** in a cold-start agent run —
|
|
35
|
+
is: copy this folder, change three things, write one test, run one command.
|
|
36
|
+
|
|
37
|
+
## The protocol
|
|
38
|
+
|
|
39
|
+
1. **Copy `src/Widget.tsx`** to `fixtures/<your-name>/src/Widget.tsx` (the
|
|
40
|
+
other files here — `index.html`, `vite.config.ts`, `src/main.tsx` — are
|
|
41
|
+
this fixture's CI plumbing; you don't need them unless you want a runnable
|
|
42
|
+
page).
|
|
43
|
+
2. **Change three things in `src/Widget.tsx`:**
|
|
44
|
+
- the `design(…)` — your artboard size + layers (name every layer you want
|
|
45
|
+
to bind: `.text('…', { name: 'headline' })`);
|
|
46
|
+
- the `product` — your catalog ids (`productId`, `mockupIds`; get them from
|
|
47
|
+
`getProduct()` in `@snowcone-app/sdk` — the ids here are demo values).
|
|
48
|
+
A product with a COLOR variant axis also needs `variantId`, or the render
|
|
49
|
+
hangs (pass `options` too so it fails fast instead);
|
|
50
|
+
- the parts — `<Editor.TextField layer={d.layer('…')}/>` per bindable
|
|
51
|
+
layer (`optional` ⇒ doesn't gate Buy), `<Editor.Preview/>`,
|
|
52
|
+
`<Editor.Buy onAddToCart={…}/>`.
|
|
53
|
+
3. **Write your test** at `src/components/editor/__tests__/<your-name>.test.tsx`,
|
|
54
|
+
copying the pattern in `canonicalExample.test.tsx` (it imports this widget).
|
|
55
|
+
4. **Run `pnpm -C packages/canvas verify`** — typecheck + tests + the
|
|
56
|
+
bundle-isolation gate. Green = your integration is proven, no browser needed.
|
|
57
|
+
|
|
58
|
+
`index.html` / `vite.config.ts` / `src/main.tsx` here are load-bearing only for
|
|
59
|
+
THIS canonical fixture (the CI import-graph check builds it). Your copy doesn't
|
|
60
|
+
need them unless you also want a runnable page — the test is the proof.
|
|
61
|
+
|
|
62
|
+
## The test-harness contract (what the canonical test uses and why)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { FakeRenderSession, mountFlushed } from './testHarness.js';
|
|
66
|
+
import { expectNoEditorErrors } from '../../../testing/editorErrors.js';
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- **Two `vi.mock` blocks are required**, copy them verbatim:
|
|
70
|
+
- `@snowcone-app/sdk` → swaps `RenderSession` for `FakeRenderSession` (no
|
|
71
|
+
network; you drive mockups by hand);
|
|
72
|
+
- `@iconify/react` → the embed entry registers icons at module load, and the
|
|
73
|
+
registry's async timers leak across happy-dom tests without the stub.
|
|
74
|
+
- `FakeRenderSession.reset()` in `beforeEach`; `FakeRenderSession.last` is the
|
|
75
|
+
session your Root constructed.
|
|
76
|
+
- `session.renderState` is a spy — every render push, with the serialized
|
|
77
|
+
state as call arg `[1]` (assert your personalization landed there).
|
|
78
|
+
- `session.emitMockups([{ mockupId, imageUrl }])` simulates the server
|
|
79
|
+
answering. `mockupId` must be one of your `product.mockupIds` when your
|
|
80
|
+
Preview pins `mockupId=…`; an un-pinned Preview shows the first result.
|
|
81
|
+
- `mountFlushed(<YourWidget/>)` renders and flushes effects + any settled
|
|
82
|
+
lazy chunks; returns `{ container, unmount }`.
|
|
83
|
+
- `expectNoEditorErrors()` in `afterEach` — REQUIRED. Root's boundary contains
|
|
84
|
+
composition errors behind an error card in prod; this is what makes a
|
|
85
|
+
contained error fail your test instead of passing green.
|
|
86
|
+
|
|
87
|
+
## Selector vocabulary (testing or driving the widget)
|
|
88
|
+
|
|
89
|
+
| Selector | Means |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `[data-sc-part="preview"]` | the live mockup `<img>` |
|
|
92
|
+
| `[data-sc-part="preview-error"]` | the Preview's render/grant-failure surface (`role="alert"`, message in `data-sc-error`) — shown when a render fails before any mockup exists; a stale mockup beats a blank, so later errors keep the image |
|
|
93
|
+
| `[data-sc-part="textfield"]` | a bound text input; `aria-label` defaults to the **layer name**, so `input[aria-label="headline"]` targets one field |
|
|
94
|
+
| `[data-sc-part="buy"]` | the add-to-cart button |
|
|
95
|
+
| `[data-sc-buy-state]` | `ready` / `incomplete` / `busy` |
|
|
96
|
+
| `[data-sc-buy-missing]` | comma-joined reasons while incomplete, in a STABLE evaluation order you can assert on: `option:<key>`… (variant axes unselected, attribute order) → `variant` (complete selection matching no combination) → `layer:<name>` / `layer-seed:<name>` (per mounted field, mount order; `layer-seed` = still the placeholder) → `empty-design` → `quantity` |
|
|
97
|
+
| `[data-sc-part="error"]` | Root's contained-error card (`[data-sc-part="error-retry"]` when retryable) |
|
|
98
|
+
|
|
99
|
+
Multiple `<Editor.Preview/>` AND multiple `<Editor.TextField/>` under one Root
|
|
100
|
+
are valid (one field per named layer). One `<Editor.Canvas/>` max.
|
|
101
|
+
|
|
102
|
+
## Variant selection + quantity (Buy-owned)
|
|
103
|
+
|
|
104
|
+
`Editor.Buy` owns variant selection and quantity; YOU render the variant UI
|
|
105
|
+
with `useProductSelection()` (same barrel). `select(key, choice)` re-resolves
|
|
106
|
+
the variant and the live preview follows — no remount, no session rebuild.
|
|
107
|
+
`<Editor.Buy defaultQuantity={2}/>` seeds quantity; `setQuantity` changes it
|
|
108
|
+
live. Don't drive variant switching through the Root `product.variantId` prop
|
|
109
|
+
— that re-keys the whole render session (initial `variantId` on a color
|
|
110
|
+
product is still required, it seeds the selection).
|
|
111
|
+
|
|
112
|
+
A variant product needs an `options` block on the Root `product` — pass your
|
|
113
|
+
catalog product's `options` verbatim; the shape is:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const product = {
|
|
117
|
+
productId: 'BEEB77',
|
|
118
|
+
mockupIds: ['FV1qjO'],
|
|
119
|
+
placements: ['Front'],
|
|
120
|
+
variantId: 'gv-black-s', // initial selection (REQUIRED for color axes)
|
|
121
|
+
options: {
|
|
122
|
+
attributes: {
|
|
123
|
+
Color: { choices: [{ label: 'Black', hex: '#000' }, { label: 'White', hex: '#fff' }] },
|
|
124
|
+
Size: { choices: [{ label: 'S' }, { label: 'M' }] }, // no hex = non-color axis
|
|
125
|
+
},
|
|
126
|
+
combinations: [
|
|
127
|
+
{ variantId: 'gv-black-s', Color: 'Black', Size: 'S', price: 1900 },
|
|
128
|
+
{ variantId: 'gv-black-m', Color: 'Black', Size: 'M', price: 2100 },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**The currency rule:** `attributes[key].choices` are always `{ label, hex? }`
|
|
135
|
+
OBJECTS (use `hex` for swatches), but `select()`, `selection`, and
|
|
136
|
+
`disabledChoices` all speak the choice's **`label` string** — pass
|
|
137
|
+
`choice.label`, never the object. The hook's full surface:
|
|
138
|
+
`{ attributes, selection, select, disabledChoices, variantId, price,
|
|
139
|
+
quantity, setQuantity, complete, missing }`.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
function VariantPicker() {
|
|
143
|
+
const { attributes, selection, select, disabledChoices, price, quantity, setQuantity } =
|
|
144
|
+
useProductSelection();
|
|
145
|
+
return (
|
|
146
|
+
<>
|
|
147
|
+
{Object.entries(attributes).map(([key, attr]) => (
|
|
148
|
+
<div key={key}>
|
|
149
|
+
{attr.choices.map(({ label = '', hex }) => (
|
|
150
|
+
<button
|
|
151
|
+
key={label}
|
|
152
|
+
style={hex ? { background: hex } : undefined}
|
|
153
|
+
disabled={disabledChoices[key]?.includes(label)}
|
|
154
|
+
aria-pressed={selection[key] === label}
|
|
155
|
+
onClick={() => select(key, label)}
|
|
156
|
+
>
|
|
157
|
+
{label}
|
|
158
|
+
</button>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
<button onClick={() => setQuantity(quantity + 1)}>qty {quantity} +</button>
|
|
163
|
+
</>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Failure-mode fixtures (build your error UI against real failures)
|
|
169
|
+
|
|
170
|
+
From `@snowcone-app/canvas/testing`: `test403Design()` (asset-origin
|
|
171
|
+
allowlist denial — deterministic 403), `testIncompleteDesign()` (Buy gates
|
|
172
|
+
out of the box), `failingGrant()` (grant fetch rejects). For live contract
|
|
173
|
+
checks, `assertRenders(design, product, { shop })` renders twice (seed +
|
|
174
|
+
mutated bindings) and throws structured errors (`E_BINDING_INERT`,
|
|
175
|
+
`E_ASSET_FORBIDDEN`, `E_RENDER_FAILED`) — network-real, so nightly/acceptance
|
|
176
|
+
jobs, not per-PR unit tests.
|
|
177
|
+
|
|
178
|
+
All `Editor.*` parts are stable (ADR-0085 — the default editor is rebuilt from
|
|
179
|
+
them, CI-gated); the public surface is additive-only.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Personalization widget — no-canvas Editor.* composition</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "snowcone-personalization-widget-example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "The canonical @snowcone-app/canvas Editor.* example: a no-canvas personalization widget (live preview + bound text field + smart add-to-cart).",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"build": "tsc --noEmit && vite build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@snowcone-app/canvas": "^0.21.0",
|
|
14
|
+
"react": "^19.1.0",
|
|
15
|
+
"react-dom": "^19.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.2.2",
|
|
19
|
+
"@types/react-dom": "^19.2.2",
|
|
20
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"vite": "^5.4.21"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE canonical personalization example (ADR-0085 / plan D7): a no-canvas
|
|
3
|
+
* merch widget — live product preview, ONE personalized text layer, smart
|
|
4
|
+
* add-to-cart — composed from the public `Editor.*` parts. The full canvas
|
|
5
|
+
* editor never loads; CI asserts this build's static import graph excludes
|
|
6
|
+
* the rendering engine (`scripts/check-import-graph.mjs`).
|
|
7
|
+
*
|
|
8
|
+
* This fixture is also the P0.5 hard-gate exhibit: the same public surface
|
|
9
|
+
* (`Editor.Root` + parts) serves both this composition and the full
|
|
10
|
+
* canvas-mounting editor (see the dev app's scrolling editor page).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { Editor, design, type EditorBuyPayload } from '@snowcone-app/canvas/embed';
|
|
15
|
+
|
|
16
|
+
const SHOP_KEY = import.meta.env.VITE_SNOWCONE_SHOP ?? 'demo-shop';
|
|
17
|
+
|
|
18
|
+
// The design is the single source of truth: it seeds the first render AND
|
|
19
|
+
// mints the typed layer tokens the TextField binds with.
|
|
20
|
+
const nameDrop = design({ name: 'Front', width: 1480, height: 2328 })
|
|
21
|
+
.background('https://dqMwU8Jct3.storage.snowcone.app/demo/demo-008.jpg', {
|
|
22
|
+
aspectRatio: 1480 / 2328,
|
|
23
|
+
name: 'art',
|
|
24
|
+
})
|
|
25
|
+
.text('YOUR NAME', { anchor: 'top', name: 'name' });
|
|
26
|
+
|
|
27
|
+
const product = {
|
|
28
|
+
productId: 'BEEB77',
|
|
29
|
+
mockupIds: ['FV1qjO'],
|
|
30
|
+
placements: ['Front'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function PersonalizationWidget({
|
|
34
|
+
onAddToCart,
|
|
35
|
+
}: {
|
|
36
|
+
/** Your cart handler. NOTE the payload's `designState` is the EDITOR shape
|
|
37
|
+
* (flat `elements` + artboard metadata) — persist it as-is. It is NOT the
|
|
38
|
+
* server render shape (`artboards[].elements`) the session pushes;
|
|
39
|
+
* `payload.serverRequest` carries that one if you need it. */
|
|
40
|
+
onAddToCart?: (payload: EditorBuyPayload) => void;
|
|
41
|
+
} = {}): React.ReactElement {
|
|
42
|
+
return (
|
|
43
|
+
<Editor.Root shop={SHOP_KEY} product={product} design={nameDrop} grantUrl="/api/realtime/grant">
|
|
44
|
+
<div style={{ display: 'grid', gap: 12, maxWidth: 320 }}>
|
|
45
|
+
<Editor.Preview alt="Your personalized tee" fallback={<div aria-busy="true">Rendering…</div>} />
|
|
46
|
+
<Editor.TextField layer={nameDrop.layer('name')} placeholder="Your name" />
|
|
47
|
+
<Editor.Buy
|
|
48
|
+
onAddToCart={
|
|
49
|
+
onAddToCart ??
|
|
50
|
+
(({ productId, quantity, designState }) => {
|
|
51
|
+
// The host's one line: persist the designState (the
|
|
52
|
+
// re-renderable source of truth) with the cart line.
|
|
53
|
+
console.log('add-to-cart', { productId, quantity, designState });
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
Add to cart · $24
|
|
58
|
+
</Editor.Buy>
|
|
59
|
+
</div>
|
|
60
|
+
</Editor.Root>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { PersonalizationWidget } from './Widget.js';
|
|
4
|
+
|
|
5
|
+
createRoot(document.getElementById('root')!).render(
|
|
6
|
+
<React.StrictMode>
|
|
7
|
+
<PersonalizationWidget />
|
|
8
|
+
</React.StrictMode>,
|
|
9
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"types": ["vite/client"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
// Standalone build of the canonical example against the PUBLISHED
|
|
5
|
+
// @snowcone-app/canvas (the in-repo fixture variant of this file resolves the
|
|
6
|
+
// package to workspace source instead; scripts/assemble-example.mjs swaps in
|
|
7
|
+
// this one when the example ships in the tarball or runs in the
|
|
8
|
+
// published-pins CI gate).
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
plugins: [react()],
|
|
11
|
+
});
|
package/llms.txt
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @snowcone-app/canvas — agent index (ships inside the npm tarball)
|
|
2
|
+
|
|
3
|
+
Zero-network onboarding: everything listed here is included in this package.
|
|
4
|
+
Read these before fetching anything.
|
|
5
|
+
|
|
6
|
+
- example/ — THE canonical integration (a no-canvas personalization widget:
|
|
7
|
+
Editor.Root + Editor.Preview + Editor.TextField + Editor.Buy; the design
|
|
8
|
+
editor chunk never loads). Copy example/src/Widget.tsx into your app;
|
|
9
|
+
example/README.md has the copy protocol, the data-sc-* selector vocabulary,
|
|
10
|
+
variant selection + quantity via useProductSelection(), and the
|
|
11
|
+
failure-mode fixtures.
|
|
12
|
+
- STYLING_CONTRACT.md — GENERATED restyling contract: every data-sc-part
|
|
13
|
+
value, the DOM each part renders, and every .sc-root theming token.
|
|
14
|
+
Restyle the parts without reading package source.
|
|
15
|
+
- README.md — package overview: the canonical example first, then the
|
|
16
|
+
standalone design editor (SnowconeCanvas).
|
|
17
|
+
|
|
18
|
+
Entry points:
|
|
19
|
+
|
|
20
|
+
- @snowcone-app/canvas/embed — the Editor.* parts and SnowconeEditor (the
|
|
21
|
+
turnkey default composed from those same parts), the design() builder and
|
|
22
|
+
typed layer tokens, useProductSelection(), the turnkey
|
|
23
|
+
RealtimeEmbed/RealtimeCanvas/RealtimeMockup trio, and the chrome-building
|
|
24
|
+
hooks (useEditor, useLayers, useCommands, ...).
|
|
25
|
+
- @snowcone-app/canvas — SnowconeCanvas, the standalone design editor.
|
|
26
|
+
- @snowcone-app/canvas/testing — assertRenders (two-render binding-liveness
|
|
27
|
+
check), expectNoEditorErrors (REQUIRED in every integration test), and the
|
|
28
|
+
deterministic failure fixtures (test403Design, testIncompleteDesign,
|
|
29
|
+
failingGrant).
|
|
30
|
+
- @snowcone-app/canvas/style.css — the required stylesheet. Fully namespaced
|
|
31
|
+
(.sc-root scope); it cannot collide with host CSS.
|
|
32
|
+
|
|
33
|
+
Contracts to rely on:
|
|
34
|
+
|
|
35
|
+
- All Editor.* throws are structured: EditorError with a stable `code` +
|
|
36
|
+
`error.data` (valid names, offending values, docSlug). Assert on codes,
|
|
37
|
+
never message prose.
|
|
38
|
+
- Buy incompleteness is state, not an error: [data-sc-buy-state="incomplete"]
|
|
39
|
+
with machine-readable reason codes in [data-sc-buy-missing].
|
|
40
|
+
- The public embed surface is additive-only (CI-pinned).
|
|
41
|
+
|
|
42
|
+
Hosted docs (one fetch, the source of truth):
|
|
43
|
+
https://developers.snowcone.app/llms-full.txt
|
|
44
|
+
The primitives guide (grant-route framework variants, catalog ids, CMS/typed
|
|
45
|
+
layer notes): https://developers.snowcone.app/primitives
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowcone-app/canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Self-contained canvas editor component for e-commerce product customization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -41,7 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|
|
44
|
-
"README.md"
|
|
44
|
+
"README.md",
|
|
45
|
+
"llms.txt",
|
|
46
|
+
"STYLING_CONTRACT.md",
|
|
47
|
+
"example"
|
|
45
48
|
],
|
|
46
49
|
"sideEffects": [
|
|
47
50
|
"**/*.css",
|
|
@@ -107,7 +110,7 @@
|
|
|
107
110
|
"react-colorful": "^5.6.1",
|
|
108
111
|
"tailwind-merge": "^3.0.0",
|
|
109
112
|
"tailwind-variants": "^3.1.1",
|
|
110
|
-
"@snowcone-app/sdk": "0.
|
|
113
|
+
"@snowcone-app/sdk": "0.16.0"
|
|
111
114
|
},
|
|
112
115
|
"devDependencies": {
|
|
113
116
|
"@axe-core/playwright": "^4.11.0",
|
|
@@ -169,7 +172,7 @@
|
|
|
169
172
|
"vite-plugin-node-polyfills": "^0.24.0",
|
|
170
173
|
"vitest": "^3.2.4",
|
|
171
174
|
"zod": "^4.1.12",
|
|
172
|
-
"@snowcone-app/ui": "0.2.
|
|
175
|
+
"@snowcone-app/ui": "0.2.6"
|
|
173
176
|
},
|
|
174
177
|
"scripts": {
|
|
175
178
|
"dev": "vite",
|
|
@@ -183,10 +186,12 @@
|
|
|
183
186
|
"dev:lib": "pnpm run bundle:worker && vite build --config vite.lib.config.ts --watch",
|
|
184
187
|
"preview": "vite preview",
|
|
185
188
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
189
|
+
"typecheck:verify": "tsc --noEmit -p tsconfig.verify.json",
|
|
186
190
|
"generate": "node scripts/generate.js",
|
|
187
|
-
"
|
|
191
|
+
"gen:styling-contract": "STYLING_CONTRACT_WRITE=1 vitest run src/components/editor/__tests__/stylingContract.test.tsx",
|
|
192
|
+
"test": "pnpm run typecheck:verify && pnpm run test:unit && pnpm run test:component && pnpm run test:ssr && pnpm run check:graph",
|
|
188
193
|
"check:graph": "node scripts/check-import-graph.mjs",
|
|
189
|
-
"verify": "pnpm run typecheck && pnpm run test:unit && pnpm run test:component && pnpm run check:graph",
|
|
194
|
+
"verify": "pnpm run typecheck && pnpm run typecheck:verify && pnpm run test:unit && pnpm run test:component && pnpm run check:graph",
|
|
190
195
|
"test:unit": "vitest run",
|
|
191
196
|
"test:ssr": "vitest run --config vitest.ssr.config.ts",
|
|
192
197
|
"test:unit:watch": "vitest",
|