superdoc 2.20.0-next.9 → 2.20.1-next.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.
package/README.md CHANGED
@@ -28,3 +28,29 @@ See [package compatibility](https://docs.superdoc.dev/resources/package-compatib
28
28
  ## License
29
29
 
30
30
  AGPL-3.0. Commercial licenses are available from [SuperDoc](https://www.superdoc.dev).
31
+
32
+ ## Navigate to a bookmark
33
+
34
+ After the V2 browser editor is ready, navigate by an exact bookmark name:
35
+
36
+ ```js
37
+ const result = await editor.ui.bookmarks.navigateTo('Terms');
38
+ if (!result.success) console.log(result.reason);
39
+ ```
40
+
41
+ You can also pass a `BookmarkAddress` returned by `editor.activeEditor.doc.bookmarks.get()`
42
+ or `.list()`. The method reveals the bookmark's start and selects its complete range,
43
+ including across paragraphs. A collapsed bookmark places a caret. It supports editing,
44
+ suggesting, and viewing modes. Navigation stays local in a shared room and does not change
45
+ content or another client's selection. It does not take native keyboard focus.
46
+
47
+ Scrolling defaults to instant, centered navigation. Pass `{ block: 'start', behavior: 'auto' }`
48
+ as a second argument to change alignment or scrolling behavior.
49
+
50
+ The first release supports body bookmarks. Non-body and table-column bookmarks, malformed,
51
+ missing, deleted, or ambiguous targets return `{ success: false, ok: false, reason: 'target-unresolved' }`.
52
+ Name-only lookup requires a unique match across the document; an explicit body address scopes
53
+ lookup to the body. Resolution or reveal failure preserves the existing selection. Navigation
54
+ abandons stale work after content changes, document replacement, or a newer request, and times
55
+ out after 15 seconds. Missing editor or host capabilities return their readiness or availability
56
+ reason. The handle and address types are exported from `superdoc/ui`.
@@ -4219,6 +4219,14 @@ function readCollapsedHostTrackChangeId(snapshot) {
4219
4219
  const focusId = focusVisual?.trackChangeId;
4220
4220
  return typeof anchorId === "string" && anchorId.length > 0 && anchorId === focusId ? anchorId : null;
4221
4221
  }
4222
+ function isNonCollapsedHostSelection(snapshot) {
4223
+ if (!snapshot || typeof snapshot !== "object") return false;
4224
+ const record = snapshot;
4225
+ const anchor = record.anchor;
4226
+ const focus = record.focus;
4227
+ if (typeof anchor?.blockId !== "string" || !anchor.blockId || typeof focus?.blockId !== "string" || !focus.blockId || !Number.isFinite(anchor.blockOffset) || !Number.isFinite(focus.blockOffset)) return false;
4228
+ return anchor.blockId !== focus.blockId || anchor.blockOffset !== focus.blockOffset || Number.isFinite(anchor.trackedBlockOffset) && Number.isFinite(focus.trackedBlockOffset) && anchor.trackedBlockOffset !== focus.trackedBlockOffset;
4229
+ }
4222
4230
  var trackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
4223
4231
  /**
4224
4232
  * Collect the raw source-provenance ids a tracked-change list item exposes,
@@ -6250,7 +6258,7 @@ function createSuperDocUI(options) {
6250
6258
  }
6251
6259
  if (pendingPostPaintSelectionRefresh) continue;
6252
6260
  selectionEpoch += 1;
6253
- seedCaretSelectionFromHost();
6261
+ refreshSelectionFromHost();
6254
6262
  recompute(paintCompleted ? "post-paint-selection" : "post-paint-selection-failed");
6255
6263
  }
6256
6264
  };
@@ -6647,8 +6655,9 @@ function createSuperDocUI(options) {
6647
6655
  * effective font resolved from it) on the next recompute, instead of serving
6648
6656
  * the previous selection until the worker `selection.current` round-trip lands
6649
6657
  * (SD-3652). Only a resolvable COLLAPSED CARET is seeded - a range's snapshot
6650
- * cannot be resolved synchronously in worker mode, so ranges defer to the async
6651
- * read (seeding an empty value would wrongly disable range-only commands). The
6658
+ * cannot be resolved synchronously in worker mode, so a new range starts an
6659
+ * authoritative async read without waiting for the input-idle gate (SD-5394).
6660
+ * Seeding an empty value would wrongly disable range-only commands. The
6652
6661
  * authoritative async read still overwrites marks/text/review overlap, but
6653
6662
  * foreground typing defers and coalesces that worker hop through the shared
6654
6663
  * retry gate. Previous marks are carried forward for toolbar continuity. A
@@ -6656,14 +6665,19 @@ function createSuperDocUI(options) {
6656
6665
  * same carrier, or while an active typing dispatch advances the same caret by
6657
6666
  * one code point. Pending work alone never carries review command context.
6658
6667
  */
6659
- const seedCaretSelectionFromHost = (hostSelectionSnapshot) => {
6668
+ const refreshSelectionFromHost = (hostSelectionSnapshot) => {
6669
+ const foreground = foregroundMutationState();
6670
+ if (isNonCollapsedHostSelection(hostSelectionSnapshot) && !(foreground && foreground.active > 0)) {
6671
+ pendingSelectionSeedValidationToken = null;
6672
+ issueAsyncRead("selection", selectionReadToken(), selectionCurrentRun, normalizeSelectionInfo);
6673
+ return;
6674
+ }
6660
6675
  const host = getHost();
6661
6676
  const read = host?.readLiveSelectionSyncSnapshot;
6662
6677
  if (typeof read !== "function") return;
6663
6678
  const seed = normalizeSelectionInfo(safeCall(() => read.call(host), null));
6664
6679
  if (!seed || !isCollapsedCaretSnapshot(seed)) return;
6665
6680
  const token = selectionReadToken();
6666
- const foreground = foregroundMutationState();
6667
6681
  const deferAuthoritativeRead = Boolean(foreground && (foreground.active > 0 || foreground.pending > 0));
6668
6682
  if (!deferAuthoritativeRead) {
6669
6683
  pendingSelectionSeedValidationToken = null;
@@ -8895,7 +8909,7 @@ function createSuperDocUI(options) {
8895
8909
  const unsubscribe = next.subscribe((snapshot) => {
8896
8910
  if (foregroundMutationActive() && caretSelectionNeedsProjectionPaint() && schedulePostPaintSelectionRefresh()) return;
8897
8911
  selectionEpoch += 1;
8898
- seedCaretSelectionFromHost(snapshot);
8912
+ refreshSelectionFromHost(snapshot);
8899
8913
  recompute("host-selection");
8900
8914
  });
8901
8915
  if (typeof unsubscribe === "function") detachHostSelection = unsubscribe;
@@ -11274,6 +11288,113 @@ function createSuperDocUI(options) {
11274
11288
  };
11275
11289
  }
11276
11290
  };
11291
+ let bookmarkNavigationGeneration = 0;
11292
+ documentResetHooks.push(() => {
11293
+ bookmarkNavigationGeneration += 1;
11294
+ });
11295
+ const bookmarks = { navigateTo: async (input, options) => {
11296
+ const generation = ++bookmarkNavigationGeneration;
11297
+ const unresolved = () => ({
11298
+ success: false,
11299
+ ok: false,
11300
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
11301
+ });
11302
+ const address = typeof input === "string" ? {
11303
+ kind: "entity",
11304
+ entityType: "bookmark",
11305
+ name: input
11306
+ } : input;
11307
+ if (!address || address.kind !== "entity" || address.entityType !== "bookmark" || typeof address.name !== "string" || address.name.length === 0 || address.story !== void 0 && (!address.story || address.story.kind !== "story" || address.story.storyType !== "body")) return unresolved();
11308
+ const editor = getEditor();
11309
+ if (!editor || disposed) return {
11310
+ success: false,
11311
+ ok: false,
11312
+ reason: SUPERDOC_UI_REASONS.notReady
11313
+ };
11314
+ const doc = getDoc();
11315
+ const api = doc?.bookmarks;
11316
+ if (typeof api?.list !== "function" || typeof api?.get !== "function") return {
11317
+ success: false,
11318
+ ok: false,
11319
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
11320
+ };
11321
+ const helper = getSelectionApplyHelper();
11322
+ if (!("helper" in helper)) return {
11323
+ success: false,
11324
+ ok: false,
11325
+ reason: helper.reason
11326
+ };
11327
+ const host = getHost();
11328
+ if (typeof host?.scrollTargetIntoView !== "function") return {
11329
+ success: false,
11330
+ ok: false,
11331
+ reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable
11332
+ };
11333
+ const token = metadataResolutionToken();
11334
+ let active = true;
11335
+ const current = () => active && !disposed && generation === bookmarkNavigationGeneration && getEditor() === editor && getDoc() === doc && getHost() === host && metadataResolutionToken() === token;
11336
+ const run = async () => {
11337
+ try {
11338
+ const directory = await api.list.call(api, address.story ? { in: address.story } : {});
11339
+ if (!current()) return unresolved();
11340
+ if ((Array.isArray(directory?.items) ? directory.items : []).filter((item) => item?.name === address.name).length !== 1) return unresolved();
11341
+ const info = await api.get.call(api, { target: address });
11342
+ if (!current()) return unresolved();
11343
+ const from = info?.range?.from;
11344
+ const to = info?.range?.to;
11345
+ const pointValid = (point) => typeof point?.blockId === "string" && point.blockId.length > 0 && Number.isInteger(point.offset) && point.offset >= 0;
11346
+ if (info?.name !== address.name || info?.address?.story && info.address.story.storyType !== "body" || info?.tableColumn || !pointValid(from) || !pointValid(to) || from.blockId === to.blockId && from.offset > to.offset) return unresolved();
11347
+ const result = await scrollTargetIntoView({
11348
+ kind: "text",
11349
+ segments: [{
11350
+ blockId: from.blockId,
11351
+ range: {
11352
+ start: from.offset,
11353
+ end: from.offset
11354
+ }
11355
+ }]
11356
+ }, {
11357
+ block: options?.block ?? "center",
11358
+ behavior: options?.behavior ?? "instant",
11359
+ shouldContinue: current
11360
+ });
11361
+ if (!current()) return unresolved();
11362
+ if (!result.ok) return result;
11363
+ const applied = selection.apply({
11364
+ kind: "selection",
11365
+ start: {
11366
+ kind: "text",
11367
+ blockId: from.blockId,
11368
+ offset: from.offset
11369
+ },
11370
+ end: {
11371
+ kind: "text",
11372
+ blockId: to.blockId,
11373
+ offset: to.offset
11374
+ }
11375
+ });
11376
+ return {
11377
+ ...applied,
11378
+ success: applied.ok
11379
+ };
11380
+ } catch {
11381
+ return unresolved();
11382
+ }
11383
+ };
11384
+ let timer;
11385
+ const timeout = new Promise((resolve) => {
11386
+ timer = setTimeout(() => {
11387
+ active = false;
11388
+ resolve(unresolved());
11389
+ }, 15e3);
11390
+ });
11391
+ try {
11392
+ return await Promise.race([run(), timeout]);
11393
+ } finally {
11394
+ active = false;
11395
+ clearTimeout(timer);
11396
+ }
11397
+ } };
11277
11398
  /**
11278
11399
  * Derive the browser-shell's `importedId` carrier-lookup alias from a
11279
11400
  * public list row's `wordRevisionIds` / `sourceIds` provenance fields,
@@ -12507,6 +12628,7 @@ function createSuperDocUI(options) {
12507
12628
  };
12508
12629
  let searchRequestGeneration = 0;
12509
12630
  let pendingFallbackGeneration = null;
12631
+ let completedFallbackSearch = null;
12510
12632
  /**
12511
12633
  * How to tear down the session that is currently open, captured when it was
12512
12634
  * opened.
@@ -12555,6 +12677,7 @@ function createSuperDocUI(options) {
12555
12677
  * is deliberately not made here.
12556
12678
  */
12557
12679
  const resetSearchForActiveEditorChange = () => {
12680
+ completedFallbackSearch = null;
12558
12681
  searchRequestGeneration += 1;
12559
12682
  pendingFallbackGeneration = null;
12560
12683
  shellFallbackOutstanding = false;
@@ -12608,8 +12731,8 @@ function createSuperDocUI(options) {
12608
12731
  const editSearch = getEditCommandSearch();
12609
12732
  if (editSearch && typeof editSearch.query === "function") safeCall(() => editSearch.query({ query: "" }), null);
12610
12733
  };
12611
- const readEditCommandCanReplace = () => readEditCommandStateEntry("find.replace")?.enabled === true;
12612
- const readEditCommandCanReplaceAll = () => readEditCommandStateEntry("find.replaceAll")?.enabled === true;
12734
+ const readEditCommandCanReplace = () => pendingFallbackGeneration !== searchRequestGeneration && readEditCommandStateEntry("find.replace")?.enabled === true;
12735
+ const readEditCommandCanReplaceAll = () => pendingFallbackGeneration !== searchRequestGeneration && readEditCommandStateEntry("find.replaceAll")?.enabled === true;
12613
12736
  const applyHostSearchResult = (result, fallbackCanReplace = false, fallbackCanReplaceAll = fallbackCanReplace) => {
12614
12737
  const snapshot = normalizeHostSearchResult(result, fallbackCanReplace, fallbackCanReplaceAll);
12615
12738
  if (!snapshot) {
@@ -12770,6 +12893,7 @@ function createSuperDocUI(options) {
12770
12893
  return { ok: true };
12771
12894
  },
12772
12895
  close: () => {
12896
+ completedFallbackSearch = null;
12773
12897
  searchRequestGeneration += 1;
12774
12898
  cancelRefreshShellSearch();
12775
12899
  if (releaseActiveSearchSession) releaseSearchSession();
@@ -12794,6 +12918,16 @@ function createSuperDocUI(options) {
12794
12918
  const caseSensitive = Boolean(options?.caseSensitive);
12795
12919
  const includeDeletedText = (options?.includeTrackedDeletions ?? options?.includeDeletedText) === true;
12796
12920
  const regex = options?.regex === true;
12921
+ if ((!host || typeof host.setSession !== "function") && completedFallbackSearch !== null && completedFallbackSearch === getEditCommandSearch() && pendingFallbackGeneration !== searchRequestGeneration && searchRefreshTimer === null && searchState.open && searchState.available && !searchState.reason && searchState.query === query && searchState.caseSensitive === caseSensitive && searchState.includeDeletedText === includeDeletedText && searchState.regex === regex) {
12922
+ syncSearchStateFromHost();
12923
+ emitSearch();
12924
+ return searchState;
12925
+ }
12926
+ completedFallbackSearch = null;
12927
+ if (searchRefreshTimer !== null) {
12928
+ clearTimeout(searchRefreshTimer);
12929
+ searchRefreshTimer = null;
12930
+ }
12797
12931
  const generation = ++searchRequestGeneration;
12798
12932
  cancelRefreshShellSearch();
12799
12933
  cancelOutstandingShellFallback();
@@ -12829,6 +12963,7 @@ function createSuperDocUI(options) {
12829
12963
  if (generation !== searchRequestGeneration) return;
12830
12964
  shellFallbackOutstanding = false;
12831
12965
  pendingFallbackGeneration = null;
12966
+ completedFallbackSearch = editSearch;
12832
12967
  applyHostSearchResult(resolved, readEditCommandCanReplace(), readEditCommandCanReplaceAll());
12833
12968
  }, () => {
12834
12969
  if (generation !== searchRequestGeneration) return;
@@ -12841,6 +12976,7 @@ function createSuperDocUI(options) {
12841
12976
  return searchState;
12842
12977
  }
12843
12978
  pendingFallbackGeneration = null;
12979
+ completedFallbackSearch = editSearch;
12844
12980
  applyHostSearchResult(result, readEditCommandCanReplace(), readEditCommandCanReplaceAll());
12845
12981
  applyHostSearchResult(safeCall(() => editSearch.getState(), null), readEditCommandCanReplace(), readEditCommandCanReplaceAll());
12846
12982
  return searchState;
@@ -12983,6 +13119,7 @@ function createSuperDocUI(options) {
12983
13119
  return { ok: true };
12984
13120
  },
12985
13121
  clear: () => {
13122
+ completedFallbackSearch = null;
12986
13123
  searchRequestGeneration += 1;
12987
13124
  cancelRefreshShellSearch();
12988
13125
  cancelOutstandingShellFallback();
@@ -13098,6 +13235,7 @@ function createSuperDocUI(options) {
13098
13235
  };
13099
13236
  let searchRefreshTimer = null;
13100
13237
  scheduleActiveSearchRefresh = () => {
13238
+ completedFallbackSearch = null;
13101
13239
  if (searchRefreshTimer !== null) clearTimeout(searchRefreshTimer);
13102
13240
  if (!searchState.open || !searchState.query) return;
13103
13241
  const scheduledGeneration = searchRequestGeneration;
@@ -13124,19 +13262,29 @@ function createSuperDocUI(options) {
13124
13262
  const result = typeof editSearch?.refresh === "function" ? safeCall(() => editSearch.refresh(input), null) : safeCall(() => host.refresh(), null);
13125
13263
  if (isPromiseLike(result)) {
13126
13264
  pendingFallbackGeneration = generation;
13265
+ setSearchState({
13266
+ canReplace: false,
13267
+ canReplaceAll: false
13268
+ });
13127
13269
  Promise.resolve(result).then((resolved) => {
13128
13270
  if (generation !== searchRequestGeneration || disposed) return;
13271
+ releaseRefreshShellSearch = null;
13129
13272
  pendingFallbackGeneration = null;
13273
+ completedFallbackSearch = editSearch;
13130
13274
  applyHostSearchResult(resolved, readEditCommandCanReplace(), readEditCommandCanReplaceAll());
13131
13275
  }, () => {
13132
13276
  if (generation !== searchRequestGeneration || disposed) return;
13133
- pendingFallbackGeneration = null;
13277
+ releaseRefreshShellSearch = null;
13134
13278
  setSearchState({
13135
13279
  available: false,
13136
13280
  reason: SUPERDOC_UI_REASONS.searchUnavailable
13137
13281
  });
13138
13282
  });
13139
- } else if (result !== null) applyHostSearchResult(result);
13283
+ } else if (result !== null) {
13284
+ releaseRefreshShellSearch = null;
13285
+ completedFallbackSearch = editSearch;
13286
+ applyHostSearchResult(result);
13287
+ }
13140
13288
  }, 120);
13141
13289
  };
13142
13290
  const stylesSub = sliceHandle((s) => s.styles);
@@ -13186,6 +13334,7 @@ function createSuperDocUI(options) {
13186
13334
  watermark.destroy();
13187
13335
  clearContentControlHighlight();
13188
13336
  disposed = true;
13337
+ completedFallbackSearch = null;
13189
13338
  if (searchRefreshTimer !== null) clearTimeout(searchRefreshTimer);
13190
13339
  searchRefreshTimer = null;
13191
13340
  cancelRefreshShellSearch();
@@ -13261,6 +13410,7 @@ function createSuperDocUI(options) {
13261
13410
  comments,
13262
13411
  trackChanges,
13263
13412
  contentControls,
13413
+ bookmarks,
13264
13414
  fonts,
13265
13415
  zoom,
13266
13416
  document: documentHandle,
@@ -4220,6 +4220,14 @@ function readCollapsedHostTrackChangeId(snapshot) {
4220
4220
  const focusId = focusVisual?.trackChangeId;
4221
4221
  return typeof anchorId === "string" && anchorId.length > 0 && anchorId === focusId ? anchorId : null;
4222
4222
  }
4223
+ function isNonCollapsedHostSelection(snapshot) {
4224
+ if (!snapshot || typeof snapshot !== "object") return false;
4225
+ const record = snapshot;
4226
+ const anchor = record.anchor;
4227
+ const focus = record.focus;
4228
+ if (typeof anchor?.blockId !== "string" || !anchor.blockId || typeof focus?.blockId !== "string" || !focus.blockId || !Number.isFinite(anchor.blockOffset) || !Number.isFinite(focus.blockOffset)) return false;
4229
+ return anchor.blockId !== focus.blockId || anchor.blockOffset !== focus.blockOffset || Number.isFinite(anchor.trackedBlockOffset) && Number.isFinite(focus.trackedBlockOffset) && anchor.trackedBlockOffset !== focus.trackedBlockOffset;
4230
+ }
4223
4231
  var trackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
4224
4232
  /**
4225
4233
  * Collect the raw source-provenance ids a tracked-change list item exposes,
@@ -6251,7 +6259,7 @@ function createSuperDocUI(options) {
6251
6259
  }
6252
6260
  if (pendingPostPaintSelectionRefresh) continue;
6253
6261
  selectionEpoch += 1;
6254
- seedCaretSelectionFromHost();
6262
+ refreshSelectionFromHost();
6255
6263
  recompute(paintCompleted ? "post-paint-selection" : "post-paint-selection-failed");
6256
6264
  }
6257
6265
  };
@@ -6648,8 +6656,9 @@ function createSuperDocUI(options) {
6648
6656
  * effective font resolved from it) on the next recompute, instead of serving
6649
6657
  * the previous selection until the worker `selection.current` round-trip lands
6650
6658
  * (SD-3652). Only a resolvable COLLAPSED CARET is seeded - a range's snapshot
6651
- * cannot be resolved synchronously in worker mode, so ranges defer to the async
6652
- * read (seeding an empty value would wrongly disable range-only commands). The
6659
+ * cannot be resolved synchronously in worker mode, so a new range starts an
6660
+ * authoritative async read without waiting for the input-idle gate (SD-5394).
6661
+ * Seeding an empty value would wrongly disable range-only commands. The
6653
6662
  * authoritative async read still overwrites marks/text/review overlap, but
6654
6663
  * foreground typing defers and coalesces that worker hop through the shared
6655
6664
  * retry gate. Previous marks are carried forward for toolbar continuity. A
@@ -6657,14 +6666,19 @@ function createSuperDocUI(options) {
6657
6666
  * same carrier, or while an active typing dispatch advances the same caret by
6658
6667
  * one code point. Pending work alone never carries review command context.
6659
6668
  */
6660
- const seedCaretSelectionFromHost = (hostSelectionSnapshot) => {
6669
+ const refreshSelectionFromHost = (hostSelectionSnapshot) => {
6670
+ const foreground = foregroundMutationState();
6671
+ if (isNonCollapsedHostSelection(hostSelectionSnapshot) && !(foreground && foreground.active > 0)) {
6672
+ pendingSelectionSeedValidationToken = null;
6673
+ issueAsyncRead("selection", selectionReadToken(), selectionCurrentRun, normalizeSelectionInfo);
6674
+ return;
6675
+ }
6661
6676
  const host = getHost();
6662
6677
  const read = host?.readLiveSelectionSyncSnapshot;
6663
6678
  if (typeof read !== "function") return;
6664
6679
  const seed = normalizeSelectionInfo(safeCall(() => read.call(host), null));
6665
6680
  if (!seed || !isCollapsedCaretSnapshot(seed)) return;
6666
6681
  const token = selectionReadToken();
6667
- const foreground = foregroundMutationState();
6668
6682
  const deferAuthoritativeRead = Boolean(foreground && (foreground.active > 0 || foreground.pending > 0));
6669
6683
  if (!deferAuthoritativeRead) {
6670
6684
  pendingSelectionSeedValidationToken = null;
@@ -8896,7 +8910,7 @@ function createSuperDocUI(options) {
8896
8910
  const unsubscribe = next.subscribe((snapshot) => {
8897
8911
  if (foregroundMutationActive() && caretSelectionNeedsProjectionPaint() && schedulePostPaintSelectionRefresh()) return;
8898
8912
  selectionEpoch += 1;
8899
- seedCaretSelectionFromHost(snapshot);
8913
+ refreshSelectionFromHost(snapshot);
8900
8914
  recompute("host-selection");
8901
8915
  });
8902
8916
  if (typeof unsubscribe === "function") detachHostSelection = unsubscribe;
@@ -11275,6 +11289,113 @@ function createSuperDocUI(options) {
11275
11289
  };
11276
11290
  }
11277
11291
  };
11292
+ let bookmarkNavigationGeneration = 0;
11293
+ documentResetHooks.push(() => {
11294
+ bookmarkNavigationGeneration += 1;
11295
+ });
11296
+ const bookmarks = { navigateTo: async (input, options) => {
11297
+ const generation = ++bookmarkNavigationGeneration;
11298
+ const unresolved = () => ({
11299
+ success: false,
11300
+ ok: false,
11301
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
11302
+ });
11303
+ const address = typeof input === "string" ? {
11304
+ kind: "entity",
11305
+ entityType: "bookmark",
11306
+ name: input
11307
+ } : input;
11308
+ if (!address || address.kind !== "entity" || address.entityType !== "bookmark" || typeof address.name !== "string" || address.name.length === 0 || address.story !== void 0 && (!address.story || address.story.kind !== "story" || address.story.storyType !== "body")) return unresolved();
11309
+ const editor = getEditor();
11310
+ if (!editor || disposed) return {
11311
+ success: false,
11312
+ ok: false,
11313
+ reason: SUPERDOC_UI_REASONS.notReady
11314
+ };
11315
+ const doc = getDoc();
11316
+ const api = doc?.bookmarks;
11317
+ if (typeof api?.list !== "function" || typeof api?.get !== "function") return {
11318
+ success: false,
11319
+ ok: false,
11320
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
11321
+ };
11322
+ const helper = getSelectionApplyHelper();
11323
+ if (!("helper" in helper)) return {
11324
+ success: false,
11325
+ ok: false,
11326
+ reason: helper.reason
11327
+ };
11328
+ const host = getHost();
11329
+ if (typeof host?.scrollTargetIntoView !== "function") return {
11330
+ success: false,
11331
+ ok: false,
11332
+ reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable
11333
+ };
11334
+ const token = metadataResolutionToken();
11335
+ let active = true;
11336
+ const current = () => active && !disposed && generation === bookmarkNavigationGeneration && getEditor() === editor && getDoc() === doc && getHost() === host && metadataResolutionToken() === token;
11337
+ const run = async () => {
11338
+ try {
11339
+ const directory = await api.list.call(api, address.story ? { in: address.story } : {});
11340
+ if (!current()) return unresolved();
11341
+ if ((Array.isArray(directory?.items) ? directory.items : []).filter((item) => item?.name === address.name).length !== 1) return unresolved();
11342
+ const info = await api.get.call(api, { target: address });
11343
+ if (!current()) return unresolved();
11344
+ const from = info?.range?.from;
11345
+ const to = info?.range?.to;
11346
+ const pointValid = (point) => typeof point?.blockId === "string" && point.blockId.length > 0 && Number.isInteger(point.offset) && point.offset >= 0;
11347
+ if (info?.name !== address.name || info?.address?.story && info.address.story.storyType !== "body" || info?.tableColumn || !pointValid(from) || !pointValid(to) || from.blockId === to.blockId && from.offset > to.offset) return unresolved();
11348
+ const result = await scrollTargetIntoView({
11349
+ kind: "text",
11350
+ segments: [{
11351
+ blockId: from.blockId,
11352
+ range: {
11353
+ start: from.offset,
11354
+ end: from.offset
11355
+ }
11356
+ }]
11357
+ }, {
11358
+ block: options?.block ?? "center",
11359
+ behavior: options?.behavior ?? "instant",
11360
+ shouldContinue: current
11361
+ });
11362
+ if (!current()) return unresolved();
11363
+ if (!result.ok) return result;
11364
+ const applied = selection.apply({
11365
+ kind: "selection",
11366
+ start: {
11367
+ kind: "text",
11368
+ blockId: from.blockId,
11369
+ offset: from.offset
11370
+ },
11371
+ end: {
11372
+ kind: "text",
11373
+ blockId: to.blockId,
11374
+ offset: to.offset
11375
+ }
11376
+ });
11377
+ return {
11378
+ ...applied,
11379
+ success: applied.ok
11380
+ };
11381
+ } catch {
11382
+ return unresolved();
11383
+ }
11384
+ };
11385
+ let timer;
11386
+ const timeout = new Promise((resolve) => {
11387
+ timer = setTimeout(() => {
11388
+ active = false;
11389
+ resolve(unresolved());
11390
+ }, 15e3);
11391
+ });
11392
+ try {
11393
+ return await Promise.race([run(), timeout]);
11394
+ } finally {
11395
+ active = false;
11396
+ clearTimeout(timer);
11397
+ }
11398
+ } };
11278
11399
  /**
11279
11400
  * Derive the browser-shell's `importedId` carrier-lookup alias from a
11280
11401
  * public list row's `wordRevisionIds` / `sourceIds` provenance fields,
@@ -12508,6 +12629,7 @@ function createSuperDocUI(options) {
12508
12629
  };
12509
12630
  let searchRequestGeneration = 0;
12510
12631
  let pendingFallbackGeneration = null;
12632
+ let completedFallbackSearch = null;
12511
12633
  /**
12512
12634
  * How to tear down the session that is currently open, captured when it was
12513
12635
  * opened.
@@ -12556,6 +12678,7 @@ function createSuperDocUI(options) {
12556
12678
  * is deliberately not made here.
12557
12679
  */
12558
12680
  const resetSearchForActiveEditorChange = () => {
12681
+ completedFallbackSearch = null;
12559
12682
  searchRequestGeneration += 1;
12560
12683
  pendingFallbackGeneration = null;
12561
12684
  shellFallbackOutstanding = false;
@@ -12609,8 +12732,8 @@ function createSuperDocUI(options) {
12609
12732
  const editSearch = getEditCommandSearch();
12610
12733
  if (editSearch && typeof editSearch.query === "function") safeCall(() => editSearch.query({ query: "" }), null);
12611
12734
  };
12612
- const readEditCommandCanReplace = () => readEditCommandStateEntry("find.replace")?.enabled === true;
12613
- const readEditCommandCanReplaceAll = () => readEditCommandStateEntry("find.replaceAll")?.enabled === true;
12735
+ const readEditCommandCanReplace = () => pendingFallbackGeneration !== searchRequestGeneration && readEditCommandStateEntry("find.replace")?.enabled === true;
12736
+ const readEditCommandCanReplaceAll = () => pendingFallbackGeneration !== searchRequestGeneration && readEditCommandStateEntry("find.replaceAll")?.enabled === true;
12614
12737
  const applyHostSearchResult = (result, fallbackCanReplace = false, fallbackCanReplaceAll = fallbackCanReplace) => {
12615
12738
  const snapshot = normalizeHostSearchResult(result, fallbackCanReplace, fallbackCanReplaceAll);
12616
12739
  if (!snapshot) {
@@ -12771,6 +12894,7 @@ function createSuperDocUI(options) {
12771
12894
  return { ok: true };
12772
12895
  },
12773
12896
  close: () => {
12897
+ completedFallbackSearch = null;
12774
12898
  searchRequestGeneration += 1;
12775
12899
  cancelRefreshShellSearch();
12776
12900
  if (releaseActiveSearchSession) releaseSearchSession();
@@ -12795,6 +12919,16 @@ function createSuperDocUI(options) {
12795
12919
  const caseSensitive = Boolean(options?.caseSensitive);
12796
12920
  const includeDeletedText = (options?.includeTrackedDeletions ?? options?.includeDeletedText) === true;
12797
12921
  const regex = options?.regex === true;
12922
+ if ((!host || typeof host.setSession !== "function") && completedFallbackSearch !== null && completedFallbackSearch === getEditCommandSearch() && pendingFallbackGeneration !== searchRequestGeneration && searchRefreshTimer === null && searchState.open && searchState.available && !searchState.reason && searchState.query === query && searchState.caseSensitive === caseSensitive && searchState.includeDeletedText === includeDeletedText && searchState.regex === regex) {
12923
+ syncSearchStateFromHost();
12924
+ emitSearch();
12925
+ return searchState;
12926
+ }
12927
+ completedFallbackSearch = null;
12928
+ if (searchRefreshTimer !== null) {
12929
+ clearTimeout(searchRefreshTimer);
12930
+ searchRefreshTimer = null;
12931
+ }
12798
12932
  const generation = ++searchRequestGeneration;
12799
12933
  cancelRefreshShellSearch();
12800
12934
  cancelOutstandingShellFallback();
@@ -12830,6 +12964,7 @@ function createSuperDocUI(options) {
12830
12964
  if (generation !== searchRequestGeneration) return;
12831
12965
  shellFallbackOutstanding = false;
12832
12966
  pendingFallbackGeneration = null;
12967
+ completedFallbackSearch = editSearch;
12833
12968
  applyHostSearchResult(resolved, readEditCommandCanReplace(), readEditCommandCanReplaceAll());
12834
12969
  }, () => {
12835
12970
  if (generation !== searchRequestGeneration) return;
@@ -12842,6 +12977,7 @@ function createSuperDocUI(options) {
12842
12977
  return searchState;
12843
12978
  }
12844
12979
  pendingFallbackGeneration = null;
12980
+ completedFallbackSearch = editSearch;
12845
12981
  applyHostSearchResult(result, readEditCommandCanReplace(), readEditCommandCanReplaceAll());
12846
12982
  applyHostSearchResult(safeCall(() => editSearch.getState(), null), readEditCommandCanReplace(), readEditCommandCanReplaceAll());
12847
12983
  return searchState;
@@ -12984,6 +13120,7 @@ function createSuperDocUI(options) {
12984
13120
  return { ok: true };
12985
13121
  },
12986
13122
  clear: () => {
13123
+ completedFallbackSearch = null;
12987
13124
  searchRequestGeneration += 1;
12988
13125
  cancelRefreshShellSearch();
12989
13126
  cancelOutstandingShellFallback();
@@ -13099,6 +13236,7 @@ function createSuperDocUI(options) {
13099
13236
  };
13100
13237
  let searchRefreshTimer = null;
13101
13238
  scheduleActiveSearchRefresh = () => {
13239
+ completedFallbackSearch = null;
13102
13240
  if (searchRefreshTimer !== null) clearTimeout(searchRefreshTimer);
13103
13241
  if (!searchState.open || !searchState.query) return;
13104
13242
  const scheduledGeneration = searchRequestGeneration;
@@ -13125,19 +13263,29 @@ function createSuperDocUI(options) {
13125
13263
  const result = typeof editSearch?.refresh === "function" ? safeCall(() => editSearch.refresh(input), null) : safeCall(() => host.refresh(), null);
13126
13264
  if (isPromiseLike(result)) {
13127
13265
  pendingFallbackGeneration = generation;
13266
+ setSearchState({
13267
+ canReplace: false,
13268
+ canReplaceAll: false
13269
+ });
13128
13270
  Promise.resolve(result).then((resolved) => {
13129
13271
  if (generation !== searchRequestGeneration || disposed) return;
13272
+ releaseRefreshShellSearch = null;
13130
13273
  pendingFallbackGeneration = null;
13274
+ completedFallbackSearch = editSearch;
13131
13275
  applyHostSearchResult(resolved, readEditCommandCanReplace(), readEditCommandCanReplaceAll());
13132
13276
  }, () => {
13133
13277
  if (generation !== searchRequestGeneration || disposed) return;
13134
- pendingFallbackGeneration = null;
13278
+ releaseRefreshShellSearch = null;
13135
13279
  setSearchState({
13136
13280
  available: false,
13137
13281
  reason: SUPERDOC_UI_REASONS.searchUnavailable
13138
13282
  });
13139
13283
  });
13140
- } else if (result !== null) applyHostSearchResult(result);
13284
+ } else if (result !== null) {
13285
+ releaseRefreshShellSearch = null;
13286
+ completedFallbackSearch = editSearch;
13287
+ applyHostSearchResult(result);
13288
+ }
13141
13289
  }, 120);
13142
13290
  };
13143
13291
  const stylesSub = sliceHandle((s) => s.styles);
@@ -13187,6 +13335,7 @@ function createSuperDocUI(options) {
13187
13335
  watermark.destroy();
13188
13336
  clearContentControlHighlight();
13189
13337
  disposed = true;
13338
+ completedFallbackSearch = null;
13190
13339
  if (searchRefreshTimer !== null) clearTimeout(searchRefreshTimer);
13191
13340
  searchRefreshTimer = null;
13192
13341
  cancelRefreshShellSearch();
@@ -13262,6 +13411,7 @@ function createSuperDocUI(options) {
13262
13411
  comments,
13263
13412
  trackChanges,
13264
13413
  contentControls,
13414
+ bookmarks,
13265
13415
  fonts,
13266
13416
  zoom,
13267
13417
  document: documentHandle,
@@ -19,7 +19,7 @@ var COLLABORATION_UPGRADE_ENGINE_MINIMUM_NODE_MAJOR = 20;
19
19
  var PRIVATE_ENGINE_INFO = (0, _superdoc_docx_engine_collaboration_upgrade_engine.getCollaborationUpgradeEngineInfo)();
20
20
  var ENGINE_INFO = Object.freeze({
21
21
  ...PRIVATE_ENGINE_INFO,
22
- superdocVersion: "2.20.0-next.9",
22
+ superdocVersion: "2.20.1-next.1",
23
23
  roomSchemaVersion: Object.freeze({ ...PRIVATE_ENGINE_INFO.roomSchemaVersion }),
24
24
  supportedBundleVersions: SUPPORTED_COLLABORATION_UPGRADE_BUNDLE_VERSIONS,
25
25
  supportedV1ReaderContractVersions: SUPPORTED_V1_READER_CONTRACT_VERSIONS
@@ -18,7 +18,7 @@ var COLLABORATION_UPGRADE_ENGINE_MINIMUM_NODE_MAJOR = 20;
18
18
  var PRIVATE_ENGINE_INFO = getCollaborationUpgradeEngineInfo$1();
19
19
  var ENGINE_INFO = Object.freeze({
20
20
  ...PRIVATE_ENGINE_INFO,
21
- superdocVersion: "2.20.0-next.9",
21
+ superdocVersion: "2.20.1-next.1",
22
22
  roomSchemaVersion: Object.freeze({ ...PRIVATE_ENGINE_INFO.roomSchemaVersion }),
23
23
  supportedBundleVersions: SUPPORTED_COLLABORATION_UPGRADE_BUNDLE_VERSIONS,
24
24
  supportedV1ReaderContractVersions: SUPPORTED_V1_READER_CONTRACT_VERSIONS
@@ -3649,7 +3649,7 @@ export declare const OPERATION_DEFINITIONS: {
3649
3649
  readonly find: {
3650
3650
  readonly memberPath: "find";
3651
3651
  readonly description: "Search the document for text or node matches using SDM/1 selectors. Returns discovery-grade results: for mutation targeting, use query.match instead.";
3652
- readonly expectedResult: "Returns an SDFindResult envelope ({ total, limit, offset, items }). Each item is an SDNodeResult ({ node, address }).";
3652
+ readonly expectedResult: "Returns an SDFindResult envelope ({ total, limit, offset, items }). Each item is an SDNodeResult ({ node, address }). Text hits contain only the matched text; use getNode or getNodeById for the full containing node. Node selectors return full node projections.";
3653
3653
  readonly requiresDocumentContext: true;
3654
3654
  readonly metadata: CommandStaticMetadata;
3655
3655
  readonly referenceDocPath: "find.mdx";