Merge main into stable#3761
Conversation
…ning fix(fonts): harden raw fonts config handling (SD-3441) Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 7fd17e2f713dc9627abf6810c94ce3e5c1d3a716 Ported-Public-Prefix: superdoc/public
This pull request improves the detection logic for automatically converting pasted text into links, making it more precise. The main focus is to ensure that only text with an explicit URL intent (such as starting with `www.`, `http(s)://`, `mailto:`, `tel:`, or `sms:`) is auto-linked, and to prevent plain text or relative paths from being mistakenly treated as URLs. Tests have been updated to reflect these changes and to prevent protocol configuration from expanding what is auto-linked. Co-authored-by: Clarence Palmer <c@cepvi.dev> Source-PR: #3710 Closes #3710 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 28036ea238efa862d2849a2ef46b49e81b15a2eb Ported-Public-Prefix: superdoc/public
### Issue In suggesting mode, deleting a selection that spans two paragraphs pushed the remainder of the second paragraph onto a new line, as if Enter was pressed. On the real Backspace/Delete keymap path the tracked interception could crash position mapping (`RangeError: Position NaN`), and the dispatch fallback then deleted the text untracked. Accepting or rejecting the change failed silently, leaving the inline decoration painted, and the hover bubble reported "Accepted" even after a reject. ### Proposed solution - ProseMirror describes a cross-paragraph deletion as a ReplaceStep carrying a structural paragraph re-join shell; the tracked-changes compiler misread that shell as replacement content, inserted it at the selection end (splitting the paragraph), and minted a malformed replacement the decision engine refuses. `replaceStep.js` now compiles slices without inline leaf content as plain tracked deletions, skips the invert/mirror mapping pairing for that mark-only compile (the source of the NaN), and emits an explicit caret hint at the deletion's left edge. - Decision resolve events now carry which decision was taken; the comment model stores it and the resolved badge shows "Rejected" vs "Accepted". The bubble's accept/reject no longer mark the thread resolved when the underlying editor decision did not apply. - Regression tests cover the command path, the real keymap path (Backspace and Delete), the accept/reject lifecycle, and the badge/resolve behavior. Source-PR: #3664 Closes #3664 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 37909c7ec03410452eac599d8afecef1189a87c9 Ported-Public-Prefix: superdoc/public
## Summary Merges latest stable patches into main with a real merge commit so stable release tags remain reachable from main. This keeps `@next` prerelease version calculation ahead of the latest published stable release. ## Merge requirement This PR must be merged with GitHub's merge-commit option. Squash or rebase merging will discard stable ancestry and break `@next` version calculation again. ## Conflict handling Merge status: `clean`. Version-only release artifact conflicts are auto-resolved to keep stable's already-published versions. Non-version conflicts fail this workflow instead of committing conflict markers. --- _Auto-created by sync-patches workflow._ Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net> Co-authored-by: Caio Pizzol <caio@superdoc.dev> Source-PR: #3738 Closes #3738 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 8f97aeff76b89146845047c53b45d09da8ad1262 Ported-Public-Prefix: superdoc/public
chore(layout-compare): layout compare updates and move Note: this ports only the public subtree changes from a mixed source commit (7 public paths, 123 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: c139e76ba3c22c2f78b3e18187a64ea6a67f3318 Ported-Public-Prefix: superdoc/public
…cross-reference-text feat(cross-reference): getText includes resolved cross-reference text Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 1ad83dcff96006bdfcf93ec00e11264a73e3d9c6 Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (3 public paths, 4 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: ce9d8da34d82197e9c487c0d9f66e5ca7ba75b29 Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (17 public paths, 360 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: cd82b960c6d697840de22ac64694366a95958abb Ported-Public-Prefix: superdoc/public
fix(document-api): multiple document api fixes and improvements Note: this ports only the public subtree changes from a mixed source commit (34 public paths, 4 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 83cf1513322c4d6f8f2eeeaaa4e9ba073dec342d Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (384 public paths, 1 non-public path ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 245f1b941f96a242b971202262498deef9400480 Ported-Public-Prefix: superdoc/public
fix: pen testing issues Note: this ports only the public subtree changes from a mixed source commit (18 public paths, 1 non-public path ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 2a22276578955cc76b38a69d1aedd1252b3f6f0e Ported-Public-Prefix: superdoc/public
…3451) (#105) Note: this ports only the public subtree changes from a mixed source commit (65 public paths, 4 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 78c46065d3a6d7009f4cbdb79a96dd91df48b848 Ported-Public-Prefix: superdoc/public
…tab stops (SD-3426) (#66) * fix(layout-engine): resolve first-line list text start from rendered tab stops In first-line indent mode, compute the marker suffix tab width from the actual tab stops (explicit or default) via computeTabWidth instead of trusting stale textStartX/textStartPx targets or flooring the gap to LIST_MARKER_GAP. This fixes justified text width for numbered lists with a first-line indent, where the stale target shifted the measured line start. Remeasure now also prefers the marker's glyphWidthPx for markerTextWidth over the re-measured width. * fix: subtree scoping and dirty-tree checks * Revert "fix: subtree scoping and dirty-tree checks" This reverts commit a1c3f6ab52d470fc6018dd1335e7802d67bb4cff. * test(layout): clarify list marker tab gap expectations Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 4eb26a7568fb8019c0d010f60c079bf78705246d Ported-Public-Prefix: superdoc/public
…ix-port docs: update DOCS readme for Orbit and latest Mintlify dev Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 149ebc67453bb93333a2a300bc51bc2d0e504b64 Ported-Public-Prefix: superdoc/public
* feat(superdoc): v2 package, v1 wiring Note: this ports only the public subtree changes from a mixed source commit (68 public paths, 1575 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 77dacc65cc5ec9280844743959b62e3afe248f3c Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (7 public paths, 33 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 03f9e83cc26463f4bf2beeaa55bed15297291343 Ported-Public-Prefix: superdoc/public
* fix(layout): render footnotes when the body fills a terminal page (SD-3400)
On the last page there is no continuation target, so the SD-2656
bodyMaxY-anchored maxReserve collapses to ~0 once the body fills the page:
the planner can place nothing, reserves[pageIndex] stays 0, the body never
yields, and every anchored footnote is silently dropped (no error). Reproduced
on Footnote tests.docx: 0 of 6 footnotes rendered.
Add a terminal-page reserve bump mirroring the existing carry-forward bump:
when a footnote is anchored on the last page and the placed reserve is short of
its demand, reserve that demand (capped at the physical band) so the next
relayout pass shrinks the body and the footnote renders on its anchor page,
matching Word. Guarded on reserves[pageIndex] < clusterDemand so pages whose
footnote already placed fully are untouched (no gap regression on non-dense
pages or multi-page splits).
Footnote tests.docx now renders all 6 footnotes (grows 1 to 2 pages).
* fix(painter): refresh position attributes on reused story fragments
Root cause of the SD-3400 note caret/selection/arrow family: the
painter keeps reused fragments' data-pm attributes fresh through the
body transaction mapping, but story fragments (notes, headers/footers)
were excluded because the body mapping is the wrong coordinate space —
so their painted positions went permanently stale after any edit above
them, and every DOM-based consumer (caret overlay, selection rects,
vertical arrow navigation, click mapping) read wrong positions.
The resolved paint item carries fresh story positions every paint, so
reused story fragments now get their attributes shifted by the delta
between the fragment's fresh first-line position and the painted one —
exact for unchanged blocks and for continuation fragments. The fuzzer
now asserts the direct invariant: painted note line ranges never
overlap once paint settles.
* fix(footnotes): caret drift in multi-paragraph note sessions
The note caret/selection overlay mapped session positions through a
visible-text-offset bridge that counts structural paragraph tokens on
the painted side (pm-range gaps between lines) but not on the hidden
editor side (raw text walk). Each Enter in a note drifted the caret 4
positions backward, so typing and arrow navigation rendered the caret
inside the previous paragraph's text.
Resolve note caret and selection geometry directly from the painted
lines' session-coordinate pm ranges (data-pm-start/end), falling back
to the offset bridge when a position is not painted (structural gaps,
hidden tracked content). Single-paragraph notes are unaffected; multi
paragraph carets now land on their own lines (live-verified).
* fix(footnotes): keep note paragraph style when splitting in a session
Pressing Enter in a note session ran the linked-style clearing heuristic
(splitRunToParagraph and splitBlock both call it on at-end splits) once
the linked-styles cache populated on the shared converter. FootnoteText
is a linked style (w:link FootnoteTextChar) but has no w:next, so Word
keeps it on Enter; clearing it made new note paragraphs render at the
document default size, appearing as random font growth mid-note.
Skip the clearing for note story sessions. The heuristic stays active
for body and header/footer flows it was built for.
* fix(footnotes): stale caret and style loss in note sessions
Verified audit of every paragraph-creation, caret, and selection path in
note sessions. Four fixes:
Caret: 'selectionUpdate' fires before 'update', so the immediate
selection-overlay flush ran before the epoch/layout gates armed and the
caret rendered against the pre-change paint on every Enter/Backspace.
Doc-changing transactions now defer to the post-paint flush; selection
only changes keep the immediate path. When the pm-range resolver misses
while a rerender is in flight (fresh unpainted paragraph), the overlay
now reschedules after paint instead of bridging against stale geometry.
Geometry: positions on paragraph-boundary tokens between painted lines
snap forward to the next line instead of failing into the offset bridge;
positions beyond the painted lines still return null so the post-paint
retry handles them.
Styles: the remaining paragraph-creation paths that dropped the note
paragraph style are now note-session aware: createParagraphNear and
liftEmptyBlock re-stamp the source paragraph's properties, joinBackward/
joinForward restore the survivor's style when a join loses it, and
clearNodes preserves paragraphProperties. Pinned with a real-keymap
burst test (Enter x3, type, Enter x2, type, Backspace x4, type) with the
linked-styles cache armed.
* fix(footnotes): caret resolution against stale painted note ranges
A 360-op real-keystroke fuzzer (typing, Enter, Backspace, arrows, Home/
End over wrapped and empty paragraphs) exposed persistent caret misses:
the painter skips repainting unchanged note paragraphs, so their painted
data-pm ranges drift after edits shift positions — absolute pm
resolution picked lines from neighboring paragraphs whose stale ranges
overlapped the selection.
Resolve the note caret by block identity instead: find the paragraph's
sdBlockId in the session doc, scope resolution to that block's painted
fragment, and translate the position into the fragment's coordinate
space via the block-start delta (exact for unchanged and fresh blocks).
Empty paragraphs anchor at their content start. Painted lines are also
sorted by pm range before scanning, since fragments come back in DOM
insertion order after incremental repaints.
The fuzzer stays as a regression suite (3 deterministic seeds, ground
truth computed from the session doc plus visible text, independent of
painted pm attributes).
* fix(footnotes): arrow navigation skipping lines in note sessions
ArrowUp/ArrowDown in a note validated their hit test against the
adjacent painted line's data-pm range and, on mismatch, binary-searched
inside that range. Painted ranges of unchanged note paragraphs drift
after edits above them (the painter skips repainting), so the stale
window resolved to a position one line away and the caret skipped lines
(user repro: ArrowUp jumped two lines at once).
Translate the adjacent line's range into current session coordinates
before the plausibility check and the goal-X fallback, anchoring on the
line's paragraph block (sdBlockId lookup in the live doc, shifted by the
fragment-start delta). Home/End boundary resolution gets the same
translation. Pinned with a behavior test that drifts the painted ranges
by editing paragraph 1 and then walks ArrowUp from the bottom asserting
exactly one visual line per press.
* fix(footnotes): native goal-column resolution for arrows in notes
Replaying the user's recorded arrow session exposed two defects in
vertical navigation inside note sessions: the goal column drifted
right on every press, and hit testing could return positions near the
note end for an adjacent line's Y. Root cause: the pipeline converted
between layout and client coordinate spaces that disagree for note
surfaces (computeCaretLayoutRect, denormalizeClientPoint, and the
binary-search fallback each spoke a different space), and the +-5
position plausibility tolerance accepted hits up to ~50px off-column.
Note sessions now resolve the target column natively: the goal column
comes from the painted caret overlay (true client space) and the
adjacent line position from caretRangeFromPoint on the painted line's
text, mapped to a position through the leaf pm attributes. No
layout/client conversions remain in the note path; body navigation
keeps its existing pipeline with the hit test now fed client-space
coordinates directly. The ArrowUp regression test asserts both one
line per press and goal-column preservation.
* fix(footnotes): resolve DOM globals via the document window
NodeFilter and Node are browser globals not defined in the JS lint
environment; take them from ownerDoc.defaultView, which is also more
correct for cross-document usage. Fixes the lint failure on CI.
* fix(footnotes): support firefox caret-from-point in arrow navigation
caretRangeFromPoint is WebKit/Blink-only; firefox exposes
caretPositionFromPoint. Without it, note sessions silently fell back to the
mixed-coordinate hitTest path and the goal column drifted by tens of pixels
on every vertical arrow. Normalize both APIs into one helper. (SD-3400)
* feat(footnote): text cursor over footnote and endnote content (SD-3400)
Note content is painted as generic .superdoc-fragment elements marked
contenteditable=false, so the browser showed a default arrow over editable
note text instead of an I-beam. Add a dedicated ensureFootnoteStyles injector
(mirroring the other per-concern ensure*Styles) that sets cursor: text on
fragments whose block-id starts with footnote-/endnote-/__sd_semantic_footnote-
/__sd_semantic_endnote-. Wired into the renderer's one-time style injection.
* feat(footnote): double-click a body reference to navigate to its note (SD-3400)
Double-clicking a footnote/endnote reference marker in the body now opens the
corresponding note. The painted reference is a superscript run carrying
data-pm-start but no note id, so #handleDoubleClick resolves the PM node at that
position; when it is a footnoteReference/endnoteReference it builds the note
target and calls the existing activateRenderedNoteSession, which focuses the
note session and scrolls it into view. Single-click behavior is unchanged.
* feat(footnote): programmatic note activation for insert-and-focus (SD-3400)
Add PresentationEditor.activateNoteSession(target): opens a footnote/endnote
note session without a pointer, focusing the note and scrolling it into view
with the caret at the note's start. Makes #activateRenderedNoteSession's click
coords optional so the no-coords path skips hit-testing and lands at note start.
This closes the last gap in the insert-footnote flow: the existing
document.footnotes.insert() API creates the body marker + note entry (and the
notes part if absent) and returns the new noteId; a custom toolbar action then
calls activateNoteSession({ storyType, noteId }) so focus moves into the new
note and the user can type immediately. Insert stays in the document API and
focus stays in the presentation layer; the toolbar action composes the two
(kept off the default toolbar).
* feat(footnote): staged backspace/delete of body markers (SD-3400)
Word-like two-step delete from the body. The first Backspace with a collapsed
caret immediately after a footnote/endnote reference selects the marker (a
TextSelection spanning the atom, since footnoteReference is selectable:false);
the second Backspace sees a non-empty selection, so the new command returns
false and the chain falls through to deleteSelection, which removes the marker.
Removal/renumber then cascade through the existing pipeline (the renderer only
paints notes that still have a body reference).
New selectFootnoteMarkerBefore / selectFootnoteMarkerAfter commands wired into
the Backspace chain (after selectInlineSdtBeforeRunStart, before
backspaceAtomBefore) and Delete chain (after selectInlineSdtAfterRunEnd).
footnoteReference is intentionally NOT added to the backspaceAtomBefore
allowlist — staged selection + deleteSelection mirrors the SDT precedent.
Verified end-to-end: 1st press selects marker, 2nd press deletes it; note drops
from the area and remaining notes renumber (6 to 5; {2:1,3:2,4:3,5:4,6:5}).
* feat(footnote): area-delete removes the footnote on both sides (SD-3400)
Clearing all content of a footnote/endnote in the note area now deletes the
whole footnote: commitNoteRuntime detects empty content and calls
footnotesRemoveWrapper, which deletes the body reference node AND removes the
OOXML note element (when no other reference remains). The document then
renumbers through the existing pipeline. This is symmetric with the body-side
staged delete — deleting from either side removes both the marker and the note
(per product decision: remove on both sides), and it avoids the orphaned-ref
state that previously left numbering inconsistent.
Guarded on the body reference still existing so a stale/duplicate commit is a
no-op. Wiring covered by unit tests (mocking the removal boundary, which is
itself covered by footnote-wrappers.test.ts).
* fix(footnote): staged delete at run boundaries and dblclick via hit chain (SD-3400)
Two manual-testing regressions:
Staged delete: each reference is wrapped in its own run, so a caret at the
start of the following run (the common position after clicking past the
superscript) saw nodeBefore as the run wrapper, failed the marker check, and
fell through to normal backspace — deleting the previous letter. The boundary
branches now unwrap a neighboring run whose trailing/leading child is a note
reference. Delete-key mirror gets the same treatment.
Double-click navigation: real pointer events land on the selection overlay
above the pages, so closest('[data-pm-start]') on the event target missed the
painted reference and the dblclick did nothing. The resolver now walks the
elementsFromPoint hit chain, mirroring the rendered-note resolver.
* feat(footnote): one-call insertFootnote command for custom toolbars (SD-3400)
Make FootnoteInsertInput.at optional: omitting it inserts the reference at the
current selection head, which is what a toolbar action needs (place a marker at
the current cursor location). docapi contract gates pass.
Add editor.commands.insertFootnote(): inserts an empty footnote at the caret
(creating the footnotes part with separators when the document has none) and
activates the new note session so the user can immediately type the note text.
Sets preventDispatch on the chain transaction because the document API
dispatches its own compound transactions. Intentionally not registered in the
default toolbar (per SD-3400) — any custom toolbar action can call it.
* feat(footnote): interaction affordance, focus feedback, instant area-delete (SD-3400)
Three UX gaps from manual testing:
Clickability: painted body reference markers now carry data-note-reference /
data-note-id (stamped in buildReferenceMarkerRun, covers endnotes too) and get
a pointer cursor plus a hover pill, signalling that the number is interactive.
Focus feedback: while a note session is open, the note's fragments at the page
bottom get the sd-note-session-active highlight (tint + accent bar + one-time
pulse). Applied on activation, re-applied after every paint (fragments are
rebuilt), removed on exit, and self-healing when the session ends through any
path. Paint-only - no layout impact.
Instant area-delete: clearing all content of a note that previously had
content now auto-exits the session, which commits the both-sides removal
immediately - no click back into the document required. Freshly inserted
empty notes are exempt until they have held content, so insert-and-type is
unaffected.
* style(footnote): soften the active-note highlight (SD-3400)
Lighter tint (0.12 to 0.07 alpha), thinner accent bar (2px to 1px) pushed 3px
away from the note line via a masked box-shadow pair, gentler activation pulse.
Feedback from manual review: the previous bar read too heavy next to the text.
* feat(footnote): enlarge reference hit target and add dev insert button (SD-3400)
The painted reference digit is ~6x11px, which made the hover affordance and
double-click navigation nearly impossible to acquire with a real mouse (the
handler itself is robust — verified with realistic per-event sequences). An
invisible ::after halo expands the interactive target to roughly 16x19px:
hover, pointer cursor, and double-click all hit the marker span, with no text
movement (pseudo-element is absolutely positioned off a position:relative
span).
Also wire an Insert footnote button into the dev app header as the demo of the
custom-toolbar action: it calls editor.commands.insertFootnote(), which inserts
at the caret and focuses the new note. The default product toolbar remains
untouched per SD-3400.
* feat(footnote): smart-scroll to the note on navigation and insert (SD-3400)
Opening a note session now brings the note into view. The scroll is smart:
no-op when the note's fragment is already fully visible, otherwise it
smooth-centers the fragment in the scroll container. Double-clicked notes are
already painted and scroll immediately; freshly inserted notes only paint after
the post-insert relayout, so the request stays pending and completes from the
layoutUpdated hook once the fragment exists. Cleared on session exit.
Verified live: double-click with the note band off-screen scrolls 0 to 490 with
the note fully visible; toolbar insert scrolls 0 to 751 onto the new note.
* fix(footnote): treat uninspectable note docs as non-empty (SD-3400)
The note-session empty watch runs isNoteContentEmpty on activation; session
editors whose doc is not a real PM document (detached/mocked editors in the
PresentationEditor suite) threw doc.descendants is not a function and broke
five existing footnote-interaction tests in CI. Emptiness triggers removal of
the footnote, so an uninspectable doc must read as NOT empty: never delete on
uncertainty.
* refactor(footnote): modularize note-session interactions (SD-3400)
Behavior-preserving modularization of the SD-3400 footnote interaction work,
gated by the existing suites (111 super-editor tests + 16 layout-bridge
footnote tests) and a browser smoke pass.
- notes/note-target.ts: single source of truth for RenderedNoteTarget,
parseRenderedNoteTarget, isSameRenderedNoteTarget, and the block-id prefix
mapping. Removes the duplicated definitions in EditorInputManager and
PresentationEditor.
- notes/NoteSessionCoordinator.ts: extracts the active-note UX (highlight,
smart scroll, emptied-note commit) out of PresentationEditor into a small
collaborator with injected deps, following the dom/ coordinator precedent.
PresentationEditor delegates via onActivated/onPaint/onExit; the logic is
now unit-tested in jsdom (7 tests) instead of browser-only.
- pointer-events/note-reference-hit.ts: pure resolver for double-clicked body
reference markers (closest + elementsFromPoint walk); EditorInputManager
keeps a thin delegating method.
- extensions/footnote/insert-footnote.js: insertFootnoteAtCursor as a plain
importable function; the PM command is now a 3-line shim, keeping the
extension as adapter (schema, NodeView, command registration) with logic in
modules. Covered by its own tests.
- incrementalLayout.ts: dedupe the cluster-demand and band-cap computations
shared by the carry-forward and terminal-page reserve bumps
(clusterDemandFor/maxBandFor).
- note-story-runtime.ts: split commitNoteRuntime into removeEmptiedNote /
commitRichNoteContent / commitPlainTextNoteContent.
* refactor(footnote): complete the story-editor mock instead of guarding types (SD-3400)
Replaces the runtime duck-typing guard added in ddf80d7. The guard was a
type-system lie: isNoteContentEmpty declares a ProseMirrorNode parameter and
then cast it to question its own type. The real defect was an incomplete test
double: PresentationEditor.test.ts mocks story editors whose state.doc had
content.size and textBetween but no descendants, violating the doc contract the
note-session empty watch relies on.
Fix at the source: the mock doc now implements descendants consistently with
the text it already claims to contain. isNoteContentEmpty keeps its honest
contract and the coordinator keeps typed ProseMirrorNode docs with no casts.
In production the session editor is always a real Editor, so no defensive
runtime checks are warranted.
* fix(footnotes): note-story insert guard and full emptied-note removal
Reject footnotes.insert when the target editor is a note, header, or
footer story (ECMA-376 17.11.14: a footnoteReference inside a footnote
or endnote is non-conformant) and strip any reference nodes that reach
a note commit through paste.
Emptying a note in the note area now removes the note everywhere: every
body reference (footnote and endnote ids are independent namespaces, so
resolution is type-aware) plus the OOXML element, in one compound
mutation.
* fix(footnotes): prune note element on body-side staged delete
The second Backspace/Delete on a staged-selected marker previously fell
through to deleteSelection, which removed only the PM reference and left
an orphaned w:footnote element in the notes part (exported to DOCX).
Route the staged delete through removeNoteReferenceAt, extracted from
footnotesRemoveWrapper: position-addressed removal that prunes the OOXML
element when the deleted marker was the note's last reference of that
type. Wired before deleteSelection in both keymap chains, keeping the
'remove on both sides' rule symmetric with the note-area delete.
* feat(footnotes): word-fidelity bootstrap for generated notes
Stamp w:pStyle FootnoteText/EndnoteText on note paragraphs generated
from plain text, matching Word's note body styling (without it, exported
new footnotes render at the Normal style size in Word).
When bootstrapping a missing notes part, also write the special-note
list to settings.xml (ECMA-376 17.11.9): w:footnotePr/w:endnotePr
listing the separator (-1) and continuation separator (0), which strict
consumers require before loading separators. Imported documents keep
their settings untouched.
* feat(footnotes): navigate to note from cross-reference double-click
Double-clicking a REF/NOTEREF cross-reference that points at a footnote
or endnote opens the corresponding note session. Word's cross-reference
bookmark wraps the original note reference in the body, so the resolver
follows crossReference.attrs.target to the named bookmarkStart and scans
its content for a note reference. Cross-references to anything else
(headings, tables) fall through to default double-click behavior.
* test(footnotes): pin backward drag selection symmetry in note sessions
Browser verification on footnote-tests.docx shows right-to-left drags in
note content produce the same range as left-to-right with anchor and
head swapped, across single-line, multi-line, past-margin, and
escape-above-note drags. Pin the direction-agnostic drag path: anchor
stays at the mousedown hit while the head follows the pointer backward.
* fix(footnotes): resolve cross-references across flat bookmark pairs
Real imports emit cross-reference bookmarks as empty bookmarkStart and
bookmarkEnd markers matched by id, with the note reference between them
(verified on the NVCA fixture); the container shape from the schema is
also supported. Scan the marker pair's document range for the note
reference. Live-verified: double-clicking the REF field opens the
referenced footnote session.
* feat(footnotes): document-default fallback for the note marker font
Marker font chain is now: explicit first-run value, then the document
default run properties (w:docDefaults, half-points converted to px),
then the constant. Notes whose first paragraph has no sized run (just
emptied or inserted) keep markers sized with the document instead of
snapping to the constant, so font changes resize the marker predictably.
Keystroke perf with 94 footnotes (NVCA): buildFootnotesInput costs
7.3ms median / 9.2ms max per keystroke, under the 16ms frame budget, so
no footnote measure cache is required.
* test(footnotes): behavior coverage for SD-3400 interactions
End-to-end Playwright specs through the presentation surface: double
click on a body reference marker opens the note session; staged
Backspace selects then removes the marker, prunes the w:footnote
element, and renumbers; clearing all note content removes the footnote
on both sides and exits the session; insertFootnote places a marker at
the cursor on a document without footnotes and focuses the new note.
Also adds an unmocked area-delete integration test exercising the real
removal pipeline (removeNoteEverywhere -> removeNoteElement) against a
real footnotes part.
* test(footnotes): pin separator-variant roundtrip fidelity
Add the footnote-tests-B fixture (explicit separator and continuation
separator notes plus the settings.xml special-footnote list) and a
roundtrip test asserting regular ids, separator types with reserved
ids, the 17.11.9 special list, and body reference order all survive
import and export.
* test(footnotes): pin arrow navigation and caret tracking in notes
Real-keystroke regression for the SD-3400 caret drift report: build a
note with an empty middle paragraph, walk the caret up and down with
arrows, and assert the painted caret lands on each paragraph's own line
and that typing lands where the caret points.
* fix(footnotes): scope dblclick reference resolution to body fragments
Header/footer and rendered-note fragments carry data-pm-start values in
their own story coordinate space. The double-click note-reference resolver
fed those positions into the BODY document's nodeAt, which throws for
positions past the body size and aborted the double-click handler before
header/footer activation ran, so footers with behindDoc textboxes could no
longer be activated. Resolve only elements outside header/footer containers
and note fragments, and bounds-check the position. (SD-3400)
* test(footnotes): restore arrow-walk and fuzz assertions for note fixes
These spec sections pin the caret, painted-range, and goal-column fixes
that landed in the note-editing-fixes slice; they exercise note sessions
through double-click activation, so they ride with the interactions
feature. (SD-3400)
* fix(footnotes): restore note text on undo via tombstoned note elements
Deleting a footnote paired an undoable PM marker delete with a part
mutation that is deliberately isolated from history, so Cmd+Z restored
the superscript but the note text stayed gone and export would write a
dangling w:footnoteReference.
Marker deletes now tombstone the note: only the PM delete happens (the
body doc is the source of truth for note liveness; band painting,
numbering, and the read API already resolve from live markers), the id
is registered as session-managed, and export prunes registered ids with
no surviving reference. Separators and pre-existing orphan notes from
the imported file always survive export. Tombstoned notes are filtered
out of tracked-changes story enumeration, and the undo-group seal the
dropped part mutation used to provide is re-added explicitly. (SD-3400)
* fix(footnotes): stop deletion at the note-area boundary after emptied-note removal
Emptying a note with held Backspace auto-removes the note and exits the
session, but the body was refocused with the stale pre-session selection
and the still-held key kept deleting body text the user was not looking
at.
removeNoteEverywhere now parks the caret at the former first-marker
position inside its own marker-deleting transaction, and the bridge arms
a boundary guard on the noteEmptied exit reason: key-repeat Backspace and
Delete events die at the boundary while a deliberate fresh press behaves
normally. The guard disarms on any other key, pointer press, or doc or
selection change. Emptied-note auto-removal is also gated out of
suggesting mode, where the tracked marker rewrite would silently diverge
from the part state. (SD-3400)
* fix(footnotes): block insertion while a header, footer, or note session is active
The insert guard only rejected story-editor targets, but the command and
the dev toolbar run against the HOST editor, which passed the guard while
a header/footer/note session was active and silently inserted the marker
into the body. New canInsertNoteAtCursor predicate (story-editor target
OR any active non-body story locator) gates insertFootnoteAtCursor and is
exported so custom toolbars can drive their disabled state like Word's
ribbon.
Both command shims also gain dispatch-gates: can().insertFootnote() and
can().deleteSelectedNoteMarker() delegated to self-dispatching
orchestrators and performed REAL mutations on a can() probe. (SD-3400)
* fix(footnotes): stop note links from editing unrelated body links
Links painted inside footnote/endnote fragments carry story-local pm
positions, but the link click pipeline resolved them against the BODY
doc: the caret jumped to an unrelated body position and, when that
position happened to sit inside a body link, the popover opened editing
the WRONG link.
The superdoc-link-click event now carries the note context (additive
detail fields: noteTarget, ctrlKey, metaKey), and LinkClickHandler never
touches the body selection for note links; modifier-click opens the URL.
Body links clicked while a story session is active now exit the session
first (the link early-return used to skip every exit path), and bookmark
anchor navigation exits the session before resolving names against the
body doc. Full in-note link editing is a follow-on. (SD-3400)
* fix(footnotes): harden tombstones, tracked-mode commits, and link routing
Adversarial-review round on the four review-feedback fixes:
- Export pruning is now a pure zip-time transform
(pruneSessionDeletedNotesPart applied at Editor.exportDocx
serialization): the live part store keeps tombstoned elements, so
undo-after-save survives and a later notes-part mutation cannot
rebuild the derived cache without them. Also rewrites the part when
everything prunes (separator-less third-party files no longer export
the stale element).
- Suggesting-mode emptied-note commit returns early without writing the
emptied content into the part (a part write is untracked and not
undoable); reopening the note shows the original text.
- Command shims set preventDispatch only on SUCCESS so a failed
insert/delete cannot poison the rest of a first()/chain transaction.
- The note-area boundary guard ignores remote y-sync transactions when
disarming (a peer edit landing mid-burst must not re-open the window).
- Body links clicked while a note session is active flag
bodyPositionsMayBeStale (the exit commit can delete markers), and the
link handler falls back to coordinate-based caret placement.
Known limitation (follow-on): the session-managed id registry is
client-local; collab peers keep tombstoned elements in their own
exports as unreferenced notes until the registry syncs. (SD-3400)
* fix(footnotes): stay in the note area after emptying; remove on leave
Review feedback: emptying a note auto-exited the session and dropped the
user into the body, where the next Backspace (held OR discrete) deleted
body text. The empty-watch auto-exit is removed: the user stays in the
now-empty note, Backspace at its start is naturally inert, and the
removal plus renumbering happen when the user leaves (the exit-time
commit already removed emptied notes). The caret then parks at the
former marker position.
This makes the key-repeat boundary guard structurally unnecessary
(deletion keys can no longer cause a session exit), so the guard module
and its arming plumbing are removed. (SD-3400)
* feat(footnotes): fidelity, cross-references, and coverage (SD-3400) [4/6] (#31)
* fix(footnote): treat uninspectable note docs as non-empty (SD-3400)
The note-session empty watch runs isNoteContentEmpty on activation; session
editors whose doc is not a real PM document (detached/mocked editors in the
PresentationEditor suite) threw doc.descendants is not a function and broke
five existing footnote-interaction tests in CI. Emptiness triggers removal of
the footnote, so an uninspectable doc must read as NOT empty: never delete on
uncertainty.
* refactor(footnote): complete the story-editor mock instead of guarding types (SD-3400)
Replaces the runtime duck-typing guard added in ddf80d7. The guard was a
type-system lie: isNoteContentEmpty declares a ProseMirrorNode parameter and
then cast it to question its own type. The real defect was an incomplete test
double: PresentationEditor.test.ts mocks story editors whose state.doc had
content.size and textBetween but no descendants, violating the doc contract the
note-session empty watch relies on.
Fix at the source: the mock doc now implements descendants consistently with
the text it already claims to contain. isNoteContentEmpty keeps its honest
contract and the coordinator keeps typed ProseMirrorNode docs with no casts.
In production the session editor is always a real Editor, so no defensive
runtime checks are warranted.
* fix(footnotes): note-story insert guard and full emptied-note removal
Reject footnotes.insert when the target editor is a note, header, or
footer story (ECMA-376 17.11.14: a footnoteReference inside a footnote
or endnote is non-conformant) and strip any reference nodes that reach
a note commit through paste.
Emptying a note in the note area now removes the note everywhere: every
body reference (footnote and endnote ids are independent namespaces, so
resolution is type-aware) plus the OOXML element, in one compound
mutation.
* fix(footnotes): prune note element on body-side staged delete
The second Backspace/Delete on a staged-selected marker previously fell
through to deleteSelection, which removed only the PM reference and left
an orphaned w:footnote element in the notes part (exported to DOCX).
Route the staged delete through removeNoteReferenceAt, extracted from
footnotesRemoveWrapper: position-addressed removal that prunes the OOXML
element when the deleted marker was the note's last reference of that
type. Wired before deleteSelection in both keymap chains, keeping the
'remove on both sides' rule symmetric with the note-area delete.
* feat(footnotes): word-fidelity bootstrap for generated notes
Stamp w:pStyle FootnoteText/EndnoteText on note paragraphs generated
from plain text, matching Word's note body styling (without it, exported
new footnotes render at the Normal style size in Word).
When bootstrapping a missing notes part, also write the special-note
list to settings.xml (ECMA-376 17.11.9): w:footnotePr/w:endnotePr
listing the separator (-1) and continuation separator (0), which strict
consumers require before loading separators. Imported documents keep
their settings untouched.
* feat(footnotes): navigate to note from cross-reference double-click
Double-clicking a REF/NOTEREF cross-reference that points at a footnote
or endnote opens the corresponding note session. Word's cross-reference
bookmark wraps the original note reference in the body, so the resolver
follows crossReference.attrs.target to the named bookmarkStart and scans
its content for a note reference. Cross-references to anything else
(headings, tables) fall through to default double-click behavior.
* test(footnotes): pin backward drag selection symmetry in note sessions
Browser verification on footnote-tests.docx shows right-to-left drags in
note content produce the same range as left-to-right with anchor and
head swapped, across single-line, multi-line, past-margin, and
escape-above-note drags. Pin the direction-agnostic drag path: anchor
stays at the mousedown hit while the head follows the pointer backward.
* fix(footnotes): resolve cross-references across flat bookmark pairs
Real imports emit cross-reference bookmarks as empty bookmarkStart and
bookmarkEnd markers matched by id, with the note reference between them
(verified on the NVCA fixture); the container shape from the schema is
also supported. Scan the marker pair's document range for the note
reference. Live-verified: double-clicking the REF field opens the
referenced footnote session.
* feat(footnotes): document-default fallback for the note marker font
Marker font chain is now: explicit first-run value, then the document
default run properties (w:docDefaults, half-points converted to px),
then the constant. Notes whose first paragraph has no sized run (just
emptied or inserted) keep markers sized with the document instead of
snapping to the constant, so font changes resize the marker predictably.
Keystroke perf with 94 footnotes (NVCA): buildFootnotesInput costs
7.3ms median / 9.2ms max per keystroke, under the 16ms frame budget, so
no footnote measure cache is required.
* test(footnotes): behavior coverage for SD-3400 interactions
End-to-end Playwright specs through the presentation surface: double
click on a body reference marker opens the note session; staged
Backspace selects then removes the marker, prunes the w:footnote
element, and renumbers; clearing all note content removes the footnote
on both sides and exits the session; insertFootnote places a marker at
the cursor on a document without footnotes and focuses the new note.
Also adds an unmocked area-delete integration test exercising the real
removal pipeline (removeNoteEverywhere -> removeNoteElement) against a
real footnotes part.
* test(footnotes): pin separator-variant roundtrip fidelity
Add the footnote-tests-B fixture (explicit separator and continuation
separator notes plus the settings.xml special-footnote list) and a
roundtrip test asserting regular ids, separator types with reserved
ids, the 17.11.9 special list, and body reference order all survive
import and export.
* test(footnotes): pin arrow navigation and caret tracking in notes
Real-keystroke regression for the SD-3400 caret drift report: build a
note with an empty middle paragraph, walk the caret up and down with
arrows, and assert the painted caret lands on each paragraph's own line
and that typing lands where the caret points.
* fix(footnotes): scope dblclick reference resolution to body fragments
Header/footer and rendered-note fragments carry data-pm-start values in
their own story coordinate space. The double-click note-reference resolver
fed those positions into the BODY document's nodeAt, which throws for
positions past the body size and aborted the double-click handler before
header/footer activation ran, so footers with behindDoc textboxes could no
longer be activated. Resolve only elements outside header/footer containers
and note fragments, and bounds-check the position. (SD-3400)
* test(footnotes): restore arrow-walk and fuzz assertions for note fixes
These spec sections pin the caret, painted-range, and goal-column fixes
that landed in the note-editing-fixes slice; they exercise note sessions
through double-click activation, so they ride with the interactions
feature. (SD-3400)
* feat(footnotes): marker interactions, navigate and delete (SD-3400) [2/6] (#32)
* feat(footnote): text cursor over footnote and endnote content (SD-3400)
Note content is painted as generic .superdoc-fragment elements marked
contenteditable=false, so the browser showed a default arrow over editable
note text instead of an I-beam. Add a dedicated ensureFootnoteStyles injector
(mirroring the other per-concern ensure*Styles) that sets cursor: text on
fragments whose block-id starts with footnote-/endnote-/__sd_semantic_footnote-
/__sd_semantic_endnote-. Wired into the renderer's one-time style injection.
* feat(footnote): double-click a body reference to navigate to its note (SD-3400)
Double-clicking a footnote/endnote reference marker in the body now opens the
corresponding note. The painted reference is a superscript run carrying
data-pm-start but no note id, so #handleDoubleClick resolves the PM node at that
position; when it is a footnoteReference/endnoteReference it builds the note
target and calls the existing activateRenderedNoteSession, which focuses the
note session and scrolls it into view. Single-click behavior is unchanged.
* feat(footnote): programmatic note activation for insert-and-focus (SD-3400)
Add PresentationEditor.activateNoteSession(target): opens a footnote/endnote
note session without a pointer, focusing the note and scrolling it into view
with the caret at the note's start. Makes #activateRenderedNoteSession's click
coords optional so the no-coords path skips hit-testing and lands at note start.
This closes the last gap in the insert-footnote flow: the existing
document.footnotes.insert() API creates the body marker + note entry (and the
notes part if absent) and returns the new noteId; a custom toolbar action then
calls activateNoteSession({ storyType, noteId }) so focus moves into the new
note and the user can type immediately. Insert stays in the document API and
focus stays in the presentation layer; the toolbar action composes the two
(kept off the default toolbar).
* feat(footnote): staged backspace/delete of body markers (SD-3400)
Word-like two-step delete from the body. The first Backspace with a collapsed
caret immediately after a footnote/endnote reference selects the marker (a
TextSelection spanning the atom, since footnoteReference is selectable:false);
the second Backspace sees a non-empty selection, so the new command returns
false and the chain falls through to deleteSelection, which removes the marker.
Removal/renumber then cascade through the existing pipeline (the renderer only
paints notes that still have a body reference).
New selectFootnoteMarkerBefore / selectFootnoteMarkerAfter commands wired into
the Backspace chain (after selectInlineSdtBeforeRunStart, before
backspaceAtomBefore) and Delete chain (after selectInlineSdtAfterRunEnd).
footnoteReference is intentionally NOT added to the backspaceAtomBefore
allowlist — staged selection + deleteSelection mirrors the SDT precedent.
Verified end-to-end: 1st press selects marker, 2nd press deletes it; note drops
from the area and remaining notes renumber (6 to 5; {2:1,3:2,4:3,5:4,6:5}).
* feat(footnote): area-delete removes the footnote on both sides (SD-3400)
Clearing all content of a footnote/endnote in the note area now deletes the
whole footnote: commitNoteRuntime detects empty content and calls
footnotesRemoveWrapper, which deletes the body reference node AND removes the
OOXML note element (when no other reference remains). The document then
renumbers through the existing pipeline. This is symmetric with the body-side
staged delete — deleting from either side removes both the marker and the note
(per product decision: remove on both sides), and it avoids the orphaned-ref
state that previously left numbering inconsistent.
Guarded on the body reference still existing so a stale/duplicate commit is a
no-op. Wiring covered by unit tests (mocking the removal boundary, which is
itself covered by footnote-wrappers.test.ts).
* fix(footnote): staged delete at run boundaries and dblclick via hit chain (SD-3400)
Two manual-testing regressions:
Staged delete: each reference is wrapped in its own run, so a caret at the
start of the following run (the common position after clicking past the
superscript) saw nodeBefore as the run wrapper, failed the marker check, and
fell through to normal backspace — deleting the previous letter. The boundary
branches now unwrap a neighboring run whose trailing/leading child is a note
reference. Delete-key mirror gets the same treatment.
Double-click navigation: real pointer events land on the selection overlay
above the pages, so closest('[data-pm-start]') on the event target missed the
painted reference and the dblclick did nothing. The resolver now walks the
elementsFromPoint hit chain, mirroring the rendered-note resolver.
* perf(layout): warm-start footnote convergence and gate numbering recompute (SD-3432) [6/6] (#70)
* perf(layout): warm-start footnote reserve convergence from the previous fixed point
Typing in a footnote document re-derived the identical reserve fixed
point from scratch on every keystroke: four convergence passes from zero
reserves, a grow/tighten round (needsWork was true every keystroke after
a cold converge), and a widow-absorb pass - nine hidden full-document
re-paginations (~73ms of a 134ms keystroke on a 90-page, 25-footnote
document; measured on SD-3432's repro file).
The previous run's applied reserves now seed the convergence loop. The
seed is ONLY a starting vector: every run still executes the full
validation ladder (pass-1 relayout + exact plan stability + grow/tighten
+ widow + preferred trials), so a stale or foreign seed costs extra
passes, never correctness. Seeds are captured only when a run ENDS on an
exact fixed point (final plan equals applied reserves elementwise) -
deliberately independent of the inner loop's stabilized flag, because
documents whose inner loop exhausts its passes routinely still end on an
exact fixed point after grow/tighten and widow absorb (the SD-3432 repro
is exactly such a case). A seeded first pass additionally requires the
recomputed separator spacing to match before it may early-break, and a
seeded run does not pre-register its vector in the oscillation detector.
The seed carries fontSignature and measurement constraints purely to
discard pathological starting vectors after font or zoom changes; it has
no document identity (no footnote ids, no content hashes). It lives as a
private PresentationEditor field threaded per render and reset on
document swap, font reflow, and layout errors; all other callers run
cold and byte-identical to before.
Measured on the SD-3432 repro: footnote pipeline 75ms -> 12ms per
keystroke, one re-pagination instead of nine; keystroke long task p50
134ms -> 74ms. The warm-vs-cold equality contract (unchanged docs, edit
chains, deleted footnotes, page-count changes, foreign seeds,
oscillating docs) and the exactly-two-paginations warm rerun are pinned
in footnoteWarmStart.test.ts via the layoutDocument spy. (SD-3432)
* perf(editor): skip numbering recompute for block-node metadata rounds
The block-node plugin appends a metadata transaction (sdBlockId /
sdBlockRev rewrites) on every keystroke. Its setNodeMarkup steps are
structural, so the numbering plugin's inline-only gate never matched and
every keystroke paid two full-document content diffs
(findDiffStart/findDiffEnd) across the appendTransaction rounds
(~16ms/keystroke on a 776-paragraph document) to conclude nothing
changed. Transactions flagged with BLOCK_NODE_METADATA_UPDATE_META only
ever rewrite those two attributes, which numbering does not read, so the
gate now treats them as numbering-irrelevant. (SD-3432)
* perf(toolbar): coalesce headless toolbar refresh per microtask
A single keystroke fires both 'transaction' and 'selectionUpdate'
synchronously from the same dispatch, and the headless toolbar rebuilt
its full snapshot for each (~7ms per rebuild on a 776-paragraph
document, measured per-listener on the SD-3432 repro), doubling the cost
inside the keystroke's synchronous window for identical snapshots.
Source-event refreshes now coalesce per microtask: subscribers are
notified once per burst, after the dispatch completes and before paint,
with a snapshot reflecting the full transaction. execute() keeps its
synchronous refresh. (SD-3432)
* perf(layout): fold footnote warm-start seed into the initial pagination
The warm-start seed previously applied at the convergence loop, so a
steady-state keystroke still paid two full paginations: the unreserved
initial layout plus one seeded validation relayout. Build the initial
pagination WITH the seed reserves instead (measuring footnote blocks
up front via the extracted measureNoteBlocks/computeNoteBodyHeights
helpers), compute the pass-1 plan against the seed base, and skip the
convergence loop entirely when that plan reproduces the seed. Steady
state is now ONE pagination per keystroke. The page-token relayout
keeps the seeded options so token convergence does not silently drop
the reserves. All guards and the validation ladder are unchanged; a
stale seed still costs passes, never correctness.
* fix(footnotes): guard note paragraph style behind every creator path
The SD-3400 per-command wrappers (splitBlock, joinBackward, joinForward,
createParagraphNear, liftEmptyBlock) re-stamp the note paragraph style
after the base command, but two creators slip through them:
createParagraphNear fired from a gap or node selection has no source
paragraph to copy from, and WebKit DOM-observer reparses recreate
paragraphs with default attributes because paragraphProperties is not
rendered to the DOM. Both depend on async selection sync and mutation
batching, so footnote paragraphs lost FootnoteText flakily on WebKit,
exposed when per-keystroke timing changed.
Add a noteStyleGuard appendTransaction in the paragraph extension: in
note story sessions, any top-level paragraph ending a transaction
without a styleId is re-stamped with its nearest styled sibling's
paragraphProperties (previous preferred, matching Word continuation
semantics and the wrappers' source choice). No-op outside note
sessions. Verified by 42/42 consecutive webkit runs of the note typing
fuzz that previously failed roughly one run in four.
* fix(super-editor): guard body-only footnote inserts and sync note tombstones
Note: this ports only the public subtree changes from a mixed source commit (78 public paths, 2 non-public paths ignored).
Ported-From-Source-Repo: superdoc/orbit
Ported-From-Source-Commit: 7e070bc3f1e15f0fd157eee1c0139e0fa7cc1f3c
Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (62 public paths, 8 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 9f5a603e187f96f4220f5f831c8892bc9e98fe40 Ported-Public-Prefix: superdoc/public
…uence-da feat(document-api): surface paragraph numbering on blocks.list and add format.paragraph.setNumbering Note: this ports only the public subtree changes from a mixed source commit (30 public paths, 5 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 7d52d95fb5a0346c0926dd4e93f9277bd553b5a2 Ported-Public-Prefix: superdoc/public
fix: batch search match tracker updates to avoid find freezes Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: cc8a675d286de3d76cee53d0671686d89019a879 Ported-Public-Prefix: superdoc/public
Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 1c9987247322fbd72bc7f2e60a3498269e7aa198 Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (7 public paths, 121 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 194b730c38171aad1b7b9c5d591d31c664a01846 Ported-Public-Prefix: superdoc/public
* fix(v2): host wiring Note: this ports only the public subtree changes from a mixed source commit (19 public paths, 34 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 68d8adae8dffc68435c010306d0446e18f655fff Ported-Public-Prefix: superdoc/public
Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 750581cb1188b03d433b1b08e893c153e041dda8 Ported-Public-Prefix: superdoc/public
Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: da33911b074eba2dcd48bda77540e23fe272db81 Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (2 public paths, 61 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 54758882ad1c96061497ab0fc11546b494adb9e2 Ported-Public-Prefix: superdoc/public
Main task: SD-3395. Sub-tasks (phases): SD-3405, SD-3406. Textbox object interaction — selection, resize, and move (Phase 2 and 3). ## Summary Adds object-level interaction for floating DrawingML textboxes represented as `shapeContainer` nodes: border selection, resize handles, and drag-to-move. ### Selection - Border clicks within a 6 px hit margin create a ProseMirror `NodeSelection` on the textbox instead of placing a caret in its text content. - Selected textboxes get a `superdoc-textbox-selected` ring; clicking outside clears it. - Works in both body and header/footer contexts. ### Resize - `TextboxResizeOverlay` renders four corner handles for selected textboxes. - Dragging a handle updates `width`/`height` on the `shapeContainer`. - Leading-edge handles also adjust `marginOffset` so the opposite corner remains fixed. - Export patches both `wp:extent` and `wps:spPr > a:xfrm > a:ext` in EMU. The `a:ext` update now targets the shape geometry path directly instead of relying on DFS first-match. ### Move - Dragging the overlay body updates `marginOffset` on the `shapeContainer`. - A temporary CSS `transform: translate(…)` provides live drag feedback. - Textbox moves now invalidate the right caches for table-cell and header/footer relayout. - Export patches `wp:positionH/V > wp:posOffset` in EMU. <img width="514" height="363" alt="Screenshot" src="https://github.com/user-attachments/assets/d5a74cb1-2634-41d5-897c-11cb9d042478" /> Co-authored-by: Artem Nistuley <artem@superdoc.dev> Source-PR: #3721 Closes #3721 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 26e7ad957de19e20ab47c72bcba55ef48e02e0bf Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (8 public paths, 45 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: fc9c18cbfbbef238c7b9ecb697c239482afd5e39 Ported-Public-Prefix: superdoc/public
Note: this ports only the public subtree changes from a mixed source commit (2 public paths, 4 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: c5ff41ba37b2baf996637b517de3141301866b4e Ported-Public-Prefix: superdoc/public
feat(v2): caret placement Note: this ports only the public subtree changes from a mixed source commit (46 public paths, 89 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 5f57e4e56a59f96377547dab4d559b6ae461e1d5 Ported-Public-Prefix: superdoc/public
This reverts commit 5f57e4e56a59f96377547dab4d559b6ae461e1d5, reversing changes made to 92accdc4c5127f0849db1aa7a47dbc13b7684305. Note: this ports only the public subtree changes from a mixed source commit (48 public paths, 89 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 695ab271a9ccb52d2e5746e9bfd4857271afacd2 Ported-Public-Prefix: superdoc/public
fix: remove orphan calls Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: f1cd5e43cd3cf775ae0cb3583ab2ae7a8a5baa7b Ported-Public-Prefix: superdoc/public
fix: install root format hook Note: this ports only the public subtree changes from a mixed source commit (2 public paths, 3 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 803f10b03771552e6e143042384dff3b32fe1b43 Ported-Public-Prefix: superdoc/public
chore: repair superdoc ci baseline Note: this ports only the public subtree changes from a mixed source commit (26 public paths, 2 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 0913535a1833abd5b88fb8d9682b35c2db2663f1 Ported-Public-Prefix: superdoc/public
…eview-navigation-can-jump-and-leave-the Note: this ports only the public subtree changes from a mixed source commit (34 public paths, 14 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 524741de675ba105a5df7ca10ca6d406605069e9 Ported-Public-Prefix: superdoc/public
…-rerender-during-ime-composition fix(super-editor): defer layout rerender and track-changes rewriting during IME composition (SD-2368) [superdoc#3711] Note: this ports only the public subtree changes from a mixed source commit (16 public paths, 1 non-public path ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 8d61fb2b09bdf73209b67e42d2ea5d7fcca50665 Ported-Public-Prefix: superdoc/public
Fix collaboration export of unrecognized Word fields (e.g. MERGEFIELD) by stopping the passthrough importer from attaching content to passthroughInline nodes; instruction text now lives only in attrs.originalXml, which export already uses. Root cause: w:instrText was imported as an inline atom with child text nodes, and y-prosemirror’s schema-checked hydration dropped that node on sync—leaving a broken field shell (fldChar begin/separate/end) without MERGEFIELD in exported DOCX. Added a regression test for w:instrText in inline context and verified TDD (red → green); block passthrough behavior is unchanged. Co-authored-by: Clarence Palmer <c@cepvi.dev> Co-authored-by: Caio Pizzol <caio@harbourshare.com> Source-PR: #3751 Closes #3751 Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 1002160f33f345c7029e2a36874c6f2166619eab Ported-Public-Prefix: superdoc/public
fix: repair comment behavior regressions Note: this ports only the public subtree changes from a mixed source commit (8 public paths, 45 non-public paths ignored). Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: f033b3cd7f1e567e4414936f5d9fdc86f2b8e0fe Ported-Public-Prefix: superdoc/public
|
📖 Docs preview: https://superdoc-merge-main-into-stable-2026-06-20.mintlify.app |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5b05511b30
ℹ️ 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".
Both doc orchestrators used a fresh per-call invoke closure as the hook scope key, so resolved track-change ids never survived between operations in a host session; a repeat trackChanges.decide on an already-resolved id then fell through to the adapter and surfaced TARGET_NOT_FOUND instead of the intended NO_OP. Key the scope by the stable opened.doc handle the session pool reuses across calls, and add an orchestrator-level regression test driving distinct per-call closures over one shared doc (existing hook tests reused a single closure and could not catch this).
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.19.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.18.0 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.14.0 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/fonts v0.1.2 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.42.0 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.13.0 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.14.0 |
|
🎉 This PR is included in esign v2.7.3 The release is available on GitHub release |
Summary
merge/main-into-stable-2026-06-20stableAuto-created by promote-stable workflow.