Skip to content

Merge main into stable#3720

Merged
caio-pizzol merged 191 commits into
stablefrom
merge/main-into-stable-2026-06-12
Jun 12, 2026
Merged

Merge main into stable#3720
caio-pizzol merged 191 commits into
stablefrom
merge/main-into-stable-2026-06-12

Conversation

@superdoc-bot

@superdoc-bot superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • creates merge/main-into-stable-2026-06-12 from stable
  • merges main into the candidate branch
  • opens the promotion PR to stable

Auto-created by promote-stable workflow.

chittolina and others added 30 commits May 15, 2026 16:25
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…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.
caio-pizzol and others added 13 commits June 10, 2026 18:37
…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
Updated version of #3158

Co-authored-by: VladaHarbour <dataart.vladyslava@harbourcollaborators.com>
Source-PR: #3545
Closes #3545

Ported-From-Source-Repo: superdoc/orbit
Ported-From-Source-Commit: 4fa37ef53fbef83b88e20285bc318b2ac38d2f68
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
@superdoc-bot
superdoc-bot Bot requested a review from a team as a code owner June 12, 2026 13:32
@github-actions

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/superdoc/src/stores/superdoc-store.js
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@caio-pizzol
caio-pizzol merged commit 999a660 into stable Jun 12, 2026
82 checks passed
@caio-pizzol
caio-pizzol deleted the merge/main-into-stable-2026-06-12 branch June 12, 2026 16:08
@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in superdoc-cli v0.17.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in superdoc-sdk v1.16.0

@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in @superdoc-dev/mcp v0.12.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in superdoc v1.40.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in @superdoc-dev/react v1.11.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in vscode-ext v2.12.0

@superdoc-bot

superdoc-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in esign v2.7.2

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in @superdoc-dev/fonts v0.1.1

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in template-builder v1.12.1

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.