Embedding the editor

Props, the canvas host, replacing parts of the UI, save errors and events.

On this page

@pagehub/editor is a React component. You give it a storage adapter for your files and a canvas host for the things around them (your stylesheet, fonts, a preview link), and it does the rest. It never fetches or hard-codes a URL itself.

Install

Terminal
pnpm add @pagehub/editor @pagehub/engine react react-dom

React 19 is a peer dependency. Runnable examples (plain HTML, Vite + React, Next.js) are in PageHubApp/demos.

Styles

The editor ships its chrome stylesheet compiled. Import it once:

CSS
/* app.css */
@import "@pagehub/editor/styles.css";

To compile it in your own Tailwind v4 + DaisyUI build instead, import the source, @pagehub/editor/styles.tailwind.css, from a Tailwind entry file.

Everything is scoped to the editor ([data-ph-editor-scope], which <Editor> puts around its UI and its overlays), and there's no global reset. Embedding it never restyles your page. If your own markup needs Tailwind's global base, import tailwindcss/preflight.css yourself.

Mount it

In React:

TypeScript
import { Editor } from "@pagehub/editor";

export function Page() {
  return (
    <div style={{ height: "100vh" }}>
      <Editor storage={storage} canvasHost={canvasHost} onEvent={onEvent} />
    </div>
  );
}

Without writing React:

TypeScript
import { mountEditor } from "@pagehub/editor";

const { unmount } = mountEditor(document.getElementById("editor")!, { storage, canvasHost });

The editor fills its parent, so give the parent a height. While the files load, it shows a skeleton. If the storage can't be opened, it shows an inline error with Retry.

Props

PropWhat it does
storageA storage adapter. <Editor> opens the site from it.
editorInstead of storage: an editing core you opened yourself with createEditor. Its extensions must include editorKit().
extensionsThe kits and plugins for the site. Default: htmlKit(), tailwindKit(), liquidKit(). See extending.
role"owner" (default), "editor", "client" (content mode: text and images only, no structure changes) or "viewer" (read-only).
dataSample data for data-bound pages in the canvas.
identity"session" (default) or "persisted" (data-edit-id stored in the files).
canvasHostWhat the canvas needs from you. See below.
canvasRenderReplace how the canvas renders a page (for example in a worker). Default: the engine's tags and filters over the editor's data.
componentsReplace or hide any piece of the UI. See below.
onEventReceives saved, conflict, selection, diff and change events.
onReadyCalled with the core once it's open.
onPaintedCalled once the canvas has painted its first page (or failed to), so you can lift your own loading screen.
autosavetrue (or an options object) saves shortly after edits stop and when the person leaves. Off by default: ⌘S and your own save.
themeThe chrome's color scheme, "light" or "dark". Default: the nearest data-theme around the editor, else the OS setting.
rightDockOffsetPixels of your own sidebars on the editor's right edge. See below.
platformPin the shortcut key to "mac" or "other". Detected by default.
activefalse while you hide the editor, so it handles no keys.
classNameClass for the root element.

The canvas host

The canvas shows the page as visitors see it, so it needs things only you know. Each member is optional, and a member you leave out is simply not done.

MemberUsed forIf left out
siteCss(state)The site's compiled stylesheet, inlined into every canvas frame. Kept per editor and refreshed in the background.The page paints with only its own <head>.
fonts(urls)Self-hosted copies of the theme's web fonts ({ css, preload }, or null).The remote font <link> tags.
runtimeUrlThe site runtime module, booted in each frame in edit mode.No runtime in the canvas.
previewUrl(route)The "Open in new tab" link on the page pill, and the live preview that runs the site's own scripts.No link.
pages()The site's page records (file, route, name), so the page selector shows the routes you serve.Each page's route comes from its file path.
hostimage_url, file_url, asset_url and icon filter providers.URLs pass through, and an image id paints a placeholder.
timezoneThe site's IANA timezone, which | date renders in.UTC.
data()Sample or connector data, merged under editor.data.None.
TypeScript
import type { CanvasHost } from "@pagehub/editor";

