# Architecture This document describes the **architecture as it stands**. For task-oriented recipes (adding a canonical, adding an adapter, conventions, gotchas), see [`DEVELOPER_GUIDE.md`](./DEVELOPER_GUIDE.md). --- ## Overview craftjs-design is a drag-and-drop website builder. Its central design idea is to **separate three concerns that most builders mix up**: 1. **What the user is composing** — an abstract tree of "components" (Button, Input, Box, Card…). 2. **Which UI library renders those components** — shadcn, MUI, Chakra, or a custom kit. 3. **How the tree is edited** — selection, drag/drop, undo/redo (handled by [Craft.js](https://craft.js.org/)). Most builders couple #1 and #2: the user picks a "Button" from a palette, but that Button is hard-wired to whichever library was chosen at project setup. Swapping libraries means rebuilding documents. This architecture decouples them via a **Canonical Component Registry** (the abstract palette) sitting above an **Adapter SDK** (the per-library renderers). Documents reference canonical ids only. **Swapping adapter ≠ migrating documents.** --- ## Four-Layer Model ``` ┌─────────────────────────────────────────────────────────────────┐ │ Editor UI (Toolbox, Inspector, Canvas chrome) │ ├─────────────────────────────────────────────────────────────────┤ │ Canonical Component Registry (abstract Button, Input…) │ ├─────────────────────────────────────────────────────────────────┤ │ Adapter Layer (shadcn / MUI / Chakra → canonical) │ ├─────────────────────────────────────────────────────────────────┤ │ Craft.js kernel + Document JSON │ └─────────────────────────────────────────────────────────────────┘ ``` Each layer talks only to its immediate neighbor. The discipline is enforced by the type contracts in `src/registry/types.ts` and `src/adapters/types.ts` — those are the *only* legal vocabulary between layers. ### Layer 1 — Editor UI (`src/editor/`) The user-facing chrome. Has no opinion about *what* components exist, only about *how to edit them*: panels, toolbars, the canvas frame. | File | Role | |---|---| | `Editor.tsx` | Top-level shell. Builds the resolver, mounts Craft.js, wraps the canvas in ``, lays out the 3-column UI. On mount, calls `_markEditorMounted()` so the registry can warn about post-mount canonical registrations. | | `Toolbox.tsx` | Left panel. Reads `listComponents()` from the registry; renders entries grouped by `category` with a search input, a "Favorites" section (toggle via star icon), and a "Recently used" section. Favorites + recents persist to `localStorage['craftjs-design.toolbox']` — user-level state, separate from the document envelope. Attaches Craft `connectors.create()` per entry; mousedown on a button records use into the recents LRU. | | `Inspector.tsx` | Right panel. Reads the selected node from Craft state, shows type/id, exposes Delete (root-guarded), mounts the `ResponsiveBar`, mounts a `SlotPicker` when the canonical declares more than one style slot, and renders panels from the panel registry (`getPanelsFor(def)`). Tracks `activeSlot` (`'root'` by default). Resize handles are rendered as a canvas overlay (see `canvas/ResizeOverlay.tsx`), not inside the inspector. | | `canvas/ResizeOverlay.tsx` | Fixed-position overlay rendered over the selected node's bounding rect. Four corner handles. Mutates `dom.style.width/height` directly during drag (60fps), commits final px to `style.inline.root.{width,height}` via `setProp` on release. Tracks node position on selection change, scroll (capture phase), window resize, and `ResizeObserver` ticks. | | `documents/DocumentMenu.tsx` | Top-bar dropdown showing the active document's name + chevron. Inline rename, duplicate, delete actions for the active doc; "New blank document", nested ``, and a "Switch to" list of other documents. | | `documents/TemplatePicker.tsx` | Nested popover listing registered starter templates (name + description). Clicking a template invokes the supplied `onPick` and closes. | | `documents/useDocumentSwitcher.ts` | Hook orchestrating runtime document switches. `switchTo(id)` / `createBlank(name)` / `createFromTemplate(id, name)`. Each one snapshots the current canvas via `query.serialize()`, persists to the active doc, swaps `activeId`, loads the target's blob (or the Empty template seed), calls `actions.deserialize`, and applies theme + adapter. | | `ResolverUpdater.tsx` | Side-effect component rendered inside ``. Subscribes to the registry version counter via `useSyncExternalStore` and calls `actions.setOptions((opts) => { opts.resolver = getResolver() })` on every bump. Powers hot canonical reload — `registerCanonical` at runtime updates Craft's internal resolver without remounting. | | `ShareButton.tsx` | Toolbar Share button. Popover renders the encoded URL ready to copy. Over the 30 KB cap, switches to "Copy as JSON" with a paste-into-importer message. | | `inspector/ResponsiveBar.tsx` | Six breakpoint pills (`base` / `sm` / `md` / `lg` / `xl` / `2xl`). Active pill = which class slice the panels read/write. Loud "writing to: …" status line warns when an edit will only apply at and above a breakpoint. | | `inspector/SlotPicker.tsx` | Pill bar above the panels for Pattern B canonicals (`styleSlots.length > 1`). Switches `activeSlot` (`'root'`, `'header'`, `'body'`, `'footer'`, etc.). Resets to `'root'` on selection change. | | `inspector/panel-registry.ts` | Pluggable panel registry. `registerPanel` / `unregisterPanel` / `listPanels` / `getPanelsFor`. Inspector reads from this — both built-ins and SDK-authored panels live here. | | `inspector/built-in-panels.ts` | Side-effect module that registers the 7 built-in panels (Layout / Size / Spacing / Typography / Appearance / Effects / Properties) via `registerPanel` at module load. Imported from `App.tsx`. | | `inspector/{Typography,Layout,Spacing,Size,Appearance,Effects}Panel.tsx` | The 6 class-editing panels. Each accepts `{ nodeId, slot }`. Backed by the matching `tw-classes` slice parse/merge pair. | | `inspector/PropsPanel.tsx` | Auto-form derived from each canonical's Zod `propsSchema`. Top-level component dispatches one `PropField` per schema entry; recursive `PropField` handles `ZodEnum` / `ZodString` / `ZodBoolean` / `ZodNumber` / `ZodArray` / `ZodObject`. Unsupported kinds render a labeled badge. | | `inspector/fields/PropField.tsx` | Recursive Zod-kind dispatcher extracted from PropsPanel. Used at the top level by PropsPanel and recursively by ArrayField / ObjectField when descending into nested element schemas. | | `inspector/fields/ArrayField.tsx` | `z.array(z.object/scalar)` editor — stacked item cards with `↑ / ↓ / 🗑` controls and a "+ Add" button. Caps at one level: `z.array(z.array(…))` shows "unsupported deep nesting". | | `inspector/fields/ObjectField.tsx` | `z.object` editor used by ArrayField when the element is an object. Renders the sub-schema's fields recursively via PropField. | | `inspector/fields/defaults.ts` | `defaultValueFor(schema)` — seeds new items when the "+ Add" button fires. | | `inspector/shared/useNodeClasses.ts` | Read/write helper. Signature: `useNodeClasses(nodeId, slot = 'root')`. Returns `classString` (active breakpoint, scoped to the slot), `inlineStyle` (active-breakpoint arbitrary CSS for the slot), `writeClasses`, `writeInline`. At non-base, reads/writes route through `style.responsiveInline[bp][slot]`. Funnels every inspector style I/O through one place. | | `inspector/shared/ColorPicker.tsx` | Popover with three sections: token swatch grid, `react-colorful` visual picker (sat/lightness + hue), hex text input. Tagged-union `ColorPickerValue` (`token` / `hex` / `unset`). Works at every breakpoint. | | `inspector/shared/NumericInput.tsx` | Hybrid text input accepting tokens or arbitrary CSS values (`13px`, `50%`, `1.5rem`). Step buttons walk the token scale; Popover dropdown for picking. Works at every breakpoint. | | `inspector/shared/BoxSidesEditor.tsx` | Linked-corners editor (padding / margin). Linked mode uses `NumericInput`; per-side mode uses `ValueSelect` (token-only). | | `inspector/shared/CollapsibleSection.tsx` | Native `
`/`` wrapper used by Inspector to make each panel collapsible. | | `inspector/shared/ValueSelect.tsx` | Generic typed Select (Radix-backed shadcn Select) for closed enums. Supports per-item `renderOption` for icons/swatches. | | `inspector/shared/ColorSelect.tsx` | **Deprecated** — superseded by ColorPicker. Token-only native `