Editor Known Gaps

This page exists because the editor surface drifts. Types are exported, code is stripped, two implementations of the same idea coexist. Documenting the gaps explicitly is cheaper than pretending the API is whole.

Each entry follows the same shape: where, what, what was expected, who gets hurt, what the fix looks like.

EditorProps Missing Wiring

Where. packages/ui/src/components/ui/editor.tsx:41 (the EditorProps interface) and apps/demo/src/components/demos/EditorPlayground.tsx:438 (the consumer that passes the missing props).

Current behavior. The component exports EditorSidebarConfig (line 80), EditorRulePaletteConfig (line 92), SlashCommand (line 65), SaveCompositeData (line 73), and RuleConfigField (line 108). None of these types are referenced inside EditorProps. The demo passes sidebar, rulePalette, commandPalette, inlineToolbar, blockContextMenu, onSaveAsComposite to <Editor>. The component receives them as part of the spread HTMLDivAttributes and silently drops them. No runtime error. No effect.

Expected behavior. The props should be declared and wired into the component. Sidebar mounts a block palette. Rule palette mounts a rule list. Command palette mounts a slash menu. Inline toolbar mounts a floating format bar. Block context menu mounts on right-click. Save-as-composite handler fires when the user requests it.

Blast radius. Every consumer that wants the rich editor experience has to recompose these surfaces externally. The demo already does, which is why the pattern is visible in EditorPlayground.tsx. New consumers who read the type exports and assume the props work will spend time debugging until they read the source.

Fix sketch. Restore the wiring stripped post-PR #1048. Each surface mounts as a portal or sibling of the canvas. The types are intact; only the JSX integration is missing. Estimated work: medium. The primitives that back each surface are all present and functional.

Inline Formatter Write Path Is Dangling

Where. packages/ui/src/primitives/inline-formatter.ts:619 (serializeSelection) and :674 (deserializeToDOM). And packages/ui/src/components/ui/editor.tsx (no call site).

Current behavior. The inline formatter applies marks (bold, italic, code, strikethrough, link) directly to the contentEditable DOM. The serializeSelection() function exists and walks the DOM to produce InlineContent[]. The deserializeToDOM() function exists and walks InlineContent[] to produce a DocumentFragment. The editor never calls serializeSelection. So when a user formats text, the DOM updates and the visual is correct, but block.content is never updated. On save, the content shape is whatever it was before the edit, and the formatting is lost.

Expected behavior. Every inline format operation (toggle, apply, remove) should call serializeSelection for the affected block and write the result back into the block’s content field through the document-editor or block-handler. Round-trip MDX edits should preserve user-applied marks the same way they preserve imported marks.

Blast radius. Bold, italic, code, strikethrough, and link applied via the inline toolbar do not survive save. Imported marks from MDX render correctly because deserializeToDOM runs on the read path. Edits in the DOM render correctly. The save path drops them. Gitpress and any other MDX-round-trip consumer is the most exposed.

Fix sketch. Hook the inline-toolbar commit and the contentEditable blur. On both, call serializeSelection for the focused block, route through block-handler.updateBlock(id, { content: serialized }). The bridge functions are correct; only the call site is missing.

Editor.deselect Is A Stub

Where. packages/ui/src/components/ui/editor.tsx:408.

Current behavior. deselect is exposed on EditorControls and is an empty function. Calling it does nothing.

Expected behavior. It should clear the document-editor’s selection state, blur the active contentEditable, and emit any focus-change callbacks subscribers have registered.

Blast radius. Small but real. Consumers who programmatically need to drop selection (after a save, after a navigation) think they have a way to do it. They do not.

Fix sketch. Wire deselect() to call docEditorRef.current?.deselect() if the primitive supports it, plus a manual (document.activeElement as HTMLElement)?.blur() as a fallback. The primitive likely needs a parallel method added.

Two MDX Serializers Exist

Where. packages/ui/src/primitives/serializer-mdx.ts (the full bidirectional one, 873 lines) and packages/composites/src/serializer.ts (the legacy one-way toMdx()).

Current behavior. Both ship. The full one in @rafters/ui conforms to the EditorSerializer interface, handles InlineContent[] marks, supports frontmatter, supports MDX JSX, supports round-trip parsing. The legacy one in @rafters/composites is one-way (no parser), drops quote, input, button, image to <!-- unknown --> comments, treats content as a plain string, and does not handle frontmatter.

Expected behavior. One serializer. The full one is the right one. The legacy toMdx() should be removed once internal callers are migrated, or kept only as an internal helper that the bridge layer uses.

Blast radius. Anyone reaching for “the MDX serializer” can pick the wrong one. The @rafters/composites version produces lossy output and looks plausible enough at first glance to ship.

Fix sketch. Audit callers of composites/serializer.ts:toMdx. Replace each with mdxSerializer from @rafters/ui. Delete the legacy file. Update tests. The visible callers are bridge.ts and possibly some demo code; trace from there.

Composites Imports From UI

Where. packages/composites/src/registry.ts:8 (imports fuzzyScore from @rafters/ui/primitives/typeahead) and packages/composites/src/bridge.ts:10 (type-imports BlockPaletteItem from @rafters/ui/primitives/block-palette).

Current behavior. @rafters/composites declares zod as its only intentional workspace dependency. In practice it depends on @rafters/ui for one helper function and one type alias.

Expected behavior. Composites should depend only on zod. The architectural intent is that composites is a pure data-and-validation package any consumer can pull in without dragging the UI layer.

Blast radius. Today: a circular-ish dependency that complicates package boundaries. Tomorrow: anyone bundling composites for a non-React consumer (a worker, a CLI build pipeline, a service) gets the entire UI primitive layer along for the ride.

Fix sketch. Inline fuzzyScore into composites/registry.ts. It’s a small pure function. Define BlockPaletteItem locally in bridge.ts (or in a new composites/src/types.ts) with the same shape. Both edits are mechanical. The two packages become structurally compatible without import-time coupling.

Asymmetric Circular-Reference Depth Limits

Where. packages/composites/src/serializer.ts (legacy toMdx, MAX_DEPTH=50) and packages/composites/src/bridge.ts:96 (instantiateBlocks, maxDepth=10).

Current behavior. The two functions cap recursion at different depths. Most composites instantiate fine because real depth is usually 2 or 3. A pathological composite that nests deeply could be serialized but not instantiated, or vice versa.

Expected behavior. Either a single shared constant or a documented reason for the difference.

Blast radius. Low. No reported bugs. The asymmetry is more of a code-smell than an active issue.

Fix sketch. Pick one number. 10 is probably correct (deep nesting indicates a misuse). Move it to a shared constant in composites/src/constants.ts. Update both call sites.

Rule Runtime Validation Gap

Where. packages/composites/src/rules.ts and packages/composites/src/built-in-rules/*.ts.

Current behavior. matchRules does nominal name matching. The Zod schemas in built-in-rules/ are loaded but never invoked from blocks. There is no engine that walks a block tree, reads each block’s rules, resolves the rule name to its Zod schema, and validates the block’s content against it.

Expected behavior. A consumer should be able to call something like validateBlocks(blocks): ValidationResult[] and get back per-block validation errors based on the rules attached to each block.

Blast radius. Composites that declare input rules can claim compatibility with consumers that declare matching output rules, even when the actual data shapes don’t agree. The compatibility check is by name only. Errors surface late, in user-visible UI.

Fix sketch. Add validateBlocks(blocks, ruleRegistry) to composites/src/rules.ts. Walk blocks. For each block with rules, look up the rule’s Zod schema in the registry, run parse() against the block’s content (or the relevant meta field per rule), accumulate errors. The schemas exist; only the engine is missing.

Registry Import-Path Bug In pnpx rafters add editor

Where. Distributed via the rafters CLI, surfaces in any consumer that runs pnpx rafters add editor.

Current behavior. Primitives are installed to src/lib/primitives/ but their internal imports reference @/src/components/ui/ instead of @/src/lib/primitives/. Affected files include block-handler.ts, block-canvas.ts, block-context-menu.ts, escape-keydown.ts, inline-formatter.ts, collision-detector.ts, outside-click.ts, command-palette.ts, canvas-drop-zone.ts, slot.ts, inline-toolbar.ts, typeahead.ts, focus-trap.ts, roving-focus.ts, clipboard.ts, keyboard-handler.ts, block-palette.ts, rule-palette.ts. Also: the shared types.ts primitive is not installed.

Expected behavior. Primitives’ relative imports should resolve to wherever the registry installer placed them. The shared types.ts should be installed alongside.

Blast radius. Every external consumer who installs the editor via the CLI hits this on day one. They have to manually sed paths or copy files.

Fix sketch. Update the registry build to rewrite import paths based on installation target. Include types.ts in the editor primitive bundle. Track the registry path through the build instead of hard-coding components/ui/.

Save-As-Composite UI Missing

Where. packages/ui/src/components/ui/editor.tsx. The SaveCompositeData type at line 73 and onSaveAsComposite callback are exported but no UI exists to invoke them.

Current behavior. Type-level support is in place. The bridge function serializeToComposite in composites/src/bridge.ts:239 takes blocks and metadata and produces a CompositeFile. There is no in-editor UI to collect the metadata (name, category, description) and trigger the handler.

Expected behavior. A consumer-driven action (right-click menu item, slash command, toolbar button) collects metadata via a small dialog and calls onSaveAsComposite({ name, category, description, blocks }).

Blast radius. Composite authoring is a designer flow. Without UI, designers can’t save composites from inside the editor. Today they edit JSON files by hand or use the CLI.

Fix sketch. A dialog primitive plus a small form. Wire to a context menu item or toolbar button. Call the prop. The serializer does the rest. Depends on the EditorProps wiring gap above.

How These Get Fixed

These gaps are documentation-first. None of them are getting fixed in the same change as the docs that describe them. The sequence is intentional: write the truth, then write the fix plan, then ship the fix.

If a fix is in flight, the entry should still describe the gap as it stands today. Update the entry only when the fix lands.