Design editor
Architecture — four altitudes, one state
The map
Most editor SDKs make you choose between a widget you can't change and a pile of low-level APIs you must assemble. Snowcone ships one stack with four altitudes — each a complete, supported way in, each built on the one below it. Start anywhere; move down only where you need more control.
The Snowcone editor stack
Four altitudes. One design state.
Drop-in<SnowconeEditor />
The whole editor, one tag. Header, canvas, live mockup, buy pill.
ComposeEditor.* parts
Behavior-carrying parts in YOUR layout — style every pixel via the published contract.
HooksuseLayers · useSelectedElement · bindings
State and commands, zero DOM of ours. Build any grammar — PDP, chat remix, Figma-style workspace.
Wiredesign() → live render
No browser, no React. Compose a design in code and a finished product photo streams back.
01 · Drop-in — SnowconeEditor
One tag, the whole product editor: history, overflow menu, the completeness-gated buy pill, the editable canvas, and a live server-rendered mockup. Theme it with CSS custom properties; persist the design state from one callback. Details: Editor primitives.
// Altitude 01 — the whole editor, one tag. Header (history · menu · buy
// pill), editable canvas, live buyable mockup. Internally it is NOTHING but
// the altitude-02 parts in our arrangement (CI-enforced).
import { SnowconeEditor, design } from '@snowcone-app/canvas/embed';
import '@snowcone-app/canvas/style.css';
const d = design({ name: 'Front', width: 1480, height: 2328 })
.background('https://dqMwU8Jct3.storage.snowcone.app/demo/demo-008.jpg', {
aspectRatio: 1480 / 2328,
name: 'art',
})
.text('YOUR NAME', { anchor: 'top', name: 'headline' });
export function ProductPage() {
return (
<SnowconeEditor
shop="YOUR_SHOP_ID"
grantUrl="/api/realtime/grant"
product={{ productId: 'BEEB77', mockupIds: ['FV1qjO'], variantId: 'Pv1sLC', placements: ['Front'] }}
design={d}
onAddToCart={({ designState }) => saveDraft(designState)}
/>
);
}02 · Compose — Editor.* parts
The same behaviors as unstyled parts you arrange yourself: Editor.Root owns the session and design state; Editor.Preview, Editor.TextField, Editor.Buy, Editor.Canvas and friends mount anywhere under it. Your page owns every pixel of layout; styling goes through the published styling contract (stable data-sc-part hooks + .sc-root theme primitives). Compositions without Editor.Canvas never download the canvas engine.
// Altitude 02 — the same behaviors as parts in YOUR layout. Each part
// carries state subscription and contracts; your page owns every pixel of
// layout and styles via the published [data-sc-part] contract. This
// composition never loads the canvas engine at all.
import { Editor, design } from '@snowcone-app/canvas/embed';
import '@snowcone-app/canvas/style.css';
const d = design({ name: 'Front', width: 1480, height: 2328 })
.background('https://dqMwU8Jct3.storage.snowcone.app/demo/demo-008.jpg', {
aspectRatio: 1480 / 2328,
name: 'art',
})
.text('YOUR NAME', { anchor: 'top', name: 'headline' });
export function PersonalizationWidget() {
return (
<Editor.Root
shop="YOUR_SHOP_ID"
grantUrl="/api/realtime/grant"
product={{ productId: 'BEEB77', mockupIds: ['FV1qjO'], variantId: 'Pv1sLC', placements: ['Front'] }}
design={d}
>
<Editor.Preview alt="Live product mockup" />
<Editor.TextField layer={d.layer('headline')} placeholder="Your name" />
<Editor.Buy onAddToCart={({ designState }) => saveDraft(designState)}>
Add to cart
</Editor.Buy>
</Editor.Root>
);
}03 · Hooks — your DOM entirely
Below the parts sits the headless floor: hooks that expose editor state and commands and render nothing. useLayers, useSelectedElement, useProductSelection, and the layer bindings (useTextBinding, useImageBinding, useColorBinding, useFontBinding) are what the parts themselves are built from. This is the altitude for product grammars we didn't anticipate — a Figma-shaped workspace, a conversational remix flow, a kiosk.
// Altitude 03 — state and commands with zero DOM of ours. Render anything:
// a Figma-style layers rail, a chat-shaped remix flow, a kiosk. Runs under
// <Editor.Root> with an <Editor.Canvas/> mounted somewhere in the tree.
import { useLayers, useSelectedElement } from '@snowcone-app/canvas/embed';
export function LayerStrip() {
const { flatLayers, selectLayer } = useLayers({ previewSize: 64 });
const selected = useSelectedElement();
return (
<nav>
{flatLayers.map((layer) => (
<button
key={layer.id}
aria-pressed={selected?.id === layer.id}
onClick={() => selectLayer(layer.id)}
>
{layer.name}
</button>
))}
</nav>
);
}04 · Wire — no browser at all
The floor under everything is pure data: compose a design by intent with design() and render it server-side over a WebSocket — about 1 KB of state up, a finished product photo back. No React, no DOM, no pixel upload. This is the altitude agents, scripts, and backends integrate directly, and it's the same state every higher altitude emits. Details: Realtime rendering.
// Altitude 04 — no browser, no React. Compose a design by intent and a
// finished product photo streams back. This is the layer agents and servers
// integrate directly.
import { RenderSession } from '@snowcone-app/sdk';
import { design } from '@snowcone-app/canvas';
const session = new RenderSession({
shop: 'YOUR_SHOP_ID', // publishable
grantUrl: '/api/realtime/grant', // your proxy → mintRealtimeGrant
product: { productId: 'BC43BE', mockupIds: ['yoALRE'] },
});
session.onMockups((results) => { img.src = results[0].imageUrl; });
const d = design({ name: 'Front', width: 1480, height: 2328 })
.background('https://dqMwU8Jct3.storage.snowcone.app/demo/sunset.avif', {
aspectRatio: 1480 / 2328,
})
.text('YOUR NAME', { anchor: 'top' })
.shape('star', { anchor: 'bottom-left', fill: '#000' });
await session.renderDesign('Front', d, 200);Why descending is safe
The four altitudes are not four products — they are one state model with four amounts of supplied UI. The drop-in editor IS the parts in our arrangement (enforced in CI: the default editor must build from the public surface, so nothing is held back as private API). The parts ride the hooks. Everything serializes to the same design state that the wire renders, the cart persists, and the print file is produced from. Moving down an altitude is recomposition, never a rewrite — and a design authored at any altitude opens at every other one.
Pick your altitude
Ship a product editor today → SnowconeEditor (01). Your layout, your brand, standard editor behaviors → Editor.* parts (02). A product grammar of your own — every pixel and interaction yours → the hooks (03), with parts mixed in where they fit. No UI at all — agents, automations, server-side catalogs → design() + realtime rendering (04). When in doubt start at 01; descending later costs a recomposition, not a migration.

