docs/plans/2026-05-23-slate-v2-multi-root-react-dx-ralplan.md
Status: done - Slate Ralplan complete, ready for Ralph execution
Runtime id: 019e46be-4ec4-7d11-bc6e-9fcf033a8803
Skill: slate-ralplan
Scope: .tmp/slate-v2 multi-root React API, package ownership, and example DX
Make multi-root editing feel like Slate with one editor and multiple editable surfaces.
The app should own document layout and product state. The package should own root activation, view-editor creation, DOM selection preservation, focus repair, and root-local DOM sync.
The current example is dirty app land.
The right public DX is:
const editor = useSlateEditor({
extensions: [documentTitle],
initialValue: {
roots: {
footer: [{ type: 'paragraph', children: [{ text: 'Prepared' }] }],
header: [{ type: 'paragraph', children: [{ text: 'Confidential' }] }],
main: [{ type: 'paragraph', children: [{ text: 'Body' }] }],
},
state: {
[documentTitle.key]: 'Q2 Operating Plan',
},
},
})
return (
<Slate editor={editor}>
<DocumentTitleInput />
<Editable root="header" aria-label="Header editor" />
<Editable aria-label="Body editor" />
<Editable root="footer" aria-label="Footer editor" />
</Slate>
)
That is the API users will expect from legacy Slate: one editor provider, many
editable DOM surfaces. SlateRuntime, createEditorView, and manual
window.getSelection() calls should not appear in the canonical example.
Current score after pass 10: 0.92. Target score after execution: >= 0.92, with no dimension below 0.85.
This plan is ready for Ralph execution. All Slate Ralplan passes, issue-sync accounting, confidence scoring, and closure gates are closed.
site/examples/ts/multi-root-document.tsx
createEditorView, SlateRuntime, useSlateRuntime,
useSlateRuntimeState, and useSlateViewState;updateHistory manually creates a root view editor for undo and redo;focusEditableAtEnd and focusEditablePreservingSelection directly mutate
DOM selection and dispatch selectionchange;RootEditor owns activeRoot, flushSync, DOM ids, and root focus;restoreTitleFocus and restoreActiveRootFocus queue focus repairs in app
code.packages/slate-react/src/components/slate.tsx
<SlateRuntime><Slate root>...</Slate></SlateRuntime>;<Slate editor> and <Slate root> are mutually exclusive.packages/slate-react/src/hooks/use-state-field.ts
useSetStateField already preserves DOM selection and avoids forced focus;packages/slate-react/src/hooks/use-slate-runtime.tsx
playwright/integration/examples/multi-root-document.test.ts
<Slate editor={editor}> should be the canonical multi-root provider.
<Slate editor={editor}>
<TitleField />
<Header />
<Body />
<Footer />
</Slate>
SlateRuntime remains as an advanced substrate API for custom hosts and
adapter authors. Canonical examples should not teach it first; advanced docs
can document it explicitly.
Editable should accept root?: RootKey directly.
<Editable root="header" />
<Editable />
<Editable root="footer" />
Internally, Editable root should:
State field hooks should work under <Slate editor> even when they are rendered
outside a specific Editable.
const title = useStateFieldValue(documentTitle)
const setTitle = useSetStateField(documentTitle)
return (
<input
value={title}
onChange={(event) => setTitle(event.currentTarget.value)}
/>
)
Add a tuple convenience hook only if it is implemented as a tiny wrapper:
const [title, setTitle] = useStateField(documentTitle)
This should preserve the existing setter semantics:
Add or alias public hooks around root language:
const headerText = useSlateRootState('header', rootText)
const activeRoot = useSlateActiveRoot()
const editor = useSlateRootEditor(activeRoot)
useSlateViewState can remain as a lower-level alias, but examples should use
useSlateRootState. "View" is implementation vocabulary. "Root" is user DX.
App code should not call createEditorView(runtime, { root: activeRoot }) for
undo and redo.
const activeRoot = useSlateActiveRoot()
const editor = useSlateRootEditor(activeRoot)
editor.update((tx) => {
tx.history.undo()
})
useSlateActiveEditor() may exist as a tiny convenience over active root plus
root editor, but it should not be the only documented command path.
const activeRoot = useSlateActiveRoot()
const editor = useSlateRootEditor(activeRoot)
The package still owns how active root maps to the runtime view.
Delete this from site/examples/ts/multi-root-document.tsx:
createEditorViewSlateRuntimeuseSlateRuntimeuseSlateRuntimeState where a normal editor selector existsuseSlateViewState in favor of useSlateRootStateactiveRoot React state in the examplesetActiveRootflushSyncfocusEditableAtEndfocusEditablePreservingSelectionrestoreTitleFocusrestoreActiveRootFocuswindow.getSelection() / document.createRange() / selectionchange
dispatch from app codeThe example should contain normal Slate code only:
<SlateRuntime><Slate root>Rejected as canonical DX. It exposes the runtime/view split to every user and forces examples to manage focus and active root manually.
Keep it only as an advanced substrate for custom host integrations.
MultiRootEditor ComponentRejected for raw Slate. Plate can ship product-shaped components. Slate should
ship primitives: Slate, Editable root, state field hooks, and root hooks.
createEditorViewRejected for app code. It is the right internal primitive, but not the right example API.
useSlateViewStateRejected for public examples. The hook may stay, but the documented API should say root, not view.
slate-react should own:
Apps should own:
Add or update packages/slate-react/test/* rows:
<Slate editor> can host multiple <Editable root> descendants.<Editable /> defaults to main.<Editable root="header"> binds operations, selection, and editor context to
header.<Slate editor><Editable /></Slate> behavior is unchanged.useStateFieldValue and useSetStateField work under top-level <Slate>
outside any Editable.useSlateRootState(root, selector) only re-renders for that root's changes.useSlateActiveRoot updates after focus, pointer activation, keyboard entry,
and selection entry.useSlateRootEditor(activeRoot) runs history against the expected root.useSlateActiveEditor is tested only as a convenience wrapper if it
exists.Keep the current Playwright coverage, but make it prove package ownership:
Add a source-level assertion for the example:
restoreTitleFocus;restoreActiveRootFocus;window.getSelection;document.createRange;createEditorView;SlateRuntime.renderElement, renderLeaf, useEditor,
and selection hooks.<Slate editor> so it can own the runtime context for document
roots.<Slate root> to Editable root.useSlateRootStateuseSlateActiveRootuseSlateRootEditoruseSlateActiveEditoruseStateFieldslate-react.site/examples/ts/multi-root-document.tsx to one <Slate> and many
<Editable root> surfaces.Run from .tmp/slate-v2:
bun test ./packages/slate-react/test/slate-runtime-provider-contract.test.tsx
bun test ./packages/slate-react/test/state-field-selector-contract.test.tsx
bun --filter ./packages/slate-react typecheck
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium --workers=1
bun lint:fix
If package exports or public hook files move, run the repo's barrel/update command before the final typecheck.
The plan is ready for Ralph only when:
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| 1. Current-state read and initial score | complete | Live .tmp/slate-v2 source, existing plan, research index/log, issue ledgers, PR reference, and multi-root browser tests read. | Changed lane from ready for Ralph execution to pending full-gate review; added confidence scorecard and pass ledger. | Issue discovery, ecosystem synthesis, maintainer objection, high-risk proof, and closure gates remain open. | Slate Ralplan |
| 2. Related issue discovery | complete | Live/open, sync, issue coverage, package impact, requirements, clusters, gitcrawl clusters, fork dossier, and PR reference rows read. | Added related issue classification matrix; kept new exact fixed/improved claims at zero; marked older SlateRuntime + <Slate root> public accounting stale for pass 11. | Need durable ledger/PR wording sync in pass 11. | Slate Ralplan / ClawSweeper discipline |
| 3. Issue-ledger pass | complete | Full issue-ledger index, package impact matrix, requirements file, issue clusters, gitcrawl clusters, open ledger, test candidate map, benchmark candidate map, sync ledger, coverage matrix, fork dossier, and PR reference read. | Added ownership/accounting pass: runtime-boundary issue pressure supports moving focus/selection/root ownership into slate-react/slate-dom, but still produces zero new issue claims for this plan. | Need intent/decision brief to settle public API shape and non-goals. | Slate Ralplan |
| 4. Intent/boundary and decision brief | complete | Source plan, current example evidence, issue-ledger ownership pass, and intent-boundary guidance read. | Added intent/outcome/scope/non-goals/decision boundaries plus public API decision brief; chose <Slate editor> + <Editable root> as canonical and demoted runtime/view APIs to substrate. | Need research/ecosystem synthesis before score can exceed 0.85 on research or migration. | Slate Ralplan |
| 5. Research/ecosystem synthesis | complete | Read/update decision, state/tx namespace decision, data-model-first React runtime decision, Lexical/ProseMirror/Tiptap corpus ledger, React 19.2 external-store research, and steal/reject/defer map read. | Added ecosystem strategy table for Lexical, ProseMirror, Tiptap, React 19.2, VS Code, Pretext/Premirror, Plate, and slate-yjs/collab. | Need performance/DX/migration/regression pressure matrix before score can approach threshold. | Slate Ralplan |
| 6. Performance/DX/migration/regression pressure passes | complete | Slate Ralplan pressure-pass guidance, performance, performance-oracle, tdd, Vercel React, react-useeffect, shadcn, live runtime selectors, state-field selectors, provider contracts, and browser tests read. | Added perf/DX/migration/regression matrix, cohorts, repeated-unit budgets, 10x/100x/1000x projections, proof rows, and source-cleanliness assertions. | Need maintainer objection and high-risk passes before closure. | Slate Ralplan |
| 7. Maintainer objection / steelman | complete | Steelman guidance, current API decision, runtime/view substrate evidence, issue-ledger ownership, and research decisions read. | Added maintainer objection ledger; kept canonical <Slate editor> + <Editable root> but revised wording to keep runtime/view APIs as advanced substrate, not legacy-bad API. | Need high-risk deliberate pass for API/browser/focus blast radius. | Slate Ralplan |
| 8. High-risk deliberate mode | complete | High-risk deliberate guidance, current plan, maintainer objection pass, perf/DX proof matrix, and continuation state read. | Added high-risk trigger, blast-radius matrix, three-scenario pre-mortem, expanded proof plan, adoption/rollback answer, and keep-with-guardrails verdict. | Need ecosystem maintainer pass for Plate/slate-yjs/plugin substrate answers. | Slate Ralplan |
| 9. Ecosystem maintainer pass | complete | Slate Ralplan ecosystem guidance, Plate-fit hard-cut dossier, issue coverage extension/collab rows, state/tx namespace decision, and issue requirements read. | Added Plate/plugin and slate-yjs/collab maintainer answers, affected extension points, migration-backbone surfaces, collab contract, and proof requirements. | Need revision pass to fold accepted guardrails into final target and resolve stale wording. | Slate Ralplan |
| 10. Revision pass | complete | Full target sections, pass 7-9 guardrails, stale score/current-status wording, API hook preference, source-cleanliness scope, and ecosystem answers reviewed. | Folded accepted guardrails into top-level API/test/execution target; explicit useSlateRootEditor(root) is canonical; runtime/view APIs are advanced substrate; score raised to threshold but lane remains pending. | Need issue sync accounting before closure. | Slate Ralplan |
| 11. Issue sync accounting pass | complete | Current manual sync ledger, issue coverage matrix, PR reference, generated live rows, gitcrawl clusters, fork dossier multi-root row, and pass 10 issue-sync scope read. | Synced public target wording to canonical <Slate editor> + <Editable root> while preserving advanced runtime/view substrate and zero new fixed/improved issue claims. | Need closure score/final gates. | Slate Ralplan |
| 12. Closure score and final gates | complete | Pass ledger, score threshold, issue-sync artifacts, allowed edit scope, completion state, and continuation state audited. | Added closure audit and done handoff; marked lane ready for Ralph execution. | None for Slate Ralplan; implementation belongs to Ralph. | Ralph |
site/examples/ts/multi-root-document.tsx currently teaches the wrong layer:
createEditorView, SlateRuntime, useSlateRuntime,
useSlateRuntimeState, and useSlateViewState at lines 4-18;activeRoot in app React state and uses flushSync at lines 294-320;<Slate root> per root at lines 354-366;<SlateRuntime runtime={runtime}> at lines 530-568.packages/slate-react/src/components/slate.tsx confirms the current public
shape:
SlateProps has editor?: ... and root?: RootKey at lines 181-197;<Slate editor> and <Slate root> are mutually exclusive at lines 212-214;<SlateRuntime> at lines 216-225;SlateRuntimeView creates and registers a view editor at lines 238-267.packages/slate-react/src/hooks/use-state-field.ts already has the right
state-field mutation default:
useSetStateField routes through editor.update;packages/slate-react/src/hooks/use-slate-runtime.tsx already has substrate the
clean API should reuse:
The browser tests are valuable and should survive the cleanup:
playwright/integration/examples/multi-root-document.test.ts lines 7-85;Research layer:
docs/research/index.md points this surface to the editor architecture lane,
React 19.2 external-store/background UI, and the read/update runtime decisions
at lines 75-130.docs/research/log.md confirms editor architecture evidence has already been
maintained for React 19.2, ProseMirror, Lexical, VS Code, and Slate v2 at lines
199-213.docs/research/decisions/slate-v2-read-update-runtime-architecture.md
accepts editor.read / editor.update as the public lifecycle and places
DOM selection policy outside React at lines 21-51 and 133-150.docs/research/sources/editor-architecture/read-update-runtime-corpus-ledger.md
records Lexical read/update and dirty reconciliation, ProseMirror transaction
and centralized DOM selection authority, and Tiptap command/extension DX at
lines 23-129.docs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.md
says React 19.2 supports external stores and background UI, but does not
replace core invalidation at lines 29-64 and 80-102.docs/research/systems/slate-v2-perfect-plan-steal-reject-defer-map.md
explicitly rejects Tiptap-style chain().focus() ceremony as required public
UX at lines 158-170.Issue and PR ledgers:
docs/slate-issues/gitcrawl-live-open-ledger.md reports 630 live open issues
at lines 8-14.docs/slate-issues/gitcrawl-v2-sync-ledger.md already records the older
multi-root/provider target as optional SlateRuntime plus <Slate root> at
lines 55-61; this plan likely needs to revise that accounting.#6016,
#5537, and #5117 at lines 78-80 and 97-98.docs/slate-v2/ledgers/issue-coverage-matrix.md has a 2026-05-21
React Runtime Provider / Multi-root Planning Sync section at lines 76-90.docs/slate-v2/references/pr-description.md says PR claims must not add issue
numbers because they sound related at lines 135-143.| Dimension | Weight | Score | Weighted | Evidence | Reason |
|---|---|---|---|---|---|
| React 19.2 runtime performance | 0.20 | 0.78 | 0.156 | use-slate-runtime.tsx root registry/text-op grouping; React 19.2 research lines 29-64 and 80-102. | Direction is right: root-local registry and external-store posture exist. Score stays below 0.85 because active-root subscriptions and view-editor creation are not designed in the plan yet. |
| Slate-close unopinionated DX | 0.20 | 0.84 | 0.168 | Plan target <Slate editor><Editable root>; live example lines 530-568 show current dirt. | One Slate with many Editables is the right Slate-ish surface. Score stays below 0.85 until decision brief and migration wording are complete. |
| Plate and slate-yjs migration backbone | 0.15 | 0.76 | 0.114 | State fields are collab/history/persist-capable in example lines 20-26; sync ledger lines 43-53. | Good substrate, but no Plate/plugin or slate-yjs maintainer answer for root views and state fields yet. |
| Regression-proof testing strategy | 0.20 | 0.82 | 0.164 | Existing Playwright rows lines 7-416; plan test rows lines 256-297. | Strong browser rows exist. Missing package-level contract rows and source-cleanliness assertion keep it below 0.85. |
| Research evidence completeness | 0.15 | 0.78 | 0.117 | Research index/log, read/update decision, corpus ledger, React 19.2 source page, perfect-plan map. | Relevant research exists. Score stays below 0.85 because this plan lacks the required ecosystem synthesis table for the multi-root decision. |
| shadcn-style composability and hook/component minimalism | 0.10 | 0.80 | 0.080 | Plan API target lines 80-181; shadcn lens loaded for component minimalism. | Minimal surface is promising. Need explicit prop/hook minimalism pass and no product MultiRootEditor defense. |
Total: 0.80.
Threshold not met:
>= 0.92;>= 0.85;<Slate editor> with many <Editable root> surfaces.SlateRuntime plus
<Slate root> as public target; that should become an advanced substrate, not
canonical DX.| Lens | Applicability | Current result | Next required delta |
|---|---|---|---|
intent-boundary-pass | applied, pending pass 4 | Intent is clear enough to proceed. | Add non-goals, decision boundaries, and viable options. |
vercel-react-best-practices | applied, pending pass 6 | Relevant rules are external-store subscriptions, rerender-defer-reads, effect-to-event, event-handler refs, and stable callbacks. | Prove active root does not become a global React state rerender source. |
performance-oracle | applied, pending pass 6 | Root registry and operation grouping are bounded by touched roots. | Add 10x/100x/1000x multi-root cohort expectations. |
performance | applied, pending pass 6 | Multi-root repeated units are root views/editable surfaces. | Add budgets for mounted root views, selectors, listeners, DOM sync, and focus listeners. |
tdd | applied, pending pass 6 | Existing tests are good behavioral seeds. | Require package contracts before example rewrite, then browser proof. |
react-useeffect | applied, pending pass 6 | App focus repair is exactly the type of effect/timing workaround to remove. | Package internals may synchronize with DOM, but examples should not. |
shadcn | applied, pending pass 6 | Useful as component minimalism lens only. | No product-shaped MultiRootEditor; keep primitive composition. |
steelman-pass | applied, pending pass 7 | Triggered by public React API change. | Add strongest objection and migration answer. |
high-risk-deliberate-pass | applied, pending pass 8 | Triggered by public API and browser focus/selection behavior. | Add blast radius and proof plan. |
clawsweeper | applied, pending passes 2-3 | Ledger-first issue discipline loaded. | Reuse existing ledgers, no broad live GitHub search. |
ready for Ralph execution to full-gate pending.ClawSweeper discipline applied: use the existing issue ledgers first, avoid a broad live GitHub search, and do not inflate issue claims because a bug sounds related.
Search scope:
docs/slate-issues/gitcrawl-live-open-ledger.mddocs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/ledgers/fork-issue-dossier.mddocs/slate-v2/references/pr-description.mddocs/slate-issues/package-impact-matrix.mddocs/slate-issues/requirements-from-issues.mddocs/slate-issues/issue-clusters.mddocs/slate-issues/gitcrawl-clusters.md| Issue / cluster | Current ledger status | Discovery result for this plan | Claim policy | Proof route | Sync impact |
|---|---|---|---|---|---|
#6016 displaying two Slate components with same initial value | triage-closed / non-fix; use one SlateRuntime with root-bound views. | Still relevant, but the target changes from "many Slate providers" to "one provider with many roots." It does not support sharing the same node object across independent runtimes. | No Fixes. Keep as architectural pressure and documentation guard. | Example proves one editor/runtime with header/main/footer roots, not two editors with shared object identity. | Pass 11 should update stale provider wording from <SlateRuntime><Slate root> to canonical <Slate><Editable root>. |
#5537 autofocus with multiple editors | cluster-synced v2-react-runtime. | Strong related pressure: root focus must be package-owned. Not exact enough to close from this DX plan alone. | No new claim. | Browser row: changing title/header/body/footer preserves focused root and follow-up typing target. | Keep cluster-synced; add note that canonical API removes app-land focus repair. |
#5117 placeholder measurement leaks across editors | future-proof / example pressure. | Related to per-root DOM bridge and measurements. Not exact enough to claim fixed. | No new claim. | Package tests should assert root-local editable DOM ownership and no cross-root placeholder/measurement leak. | Keep future-proof. |
#4612 external state update | improves-claimed. | Preserved: state fields and explicit setters give a package-owned external update path without app focus hacks. | Preserve existing improves claim; no exact Fixes. | Package state-field hook rows and browser title-edit row. | Pass 11 should keep status unchanged but align wording with top-level provider hooks. |
#5281 controlled input | not-claimed. | Still not a controlled-input redesign. State fields are document-owned runtime state, not React-controlled children. | No claim. | None. | Explicitly preserve not-claimed. |
#3497 loses focus when parent triggers unrelated state change | cluster-synced v2-react-runtime. | Related: app rerenders should not own editor focus repair. This plan improves the architecture, but exact closure needs direct repro proof. | No new claim. | Add browser row: unrelated app state update does not move caret from active root. | Keep cluster-synced. |
#3634 / #4961 focus after programmatic change cluster | cluster-synced v2-react-runtime. | Related to selection import/export and focus restoration. The plan should preserve rooted selection identity. | No new claim. | Browser rows around toolbar/title update, undo, redo, and follow-up typing target. | Keep cluster-synced. |
#3893 toolbar button focus state | related. | Related because active root should not depend on toolbar DOM focus. | No claim until toolbar/menu scenario has exact proof. | Add or preserve toolbar interaction row if execution touches toolbar. | Keep related. |
#5867, #5538, #5826 focus/scroll rows | related / focus-scroll pressure. | Relevant to package-owned focus/selection repair, especially focus and scrollSelectionIntoView defaults. | No new claim. | Preserve no-forced-scroll defaults in state-field setter and add browser row only if scroll behavior changes. | Keep related. |
#4477, #4483, #3383, #5515, #3741, #3715, #3482 adjacent rows | existing statuses preserved. | Multi-root React DX should not touch their claim status. | No new claim. | Existing issue-specific proofs only. | Preserve. |
#6013, #5605, #5709 provider initialization claims | existing fixed claims in matrix/PR reference. | This plan is adjacent, not the source of those claims. | Preserve only if execution does not regress provider initialization. | Existing provider-init proof plus multi-root browser sanity. | No new claim; avoid bundling this DX cleanup into those fixes. |
The issue lesson is simple: most of the related bugs are not "multi-root API" bugs; they are React runtime, focus, DOM selection, and root-ownership bugs. That makes the current example worse than a messy sample. It teaches users to work around the exact subsystem Slate v2 is supposed to own.
The accepted public direction therefore stays:
<Slate editor={editor}> with many <Editable root> surfaces;SlateRuntime, createEditorView, and lower-level view
registration;0.0.#6013, #5605, #5709.#4612.#5281.#5537, #5117, #3497, #3634, #4961,
#3893, #5867, #5538, #5826.#6016 remains a non-fix for sharing one node object across independent
runtimes, but becomes a strong proof row for one runtime with many root
editables.Total remains 0.80.
The issue-discovery gate is closed, but the issue-ledger sync, decision brief, ecosystem synthesis, performance/DX testing matrix, steelman, high-risk, and closure passes are still open. Raising the score now would be fake confidence.
SlateRuntime plus
<Slate root> as stale for the issue-sync pass.Full ledger pass applied across the issue corpus artifacts, not just the rows that already sounded close to multi-root editing.
Read set:
docs/slate-issues/open-issues-ledger.mddocs/slate-issues/test-candidate-map.mddocs/slate-issues/test-candidate-map/*docs/slate-issues/benchmark-candidate-map.mddocs/slate-issues/package-impact-matrix.mddocs/slate-issues/requirements-from-issues.mddocs/slate-issues/issue-clusters.mddocs/slate-issues/gitcrawl-clusters.mddocs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/ledgers/fork-issue-dossier.mddocs/slate-v2/references/pr-description.mdThe full ledger does support this plan, but not by creating a pile of new issue claims.
The package-impact matrix says runtime-boundary ownership has 407 issues,
while core-engine ownership has 113. It also says docs/examples must not
absorb architecture decisions that belong in packages. That is directly
applicable here: multi-root-document.tsx should not be the place where active
root, focus repair, DOM selection preservation, and root-local DOM sync are
invented.
The requirements file makes the same call in architecture language:
slate-react-v2 and
slate-dom-v2;slate-react-v2 owns subscriptions, lifecycle, focus timing, placeholder and
render timing, editor replacement semantics, and React-facing lifecycle;slate-dom-v2 owns DOM point/path translation, clipboard DOM formats,
selection bridge mechanics, shadow DOM, and nested-editor DOM boundaries.This plan is therefore correctly package-owned:
slate-react: public React provider, Editable root, root-view lifecycle,
hook ergonomics, active root subscription, focus lifecycle;slate-dom: DOM selection bridge and root-local DOM sync mechanics;slate: roots, operations, state fields, commits, and selection/root identity;| Issue | Test-candidate state | What this plan can use | What it cannot claim |
|---|---|---|---|
#6016 | ready-now: dual-editor render with same Slate value reference; v2 capability test, not current-Slate obligation. | Use as proof pressure for one runtime/editor with multiple root editables. | Cannot claim support for sharing one node-object graph across independent runtimes. |
#6013 / #5605 | ready-now / duplicate via #6013. | Preserve existing provider-init fixed claims. | No new claim from the multi-root DX plan. |
#5709 | ready-now: provider hook editor replacement. | Preserve existing provider replacement fixed claim. | No new claim unless execution touches replacement behavior. |
#5537 | not-a-test-candidate: raw DOM focus across multiple editors. | Treat as pressure to keep raw DOM focus hacks out of app examples. | No exact fixed/improved claim. |
#5117 | ready-now: multiple editors placeholder min-height isolation. | Add root-local DOM/placeholder proof obligation if execution touches placeholder or inactive roots. | No exact closure without the specific placeholder repro. |
#4961 | ready-now: ReactEditor.focus after insertNodes. | Use as focus-after-programmatic-change pressure. | No exact closure from API cleanup alone. |
#3634 | ready-now: ReactEditor.focus should allow immediate typing between editors. | Use as active-root/follow-up typing browser proof pressure. | No exact closure without the original focus scenario. |
#3497 | ready-now: parent rerenders from unrelated state should not steal focus. | Add unrelated parent-state update proof for multi-root example execution. | No exact closure until replay exists. |
#4612 | not-a-test-candidate in current contract. | Preserve existing improves claim through state fields and explicit transaction APIs. | Do not claim a React-controlled value fix. |
#5281 | not-a-test-candidate: controlled input request. | Preserve explicit non-claim. | Do not turn state fields into controlled editor children. |
#5867 | ready-now: DOMEditor.focus with selected inline void. | Use as focus-preserves-logical-selection pressure. | No exact closure unless inline void selection is replayed. |
#5538 | blocked-on-repro: focus-to-end scroll jump. | Keep as focus/scroll pressure. | No claim. |
#5826 | ready-now: refocus auto-scroll on long document. | Preserve no-forced-scroll defaults and browser-scroll proof if execution touches focus defaults. | No claim without long-document refocus replay. |
#3893 | ready-now: focus state update when clicking ordinary HTML buttons. | Use as toolbar/button active-root pressure. | No claim without exact toolbar button proof. |
The benchmark candidate map does not add a direct performance issue for this specific API cleanup. The performance obligation comes from runtime-boundary architecture and rerender/subscription breadth, not from a known benchmark issue whose closure this plan can claim.
The public API cleanup is justified by issue corpus ownership:
slate-react, where the ledger says React
lifecycle/focus/subscription bugs belong.slate-dom, instead of burying
window.getSelection() and selectionchange dispatches in app code.It does not justify any broad issue closure. The correct claim policy is still:
0;0;#6013, #5605, #5709 fixed claims;#4612 improves claim;Total remains 0.80.
The ledger pass closes an evidence gap, but the plan still lacks the decision brief, ecosystem strategy table, maintainer objection ledger, high-risk pass, performance/DX proof matrix, revision pass, and final issue sync accounting.
Intent-boundary pass applied. No user question is needed: the repo and issue evidence answer the boundary question.
| Field | Decision |
|---|---|
| Intent | Make multi-root editing feel like normal Slate: one editor object, one provider, multiple editable DOM surfaces. |
| Desired outcome | App code renders document layout and product fields. slate-react creates root views, tracks active root, preserves focus, restores root-local selection, and routes root-local DOM sync. |
| In scope | Public React API shape, root provider composition, root hooks, state-field hooks outside an editable, active-root/history routing, example cleanup, and package/browser proof obligations. |
| Non-goals | No product MultiRootEditor; no Plate-only component; no support for sharing the same node-object graph across independent runtimes; no React-controlled value comeback; no broad issue closure; no pagination/layout work in this plan. |
| Decision boundaries | This plan may decide the public multi-root React API, hook naming direction, app/package ownership split, issue claim policy, and required proof rows. It may not edit .tmp/slate-v2 implementation from the Ralplan skill. |
| Unresolved user-decision points | None. Hook naming can still be refined during execution review, but the architectural boundary is settled. |
Pressure test:
If the example still needs restoreTitleFocus, restoreActiveRootFocus,
flushSync, DOM ids, or window.getSelection(), the API has failed. A public
example is not allowed to normalize package architecture debt into app code.
| Option | Pros | Cons | Decision |
|---|---|---|---|
<Slate editor={editor}> with many <Editable root> surfaces | Matches Slate composition, keeps one editor object, hides root-view lifecycle, lets examples stay clean. | Requires slate-react to own view creation and active-root context instead of exposing the lower layer. | Chosen. |
Keep canonical <SlateRuntime><Slate root> | Mirrors current substrate and is explicit about runtime/view split. | Forces every app to learn internals, makes examples own focus repair, and teaches the wrong layer. | Rejected as canonical; keep as advanced substrate only. |
Independent <Slate> editors with shared value object | Looks close to legacy multi-editor examples and explains #6016 directly. | Shared node-object identity across runtimes is unsupported and makes DOM path ownership ambiguous. | Rejected. |
Product MultiRootEditor wrapper | Very easy demo API. | Too opinionated for raw Slate, hides primitives, and belongs in Plate or app code. | Rejected. |
| Keep app-owned focus helpers | Minimal package work. | It preserves the exact class of runtime-boundary bugs v2 is meant to remove. | Rejected. |
Canonical userland:
<Slate editor={editor}>
<DocumentTitleInput />
<Editable root="header" />
<Editable />
<Editable root="footer" />
</Slate>
Root API split:
Editable root?: RootKey creates or reuses the root view editor and provides
the correct editor context to descendants.useSlateActiveRoot() returns the root that currently owns editing intent.useSlateRootEditor(root) returns a root-bound editor explicitly.useSlateActiveEditor() may exist as a tiny convenience over active root plus
root editor, but should not be the only API.useSlateRootState(root, selector) is public example vocabulary;
useSlateViewState may remain lower-level.useStateFieldValue, useSetStateField, and optional useStateField work
under top-level <Slate> outside any Editable.This keeps the public API explicit enough for multi-root correctness without
making every app call createEditorView.
<Slate root> becomes legacy/substrate wording, not the canonical public
example.Editable becomes the public root-view mounting point.SlateRuntime and createEditorView available for custom hosts.Total rises from 0.80 to 0.81.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.78 | 0.78 | No new subscription/perf matrix yet. |
| Slate-close unopinionated DX | 0.84 | 0.88 | Public API choice, non-goals, and rejected alternatives are now explicit. |
| Plate and slate-yjs migration backbone | 0.76 | 0.77 | Product wrapper rejection helps, but collab/plugin substrate still needs pass 9. |
| Regression-proof testing strategy | 0.82 | 0.82 | No new test matrix beyond proof obligations. |
| Research evidence completeness | 0.78 | 0.78 | Ecosystem synthesis still pending. |
| shadcn-style composability and hook/component minimalism | 0.80 | 0.83 | The plan now rejects a product wrapper and keeps primitive composition. |
Weighted total: 0.81.
Still below threshold because research/ecosystem, performance/DX, maintainer objection, high-risk, revision, issue-sync, and closure gates are open.
Editable, not root on a nested <Slate>, as the canonical
public mounting point.0.81, still pending.Research/ecosystem synthesis applied. This pass converts the research layer into specific architecture calls for the multi-root React DX slice.
Read set:
docs/research/decisions/slate-v2-read-update-runtime-architecture.mddocs/research/decisions/slate-v2-state-tx-public-api-and-extension-namespaces.mddocs/research/decisions/slate-v2-data-model-first-react-perfect-runtime.mddocs/research/sources/editor-architecture/read-update-runtime-corpus-ledger.mddocs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.mddocs/research/systems/slate-v2-perfect-plan-steal-reject-defer-map.md| System | Steal | Reject | Adapt For Multi-root React DX |
|---|---|---|---|
| Lexical | Read/update lifecycle, dirty leaves/elements, update tags for policy, command execution inside update context. | Class-node document model, $function public style, bespoke node mutation as the mental model. | Root-view lifecycle should run inside the Slate transaction/commit discipline. Active root, DOM sync, and selection restoration should be commit effects, not React app effects. |
| ProseMirror | Transaction authority, selection mapping, durable bookmarks, one DOM observer/selection import/export authority. | Integer positions as public Slate coordinates, schema-first identity, heavy plugin system as raw Slate DX. | Root-local selection caches should be Slate path/root aware, but the authority model should be ProseMirror-like: one package-owned DOM selection bridge, not per-example repairs. |
| Tiptap | Extension packaging, command ergonomics, React selector guidance, product-quality docs. | Required chain().focus().run() ceremony as normal mutation DX. | Plate can own product components and command sugar. Raw Slate should expose primitive root hooks and state/tx namespaces; focus freshness must be runtime-owned instead of user-called focus(). |
| React 19.2 | useSyncExternalStore, transitions/deferred non-urgent UI, Activity for hidden panes, Performance Tracks for render proof. | Treating React scheduling as a replacement for editor invalidation. | Active editing stays urgent and root-local. Status badges, commit panels, hidden roots, and diagnostics can subscribe/defer; active root must not become global app state that rerenders every root. |
| VS Code | Split model, view model, decorations, widgets, comments, and services. | Turning raw Slate into a full app/editor workbench. | Multi-root roots are view surfaces over one document runtime. Product UI stays outside raw Slate; root hooks expose enough view state for app layout without owning selection repair. |
| Pretext / Premirror | Derived layout/measurement lane over document truth. | Pulling pagination/layout into this React DX cleanup. | Keep this plan root/view/focus only. The root API should not block future layout roots, but pagination remains a separate layout strategy lane. |
| Plate | Product components, plugin bundles, opinionated toolbars, renderer/plugin composition. | Raw Slate shipping a product MultiRootEditor or Plate-shaped tf/api core names. | Raw Slate provides clean primitives: <Slate>, <Editable root>, state fields, root hooks, extension namespaces. Plate can wrap them into document editor components later. |
| slate-yjs / collaboration | Operation-first model, transaction metadata, remote/local commit distinction, state fields that persist outside node content. | A Yjs-specific public React API or controlled React value revival. | Multi-root roots and document state must remain operation/transaction-owned so collaboration can replay root-specific operations without app-owned focus hacks. |
Keep:
editor.read / editor.update lifecycle.state and tx grouped namespaces.Reject:
editor.selection / editor.children / editor.operations
as the way app code coordinates roots;window.getSelection() and manual selectionchange;chain().focus() as the required fix for stale selection;Diverge:
The research points to one best architecture:
Slate document/runtime
owns roots, operations, state fields, commits, selection identity
slate-dom
owns DOM selection import/export and root-local DOM sync mechanics
slate-react
owns provider composition, Editable root mounting, root-view lifecycle,
active-root subscriptions, focus lifecycle, and hooks
apps / Plate
own layout, product toolbars, product wrappers, and opinionated UI
That exactly supports <Slate editor> plus many <Editable root> surfaces.
It also explains why the current example is wrong: it lets app React state and
DOM selection repair impersonate a runtime subsystem.
Total rises from 0.81 to 0.84.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.78 | 0.80 | External-store and urgent/non-urgent split is now tied to active-root subscriptions, but perf budgets are still pending. |
| Slate-close unopinionated DX | 0.88 | 0.89 | Ecosystem pass reinforces one editor/many editables and rejects focus ceremony. |
| Plate and slate-yjs migration backbone | 0.77 | 0.81 | Plate and collab boundaries are clearer, but pass 9 still needs maintainer-level substrate answers. |
| Regression-proof testing strategy | 0.82 | 0.82 | No new replay matrix yet. |
| Research evidence completeness | 0.78 | 0.90 | Research sources are now synthesized into concrete keep/reject/diverge calls. |
| shadcn-style composability and hook/component minimalism | 0.83 | 0.85 | Primitive composition is now explicitly preferred over product wrappers. |
Weighted total: 0.84.
Still below threshold because the pressure passes, maintainer objection, high-risk pass, ecosystem maintainer pass, revision pass, issue sync, and final closure gates are open.
0.84, still pending.Pressure passes applied: performance, performance-oracle, TDD, Vercel React, react-useeffect, and shadcn-style primitive composition.
Read set:
performance skill: cohorts, repeated-unit budgets, interaction latency,
memory/DOM tags, degradation contracts, React 19 runtime proof.performance-oracle skill: 10x/100x/1000x projection, algorithmic and memory
pressure.tdd skill: behavior-first tests through public APIs.vercel-react-best-practices skill: selector/rerender/event-handler rules.react-useeffect skill: effects are escape hatches; use external-store
subscriptions and event handlers instead of effect-driven state mirroring.shadcn skill: compose primitives, not custom product wrappers..tmp/slate-v2 evidence:
useSlateRuntimeState and useSlateViewState route through selector
context and root-aware shouldUpdate;useStateFieldValue filters by dirtyStateKeys;| Pressure | Required shape | Evidence / current substrate | Execution requirement |
|---|---|---|---|
| Hot subscription path | Root selectors must only update for touched roots or dirty state fields. | useSlateViewState filters through isRootAffected; useStateFieldValue filters through dirtyStateKeys. | New useSlateRootState must preserve the same filtering and not become a broad runtime subscription. |
| Listener count | Many roots must not install many document-level focus listeners. | Provider contract already asserts one focusin and one focusout listener pair across sibling root views. | Editable root refactor must keep one runtime-level listener pair. |
| View lookup | Root view lookup must be O(1) by root key, not a scan of mounted editables. | Runtime currently keeps mounted view editors by root. | Public Editable root must reuse that registry instead of prop-drilling editors. |
| DOM sync | Text DOM sync must run for changed root/runtime ids only. | Runtime groups text operations by root before sync. | Keep root-local grouping after Editable root moves view creation. |
| Active root | Active root is interaction state, not app React state. | Current example leaks it into app state; plan moves it into runtime hooks. | useSlateActiveRoot should subscribe to a small scalar; event handlers should read active editor lazily when only commands need it. |
| State fields | Non-node document state should not rerender body roots. | useStateFieldValue ignores body commits when field key is unchanged. | Top-level state-field hooks under <Slate> must preserve selection/focus defaults and dirty-key filtering. |
| Hidden/inactive roots | Inactive roots must not receive DOM sync or urgent rerenders unless affected. | Existing tests assert unmounted sibling roots do not receive sync work. | Keep hidden/inactive root behavior explicit; no silent global materialization. |
| Cohort | Shape | Budget |
|---|---|---|
| Normal | 1-3 roots: title, header, main, footer. | One runtime provider, one global focus listener pair, one mounted editor view per visible root, root-local updates only. |
| Large | 10-20 roots: sections, side notes, header/footer variants. | O(changed roots + subscribed affected selectors); no per-root document listeners; inactive roots may stay mounted but idle. |
| Stress | 100 root surfaces. | Root registry lookup stays O(1); committing to one root must not rerender all roots; root selector fanout must be measurable by affected subscribers. |
| Pathological | 1000 roots or generated split surfaces. | Not a default native-editing target. Use explicit virtualization/layout strategy later; this plan must not fake support by degrading native selection invisibly. |
10x / 100x / 1000x projection:
Keep:
<Slate editor> as the document provider.<Editable root> as the root DOM surface.useSlateRootState, useSlateActiveRoot, useSlateRootEditor.useSlateActiveEditor convenience.Cut from canonical example:
SlateRuntimecreateEditorViewuseSlateRuntimeuseSlateRuntimeStateuseSlateViewStateflushSyncwindow.getSelection() / document.createRange() / selectionchangeDo not overbuild:
MultiRootEditor;api/tf naming in raw Slate;Plate route:
slate-yjs / collaboration route:
collab: 'shared', persisted, and history-aware;value revival.This is a believable backbone route, not current adapter support. Pass 9 still
needs the ecosystem maintainer answer before the migration dimension can go
above 0.85.
Package contract rows to add or preserve:
| Behavior | Test shape |
|---|---|
| One provider hosts multiple roots | Render <Slate editor> with header/main/footer <Editable root> descendants; descendants see root-bound editor contexts. |
| Root selectors are scoped | Commit in main; header selector does not rerender. Commit mark-only change in header; header selector rerenders. |
| Active root is package-owned | Focus, pointer entry, keyboard entry, and selection entry update useSlateActiveRoot without app state. |
| History targets active root | Undo/redo command through package hook restores the correct root selection and follow-up typing target. |
| State field hooks work outside roots | Title input under <Slate> but outside any Editable reads/writes state field and does not focus or scroll an editor. |
| Root view lifecycle is bounded | Sibling roots share one runtime listener pair and distinct root view editors. |
| Unmounted sibling roots stay idle | Root-local text DOM sync does not run for unmounted or untouched roots. |
| Source cleanliness | Example source contains none of the forbidden runtime/focus/DOM repair symbols. |
Browser rows to preserve or add:
| Behavior | Browser proof |
|---|---|
| Root-local editing | Type in header/body/footer/title and assert only that target changes. |
| Parent rerender focus | Trigger unrelated app state update while caret is in a root; focus and follow-up typing stay in that root. |
| Active-root history | Undo body then header while focus remains in body; selection and follow-up typing stay body-local. |
| Toolbar/button focus | Click ordinary toolbar button; active root remains the last editor root unless command explicitly changes it. |
| Blank-area click | Clicking blank area at end/mid paragraph focuses the expected root and caret target. |
| Placeholder isolation | Inactive/empty root placeholder or min-height state does not leak across roots. |
| No forced scroll | State-field update and root switch do not scroll editor unless the command asks for scroll. |
| Native behavior | Select-all, copy, paste, shortcuts, IME/composition, and follow-up typing stay root-local. |
All implementation proof belongs in .tmp/slate-v2, not plate-2.
This Ralplan pass records required proof only. Ralph execution must run the
named .tmp/slate-v2 tests and browser rows before any implementation claim.
Total rises from 0.84 to 0.88.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.80 | 0.87 | Root-scoped selectors, listener budget, DOM sync locality, and cohort budgets are now explicit. |
| Slate-close unopinionated DX | 0.89 | 0.90 | Public primitive API remains simple and the bad example concepts are explicitly cut. |
| Plate and slate-yjs migration backbone | 0.81 | 0.85 | Plate/product and collaboration backbone routes are explicit, but ecosystem maintainer pass is still pending. |
| Regression-proof testing strategy | 0.82 | 0.88 | Package contract rows, browser rows, source-cleanliness assertions, and workspace proof rules are explicit. |
| Research evidence completeness | 0.90 | 0.90 | No change; pass 5 already closed the research synthesis. |
| shadcn-style composability and hook/component minimalism | 0.85 | 0.87 | Primitive composition and no product wrapper are now tied to testable API constraints. |
Weighted total: 0.88.
Still below threshold because maintainer objection, high-risk deliberate mode, ecosystem maintainer pass, revision pass, issue sync accounting, and closure gates are open.
0.88, still pending.Steelman pass applied. This is the skeptical Slate-maintainer read: keep the simple object model, avoid over-abstracting, do not make raw Slate feel like a closed framework, and do not rename things just to look cleaner.
| Decision | Strongest fair objection | Steelman antithesis | Tradeoff tension | Answer | Verdict |
|---|---|---|---|---|---|
Canonical <Slate editor> with many <Editable root> surfaces | "Slate has always been simple JS composition. Why invent a higher-level runtime API when users can already compose editors?" | Keep the current explicit runtime/view API. It is honest about what exists and does not hide complexity. | The package has to own more lifecycle code and the public API grows. | The current example proves explicit runtime/view leaks too much: app code creates views, tracks active root, and patches DOM selection. That is not Slate simplicity; it is browser-runtime debt in userland. Keep <Slate editor><Editable root> as canonical. | keep |
Move root view mounting from <Slate root> to Editable root | "Slate root is more symmetric with the existing provider shape. Putting root on Editable could blur provider and surface responsibilities." | Keep each root as its own provider under SlateRuntime, so context boundaries stay explicit. | Editable becomes more than a renderer; it becomes a root-view mount point. | A root is only meaningful when a DOM editing surface exists. The provider should describe the document runtime; the editable should mount the root view. This matches what users see and removes nested providers from normal examples. | keep |
Demote SlateRuntime / createEditorView from examples | "Power users need these primitives. Hiding them makes Slate feel closed and less hackable." | Keep low-level APIs public and documented first, then let examples show exactly what is happening. | Docs need two layers: normal DX and advanced host integration. | Keep the APIs as advanced substrate. Do not call them bad or legacy in docs. The canonical example should not require them, but custom hosts can still use them. | keep with wording revision |
Add root hooks (useSlateRootState, useSlateActiveRoot, useSlateRootEditor) | "More hooks can turn Slate into a React framework and make the API feel bigger." | Keep only lower-level runtime hooks and let apps build their own root state. | More public hooks mean more API to maintain. | These hooks are narrow aliases over real runtime concepts. They reduce app-land focus/selection hacks and are easier to teach than view vocabulary. Keep only primitives; no product wrapper. | keep |
| Package-owned active root/focus restoration | "Focus timing is app-specific; package-level focus repair can become magic and hard to debug." | Let apps control focus explicitly with DOM APIs and callbacks. | Package internals become responsible for tricky browser timing. | The issue ledger says focus/selection/runtime ownership is the core pain. App-level DOM focus repair is the bug class, not the solution. Package ownership must be observable through tests and opt-in metadata, not hidden magic. | keep |
| State fields outside editable roots | "Non-node document state may pull Slate toward app state management." | Keep non-node UI state in app React state and only store document nodes in Slate. | State fields add a document-owned data channel that can be abused. | The plan limits state fields to document-owned state with persistence/history/collab policy. Product UI state stays app/Plate-owned. Title/settings examples are valid document state; toolbar hover state is not. | keep |
| Source-cleanliness assertions | "Testing source strings is brittle and not behavior-driven." | Only test browser behavior and let internals change freely. | Source checks can become annoying during refactors. | Here the source check guards pedagogy, not runtime behavior. The example must not teach window.getSelection, createEditorView, or app-owned focus repair. Keep it narrow and example-specific. | keep |
SlateRuntime, <Slate root>, or createEditorView as
"bad" APIs in maintainer-facing docs. Call them advanced substrate or custom
host APIs.useSlateRootEditor(root) path. useSlateActiveEditor() may
exist, but it must not be the only command route because implicit active-root
behavior can feel too magical.<Slate editor><Editable /></Slate> unchanged as the
baseline mental model.MultiRootEditor from raw Slate.SlateRuntime ceremony from introductory examples.This is not "make Slate more closed." It is "keep the low-level runtime available, but stop making every app act like a runtime maintainer."
The raw Slate surface stays primitive:
<Slate editor={editor}>
<Editable root="header" />
<Editable />
<Editable root="footer" />
</Slate>
Advanced hosts can still use the runtime/view substrate. Normal users should not need to know that substrate to render header, body, and footer roots for one document.
Total rises from 0.88 to 0.90.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.87 | 0.87 | No new perf evidence; high-risk proof still pending. |
| Slate-close unopinionated DX | 0.90 | 0.92 | Objections are answered while preserving primitive composition and low-level substrate. |
| Plate and slate-yjs migration backbone | 0.85 | 0.86 | Maintainer pass clarifies raw Slate vs Plate/product boundary. |
| Regression-proof testing strategy | 0.88 | 0.89 | Source-cleanliness scope and proof requirements are sharper. |
| Research evidence completeness | 0.90 | 0.90 | No change. |
| shadcn-style composability and hook/component minimalism | 0.87 | 0.89 | Product wrapper is rejected again; primitive API remains the contract. |
Weighted total: 0.90.
Still below threshold because high-risk deliberate mode, ecosystem maintainer pass, revision pass, issue sync accounting, and closure gates are open.
<Slate editor> plus <Editable root>.useSlateRootEditor(root) in addition to any active-editor
convenience.0.90, still pending.High-risk deliberate pass applied.
This plan changes a public React API path and moves focus, selection,
root-view lifecycle, and history-targeting behavior from app code into
slate-react / slate-dom. That is high-risk because failure is immediately
visible as lost caret, wrong-root typing, scroll jumps, stale hook values, or
broken custom-host integrations.
| Area | Affected surface | Risk | Guardrail |
|---|---|---|---|
| Public React API | <Slate editor>, <Editable root>, useSlateRootState, useSlateActiveRoot, useSlateRootEditor, optional useSlateActiveEditor | API confusion or incompatible provider nesting. | Additive canonical path first; preserve single-root behavior; preserve advanced runtime/view substrate. |
| Runtime/view substrate | SlateRuntime, <Slate root>, createEditorView, runtime root registry | Custom hosts may depend on explicit view creation. | Keep substrate APIs valid; document as advanced/custom-host layer, not removed. |
| Selection/focus | focusin/focusout, selection entry, root-local selection cache, DOM selection import/export | Wrong root becomes active, toolbar clicks steal focus, title input loses focus, undo restores selection into sibling root. | Package contracts plus browser rows for title, toolbar, parent rerender, undo/redo, blank-area click, and follow-up typing. |
| History | active root undo/redo, root-bound view editor selection restore | Operation replay succeeds but caret lands in wrong root. | Browser rows must assert active element, selection containment, and follow-up typing target after undo/redo. |
| State fields | document title/settings outside editable roots | State update can focus editor, scroll editor, or rerender body roots. | Preserve dom: preserve, focus: false, scroll: false, and dirty-state-key filtering. |
| Performance | root subscriptions, listener count, DOM sync fanout | Many roots cause global rerenders or listener explosions. | O(changed roots + affected selectors), one focus listener pair, O(1) root lookup, root-local DOM sync. |
| Docs/examples | canonical multi-root example | Example can keep teaching low-level APIs despite package fixes. | Source-cleanliness assertion scoped to canonical example. |
| Issue/PR claims | related focus/multi-editor issues | Overclaiming fixes without exact repro proof. | Keep new fixed/improved claims at zero until exact proof exists. |
The API looks clean, but focus gets magical.
A toolbar click, title-field edit, or undo operation silently changes active
root. Users see text inserted into the wrong root. The fix is not to push
focus back to app code; it is to add explicit root-owner tests, expose
useSlateRootEditor(root), and keep active-editor convenience optional.
The refactor hides runtime power from advanced hosts.
Custom integrations that need SlateRuntime or createEditorView think the
API was deprecated or broken. The fix is maintainer-facing docs and examples
that keep the substrate valid while making it clear normal app code starts
at <Slate editor> plus <Editable root>.
Root-scoped architecture accidentally becomes global.
A small hook alias subscribes to the whole runtime, root commits rerender all surfaces, or each root adds focus listeners. The fix is to block execution closure until selector fanout, listener count, and root-local DOM sync tests prove bounded behavior.
| Proof lane | Required proof |
|---|---|
| Unit / contract | <Slate editor> hosts multiple <Editable root> descendants; root editor contexts are distinct; root selectors short-circuit unchanged roots; state-field hooks outside roots preserve focus/scroll defaults; root lookup remains registry-backed. |
| Integration | History/undo targets active root through package hooks; mounted roots share one document listener pair; unmounted sibling roots stay idle; runtime/view substrate still works for advanced tests. |
| Browser | Type in header/body/footer/title; parent rerender does not steal focus; toolbar button preserves active root; undo/redo preserves focused-root caret; blank-area clicks map to expected root/caret; select-all/copy/paste/shortcuts stay root-local. |
| Migration / adoption | Single-root <Slate editor><Editable /></Slate> remains unchanged; old advanced <SlateRuntime><Slate root> path still has tests or docs; new hooks are additive aliases where possible. |
| Docs / examples | Canonical example uses one provider and many editables; source-cleanliness check rejects app-owned focus/selection repair; docs call runtime/view APIs advanced substrate, not bad APIs. |
| Performance | Selector fanout test, one listener pair test, root-local DOM sync test, and stress budget notes for 10x/100x roots. |
| Issue accounting | No new fixed/improved claims; related issue rows stay related until exact repro proof lands. |
Security-specific proof is not applicable. The risk is editor behavior, selection, performance, and public API adoption.
Adoption:
Rollback/remediation:
<Editable root> proves too risky, keep <SlateRuntime><Slate root> as
the escape hatch while preserving the package-level focus/selection contract;useSlateRootEditor(root)
as the documented command path and demote useSlateActiveEditor;multi-root-document.tsx only;Keep, with guardrails.
The risk is real, but the current app-owned focus/selection repair is worse. The plan is acceptable only because it preserves the low-level substrate, requires explicit root-editor APIs, and blocks execution closure on package and browser proof.
Total rises from 0.90 to 0.91.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.87 | 0.89 | High-risk pass adds listener/fanout/DOM-sync closure blockers. |
| Slate-close unopinionated DX | 0.92 | 0.92 | No change; decision already accepted with maintainer objections answered. |
| Plate and slate-yjs migration backbone | 0.86 | 0.87 | Adoption/rollback keeps raw primitives and advanced substrate available. |
| Regression-proof testing strategy | 0.89 | 0.92 | Expanded proof plan covers unit, integration, browser, migration, docs, perf, and issue accounting. |
| Research evidence completeness | 0.90 | 0.90 | No change. |
| shadcn-style composability and hook/component minimalism | 0.89 | 0.89 | No change. |
Weighted total: 0.91.
Still below threshold because the ecosystem maintainer pass, revision pass, issue sync accounting, and closure gates are open.
0.91, still pending.Ecosystem maintainer pass applied because this plan touches root identity, React runtime boundaries, extension hook surfaces, state fields, operation routing, and collaboration-relevant local metadata.
Can a product layer migrate without wrapping every core call, losing composition, or becoming a compatibility junk drawer?
Yes, if raw Slate keeps this boundary:
<Slate editor>, <Editable root>,
useSlateRootEditor(root), useSlateActiveRoot, useSlateRootState,
state fields, editor.read, editor.update, state.*, and tx.*;The Plate-fit ledgers already make this law explicit: renderer composition
pressure is real, but raw Slate should not own a renderer registry; Plate owns
product renderer/plugin composition. This multi-root plan follows the same
rule. It gives Plate a better primitive backbone rather than shipping a raw
Slate MultiRootEditor.
Do operations, identity, snapshots, normalization, remote apply, and conflict behavior stay deterministic?
Yes, with these constraints:
persist, history, collab) instead
of hiding non-node state inside document nodes;This does not claim slate-yjs adapter compatibility. It says the raw substrate does not block it. Existing collaboration ledger rows stay conservative: operation replay and high-QPS selection work can improve the collaboration family, but no OT/Yjs/browser collaboration closure is created by this API cleanup.
| Surface | Ecosystem rule |
|---|---|
Editable root | Public root mount primitive for raw Slate and Plate wrappers. |
useSlateRootEditor(root) | Explicit command route for toolbars and product UI. |
useSlateActiveRoot | Local UI/runtime signal, not collaborative document truth. |
useSlateRootState(root, selector) | Root-scoped selector primitive for product chrome and diagnostics. |
useStateFieldValue / useSetStateField / optional useStateField | Document-owned non-node state with explicit persistence/history/collab policy. |
state.* / tx.* extension namespaces | Plugin migration backbone for typed read/write extension APIs. |
SlateRuntime / createEditorView | Advanced substrate for custom hosts and adapters, not removed. |
Plate/plugin code should be able to migrate by wrapping primitives:
function DocumentFrame({ editor }: { editor: Editor }) {
return (
<Slate editor={editor}>
<PlateTitleField />
<Editable root="header" />
<Editable />
<Editable root="footer" />
<PlateToolbar />
</Slate>
)
}
Toolbar commands should use explicit root editor access when needed:
const root = useSlateActiveRoot()
const editor = useSlateRootEditor(root)
editor.update((tx) => {
tx.nodes.set({ type: 'heading' })
})
Plugins should extend state and tx namespaces rather than monkeypatching
root view internals:
defineEditorExtension({
key: 'comments',
state: {
comments(state) {
return { activeThread() {} }
},
},
tx: {
comments(tx) {
return { resolveThread() {} }
},
},
})
That gives Plate product DX room without forcing raw Slate to own Plate-shaped APIs.
| Contract | Required answer |
|---|---|
| Root identity | Serialized operations and snapshots must identify the root deterministically. |
| Local active root | Must stay local metadata unless explicitly modeled as shared document state. |
| State fields | Must declare collab policy; shared fields replay through transaction/state APIs. |
| Remote apply | Must not require mounted React views or DOM selection. |
| History | Local undo/redo can restore local focus/selection, but remote collaboration metadata must not enter local undo accidentally. |
| Normalization | Root-local normalization must remain deterministic independent of mounted editable surfaces. |
<Slate> and root editables.useSlateRootEditor(activeRoot)
mutates the intended root.Keep.
The plan gives ecosystem maintainers the right substrate:
Total stays 0.91, but the migration dimension improves.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.89 | 0.89 | No new perf proof. |
| Slate-close unopinionated DX | 0.92 | 0.92 | No change. |
| Plate and slate-yjs migration backbone | 0.87 | 0.91 | Plate/plugin and collab maintainer answers are now explicit. |
| Regression-proof testing strategy | 0.92 | 0.92 | No change. |
| Research evidence completeness | 0.90 | 0.91 | Ecosystem ledger rows are now tied to the plan. |
| shadcn-style composability and hook/component minimalism | 0.89 | 0.89 | No change. |
Weighted total: 0.91.
Still below threshold because revision, issue sync accounting, and closure gates are open.
0.91, still pending.Revision pass applied. This pass folds accepted guardrails back into the top-level target so Ralph gets one coherent plan instead of a pile of review appendices.
| Area | Before revision | After revision |
|---|---|---|
| Current status | Pointed to ecosystem pass and revision-next. | Points to revision complete and issue-sync next. |
| Score header | Said current score was after pass 8. | Updated to pass 10 score 0.92. |
| Completion wording | Said full Slate Ralplan gates broadly remained open. | Names the actual remaining gates: issue-sync accounting and closure. |
| Runtime substrate wording | SlateRuntime "may remain" but docs/examples should not teach it first. | SlateRuntime remains valid advanced substrate; canonical examples avoid it, advanced docs may document it. |
| Active editor hook | Preferred useSlateActiveEditor() first. | Canonical command route is explicit useSlateRootEditor(useSlateActiveRoot()); useSlateActiveEditor() is optional convenience only. |
| Test target | Allowed useSlateActiveEditor or root editor as equivalent. | Requires useSlateRootEditor(activeRoot) proof; optional active-editor wrapper proof only if shipped. |
| Ralph order | Root hook list treated active/root editor as alternatives. | Requires useSlateRootEditor; active-editor convenience is optional. |
Canonical public API:
const editor = useSlateEditor({ initialValue, extensions })
return (
<Slate editor={editor}>
<DocumentTitleInput />
<Editable root="header" />
<Editable />
<Editable root="footer" />
</Slate>
)
Canonical command path:
const activeRoot = useSlateActiveRoot()
const editor = useSlateRootEditor(activeRoot)
editor.update((tx) => {
tx.history.undo()
})
Advanced substrate:
SlateRuntime<Slate root>createEditorViewuseSlateRuntimeStateuseSlateViewStateThose APIs stay valid for custom hosts, adapter authors, and advanced docs. They are not canonical example DX.
MultiRootEditor.Pass 11 must sync wording in:
docs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/references/pr-description.mdRequired sync decisions:
SlateRuntime plus
<Slate root> as public target to canonical <Slate editor> plus
<Editable root>;#6016 triage-closed/non-fix;#5537, #5117, #3497, #3634, #4961, #3893, #5867,
#5538, and #5826 related only;#5281 not claimed;#4612 improves claim;#6013, #5605, and #5709 fixed claims;Total rises from 0.91 to 0.92.
| Dimension | Previous | Current | Reason |
|---|---|---|---|
| React 19.2 runtime performance | 0.89 | 0.90 | Final target now explicitly preserves root-scoped selector and runtime-substrate guardrails. |
| Slate-close unopinionated DX | 0.92 | 0.93 | Canonical command path is explicit root editor, reducing magic while keeping one-provider DX. |
| Plate and slate-yjs migration backbone | 0.91 | 0.92 | Ecosystem answers are folded into the target and issue-sync scope. |
| Regression-proof testing strategy | 0.92 | 0.92 | No change; proof rows already explicit. |
| Research evidence completeness | 0.91 | 0.91 | No change. |
| shadcn-style composability and hook/component minimalism | 0.89 | 0.91 | Product wrapper remains cut and primitive composition is now top-level target. |
Weighted total: 0.92.
The score threshold is met, but completion is still pending. Score is necessary, not sufficient. Issue sync accounting and closure gates remain open.
useSlateRootEditor(activeRoot) the canonical command route.useSlateActiveEditor() optional only.0.92, still pending.Issue-sync accounting pass applied.
Read set:
docs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/references/pr-description.mddocs/slate-issues/gitcrawl-live-open-ledger.mddocs/slate-issues/gitcrawl-clusters.mddocs/slate-v2/ledgers/fork-issue-dossier.md| Artifact | Change |
|---|---|
docs/slate-issues/gitcrawl-v2-sync-ledger.md | Revised React multi-root DX wording from optional SlateRuntime + <Slate root> as the accepted public target to canonical <Slate editor> + multiple <Editable root> surfaces. Preserved runtime/view APIs as advanced substrate. |
docs/slate-v2/ledgers/issue-coverage-matrix.md | Revised React Runtime Provider / Multi-root Planning Sync target to canonical provider/editable-root API, with runtime/view APIs as advanced substrate/custom-host APIs. |
docs/slate-v2/references/pr-description.md | Replaced stale "later optional SlateRuntime / Slate root" wording with the canonical multi-root React DX target and advanced substrate note. |
Generated live rows were read for current issue presence, but not edited.
0.0.#6016: remains triage-closed/non-fix. Shared node-object graphs across
independent editor runtimes remain unsupported; canonical answer is one
editor/runtime with root-bound editable surfaces.#5537, #5117, #3497, #3634, #4961, #3893, #5867, #5538,
#5826: remain related pressure only.#5281: remains not claimed.#4612: preserves existing improves claim.#6013, #5605, #5709: preserve existing fixed claims.The issue-facing truth is now aligned:
canonical API:
<Slate editor={editor}>
<Editable root="header" />
<Editable />
<Editable root="footer" />
</Slate>
advanced substrate:
SlateRuntime
<Slate root="...">
createEditorView
useSlateRuntimeState
useSlateViewState
This is a planning/accounting sync only. It does not claim .tmp/slate-v2
implementation proof.
Score remains 0.92.
The issue-sync gate is now complete. The only remaining scheduled pass is the closure score and final gates pass.
Closure pass applied. This pass audits whether the Slate Ralplan lane can truthfully move from pending to done.
| Gate | Result | Evidence |
|---|---|---|
| Pass schedule | pass | Pass rows 1-11 were complete before this closure pass; pass 12 is now complete. |
| Current pass | pass | current_pass is closure-score-and-final-gates; current_pass_status is complete. |
| Confidence threshold | pass | Final score is 0.92, meeting the >= 0.92 threshold. |
| Minimum dimension floor | pass | Lowest final dimension is 0.90, above the 0.85 floor. |
| Issue discovery and ClawSweeper discipline | pass | Issue discovery, issue-ledger pass, and issue-sync accounting are complete. |
| Issue claim accounting | pass | New fixed claims: 0; new improved claims: 0; existing claims preserved only where already recorded. |
| Research / ecosystem evidence | pass | Research synthesis, ecosystem maintainer pass, Plate/plugin, and slate-yjs/collab answers are complete. |
| Performance / DX / migration / regression passes | pass | Perf/DX/migration/regression pressure rows and proof matrix are complete. |
| Maintainer and high-risk passes | pass | Maintainer objection, steelman, high-risk deliberate mode, adoption, rollback, and proof guardrails are complete. |
| Allowed edit scope | pass | This skill edited only planning, issue-ledger/reference, and scoped .tmp state artifacts. No .tmp/slate-v2 implementation files were edited. |
| Continuation state | pass | No next Slate Ralplan pass remains. Ralph owns implementation execution. |
| Dimension | Final score | Closure note |
|---|---|---|
| React 19.2 runtime performance | 0.90 | Root-scoped selector and runtime-substrate guardrails are explicit. |
| Slate-close unopinionated DX | 0.93 | Canonical API is one <Slate editor> with many <Editable root> surfaces. |
| Plate and slate-yjs migration backbone | 0.92 | Raw Slate primitives, Plate/product ownership, and collab-local metadata are separated. |
| Regression-proof testing strategy | 0.92 | Package, browser, source-cleanliness, and workspace proof rows are explicit. |
| Research evidence completeness | 0.91 | Lexical, ProseMirror, Tiptap, React 19.2, Plate, slate-yjs, Pretext/Premirror, and VS Code evidence is synthesized. |
| shadcn-style composability and hook/component minimalism | 0.91 | Product wrapper remains cut; primitive composition is the top-level target. |
Weighted total: 0.92.
Threshold: pass.
<Slate editor={editor}> with multiple
<Editable root> surfaces.useSlateRootEditor(useSlateActiveRoot()).useSlateActiveEditor(), only if it stays a thin
package-owned helper.SlateRuntime, <Slate root>,
createEditorView, useSlateRuntimeState, and useSlateViewState..tmp/slate-v2, not from this Slate Ralplan
pass.Required Ralph verification from .tmp/slate-v2:
bun test ./packages/slate-react/test/slate-runtime-provider-contract.test.tsx
bun test ./packages/slate-react/test/state-field-selector-contract.test.tsx
bun --filter ./packages/slate-react typecheck
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium --workers=1
bun lint:fix
Execution started for the closed Slate Ralplan.
| Field | Value |
|---|---|
| Status | complete |
| Owner | .tmp/slate-v2/packages/slate-react |
| Public behavior | <Slate editor> hosts multiple root-bound <Editable root> surfaces without app-created views. |
| Trigger | Public React API behavior requires red-green-refactor proof. |
| Scope | Package API/hooks/provider, canonical multi-root example, focused package/browser verification. |
| Related issue sweep | No rerun yet; pass 11 already swept this multi-root React DX surface with zero new fixed/improved claims. |
| Reference docs | Added docs/solutions/developer-experience/2026-05-23-slate-react-multi-root-editable-dx-needs-package-owned-root-views.md; no issue-claim or PR narrative change. |
| Red proof | bun --filter ./packages/slate-react test:vitest slate-runtime-provider-contract -t "Slate editor hosts multiple root-bound Editable surfaces" failed with useSlateActiveRoot is not a function. |
| Implementation | Added root-named hooks, root-bound view editor creation, <Editable root>, top-level <Slate editor> runtime context, root-local DOM text sync, and rewrote the canonical example to one Slate provider. |
| Build-fix | Site typecheck exposed example helper type erasure around history; fixed helper types with ReactEditor and removed global DOM lookup for root chrome focus. |
| Diff review | Complete; no P0/P1/P2 findings. Accepted small internal casts around view-editor rebinding because the runtime already owns that substrate boundary. |
| Verification | bun lint:fix; bun --filter ./packages/slate-react test:vitest slate-runtime-provider-contract; bun --filter ./packages/slate-react test:vitest state-field-selector-contract; bun --filter ./packages/slate-react typecheck; bun typecheck:site; source-cleanliness rg; PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/multi-root-document.test.ts --project=chromium --workers=1. |
| Next action | None. Ralph execution is complete. |