Design editor

Architecture — four altitudes, one state

The editor is one stack with four entry points, from a drop-in component to a pure design-to-render wire. Every altitude reads and writes the same design state, so you descend for control without rewriting.

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.

01

Drop-in<SnowconeEditor />

The whole editor, one tag. Header, canvas, live mockup, buy pill.

02

ComposeEditor.* parts

Behavior-carrying parts in YOUR layout — style every pixel via the published contract.

03

HooksuseLayers · useSelectedElement · bindings

State and commands, zero DOM of ours. Build any grammar — PDP, chat remix, Figma-style workspace.

04

Wiredesign() → live render

No browser, no React. Compose a design in code and a finished product photo streams back.

Descend without rewriting — every altitude reads and writes the same design state.CI-enforced — the drop-in editor is built from the public parts, so nothing is held back.
The four layers of the Snowcone editor stack: SnowconeEditor drop-in, Editor.* parts, headless hooks, and the pure design-to-render wire — all sharing one design state.

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.

tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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 behaviorsEditor.* 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 catalogsdesign() + realtime rendering (04). When in doubt start at 01; descending later costs a recomposition, not a migration.