fix(sdt): empty SDT placeholder text + cursor/keyboard interactions (SD-3237)#3542
Merged
caio-pizzol merged 26 commits intoMay 27, 2026
Merged
Conversation
Undo/redo transactions were being blocked by the structured-content lock plugin, preventing recovery of SDT content after deletion. Bypass the lock guard when the transaction is a history undo or redo.
When backspace or delete targets an sdtContentLocked structured-content SDT, collapse the selection to the wrapper boundary instead of letting the keystroke fall through. Prevents the locked inline content from being mutated while keeping the caret in a usable position for the next edit.
Replace the invisible 8px spacer with a full "Click or tap here to enter text" placeholder for both inline and block structured-content controls. The placeholder is layout-only (no document text), styled via CSS pseudo-element, and selected-node highlight inherits the system Highlight color. Wire the new chrome into pointer mapping, caret geometry, and the input manager so clicks on the placeholder land inside the SDT instead of snapping to the wrapper boundary. ArrowLeft from the trailing boundary now re-enters an empty inline SDT, and Backspace/Delete inside an unlocked inline SDT no longer escapes into surrounding text.
Route empty block-SDT paragraphs through paragraphToFlowBlocks so the placeholder run picks up the paragraph's resolved font, size, and color instead of falling back to the document defaults. The painter now applies those run styles to the placeholder span, keeping "Click or tap here to enter text" visually consistent with the surrounding paragraph chrome.
Use `max-content` with `max-width: 130px` instead of stretching block SDT labels to the chrome width, so short labels no longer span the entire block. Inner span now flexes with `min-width: 0` to keep ellipsis behavior.
Stop maxing the measured placeholder width with the fallback — for empty SDTs we now trust the measured value and only fall back when measurement returns zero. Also treat empty-SDT placeholder runs as visible content so chrome geometry (--sd-sdt-chrome-left/width) is emitted for them.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 826a5a153f
ℹ️ 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".
There was a problem hiding this comment.
1 issue found across 25 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…int modes Remove CSS rules that blanked the ::before content for empty SDT placeholders in viewing and print modes so the placeholder prompt stays visible. Update tests to assert the rules are absent.
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.
Summary
Stacks on top of
luccas/sdt-table-navigation. Fixes hover/click cursor placement and keyboard interactions for empty inline and block structured-content controls (SDTs), and replaces the invisible 8px spacer with a real "Click or tap here to enter text" placeholder.Changes
Placeholder rendering (visible "Click or tap here to enter text")
contracts: introduceEMPTY_SDT_PLACEHOLDER_TEXTconstant, broadenvisualPlaceholderto'emptyInlineSdt' | 'emptyBlockSdt', and addisEmptySdtPlaceholderRunhelper (keepsisEmptyInlineSdtPlaceholderRunas a narrow alias).pm-adapter/sdt/structured-content-block.ts: emit a placeholder paragraph for empty block SDTs (including empty-paragraph and vanished-paragraph cases). Empty block paragraphs route throughparagraphToFlowBlocksso the placeholder run inherits the paragraph's resolved font, size, and color.painters/dom: render placeholder via CSS pseudo-element (::beforecontent fromdata-placeholder-text), not document text. New shared classsuperdoc-empty-sdt-placeholderplus per-variantsuperdoc-empty-inline-sdt-placeholder/superdoc-empty-block-sdt-placeholder. Selected-node highlight inherits systemHighlightcolor. Placeholder is suppressed in viewing/print modes and whenappearance='hidden'.measuring/dom: measure the actual placeholder text width (with letter-spacing) instead of a fixed 8px spacer; trust the measured value when non-zero, fall back to a length-based estimate otherwise.layout-bridge/remeasure: treat the placeholder as visible content during line breaking — return its text fromrunText(or empty string whenappearance='hidden') and include its measured width in the line-fit loop so it can wrap atomically.contracts/pm-range: keep the placeholder run's PM range atomic when computing line ranges (don't expand by character).painters/dom/utils/sdt-helpers: skip container chrome styling whenappearance='hidden'.painters/dom/styles: size block SDT labels tomax-content(capped at 130px) so short labels don't span the chrome width; inner span flexes withmin-width: 0for ellipsis.painters/dom/renderer: includeappearancein the SDT dataset and expose it on the DOM so CSS/JS can target hidden SDTs.Pointer / caret / keyboard interactions
DomPointerMapping: clicks on the placeholder snap to itspmStart(inside the SDT) instead of the nearest wrapper edge.DomSelectionGeometry: caret geometry for the placeholder uses the surrounding line rect and only treats positions strictly pastpmEndas "at end".EditorInputManager: a click on an empty SDT placeholder no longer redirects to the wrapper's before/after boundary.structured-content-select-plugin: ArrowLeft from immediately after an empty inline SDT re-enters it.structured-content-lock-plugin:run) stays inside and deletes within the SDT instead of escaping into surrounding text.sdtContentLockedinline SDT collapses the selection to the wrapper boundary rather than letting the keystroke fall through.historyUndo/historyRedotransactions through the lock guard so SDT content can be recovered after deletion.