const canvasHost: CanvasHost = {
  siteCss: async () => (await fetch("/site.css")).text(),
  runtimeUrl: "/assets/runtime.js",
  previewUrl: (route) => `/preview${route}`,
  host: { imageUrl: (id, { width }) => `https://cdn.example.com/${id}?w=${width}` },
};

devCanvasHost in @pagehub/dev-server/client is a complete implementation over the dev server's endpoints.

Replace parts of the UI

Every visible piece is a named slot. Replace one with your own component, or hide it with null:

TypeScript
<Editor storage={storage} components={{ TopBar: MyTopBar, LayersPanel: null }} />

The slots are TopBar, PageSelector, BreakpointSwitcher, SaveStatus, Sidebar, AddPanel, LayersPanel, Inspector, InspectorSection, Breadcrumb, Canvas, ComponentBoard, CanvasOverlays, NodeTools, InlineTextToolbar, ContextMenu, CommandPalette, MediaManager, ThemePanel, ModifiersPanel, ScriptsPanel, InspectorDetail, ColorPicker, IconPicker, FontPicker, CodeView, DiffPreview, SaveBanner and EmptyState. The props each receives are exported as types (SlotProps, MediaManagerProps, PanelProps and so on).

  • @pagehub/editor/components exports the default components, so you can wrap one instead of rewriting it.
  • @pagehub/editor/primitives exports the building blocks for new panels: Modal, Drawer, FloatingPanel, AnchoredPopover, Chip, PopoverChip, the toolbar controls, MenuSurface, Skeleton, Spinner, InlineError, Icon and tooltip.
  • Inside a slot, useEditorStore, useEditorState, useRunCommand and useUi read and drive the editor.

Media is yours to provide. MediaManager is null by default, so the Media panel and the image picker stay inert until you pass one. The editor exports the standard manager from @pagehub/editor/components:

TypeScript
import { MediaManager } from "@pagehub/editor/components";

<Editor storage={storage} components={{ MediaManager }} />

The manager reads its library from the mediaFacet in @pagehub/engine/host. Other catalogues you can supply there: icons, fonts, theme colors, blocks, theme presets and font pairings.

The right-hand drawer

The big panels (Theme, Media, Modifiers, Scripts) and the inspector's larger editors (Show only when, Repeat for each, Edit HTML, Action, Event handlers) open in one drawer docked to the editor's right edge, one at a time. It pushes the canvas aside instead of covering it, and Esc closes it.

If your app has sidebars on the same edge, tell the editor how wide they are:

TypeScript
<Editor storage={storage} rightDockOffset={320} />

The editor reserves that many pixels on its right as padding, so the canvas never runs under your sidebars and the drawer opens just inboard of them. The default is 0. Change the number whenever your sidebars open, close or resize.

Code
┌──────────────────────────────── editor ─────────────────────────────────┐
│ top bar                                                                  │
├──────────┬──────────────────────────────┬───────────┬────────────────────┤
│ sidebar  │ canvas (shrinks)             │ drawer    │ rightDockOffset    │
│          │                              │           │ (your sidebars)    │
└──────────┴──────────────────────────────┴───────────┴────────────────────┘

Save errors

When the storage throws a StorageError, the save banner and the top bar's save icon say what happened, using the error's code:

  • maintenance is editing paused, not a failure. It shows an amber status (with the storage's detail text if it gave one), keeps the draft in the editor and offers Try again. It clears itself on the next successful save.
  • network says the server can't be reached, with Try again.
  • format_changed says the site was upgraded and the page must be reloaded.
  • forbidden, not-found, too-large and conflict each say what happened, with no Try again. Any other failure is a generic message with Try again.

If your storage has writeBatch, a save is one call, so a conflict on any file saves none of them. The conflict banner (Reload theirs / Keep mine) lists the stale files.

Events

TypeScript
<Editor
  storage={storage}
  onEvent={(event) => {
    if (event.type === "saved") console.log("wrote", event.written);
    if (event.type === "change") markDirty(event.dirty); // unsaved paths, after 500 ms of quiet
  }}
/>
typeFieldsWhen
changedirtyThe text changed and went quiet. dirty is what a save would write.
savedwrittenA save wrote files.
conflictpath, theirs, versionA file collided with a newer version.
selectionhandles, primaryThe selection changed.
difffilesEvery change, with the pending diff per file.