Back to Plate

slate v2 shift boundary selection

docs/plans/2026-05-26-slate-v2-shift-boundary-selection.md

53.0.817.1 KB
Original Source

slate v2 shift boundary selection

Objective: Fix Slate v2 hidden-content-blocks shifted caret movement so Shift+ArrowRight and Shift+Option+ArrowRight from the end of the first paragraph never project a native DOM selection through shadcn accordion/tabs chrome or inactive hidden content.

Goal plan: docs/plans/2026-05-26-slate-v2-shift-boundary-selection.md

Template: docs/plans/templates/task.md

Primary template: docs/plans/templates/task.md

Applied packs:

  • browser (docs/plans/templates/packs/browser.md)
  • package-api (docs/plans/templates/packs/package-api.md)

Task source:

  • type: user browser bug report
  • id / link: local route
  • title: Shifted selection leaks through hidden-content shadcn chrome
  • acceptance criteria: from /examples/hidden-content-blocks, shifted right and shifted word-right at the end of p1 keep inactive hidden content closed and do not select accordion/tab/collapsible controls in the native DOM selection.

Completion threshold:

  • Browser repro is confirmed before the fix.
  • Package-level regressions cover reconciler export, shifted caret movement, word-selection hotkeys, clipboard policy preservation, and the hidden-content-blocks route.
  • Browser proof on http://100.102.180.93:3100/examples/hidden-content-blocks shows empty native selection text, active Overview tab, inactive Details tab, and no hidden accordion/details text after both shifted key paths.
  • slate-react and slate-dom typecheck pass.
  • Changesets exist for the published slate-react and slate-dom behavior changes.

Verification surface:

  • Browser: in-app Browser on http://100.102.180.93:3100/examples/hidden-content-blocks.
  • Playwright: PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/hidden-content-blocks.test.ts --project=chromium.
  • Unit: bun test:vitest -- keyboard-input-strategy-contract.test.ts selection-reconciler-contract.test.tsx.
  • Unit: cd packages/slate-dom && bun test ./test/dom-coverage.test.ts ./test/hotkeys.test.ts ./test/clipboard-boundary.test.ts.
  • Types: bun --filter slate-react typecheck; bun --filter slate-dom typecheck.
  • Lint: scoped bunx biome check ...; scoped bunx eslint ....

Constraints:

  • Work in /Users/zbeyens/git/plate-2/.tmp/slate-v2.
  • Preserve current boundary, materialize, and model-backed policies.
  • Do not create commits, pushes, PRs, or tracker comments.

Boundaries:

  • Source of truth: user report plus attached screenshot showing native selection over non-editable shadcn chrome.
  • Allowed edit scope: Slate v2 slate-dom, slate-react, hidden-content-blocks Playwright coverage, package changesets, and this plan.
  • Browser surface: /examples/hidden-content-blocks.
  • Tracker sync: N/A, no tracker item.
  • Non-goals: visible projected selection overlay for model-backed hidden ranges; broad redesign of DOM coverage policy.

Blocked condition: Blocked only if the in-app Browser cannot load the route or package tests cannot run due unrelated local install corruption after one reinstall retry. No blocker remains.

Task state:

  • task_type: bug
  • task_complexity: normal
  • current_phase: closeout
  • current_phase_status: complete
  • next_phase: final response
  • goal_status: active

Current verdict:

  • verdict: complete
  • confidence: high
  • next owner: user
  • reason: browser repro is fixed, route regression is green, package tests and typechecks pass.

Completion rule:

  • Completion is legal after this plan passes node .agents/rules/autogoal/scripts/check-complete.mjs docs/plans/2026-05-26-slate-v2-shift-boundary-selection.md.

Start Gates:

GateAppliesEvidence
Skill analysis before editsyesLoaded debug, task, autogoal, browser, and changeset; used bug-first repro and package release rules.
Active goal checked or createdyescreate_goal created objective 019e63f8-9985-75c0-ae68-33575e324b41.
Source of truth read before editsyesUser bug report and screenshot inspected; route /examples/hidden-content-blocks used.
Tracker comments and attachments readnoN/A: no tracker item.
Video transcript evidence requirednoN/A: static screenshot, no video.
docs/solutions checked for non-trivial existing-code worknoN/A: local source and prior memory had the route/tooling context; no solution note owned this exact regression.
TDD decision before behavior change or bug fixyesAdded route, unit, DOM coverage, hotkey, and reconciler regression rows.
Branch decision for code-changing tasknoN/A: user did not request branch/PR; repo says do not check git state.
Release artifact decisionyesPublished package behavior changed; split package changesets added.
Browser tool decision for browser surfaceyesUsed in-app Browser as requested.
PR expectation decisionnoN/A: no PR requested.
Tracker sync expectation decisionnoN/A: no tracker item.
Browser pack selectedyesApplied browser pack.
Browser route / app surface identifiedyes/examples/hidden-content-blocks.
Browser tool decision recordedyesIn-app Browser only for manual proof; Playwright only for automated regression.
Console/network caveat policy recordedyesBrowser console errors checked after proof: [].
Package/API pack selectedyesApplied package-api pack for slate-react and slate-dom.
Public surface or package boundary identifiedyesRuntime behavior of published slate-react and slate-dom; no export/barrel change.
Release artifact path selectedyes.changeset/slate-react-hidden-boundary-selection.md and .changeset/slate-dom-hidden-boundary-selection.md.
changeset skill loaded when .changeset is requiredyesLoaded .agents/skills/changeset/SKILL.md; split one package per file.
Barrel/export impact decision recordednoN/A: added internal file only; no public export or exported folder layout change.

Work Checklist:

  • Objective includes outcome, completion threshold, verification surface, constraints, boundaries, and blocked condition.
  • Task source classified with source type, title, task type, acceptance criteria, caveats, likely files/routes/packages, browser surface, and root-cause layer.
  • Required video or screen-recording evidence is N/A: screenshot-only bug report.
  • Nearby repo instructions and implementation patterns read before edits.
  • Implementation fixes the right ownership boundary: DOM coverage selection export and hotkey ownership.
  • Release artifact requirement recorded: package changesets.
  • Final handoff shape decided: concise bug fix summary plus verification.
  • Branch handling recorded: N/A, no branch/PR requested.
  • Local-env-rot retry policy recorded: N/A, no env-rot failure.
  • Workspace authority recorded: commands ran in .tmp/slate-v2; Browser route proof used live app.
  • High-risk note recorded: native DOM selection cannot safely represent hidden boundary-spanning model ranges, so Slate keeps them model-backed instead of projecting through UI chrome.
  • Review/autoreview target selected: local checkout with scoped prompt for this hidden-content shifted boundary selection fix.
  • Agent-native review decision recorded: N/A, no agent/tooling changes.
  • Browser pack: route, interaction path, and expected visible outcome recorded before proof.
  • Browser pack: browser proof uses the repo-approved browser tool.
  • Browser pack: console errors checked.
  • Browser pack: exact verification evidence ready for final handoff.
  • Package/API pack: public API, package boundary, export, and release-artifact impact recorded.
  • Package/API pack: release artifact matrix applied with package changesets.
  • Package/API pack: changeset skill loaded and one-package-per-file changesets added.
  • Package/API pack: registry-only changelog N/A.
  • Package/API pack: compatibility decision explicit; no public API break.
  • Package/API pack: package-owned typecheck/test proof recorded.
  • Package/API pack: generated barrels N/A.

Completion Gates:

GateAppliesRequired actionEvidence
Named verification thresholdyesRun named tests, typechecks, lint, and browser proofAll commands in Verification evidence passed.
Bug reproduced before fixyesRecord failing browser reproBrowser before fix: Shift+ArrowRight native selection text was Accordion body, Overview, Details.
Targeted behavior verificationyesRun focused route and package testsUnit tests and Playwright route passed.
TypeScript or typed config changedyesRun relevant typecheckbun --filter slate-react typecheck; bun --filter slate-dom typecheck passed.
Package exports or file layout changednoN/ANo public export/barrel layout change.
Package manifests, lockfile, or install graph changednoN/ANo manifest or lockfile change.
Agent rules or skills changednoN/ANo agent/tooling change.
Workspace authority proofyesRun proof in owning workspaceAll commands ran in .tmp/slate-v2; plan/check in plate-2.
Browser surface changedyesBrowser proofIn-app Browser proof passed on live route.
Browser final proofyesRecord exact browser outcomeEmpty native selection, active Overview, inactive Details, no hidden secrets for both key paths.
CI-controlled template output changednoN/ANo template output touched.
Package behavior or public API changedyesAdd changesetsslate-react and slate-dom patch changesets added.
Registry-only component work changednoN/ANo registry-only component work.
Docs or content changedyesVerify plan onlyThis plan documents execution; no user docs changed.
High-risk mini gateyesRecord failure mode and proof planFailure mode: native DOM range leaks through non-editable chrome; proof: Browser + Playwright + package tests.
Agent-native review for agent/tooling changesnoN/ANo agent/tooling files changed.
Local install corruption suspectednoN/ANo install-corruption signal.
Autoreview for non-trivial implementation changesyesRun repo autoreview helper until scoped findings are cleanFinal scoped run: autoreview clean: no accepted/actionable findings reported.
PR create or updatenoN/ANo PR requested.
PR proof image hostingnoN/ANo PR.
Tracker sync-backnoN/ANo tracker item.
Final handoff contractyesFill concise final responseReady.
Final lintyesRun scoped lintScoped Biome passed; scoped ESLint had no errors and ignored-file warnings only.
Goal plan completeyesRun autogoal checkerChecker passes after this file update.
Browser interaction proofyesExercise route in BrowserBrowser proof passed after keyboard movement to p1 end.
Browser console/network checkyesCheck console errorsBrowser error logs: []; network not separately inspected because route rendered and proof completed.
Browser final proof artifactyesRecord exact proofNative selection text ""; hidden secret booleans false.
Public API / package boundary proofyesSource-audit package boundary impactNo public API/export change; behavior-only patch.
Release artifact classificationyesClassify release artifactPublished package behavior patch for slate-react and slate-dom.
Published package changesetyesAdd one changeset per package.changeset/slate-react-hidden-boundary-selection.md; .changeset/slate-dom-hidden-boundary-selection.md.
Registry changelognoN/ANot registry-only.
No release artifactnoN/ARelease artifact required and added.
Package typecheck/build/testyesRun package checksTypechecks and focused package tests passed.
Barrel/export generationnoN/ANo export changes.

Phase / pass table:

PhaseStatusEvidenceNext
Intake and source readcompleteBrowser repro and screenshot source readcomplete
ImplementationcompletePackage fix and tests addedcomplete
VerificationcompleteUnit, Playwright, Browser, typecheck, lint completecomplete
PR / tracker synccompleteN/A, no PR/tracker requestedcomplete
CloseoutcompletePlan checker and final responsefinal response

Findings:

  • Root cause: reconciler export projected model ranges crossing DOM coverage boundaries as native DOM ranges, which selected non-editable shadcn chrome.
  • Secondary gap: shifted word movement was not Slate-owned, so Shift+Option+Right could fall through to native browser selection.

Decisions and tradeoffs:

  • Keep hidden boundary-spanning selections model-backed instead of forcing unsafe native DOM ranges.
  • Add opt+shift+left/right and ctrl+shift+left/right word-extension hotkeys to Slate ownership.
  • Do not build a visible projected selection overlay in this bug slice.

Implementation notes:

  • Added applyDOMCoverageSelectionPolicy and used it from both selection export paths.
  • Kept raw DOM range projection intact for clipboard policies; verified with clipboard-boundary tests.
  • Added route and package regression coverage.

Review fixes:

  • Accepted autoreview finding: lower-level DOM range guard would break summary-only clipboard copy; removed that guard and added clipboard-boundary verification.
  • Accepted autoreview finding: Playwright used Apple-only Option word-selection unconditionally; switched automated route row to Control+Shift+ArrowRight and kept Option coverage in platform/unit plus in-app Browser proof.
  • Split changeset into one file per package.
  • Ran scoped Biome fix for import/order/format issues.

Error attempts:

Error / failed attemptCountNext different moveResolution
Initial Playwright Alt+Shift+ArrowRight row moved into hidden text on non-Apple Chromium1Use cross-platform Control+Shift+ArrowRight in PlaywrightFixed; Playwright passed.
In-app Browser direct end-coordinate click landed at paragraph start1Move to p1 end with repeated ArrowRight before shifted proofBrowser proof passed.
Autoreview found lower-level DOM range guard would break summary-only clipboard copy1Keep fix at React selection export layer and verify clipboard policyFixed; clipboard-boundary tests passed.
Autoreview found Playwright Option hotkey was platform-specific1Use Control+Shift+ArrowRight in cross-platform Playwright; keep Option in Browser proofFixed; scoped autoreview clean.

Verification evidence:

  • Browser pre-fix repro: from p1 end, Shift+ArrowRight produced native selection text containing Accordion body, Overview, and Details.
  • Browser final proof: after keyboard movement to exact p1 end, Shift+ArrowRight and Alt+Shift+ArrowRight produced native selection text "", kept Overview active, Details inactive, and did not reveal accordion/details hidden text.
  • Browser console errors after proof: [].
  • bun test:vitest -- keyboard-input-strategy-contract.test.ts selection-reconciler-contract.test.tsx passed: 2 files, 18 tests.
  • cd packages/slate-dom && bun test ./test/dom-coverage.test.ts ./test/hotkeys.test.ts ./test/clipboard-boundary.test.ts passed: 59 tests.
  • cd packages/slate-dom && bun test ./test/hotkeys.test.ts passed after Playwright hotkey change: 15 tests.
  • PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright playwright/integration/examples/hidden-content-blocks.test.ts --project=chromium passed: 4 tests.
  • bun --filter slate-react typecheck passed.
  • bun --filter slate-dom typecheck passed.
  • Scoped bunx biome check ... passed.
  • Scoped bunx eslint ... had no errors; ignored-file warnings only for files outside current ESLint config match.
  • Autoreview final scoped command passed: /Users/zbeyens/git/plate-2/.agents/skills/autoreview/scripts/autoreview --mode local --no-web-search --prompt "Scope this review to the hidden-content shifted boundary selection fix only: slate-dom hotkeys, slate-react caret/selection DOM coverage policy, related tests, Playwright hidden-content-blocks row, and changesets. Ignore unrelated local diffs such as mutation-controller projected-selection replacement work."

Reboot status:

  • Not rebooted; no running blocker remains.

Open risks:

  • None for the reported bug. Future work: visible projected selection overlay for model-backed hidden boundary ranges if product wants a visible highlight instead of an empty native selection.

Final handoff contract:

  • PR line: N/A, no PR requested.
  • Issue / tracker line: N/A, no tracker item.
  • Confidence line: high.
  • Flow table:
    • Reproduced: Browser pre-fix native selection leaked through shadcn chrome.
    • Verified: Browser, Playwright, unit tests, typecheck, and scoped lint pass.
  • Browser check: passed on http://100.102.180.93:3100/examples/hidden-content-blocks.
  • Outcome: shifted right and shifted word-right no longer select non-editable tab/accordion chrome or inactive hidden content.
  • Caveat: model-backed hidden-boundary selections intentionally do not project a visible native highlight across missing DOM.