Merge main into stable#3720
Conversation
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…ecause-tc-embedded-_toc
…er zoom Adds a new `layout-change` event that fires when container dimensions change, enabling customers to implement responsive fit-to-container zoom without manual polling or ResizeObservers. Payload includes containerWidth, documentWidth, and fitZoom (calculated zoom to fit document in container). Base document width is captured once at 100% zoom to avoid feedback loops when setZoom is called. Closes SD-3294 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…idth Defer base width capture until isReady is true to avoid latching stale measurements before DOCX layout resolves (e.g., landscape or multi-section documents). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Verify that: - layout-change is not emitted before isReady - payload includes containerWidth, documentWidth, and fitZoom Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…nt highlight (SD-3319) Adds an activate-only method to the Custom UI comments handle so a consumer that scrolls a comment into view itself (e.g. ui.viewport rect plus its own scroll) can mark it active without using scrollTo. Routes through the existing setActiveComment command: highlight-only, no scroll, no selection move, and getSnapshot().activeIds stays selection-derived. Unknown ids (by id or importedId) are rejected so activating a missing id can't fade every other comment.
…e event (SD-3323)
The pending comments-update event now carries pendingSelection, the Document API selection snapshot captured before insertComment('pending') clears the live DOM selection. Consumers building a custom comment composer can forward it straight to ui.comments.createFromCapture instead of continuously tracking the selection ahead of the floating-bubble click.
createFromCapture's input is widened to CommentAnchorCapture (anything carrying a target), so the SelectionInfo on the event passes directly without reconstructing a full SelectionCapture. The field is typed SelectionInfo (not SelectionCapture) since that is what the snapshot actually is.
Related to IT-1113.
… (SD-3329)
getRect now accepts Document API text addresses/targets, not just
entities. A TextAddress (one block) or TextTarget (segments) resolves
via resolveTextTarget to document positions, then to painted rects via a
new PresentationEditor.getBodyRangeRects (body-surface-pinned so a body
target returns body geometry even mid header/footer edit). Multi-segment
targets yield per-segment rects concatenated in document order, returned
in the existing { rect, rects, pageIndex } shape.
Body story only for now: a non-body story target returns
{ success: false, reason: 'unresolved' } (story-aware text rects are a
follow-up). No raw PM positions, no pattern matching, no public position
tracker, no overlay rendering layer.
Addresses npm audit warning for SNYK-JS-UUID-16133035. Note: SuperDoc was not actually vulnerable - we only use the 2-param signature which returns a string directly. The vulnerability only affects the 4-param signature that writes to a caller-provided buffer. Ref: SD-3361 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reshapes the layout-change contract from the base branch before it
ships, and models zoom as mode + value, the shape document viewers use.
- Rename layout-change to viewport-change: the public surface already
exports LayoutUpdatePayload for document layout passes, and what this
event reports is viewport fit. Payload { availableWidth,
documentWidth, fitZoom } carries pure measurements (sidebar-aware,
policy-free).
- Resolve the base document width from page styles, re-resolved per
evaluation, instead of a one-time DOM capture: the measured element
scales with zoom, so any zoom applied before capture corrupted
fitZoom permanently. Never emit before an editor exists.
- zoom config: initial (seeded before first paint, no flash), mode
(manual | fit-width), fitWidth bounds and padding. Padding and
clamping shape the applied fit only, never the metrics.
- setZoom() switches the mode to manual, so picking a percentage stops
the auto-fit instead of fighting it; setZoomMode('fit-width')
re-enters fitting and applies immediately. The fit application
writes zoom state directly and emits zoomChange with the mode.
- New reads: getZoomState(), getViewportMetrics() (latest metrics
readable any time, so late subscribers cannot miss the first
measurement). New constructor callbacks onZoomChange /
onViewportChange register before the first emit.
Adds onZoomChange and onViewportChange as explicitly plumbed callback props (the callbacksRef pattern), so swapped handler identities stay fresh across rerenders without rebuilding the SuperDoc instance. The zoom config flows through props automatically via SuperDocConfig. Event types re-derive from the core Config so the wrapper cannot drift from the core contract.
…SD-3294) Documents the zoom config (initial, mode, fitWidth), the new setZoomMode / getZoomState / getViewportMetrics methods, the viewport-change event and its pure-metrics payload, the zoomChange mode field, and the React responsive-zoom pattern.
…wport metrics (SD-3294) Two gaps a zoom UI hits in practice: - setZoomMode emitted nothing unless the numeric value later changed, so mode-only transitions (entering fit-width at the clamped value, returning to manual) were invisible to zoomChange subscribers. It now emits zoomChange with the current value on every mode change and no-ops on a same-mode call. - The width resolver required a DOCX activeEditor, so PDF-only instances never produced viewport metrics even though setZoom supports PDFs, and multi-document instances measured only the active editor. The resolver now takes the widest measurable page across all documents: DOCX from per-document page styles, PDF from rendered pages normalized by their actual scale factor back to CSS px at 100% zoom (a 612pt letter page renders 816 CSS px), with a pdf:document-ready re-evaluation hook. HTML documents reflow and contribute nothing; an HTML-only instance reports no metrics.
…D-3294) Custom UI gets first-class zoom: ui.zoom exposes one slice (mode, value, fitZoom, bounds, viewport metrics) recomputed on zoomChange and viewport-change, with set(percent) and setMode passthroughs to the host. Hosts without the zoom surface degrade to a static manual/100 snapshot with no-op mutations. React mirrors it with useSuperDocZoom (slice plus bound actions), and the toolbar registry gains a zoom-fit-width toggle command so custom toolbars can offer Fit width without reaching for the host instance. The numeric zoom command is untouched. The built-in toolbar's Fit width affordance stays a follow-up: the state and command layer it needs ships here.
…ent' into caio/sd-3294-fit-to-container-config # Conflicts: # packages/superdoc/src/core/SuperDoc.ts # packages/superdoc/src/public/index.ts # tests/consumer-typecheck/snapshots/superdoc-root-classification.json # tests/consumer-typecheck/snapshots/superdoc-root-exports.json # tests/consumer-typecheck/snapshots/superdoc-root-exports.md # tests/consumer-typecheck/src/all-public-types.ts
…ommand (SD-3294) Extracts the pdf measurement math into a pure helper (normalizePdfPageMeasurement) and locks it directly: scale-relative conversion back to CSS px at 100%, the zoom-fallback path, and the zoom-desync case where a seeded zoom has not reached the viewer yet. Component tests cover the widest-page rule across mixed-orientation documents and the pdf DOM path with a stubbed scale factor. Registry tests lock zoom-fit-width active/disabled state and the fit-width/manual toggle.
… (SD-3294) The UI host-event comment said three events; viewport-change made it four. Root export snapshots regenerate for the union of this branch's zoom types and the font types the base brought in from main.
The pre-commit format hook ran over the merge commit's full staged set and prettified 15 generated and upstream files this branch never touches (mcp catalog, document-api templates, font-system, sdk dispatch). A clean merge takes the base side verbatim for files only one side changed; restore those bytes so the PR diff carries zoom work only. Committed with hooks disabled so the formatter does not reintroduce the drift.
…dth (SD-3294) The mode-model rework widened the emit condition to any rounded availableWidth change, which the dedup unit test correctly rejected in CI: px-level jitter during a window drag would spam consumers with emits that cannot change any fit decision. Restore the intended key (rounded fitZoom plus rounded documentWidth); meaningful available-width changes already surface through fitZoom.
…3278)
Multi-line text in text-mode mutations stored newlines as a raw \n inside
one <w:t>, which Word collapses while SuperDoc renders a break. Convert
newlines to lineBreak nodes at creation, split any residual raw newline
into <w:t>/<w:br/> on export, and make the read model agree that a
lineBreak reads as \n so rewrite/search/query stay consistent. Serializes
as a Word-native <w:br/> (ECMA-376 17.3.3.1).
- buildTextWithTabs: normalize \n, \r\n, \r to lineBreak nodes, gated on
parent admission (probed per edit position) for text*-only parents
- materializeLineBreak: prefer lineBreak over hardBreak (soft, not page)
- getTextNodeForExport: split residual raw newline into <w:t>/<w:br/>
- del-translator: rename every <w:t> in a split run to <w:delText>
- lineBreak.leafText = '\n' so textBetweenWithTabs / charOffsetToDocPos /
text-offset-resolver read a break as \n; idempotent rewrite no longer
duplicates it, a rewrite to single-line text removes it
- SearchIndex honors leafText, and a single hit spanning text+lineBreak+
text coalesces to one contiguous range so query.match('Alpha\nBeta')
works (block separators still split; D5 guard intact)
- list paragraph beforeinput removes the placeholder break when text is
typed; visible text models skip tracked-deleted leaf nodes
… (SD-3278)
Typing into a list item that holds only a placeholder break dropped the
caret before the first inserted character, so subsequent native
keystrokes prepended instead of appended ("abcdef" landed as "bcdefa").
Move the selection past the inserted text after the delete+insert.
…s (SD-3278) Coalesce adjacent search segments only when they are both offset-contiguous (same hit) and document-adjacent (segment.docFrom === current.to). This merges text + lineBreak + text within one run into a single range without bridging a skipped/tracked-deleted leaf or a run boundary, so the downstream D5 contiguity guard still rejects genuinely separate edits.
…verage docs: document Custom UI comment and viewport APIs
…assets feat(font-system): bundle docfonts 0.16 census rows
Note: this ports only the public subtree changes from a mixed source commit (106 public paths, 138 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: b49ed6954e50367e4175101a135ef64bd7f4278b Ported-Public-Prefix: superdoc/public
test(behavior): deflake triple-click selection and list-marker font specs Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 30df0503795007557ca59162e5c8c29b9aedc30d Ported-Public-Prefix: superdoc/public
## What Fixes SD-3328. The selection highlight disappeared over empty space inside table cells. While tracking that down I found two more bugs on the same selection rendering path, so this PR groups the three related fixes. ## Demo https://github.com/user-attachments/assets/812e2ba6-6072-4ed3-a9e0-6ad8384f57a7 ## Background Presentation mode draws its own selection overlay. It does not use the browser selection. The overlay rects come from `computeSelectionRectsFromDom`, which maps the ProseMirror selection to the painted DOM and reads `getClientRects()`. ```mermaid flowchart LR PM["PM selection (from, to)"] --> C[computeSelectionRectsFromDom] C --> R["DOM Range + getClientRects()"] R --> O[overlay rects] ``` ## 1. Dragging into an empty paragraph in a table collapsed the selection (the reported bug) prosemirror-tables normalizes a text selection whose endpoints resolve to different cells when the head sits at the start of its block (`parentOffset === 0`). An empty paragraph in a cell only ever has offset 0, so dragging a body selection onto one rewrote `[44, 2026]` to `[44, 49]` on dispatch. Text positions in a cell escape because their offset is greater than 0, which is why dragging through cell text worked but an empty paragraph killed it. ```mermaid sequenceDiagram participant Drag participant PM as TextSelection.create participant Tables as prosemirror-tables Drag->>PM: create [44, 2026] PM-->>Drag: [44, 2026] Drag->>Tables: dispatch Tables-->>Drag: normalizes to [44, 49] (collapse) ``` Fix: a detector mirrors that exact condition. When a drag frame would collapse, the handler keeps the last good selection and resumes extending as soon as the head moves into cell text. The detector fails open on any unexpected document shape so it can never block a normal selection. ## 2. Interior lines of a multiline selection rendered blank A multiline selection built one DOM Range across every line and trusted `getClientRects()`. Each `.superdoc-line` is absolutely positioned, and some Chrome builds return incomplete rects for a Range crossing those boxes: the first and last lines render, the interior lines collapse to a few slivers. `intersectsNode` does not catch it because the entries still report as intersected. Fix: when a selection spans more than one line, compute rects per line. Interior lines use the line element box, which is reliable across browsers and gives the full width highlight normal selection shows. The first and last lines keep the precise Range so they respect the selection offset and first line indent. ## 3. Empty cell paragraphs resolved to no hit position The geometry hit test returned null for an empty cell paragraph (no runs), which aborted the active drag and froze the selection. Now it falls back to the paragraph PM start so every empty cell resolves to a valid position. ## Testing - New regression tests for each fix. The table detector test pins the behavior against the real prosemirror-tables plugin (it must flag exactly the selections that actually collapse). - All presentation editor tests pass (1139). - Verified each fix with a real drag against the dev server. Co-authored-by: Nick Bernal <nick@superdoc.dev> Source-PR: #3688 Closes #3688 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 1185db8cce133d89e3b065fece98271617fa7d7d Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (3 public paths, 5 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: d74e263e8a58f2e3f65a9b6cffda6c8959f9a50a Ported-Public-Prefix: superdoc/public
chore: empty Note: this ports only the public subtree changes from a mixed source commit (2 public paths, 2 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: a42be3c05e82d1ba3c923bcbea1fcc75dea8dcf8 Ported-Public-Prefix: superdoc/public
Co-authored-by: aorlov <andrii@harbourshare.com> Co-authored-by: Nick Bernal <nick@superdoc.dev> Co-authored-by: Nick Bernal <117235294+harbournick@users.noreply.github.com> Source-PR: #3585 Closes #3585 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 88bae78af91a0b61b6073691e921a4fd711ab827 Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (10 public paths, 5 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: abbc378dc5443c57e24b4632be3be101cd0b4516 Ported-Public-Prefix: superdoc/public
Co-authored-by: VladaHarbour <dataart.vladyslava@harbourcollaborators.com> Co-authored-by: Nick Bernal <117235294+harbournick@users.noreply.github.com> Source-PR: #3716 Closes #3716 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: bbf32cc8cf07d7a3feea713b10515efed3c96a1d Ported-Public-Prefix: superdoc/public
…SD-3366) ## Summary A paragraph whose style resolves `pageBreakBefore`, placed directly after an explicit page break (`w:br w:type="page"`), rendered an extra blank page. Word collapses the redundant break. Fixes [SD-3366](https://linear.app/superdocworkspace/issue/SD-3366/bug-pagebreakbefore-inside-style-preceded-by-pagebreak-results-in-an). Also the root cause behind IT-1152 (the SDT wrapper is irrelevant, confirmed in the ticket). ## Root cause The layout engine already had a fresh-page guard (`shouldSkipRedundantPageBreakBefore`, added for page-forcing section breaks), but it only fires when the fresh page has zero fragments. An explicit page break paragraph leaves its empty remnant (the paragraph mark) on the fresh page, so the guard never fired and the style break added a blank page. ## Word semantics (fixture-verified) Word's rule is structural, not geometric. Verified by rendering a 5-shape fixture matrix in Word: | Shape | Structure before the pageBreakBefore paragraph | Word | |---|---|---| | A | paragraph ending in explicit break (empty remnant) | suppressed | | B | plain empty paragraph, no break | fires | | C | explicit break + text after it | fires | | D | explicit break + one extra empty paragraph | fires | | E | explicit break at end of a text paragraph | suppressed | Only the break paragraph's own remnant is forgiven; any other content re-arms the break. ## Fix `isPageBreakBeforeSatisfiedByExplicitBreak` in `layout-engine/src/index.ts`: suppress a `source=pageBreakBefore` break when the preceding blocks are `[explicit pageBreak][empty remnant paragraph]`. The directly-adjacent case (shape E, no remnant emitted) was already covered by the existing geometric guard. ## Tests - 6 new layout-engine tests covering shapes A–E plus the explicit+explicit double-break case (intentional blank page still honored). - 3 new adapter contract tests pinning the FlowBlock emission shapes the window check relies on. - Full suites green: layout-engine 755 tests, layout-adapter 1972 tests. ## Verification - All 3 ticket fixtures + 5 mutation fixtures match Word page counts exactly (v05: 2 pages, was 3; IT-1152 repro: 4 pages; minimal repro: 2 pages). - Corpus scan (488 docs): exactly 1 doc contains the trigger adjacency (`sd-1962-nested-tables-and-sections.docx`, where Word also suppresses); 10 others have both features but never adjacent, provably unchanged. Co-authored-by: Tadeu Tupinamba <tadeu.tupiz@gmail.com> Source-PR: #3712 Closes #3712 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 29f886dce3f7f20a97cb06d1f8a1e7843578742a Ported-Public-Prefix: superdoc/public
|
📖 Docs preview: https://superdoc-merge-main-into-stable-2026-06-12.mintlify.app |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e4a7bf515
ℹ️ 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".
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
🎉 This PR is included in superdoc-cli v0.17.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.16.0 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.12.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.40.0 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.11.0 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.12.0 |
|
🎉 This PR is included in esign v2.7.2 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/fonts v0.1.1 The release is available on GitHub release |
|
🎉 This PR is included in template-builder v1.12.1 The release is available on GitHub release |
Summary
merge/main-into-stable-2026-06-12fromstablemaininto the candidate branchstableAuto-created by promote-stable workflow.