AI agents
Pitfalls and invariants
Rules that must stay true when integrating or changing @skaile/forge-editor: CSS order, mandatory context, SSR, mount-time props, bundle isolation, entries.
Check this list before and after every change to an integration. Each bullet is a failure that does not announce itself at compile time.
- CSS order.
core.cssafter Tailwind and Nuxt UI, feature sheets after it,themes.csslast, and@sourceso Tailwind sees the library. The exact order per host is in CSS and UI-provider requirements. - Editor context is mandatory.
EditorSurface,SimpleEditor,AdvancedEditorandEditorToolbarthrow withoutapp.use(createEditor())(the Nuxt module does this in its client plugin); the React components throw without<EditorContextProvider context={createEditorContext()}>, and the React host owns (disposes) that context. - Notices. Features send user-facing messages through
ctx.notify/ the context'snotify(Notifier,silentNotifierdrops them). Vue: the queued notifier hands them to Nuxt UI toasts once a surface mounted (needs<UApp>). React: passcreateEditorContext({ notify: reactNotifier })and mount sonner's<Toaster />(exported by/react) once; without it no notice is visible. - UI provider. Wrap the page in
<UApp>; plain Vue also needsapp.use(ui)(@nuxt/ui/vue-plugin) and theui({ router: false })Vite plugin. - SSR. Editors are client-only. Nuxt apps in this repository run
ssr: false; with SSR enabled wrap editors in<ClientOnly>. - Mount-time props. The preset (schema, features),
collaboration,abilities,extensions,commands,blockActions,blockTypes,blockUi,coreContent,documentIdare read once at setup. Change them by remounting with a newkey.editable,metadataand the model stay reactive. Build presets in<script setup>(Vue) or a component/useMemo(React), not on every render. - Stable props. Pass
metadata,abilitiesandextensionsas constants; a new object per render replaces the lists each time. - Schema match. A simple editor rejects advanced JSON through the
contentErrorevent (the last valid document stays).collaborationneeds a preset containingcollab;commentsFeature()needscollab. - Bundle isolation.
core/settingsimport no feature; a feature importscore,settingsand its ownschema/protocol, never another feature; integrations import only the features they connect; thesimplebundle contains no feature code, Yjs, AI SDK, Nuxt or server modules. Keep it that way when adding code, and prove it withcheck:editor-entries. Rules: Dependency rules. - Entries. Server code imports only framework-free entries (
/schema,/protocol,/types,/settings) or/server. A new public entry needs an explicitpackage.json#exportsrow; there is no wildcard. - Icons.
i-lucide-*names resolve at build time inside the Vite root; a workspace library lies outside it. Bundle them (seeexamples/vue-simple/vite.config.ts) to avoid Iconify API requests at runtime. - React hosts. CSS: Tailwind,
tw-animate-css, then either.../react/styles/ui-vars.css,core.cssand the feature sheets, or for the advanced editor the single.../react/styles/advanced.css; the shadcn theme block, and@sourceto the wholedistof the package (node_modules/@skaile/forge-editor/dist;packages/editor/srcin this repository), never justreact: the Features' components are infeatures/*/react; icons:addCollection(lucide)from@iconify/reactbefore rendering;<Toaster />for notices. The preset,metadata,abilities,extensionsandcommandsare mount-time: keep them module constants (new objects per render replace the lists).editorfromonReadygoes inuseState, andEditorToolbartakes it aseditor. Examples: Quick start: React and Advanced editor in React. - Settings keys. Only keys of
EditorSettingspersist; unknown ones are dropped on read and write. - Design tokens.
@skaile/forge-editor/styles/tokens.cssdeclares the shared--sk-*variables (--sk-bg,--sk-fg,--sk-muted,--sk-border,--sk-primary,--sk-primary-fg,--sk-success,--sk-warning,--sk-danger,--sk-radius,--sk-font) under:where(:root)(dark values under:where(.dark)). Later UI work in this package reads--sk-*; map them once in the host. - Module order.
@nuxt/uibefore@skaile/forge-editor/nuxtinmodules. - Adapters tier. Entries under
adapters/,anchors,comments/{server,json,drizzle,client}andserver/nitrosupply storage and wiring; they import no application code. Vue is reachable only from./adapters/vue, React only from./adapters/react(and the framework layers), Nitro (h3) only from./server/nitro,drizzle-ormonly from the two drizzle entries;./adaptersis types only../server/nestand./adapters/prismaare reserved and not exported. The Nitro host and the HTTP backend (createHttpBackend, whichuseEditorDocumentreads documents, collaboration and comments through) speak one wire contract:GET/PUT /api/editor/documents/:id(TipTap JSON, 404 = missing,PUT204 and only for adapters that manage documents),GET/POST /api/editor/comments/:id(handleCommentsRequest, which trusts its caller),/collab/:id(Hocuspocus). Every route authenticates throughports.identity.authenticate({ headers, token });nullanswers 401; the default outside production is an anonymous dev user, in production nobody. The JSON and forge adapters and the JSON comment store are single-node. Mount the surface with:key="id"andv-if="doc.ready.value"(React:key={id}anddoc.ready), bind withv-bind="doc.bind.value"(React:{...doc.bind}). Runnable reference:examples/vue-nitro-json(bun run proto:vue-nitro-json, port 3046).