Skip to content

feat(custom-ui): add contentControl as a first-class viewport entity (SD-3156)#3310

Merged
caio-pizzol merged 2 commits into
mainfrom
caio-pizzol/SD-3156-content-control-viewport-entity
May 14, 2026
Merged

feat(custom-ui): add contentControl as a first-class viewport entity (SD-3156)#3310
caio-pizzol merged 2 commits into
mainfrom
caio-pizzol/SD-3156-content-control-viewport-entity

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Adds content controls (SDTs) to the same ui.viewport surface 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 scraping data-sdt-* attributes themselves. Primitive layer for the umbrella in SD-3155; the ergonomic React hook + demo follow in SD-3157.

  • ViewportEntityHit gains { type: 'contentControl', id, scope?, tag? }. The hit carries only attrs the painter already stamps; full property data comes from editor.doc.contentControls.get with a ContentControlTarget.
  • ViewportGetRectInput.target widens to a UI-local ViewportEntityAddress = EntityAddress | ContentControlViewportAddress. The Document API's EntityAddress stays narrow because content controls aren't a doc-api navigation primitive.
  • entity-at.ts walks data-sdt-id + data-sdt-type=structuredContent, filtering out fieldAnnotation / documentSection / docPartObject. Innermost-first.
  • getRect allowlist + PresentationEditor.getEntityRects route contentControl to 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-key today, 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 in demos/custom-ui against the SD-3110 hidden-SDT fixture: each of the 5 painted SDT wrappers returns the expected entityAt hit and a single paintedRect; wrapper + child-run overmatch is gone.

Review: check that no other call site builds a ViewportEntityAddress literal where the new ContentControlViewportAddress variant breaks a switch (target.entityType) without a default case. I scanned and found none, but worth a second pair of eyes on getEntityRects callers.

Part of SD-3155.

…(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.
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 14, 2026 20:28
@linear
Copy link
Copy Markdown

linear Bot commented May 14, 2026

SD-3156

SD-3155

@codecov-commenter
Copy link
Copy Markdown

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.
@caio-pizzol caio-pizzol merged commit 9477a9e into main May 14, 2026
7 checks passed
@caio-pizzol caio-pizzol deleted the caio-pizzol/SD-3156-content-control-viewport-entity branch May 14, 2026 21:41
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 14, 2026

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

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 14, 2026

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

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 14, 2026

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

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 14, 2026

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

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 14, 2026

🎉 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.
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 15, 2026

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

The release is available on GitHub release

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