superdoc 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +12 -90
  2. package/dist/chunks/{create-super-doc-ui-CqGmoMMA.cjs → create-super-doc-ui-Bom7oDPv.cjs} +194 -29
  3. package/dist/chunks/{create-super-doc-ui-CmOSA_lN.es.js → create-super-doc-ui-DbzVC0rr.es.js} +194 -29
  4. package/dist/chunks/{eventemitter3-DEIiXiH2.es.js → eventemitter3-CMOjDs74.es.js} +1 -1
  5. package/dist/chunks/{eventemitter3-C_TAnXOl.cjs → eventemitter3-D38u8ta_.cjs} +1 -1
  6. package/dist/chunks/{jszip-D8mAFF-r.cjs → jszip-RC64TPzL.cjs} +1 -1
  7. package/dist/chunks/{jszip-BzJ3CyxR.es.js → jszip-VxCFV9YS.es.js} +1 -1
  8. package/dist/chunks/{rolldown-runtime-0pSA04fp.es.js → rolldown-runtime-B5TtN50X.es.js} +1 -1
  9. package/dist/chunks/{rolldown-runtime-74VwNCz7.cjs → rolldown-runtime-DtsSbkrh.cjs} +1 -1
  10. package/dist/collaboration-upgrade-engine.cjs +1 -1
  11. package/dist/collaboration-upgrade-engine.es.js +1 -1
  12. package/dist/document-api/src/types/track-changes.types.d.ts +17 -0
  13. package/dist/layout-engine/contracts/src/graphic-placement.d.ts +35 -6
  14. package/dist/layout-engine/contracts/src/index.d.ts +296 -13
  15. package/dist/layout-engine/contracts/src/resolved-layout.d.ts +5 -0
  16. package/dist/layout-engine/dom-contract/src/class-names.d.ts +2 -0
  17. package/dist/layout-engine/layout-bridge/src/footnote-scorer.d.ts +1 -1
  18. package/dist/layout-engine/layout-bridge/src/hydrateTableTextboxMeasures.d.ts +9 -0
  19. package/dist/layout-engine/layout-bridge/src/incrementalLayout.d.ts +40 -12
  20. package/dist/layout-engine/layout-bridge/src/index.d.ts +3 -2
  21. package/dist/layout-engine/layout-bridge/src/layoutHeaderFooter.d.ts +10 -7
  22. package/dist/layout-engine/layout-bridge/src/remeasure.d.ts +2 -2
  23. package/dist/layout-engine/layout-engine/src/anchors.d.ts +4 -0
  24. package/dist/layout-engine/layout-engine/src/execution.d.ts +21 -0
  25. package/dist/layout-engine/layout-engine/src/floating-objects.d.ts +15 -2
  26. package/dist/layout-engine/layout-engine/src/floating-table-anchor.d.ts +2 -0
  27. package/dist/layout-engine/layout-engine/src/footnote-anchor-index.d.ts +17 -0
  28. package/dist/layout-engine/layout-engine/src/index.d.ts +13 -10
  29. package/dist/layout-engine/layout-engine/src/inline-drawing-alignment.d.ts +9 -0
  30. package/dist/layout-engine/layout-engine/src/layout-drawing.d.ts +3 -3
  31. package/dist/layout-engine/layout-engine/src/layout-paragraph.d.ts +2 -2
  32. package/dist/layout-engine/layout-engine/src/layout-textbox.d.ts +3 -3
  33. package/dist/layout-engine/layout-engine/src/pageNumbering.d.ts +3 -41
  34. package/dist/layout-engine/layout-engine/src/paginator.d.ts +7 -2
  35. package/dist/layout-engine/layout-engine/src/resolvePageTokens.d.ts +2 -0
  36. package/dist/layout-engine/layout-engine/src/section-breaks.d.ts +1 -0
  37. package/dist/layout-engine/layout-engine/src/section-props.d.ts +5 -0
  38. package/dist/layout-engine/painters/dom/src/chart-renderer.d.ts +3 -3
  39. package/dist/layout-engine/painters/dom/src/images/image-outline.d.ts +7 -0
  40. package/dist/layout-engine/painters/dom/src/images/render-placeholder.d.ts +12 -0
  41. package/dist/layout-engine/painters/dom/src/renderer.d.ts +54 -0
  42. package/dist/layout-engine/painters/dom/src/runs/text-effects.d.ts +30 -0
  43. package/dist/layout-engine/painters/dom/src/runs/text-run.d.ts +6 -4
  44. package/dist/layout-engine/painters/dom/src/shapes/preset-text-warp-data.d.ts +32 -0
  45. package/dist/layout-engine/painters/dom/src/shapes/wordart-warp.d.ts +39 -0
  46. package/dist/layout-engine/painters/dom/src/table/border-utils.d.ts +20 -27
  47. package/dist/layout-engine/painters/dom/src/table/renderTableFragment.d.ts +1 -1
  48. package/dist/layout-engine/style-engine/src/ooxml/word-style-model/parse-theme.d.ts +7 -0
  49. package/dist/public/ui-react.cjs +1 -1
  50. package/dist/public/ui-react.es.js +1 -1
  51. package/dist/public/ui.cjs +1 -1
  52. package/dist/public/ui.es.js +1 -1
  53. package/dist/style.css +87 -87
  54. package/dist/style.layered.css +87 -87
  55. package/dist/superdoc/src/core/config/normalize-ui-config.d.ts +5 -0
  56. package/dist/superdoc/src/core/types/index.d.ts +24 -0
  57. package/dist/superdoc.cjs +88 -79
  58. package/dist/superdoc.es.js +88 -79
  59. package/dist-cdn/style.layered.css +1 -1
  60. package/dist-cdn/superdoc.min.css +1 -1
  61. package/dist-cdn/superdoc.min.js +37 -37
  62. package/package.json +2 -6
  63. package/AGENTS.md +0 -337
