Merge main into stable#3686
Conversation
…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 <[email protected]>
…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 <[email protected]>
Verify that: - layout-change is not emitted before isReady - payload includes containerWidth, documentWidth, and fitZoom Co-Authored-By: Claude Opus 4.5 <[email protected]>
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 <[email protected]>
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.
Five verified issues from the multi-agent and Codex review of #3659: - zoom.initial now reaches every surface at first paint: PdfViewer seeds its scale from a new initialScale prop (the activeZoom watcher never fires for a seeded ref, so a PDF painted 100% while getZoom() said 50, putting overlay math 2x off), and the non-layout-engine CSS fallback applies once from the document/editor ready hooks via the factored style application. - Fit-width targets what the renderer paints: the resolver prefers the widest laid-out page (editor.getPages(), the same source SuperEditor's container sizing uses for landscape sections) with body page styles as the pre-pagination fallback. - setZoom/setZoomMode before init now warn and emit nothing instead of advertising a change that was never persisted. - Stored viewport metrics are always latest (refreshed on any field change, frozen against consumer mutation) while the viewport-change event stays deduped to fit-relevant changes; all five public doc surfaces now state that contract precisely. getZoomState() derives its bounds from the same resolver the policy clamps with. - The applied fit floors at 1 (fractional bounds plus a degenerate container could round to 0, which the presentation engine rejects), and width/pagination evaluations defer a tick so measurement never runs against a mid-flush DOM (also fixes the one-frame sidebar bounce). The PDF page scan is skipped without PDF documents, the sidebar measures through a template ref, and the pt-to-px constant imports from the same module PdfViewerPage writes --scale-factor with.
The geometry 'zoom' latch only arms when the zoom value actually changed (seeded from the host state), so mode-only zoomChange emissions with no repaint to consume the tag no longer mis-label the next unrelated layout notification. useSuperDocZoom memoizes its return so the object identity is stable across unrelated parent renders, matching the controller-side slice memo it sits on.
The span-rewrite path got the same parentAllowsLineBreak probe as the rewrite/insert paths but had no newline test, though its comment claimed coverage. Add two cases: a single '\n' in a normal parent mints one lineBreak (no hardBreak, no raw newline text node), and the same into a text*-only total-page-number falls back to literal text with no lineBreak.
The previous lockfile was generated from a dirty working tree (importer entries for untracked local directories, super-editor/superdoc entries not matching the committed manifests) and failed frozen installs. Regenerated on current main with only the uuid catalog change applied; two consecutive regens produce byte-identical output. Beyond the uuid entries, the clean regen re-keys the docs mintlify chain's optional @types/node peer back to the catalog-pinned 22.19.2 and records the registry's new deprecated flag on @microsoft/teamsapp-cli.
uuid@11 bundles type declarations for every dist flavor, and the DefinitelyTyped package is now a deprecation stub. Removes the catalog entry and the two devDependency consumers.
… described (SD-3294) The d643fb9 message documented two freshness tiers, but a format-hook reformatting made the scripted edit miss silently and the diff never contained them. This commit holds the actual change: stored metrics refresh on any field change (frozen against consumer mutation) so getViewportMetrics() and ui.zoom reads are always latest, while the viewport-change event stays deduped to fit-relevant changes. The ui zoom slice's reference-keyed memo now documents the field-gated replacement invariant it relies on. Also drops two em dashes from comments per repo writing rules.
…D-3294) The F2 fix made the resolver prefer editor.getPages() with page styles as the pre-pagination fallback; the composable overview and the events doc still said page styles only.
- anchor tab caret Y to line top for trailing tab (was bottom-aligned tab box) - position soft-break continuation caret at line start, alignment-aware (left/center/right), instead of the page right edge
- anchor tab caret Y to line top for trailing tab (was bottom-aligned tab box) - position soft-break continuation caret at line start, alignment-aware (left/center/right), instead of the page right edge
…options feat(toolbar): list document fonts in the font picker
…359) (#3638) * fix(layout-engine): balance explicit equal-width continuous columns balanceSectionOnPage skipped every section with equalWidth=false plus explicit widths, so continuous newspaper sections declared as <w:cols w:num=N w:equalWidth=0> with equal <w:col w:w> children (the common case) never balanced and rendered single-column. Narrow the skip to GENUINELY-unequal widths: explicit widths that are all equal now balance like implicit equal columns. Genuinely-unequal widths still fill column-by-column (Word parity, unchanged). (SD-2324) * fix(layout-adapter): honor per-column w:space for unequal columns Per ECMA-376 §17.6.4, when columns are not equal width (w:equalWidth=0) the section-level w:cols/@w:space is ignored and the inter-column gap comes from each <w:col w:space>. extractColumns used the section space, over-spacing explicit columns so their widths scaled down to fit and diverged from Word (e.g. the 2002 ISDA sections). Use the per-column w:space for unequal columns; equal-width columns keep the section space. Advances SD-2629 for the uniform-spacing case. (SD-2324) * fix(columns): equal-mode column correctness + explicit count cap (SD-2324) Equal-width sections (w:equalWidth="1" or omitted) now match Word: extraction drops child <w:col> widths and takes the gap from the section w:space (default 720), and normalizeColumnLayout honours per-column widths only when w:equalWidth="0". For explicit columns (w:equalWidth="0"), cap the count to min(w:num, valid child-width count) at the source, so a w:num larger than the provided <w:col> widths no longer creates surplus 1px phantom columns in the fill loop (which reads the raw count). A matching clamp in normalizeColumnLayout stays as a defensive net. * test(columns): cover valid-width count cap, equal-mode count, and absent w:cols (SD-2324) Adds three extraction unit tests for the landed column fix: count caps to the valid child-width count (four <w:col> but two usable w:w -> 2), equal mode takes the count from w:num (count 3, no children), and a section without <w:cols> yields no columnsPx. * fix(layout-engine): balance continuous multi-column sections like Word A two-column section ending at a continuous section break rendered with lumpy columns (SD-3359, IT-1150): on the repro NDA the left column ran 169px deeper than the right, and the following single-column content started below the unbalanced overhang. Three defects, all in the balancing path: - balanceSectionOnPage fed the balancer atomic fragments (canBreak: false, no lineHeights), so a paragraph straddling the column boundary could not split. The balancer already computes line-level break points (blockBreakPoints) with widow/orphan control, but no caller consumed them. Paragraph fragments now opt in with their per-line heights, and a chosen break is applied as fragment surgery: the first half keeps the leading lines, a cloned second half carries the remaining lines to the top of the next column, the same fromLine/toLine and continuation model pagination uses. Paragraphs with w:keepLines (ECMA-376 17.3.1.14) and sectPr marker paragraphs stay atomic. - The binary search floored its target at the tallest block's full height. For a breakable paragraph the indivisible chunk is its tallest line, so the search could never reach the truly balanced height and packed the overflow lines into column 0. - The post-layout gate keyed "mid-doc continuous" off the section's own begin type (its sectPr w:type, 17.6.22) instead of the type of the break that ends it, which is the NEXT section's begin type. A 2-col section that merely started continuous balanced even when ended by a nextPage break; per the 17.18.77 note only a continuous break balances the previous section. Verified against the IT-1150 repro (169px -> 14px imbalance, trailing paragraph tucked directly below the balanced region) and eleven spec-derived document mutations (3 columns, explicit equal and unequal w:col, keepLines, explicit column break, nextPage/evenPage/nextColumn end breaks, w:sep, implicit and explicit body sectPr): all conform to ECMA-376 and the documented Word behaviors (sd-1655, sd-1480, mixed-columns-tabs-tnr). Adds 4 unit tests and 2 layoutDocument integration tests. * fix(layout-engine): honor remeasured fragment lines when splitting columns Review follow-up. A paragraph fragment remeasured for a narrower column (or beside a float) carries its own `lines` array, and resolveParagraph renders that array INSTEAD of slicing measure.lines by fromLine/toLine. The column split cloned the fragment wholesale, so both halves kept the full remeasured array and each column rendered the entire paragraph. Three coordinated corrections: - The split slices `lines` across the halves (first keeps the leading slice, the clone carries the rest), so each column renders only its own lines. - Break points are computed against the fragment's own remeasured line heights when present; measure.lines describes the original width and can disagree in both count and height. - getFragmentHeight sums fragment.lines when present, matching how resolveLayout sizes such fragments, so balancing cursors agree with what the resolve stage actually renders. Adds a regression test with a remeasured fragment (22px lines vs a stale 20px measure) asserting the halves partition the lines and the column cursors advance by the remeasured heights. --------- Co-authored-by: Caio Pizzol <[email protected]> Co-authored-by: Caio Pizzol <[email protected]>
This reverts commit ef5fef5.
fix: caret positioning for tabs and soft-break lines
docs: add garhm to community contributors
…7/superdoc-font-toolbar-catalog
…c-96bf-ffbe04b51e27/superdoc-font-toolbar-catalog fix(toolbar): scroll long font dropdowns
|
📖 Docs preview: https://superdoc-merge-main-into-stable-2026-06-09.mintlify.app |
Agent docs auditFound deterministic findings on 2 changed agent-doc item(s).
|
|
The ecma-spec MCP tools are being denied in this environment, so I verified against ECMA-376 from established knowledge of these (core, unambiguous) run-content elements. Here's my review. Status: PASS Both handlers are spec-compliant. I checked every element and attribute these changes touch.
The two changes also compose correctly: the newline split can produce multiple No non-existent attributes/elements, no missing required attributes, and the Note: I couldn't reach the ecma-spec MCP tools (permission denied in this session), so this verification is from ECMA-376 knowledge rather than a live schema query. If you want, re-run with the spec tools granted and I'll confirm |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4313204d3
ℹ️ 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".
| const zoomMode = ref('manual'); | ||
| // Latest viewport measurements (availableWidth / documentWidth / fitZoom), | ||
| // written by the viewport-fit composable; null until editors mount. | ||
| /** @type {import('vue').Ref<import('@superdoc/core/types/index.js').SuperDocViewportMetrics | null>} */ | ||
| const viewportMetrics = ref(null); |
There was a problem hiding this comment.
Reset viewport zoom state on store init
When the Pinia store is reused for a later SuperDoc instance or document reinitialization, these new session-scoped refs survive reset() because that function only clears document/user fields. A prior session that entered fit-width or measured a viewport can therefore make a new/default SuperDoc keep applying fit-width and report stale metrics before its own config asks for them. Please clear zoomMode back to 'manual' and viewportMetrics back to null during reset/init before seeding from config.
Useful? React with 👍 / 👎.
| const padded = computeFitZoom(availableWidth - options.padding, documentWidth); | ||
| if (padded === null) return null; |
There was a problem hiding this comment.
Clamp fit-width when padding exhausts the viewport
When config.zoom.fitWidth.padding is greater than or equal to the measured available width, this call returns null, so applyFitWidth() skips updating activeZoom. In fit-width mode that leaves the previous zoom value in place instead of honoring the configured min, which is easy to hit in narrow containers or with a reserved sidebar/gutter and causes the document to remain over-zoomed. Treat the padded width as zero and clamp to options.min rather than bailing out.
Useful? React with 👍 / 👎.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Summary
merge/main-into-stable-2026-06-09fromstablemaininto the candidate branchstableAuto-created by promote-stable workflow.