feat(custom-ui): add contentControl as a first-class viewport entity (SD-3156)#3310
Merged
caio-pizzol merged 2 commits intoMay 14, 2026
Merged
Conversation
…(SD-3156)
Wires content controls onto the same viewport surface comments and
tracked changes already use, so consumers can hit-test and look up
painted rects for the SDT under the cursor without scraping data-sdt-*
attrs themselves.
Touches the five places that today gate the entity surface to
'comment' | 'trackedChange':
- ViewportEntityHit gains a third variant
{ type: 'contentControl', id, scope?, tag? }. The hit carries only
the fields the painter already stamps (data-sdt-id / sdt-scope /
sdt-tag); alias / controlType / lockMode require metadata plumbing
that doesn't exist on main yet, so consumers wanting full property
data call editor.doc.contentControls.get with a ContentControlTarget
({ kind, nodeType: 'sdt', nodeId }).
- ViewportGetRectInput.target is widened from @superdoc/document-api
EntityAddress (comment | trackedChange) to a UI-local
ViewportEntityAddress that adds ContentControlViewportAddress. The
Document API's EntityAddress stays narrow — content controls aren't
a doc-api navigation primitive (no editor.doc.contentControls.
navigateTo), so the union extension lives on the UI surface.
- entity-at.ts walks data-sdt-id + data-sdt-type, filtering explicitly
to 'structuredContent' so field annotations, document sections, and
doc-part objects don't surface through the contentControls.*
namespace. Innermost-first ordering matches comment + tracked-change.
- ui.viewport.getRect's entity-type allowlist accepts 'contentControl'.
- PresentationEditor.getEntityRects routes 'contentControl' to a new
findRenderedContentControlElements helper next to the existing
comment / tracked-change finders. The helper restricts its selector
to the two wrapper classes (INLINE_SDT_WRAPPER / BLOCK_SDT) because
the painter also stamps SDT metadata on every child text-run
element; a naive [data-sdt-id][data-sdt-type=structuredContent]
selector returns wrapper + every run, polluting the single-painted-
occurrence contract `rect` / `rects` promises.
v1 is body-only. SDT wrappers don't stamp data-story-key today, so the
finder accepts storyKey for signature parity but ignores it; a header /
footer SDT will still match. JSDoc on the helper documents the
limitation and the path forward.
Tests:
- entity-at.test.ts: 17 cases (inline, block, nested, dedup, non-
structuredContent SDT types intentionally ignored, attr-less id
ignored, mixed entity stacks, plus two compile-time contract checks
asserting ViewportEntityHit and ViewportGetRectInput.target accept
the contentControl variant).
- EntityRectFinder.test.ts: 8 cases (single, multi-fragment, type
filter, empty inputs, attr escaping, classless-element rejection,
child-run overmatch regression).
Full super-editor suite: 12956/12956 green.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…3156) Follow-up commit on top of the initial SD-3156 amend addressing four findings from PR #3310 review: 1. Public barrel was missing the new types. `ContentControlViewport Address` and `ViewportEntityAddress` weren't re-exported from `superdoc/ui`, so typed consumers couldn't annotate a content- control `getRect` call even though the runtime accepted it. Added both to `packages/superdoc/src/ui.d.ts`. 2. Wrapper-class scoping needed a regression. The painter stamps SDT metadata on the wrapper AND every child text-run; a naive `[data-sdt-id][data-sdt-type=structuredContent]` selector returns wrapper + every run. The amend already restricted the finder to the two wrapper classes; this commit adds the regression test that would catch a future drift. 3. Cross-story behavior codified. SDT wrappers don't stamp `data-story-key` today, so the `storyKey` argument on the finder is a no-op — a content control with the same id in body and header matches both. New unit test asserts that v1 behavior so a future change can't silently narrow it without an intentional test update. JSDoc on the helper now points to SD-3155 (umbrella) for the follow-up. 4. Test helper used hardcoded class strings. Swapped to `DOM_CLASS_NAMES.INLINE_SDT_WRAPPER` / `BLOCK_SDT` so a future rename can't silently de-sync the helper from production. Barrel regression test uses a file-scan strategy: vitest strips types at runtime and the workspace tsc config excludes `*.test.ts`, so a type-import-only approach wouldn't catch the regression. The scan asserts the four relevant `type` names appear in the export list. I verified the test fails when the barrel is reverted. Verified locally: super-editor suite 12957 pass (+1 cross-story case), superdoc suite 988 pass (+4 barrel cases). Live in custom-ui demo against the SD-3110 fixture: broad selector returns 2 matches per SDT (wrapper + child run), wrapper-class selector returns 1; injected header-story wrapper for SDT 1001 returns both body and header matches as documented. Part of SD-3155.
Contributor
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.102 The release is available on GitHub release |
Contributor
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.146 The release is available on GitHub release |
Contributor
|
🎉 This PR is included in vscode-ext v2.3.0-next.148 |
Contributor
|
🎉 This PR is included in superdoc-cli v0.8.0-next.117 The release is available on GitHub release |
Contributor
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.100 |
caio-pizzol
added a commit
that referenced
this pull request
May 15, 2026
…rtEntityAddress (SD-3156) (#3316) The public `superdoc/ui` barrel (`packages/superdoc/src/ui.d.ts`) imports both names from `@superdoc/super-editor/ui`, but they were missing from `packages/super-editor/src/ui/index.ts`. The existing `ui.barrel.test.ts` only string-scans the barrel file, so it didn't catch the upstream gap. CI's consumer-typecheck matrix (`bundler | node16 | nodenext` with `skipLibCheck=false`) was failing with TS2305 on both names since #3310 landed.
Contributor
|
🎉 This PR is included in superdoc v1.30.0-next.97 The release is available on GitHub release |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds content controls (SDTs) to the same
ui.viewportsurface that comments and tracked changes already use. Consumers can now hit-test the content control under a pointer and ask for its painted rect, instead of scrapingdata-sdt-*attributes themselves. Primitive layer for the umbrella in SD-3155; the ergonomic React hook + demo follow in SD-3157.ViewportEntityHitgains{ type: 'contentControl', id, scope?, tag? }. The hit carries only attrs the painter already stamps; full property data comes fromeditor.doc.contentControls.getwith aContentControlTarget.ViewportGetRectInput.targetwidens to a UI-localViewportEntityAddress = EntityAddress | ContentControlViewportAddress. The Document API'sEntityAddressstays narrow because content controls aren't a doc-api navigation primitive.entity-at.tswalksdata-sdt-id+data-sdt-type=structuredContent, filtering outfieldAnnotation/documentSection/docPartObject. Innermost-first.getRectallowlist +PresentationEditor.getEntityRectsroutecontentControlto a new wrapper-class-scoped finder (.superdoc-structured-content-inline/.superdoc-structured-content-block). The class filter is load-bearing: the painter also stamps SDT metadata on every child text-run, so a naive[data-sdt-id]selector returns wrapper + every run.Story filtering is not supported in v1. SDT wrappers don't stamp
data-story-keytoday, so a header/footer SDT with the same id can still match; the JSDoc names the limitation.Verified locally:
pnpm --filter @superdoc/super-editor exec vitest run→ 12956/12956. Live indemos/custom-uiagainst the SD-3110 hidden-SDT fixture: each of the 5 painted SDT wrappers returns the expectedentityAthit and a singlepaintedRect; wrapper + child-run overmatch is gone.Review: check that no other call site builds a
ViewportEntityAddressliteral where the newContentControlViewportAddressvariant breaks aswitch (target.entityType)without a default case. I scanned and found none, but worth a second pair of eyes ongetEntityRectscallers.Part of SD-3155.