docs/plans/2026-04-13-slate-react-runtime-one-shot-breakup-plan.md
Break up the former packages/slate-react/test/runtime.tsx runtime landfill
into behavior-domain proof owners.
The plan must:
surface-contract.tsx on API/surface ownershipCurrent files:
496 lines560 lines542 lines985 lines755 lines302 lines639 lines424 lines1174 lines106 linesWhat is now real:
runtime.tsx is gonewithReact / ReactEditor, primitives, mounted editable
behavior, projection/ref behavior, app-owned customization, and large-doc
lanes now have explicit ownerstest-utils.tsruntime-fixtures.tsReactEditor and withReact should ride the mounted bridge, not a fake old
plugin stack:
2026-04-09-slate-v2-reacteditor-should-ride-the-mounted-bridge-and-keep-base-components-standalone.mdAfter the one-shot breakup, packages/slate-react/test/ should look like:
surface-contract.tsx
API-facing surface and low-level public behavior already split outtest-utils.ts
shared JSDOM mount helpers onlylarge-doc-and-scroll.tsx
large-document shells, promotion, full-doc select-all/paste, scroll behaviorprovider-hooks-contract.tsx
provider/editor lifecycle, useSlateStatic, useSlateSelector,
useSlateWithV, editor/readOnly/focused/composing hooks, element hooksreact-editor-contract.tsx
withReact, ReactEditor, DOM translation, root/window/shadow-root helpers,
mounted bridge seamprimitives-contract.tsx
ZeroWidthString, TextString, SlateText, SlateLeaf, SlateElement,
SlateSpacer, SlatePlaceholder, EditableText, EditableTextBlocks,
VoidElementeditable-behavior.tsx
root mounting, DOM-to-snapshot reconciliation, keydown/paste forwarding,
readOnly, controlled replacement, rich-inline anchor reset/refocusprojections-and-selection-contract.tsx
projection store behavior, range-ref-backed projections, root/node ref hooks,
selector invalidation localityapp-owned-customization.tsx
markdown preview, markdown shortcuts, forced layout, styling, hovering
toolbar, image/embed actions, table renderingruntime.tsx is gone.
provider-hooks-contract.tsxMove these runtime rows:
selector subscriptions stay slice-scoped across a transactionuseSlateStatic returns the provider editor and updates when the provider editor changesSlate initializes fresh editors from initialValue and re-initializes when the provider editor changesSlate publishes onChange, onValueChange, and onSelectionChange on the current snapshot seamslate-react hook surface exposes editor, selection, readOnly, and current boolean contextsslate-react focused and readOnly hooks stay correct outside Editable descendantsslate-react element hooks expose current element context and selected stateuseSlateSelector keeps referential stability when custom equality says values are equaluseSlateWithV exposes the provider editor with the current snapshot versionswitching provider editor instances updates subscribers to the new editorWhy:
react-editor-contract.tsxMove these runtime rows:
withReact and ReactEditor expose the current compatibility seamwithReact composes with withLinks and honors wrapper-owned inline behaviorwithReact composes with withMentions and honors wrapper-owned insertMention behaviorwithReact composes with runtime forced-layout behaviorReactEditor DOM target and event helpers expose the current mounted bridge seamReactEditor root/window helpers expose the mounted document boundaryReactEditor root/window helpers expose the mounted shadow-root boundaryWhy:
primitives-contract.tsxMove these runtime rows:
slate-react exports the current named render and component prop typesZeroWidthString renders line-break placeholders without FEFF by defaultZeroWidthString retains FEFF for non-linebreak placeholdersTextString repairs stale native text on rerenderSlateText and SlateLeaf own the v2 text-node shapeSlateElement and SlateSpacer own the v2 element and spacer shapeSlatePlaceholder owns the v2 placeholder overlay shapeSlatePlaceholder supports arbitrary intrinsic tags through asEditableText composes text, zero-width, and optional placeholder branchesEditableText forwards arbitrary intrinsic placeholder tagsEditableText supports renderPlaceholderEditableText supports renderTextEditableText splits a text node into projected leaves and refreshes segment dataEditableText exposes text and leafPosition to renderLeafEditableText exposes leaf marks to renderSegmentEditableBlocks exposes renderLeafEditableBlocks forwards renderTextEditableBlocks exposes renderElement attributesEditableBlocks preserves existing text marks during DOM reconciliationEditableText renders zero-length projection slices as mark placeholdersEditableText can derive text and runtime binding from a pathEditableTextBlocks can render from the public editor + projectionStore surfaceEditableBlocks aliases the public top-level text-block surfaceEditableBlocks can render mixed inline descendants through the public surfaceEditableBlocks falls back to editor.isInline when no isInline prop is suppliedEditableElement owns the minimal editable element wrapper shapeEditableElement supports arbitrary intrinsic tags through asVoidElement owns the minimal void wrapper and spacer shapeVoidElement supports arbitrary intrinsic tags for wrapper and contentWhy:
editable-behavior.tsxMove these runtime rows:
Editable owns root mounting and DOM-to-snapshot reconciliationEditableBlocks survives text-to-inline replacement on the same path without hook-order crashesEditableBlocks forwards keydown handlers to app-owned keyboard policyEditableBlocks forwards paste handlers to app-owned paste policyEditableBlocks supports readOnly on the structured editing surfaceoptional Activity boundary preserves local state and resumes on latest committed snapshotcontrolled replacement works through package hooks without effect mirroringEditableBlocks rich-inline anchor reset establishes a new history boundary without effect mirroringEditableBlocks rich-inline anchor restores DOM selection on refocus after resetEditableBlocks rich-inline anchor keeps selector invalidation localEditableBlocks keeps unchanged text segments stable across top-level prependsWhy:
projections-and-selection-contract.tsxMove these runtime rows:
root and node ref hooks delegate DOM ownership to slate-domprojection subscriptions stay local when external decoration state changesselection-derived annotation projections track committed selection changesrange-ref-backed projections support persistent annotation anchorsWhy:
app-owned-customization.tsxMove these runtime rows:
EditableBlocks supports app-owned markdown preview projectionsEditableBlocks supports app-owned markdown shortcutsEditableBlocks supports app-owned forced layout enforcementEditableBlocks forced layout restores the second paragraph when only a title remainsEditableBlocks supports app-owned styling surfacesEditableBlocks supports app-owned hovering toolbar stateVoidElement supports app-owned editable void controls without mutating editor contentEditableBlocks supports app-owned image and embed void actionsWhy:
Executed order:
provider-hooks-contract.tsxreact-editor-contract.tsxprimitives-contract.tsxeditable-behavior.tsxprojections-and-selection-contract.tsxapp-owned-customization.tsxruntime.tsxmountApp or mountAppInShadowRoot.
Use test-utils.ts.ReactEditor bridge rows into provider-hook files.After the full breakup:
pnpm turbo build --filter=./packages/slate-reactpnpm turbo typecheck --filter=./packages/slate-reactpnpm --filter slate-react testpnpm lint:fixsurface-contract.tsxprovider-hooks-contract.tsxreact-editor-contract.tsxprimitives-contract.tsxeditable-behavior.tsxprojections-and-selection-contract.tsxapp-owned-customization.tsxlarge-doc-and-scroll.tsxtest-utils.tsruntime.tsx no longer acts as the default owner for unrelated React package
behaviorwithReact, ReactEditor,
projection, primitive, editable behavior, or app-owned customization
regression in one file opensurface-contract.tsx still contains some mounted behavior rows
mitigation:
leave it alone in this one-shot pass unless they clearly belong in one of the
extracted files; do not widen the plan into churn for churn’s sakeIf you do this one-shot pass well, runtime.tsx stops being “the React bucket”
and becomes either tiny or dead.
That is the point.