Merge main into stable#3674
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 <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>
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.
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.
…ner-config feat(superdoc): zoom modes with viewport metrics and fit-width (SD-3294)
…nge-event SD-3294 - add layout-change event for responsive fit-to-container zoom
…otices docs: update bundled font license notices
chore(deps): bump uuid to ^11.1.1 (CVE-2026-41907)
…ollapses-generated-line-breaks-in-word fix(super-editor): preserve generated line breaks in DOCX export
…om-example Add responsive zoom example
…llbacks The hand-vendored evidence DATA becomes an import from the published @docfonts/fallbacks registry (pinned 0.2.0), pinned to SuperDoc's local type contract by a const assignment that fails the build if the package shape drifts. The type shapes stay local so the public facade stays self-contained: re-exporting the package's types would leave an unresolvable @docfonts/fallbacks reference in superdoc's emitted .d.ts. One upstream source of truth for the measured data. docfonts owns the evidence and the asset-safe fallback decision; SuperDoc owns what activates. The resolver derives its maps through getRenderableFallback gated by BUNDLED_MANIFEST, so the registry's extra substitutes (Georgia, Arial Narrow, ...) stay inert until their assets ship; key normalization stays normalizeFamilyKey. No behavior change: the same seven rows activate, the default toolbar is unchanged, and FONT_OFFERINGS now classifies the full registry for the later document-specific surface without expanding the toolbar.
…acks-adoption refactor(font-system): source substitution evidence from @docfonts/fallbacks
…-test-fixtures fix: align column test fixtures
…06-000549 🔄 Sync stable → main
Pin the font-system to the face-scope-safe docfonts release. 0.3.0 adds the face-aware lookups (getRenderableFallbackForFace / getFallbackDecisionForFace), a faces field on every result, and the category-fallback face fix - all additive. The vendored wrapper and the resolver are unchanged: the resolver reads only policyAction/substituteFamily from getRenderableFallback (same at 0.3.0), and the SUBSTITUTION_EVIDENCE data-row shape is unchanged so the drift-guard const still compiles. No behavior change. resolveFace stays face-safe via runtime hasFace, which is more accurate than the package's static faces; this just makes 0.3.0 the baseline so a future Regular-only bundled substitute is handled correctly.
…bump chore(font-system): upgrade @docfonts/fallbacks to 0.3.0
A report row's reason (bundled_substitute / category_fallback) says SuperDoc substituted, not how faithful it is - so a consumer can't tell Calibri -> Carlito (metric_safe) from Cambria -> Caladea (visual_only). Add an optional evidence field, present ONLY when SuperDoc rendered the recommended substitute. The projection is docfonts': bump @docfonts/fallbacks to 0.4.0 and read it off the resolved fallback instead of re-deriving locally: - buildFontReport copies it from getRenderableFallback (top-level verdict, all glyph exceptions). - buildFaceReport copies it from getRenderableFallbackForFace (per-face verdict, only that face's exceptions) - so Cambria Regular reads metric_safe with no Bold-Italic exception, Bold Italic reads visual_only and carries it. - Fields copied into SuperDoc's LOCAL ResolvedFontEvidence (evidenceId, policyAction, verdict, lineBreakSafe, glyphExceptions), so @docfonts/fallbacks never leaks into the emitted .d.ts. Additive and behavior-preserving: reason / missing / loadStatus unchanged; as_requested / custom_mapping / registered_face / fallback_face_absent carry no evidence.
…reporting feat(font-system): attach docfonts verdict evidence to font reports
|
📖 Docs preview: https://superdoc-merge-main-into-stable-2026-06-06.mintlify.app |
Agent docs auditFound deterministic findings on 1 changed agent-doc item(s).
|
|
The ecma-spec MCP calls were denied permission in this environment, so I verified against ECMA-376 from knowledge of the schema (CT_RunTrackChange, CT_Br, ST_Text). Here's the review. Status: PASS The OOXML-relevant changes in this PR are spec-compliant. Two handlers changed:
One out-of-scope observation (not introduced by this PR): |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b99d22b3a
ℹ️ 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".
| if (config.zoom?.initial !== undefined) { | ||
| const initialZoom = config.zoom.initial; | ||
| if (typeof initialZoom === 'number' && Number.isFinite(initialZoom) && initialZoom > 0) { | ||
| activeZoom.value = initialZoom; |
There was a problem hiding this comment.
Reset zoom before applying optional initial config
When the same Pinia store is initialized more than once, a later config that omits zoom.initial (or provides an invalid value) skips this branch after reset(), but reset() does not restore activeZoom to 100. That means a document opened after a prior 50% initialization or setZoom(50) can start at 50% unexpectedly even though the new config requested the default; reset the zoom state before applying the optional override.
Useful? React with 👍 / 👎.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Summary
merge/main-into-stable-2026-06-06fromstablemaininto the candidate branchstableAuto-created by promote-stable workflow.