package/README.md CHANGED
@@ -1,106 +1,28 @@
1
- # SuperDoc
1
+ # superdoc
2
2
 
3
- > The document engine for DOCX files.
3
+ Browser editor for opening, editing, and rendering DOCX files.
4
4
 
5
- [![Documentation](https://img.shields.io/badge/docs-available-1355ff.svg)](https://docs.superdoc.dev/)
6
- [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-1355ff.svg)](https://www.gnu.org/licenses/agpl-3.0)
7
- [![npm version](https://img.shields.io/npm/v/superdoc.svg?color=1355ff)](https://www.npmjs.com/package/superdoc)
8
- [![codecov](https://codecov.io/gh/superdoc/docx-editor/branch/main/graph/badge.svg)](https://codecov.io/gh/superdoc/docx-editor)
9
- [![Discord](https://img.shields.io/badge/discord-join-1355ff)](https://discord.com/invite/b9UuaZRyaB)
10
-
11
- SuperDoc renders and edits DOCX files in the browser. Built on OOXML — not bolted onto HTML. As you type, you write directly to the XML. Import a document, edit it, export it. Nothing lost.
12
-
13
- ## Features
14
-
15
- - **Real DOCX, not rich text** — Built on OOXML. Real pagination, section breaks, headers/footers, complex tables. Not a contenteditable wrapper with export bolted on.
16
- - **Self-hosted** — Runs entirely in the browser. Your documents never leave your servers.
17
- - **Any framework** — React, Vue, Angular, Svelte, vanilla JS. One component, zero lock-in.
18
- - **Real-time collaboration** — Yjs-based CRDT. Multiplayer editing with comments, tracked changes, and automatic conflict resolution.
19
- - **Built for agents** — [SDK](https://www.npmjs.com/package/@superdoc-dev/sdk), [CLI](https://www.npmjs.com/package/@superdoc-dev/cli), and [MCP server](https://www.npmjs.com/package/@superdoc-dev/mcp) let LLMs read, edit, and save .docx files programmatically.
20
- - **Dual licensed** — AGPLv3 for community use. [Commercial license](https://www.superdocportal.dev/get-in-touch) for proprietary deployments.
21
-
22
- ## Quick start
5
+ ## Install
23
6
 
24
7
  ```bash
25
8
  npm install superdoc
26
9
  ```
27
10
 
28
- ```javascript
29
- import 'superdoc/style.css';
30
- import { SuperDoc } from 'superdoc';
31
-
32
- const superdoc = new SuperDoc({
33
- selector: '#superdoc',
34
- documentMode: 'editing',
35
- documents: [
36
- {
37
- id: 'my-doc-id',
38
- type: 'docx',
39
- data: fileObject,
40
- },
41
- ],
42
- });
43
- ```
44
-
45
- Supported document modes are `editing`, `viewing`, and `suggesting`. Do not use `edit`, `view`, or `suggest`.
46
-
47
- ### Browser Document API contract
48
-
49
- `superdoc.activeEditor.doc` is the browser-facing v2 Document API facade. In the
50
- browser it is intentionally async-capable, so custom UI code must tolerate
51
- promise-returning reads and writes, including the default worker-backed runtime.
52
- SDK/headless document-automation surfaces remain synchronous; this async
53
- contract is specific to the browser `superdoc` package.
54
-
55
- ### Vanilla wrapper for a DOCX file
56
-
57
- When an agent needs to create a small vanilla JS wrapper around a DOCX `File`, use this shape:
11
+ ## Quick start
58
12
 
59
- ```javascript
13
+ ```js
60
14
  import { SuperDoc } from 'superdoc';
61
15
  import 'superdoc/style.css';
62
16
 
63
- export function embedSuperDoc({ file }) {
64
- return new SuperDoc({
65
- selector: '#editor',
66
- documentMode: 'editing',
67
- documents: [
68
- {
69
- id: 'contract',
70
- type: 'docx',
71
- data: file,
72
- },
73
- ],
74
- });
75
- }
76
- ```
77
-
78
- For React, Vue, and other frameworks, see the [documentation](https://docs.superdoc.dev).
79
-
80
- ### Using an AI coding agent?
81
-
82
- ```bash
83
- claude mcp add superdoc -- npx @superdoc-dev/mcp # connect agent to DOCX files
17
+ const editor = new SuperDoc({
18
+ selector: '#editor',
19
+ document: '/document.docx',
20
+ });
84
21
  ```
85
22
 
86
- ## Documentation
87
-
88
- [docs.superdoc.dev](https://docs.superdoc.dev) — installation, integration guides, collaboration setup, API reference, and more.
89
-
90
- ## Contributing
91
-
92
- Check the [issue tracker](https://github.com/superdoc/docx-editor/issues) for open issues, or read the [Contributing Guide](../../CONTRIBUTING.md) to get started. Bug reports with reproduction .docx files are especially valuable.
93
-
94
- ## Community
95
-
96
- - [Discord](https://discord.com/invite/b9UuaZRyaB) — Chat with the team and other contributors
97
- - [Email](mailto:q@superdoc.dev) — Reach the team directly
23
+ See the [editor quick start](https://docs.superdoc.dev/editor/quickstart) for a complete example and the
24
+ [SuperDoc documentation](https://docs.superdoc.dev) for configuration and APIs.
98
25
 
99
26
  ## License
100
27
 
101
- - Open source: [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.html)
102
- - Commercial: [Enterprise License](https://www.superdocportal.dev/get-in-touch)
103
-
104
- ---
105
-
106
- Created and maintained by [Harbour](https://www.superdoc.dev) and the SuperDoc community.
28
+ AGPL-3.0. Commercial licenses are available from [SuperDoc](https://www.superdoc.dev).
@@ -13,6 +13,8 @@ var DOM_CLASS_NAMES = {
13
13
  PAGE: "superdoc-page",
14
14
  /** Fragment container (paragraph, table, image block, etc.). */
15
15
  FRAGMENT: "superdoc-fragment",
16
+ /** Non-flowing fragment whose visual position must not define body flow geometry. */
17
+ FLOATING_FRAGMENT: "superdoc-floating-fragment",
16
18
  /** Line container within a fragment. */
17
19
  LINE: "superdoc-line",
18
20
  /**
@@ -614,7 +616,6 @@ var SUPERDOC_UI_REASONS = {
614
616
  * No descriptor imports a v1 editor internal or a private v2 runtime package,
615
617
  * and no routed command mutates while the document is read-only / viewing.
616
618
  *
617
- * See `docs/architecture/custom-ui-command-matrix.md` for the per-id matrix.
618
619
  */
619
620
  /**
620
621
  * The 14 canonical v2-native command ids. These are the historically stable
@@ -1873,6 +1874,23 @@ INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "mark").map((entry)
1873
1874
  * `false` when the surface is unavailable (view mode, a pre-ready editor, or a
1874
1875
  * host that has not exposed a Document API facade) rather than throwing.
1875
1876
  */
1877
+ function uiBenchNowMs() {
1878
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
1879
+ }
1880
+ function readUiBenchRuntime() {
1881
+ const benchGlobal = globalThis;
1882
+ return {
1883
+ sink: typeof benchGlobal.__superdocV2BenchPipelineTiming === "function" ? benchGlobal.__superdocV2BenchPipelineTiming : null,
1884
+ workerContext: benchGlobal.__superdocV2BenchWorkerMessageContext ?? null
1885
+ };
1886
+ }
1887
+ function emitUiBenchTiming(event) {
1888
+ const { sink } = readUiBenchRuntime();
1889
+ if (!sink) return;
1890
+ try {
1891
+ sink(event);
1892
+ } catch {}
1893
+ }
1876
1894
  var sharedUiTrackedChangesCatalogByHost = /* @__PURE__ */ new WeakMap();
1877
1895
  function acquireSharedUiTrackedChangesCatalog(host) {
1878
1896
  let state = sharedUiTrackedChangesCatalogByHost.get(host);
@@ -3702,6 +3720,7 @@ function createSuperDocUI(options) {
3702
3720
  if (options?.invalidateQueuedNavigation !== false) queuedTrackChangeNavigationInvalidation += 1;
3703
3721
  };
3704
3722
  const listeners = /* @__PURE__ */ new Set();
3723
+ let state;
3705
3724
  let disposed = false;
3706
3725
  let controllerRef = null;
3707
3726
  const asyncReads = /* @__PURE__ */ new Map();
@@ -3714,12 +3733,13 @@ function createSuperDocUI(options) {
3714
3733
  let foregroundAsyncRetryTimer = null;
3715
3734
  let pendingSelectionSeedValidationToken = null;
3716
3735
  let typingContentInvalidationTimer = null;
3736
+ const pendingAsyncReadSettlements = [];
3717
3737
  const scheduleAsyncRefresh = () => {
3718
3738
  if (asyncRefreshScheduled || disposed) return;
3719
3739
  asyncRefreshScheduled = true;
3720
3740
  const run = () => {
3721
3741
  asyncRefreshScheduled = false;
3722
- if (!disposed) recompute();
3742
+ if (!disposed) recompute("async-read-settled");
3723
3743
  };
3724
3744
  if (typeof queueMicrotask === "function") queueMicrotask(run);
3725
3745
  else Promise.resolve().then(run);
@@ -4273,6 +4293,24 @@ function createSuperDocUI(options) {
4273
4293
  failureCount: 0,
4274
4294
  retryAtMs: 0
4275
4295
  });
4296
+ const { sink, workerContext } = readUiBenchRuntime();
4297
+ if (sink) {
4298
+ const settledAtMs = uiBenchNowMs();
4299
+ pendingAsyncReadSettlements.push({
4300
+ key,
4301
+ settledAtMs,
4302
+ commandId: workerContext?.commandId ?? null,
4303
+ commandKind: workerContext?.commandKind ?? null
4304
+ });
4305
+ emitUiBenchTiming({
4306
+ stage: "superdoc-ui-async-read-settled",
4307
+ atMs: settledAtMs,
4308
+ key,
4309
+ settledAtMs,
4310
+ commandId: workerContext?.commandId ?? null,
4311
+ commandKind: workerContext?.commandKind ?? null
4312
+ });
4313
+ }
4276
4314
  scheduleAsyncRefresh();
4277
4315
  }, () => {
4278
4316
  const entry = asyncReads.get(key);
@@ -6533,31 +6571,57 @@ function createSuperDocUI(options) {
6533
6571
  diagnostics
6534
6572
  };
6535
6573
  };
6536
- const computeState = () => {
6537
- const documentMode = readDocumentMode();
6538
- const selection = computeSelection();
6574
+ const computeState = (reason = "direct") => {
6575
+ const { sink } = readUiBenchRuntime();
6576
+ const startedAtMs = sink ? uiBenchNowMs() : 0;
6577
+ const phaseMs = {};
6578
+ const measure = (phase, run) => {
6579
+ if (!sink) return run();
6580
+ const phaseStartedAtMs = uiBenchNowMs();
6581
+ try {
6582
+ return run();
6583
+ } finally {
6584
+ phaseMs[phase] = uiBenchNowMs() - phaseStartedAtMs;
6585
+ }
6586
+ };
6587
+ const documentMode = measure("documentMode", readDocumentMode);
6588
+ const selection = measure("selection", computeSelection);
6539
6589
  if (selection.status === "ready") pruneSelectionScopedAsyncReads(selection);
6540
6590
  reconcilePendingInlineFormat(selection);
6541
6591
  const selectionSignature = selectionInlineValueSignature(selection);
6542
6592
  for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature || optimistic.selectionSignature !== selectionSignature) optimisticInlineToggles.delete(commandId);
6543
6593
  else if (optimistic.settled && selection.status === "ready") optimisticInlineToggles.delete(commandId);
6544
- return {
6594
+ const nextState = {
6545
6595
  ready: getEditor() != null,
6546
6596
  documentMode,
6547
- document: computeDocument(),
6597
+ document: measure("document", computeDocument),
6548
6598
  selection,
6549
6599
  toolbar: {
6550
6600
  context: documentMode,
6551
- commands: computeCommandStates(selection),
6601
+ commands: measure("toolbarCommands", () => computeCommandStates(selection)),
6552
6602
  copyFormatActive: painter.mode !== "idle"
6553
6603
  },
6554
- comments: computeComments(selection),
6555
- trackChanges: computeTrackChanges(selection),
6556
- contentControls: computeContentControls(selection),
6557
- zoom: computeZoom(),
6558
- fonts: computeFonts(),
6559
- styles: computeStyles(selection)
6560
- };
6604
+ comments: measure("comments", () => computeComments(selection)),
6605
+ trackChanges: measure("trackChanges", () => computeTrackChanges(selection)),
6606
+ contentControls: measure("contentControls", () => computeContentControls(selection)),
6607
+ zoom: measure("zoom", computeZoom),
6608
+ fonts: measure("fonts", computeFonts),
6609
+ styles: measure("styles", () => computeStyles(selection))
6610
+ };
6611
+ if (sink) {
6612
+ const completedAtMs = uiBenchNowMs();
6613
+ emitUiBenchTiming({
6614
+ stage: "superdoc-ui-compute-state",
6615
+ atMs: completedAtMs,
6616
+ reason,
6617
+ startedAtMs,
6618
+ completedAtMs,
6619
+ durationMs: completedAtMs - startedAtMs,
6620
+ phaseMs,
6621
+ asyncReadSettlements: pendingAsyncReadSettlements.splice(0)
6622
+ });
6623
+ }
6624
+ return nextState;
6561
6625
  };
6562
6626
  const syncOptimisticInlineSelection = (selection) => {
6563
6627
  const nextSignature = selectionInlineValueSignature(selection);
@@ -6642,7 +6706,7 @@ function createSuperDocUI(options) {
6642
6706
  const unsubscribe = next.subscribe((snapshot) => {
6643
6707
  selectionEpoch += 1;
6644
6708
  seedCaretSelectionFromHost(snapshot);
6645
- recompute();
6709
+ recompute("host-selection");
6646
6710
  });
6647
6711
  if (typeof unsubscribe === "function") detachHostSelection = unsubscribe;
6648
6712
  } catch {
@@ -6667,15 +6731,24 @@ function createSuperDocUI(options) {
6667
6731
  const target = rawTarget && typeof rawTarget === "object" ? rawTarget : null;
6668
6732
  let next = null;
6669
6733
  if (target?.entityType === "trackedChange" && typeof target.entityId === "string" && target.entityId.length > 0) {
6670
- const paintedEntityId = target.entityId;
6671
6734
  const targetStory = target.story && typeof target.story === "object" ? target.story : void 0;
6672
- const allStoryItems = readAllStoryTrackChanges();
6673
- const entityId = (allStoryItems ? targetStory ? buildStoryScopedTrackedChangeIdContext(allStoryItems, targetStory).toPublicId(paintedEntityId) : buildTrackedChangeIdContext(allStoryItems).toPublicId(paintedEntityId) : null) ?? paintedEntityId;
6674
6735
  const story = targetStory && storyLocatorSignature(targetStory) !== storyLocatorSignature(null) ? targetStory : void 0;
6736
+ const rawPaintedEntityId = typeof target.paintedEntityId === "string" && target.paintedEntityId.length > 0 ? target.paintedEntityId : null;
6737
+ const lookupEntityId = rawPaintedEntityId ?? target.entityId;
6738
+ const publicId = (() => {
6739
+ if (story) {
6740
+ const allStoryItems = readAllStoryTrackChanges();
6741
+ const storyContext = allStoryItems ? buildStoryScopedTrackedChangeIdContext(allStoryItems, story) : null;
6742
+ return storyContext?.toPublicId(lookupEntityId) ?? storyContext?.toPublicId(target.entityId) ?? target.entityId;
6743
+ }
6744
+ const bodyContext = buildTrackedChangeIdContext(Array.isArray(state?.trackChanges.items) ? state.trackChanges.items : []);
6745
+ return bodyContext.toPublicId(lookupEntityId) ?? bodyContext.toPublicId(target.entityId) ?? target.entityId;
6746
+ })();
6747
+ const paintedEntityId = rawPaintedEntityId ?? (lookupEntityId !== publicId ? lookupEntityId : null);
6675
6748
  next = {
6676
- id: entityId,
6749
+ id: publicId,
6677
6750
  ...story ? { story } : {},
6678
- ...typeof target.paintedEntityId === "string" && target.paintedEntityId.length > 0 ? { paintedEntityId: target.paintedEntityId } : entityId !== paintedEntityId ? { paintedEntityId } : {}
6751
+ ...paintedEntityId && paintedEntityId !== publicId ? { paintedEntityId } : {}
6679
6752
  };
6680
6753
  }
6681
6754
  if (rawTarget == null && next == null && explicitActiveChange != null && hasPendingTrackChangeRevealFocus(explicitActiveChange)) return false;
@@ -6741,7 +6814,7 @@ function createSuperDocUI(options) {
6741
6814
  };
6742
6815
  syncCoordinatorEditor();
6743
6816
  syncTrackedChangeFocusFromHostReviewTarget(readHostActiveReviewTarget(readHostReviewSource()));
6744
- let state = computeState();
6817
+ state = computeState("initial");
6745
6818
  lastOptimisticInlineSelectionSignature = selectionInlineValueSignature(state.selection);
6746
6819
  const syncHostEventsSubscription = () => {
6747
6820
  const events = (getEditor()?.host)?.events;
@@ -6809,8 +6882,10 @@ function createSuperDocUI(options) {
6809
6882
  currentHostEventsSource = null;
6810
6883
  }
6811
6884
  };
6812
- const recompute = () => {
6885
+ const recompute = (reason = "unspecified") => {
6813
6886
  if (disposed) return;
6887
+ const { sink } = readUiBenchRuntime();
6888
+ const startedAtMs = sink ? uiBenchNowMs() : 0;
6814
6889
  editCommandsRecomputeQueued = false;
6815
6890
  syncCoordinatorEditor();
6816
6891
  syncHostSelectionSubscription();
@@ -6819,10 +6894,36 @@ function createSuperDocUI(options) {
6819
6894
  syncHostReviewSubscription();
6820
6895
  syncDocumentSelectionFallbackSubscription();
6821
6896
  syncHostEventsSubscription();
6822
- const nextState = computeState();
6897
+ const nextState = computeState(reason);
6823
6898
  syncOptimisticInlineSelection(nextState.selection);
6824
6899
  state = nextState;
6825
- for (const listener of [...listeners]) listener(state);
6900
+ let listenerTotalMs = 0;
6901
+ let listenerMaxMs = 0;
6902
+ for (const listener of [...listeners]) {
6903
+ if (!sink) {
6904
+ listener(state);
6905
+ continue;
6906
+ }
6907
+ const listenerStartedAtMs = uiBenchNowMs();
6908
+ listener(state);
6909
+ const listenerMs = uiBenchNowMs() - listenerStartedAtMs;
6910
+ listenerTotalMs += listenerMs;
6911
+ listenerMaxMs = Math.max(listenerMaxMs, listenerMs);
6912
+ }
6913
+ if (sink) {
6914
+ const completedAtMs = uiBenchNowMs();
6915
+ emitUiBenchTiming({
6916
+ stage: "superdoc-ui-recompute",
6917
+ atMs: completedAtMs,
6918
+ reason,
6919
+ startedAtMs,
6920
+ completedAtMs,
6921
+ durationMs: completedAtMs - startedAtMs,
6922
+ listenerCount: listeners.size,
6923
+ listenerTotalMs,
6924
+ listenerMaxMs
6925
+ });
6926
+ }
6826
6927
  };
6827
6928
  /**
6828
6929
  * Work that is bound to one specific active editor and cannot survive a swap.
@@ -7412,7 +7513,7 @@ function createSuperDocUI(options) {
7412
7513
  if (options && options.success === false) return options;
7413
7514
  if (!selection.selectionTarget) return options ? op(input, options) : op(input);
7414
7515
  return op(input, {
7415
- ...options ?? {},
7516
+ ...options,
7416
7517
  offsetSpace: "selection"
7417
7518
  });
7418
7519
  };
@@ -8743,6 +8844,66 @@ function createSuperDocUI(options) {
8743
8844
  }
8744
8845
  };
8745
8846
  /**
8847
+ * Derive the browser-shell's `importedId` carrier-lookup alias from a
8848
+ * public list row's `wordRevisionIds` / `sourceIds` provenance fields,
8849
+ * mirroring `create-v2-tracked-changes-adapter.js`'s own derivation
8850
+ * (`wordRevisionIds.insert ?? .delete ?? .format ?? sourceIds.wordIdInsert
8851
+ * ?? .wordIdDelete ?? .wordIdOther[0]`). A carrier imported from a legacy
8852
+ * Word revision id is painted with THIS id, not the canonical public id —
8853
+ * `focusTrackedChange`'s carrier lookup only tries it when explicitly
8854
+ * passed as `importedId` on an object input (see `scrollTrackChangeIntoView`).
8855
+ */
8856
+ const deriveTrackedChangeImportedId = (row) => {
8857
+ if (!row || typeof row !== "object") return null;
8858
+ const record = row;
8859
+ const wordRevisionIds = record.wordRevisionIds && typeof record.wordRevisionIds === "object" ? record.wordRevisionIds : null;
8860
+ const sourceIds = record.sourceIds && typeof record.sourceIds === "object" ? record.sourceIds : null;
8861
+ const wordIdOther = Array.isArray(sourceIds?.wordIdOther) ? sourceIds.wordIdOther : void 0;
8862
+ const candidate = wordRevisionIds?.insert ?? wordRevisionIds?.delete ?? wordRevisionIds?.format ?? sourceIds?.wordIdInsert ?? sourceIds?.wordIdDelete ?? wordIdOther?.[0];
8863
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : null;
8864
+ };
8865
+ /**
8866
+ * Scroll a tracked-change target into view, falling back to the v2
8867
+ * browser-shell's `focusTrackedChange` (the same mechanism the built-in
8868
+ * comments/tracked-changes sidebar uses via `CommentDialog.vue`'s
8869
+ * `setFocus`) when the shared host geometry path can't resolve the target.
8870
+ * The host's `normalizeGeometryTarget` only understands `text`/`replacement`
8871
+ * targets (enriched with painted `segments`) and never `structural`/
8872
+ * `formatting` targets, so those always fail the primary path and need this
8873
+ * fallback.
8874
+ *
8875
+ * `story`, when present, is threaded into the fallback input as
8876
+ * `trackedChangeStory` — a bare id always resolves to the body story in
8877
+ * `focusTrackedChange`'s target resolver, which would collapse a non-body
8878
+ * (footnote/header/footer) occurrence onto its body counterpart sharing the
8879
+ * same raw id. `importedId`, when present, is threaded in too: the carrier
8880
+ * lookup (`buildTrackChangeCarrierLookupIds`) tries it as an ADDITIONAL alias
8881
+ * alongside the canonical id, for rows whose painted carrier only matches
8882
+ * through a legacy-imported Word revision id, not the canonical public id
8883
+ * (`create-v2-tracked-changes-adapter.test.js` pins this alias path as
8884
+ * necessary for some rows).
8885
+ */
8886
+ const scrollTrackChangeIntoView = async (changeId, target, story, importedId, options) => {
8887
+ const primary = await scrollTargetIntoView(target, options);
8888
+ if (primary.ok) return primary;
8889
+ const trackedChanges = getV2TrackedChanges();
8890
+ const focusTrackedChange = trackedChanges?.focusTrackedChange;
8891
+ if (typeof focusTrackedChange !== "function") return primary;
8892
+ const fallbackInput = story || importedId ? {
8893
+ commentId: changeId,
8894
+ ...story ? { trackedChangeStory: story } : {},
8895
+ ...importedId ? { importedId } : {}
8896
+ } : changeId;
8897
+ try {
8898
+ const fallbackResult = await Promise.resolve(focusTrackedChange.call(trackedChanges, fallbackInput));
8899
+ if (fallbackResult && typeof fallbackResult === "object" && fallbackResult.ok === true) return {
8900
+ success: true,
8901
+ ok: true
8902
+ };
8903
+ } catch {}
8904
+ return primary;
8905
+ };
8906
+ /**
8746
8907
  * Resolve a document entity (comment / tracked change) to its anchor target.
8747
8908
  * Prefers the already-loaded list row, falling back to a live `get({ id })`
8748
8909
  * read when the list does not carry the row.
@@ -9004,9 +9165,11 @@ function createSuperDocUI(options) {
9004
9165
  rollback();
9005
9166
  return { success: false };
9006
9167
  }
9168
+ const activeRow = trackChangesSub.get().items.find((item) => entityRowMatchesRequest(item, activeId, optimisticActiveChange?.story)) ?? trackChangesSub.get().items.find((item) => readEntityId(item) === activeId);
9169
+ const importedId = deriveTrackedChangeImportedId(activeRow);
9007
9170
  trackedChangeNavigationInFlight += 1;
9008
9171
  try {
9009
- const result = await scrollTargetIntoView(target, {
9172
+ const result = await scrollTrackChangeIntoView(activeId, target, optimisticActiveChange?.story, importedId, {
9010
9173
  behavior: "auto",
9011
9174
  shouldContinue: isCurrent
9012
9175
  });
@@ -9130,7 +9293,9 @@ function createSuperDocUI(options) {
9130
9293
  const requestedAtInvalidation = trackChangeRevealInvalidation;
9131
9294
  const loadedItems = trackChangesSub.get().items;
9132
9295
  const publicId = buildTrackedChangeIdContext(loadedItems).toPublicId(changeId) ?? changeId;
9133
- const story = readEntityRequestStory(loadedItems.find((item) => readEntityId(item) === publicId));
9296
+ const matchingRow = loadedItems.find((item) => readEntityId(item) === publicId);
9297
+ const story = readEntityRequestStory(matchingRow);
9298
+ const importedId = deriveTrackedChangeImportedId(matchingRow);
9134
9299
  const previousActiveChange = explicitActiveChange;
9135
9300
  const requestedActiveChange = {
9136
9301
  id: publicId,
@@ -9162,7 +9327,7 @@ function createSuperDocUI(options) {
9162
9327
  recompute();
9163
9328
  trackedChangeNavigationInFlight += 1;
9164
9329
  try {
9165
- const result = await scrollTargetIntoView(target, {
9330
+ const result = await scrollTrackChangeIntoView(publicId, target, story, importedId, {
9166
9331
  behavior: "auto",
9167
9332
  shouldContinue: isCurrent
9168
9333
  });