# Integration Guide How to embed the editor in your own React app. The shipped artifact is a Vite library-mode bundle: an ES module exporting `` plus the full SDK. React, React DOM, and Craft.js are externalized as peer dependencies — your host app provides them. This guide assumes a Vite + React 19 host (the editor's own development target). Older React versions are not supported — the React 19 ref-as-prop semantics + the unified `Fragment` are load-bearing. ## Install ```bash npm install @crafted-design/editor react@19 react-dom@19 @craftjs/core@^0.2.12 ``` `@mui/material`, `@emotion/react`, and `@emotion/styled` are **optional peer dependencies** — they are NOT bundled. Install them only if you use the full `@crafted-design/editor` entry (which registers the MUI adapter) or import `/adapters/mui`; the lean `/core` entry (shadcn + plain-HTML) needs no extra peers. See *Subpath exports* below. ## Subpath exports Since `0.7.0` the package is modular — pick the entry that matches the adapters you want, so you don't bundle a UI library you never render: | Import path | What you get | External peers | |---|---|---| | `@crafted-design/editor` | **Full** `` — registers editor + shadcn + plain-HTML **+ MUI**. The batteries-included default. | requires `@mui/material`, `@emotion/react`, `@emotion/styled` | | `@crafted-design/editor/core` | **Lean** `` — registers editor + shadcn + plain-HTML, **no MUI**. Same full export surface (Editor, SDK, stores, doc helpers). | none | | `@crafted-design/editor/adapters/shadcn` | Side-effect import that registers just the shadcn adapter. | none | | `@crafted-design/editor/adapters/html` | Registers just the plain-HTML adapter (no UI library). | none | | `@crafted-design/editor/adapters/mui` | Registers just the MUI adapter. | `@mui/material`, `@emotion/react`, `@emotion/styled` | | `@crafted-design/editor/sdk` | SDK-only surface (`registerAdapter`, `registerCanonical`, `registerPanel`, `registerTheme`, `registerTemplate`, `registerFontToken`, `useNodeClasses`, all the matching types). No editor UI — use when authoring a canonical / adapter / panel without pulling in ``. | none | | `@crafted-design/editor/index.css` | Tailwind CSS bundle (global preflight + `:root` tokens). Import once per page; no JS overhead. | none | | `@crafted-design/editor/index.scoped.css` | Same stylesheet, every rule scoped under `.crafted-design-scope` — for embedding inline in a Tailwind-v4 host without a double preflight / token clobbering. Use *instead of* `index.css`. See [Inline embedding](#inline-embedding-into-a-tailwind-v4-app-170). | none | Typical setups: ```ts // shadcn-only host — no MUI in the bundle, nothing extra to install import { Editor } from '@crafted-design/editor/core' import '@crafted-design/editor/index.css' // want MUI too — install the peers, use the full entry // npm install @mui/material @emotion/react @emotion/styled import { Editor } from '@crafted-design/editor' // lean core + opt into one extra adapter explicitly import { Editor } from '@crafted-design/editor/core' import '@crafted-design/editor/adapters/mui' // side-effect: registers MUI ``` Opt-in is at the **import boundary**, not at runtime: importing an adapter registers it before `` mounts. (Registering an adapter after mount would reshape the provider tree and remount the canvas, so it isn't supported.) `.d.ts` files ship alongside every JS entry, so TypeScript hosts resolve types without configuration. See [ADAPTER_VERSIONING.md](./ADAPTER_VERSIONING.md) for the peer-dependency policy and [ADAPTER_MATRIX.md](./ADAPTER_MATRIX.md) for per-adapter coverage. ## Bundle format The package ships **ESM only**, **unminified, with source maps**. There is no CommonJS/UMD build and no separate `*.min.js` — both are deliberate: - **ESM-only** avoids the dual-package hazard; modern bundlers and Node ≥ 20 consume ESM directly. - **Unminified** because you consume the editor through your own bundler, which minifies the final app. Shipping a parallel minified entry would double the published surface and the `exports` map for no real benefit, and the source maps give you readable stack traces in development. The SDK subpath (`/sdk`) is **side-effect-free**, so a bundler tree-shakes any authoring symbol you don't import. (Importing `/sdk` registers nothing beyond the editor's three baseline font tokens — `sans`/`heading`/`mono`.) ## Minimal embed ```tsx import { Editor } from '@crafted-design/editor' import '@crafted-design/editor/index.css' function App() { return } export default App ``` The editor takes 100% of its parent's height (it uses `h-screen` internally). Wrap in a container if you want it to share screen real estate: ```tsx function App() { return (
) } ``` ## Embedding as a controlled component (1.6.0) By default `` is a self-contained app: it owns its document, persists to IndexedDB, and shows its own Save/Load chrome. To embed it inside your own UI — a step in a multi-step form, a drawer, a tab — drive it as a **controlled component** instead. All of these props are additive and optional; with none passed, behavior is identical to the minimal embed above. ```tsx import { Editor, type EditorHandle } from '@crafted-design/editor/core' import type { EditorDocument } from '@crafted-design/editor/core' function CardEditor() { // The host owns the document. const [doc, setDoc] = useState(seedFromYourBackend) return ( setDoc(next)} // debounced; persist with JSON.stringify(next) persistence={false} // never touch the built-in IndexedDB store hideChrome // drop the Save/Load bar — render your own /> ) } ``` | Prop | Type | Effect | |---|---|---| | `value` | `EditorDocument \| string` | **Controlled.** The document the editor renders; re-seeds whenever its identity changes. Persistence is forced off. | | `defaultValue` | `EditorDocument \| string` | **Uncontrolled** one-time seed on mount; edits stay internal, surfaced via `onChange`. Ignored when `value` is set. | | `onChange` | `(doc: EditorDocument) => void` | Fired (debounced) on every change — structural edits **and** prop/style edits. The same envelope `Export` produces. | | `onChangeDebounceMs` | `number` | Debounce window for `onChange`. Default `150`. | | `persistence` | `boolean` | Whether the editor manages its own IndexedDB store/autosave. Default `true`. `value` implies `false`. | | `hideChrome` | `boolean` | Hide document-management chrome (Save/Load/Import/Export/Share bar, onboarding tour, quota banners, cross-tab watcher). Keeps toolbox + canvas + inspector. | Both `value` and `defaultValue` accept an `EditorDocument` envelope **or** its JSON string — each is validated + migrated on the way in, exactly like an Import. Build a seed without an editor using the headless [`buildDocument`](./SDK_GUIDE.md), or feed a string straight from your backend. **No feedback loop.** The natural controlled wiring — `edit → onChange → setState → new value → re-apply` — does **not** loop: the editor tracks the last serialized tree and skips re-applying a `value` it already produced. Re-applying an identical envelope is a no-op. **Reading on demand (imperative ref).** Redundant with `onChange` but convenient for a "serialize on click" button without holding the doc in state: ```tsx const ref = useRef(null) // … ``` A runnable end-to-end example (controlled `value` + `onChange` + ref + a live `` preview) lives in [`examples/controlled-host`](../examples/controlled-host). > **CSS isolation.** This controlled API removes the persistence/chrome/seed > machinery. To embed **inline** in an app already running Tailwind v4 (no > iframe), import the scoped stylesheet — see > [Inline embedding into a Tailwind-v4 app](#inline-embedding-into-a-tailwind-v4-app-170) below. ## Inline embedding into a Tailwind-v4 app (1.7.0) The default stylesheet `@crafted-design/editor/index.css` is a full Tailwind v4 build — a global preflight (the `*` reset) + the editor's design tokens. **Tokens don't clobber yours (1.8.2+).** The editor's document tokens (`--primary`, `--background`, `.dark`, `[data-theme]`) ship in a cascade layer (`@layer crafted-design`), so your app's **unlayered** `:root` / `.dark` tokens always win — importing `index.css` no longer overrides your brand colors app-wide. (The editor's `--ed-*` *chrome* tokens stay unlayered, but a host has no `--ed-*` to collide with.) The trade-off: because your `:root` wins everywhere, the editor **canvas** also inherits your brand tokens, and a preflight is still global. For full subtree isolation — host tokens never reach the canvas, no second preflight — use the **scoped** stylesheet instead: ```tsx // in a Tailwind-v4 host — INSTEAD of index.css: import '@crafted-design/editor/index.scoped.css' ``` Every rule in it is prefixed with `.crafted-design-scope` (and the editor's `:root` tokens are rehomed onto that class), which `` and `` put on their root. So: - The editor's preflight resets **only inside** the editor subtree — your page isn't double-reset. - The editor's tokens live **only inside** `.crafted-design-scope` — your host's `:root` / `--color-*` tokens are untouched, and vice-versa. - Runtime overlays (Modal/Drawer/Toast) portal into a scope-classed container, so they're styled correctly even though they're DOM-detached. **When to use which:** | Host | Stylesheet | |---|---| | Want the editor canvas fully isolated from host tokens (and no double preflight) | `index.scoped.css` | | Fine with the canvas inheriting your brand tokens; just don't want your `:root` clobbered | `index.css` (global — tokens are layered) | | No CSS framework / standalone / its own route / iframe | `index.css` (global) | Notes & limits: - The scoped sheet **omits a global preflight** — it assumes the host already has one (Tailwind v4). A host with no reset at all should use `index.css`. - The editor owns its look: scoping makes the editor's utilities un-overridable by host CSS (intended). Theme the **canvas** via `registerTheme` and the **chrome** via `editorTheme`, not by overriding editor utilities. (`editorTheme` works under the scoped sheet too — the `--ed-*` chrome tokens stay global so the prop's inline values still apply; 1.8.3+.) - The **MUI** adapter renders overlays via MUI's own portals (emotion-styled), outside the Tailwind scoped sheet; the scoped sheet targets the shadcn / html (Tailwind) stacks. `examples/controlled-host` embeds inline with the scoped sheet. ## Pinning the adapter (host-chosen design system) The product model is that **you** — the host — choose the design system; the people using your editor don't. Pin it with the `adapter` prop: ```tsx import { Editor } from '@crafted-design/editor' // full entry registers MUI import '@crafted-design/editor/index.css' function App() { return } ``` What pinning does: - The active adapter is set to `mui` before first paint. - The **AdapterSwitcher disappears** from the toolbar — end users can't change the design system. - **Loading a document does not override it.** A document saved under shadcn still opens — documents store canonical ids, not library components, so it simply renders through MUI. The envelope's `adapterId` is a preference, not a command, while pinned. > ⚠ **MUI requires its peers.** The MUI adapter (whether via the full entry or > `/adapters/mui`) needs the optional peer dependencies installed: > > ```bash > npm install @mui/material @emotion/react @emotion/styled > ``` > > Pinning `adapter="mui"` without registering the MUI adapter (or without the > peers, which makes its import fail) logs a console warning and falls back to > the default `shadcn`. Want to pin a starting adapter but still let users switch? Both knobs are independent: ```tsx // starts on plain HTML, switcher stays // default adapter (shadcn), no switcher // legacy behavior: switcher shows all registered adapters ``` `allowUserToSwitchAdapter` defaults to `false` when `adapter` is set, `true` otherwise. ## Customizing the registry The editor pre-registers 48 canonicals, the built-in adapters (shadcn + MUI + plain-HTML on the full entry; shadcn + plain-HTML on `/core`), 7 themes, inspector panels, and starter templates. Override any of these by calling the SDK BEFORE rendering ``: > **Adapter coverage policy.** The three built-in adapters (shadcn, MUI, > plain-HTML) implement **every** canonical — see > [ADAPTER_MATRIX.md](./ADAPTER_MATRIX.md). The in-repo Chakra adapter is an > *example* (a third-party-adapter demo covering a 20-canonical subset) and is > NOT part of the published package. When a document uses a canonical the > active adapter doesn't implement, the node renders a labeled placeholder > (` — no impl in adapter ""`) instead of crashing, so you > can swap adapters or remove the node. ### Remove a built-in canonical ```tsx import { Editor, unregisterCanonical } from '@crafted-design/editor' unregisterCanonical('alert') // drops Alert from the toolbox function App() { return } ``` ### Add a custom canonical ```tsx import { z } from 'zod' import { Editor, registerCanonical } from '@crafted-design/editor' registerCanonical({ id: 'callout', category: 'feedback', displayName: 'Callout', tags: ['alert', 'banner'], isCanvas: true, styleSlots: ['root'], propsSchema: z.object({ intent: z.enum(['info', 'warning', 'success']), }), defaults: { props: { intent: 'info' }, style: { classes: { root: 'p-4 rounded-md border' } }, }, }) // (Add adapter impls for your supported adapters too.) ``` ### Add a custom adapter ```tsx import { Editor, registerAdapter, type AdapterRenderProps } from '@crafted-design/editor' function MyBox({ children, rootRef, className }: AdapterRenderProps) { return
{children}
} registerAdapter({ id: 'mylib', displayName: 'My Library', components: { box: MyBox }, }) ``` ### Add a custom inspector panel ```tsx import { Editor, registerPanel, useNodeClasses } from '@crafted-design/editor' function NotesPanel({ nodeId }: { nodeId: string }) { const { classString, writeClasses } = useNodeClasses(nodeId) return (