Skip to content

Merge main into stable#3674

Open
superdoc-bot[bot] wants to merge 53 commits into
stablefrom
merge/main-into-stable-2026-06-06
Open

Merge main into stable#3674
superdoc-bot[bot] wants to merge 53 commits into
stablefrom
merge/main-into-stable-2026-06-06

Conversation

@superdoc-bot
Copy link
Copy Markdown
Contributor

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

Summary

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

Auto-created by promote-stable workflow.

mattConnHarbour and others added 30 commits May 28, 2026 16:26
…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.
harbournick and others added 21 commits June 5, 2026 15:42
…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
…ollapses-generated-line-breaks-in-word

fix(super-editor): preserve generated line breaks in DOCX export
…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
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
@superdoc-bot superdoc-bot Bot requested a review from a team as a code owner June 6, 2026 06:16
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 6, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 6, 2026

Agent docs audit

Found deterministic findings on 1 changed agent-doc item(s).

AGENTS.md (102 lines)

  • 2 broken path ref(s)

Broken path refs:

  • core/layout-adapter/
  • src/editors/v1/core/layout-adapter

Deterministic L1 only: no AI, no Bash, no secrets. Semantic L2/L3 audit runs weekly on main. Policy: agent-docs-policy.md.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 6, 2026

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:

del-translator.js — renames every <w:t> inside <w:del> to <w:delText> (not just the first).

  • This is correct. ECMA-376 requires deleted run text to use w:delText rather than w:t; a leftover <w:t> inside <w:del> would not be treated as deleted by Word. Renaming all text siblings (now that a run can hold <w:t>…<w:br/>…<w:t>) is the right fix.
  • Leaving w:br / w:tab / w:noBreakHyphen untouched is also correct — there are no "deleted" variants of those; the <w:del> wrapper alone conveys the deletion. (w:del, w:delText)

translate-text-node.js — splits text on \n (after CRLF/CR normalization) and emits a bare <w:br/> between <w:t> segments inside a single <w:r>.

  • <w:br/> with no w:type is valid and defaults to textWrapping (a soft line break), which is the intended semantics — not a page break. (w:br)
  • Interleaving <w:t>/<w:br/> within one run is valid run inner content, and the run stays a single run so <w:ins>/<w:del> wrappers still wrap exactly one run.
  • xml:space="preserve" is a legal global attribute on <w:t>, correctly applied only to segments with edge whitespace. (w:t)

One out-of-scope observation (not introduced by this PR): del-translator.js:125 writes a w:authorEmail attribute on <w:del>, which is not an ECMA-376 attribute of CT_RunTrackChange (only w:id, w:author, w:date are defined). It's untouched by this diff, so it doesn't affect this review's verdict — flagging it in case you want a separate cleanup.

Copy link
Copy Markdown

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

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

Comment on lines +76 to +79
if (config.zoom?.initial !== undefined) {
const initialZoom = config.zoom.initial;
if (typeof initialZoom === 'number' && Number.isFinite(initialZoom) && initialZoom > 0) {
activeZoom.value = initialZoom;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.95128% with 39 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/superdoc/src/SuperDoc.vue 60.65% 24 Missing ⚠️
...kages/superdoc/src/composables/use-viewport-fit.js 96.07% 12 Missing ⚠️
packages/superdoc/src/core/SuperDoc.ts 93.33% 2 Missing ⚠️
...es/superdoc/src/components/PdfViewer/PdfViewer.vue 85.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

4 participants