Skip to content

fix(sdt): block image resize inside locked SDTs and align chrome to content (SD-3258)#3490

Merged
caio-pizzol merged 126 commits into
luccas/sd-3218-sdt-field-deletion-contractfrom
luccas/sd-3258-bug-image-resizing-inside-structured-content
May 27, 2026
Merged

fix(sdt): block image resize inside locked SDTs and align chrome to content (SD-3258)#3490
caio-pizzol merged 126 commits into
luccas/sd-3218-sdt-field-deletion-contractfrom
luccas/sd-3258-bug-image-resizing-inside-structured-content

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

Fixes image resizing and selection interactions inside structured-content (SDT) blocks, and overhauls the SDT block chrome so the border/hover backdrop hug the actual content instead of the full paragraph fragment. While the originating bug (SD-3258) was about image resize inside an SDT, the fix cascaded into geometry, diffing, versioning, and per-field-type styling work because the chrome was previously inflating paragraph geometry and the image runs were not being diffed/versioned at all.

Image resize inside SDTs

  • ImageResizeOverlay.vue now walks the image's ancestor SDT chain (block + inline) and treats the resize as disabled whenever any wrapping SDT has a content-locked lock mode (contentLocked / sdtContentLocked). The "disabled" path also short-circuits transaction dispatch so a stuck drag cannot mutate the doc.
  • New shared helpers isContentLockedMode / isSdtLockedMode live in extensions/structured-content/lockModes.js and replace the duplicated string checks in StructuredContentViewBase.js and the overlay.

Block SDT chrome geometry

  • Border moved off the fragment element onto a ::after pseudo, with pointer-events: none and the 1px padding dropped — chrome no longer changes the fragment's measured size.
  • Hover/group-hover background moved onto a ::before pseudo for the same reason.
  • New applyBlockSdtChromeBounds in painters/dom/renderer.ts measures the actual painted content per line (honoring alignment, indents, hanging/first-line offsets, explicit segment positioning, RTL, justified lines, list markers, and multi-fragment continuation) and exposes --sd-sdt-chrome-left / --sd-sdt-chrome-width. The ::before / ::after and the block label all read those vars so the frame hugs the content.
  • Multi-fragment radius / collapsed-border rules and the viewing-mode suppression were re-pointed at the new pseudo-elements so they keep working.
  • Template-builder per-field-type CSS (field-types.css, utils.ts) now also colors the ::after border for default, hover, and selected states — so the field color matches the new chrome instead of leaking through to the now-invisible direct border.

Selection / focus

  • PresentationEditor.#focusEditorAfterImageSelection now blurs/refocuses before scheduling a selection update, and the scheduled update runs immediately so the SDT-selected chrome paints in the same frame as the image NodeSelection.
  • The selected-block-SDT resolver was widened: when the current NodeSelection is an image (or any non-structuredContentBlock node), it walks the doc to find the enclosing block SDT and applies the selected class to that container. New PresentationEditor tests cover the image-inside-SDT and direct-SDT cases.

Inline image vertical alignment

  • ImageRun.verticalAlign now accepts 'top' | 'bottom'; pm-adapter and the painter default to 'top' so the image box stays inside the measured line height (previously 'bottom' made it overflow the line).
  • To keep surrounding text visually anchored under the image, the painter now sets lineHeight: normal + verticalAlign: bottom on plain text runs that share a line with an inline image — runs with explicit vertAlign / baselineShift are left alone.

Diffing & versioning of inline images

  • layout-bridge/diff.ts now compares ImageRun runs by all visible properties (src, size, alt, spacing, clipPath, rotation/flip/lum, hyperlink, sdt, dataAttrs, etc.) instead of treating them as opaquely equal — so resizing or relocking an inline image marks the paragraph dirty.
  • layout-resolved/versionSignature.ts and the painter's local deriveBlockVersion now fold the same fields into the per-block version hash via a stableSerializeEvidenceValue helper, so version-based memoization invalidates correctly.

Tests

  • New ImageResizeOverlay.test.js covering the lock-mode/disabled path.
  • New PresentationEditor.test.ts cases for image-NodeSelection-inside-block-SDT chrome.
  • Expanded layout-bridge/diff.test.ts and layout-resolved/versionSignature.test.ts for the new image diff fields.
  • New painters/dom/index.test.ts and styles.test.ts cases covering chrome bounds, justified/indented/RTL/continuation cases, and the new pseudo-element styling.

…ragraph geometry

Move block SDT border styling onto a ::after pseudo-element with pointer-events:
none, and drop the 1px padding so the chrome no longer changes fragment
geometry. Default inline image verticalAlign to 'top' so the image box stays
within the measured line height.
When a paragraph line contains an inline image, set surrounding normal
text runs to lineHeight: 'normal' and verticalAlign: 'bottom' so they sit
beside the top-aligned image. Runs with explicit vertical positioning
(vertAlign, baselineShift) are left untouched.
Compute content bounds from rendered lines (honoring paragraph alignment)
and expose --sd-sdt-chrome-left/--sd-sdt-chrome-width on the fragment.
Hover background moves to a ::before pseudo-element; both ::before and
::after use the chrome vars so the frame hugs the content instead of
spanning the full fragment width.
Extend per-type field color CSS to target the structured-content block
::after pseudo-element so the label backdrop matches the field color in
default, hover, and selected states.
@luccas-harbour luccas-harbour requested a review from a team as a code owner May 25, 2026 20:33
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 25, 2026

SD-3258

Register data URI image sources as DOCX media parts during export so
the relationship target resolves correctly. Map the svg+xml MIME
subtype to a .svg extension when deriving the media filename.
…processing

Detect SVG data URIs with known finite sizes and register them in place
during browser-path image handling, skipping the canvas-based resize
pipeline that strips vector content. Normalize svg+xml extensions to
.svg when generating media filenames so the relationship target matches
the stored media key.
…tadata

Only emit the empty-inline-SDT placeholder when the resolved metadata
describes a structuredContent node, so other inline SDT variants aren't
collapsed into a placeholder text run when their content is empty.
luccas-harbour and others added 28 commits May 27, 2026 17:02
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.
…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.
…ments

fix(sdt): empty SDT placeholder text + cursor/keyboard interactions (SD-3237)
fix(super-editor): repair caret, keyboard, and drag interactions around block SDTs (SD-3237)
…ling-toolbar-when-selection-includes-a-locked-sdt

feat(super-editor): disable toolbar mutations inside content-locked SDTs (SD-3274)
…et-as-presetcontent-in-sdt-field-does-not-persist

fix(super-editor): persist data-URI images set as SDT preset content (SD-3116)
@caio-pizzol caio-pizzol merged commit 0174505 into luccas/sd-3218-sdt-field-deletion-contract May 27, 2026
6 checks passed
@caio-pizzol caio-pizzol deleted the luccas/sd-3258-bug-image-resizing-inside-structured-content branch May 27, 2026 23:14
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.

3 participants