Skip to content

feat(ui): story-aware selection state and capture/restore (SD-2954)#3166

Merged
caio-pizzol merged 7 commits intomainfrom
caio/SD-2954-selection-story-scope
May 5, 2026
Merged

feat(ui): story-aware selection state and capture/restore (SD-2954)#3166
caio-pizzol merged 7 commits intomainfrom
caio/SD-2954-selection-story-scope

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Stacked on top of #3157. The controller seam stamps the active story locator onto the live TextTarget when the routed editor is a header / footer / footnote / endnote, so state.selection.target, state.selection.selectionTarget, and ui.selection.capture() all carry the same story field that ui.viewport.positionAt got in SD-2943. Without it, custom-UI flows that read the live selection and feed it to editor.doc.insert / replace route to body and silently fail to find the block.

ui.selection.restore now compares the captured story against the active surface up-front and returns a typed 'stale' on mismatch (or when the captured story is no longer active). Match by structural value across storyType, refId, noteId, headerFooterKind, variant, and serialized section, so a fresh locator object with the same fields still matches. Captures with no story keep the prior body/default behavior.

The selection-info resolver runs against the routed editor and has no path back to the host's PresentationEditor, so the controller seam is the only place where both editors are reachable. Same shape SD-2943 used for the same reason.

Scope: this fixes the controller surface (ui.state.selection.target, ui.state.selection.selectionTarget, ui.selection.capture, ui.selection.restore). Direct editor.doc.selection.current() calls are intentionally unchanged. Threading story through the lower-level adapter is a deeper change that belongs in its own ticket; flagging here so reviewers don't expect it to land with this PR.

Verified: pnpm exec vitest run src/ui -> 258 passed (16 files, +4 new); pnpm exec tsc -b tsconfig.references.json -> clean.

caio-pizzol and others added 5 commits May 5, 2026 10:40
Two primitives consumers building custom UI keep reaching for and not
finding on the public surface:

ui.viewport.getHost() returns the editor's painted host element so
custom-UI components scope DOM listeners to the editor without a CSS
class filter. The information already lives on
presentationEditor.visibleHost; this lifts it onto the controller.

ui.viewport.positionAt({ x, y }) resolves a viewport coordinate to a
caret position on the routed editor's PM document, returning both the
SelectionPoint and the SelectionTarget shapes so consumers can pass
the result straight to editor.doc.insert / replace / etc. The natural
pair to entityAt: while entityAt answers "what entity is under this
point?", positionAt answers "what caret position is under this
point?" — the missing primitive that lets right-click menus offer
"Paste here" / "Insert at this point" honestly, instead of dispatching
against the user's previous selection.

Both methods scope to the controller's painted host: a multi-instance
page can't have one controller's positionAt return positions in
another's PM doc, and post-destroy calls return null.

Tests cover the happy path, the no-editor-mounted case, and the
missing-posAtCoords stub case.
…-2943)

readBlockId now uses the sdBlockId ?? id ?? blockId fallback the
selection resolver already applies, so positionAt resolves paragraph
clicks instead of returning null. Adds PresentationEditor.getActiveStoryLocator
(unifies story-session and header/footer-session locators) and threads
the result onto SelectionPoint.story / SelectionTarget.story so doc-api
operations route to the active story instead of falling back to body.
The controller stamps the active story locator onto the live
TextTarget when the routed editor is a header/footer/footnote/endnote,
so state.selection.target / selectionTarget and ui.selection.capture()
all carry the same routing information ui.viewport.positionAt got in
SD-2943. ui.selection.restore now compares the captured story against
the active surface and returns a typed 'stale' on mismatch instead of
falling through to a less-specific resolver failure. Captures with no
story keep the prior body/default behavior.

The fix is scoped to the controller surface. Direct
editor.doc.selection.current() calls still return body-scoped targets;
threading story through the lower-level resolver is a separate change.
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 5, 2026 16:17
@linear
Copy link
Copy Markdown

linear Bot commented May 5, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50a391e520

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/super-editor/src/ui/create-super-doc-ui.ts Outdated
Move readActiveStoryLocator and attachStoryToTextTarget below
textTargetToSelectionTarget so the existing JSDoc reattaches to its
function (it was orphaned between two JSDoc blocks).

Move the SD-2954 story-mismatch check after the isEditable / setTextSelection
guards so a header capture restored against a viewing-mode editor
still surfaces 'read-only', matching what body captures already see
in the same condition. Adds a regression test covering the read-only
+ story-capture path.
Base automatically changed from caio/sd-2943-viewport-host-positionat to main May 5, 2026 16:31
readActiveStoryLocator was reading hostEditor.presentationEditor
directly, missing the legacy _presentationEditor field and the
superdocStore.documents[].getPresentationEditor() lookup that
resolveToolbarSources covers. Mounts using either fallback would still
report body-scoped selection state and return 'stale' for valid story
captures.

Route the locator lookup through resolveToolbarSources so all three
documented presentation-resolution paths surface the active story.
Selection-restore drops its duplicate helper and accepts the
pre-resolved locator from the controller, removing the separate code
path. Adds a regression test covering the _presentationEditor fallback.
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@caio-pizzol caio-pizzol self-assigned this May 5, 2026
@caio-pizzol caio-pizzol merged commit 0132a97 into main May 5, 2026
68 of 69 checks passed
@caio-pizzol caio-pizzol deleted the caio/SD-2954-selection-story-scope branch May 5, 2026 17:34
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.55

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in vscode-ext v2.3.0-next.99

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in @superdoc-dev/react v1.2.0-next.97

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc-cli v0.8.0-next.73

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc v1.30.0-next.56

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc-sdk v1.8.0-next.56

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants