superdoc 2.4.0-next.10 → 2.4.0-next.12

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 (42) hide show
  1. package/dist/chunks/{FindReplaceSurface-9VkPw_-X.cjs → FindReplaceSurface-0p7EtMZh.cjs} +4 -3
  2. package/dist/chunks/{FindReplaceSurface-BuvaIrQ5.es.js → FindReplaceSurface-40AnrqWk.es.js} +4 -2
  3. package/dist/chunks/{PasswordPromptSurface-Bu5xHm91.cjs → PasswordPromptSurface-31lYPcCK.cjs} +4 -3
  4. package/dist/chunks/{PasswordPromptSurface-Bz9_sKhX.es.js → PasswordPromptSurface-CwYeO3iQ.es.js} +4 -2
  5. package/dist/chunks/{PdfViewer-CdVGB6Fe.es.js → PdfViewer-D8h7LRZy.es.js} +103 -19
  6. package/dist/chunks/{PdfViewer-D4eD2dV1.cjs → PdfViewer-Dug6yDRC.cjs} +105 -20
  7. package/dist/chunks/_plugin-vue_export-helper-BOaGB7Aw.es.js +8 -0
  8. package/dist/chunks/_plugin-vue_export-helper-SDR04tiH.cjs +13 -0
  9. package/dist/chunks/{blank-docx-DP8RUPW-.cjs → blank-docx-BuFAbRjs.cjs} +2 -0
  10. package/dist/chunks/{blank-docx-XRX6Ker2.es.js → blank-docx-DzQccOlW.es.js} +2 -0
  11. package/dist/chunks/constants-B6VBlmKp.es.js +4 -0
  12. package/dist/chunks/constants-DpXuDx_g.cjs +9 -0
  13. package/dist/chunks/{create-super-doc-ui-ByvmsAAG.cjs → create-super-doc-ui-BWYOinLD.cjs} +1506 -455
  14. package/dist/chunks/{create-super-doc-ui-DBXOhHdW.es.js → create-super-doc-ui-D0mNmAHJ.es.js} +1695 -416
  15. package/dist/chunks/{eventemitter3-DqY4aSMf.cjs → eventemitter3-C_TAnXOl.cjs} +125 -16
  16. package/dist/chunks/{eventemitter3-Bt2s0X0a.es.js → eventemitter3-DEIiXiH2.es.js} +124 -16
  17. package/dist/chunks/jszip-BzJ3CyxR.es.js +4717 -0
  18. package/dist/chunks/jszip-D8mAFF-r.cjs +4758 -0
  19. package/dist/chunks/{rolldown-runtime-D7PMmH3s.es.js → rolldown-runtime-0pSA04fp.es.js} +12 -3
  20. package/dist/chunks/{rolldown-runtime-1Y-nnZJ3.cjs → rolldown-runtime-74VwNCz7.cjs} +11 -2
  21. package/dist/chunks/{uuid-CFp0WGVU.cjs → uuid-BhG0ngwk.cjs} +10 -6
  22. package/dist/chunks/{uuid-B2Sqk-3p.es.js → uuid-H0Xcmmhy.es.js} +8 -3
  23. package/dist/collaboration-upgrade-engine.cjs +34 -9
  24. package/dist/collaboration-upgrade-engine.es.js +30 -5
  25. package/dist/public/ui-react.cjs +66 -2
  26. package/dist/public/ui-react.es.js +66 -1
  27. package/dist/public/ui.cjs +1 -1
  28. package/dist/public/ui.es.js +1 -1
  29. package/dist/style.css +35 -0
  30. package/dist/style.layered.css +35 -0
  31. package/dist/superdoc.cjs +5774 -1147
  32. package/dist/superdoc.es.js +5649 -1021
  33. package/dist-cdn/style.layered.css +1 -1
  34. package/dist-cdn/superdoc.min.css +1 -1
  35. package/dist-cdn/superdoc.min.js +36 -36
  36. package/package.json +14 -14
  37. package/dist/chunks/_plugin-vue_export-helper-CZ1Nl59K.cjs +0 -11
  38. package/dist/chunks/_plugin-vue_export-helper-DSMAhhwD.es.js +0 -6
  39. package/dist/chunks/constants-BfH0br5u.es.js +0 -3
  40. package/dist/chunks/constants-D7TNtyJd.cjs +0 -14
  41. package/dist/chunks/jszip-C8srOKAO.es.js +0 -4650
  42. package/dist/chunks/jszip-Cs9JBLlJ.cjs +0 -4691
@@ -1,44 +1,128 @@
1
- const DOM_CLASS_NAMES = {
1
+ //#region ../layout-engine/dom-contract/src/class-names.ts
2
+ /**
3
+ * DOM Contract: Class Names
4
+ *
5
+ * CSS class names stamped on rendered document elements by the DOM painter.
6
+ * These names form a public contract read by the painter (emitter) and by
7
+ * editor-side DOM observation code (reader).
8
+ *
9
+ * Changing a value here is a breaking change for both systems.
10
+ */
11
+ var DOM_CLASS_NAMES = {
12
+ /** Top-level page container element. */
2
13
  PAGE: "superdoc-page",
14
+ /** Fragment container (paragraph, table, image block, etc.). */
3
15
  FRAGMENT: "superdoc-fragment",
16
+ /** Line container within a fragment. */
4
17
  LINE: "superdoc-line",
18
+ /**
19
+ * Inline structured-content (SDT) wrapper.
20
+ *
21
+ * Carries `data-pm-start` / `data-pm-end` for selection highlighting.
22
+ * Should be EXCLUDED from click-to-position mapping — child spans are
23
+ * the character-level targets.
24
+ */
5
25
  INLINE_SDT_WRAPPER: "superdoc-structured-content-inline",
26
+ /** Inline structured-content label chrome. */
6
27
  INLINE_SDT_LABEL: "superdoc-structured-content-inline__label",
28
+ /** Block-level structured-content container. */
7
29
  BLOCK_SDT: "superdoc-structured-content-block",
30
+ /** Block-level structured-content label chrome. */
8
31
  BLOCK_SDT_LABEL: "superdoc-structured-content__label",
32
+ /** Table fragment container (resize overlay and click-mapping target). */
9
33
  TABLE_FRAGMENT: "superdoc-table-fragment",
34
+ /** Document section container. */
10
35
  DOCUMENT_SECTION: "superdoc-document-section",
36
+ /**
37
+ * Grouped hover highlight applied to all fragments of the same block SDT.
38
+ * Set by document-runtime hover coordination via event delegation.
39
+ */
11
40
  SDT_GROUP_HOVER: "sdt-group-hover",
41
+ /** Paragraph fragment rendered as a Table of Contents entry. */
12
42
  TOC_ENTRY: "superdoc-toc-entry",
43
+ /** TOC analogue of `SDT_GROUP_HOVER`, applied to every fragment sharing a `data-toc-id`. */
13
44
  TOC_GROUP_HOVER: "toc-group-hover",
45
+ /** Block-level image fragment (ImageBlock). */
14
46
  IMAGE_FRAGMENT: "superdoc-image-fragment",
47
+ /** Inline image element (ImageRun inside a paragraph). */
15
48
  INLINE_IMAGE: "superdoc-inline-image",
49
+ /** Wrapper around a paragraph's list marker (bullet glyph or ordered number). */
16
50
  LIST_MARKER: "superdoc-list-marker",
51
+ /** Clip wrapper around a cropped inline image. */
17
52
  INLINE_IMAGE_CLIP_WRAPPER: "superdoc-inline-image-clip-wrapper",
53
+ /** Field annotation outer wrapper. */
18
54
  ANNOTATION: "annotation",
55
+ /** Field annotation inner content wrapper. */
19
56
  ANNOTATION_CONTENT: "annotation-content",
57
+ /** Hidden caret anchor span appended after field annotation content. */
20
58
  ANNOTATION_CARET_ANCHOR: "annotation-caret-anchor"
21
59
  };
22
- const STRUCTURED_CONTENT_CHROME_LABEL_CLASS_NAMES = [DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL];
23
- const DATA_ATTRS = {
60
+ DOM_CLASS_NAMES.INLINE_SDT_LABEL, DOM_CLASS_NAMES.BLOCK_SDT_LABEL;
61
+ //#endregion
62
+ //#region ../layout-engine/dom-contract/src/data-attrs.ts
63
+ /**
64
+ * DOM Contract: Data Attributes
65
+ *
66
+ * Named constants for `data-*` attributes stamped on rendered DOM elements.
67
+ * These attributes are read by editor-side DOM observers, click-to-position
68
+ * mapping, and bridge compatibility code.
69
+ *
70
+ * Each constant stores the full attribute name (e.g. `"data-pm-start"`) as it
71
+ * appears in `getAttribute()` / `setAttribute()` calls and CSS selectors.
72
+ *
73
+ * The `DATASET_KEYS` mirror provides the camelCase equivalents used with
74
+ * `element.dataset.*` access.
75
+ *
76
+ * Editor-neutral (prep-001) attributes live alongside the legacy `data-pm-*`
77
+ * attributes; both surfaces are emitted so that v1 consumers continue to work
78
+ * unmodified while future editor-neutral consumers (hit-test / range mapping)
79
+ * can address rendered output without consulting ProseMirror positions.
80
+ */
81
+ /**
82
+ * Full attribute names for use with `getAttribute` / `setAttribute` / CSS selectors.
83
+ */
84
+ var DATA_ATTRS = {
85
+ /** ProseMirror start position of the element's content range. */
24
86
  PM_START: "data-pm-start",
87
+ /** ProseMirror end position of the element's content range. */
25
88
  PM_END: "data-pm-end",
89
+ /** Layout epoch stamp — incremented on each layout pass. */
26
90
  LAYOUT_EPOCH: "data-layout-epoch",
91
+ /** JSON-encoded table boundary metadata for resize overlays. */
27
92
  TABLE_BOUNDARIES: "data-table-boundaries",
93
+ /** SDT unique identifier. */
28
94
  SDT_ID: "data-sdt-id",
95
+ /** SDT type (fieldAnnotation, structuredContent, documentSection, etc.). */
29
96
  SDT_TYPE: "data-sdt-type",
97
+ /** Field annotation field identifier. */
30
98
  FIELD_ID: "data-field-id",
99
+ /** Field annotation field type (signer, text, checkbox, etc.). */
31
100
  FIELD_TYPE: "data-field-type",
101
+ /** Marks an element as draggable by the editor. */
32
102
  DRAGGABLE: "data-draggable",
103
+ /** Display label text for drag toast / accessibility. */
33
104
  DISPLAY_LABEL: "data-display-label",
105
+ /** Field annotation variant (text, image, signature, checkbox, html, link). */
34
106
  VARIANT: "data-variant",
107
+ /** Element type discriminator (annotation variant, etc.). */
35
108
  TYPE: "data-type",
109
+ /** Schema version for the editor-neutral layout boundary attributes. */
36
110
  LAYOUT_BOUNDARY_SCHEMA: "data-layout-boundary-schema",
111
+ /** Stable opaque id of the rendered fragment (see `LayoutFragmentId`). */
37
112
  LAYOUT_FRAGMENT_ID: "data-layout-fragment-id",
113
+ /** Encoded story locator (e.g. `body`, `header:rId4`, `footer:rId7`). */
38
114
  LAYOUT_STORY: "data-layout-story",
115
+ /** Source block reference (today: producer's `blockId`). */
39
116
  LAYOUT_BLOCK_REF: "data-layout-block-ref"
40
117
  };
41
- const decodeLayoutStoryDataset = (raw) => {
118
+ /**
119
+ * Decode the dataset string back into a `LayoutStoryLocator`-shaped object.
120
+ *
121
+ * Used by editor-side DOM observers. Unknown kinds fall back to
122
+ * `{ kind: 'unknown' }` so downstream code can treat the value as a
123
+ * diagnostic, not as a default body.
124
+ */
125
+ var decodeLayoutStoryDataset = (raw) => {
42
126
  if (!raw) return { kind: "unknown" };
43
127
  if (raw === "body") return { kind: "body" };
44
128
  const idx = raw.indexOf(":");
@@ -57,8 +141,21 @@ const decodeLayoutStoryDataset = (raw) => {
57
141
  default: return { kind: "unknown" };
58
142
  }
59
143
  };
60
- const SDT_BLOCK_WITH_ID_SELECTOR = `.${DOM_CLASS_NAMES.BLOCK_SDT}[${DATA_ATTRS.SDT_ID}]`;
61
- const DRAGGABLE_SELECTOR = `[${DATA_ATTRS.DRAGGABLE}="true"]`;
144
+ `${DOM_CLASS_NAMES.BLOCK_SDT}${DATA_ATTRS.SDT_ID}`;
145
+ `${DATA_ATTRS.DRAGGABLE}`;
146
+ //#endregion
147
+ //#region ../layout-engine/contracts/src/direction-context.ts
148
+ /**
149
+ * Read a paragraph's inline base direction from its attributes.
150
+ *
151
+ * Prefers the resolved {@link ParagraphDirectionContext} (SD-2776) when
152
+ * present. Falls back to `paragraphProperties.rightToLeft` for PM-node /
153
+ * editor paths that store direction on the raw OOXML properties rather
154
+ * than the typed direction context.
155
+ *
156
+ * Consumers should call this instead of inspecting attrs ad hoc so the
157
+ * direction source check stays in one place.
158
+ */
62
159
  function getParagraphInlineDirection(attrs) {
63
160
  const fromContext = attrs?.directionContext?.inlineDirection;
64
161
  if (fromContext != null) return fromContext;
@@ -66,97 +163,65 @@ function getParagraphInlineDirection(attrs) {
66
163
  if (ppRtl === true) return "rtl";
67
164
  if (ppRtl === false) return "ltr";
68
165
  }
69
- var FALLBACK_PALETTE = [
70
- "#1f6feb",
71
- "#d1242f",
72
- "#8250df",
73
- "#bf3989",
74
- "#1a7f37",
75
- "#9a6700",
76
- "#bc4c00",
77
- "#0969da",
78
- "#cf222e",
79
- "#6639ba",
80
- "#116329",
81
- "#7d4e00"
82
- ];
83
- const authorIdentityKey = (author) => {
84
- if (!author) return "";
85
- return `${typeof author.name === "string" ? author.name : ""} ${typeof author.email === "string" ? author.email : ""}`;
86
- };
87
- var hashString = (value) => {
88
- let hash = 2166136261;
89
- for (let i = 0; i < value.length; i += 1) {
90
- hash ^= value.charCodeAt(i);
91
- hash = Math.imul(hash, 16777619);
92
- }
93
- return hash >>> 0;
94
- };
95
- const fallbackAuthorColor = (author) => {
96
- return FALLBACK_PALETTE[hashString(authorIdentityKey(author)) % FALLBACK_PALETTE.length];
97
- };
98
- var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
99
- const composeAuthorColorResolver = (config) => {
100
- if (!config || config.enabled === false) return void 0;
101
- const overrides = config.overrides && typeof config.overrides === "object" ? config.overrides : void 0;
102
- const resolve = typeof config.resolve === "function" ? config.resolve : void 0;
103
- return (author) => {
104
- const safeAuthor = author ?? {};
105
- if (overrides) {
106
- if (isNonEmptyString(safeAuthor.email) && isNonEmptyString(overrides[safeAuthor.email])) return overrides[safeAuthor.email];
107
- if (isNonEmptyString(safeAuthor.name) && isNonEmptyString(overrides[safeAuthor.name])) return overrides[safeAuthor.name];
108
- }
109
- if (resolve) try {
110
- const resolved = resolve(safeAuthor);
111
- if (isNonEmptyString(resolved)) return resolved;
112
- } catch {}
113
- return fallbackAuthorColor(safeAuthor);
114
- };
115
- };
116
- const TRACKED_CHANGE_CONFIGURABLE_SEMANTIC_COLOR_KEYS = [
117
- "insertion",
118
- "deletion",
119
- "move",
120
- "move-from",
121
- "move-to",
122
- "table-cell-insertion",
123
- "table-cell-deletion",
124
- "cell-merge",
125
- "cell-split",
126
- "image-insertion",
127
- "image-deletion",
128
- "image-property-change"
129
- ];
130
- var CONFIGURABLE_SEMANTIC_COLOR_KEY_SET = new Set(TRACKED_CHANGE_CONFIGURABLE_SEMANTIC_COLOR_KEYS);
131
- const DRAWING_DIAGNOSTIC_CODES = {
166
+ //#endregion
167
+ //#region ../layout-engine/contracts/src/drawing-taxonomy.ts
168
+ /**
169
+ * Frozen canonical diagnostic codes for drawing rendering.
170
+ *
171
+ * New extractor/adapter diagnostics MUST use these exact strings. Family-
172
+ * specific codes are required — do not collapse everything to
173
+ * `render.unsupported-inline-ooxml` (plan §4).
174
+ */
175
+ var DRAWING_DIAGNOSTIC_CODES = {
176
+ /** External image relationship (`r:link` / `TargetMode="External"`). Not fetched. */
132
177
  externalImageDeferred: "render.media.external-image-deferred",
178
+ /** Drawing references a relationship id that does not exist on the owner part. */
133
179
  missingRelationship: "render.drawing.missing-relationship",
180
+ /** Relationship resolves but the target media part bytes are missing/empty. */
134
181
  missingMediaPart: "render.media.missing-part",
182
+ /** Relationship exists but is not an image relationship type. */
135
183
  unsupportedRelationshipType: "render.drawing.unsupported-relationship-type",
184
+ /** Media MIME is not in the supported allowlist. */
136
185
  unsupportedMime: "render.media.unsupported-mime",
186
+ /** Media part exceeds the host byte-size policy before bytes are materialized. */
137
187
  imageTooLarge: "render.media.image-too-large",
188
+ /** SVG content rejected by the SVG safety policy. */
138
189
  unsafeSvg: "render.media.unsafe-svg",
190
+ /** Metafile / TIFF (EMF, WMF, TIFF) with no approved conversion path. */
139
191
  unsupportedFormat: "render.media.unsupported-format",
192
+ /** OLE / ActiveX / embedded package object. */
140
193
  embeddedObjectNotSupported: "render.embedded-object-not-supported",
194
+ /** SmartArt / diagram / unknown external graphic-data object. */
141
195
  unsupportedObject: "render.drawing.unsupported-object",
196
+ /** VML structure outside any supported image-like subset. */
142
197
  vmlUnsupported: "render.drawing.vml-unsupported",
198
+ /** VML image-like content that could not be promoted to a supported image. */
143
199
  vmlImageUnsupported: "render.drawing.vml-image-unsupported",
200
+ /** Custom geometry command the extractor does not implement (e.g. `arcTo`). */
144
201
  unsupportedGeometryCommand: "render.drawing.unsupported-geometry-command",
202
+ /** Anchor fields required for honest placement are unsupported. */
145
203
  anchorUnsupported: "render.drawing.anchor-unsupported",
204
+ /** Wrap fields required for honest placement are unsupported. */
146
205
  wrapUnsupported: "render.drawing.wrap-unsupported",
206
+ /** `mc:AlternateContent` had no supported choice and no usable fallback. */
147
207
  altContentNoSupportedChoice: "render.drawing.altcontent-no-supported-choice",
208
+ /** A child of an otherwise-supported group is unsupported. */
148
209
  groupChildUnsupported: "render.drawing.group-child-unsupported"
149
210
  };
150
- const DRAWING_DIAGNOSTIC_CODE_ALIASES = {
151
- "render.media.missing-relationship": DRAWING_DIAGNOSTIC_CODES.missingRelationship,
152
- "render.media.wrong-relationship-type": DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType,
153
- "render.media.unsupported-target": DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType,
154
- "render.media.invalid-image-size": DRAWING_DIAGNOSTIC_CODES.imageTooLarge,
155
- "render.media-resolver-unavailable": DRAWING_DIAGNOSTIC_CODES.missingMediaPart,
156
- "render.chart-not-supported": DRAWING_DIAGNOSTIC_CODES.unsupportedObject,
157
- "render.textbox.vml-unsupported": DRAWING_DIAGNOSTIC_CODES.vmlUnsupported
158
- };
159
- const DRAWING_SUPPORT_TAXONOMY = {
211
+ DRAWING_DIAGNOSTIC_CODES.missingRelationship, DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType, DRAWING_DIAGNOSTIC_CODES.unsupportedRelationshipType, DRAWING_DIAGNOSTIC_CODES.imageTooLarge, DRAWING_DIAGNOSTIC_CODES.missingMediaPart, DRAWING_DIAGNOSTIC_CODES.unsupportedObject, DRAWING_DIAGNOSTIC_CODES.vmlUnsupported;
212
+ /**
213
+ * The frozen drawing support taxonomy.
214
+ *
215
+ * Invariants (enforced by `drawing-taxonomy.test.ts`):
216
+ * - every `supported` family has a real contract target (`!== 'none'`);
217
+ * - every `DrawingBlock`-targeted family declares a `drawingKind`;
218
+ * - every `fail-closed` family declares a canonical `diagnostic`;
219
+ * - every canonical diagnostic code is referenced by at least one fail-closed
220
+ * family;
221
+ * - `deferred` families make no support claim (`contract: 'none'`, no
222
+ * diagnostic).
223
+ */
224
+ var DRAWING_SUPPORT_TAXONOMY = {
160
225
  inlineBitmap: {
161
226
  family: "inlineBitmap",
162
227
  support: "supported",
@@ -352,8 +417,8 @@ const DRAWING_SUPPORT_TAXONOMY = {
352
417
  contract: "none"
353
418
  }
354
419
  };
355
- const DRAWING_FAMILIES = Object.keys(DRAWING_SUPPORT_TAXONOMY);
356
- const PAGE_CHECKPOINT_DEPENDENCY_CLASSES = Object.freeze([
420
+ Object.keys(DRAWING_SUPPORT_TAXONOMY);
421
+ Object.freeze([
357
422
  "multiple-sections",
358
423
  "furniture-page-tokens",
359
424
  "non-balanceable-multi-column-sections",
@@ -364,18 +429,31 @@ const PAGE_CHECKPOINT_DEPENDENCY_CLASSES = Object.freeze([
364
429
  "tables",
365
430
  "furniture-anchored-objects"
366
431
  ]);
432
+ //#endregion
433
+ //#region src/helpers/v2-review-mutation-impact.js
434
+ /**
435
+ * Classify the tracked-change identities carried by a v2 mutation event.
436
+ *
437
+ * Receipt entities are the durable invalidation contract between the document
438
+ * kernel and derived review UI. Consumers can refresh `upsertIds` narrowly via
439
+ * `trackChanges.get(...)`, drop `removedIds` immediately, and leave a bounded
440
+ * full-list reconciliation as background verification. History results are
441
+ * different: undo/redo can restore many identities at once, so consumers use
442
+ * one authoritative catalog after render instead of issuing one read per id.
443
+ */
367
444
  function getV2TrackedChangeMutationImpact(event) {
368
445
  if (event?.type !== "mutation:committed") return null;
369
446
  const payload = event.origin === "history" ? event.result : event.receipt;
370
447
  if (!payload || typeof payload !== "object") return null;
371
448
  const upsertIds = /* @__PURE__ */ new Set();
372
449
  const removedIds = /* @__PURE__ */ new Set();
450
+ /** @type {Array<{ from: string, to: string }>} */
373
451
  const remappedPairs = [];
374
452
  const collectEntity = (entry, into) => {
375
453
  if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return;
376
454
  if (typeof entry.entityId === "string" && entry.entityId.length > 0) into.add(entry.entityId);
377
455
  };
378
- const readEntityId$1 = (entry) => {
456
+ const readEntityId = (entry) => {
379
457
  if (entry?.kind !== "entity" || entry.entityType !== "trackedChange") return null;
380
458
  return typeof entry.entityId === "string" && entry.entityId.length > 0 ? entry.entityId : null;
381
459
  };
@@ -385,8 +463,8 @@ function getV2TrackedChangeMutationImpact(event) {
385
463
  if (Array.isArray(payload.removed)) payload.removed.forEach((entry) => collectEntity(entry, removedIds));
386
464
  if (Array.isArray(payload.invalidatedRefs)) payload.invalidatedRefs.forEach((entry) => collectEntity(entry, removedIds));
387
465
  if (Array.isArray(payload.remappedRefs)) for (const mapping of payload.remappedRefs) {
388
- const fromId = readEntityId$1(mapping?.from);
389
- const toId = readEntityId$1(mapping?.to);
466
+ const fromId = readEntityId(mapping?.from);
467
+ const toId = readEntityId(mapping?.to);
390
468
  if (fromId) removedIds.add(fromId);
391
469
  if (toId) upsertIds.add(toId);
392
470
  if (fromId && toId && fromId !== toId) remappedPairs.push({
@@ -410,6 +488,8 @@ function readAllResolvedFact(event, receipt) {
410
488
  if (event?.origin === "history" || !fact || fact.schemaVersion !== 1 || fact.targetKind !== "all" || fact.decision !== "accept" && fact.decision !== "reject" || fact.remainingLogicalCount !== 0 || typeof fact.catalogRevision !== "string" || !fact.catalogRevision || typeof fact.sourceCoverageRevision !== "string" || !fact.sourceCoverageRevision || !Number.isSafeInteger(fact.logicalTargetCount) || fact.logicalTargetCount <= 0 || !Number.isSafeInteger(fact.physicalCarrierCount) || fact.physicalCarrierCount < fact.logicalTargetCount || typeof fact.txId !== "string" || fact.txId !== receipt.txId || typeof fact.documentEpoch !== "string" || !Number.isSafeInteger(fact.commitSequence) || typeof fact.packagePreviousRevision !== "string" || typeof fact.packageNextRevision !== "string" || fact.packagePreviousRevision === fact.packageNextRevision) return null;
411
489
  return fact;
412
490
  }
491
+ //#endregion
492
+ //#region src/helpers/v2-review-mutation-reconciler.js
413
493
  var normalizeIds = (values) => new Set(Array.from(values ?? []).filter((value) => typeof value === "string" && value.length > 0));
414
494
  var itemId = (item) => {
415
495
  if (typeof item?.id === "string" && item.id.length > 0) return item.id;
@@ -418,6 +498,13 @@ var itemId = (item) => {
418
498
  return null;
419
499
  };
420
500
  var contextMatches = (left, right) => left?.adapter === right?.adapter && left?.documentId === right?.documentId && left?.editor === right?.editor && left?.reconcileToken === right?.reconcileToken;
501
+ /**
502
+ * Reconcile tracked changes only after their canonical render has made the
503
+ * read model observable. Command receipts use narrow reads; history results
504
+ * use one authoritative catalog because undo/redo can restore hundreds of
505
+ * identities in one transaction. An absent history identity only converges
506
+ * when both catalog and source coverage are explicitly complete.
507
+ */
421
508
  function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onReconciled }) {
422
509
  const pendingUpserts = /* @__PURE__ */ new Map();
423
510
  let pendingAllResolved = null;
@@ -485,7 +572,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
485
572
  reconcileMode: "targeted"
486
573
  });
487
574
  if (!isCurrent(capturedGeneration, context)) return;
488
- await notifyReconciled(context, clearResolved(idsFromResult(result), snapshot), result);
575
+ const resolvedIds = idsFromResult(result);
576
+ const clearedIds = clearResolved(resolvedIds, snapshot);
577
+ await notifyReconciled(context, clearedIds, result);
489
578
  if (new Set([...snapshot].filter(([id, version]) => pendingUpserts.get(id) === version).map(([id]) => id)).size === 0) return;
490
579
  const fallback = await hydrate(context, {
491
580
  trackedChangesListMode: "interaction-prime",
@@ -493,7 +582,9 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
493
582
  blocking: false
494
583
  });
495
584
  if (!isCurrent(capturedGeneration, context)) return;
496
- await notifyReconciled(context, clearResolved(idsFromResult(fallback), snapshot), fallback);
585
+ const fallbackResolvedIds = idsFromResult(fallback);
586
+ const fallbackClearedIds = clearResolved(fallbackResolvedIds, snapshot);
587
+ await notifyReconciled(context, fallbackClearedIds, fallback);
497
588
  };
498
589
  const reconcileAuthoritativeSnapshot = async (context, snapshot, capturedGeneration) => {
499
590
  const result = await hydrate(context, {
@@ -617,7 +708,7 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
617
708
  resolve
618
709
  });
619
710
  });
620
- const impactedIds = new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
711
+ const impactedIds = /* @__PURE__ */ new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
621
712
  const capturedEntries = new Map([...impactedIds].flatMap((id) => {
622
713
  const entry = pendingUpserts.get(id);
623
714
  return entry ? [[id, entry]] : [];
@@ -671,7 +762,19 @@ function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onRe
671
762
  }
672
763
  };
673
764
  }
674
- var V2_TYPING_COMMAND_KINDS = new Set([
765
+ //#endregion
766
+ //#region src/helpers/v2-typing-mutation-event.js
767
+ /**
768
+ * Command kinds whose committed mutations belong to the sustained typing
769
+ * cadence class (insert / replace / plain paste / Backspace / Delete).
770
+ *
771
+ * The SuperDoc shell debounces per-keystroke review-row hydration for these
772
+ * kinds instead of refreshing comments + tracked changes on every commit —
773
+ * hydrating immediately per keystroke was a measured worker-read storm during
774
+ * typing and backspace bursts. The `text.*` aliases cover integrations that
775
+ * forward Document API style command ids rather than editable-input kinds.
776
+ */
777
+ var V2_TYPING_COMMAND_KINDS = /* @__PURE__ */ new Set([
675
778
  "insert-text",
676
779
  "replace-text",
677
780
  "plain-text-paste",
@@ -693,33 +796,158 @@ var V2_TYPING_COMMAND_KINDS = new Set([
693
796
  "backspace:list-remove",
694
797
  "backspace:list-outdent"
695
798
  ]);
696
- const isV2EditableTextMutationEvent = (event) => {
799
+ /**
800
+ * True when a forwarded v2 host event is a committed editable-input mutation
801
+ * of the sustained-typing class, i.e. review-row hydration for it should be
802
+ * debounced rather than fired immediately.
803
+ *
804
+ * Events without a forwarded `editableCommandKind` (programmatic Document API
805
+ * mutations, history commits, unclassified structural commands) return false
806
+ * so they keep hydrating immediately.
807
+ *
808
+ * @param {{ type?: string, origin?: string, editableCommandKind?: string } | null | undefined} event
809
+ * @returns {boolean}
810
+ */
811
+ var isV2EditableTextMutationEvent = (event) => {
697
812
  if (event?.type !== "mutation:committed" || event.origin !== "command") return false;
698
813
  return V2_TYPING_COMMAND_KINDS.has(event.editableCommandKind);
699
814
  };
700
- const SUPERDOC_UI_REASONS = {
815
+ //#endregion
816
+ //#region src/public/ui/reasons.ts
817
+ /**
818
+ * Stable public reason taxonomy for the v2-native `superdoc/ui` controller.
819
+ *
820
+ * These strings are the canonical, stable vocabulary the controller uses to
821
+ * explain WHY a command or workflow handle is disabled, unsupported, deferred,
822
+ * or otherwise fails closed. They are part of the public custom-UI contract:
823
+ * consumer toolbars/panels can branch on them and migration docs reference
824
+ * them. Once shipped, a value must not be repurposed — add a new member rather
825
+ * than changing the meaning of an existing one.
826
+ *
827
+ * The reasons distinguish the failure-mode families a custom UI must tell
828
+ * apart (see `plans/v2-custom-ui-toolbar-parity-plan-set.md`, Workstream 2):
829
+ *
830
+ * - command unsupported by v2 → `command-unsupported`
831
+ * - reserved recognized/deferred command → `command-deferred`
832
+ * - editor / Document API not ready yet → `not-ready`, `document-api-unavailable`
833
+ * - blocked by viewing / read-only state → `document-readonly`
834
+ * - blocked by missing selection/context → `selection-required`,
835
+ * `range-selection-required`, `context-unavailable`, `target-unresolved`,
836
+ * `target-not-visible`, `geometry-unavailable`
837
+ * - operation / host capability missing → `operation-unavailable`,
838
+ * `host-capability-unavailable`, `bulk-decisions-disabled`
839
+ *
840
+ * Geometry results (`viewport.getRect`, `metadata.getRect`) carry a separate,
841
+ * geometry-local `reason` vocabulary (`not-mounted`, `unresolved`,
842
+ * `invalid-target`, `not-ready`, `unavailable`); see `ViewportRectResult`.
843
+ * That vocabulary predates this taxonomy and is intentionally narrower.
844
+ */
845
+ var SUPERDOC_UI_REASONS = {
846
+ /** The active editor is not mounted / ready yet. */
701
847
  notReady: "not-ready",
848
+ /** The editor is ready but its Document API facade is not available or has not been published yet. */
702
849
  documentApiUnavailable: "document-api-unavailable",
850
+ /** The document is in viewing / read-only mode, so mutating commands are blocked. */
703
851
  documentReadonly: "document-readonly",
852
+ /** A selection is required and none is active. */
704
853
  selectionRequired: "selection-required",
854
+ /** A non-empty (range) selection is required and the current selection is collapsed/empty. */
705
855
  rangeSelectionRequired: "range-selection-required",
856
+ /** Required pointer / entity / document context could not be resolved. */
706
857
  contextUnavailable: "context-unavailable",
858
+ /** Painted geometry for the target could not be resolved. */
707
859
  geometryUnavailable: "geometry-unavailable",
860
+ /** A navigation / geometry target could not be resolved to a live document address. */
708
861
  targetUnresolved: "target-unresolved",
862
+ /** A navigation target was resolved but could not be brought into the visible viewport. */
709
863
  targetNotVisible: "target-not-visible",
864
+ /** The command is not supported by v2 at all. */
710
865
  commandUnsupported: "command-unsupported",
866
+ /** Reserved migration reason for a recognized command that is intentionally deferred. */
711
867
  commandDeferred: "command-deferred",
868
+ /**
869
+ * The command is a real v2 operation, but its required document context
870
+ * (the current table / row / column / cell) cannot be resolved from any
871
+ * public custom-UI state: `selection.current` exposes only text block ids,
872
+ * and no public operation resolves the table ancestry of a block. This is a
873
+ * precise, named context-facade gap — distinct from `command-deferred` — for
874
+ * the table cell-context command family.
875
+ */
712
876
  tableContextUnavailable: "table-context-unavailable",
877
+ /** The backing Document API / SuperDoc operation is not present on this host. */
713
878
  operationUnavailable: "operation-unavailable",
879
+ /** Bulk accept/reject decisions are disabled unless the host opts in. */
714
880
  bulkDecisionsDisabled: "bulk-decisions-disabled",
881
+ /** A host-owned capability (geometry, hit-test, navigation) is unavailable. */
715
882
  hostCapabilityUnavailable: "host-capability-unavailable",
883
+ /** Undo / redo cannot run because the host history stack has no matching entry. */
716
884
  historyEmpty: "history-empty",
885
+ /**
886
+ * The shared search/find surface is unavailable because the host does not
887
+ * expose a search facade (e.g. a pre-ready editor, a worker-backed v2 host,
888
+ * or a build without the search substrate). Search reads return empty and
889
+ * search actions fail closed rather than fabricating matches.
890
+ */
717
891
  searchUnavailable: "search-unavailable",
892
+ /** The search pattern is an invalid or unsafe regular expression. */
718
893
  searchInvalidPattern: "search-invalid-pattern",
894
+ /**
895
+ * Find-and-replace is intentionally out of the first v2 search parity tranche.
896
+ * The shared search surface implements query / navigation only; replace and
897
+ * replace-all fail closed with this reason until replace ships. This is an
898
+ * explicit product posture, not a missing-capability accident.
899
+ */
719
900
  replaceUnsupported: "replace-unsupported",
901
+ /**
902
+ * The selection overlaps a content control (SDT) whose lock mode
903
+ * (`contentLocked` / `sdtContentLocked`) forbids styling its content — Word
904
+ * parity (SD-3274): the toolbar must not leave styling controls clickable
905
+ * when the style change cannot apply. Alignment / paragraph-level commands
906
+ * are never gated by this reason.
907
+ */
720
908
  contentControlLocked: "content-control-locked"
721
909
  };
722
- const BUILT_IN_COMMAND_IDS = {
910
+ //#endregion
911
+ //#region src/public/ui/commands.ts
912
+ /**
913
+ * Built-in command catalog for the v2-native UI controller.
914
+ *
915
+ * This module is the SINGLE SOURCE OF TRUTH for the controller's built-in
916
+ * command ids, their aliases, how each one routes (public Document API
917
+ * operation, public SuperDoc-instance method, tracked-change decision, or no
918
+ * supported route yet), whether it mutates the document, how v1-style payloads
919
+ * are normalized, and the stable fail-closed reason a recognized-but-unrouted
920
+ * command reports. `create-super-doc-ui.ts` reads these descriptors; it no
921
+ * longer carries an ad hoc route map.
922
+ *
923
+ * Posture (migration-only): every v1 headless-toolbar command id is present
924
+ * here with an explicit disposition —
925
+ *
926
+ * - `routed` → backed by a public surface today (Document API operation,
927
+ * SuperDoc-instance method, or tracked-change decision) and
928
+ * browser-proven through `ui.commands.execute`;
929
+ * - `context-gap` → a real v2 operation whose required document context (the
930
+ * current table / row / column / cell) cannot be resolved
931
+ * from public custom-UI state; reports the precise, named
932
+ * `table-context-unavailable` reason and fails closed;
933
+ * - `unsupported` → no clear public v2 equivalent (product decision) or an
934
+ * unknown id; reports `command-unsupported` and fails closed.
935
+ *
936
+ * No routeable v1 toolbar command remains `command-deferred` (that disposition
937
+ * stays in the type for migration scaffolding but is unused by the catalog).
938
+ *
939
+ * No descriptor imports a v1 editor internal or a private v2 runtime package,
940
+ * and no routed command mutates while the document is read-only / viewing.
941
+ *
942
+ * See `docs/architecture/custom-ui-command-matrix.md` for the per-id matrix.
943
+ */
944
+ /**
945
+ * The 14 canonical v2-native command ids. These are the historically stable
946
+ * `superdoc/ui` built-ins; the broader catalog below adds v1 headless-toolbar
947
+ * coverage. Kept as a named export because it is part of the public `superdoc/ui`
948
+ * surface (`verify-public-facade-emit.cjs`, consumer typechecks).
949
+ */
950
+ var BUILT_IN_COMMAND_IDS = {
723
951
  bold: "bold",
724
952
  italic: "italic",
725
953
  underline: "underline",
@@ -737,6 +965,7 @@ const BUILT_IN_COMMAND_IDS = {
737
965
  bulletList: "bullet-list",
738
966
  numberedList: "numbered-list"
739
967
  };
968
+ /** Strip a trailing `pt` unit from v1-style font sizes (`"12pt"` → `"12"`). */
740
969
  function normalizeFontSizePayload(payload) {
741
970
  const strip = (value) => typeof value === "string" ? value.replace(/\s*pt$/i, "").trim() : value;
742
971
  if (typeof payload === "string") return strip(payload);
@@ -749,6 +978,12 @@ function normalizeFontSizePayload(payload) {
749
978
  }
750
979
  return payload;
751
980
  }
981
+ /**
982
+ * Normalize v1-style zoom payloads to the percentage `SuperDoc.setZoom`
983
+ * expects (`100 === 100%`). Accepts `150`, `"150%"`, `"1.5"`, or `1.5`.
984
+ * Percentages pass through as numbers; legacy fraction-style values in the
985
+ * `0..5` range are converted to percentages (`1.5` → `150`).
986
+ */
752
987
  function normalizeZoomPayload(payload) {
753
988
  const normalizeNumber = (value, percentLiteral) => {
754
989
  if (!percentLiteral && value > 0 && value <= 5) return value * 100;
@@ -763,6 +998,12 @@ function normalizeZoomPayload(payload) {
763
998
  if (typeof payload === "number" && Number.isFinite(payload)) return normalizeNumber(payload, false);
764
999
  return payload;
765
1000
  }
1001
+ /**
1002
+ * Normalize a measurement-unit payload to the `'in'` / `'cm'` form
1003
+ * `SuperDoc.setMeasurementUnit` expects. Accepts the canonical `'in'` / `'cm'`,
1004
+ * a `{ value }` wrapper, or common long forms (`"inches"`, `"centimeters"`).
1005
+ * An unrecognized value passes through so the instance method can reject it.
1006
+ */
766
1007
  function normalizeMeasurementUnitPayload(payload) {
767
1008
  const raw = payload && typeof payload === "object" && "value" in payload ? payload.value : payload;
768
1009
  if (raw === "in" || raw === "cm") return raw;
@@ -773,6 +1014,12 @@ function normalizeMeasurementUnitPayload(payload) {
773
1014
  }
774
1015
  return raw;
775
1016
  }
1017
+ /**
1018
+ * Normalize a v1-style color payload to the `#RRGGBB` form the inline
1019
+ * `format.color` / `format.highlight` operations expect. Accepts `"#RRGGBB"`,
1020
+ * `"RRGGBB"`, or `{ value }`. `null` (clear) passes through unchanged; an
1021
+ * unrecognized value is returned as-is so the operation can reject it.
1022
+ */
776
1023
  function normalizeColorPayload(payload) {
777
1024
  const coerce = (value) => {
778
1025
  if (value === null) return null;
@@ -792,6 +1039,7 @@ function normalizeColorPayload(payload) {
792
1039
  }
793
1040
  return coerce(payload);
794
1041
  }
1042
+ /** Unwrap a `{ value }` / `{ alignment } ` / `{ styleId }` wrapper to its scalar. */
795
1043
  function unwrapScalar(payload, keys) {
796
1044
  if (payload && typeof payload === "object") {
797
1045
  const record = payload;
@@ -799,6 +1047,7 @@ function unwrapScalar(payload, keys) {
799
1047
  }
800
1048
  return payload;
801
1049
  }
1050
+ /** Normalize a v1 alignment payload (`"left"`, `{ value }`, `"justified"`) to a `ParagraphAlignment`. */
802
1051
  function normalizeAlignmentPayload(payload) {
803
1052
  const raw = unwrapScalar(payload, ["alignment", "value"]);
804
1053
  if (typeof raw !== "string") return raw;
@@ -806,6 +1055,11 @@ function normalizeAlignmentPayload(payload) {
806
1055
  if (v === "justified" || v === "both") return "justify";
807
1056
  return v;
808
1057
  }
1058
+ /**
1059
+ * Normalize a v1 line-height payload to OOXML auto line spacing (240ths of a
1060
+ * line: 240 = single, 360 = 1.5×, 480 = double). A multiplier in the `0..10`
1061
+ * range is converted (`1.5` → `360`); a larger raw value passes through.
1062
+ */
809
1063
  function normalizeLineHeightPayload(payload) {
810
1064
  const raw = unwrapScalar(payload, [
811
1065
  "line",
@@ -816,6 +1070,7 @@ function normalizeLineHeightPayload(payload) {
816
1070
  if (!Number.isFinite(num) || num <= 0) return raw;
817
1071
  return num <= 10 ? Math.round(num * 240) : Math.round(num);
818
1072
  }
1073
+ /** Preserve semantic style intent; normalize legacy linked-style payloads to a concrete ID. */
819
1074
  function normalizeStyleIdPayload(payload) {
820
1075
  if (payload && typeof payload === "object" && "role" in payload) return payload;
821
1076
  const raw = unwrapScalar(payload, [
@@ -825,6 +1080,7 @@ function normalizeStyleIdPayload(payload) {
825
1080
  ]);
826
1081
  return typeof raw === "string" ? raw.trim() : raw;
827
1082
  }
1083
+ /** Resolve a document-mode payload (`"viewing"` or `{ mode }`) to the mode string. */
828
1084
  function normalizeDocumentModePayload(payload) {
829
1085
  if (typeof payload === "string") return payload;
830
1086
  if (payload && typeof payload === "object") {
@@ -835,7 +1091,14 @@ function normalizeDocumentModePayload(payload) {
835
1091
  }
836
1092
  var UNSUPPORTED = SUPERDOC_UI_REASONS.commandUnsupported;
837
1093
  var TABLE_CONTEXT = SUPERDOC_UI_REASONS.tableContextUnavailable;
838
- const COMMAND_CATALOG = [
1094
+ /**
1095
+ * The built-in command catalog. Order is documentation-only. Every v1
1096
+ * headless-toolbar id appears here with an explicit disposition; the 14
1097
+ * canonical v2 ids route today, the kebab track-change selection ids and the
1098
+ * zoom / document-mode ids are newly routed in this unit, and the remaining v1
1099
+ * workflows are deferred or marked product-unsupported with a stable reason.
1100
+ */
1101
+ var COMMAND_CATALOG = [
839
1102
  {
840
1103
  id: "bold",
841
1104
  family: "inline marks",
@@ -1355,10 +1618,23 @@ const COMMAND_CATALOG = [
1355
1618
  }
1356
1619
  ];
1357
1620
  var CATALOG_BY_ID = new Map(COMMAND_CATALOG.map((descriptor) => [descriptor.id, descriptor]));
1358
- const ALL_BUILT_IN_COMMAND_IDS = COMMAND_CATALOG.map((d) => d.id);
1621
+ /** All built-in command ids known to the controller (routed + deferred + unsupported). */
1622
+ var ALL_BUILT_IN_COMMAND_IDS = COMMAND_CATALOG.map((d) => d.id);
1623
+ /** Resolve a command descriptor by id, or `null` when the id is unknown. */
1359
1624
  function getCommandDescriptor(id) {
1360
1625
  return CATALOG_BY_ID.get(id) ?? null;
1361
1626
  }
1627
+ //#endregion
1628
+ //#region src/public/ui/equality.ts
1629
+ /**
1630
+ * Shallow equality used by the controller to suppress redundant slice
1631
+ * notifications. v2-native, dependency-free.
1632
+ */
1633
+ /**
1634
+ * Returns `true` when `a` and `b` are the same reference, or are objects with
1635
+ * the same own-enumerable keys and `Object.is`-equal values one level deep.
1636
+ * Arrays compare by index. Primitives compare with `Object.is`.
1637
+ */
1362
1638
  function shallowEqual(a, b) {
1363
1639
  if (Object.is(a, b)) return true;
1364
1640
  if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
@@ -1372,6 +1648,16 @@ function shallowEqual(a, b) {
1372
1648
  }
1373
1649
  return true;
1374
1650
  }
1651
+ //#endregion
1652
+ //#region src/public/ui/format-painter-helpers.js
1653
+ /**
1654
+ * Maps SDRunProps field names to InlineRunPatch field names.
1655
+ * SDRunProps (read-side) and InlineRunPatch (write-side) use different keys
1656
+ * for the same properties; direct assignment between them is forbidden.
1657
+ *
1658
+ * @param {Record<string, unknown>} props - SDRunProps object from doc.getNodeById
1659
+ * @returns {Record<string, unknown>} InlineRunPatch ready for doc.format.apply
1660
+ */
1375
1661
  function sdRunPropsToInlineRunPatch(props) {
1376
1662
  if (!props || typeof props !== "object") return {};
1377
1663
  const patch = {};
@@ -1462,6 +1748,16 @@ function projectBorderToInlinePatch(border) {
1462
1748
  if (color != null) patch.color = color;
1463
1749
  return Object.keys(patch).length > 0 ? patch : void 0;
1464
1750
  }
1751
+ /**
1752
+ * Serializes a selection to a stable string key used to detect whether
1753
+ * the target selection is the same as the source (skip apply in that case).
1754
+ *
1755
+ * Falls back to the plain text target when the host has not populated an
1756
+ * explicit `selectionTarget` yet.
1757
+ *
1758
+ * @param {{ selectionTarget?: unknown; target?: unknown } | null | undefined} selection
1759
+ * @returns {string}
1760
+ */
1465
1761
  function selectionKey(selection) {
1466
1762
  const target = selection?.selectionTarget ?? selection?.target;
1467
1763
  if (!selection || !target) return "";
@@ -1471,9 +1767,21 @@ function selectionKey(selection) {
1471
1767
  return String(target);
1472
1768
  }
1473
1769
  }
1770
+ //#endregion
1771
+ //#region src/public/ui/entity-at.ts
1474
1772
  function parseCommaSeparatedIds(value) {
1475
1773
  return value.split(",").map((id) => id.trim()).filter(Boolean);
1476
1774
  }
1775
+ /**
1776
+ * Collect every public-id candidate a painted run can carry for a tracked
1777
+ * change, deduped, in priority order: the canonical
1778
+ * `data-track-change-preferred-target-id` first (when the painter stamps one),
1779
+ * then the `data-track-change-ids` list, then the singular
1780
+ * `data-track-change-id`. The caller validates these against the live
1781
+ * tracked-changes slice and keeps whichever is the public id, so it does not
1782
+ * matter which attribute carries it — an imported/raw alias on one attribute
1783
+ * never hides the public id on another (a union, not a prefer-one-attribute).
1784
+ */
1477
1785
  function getTrackChangeIds(node) {
1478
1786
  const out = [];
1479
1787
  const add = (id) => {
@@ -1485,6 +1793,21 @@ function getTrackChangeIds(node) {
1485
1793
  add(node.getAttribute("data-track-change-id"));
1486
1794
  return out;
1487
1795
  }
1796
+ /**
1797
+ * Read painted entities off `el` and every ancestor up to the document root, or
1798
+ * up to and INCLUDING `stopAt` when given. Innermost-first ordering: a tracked
1799
+ * change inside a comment highlight returns `[{ trackedChange }, { comment }]`,
1800
+ * matching what a switch on `hits[0]` expects when picking the most specific
1801
+ * entity.
1802
+ *
1803
+ * `stopAt` bounds the walk to the visible host: its own attributes are read, but
1804
+ * the walk never climbs into an app wrapper ABOVE it, so wrapper data-* cannot
1805
+ * leak into the hits. Omitting it keeps the original walk-to-document-root.
1806
+ *
1807
+ * Returns `[]` for null / non-Element starts. Uses duck-typed `getAttribute`
1808
+ * access so it works under any DOM implementation (happy-dom, jsdom, real
1809
+ * browser) without an `instanceof` check that could fail across realms.
1810
+ */
1488
1811
  function collectEntityHitsFromChain(start, stopAt) {
1489
1812
  if (!start || typeof start.getAttribute !== "function") return [];
1490
1813
  const hits = [];
@@ -1546,6 +1869,8 @@ function collectEntityHitsFromChain(start, stopAt) {
1546
1869
  }
1547
1870
  return hits;
1548
1871
  }
1872
+ //#endregion
1873
+ //#region ../document-api/src/format/inline-run-patch.ts
1549
1874
  var schemaBooleanOrNull = () => ({ oneOf: [{ type: "boolean" }, { type: "null" }] });
1550
1875
  var schemaStringOrNull = () => ({ oneOf: [{
1551
1876
  type: "string",
@@ -1637,7 +1962,7 @@ var runAttribute = (key, type, ooxmlElement, schema, runPropertyKey) => ({
1637
1962
  carrier: runAttributeCarrier(runPropertyKey ?? key),
1638
1963
  schema
1639
1964
  });
1640
- const INLINE_PROPERTY_REGISTRY = [
1965
+ var INLINE_PROPERTY_REGISTRY = [
1641
1966
  markBoolean("bold", "w:b", "bold"),
1642
1967
  markBoolean("italic", "w:i", "italic"),
1643
1968
  markBoolean("strike", "w:strike", "strike"),
@@ -1794,12 +2119,24 @@ const INLINE_PROPERTY_REGISTRY = [
1794
2119
  runAttribute("stylisticSets", "array", "w14:stylisticSets", schemaStylisticSets()),
1795
2120
  runAttribute("contextualAlternates", "boolean", "w14:cntxtAlts", schemaBooleanOrNull())
1796
2121
  ];
1797
- const INLINE_PROPERTY_KEY_SET = new Set(INLINE_PROPERTY_REGISTRY.map((entry) => entry.key));
1798
- const INLINE_PROPERTY_BY_KEY = Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry]));
1799
- const INLINE_PROPERTY_KEYS_BY_STORAGE = {
1800
- mark: INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "mark").map((entry) => entry.key),
1801
- runAttribute: INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "runAttribute").map((entry) => entry.key)
1802
- };
2122
+ new Set(INLINE_PROPERTY_REGISTRY.map((entry) => entry.key));
2123
+ var INLINE_PROPERTY_BY_KEY = Object.fromEntries(INLINE_PROPERTY_REGISTRY.map((entry) => [entry.key, entry]));
2124
+ INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "mark").map((entry) => entry.key), INLINE_PROPERTY_REGISTRY.filter((entry) => entry.storage === "runAttribute").map((entry) => entry.key);
2125
+ //#endregion
2126
+ //#region src/public/ui/create-super-doc-ui.ts
2127
+ /**
2128
+ * v2-native `createSuperDocUI` controller.
2129
+ *
2130
+ * A small, truthful layer over the public v2 active-editor facade
2131
+ * (`superdoc.activeEditor`), its read-only-guarded Document API
2132
+ * (`activeEditor.doc`), and SuperDoc lifecycle events. No v1 editor imports,
2133
+ * no private v2 runtime imports. In browser mode the underlying Document API
2134
+ * facade is async-capable, so the controller normalizes it into live slices
2135
+ * plus promise-capable workflow helpers. Every document read/mutation is
2136
+ * attempted through the public Document API and degrades to a stable noop /
2137
+ * `false` when the surface is unavailable (view mode, a pre-ready editor, or a
2138
+ * host that has not exposed a Document API facade) rather than throwing.
2139
+ */
1803
2140
  var sharedUiTrackedChangesCatalogByHost = /* @__PURE__ */ new WeakMap();
1804
2141
  function acquireSharedUiTrackedChangesCatalog(host) {
1805
2142
  let state = sharedUiTrackedChangesCatalogByHost.get(host);
@@ -1828,6 +2165,12 @@ var SUPERDOC_UI_REASON_VALUES = new Set(Object.values(SUPERDOC_UI_REASONS));
1828
2165
  function coerceSuperDocUIReason(reason, fallback) {
1829
2166
  return typeof reason === "string" && SUPERDOC_UI_REASON_VALUES.has(reason) ? reason : fallback;
1830
2167
  }
2168
+ /**
2169
+ * Normalize the host selection apply helper's return value for
2170
+ * `selection.apply` / `selection.restore`: host `{ ok: false, reason }`
2171
+ * results pass through (unknown reasons coerce to `target-unresolved`);
2172
+ * anything else — including legacy void/boolean returns — counts as success.
2173
+ */
1831
2174
  function normalizeHostSelectionApplyResult(result) {
1832
2175
  if (result && typeof result === "object") {
1833
2176
  if (result.ok === false) {
@@ -1844,6 +2187,7 @@ function normalizeHostSelectionApplyResult(result) {
1844
2187
  }
1845
2188
  return { ok: true };
1846
2189
  }
2190
+ /** Lifecycle events the controller subscribes to. */
1847
2191
  var HOST_EVENTS = [
1848
2192
  "document-replaced",
1849
2193
  "editorCreate",
@@ -1854,6 +2198,12 @@ var HOST_EVENTS = [
1854
2198
  "viewport-change",
1855
2199
  "fonts-changed"
1856
2200
  ];
2201
+ /**
2202
+ * row-862 — list toolbar ids routed through the v2 editor host edit-command
2203
+ * surface (`activeEditor.editCommands.lists.apply`) rather than a raw one-off
2204
+ * `doc.lists.apply` mutation, so readiness / mount / selection / read-only
2205
+ * gating and command state stay coherent with the rest of the editor chrome.
2206
+ */
1857
2207
  var LIST_TOGGLE_KINDS = {
1858
2208
  [BUILT_IN_COMMAND_IDS.bulletList]: "bullet",
1859
2209
  [BUILT_IN_COMMAND_IDS.numberedList]: "ordered"
@@ -1861,6 +2211,15 @@ var LIST_TOGGLE_KINDS = {
1861
2211
  function listToggleKind(id) {
1862
2212
  return LIST_TOGGLE_KINDS[id] ?? null;
1863
2213
  }
2214
+ /**
2215
+ * SD-3571 — the built-in toolbar's bullet/numbered style dropdowns emit a bare
2216
+ * toolbar style-key string (e.g. `'upper-roman'`, `'decimal-paren'`) as the
2217
+ * command argument. Map each to the Document API `ListPresetId` so the chosen
2218
+ * glyph/number format actually applies instead of being dropped to the default
2219
+ * decimal/disc list. Keys mirror `internal/toolbar/built-in/list-style-buttons.js`
2220
+ * (asserted by a coverage test); the public command surface also still accepts
2221
+ * an options object with an explicit `preset`.
2222
+ */
1864
2223
  var TOOLBAR_LIST_STYLE_PRESETS = {
1865
2224
  disc: "disc",
1866
2225
  circle: "circle",
@@ -1891,7 +2250,7 @@ var EMPTY_SELECTION = {
1891
2250
  activeChangeIds: [],
1892
2251
  quotedText: ""
1893
2252
  };
1894
- const HEAVY_DOC_READ_POLICY = [
2253
+ var HEAVY_DOC_READ_POLICY = [
1895
2254
  {
1896
2255
  key: "contentControls",
1897
2256
  match: "exact",
@@ -1938,19 +2297,22 @@ const HEAVY_DOC_READ_POLICY = [
1938
2297
  note: "reserved: full sections list"
1939
2298
  }
1940
2299
  ];
1941
- var COMMENTS_CATALOG_PART_URIS = new Set([
2300
+ var COMMENTS_CATALOG_PART_URIS = /* @__PURE__ */ new Set([
1942
2301
  "/word/comments.xml",
1943
2302
  "/word/commentsExtended.xml",
1944
2303
  "/word/commentsIds.xml"
1945
2304
  ]);
2305
+ /** Whether a coordinator cache key falls under the heavy-read policy. */
1946
2306
  function isHeavyDocReadKey(key) {
1947
2307
  return HEAVY_DOC_READ_POLICY.some((entry) => entry.match === "exact" ? entry.key === key : key.startsWith(entry.key));
1948
2308
  }
2309
+ /** Status precedence: a combined status is as "unsettled" as its worst part. */
1949
2310
  var STATUS_RANK = {
1950
2311
  ready: 0,
1951
2312
  stale: 1,
1952
2313
  pending: 2
1953
2314
  };
2315
+ /** Combine read statuses; `pending` (no data yet) dominates `stale`, which dominates `ready`. */
1954
2316
  function combineStatus(...statuses) {
1955
2317
  let worst = "ready";
1956
2318
  for (const status of statuses) if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;
@@ -1968,6 +2330,7 @@ function safeCall(fn, fallback) {
1968
2330
  function isPromiseLike(value) {
1969
2331
  return Boolean(value) && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
1970
2332
  }
2333
+ /** Resolve a dotted path (e.g. `format.bold`) to a callable on the doc facade. */
1971
2334
  function resolveDocOperation(doc, path) {
1972
2335
  if (!doc) return null;
1973
2336
  const parts = path.split(".");
@@ -2002,6 +2365,7 @@ function partialLinkEditFailure(hyperlinkResult, textResult) {
2002
2365
  textResult
2003
2366
  };
2004
2367
  }
2368
+ /** Read a stable public `target` off a doc-api entity row. */
2005
2369
  function readEntityTarget(row) {
2006
2370
  if (!row || typeof row !== "object") return null;
2007
2371
  const target = row.target;
@@ -2018,8 +2382,8 @@ function isHostGeometryTarget(target) {
2018
2382
  const isTextSegment = (segment) => {
2019
2383
  if (!segment || typeof segment !== "object") return false;
2020
2384
  const candidate = segment;
2021
- const range$1 = candidate.range;
2022
- return typeof candidate.blockId === "string" && typeof range$1?.start === "number" && typeof range$1.end === "number";
2385
+ const range = candidate.range;
2386
+ return typeof candidate.blockId === "string" && typeof range?.start === "number" && typeof range.end === "number";
2023
2387
  };
2024
2388
  if (Array.isArray(record.segments)) {
2025
2389
  const first = record.segments[0];
@@ -2031,6 +2395,12 @@ function isHostGeometryTarget(target) {
2031
2395
  const range = record.range;
2032
2396
  return typeof range?.start === "number" && typeof range.end === "number";
2033
2397
  }
2398
+ /**
2399
+ * Convert the tracked-change catalog's document-stable block address into the
2400
+ * text address accepted by the host geometry surface. Keeping the entity
2401
+ * address lets the host prefer the exact painted review carrier after the
2402
+ * containing block has been materialized.
2403
+ */
2034
2404
  function readTrackedChangeNavigationTarget(row) {
2035
2405
  if (!row || typeof row !== "object") return null;
2036
2406
  const target = readEntityTarget(row);
@@ -2089,11 +2459,35 @@ function entityRowMatchesRequest(row, id, story) {
2089
2459
  if (!story) return true;
2090
2460
  return storyLocatorSignature(readEntityStory(row)) === storyLocatorSignature(story);
2091
2461
  }
2462
+ /**
2463
+ * Story to thread through a navigation / scroll request for a loaded list row:
2464
+ * the row's own non-body story, or `undefined` for body / story-less rows.
2465
+ * Mirrors `getAt`, which omits body stories from its hits, so body-only
2466
+ * documents keep their id-only matching byte-for-byte unchanged while a
2467
+ * non-body row (footnote / endnote / header / footer) pins target and carrier
2468
+ * resolution to its own story — an id repeated across stories can no longer
2469
+ * resolve to another story's occurrence (IT-1250).
2470
+ */
2092
2471
  function readEntityRequestStory(row) {
2093
2472
  const story = readEntityStory(row);
2094
2473
  if (!story || storyLocatorSignature(story) === storyLocatorSignature(null)) return void 0;
2095
2474
  return story;
2096
2475
  }
2476
+ /**
2477
+ * Bridge a painter `data-layout-story` value to the public Document API
2478
+ * {@link StoryLocator} shape the controller's story-aware matchers speak.
2479
+ *
2480
+ * The painter encodes the layout story as `"body"` or `"<kind>:<id>"`;
2481
+ * `decodeLayoutStoryDataset` turns that back into `{ kind, id }`. Tracked-change
2482
+ * rows, however, carry a Document API `StoryLocator` (`{ kind: 'story',
2483
+ * storyType, noteId | refId }`), and `storyLocatorSignature` keys on
2484
+ * `storyType`, so the two forms must be reconciled before they can be compared.
2485
+ * Body, unknown, and id-less stories all return `undefined` so callers fall back
2486
+ * to id-only matching and never stamp a story onto the public hit — that keeps
2487
+ * body-only documents byte-for-byte unchanged. Story disambiguation only kicks
2488
+ * in for the non-body stories (footnote, endnote, header/footer) that can repeat
2489
+ * a tracked-change id across stories.
2490
+ */
2097
2491
  function layoutStoryDatasetToStoryLocator(raw) {
2098
2492
  if (typeof raw !== "string" || raw.length === 0) return void 0;
2099
2493
  const decoded = decodeLayoutStoryDataset(raw);
@@ -2118,6 +2512,22 @@ function layoutStoryDatasetToStoryLocator(raw) {
2118
2512
  default: return;
2119
2513
  }
2120
2514
  }
2515
+ /**
2516
+ * Bridge a painter `data-story-key` value to the public Document API
2517
+ * {@link StoryLocator} shape. PREFERRED over {@link layoutStoryDatasetToStoryLocator}
2518
+ * for tracked changes: the run/marker element carries the real owning story in
2519
+ * `data-story-key` (`'body'`, `'fn:<noteId>'`, `'en:<noteId>'`,
2520
+ * `'hf:part:<refId>'`), while the layout fragment's `data-layout-story` falls
2521
+ * back to `'body'` for the footnote/endnote band — so reading only the layout
2522
+ * story loses the story identity needed to disambiguate repeated source ids.
2523
+ *
2524
+ * Split on the FIRST delimiter group only: the `fn:`/`en:` id and the `hf:part:`
2525
+ * refId are taken verbatim after their prefix, so an id that itself contains a
2526
+ * colon survives intact. The resulting shapes match what `storyLocatorSignature`
2527
+ * keys on and what the all-story `list({ in: 'all' })` rows carry. Body, empty,
2528
+ * and unknown keys return `undefined` so callers fall back to id-only matching
2529
+ * and body-only documents stay byte-for-byte unchanged.
2530
+ */
2121
2531
  function storyKeyToStoryLocator(raw) {
2122
2532
  if (typeof raw !== "string" || raw.length === 0 || raw === "body") return void 0;
2123
2533
  if (raw.startsWith("hf:part:")) {
@@ -2145,6 +2555,7 @@ function storyKeyToStoryLocator(raw) {
2145
2555
  } : void 0;
2146
2556
  }
2147
2557
  }
2558
+ /** Read a public `selectionTarget` off a content-control row when available. */
2148
2559
  function readSelectionTarget(row) {
2149
2560
  if (!row || typeof row !== "object") return null;
2150
2561
  const target = row.selectionTarget;
@@ -2166,12 +2577,19 @@ function collapseSelectionTargetToCaret(target) {
2166
2577
  end: start
2167
2578
  };
2168
2579
  }
2580
+ /** Read a stable public row id from discovery/list/detail shapes. */
2169
2581
  function readEntityId(row) {
2170
2582
  if (!row || typeof row !== "object") return null;
2171
2583
  const record = row;
2172
2584
  const id = record.id ?? record.commentId ?? record.changeId;
2173
2585
  return typeof id === "string" && id.length > 0 ? id : null;
2174
2586
  }
2587
+ /**
2588
+ * Read the tracked-change carrier shared by both ends of a collapsed host
2589
+ * selection. Unlike the public sync seed, this painted caret metadata is
2590
+ * available without a worker read and proves that the caret still belongs to
2591
+ * the same review carrier.
2592
+ */
2175
2593
  function readCollapsedHostTrackChangeId(snapshot) {
2176
2594
  if (!snapshot || typeof snapshot !== "object") return null;
2177
2595
  const record = snapshot;
@@ -2184,6 +2602,11 @@ function readCollapsedHostTrackChangeId(snapshot) {
2184
2602
  return typeof anchorId === "string" && anchorId.length > 0 && anchorId === focusId ? anchorId : null;
2185
2603
  }
2186
2604
  var trackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
2605
+ /**
2606
+ * Collect the raw source-provenance ids a tracked-change list item exposes,
2607
+ * reading from both the top-level row and its nested `change` payload (the
2608
+ * public projection surfaces provenance on either shape).
2609
+ */
2187
2610
  function readTrackChangeProvenanceIds(item) {
2188
2611
  const out = [];
2189
2612
  const push = (value) => {
@@ -2224,6 +2647,7 @@ function readTrackChangeProvenanceIds(item) {
2224
2647
  if (change && change !== item) scan(change);
2225
2648
  return out;
2226
2649
  }
2650
+ /** Build (or reuse) the alias↔canonical context for a tracked-change list slice. */
2227
2651
  function buildTrackedChangeIdContext(items) {
2228
2652
  const cached = trackedChangeIdContextCache.get(items);
2229
2653
  if (cached) return cached;
@@ -2256,6 +2680,19 @@ function buildTrackedChangeIdContext(items) {
2256
2680
  }
2257
2681
  var storyScopedTrackedChangeIdContextCache = /* @__PURE__ */ new WeakMap();
2258
2682
  var declaredBodyItemsCache = /* @__PURE__ */ new WeakMap();
2683
+ /**
2684
+ * Alias↔canonical context scoped to the rows in a single `story`.
2685
+ *
2686
+ * {@link buildTrackedChangeIdContext} keys aliases purely on the raw Word
2687
+ * `w:id` / `imported:<id>` painter form, so when body / footnote / header
2688
+ * changes reuse the same `w:id`, the first row inserted wins the alias for
2689
+ * EVERY story. A click in a later story would then map through
2690
+ * `toPublicId` to the first story's public id and get dropped by the
2691
+ * subsequent `entityRowMatchesRequest(..., story)` check (or focus the wrong
2692
+ * same-story split). Restricting the alias map to same-story candidates keeps
2693
+ * each story's ids independent so the reconciliation resolves within the
2694
+ * painted story. Falls back to id-only when the row carries no comparable story.
2695
+ */
2259
2696
  function buildStoryScopedTrackedChangeIdContext(items, story) {
2260
2697
  const signature = storyLocatorSignature(story);
2261
2698
  let byStory = storyScopedTrackedChangeIdContextCache.get(items);
@@ -2272,6 +2709,21 @@ function buildStoryScopedTrackedChangeIdContext(items, story) {
2272
2709
  function isTextboxStory(story) {
2273
2710
  return !!story && typeof story === "object" && story.storyType === "textbox";
2274
2711
  }
2712
+ /**
2713
+ * Resolve a textbox selection against rows that carry no story locator.
2714
+ *
2715
+ * Textbox rows arrive from the v2 adapter without a comparable locator, but body
2716
+ * rows omit theirs too (body is the documented default). A missing story is
2717
+ * therefore not proof of textbox ownership: when a textbox change and a body
2718
+ * change reuse one Word revision id, live selection can supply both canonical
2719
+ * ids, and taking the first story-less match would publish whichever the catalog
2720
+ * happened to list first.
2721
+ *
2722
+ * The signal is a shared revision id, not a count. A selection spanning several
2723
+ * independent changes in one textbox is ordinary — those ids describe different
2724
+ * revisions and the first is the right active one. Two ids tracing back to the
2725
+ * same revision id are the ambiguous case, and that fails closed.
2726
+ */
2275
2727
  function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
2276
2728
  const storyless = selectionIds.filter((id) => items.some((item) => readEntityId(item) === id && readEntityStory(item) == null));
2277
2729
  if (storyless.length === 0) return null;
@@ -2287,6 +2739,13 @@ function resolveStorylessCanonicalTrackedChangeId(items, selectionIds) {
2287
2739
  }
2288
2740
  return storyless[0];
2289
2741
  }
2742
+ /**
2743
+ * Keep only rows that explicitly declare a story locator.
2744
+ *
2745
+ * A missing locator is not the same claim as `{ storyType: 'body' }`, but both
2746
+ * share the body story signature. Filtering the undeclared rows out keeps a body
2747
+ * selection from aliasing onto one of them.
2748
+ */
2290
2749
  function declaredBodyItems(items) {
2291
2750
  const cached = declaredBodyItemsCache.get(items);
2292
2751
  if (cached) return cached;
@@ -2295,6 +2754,17 @@ function declaredBodyItems(items) {
2295
2754
  declaredBodyItemsCache.set(items, result);
2296
2755
  return result;
2297
2756
  }
2757
+ /**
2758
+ * Map `selection.activeChangeIds` to a public tracked-change id for
2759
+ * `TrackChangesSlice.activeId`.
2760
+ *
2761
+ * Prefer the live selection's story against the all-story catalog so a
2762
+ * footnote/header/footer alias that reuses a Word revision id cannot resolve
2763
+ * to a colliding body change. Unscoped body mapping is only for body
2764
+ * selection. While the all-story catalog is unsettled for a non-body
2765
+ * selection, keep activeId null rather than publishing an unvalidated raw
2766
+ * painter alias or canonical-looking id.
2767
+ */
2298
2768
  function resolveSelectionActiveChangeId(options) {
2299
2769
  const { selectionIds, publicIds, selection, bodyItems, allStoryItems } = options;
2300
2770
  if (selectionIds.length === 0) return null;
@@ -2319,11 +2789,20 @@ function resolveSelectionActiveChangeId(options) {
2319
2789
  if (isBodySelection) return mapIds(buildTrackedChangeIdContext(bodyItems)) ?? selectionIds[0] ?? null;
2320
2790
  return null;
2321
2791
  }
2792
+ /**
2793
+ * Resolve a requested comment activation id to its thread's canonical active
2794
+ * id. Accepts a bare `id`, the `importedId` v2 minted when it had to repair
2795
+ * a malformed/duplicated source comment id on import, or a reply's id - all
2796
+ * resolve to the same thread-root comment, matching main's accepted aliases
2797
+ * for `ui.comments.setActive`. Returns `null` when `commentId` matches no
2798
+ * currently loaded comment under either alias.
2799
+ */
2322
2800
  function resolveActiveCommentId(items, commentId) {
2323
2801
  const item = items.find((candidate) => readEntityId(candidate) === commentId || candidate.importedId === commentId);
2324
2802
  if (!item) return null;
2325
2803
  return item.rootCommentId ?? item.parentCommentId ?? readEntityId(item) ?? commentId;
2326
2804
  }
2805
+ /** Derive a `{ startBlockId, endBlockId }` block range from a selection slice. */
2327
2806
  function selectionBlockRange(selection) {
2328
2807
  const sel = selection.selectionTarget;
2329
2808
  if (sel) {
@@ -2346,6 +2825,16 @@ function selectionBlockRange(selection) {
2346
2825
  };
2347
2826
  return null;
2348
2827
  }
2828
+ /**
2829
+ * Distinct block ids the selection covers, in document order. In the v2 adapter
2830
+ * the selection's `blockId` is the same identifier the public block operations
2831
+ * accept as `nodeId` (both resolve to the OOXML `paraId`), so these are valid
2832
+ * `nodeId`s for `format.paragraph.*` / `styles.paragraph.*` / `lists.*`.
2833
+ *
2834
+ * A collapsed caret still yields its containing block id (the v2
2835
+ * `selection.current` returns a zero-length text segment), so paragraph/list
2836
+ * commands work from a caret — they do not require a range selection.
2837
+ */
2349
2838
  function selectionBlockIds(selection) {
2350
2839
  const ids = [];
2351
2840
  const push = (id) => {
@@ -2365,6 +2854,7 @@ var MAX_REACTIVE_SELECTION_BLOCK_READS = 64;
2365
2854
  function canProbeEverySelectedBlock(blockIds) {
2366
2855
  return blockIds.length <= MAX_REACTIVE_SELECTION_BLOCK_READS;
2367
2856
  }
2857
+ /** Resolve the selection's story, defaulting to the main body story. */
2368
2858
  function selectionStory(selection) {
2369
2859
  const target = selection.target;
2370
2860
  const explicit = selection.selectionTarget;
@@ -2376,6 +2866,7 @@ function selectionStory(selection) {
2376
2866
  storyType: "body"
2377
2867
  };
2378
2868
  }
2869
+ /** Build a `ParagraphTarget` ( `format.paragraph.*` / `styles.paragraph.*` ). */
2379
2870
  function paragraphTarget(blockId, story) {
2380
2871
  return {
2381
2872
  kind: "block",
@@ -2384,6 +2875,7 @@ function paragraphTarget(blockId, story) {
2384
2875
  ...story && typeof story === "object" ? { story } : {}
2385
2876
  };
2386
2877
  }
2878
+ /** Build a `ListsBlockTarget` ( `lists.apply` / `lists.remove` / `lists.getState` ). */
2387
2879
  function listsBlockTarget(blockId) {
2388
2880
  return {
2389
2881
  kind: "block",
@@ -2391,6 +2883,7 @@ function listsBlockTarget(blockId) {
2391
2883
  nodeId: blockId
2392
2884
  };
2393
2885
  }
2886
+ /** Build a `ListItemAddress` ( `lists.indent` / `lists.outdent` / `lists.applyStyle` ). */
2394
2887
  function listItemTarget(blockId, story) {
2395
2888
  return {
2396
2889
  kind: "block",
@@ -2428,6 +2921,7 @@ function readParagraphIndentationFromResult(result) {
2428
2921
  ...hanging != null ? { hanging } : {}
2429
2922
  };
2430
2923
  }
2924
+ /** Build one `TextAddress` per covered text segment for `hyperlinks.wrap`. */
2431
2925
  function selectionTextAddresses(selection) {
2432
2926
  const target = selection.target;
2433
2927
  const segments = target && Array.isArray(target.segments) ? target.segments : [];
@@ -2569,12 +3063,12 @@ function selectionTextSegments(selection) {
2569
3063
  const target = selection.target;
2570
3064
  const segments = target && Array.isArray(target.segments) ? target.segments : [];
2571
3065
  const out = [];
2572
- const push = (blockId, start$1, end$1) => {
2573
- if (typeof blockId !== "string" || typeof start$1 !== "number" || typeof end$1 !== "number") return;
3066
+ const push = (blockId, start, end) => {
3067
+ if (typeof blockId !== "string" || typeof start !== "number" || typeof end !== "number") return;
2574
3068
  out.push({
2575
3069
  blockId,
2576
- start: Math.min(start$1, end$1),
2577
- end: Math.max(start$1, end$1)
3070
+ start: Math.min(start, end),
3071
+ end: Math.max(start, end)
2578
3072
  });
2579
3073
  };
2580
3074
  for (const segment of segments) {
@@ -2614,6 +3108,12 @@ function selectionInlineValueSignature(selection) {
2614
3108
  if (segments.length === 0) return null;
2615
3109
  return [selectionInlineValueStorySignature(selection), ...segments.map((segment) => `${segment.blockId}:${segment.start}-${segment.end}`)].join("|");
2616
3110
  }
3111
+ /**
3112
+ * The effective-uniformity worker operation accepts the durable
3113
+ * `selectionTarget`, not the expanded segment list. Whole-story segment lists
3114
+ * grow as source coverage arrives, so using them as this cache key restarts the
3115
+ * same worker read indefinitely while the document is loading.
3116
+ */
2617
3117
  function selectionEffectiveUniformitySignature(selection) {
2618
3118
  const target = selection.selectionTarget;
2619
3119
  if (!target || typeof target !== "object") return null;
@@ -2656,6 +3156,11 @@ function sameSelectionTextSegments(left, right) {
2656
3156
  return candidate?.blockId === segment.blockId && candidate?.start === segment.start && candidate?.end === segment.end;
2657
3157
  });
2658
3158
  }
3159
+ /**
3160
+ * Pick the `query.match` result item that best covers the selection. Pure over
3161
+ * a settled query result so the controller's async read coordinator owns the
3162
+ * (promise-capable) `query.match` call and this stays a synchronous projection.
3163
+ */
2659
3164
  function pickSelectionTextQueryItem(result, selection) {
2660
3165
  const segments = selectionTextSegments(selection);
2661
3166
  if (segments.length === 0) return null;
@@ -2681,6 +3186,7 @@ function readProjectedInlineStyleValue(styles, key) {
2681
3186
  if (key === "color" || key === "highlight") return trimmed.toUpperCase();
2682
3187
  return trimmed;
2683
3188
  }
3189
+ /** Primary named family from a resolved CSS font stack, dropping generic fallbacks (SD-3652). */
2684
3190
  function normalizeLayoutFontFamily(value) {
2685
3191
  if (typeof value !== "string") return void 0;
2686
3192
  const first = value.split(",")[0]?.trim().replace(/^['"]+|['"]+$/g, "").trim();
@@ -2689,6 +3195,7 @@ function normalizeLayoutFontFamily(value) {
2689
3195
  if (lower === "serif" || lower === "sans-serif" || lower === "monospace" || lower === "cursive" || lower === "fantasy") return;
2690
3196
  return first;
2691
3197
  }
3198
+ /** Convert a resolved run font size in CSS px to the toolbar's point value (SD-3652). */
2692
3199
  function normalizeLayoutFontSizePt(value) {
2693
3200
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
2694
3201
  const pt = Math.round(value * .75 * 2) / 2;
@@ -2749,6 +3256,7 @@ function projectInlineValuesFromQueryItem(item, selection) {
2749
3256
  }
2750
3257
  return projection;
2751
3258
  }
3259
+ /** A create-location `at` derived from the current block, or `documentEnd`. */
2752
3260
  function createLocationAt(selection, mode) {
2753
3261
  const blockId = selectionBlockIds(selection)[0];
2754
3262
  if (!blockId) return { kind: "documentEnd" };
@@ -2810,6 +3318,12 @@ function commandResultSucceeded(result) {
2810
3318
  function isLooseObject(value) {
2811
3319
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2812
3320
  }
3321
+ /**
3322
+ * Flatten mounted projection blocks into text-bearing blocks (those with a
3323
+ * `runs` array), descending into table rows/cells so table-cell content is
3324
+ * reachable (SD-3652). A block carries either `runs` (paragraph) or `rows`
3325
+ * (table); nested tables recurse.
3326
+ */
2813
3327
  function collectProjectionTextBlocks(blocks) {
2814
3328
  const out = [];
2815
3329
  const visit = (node) => {
@@ -2829,6 +3343,7 @@ function collectProjectionTextBlocks(blocks) {
2829
3343
  if (Array.isArray(blocks)) for (const block of blocks) visit(block);
2830
3344
  return out;
2831
3345
  }
3346
+ /** A projection block matches a selection block id by `sourceAnchor.sourceNodeId` or `block.id`. */
2832
3347
  function projectionBlockMatchesId(block, id) {
2833
3348
  if (block.sourceAnchor?.sourceNodeId === id) return true;
2834
3349
  return block.id === id;
@@ -2877,6 +3392,7 @@ function hyperlinkTargetFromHref(href) {
2877
3392
  url: href
2878
3393
  };
2879
3394
  }
3395
+ /** Tracked-change decision spec for a command id, from the descriptor catalog. */
2880
3396
  function trackDecisionCommand(id) {
2881
3397
  return getCommandDescriptor(id)?.trackDecision ?? null;
2882
3398
  }
@@ -2888,6 +3404,7 @@ function rectResult(rects) {
2888
3404
  rect: rects[0]
2889
3405
  };
2890
3406
  }
3407
+ /** Fail-closed empty geometry result carrying a stable reason. */
2891
3408
  function rectFailure(reason) {
2892
3409
  return {
2893
3410
  found: false,
@@ -2896,6 +3413,7 @@ function rectFailure(reason) {
2896
3413
  reason
2897
3414
  };
2898
3415
  }
3416
+ /** Offset client-space rects so they are relative to `relativeTo`, when given. */
2899
3417
  function relativizeRects(result, relativeTo) {
2900
3418
  if (!relativeTo || typeof relativeTo.getBoundingClientRect !== "function" || result.rects.length === 0) return result;
2901
3419
  let origin;
@@ -2921,6 +3439,16 @@ function relativizeRects(result, relativeTo) {
2921
3439
  rect: rects[0]
2922
3440
  };
2923
3441
  }
3442
+ /**
3443
+ * Whether a text-target segment carries the addressable shape AND the value
3444
+ * invariants the derived selection target relies on (`blockId` string plus a
3445
+ * `range` of valid integer bounds, `start >= 0` and `start <= end` — the
3446
+ * documented `TextTarget` contract). Captures can be stale or deserialized from
3447
+ * a host store, so entries are validated instead of trusted: a segment that is
3448
+ * malformed (missing `range`) or out of bounds (negative, non-integer, or
3449
+ * inverted) makes the caller return `null` (fail closed) rather than throw
3450
+ * mid-derivation or restore a clamped, different range than was captured.
3451
+ */
2924
3452
  function isAddressableSegment(segment) {
2925
3453
  if (!segment || typeof segment !== "object") return false;
2926
3454
  const candidate = segment;
@@ -2956,6 +3484,15 @@ function selectionTargetForRestore(capture) {
2956
3484
  if (capture.selectionTarget) return capture.selectionTarget;
2957
3485
  return selectionTargetFromTextTarget(capture.target);
2958
3486
  }
3487
+ /**
3488
+ * Resolve the live selection slice to a `SelectionTarget` the inline
3489
+ * `format.*` operations accept. Returns the explicit `selectionTarget` when the
3490
+ * source provides one, otherwise derives a same-story selection target from the
3491
+ * resolved text target's first/last covered segments. Returns `null` when the
3492
+ * selection is empty or has no resolvable range — the inline command then fails closed with
3493
+ * `range-selection-required` rather than calling `format.*` with a missing
3494
+ * target (which the public Document API rejects with `INVALID_INPUT`).
3495
+ */
2959
3496
  function resolveInlineSelectionTarget(selection) {
2960
3497
  if (selection.empty) return null;
2961
3498
  if (selection.selectionTarget) return selection.selectionTarget;
@@ -2963,10 +3500,17 @@ function resolveInlineSelectionTarget(selection) {
2963
3500
  if (fallback && fallback.start.kind === "text" && fallback.end.kind === "text" && fallback.start.blockId !== fallback.end.blockId) return null;
2964
3501
  return fallback;
2965
3502
  }
3503
+ /**
3504
+ * The `format.*` method name a stored inline mark is keyed by (SD-3654). The
3505
+ * host store and the toggle command both address a mark by its Document API
3506
+ * method (`docRoute` minus the `format.` prefix), which differs from the inline
3507
+ * `key` for strikethrough (`key: 'strike'` vs `docRoute: 'format.strikethrough'`).
3508
+ */
2966
3509
  function inlineFormatMethod(descriptor) {
2967
3510
  if (!descriptor.inline || typeof descriptor.docRoute !== "string") return null;
2968
3511
  return descriptor.docRoute.startsWith("format.") ? descriptor.docRoute.slice(7) : null;
2969
3512
  }
3513
+ /** Inline run-property keys cleared by `clear-formatting` via `format.apply`. */
2970
3514
  var CLEAR_INLINE_PATCH = {
2971
3515
  bold: null,
2972
3516
  italic: null,
@@ -2982,7 +3526,22 @@ var CLEAR_INLINE_PATCH = {
2982
3526
  letterSpacing: null,
2983
3527
  dstrike: null
2984
3528
  };
3529
+ /**
3530
+ * Subset of `CLEAR_INLINE_PATCH` whose registry entry supports tracked
3531
+ * changes. Tracked `format.apply` rejects the whole patch with
3532
+ * `CAPABILITY_UNAVAILABLE` if any key isn't `tracked: true` (e.g. `smallCaps`,
3533
+ * `dstrike`), regardless of whether the selection actually carries them, so
3534
+ * suggesting mode must send this filtered patch instead of the full one.
3535
+ */
2985
3536
  var TRACKED_CLEAR_INLINE_PATCH = Object.fromEntries(Object.entries(CLEAR_INLINE_PATCH).filter(([key]) => INLINE_PROPERTY_BY_KEY[key]?.tracked === true));
3537
+ /**
3538
+ * Build the public Document API input for an inline-format command from the
3539
+ * resolved selection target, the normalized payload, and the command's live
3540
+ * active state. Boolean mark commands can also receive an explicit boolean/null
3541
+ * payload, which is useful for hosts that know the desired state themselves.
3542
+ * Returns `null` when the payload is invalid for the spec (so the controller
3543
+ * fails closed instead of forwarding a malformed value).
3544
+ */
2986
3545
  function buildInlineFormatInput(spec, target, payload, active) {
2987
3546
  switch (spec.kind) {
2988
3547
  case "toggle": {
@@ -3024,6 +3583,7 @@ function buildInlineFormatInput(spec, target, payload, active) {
3024
3583
  default: return null;
3025
3584
  }
3026
3585
  }
3586
+ /** Read a `{ start, end }` cell-range from the host snapshot, when well-formed. */
3027
3587
  function readMergeRange(range) {
3028
3588
  const point = (value) => {
3029
3589
  if (!value || typeof value !== "object") return null;
@@ -3046,6 +3606,12 @@ function readMergeRange(range) {
3046
3606
  end
3047
3607
  };
3048
3608
  }
3609
+ /**
3610
+ * Build the public `tables.*` input for a table cell-context command from the
3611
+ * resolved table context. Returns `null` when the action's required context is
3612
+ * missing (e.g. split with no resolved cell), so the controller fails closed
3613
+ * with `table-context-unavailable` rather than calling with a malformed locator.
3614
+ */
3049
3615
  function buildTableCommandInput(action, context) {
3050
3616
  const { tableNodeId: nodeId, rowIndex, columnIndex, cellNodeId, mergeRange } = context;
3051
3617
  switch (action) {
@@ -3134,18 +3700,22 @@ function createSuperDocUI(options) {
3134
3700
  let frozenProjectedValues = {};
3135
3701
  let frozenProjectedValuesKey = "";
3136
3702
  let heldSettledInlineValues = null;
3703
+ /** Read the live active editor (or null). */
3137
3704
  const getEditor = () => {
3138
3705
  const editor = superdoc?.activeEditor;
3139
3706
  return editor && typeof editor === "object" ? editor : null;
3140
3707
  };
3708
+ /** Read the live browser Document API facade (or null). */
3141
3709
  const getDoc = () => {
3142
3710
  const doc = getEditor()?.doc;
3143
3711
  return doc && typeof doc === "object" ? doc : null;
3144
3712
  };
3713
+ /** Read the live v2 tracked-change facade, when exposed. */
3145
3714
  const getV2TrackedChanges = () => {
3146
3715
  const trackedChanges = getEditor()?.v2TrackedChanges;
3147
3716
  return trackedChanges && typeof trackedChanges === "object" ? trackedChanges : null;
3148
3717
  };
3718
+ /** Read the live v2 editor host (inline mode only), when exposed. */
3149
3719
  const getHost = () => {
3150
3720
  const host = getEditor()?.host;
3151
3721
  return host && typeof host === "object" ? host : null;
@@ -3184,6 +3754,7 @@ function createSuperDocUI(options) {
3184
3754
  }
3185
3755
  return null;
3186
3756
  };
3757
+ /** Read the host's stored inline marks (SD-3654/SD-3652), when any pending. */
3187
3758
  const readPendingInlineFormat = () => {
3188
3759
  const host = getHost();
3189
3760
  const fn = host?.getPendingInlineFormat;
@@ -3191,6 +3762,11 @@ function createSuperDocUI(options) {
3191
3762
  const pending = safeCall(() => fn.call(host), null);
3192
3763
  return pending && typeof pending === "object" ? pending : null;
3193
3764
  };
3765
+ /**
3766
+ * Active state contributed by a stored inline mark for this command, or null
3767
+ * when nothing is pending for it (SD-3654). Only meaningful for a collapsed
3768
+ * caret; the store is cleared on any selection move, so a range never has one.
3769
+ */
3194
3770
  const pendingInlineActive = (descriptor) => {
3195
3771
  const method = inlineFormatMethod(descriptor);
3196
3772
  if (!method) return null;
@@ -3200,6 +3776,7 @@ function createSuperDocUI(options) {
3200
3776
  if (descriptor.inline?.kind === "toggle") return value === true;
3201
3777
  return value != null && value !== "";
3202
3778
  };
3779
+ /** The pending caret font/size value for this command's `format.*` method, if any (SD-3652). */
3203
3780
  const pendingInlineValueFor = (descriptor) => {
3204
3781
  const method = inlineFormatMethod(descriptor);
3205
3782
  if (!method) return void 0;
@@ -3209,6 +3786,7 @@ function createSuperDocUI(options) {
3209
3786
  if (typeof value === "string" && value !== "") return value;
3210
3787
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
3211
3788
  };
3789
+ /** Record / clear the stored inline mark for a collapsed-caret pick (SD-3654/SD-3652). */
3212
3790
  const setPendingInlineFormatOnHost = (method, value) => {
3213
3791
  const host = getHost();
3214
3792
  const fn = host?.setPendingInlineFormat;
@@ -3219,16 +3797,23 @@ function createSuperDocUI(options) {
3219
3797
  const fn = host?.clearPendingInlineFormat;
3220
3798
  if (typeof fn === "function") safeCall(() => fn.call(host, method), void 0);
3221
3799
  };
3222
- const BOOLEAN_INLINE_FORMAT_METHODS = new Set([
3800
+ /** Inline `format.*` methods whose active state is a boolean mark (name === method). */
3801
+ const BOOLEAN_INLINE_FORMAT_METHODS = /* @__PURE__ */ new Set([
3223
3802
  "bold",
3224
3803
  "italic",
3225
3804
  "underline",
3226
3805
  "strikethrough"
3227
3806
  ]);
3228
- const reconcilePendingInlineFormat = (selection$1) => {
3807
+ /**
3808
+ * Retire a stored inline mark (SD-3654/SD-3652) once the selection's own
3809
+ * formatting has caught up to it: a boolean mark when the live activeMarks
3810
+ * match, a font/size when the projection matches. Runs each recompute so the
3811
+ * toolbar hands off from "pending" to the real marks/value without a gap.
3812
+ */
3813
+ const reconcilePendingInlineFormat = (selection) => {
3229
3814
  const pending = readPendingInlineFormat();
3230
3815
  if (!pending) return;
3231
- const activeMarks = Array.isArray(selection$1.activeMarks) ? selection$1.activeMarks : [];
3816
+ const activeMarks = Array.isArray(selection.activeMarks) ? selection.activeMarks : [];
3232
3817
  let projected = null;
3233
3818
  for (const method of Object.keys(pending)) {
3234
3819
  const value = pending[method];
@@ -3236,33 +3821,47 @@ function createSuperDocUI(options) {
3236
3821
  if (BOOLEAN_INLINE_FORMAT_METHODS.has(method)) matched = activeMarks.includes(method) === (value === true);
3237
3822
  else if (isProjectedInlineSelectionValueKey(method)) {
3238
3823
  if (value == null) continue;
3239
- if (!projected) projected = projectSelectionInlineValues(selection$1);
3824
+ if (!projected) projected = projectSelectionInlineValues(selection);
3240
3825
  matched = projected[method] === value;
3241
3826
  } else matched = false;
3242
3827
  if (matched) clearPendingInlineFormatOnHost(method);
3243
3828
  }
3244
3829
  };
3830
+ /** Read the live v2 edit-command adapters (`activeEditor.editCommands`), when exposed. */
3245
3831
  const getEditCommands = () => {
3246
3832
  const editCommands = getEditor()?.editCommands;
3247
3833
  return editCommands && typeof editCommands === "object" ? editCommands : null;
3248
3834
  };
3835
+ /** Read one edit-command snapshot entry (`activeEditor.editCommands.getSnapshot().commands[id]`). */
3249
3836
  const readEditCommandStateEntry = (commandId) => {
3250
3837
  const editCommands = getEditCommands();
3251
3838
  if (!editCommands || typeof editCommands.getSnapshot !== "function") return null;
3252
3839
  const entry = (safeCall(() => editCommands.getSnapshot(), null)?.commands)?.[commandId];
3253
3840
  return entry && typeof entry === "object" ? entry : null;
3254
3841
  };
3842
+ /** Read the `lists.apply` command state surfaced by the v2 edit-command snapshot. */
3255
3843
  const readListApplyStateEntry = () => {
3256
3844
  return readEditCommandStateEntry("lists.apply");
3257
3845
  };
3846
+ /** Read the additive list active state surfaced by the edit-command snapshot. */
3258
3847
  const readListActiveSeed = (entry) => {
3259
3848
  const seed = (entry?.value)?.seed;
3260
3849
  return seed === "bullet" || seed === "ordered" ? seed : null;
3261
3850
  };
3851
+ /** Read the lower host-state entry mirrored into the public `undo` / `redo` ids. */
3262
3852
  const readMirroredEditCommandStateEntry = (commandId) => {
3263
3853
  const mirrored = MIRRORED_EDIT_COMMAND_IDS[commandId];
3264
3854
  return mirrored ? readEditCommandStateEntry(mirrored) : null;
3265
3855
  };
3856
+ /**
3857
+ * Shared table-context resolution. Reads the V2 host table-context facade
3858
+ * (`host.getTableContext()`) — the single public surface that resolves the
3859
+ * current selection's enclosing table without private editor internals — and
3860
+ * normalizes it to the locator inputs the `tables.*` operations need. Returns
3861
+ * `null` when the caret is not inside a table, the host does not expose the
3862
+ * facade, or the snapshot is missing the table node id / indices. Both the
3863
+ * built-in toolbar authority and custom UIs consume this same resolution.
3864
+ */
3266
3865
  const readHostTableContext = () => {
3267
3866
  const host = getHost();
3268
3867
  if (!host || typeof host.getTableContext !== "function") return null;
@@ -3329,11 +3928,11 @@ function createSuperDocUI(options) {
3329
3928
  if (typeof review.getActiveReviewTarget !== "function" || typeof review.clearActiveReviewTarget !== "function") return;
3330
3929
  if (safeCall(() => review.getActiveReviewTarget(), null)?.entityType === "trackedChange") safeCall(() => review.clearActiveReviewTarget(), void 0);
3331
3930
  };
3332
- const setExplicitActiveChange = (next, options$1) => {
3931
+ const setExplicitActiveChange = (next, options) => {
3333
3932
  explicitActiveChange = next;
3334
3933
  explicitActiveChangeRevision += 1;
3335
3934
  trackChangeRevealInvalidation += 1;
3336
- if (options$1?.invalidateQueuedNavigation !== false) queuedTrackChangeNavigationInvalidation += 1;
3935
+ if (options?.invalidateQueuedNavigation !== false) queuedTrackChangeNavigationInvalidation += 1;
3337
3936
  };
3338
3937
  const listeners = /* @__PURE__ */ new Set();
3339
3938
  let disposed = false;
@@ -3455,6 +4054,7 @@ function createSuperDocUI(options) {
3455
4054
  let allTrackedChangesResolvedToken = null;
3456
4055
  let selectionEpoch = 0;
3457
4056
  let lastCoordinatorEditor = null;
4057
+ /** Token shared by document-content reads (editor identity + mutation revision). */
3458
4058
  const contentToken = () => `${editorIdentityId(getEditor())}|m${documentMutationRevision}`;
3459
4059
  const clearPostDecisionTrackChanges = () => {
3460
4060
  postDecisionTrackChangesToken = null;
@@ -3531,6 +4131,14 @@ function createSuperDocUI(options) {
3531
4131
  };
3532
4132
  notifyPostDecisionTrackChanges(receipt);
3533
4133
  };
4134
+ /**
4135
+ * Publish the exact empty catalog proved by an all-resolved receipt.
4136
+ * Reject All does not carry one removed ref per logical change, so the
4137
+ * ordinary identity-pruning path cannot clear selection-derived focus or
4138
+ * the slice total. Keep this proof scoped to the current content token: the
4139
+ * next independent document mutation advances the token and resumes normal
4140
+ * catalog/selection reads.
4141
+ */
3534
4142
  const publishAllTrackedChangesResolved = (receipt) => {
3535
4143
  const previous = state.trackChanges;
3536
4144
  const changed = previous.items.length > 0 || previous.total !== 0 || previous.activeId !== null || previous.authors.length > 0;
@@ -3555,11 +4163,18 @@ function createSuperDocUI(options) {
3555
4163
  const newlyRemoved = new Set([...ids].filter((id) => !alreadyRemoved.has(id)));
3556
4164
  if (newlyRemoved.size === 0) return;
3557
4165
  postDecisionTrackChangesToken = token;
3558
- postDecisionTrackChangeIds = new Set([...alreadyRemoved, ...newlyRemoved]);
4166
+ postDecisionTrackChangeIds = /* @__PURE__ */ new Set([...alreadyRemoved, ...newlyRemoved]);
3559
4167
  applyPostDecisionTrackChangesToCache(postDecisionTrackChangeIds);
3560
4168
  publishPostDecisionTrackChanges(newlyRemoved, receipt);
3561
4169
  };
3562
4170
  const postDecisionTrackChangeIdsForToken = (token) => postDecisionTrackChangesToken === token && postDecisionTrackChangeIds.size > 0 ? postDecisionTrackChangeIds : null;
4171
+ /**
4172
+ * Merge a narrow tracked-change read into the two settled review caches.
4173
+ * Existing enriched adapter fields are retained while the fresh Document
4174
+ * API fields win. The entry keeps its original content token, so a row that
4175
+ * settles after the typing invalidation is truthfully served as stale rather
4176
+ * than discarded or mislabeled as a complete current-token catalog.
4177
+ */
3563
4178
  const mergeTrackedChangeItemsIntoCache = (items, orderedItems = []) => {
3564
4179
  const projected = items.map(projectTrackChangesItem).filter((item) => item != null && readEntityId(item) != null);
3565
4180
  if (projected.length === 0) return;
@@ -3627,22 +4242,22 @@ function createSuperDocUI(options) {
3627
4242
  inflightToken: null
3628
4243
  });
3629
4244
  };
3630
- const hydrateTrackedChanges = async (context, options$1) => {
4245
+ const hydrateTrackedChanges = async (context, options) => {
3631
4246
  try {
3632
4247
  const listTrackedChanges = context.v2TrackedChanges?.listTrackedChanges;
3633
4248
  if (typeof listTrackedChanges === "function") {
3634
- const result$1 = await Promise.resolve(listTrackedChanges.call(context.v2TrackedChanges, {
3635
- mode: options$1.trackedChangesListMode,
3636
- refreshReason: options$1.refreshReason,
3637
- blocking: options$1.blocking,
3638
- ...options$1.targetIds?.length ? { targetIds: options$1.targetIds } : {}
4249
+ const result = await Promise.resolve(listTrackedChanges.call(context.v2TrackedChanges, {
4250
+ mode: options.trackedChangesListMode,
4251
+ refreshReason: options.refreshReason,
4252
+ blocking: options.blocking,
4253
+ ...options.targetIds?.length ? { targetIds: options.targetIds } : {}
3639
4254
  }));
3640
- const ok = options$1.trackedChangesListMode === "all" && options$1.refreshReason === "mutation-history-render-reconcile" ? result$1?.ok === true : result$1?.ok !== false;
4255
+ const ok = options.trackedChangesListMode === "all" && options.refreshReason === "mutation-history-render-reconcile" ? result?.ok === true : result?.ok !== false;
3641
4256
  return {
3642
4257
  ok,
3643
- items: ok && Array.isArray(result$1?.items) ? result$1.items : [],
3644
- ...typeof result$1?.complete === "boolean" ? { complete: result$1.complete } : {},
3645
- ...typeof result$1?.sourceCoverageComplete === "boolean" ? { sourceCoverageComplete: result$1.sourceCoverageComplete } : {}
4258
+ items: ok && Array.isArray(result?.items) ? result.items : [],
4259
+ ...typeof result?.complete === "boolean" ? { complete: result.complete } : {},
4260
+ ...typeof result?.sourceCoverageComplete === "boolean" ? { sourceCoverageComplete: result.sourceCoverageComplete } : {}
3646
4261
  };
3647
4262
  }
3648
4263
  const list = context.docTrackChanges?.list;
@@ -3663,6 +4278,13 @@ function createSuperDocUI(options) {
3663
4278
  };
3664
4279
  }
3665
4280
  };
4281
+ /**
4282
+ * Read receipt-addressed review effects after their exact canonical paint.
4283
+ * The adapter's narrow get preserves its enriched row projection; the public
4284
+ * Document API is the fallback for hosts without that bridge method. Empty
4285
+ * successful reads remain unresolved and immediately use one bounded
4286
+ * interaction-prime list instead of waiting for the typing-idle catalog.
4287
+ */
3666
4288
  const reviewMutationReconciler = createV2ReviewMutationReconciler({
3667
4289
  getContext: () => {
3668
4290
  const editor = getEditor();
@@ -3706,12 +4328,12 @@ function createSuperDocUI(options) {
3706
4328
  const reads = await Promise.all(requestedIds.map(async (id) => {
3707
4329
  try {
3708
4330
  if (typeof getTrackedChange === "function") {
3709
- const result$1 = await Promise.resolve(getTrackedChange.call(context.v2TrackedChanges, id));
4331
+ const result = await Promise.resolve(getTrackedChange.call(context.v2TrackedChanges, id));
3710
4332
  return {
3711
4333
  id,
3712
- ok: result$1?.ok !== false,
3713
- items: result$1?.ok !== false && Array.isArray(result$1?.items) ? result$1.items : [],
3714
- result: result$1
4334
+ ok: result?.ok !== false,
4335
+ items: result?.ok !== false && Array.isArray(result?.items) ? result.items : [],
4336
+ result
3715
4337
  };
3716
4338
  }
3717
4339
  const result = await Promise.resolve(getRawTrackedChange.call(context.docTrackChanges, { id }));
@@ -3778,11 +4400,12 @@ function createSuperDocUI(options) {
3778
4400
  recompute();
3779
4401
  return;
3780
4402
  }
3781
- mergeTrackedChangeItemsIntoCache((result?.items ?? []).filter((item) => {
4403
+ const items = (result?.items ?? []).filter((item) => {
3782
4404
  const projected = projectTrackChangesItem(item);
3783
4405
  const id = projected == null ? null : readEntityId(projected);
3784
4406
  return id != null && ids.has(id);
3785
- }), result?.orderedItems ?? result?.items ?? []);
4407
+ });
4408
+ mergeTrackedChangeItemsIntoCache(items, result?.orderedItems ?? result?.items ?? []);
3786
4409
  recompute();
3787
4410
  }
3788
4411
  });
@@ -3796,10 +4419,12 @@ function createSuperDocUI(options) {
3796
4419
  reviewMutationReconciler.onRender(true);
3797
4420
  }, delayMs);
3798
4421
  };
4422
+ /** Token for the live selection read, including the mode that controls its public projection. */
3799
4423
  const selectionReadToken = () => `${contentToken()}|m${readDocumentMode()}|s${selectionEpoch}`;
3800
- const selectionSignature = (selection$1) => JSON.stringify({
3801
- t: selection$1.target ?? null,
3802
- s: selection$1.selectionTarget ?? null
4424
+ /** Stable signature of a settled selection, used to key selection-scoped reads. */
4425
+ const selectionSignature = (selection) => JSON.stringify({
4426
+ t: selection.target ?? null,
4427
+ s: selection.selectionTarget ?? null
3803
4428
  });
3804
4429
  const SELECTION_SCOPED_ASYNC_READ_PREFIXES = [
3805
4430
  "contentControls:inRange:",
@@ -3808,6 +4433,7 @@ function createSuperDocUI(options) {
3808
4433
  ];
3809
4434
  const FOREGROUND_ASYNC_RETRY_MS = 120;
3810
4435
  const COLD_ASYNC_READ_START_DELAY_MS = 180;
4436
+ /** Tri-state host source-load phase derived from the loading snapshot seam. */
3811
4437
  const hostSourceLoadPhase = () => {
3812
4438
  const host = getHost();
3813
4439
  const read = host?.getDocumentLoadingSnapshot;
@@ -3830,9 +4456,14 @@ function createSuperDocUI(options) {
3830
4456
  return Array.isArray(entry.value) && entry.value.length > 0;
3831
4457
  };
3832
4458
  const heavyReadDemandActive = (key, token) => demandedHeavyReads.get(key) === token;
4459
+ /**
4460
+ * Demand-driven route for the content-controls catalog (panel / API use).
4461
+ * Explicit demand bypasses both active-loading and post-complete idle holds.
4462
+ */
3833
4463
  const ensureContentControlsCatalog = (_reason) => {
3834
4464
  demandHeavyDocRead("contentControls");
3835
4465
  };
4466
+ /** Demand-driven route for an explicitly consumed tracked-change list. */
3836
4467
  const ensureTrackChangesCatalog = () => {
3837
4468
  const entry = asyncReads.get("trackChanges");
3838
4469
  if (entry?.hasSettled || entry?.inflightToken != null) return;
@@ -3842,9 +4473,21 @@ function createSuperDocUI(options) {
3842
4473
  const HEAVY_READ_IDLE_CEILING_MS = 8e3;
3843
4474
  const HEAVY_READ_IDLE_POLL_MS = 250;
3844
4475
  let lastEditableMutationAtMs = 0;
4476
+ /**
4477
+ * Last burst-class mutation: a local typing command or a remote apply whose
4478
+ * originating operation is unavailable. Drives the steady-phase heavy-read
4479
+ * hold while one-off local programmatic mutations keep refreshing promptly.
4480
+ */
3845
4481
  let lastTypingMutationAtMs = 0;
4482
+ /**
4483
+ * True from the first deferred heavy read until the input-idle release runs.
4484
+ * Held reads stay deferred even after source-complete so a typing-driven
4485
+ * recompute cannot bypass the idle gate.
4486
+ */
3846
4487
  let heavyReadsHeldUntilIdle = false;
4488
+ /** When the STEADY-phase typing hold began; bounds it to the same ceiling. */
3847
4489
  let heavyReadsTypingHoldSinceMs = 0;
4490
+ /** Lets one ceiling-triggered recompute refresh every heavy key as a group. */
3848
4491
  let heavyReadCeilingReleaseActive = false;
3849
4492
  let heavyReadCompletionRecomputeTimer = null;
3850
4493
  const scheduleHeavyReadCompletionRecompute = () => {
@@ -3904,15 +4547,15 @@ function createSuperDocUI(options) {
3904
4547
  const host = getEditor()?.host;
3905
4548
  const read = host?.getForegroundMutationState;
3906
4549
  if (typeof read !== "function") return null;
3907
- const state$1 = safeCall(() => read.call(host), null);
4550
+ const state = safeCall(() => read.call(host), null);
3908
4551
  return {
3909
- active: typeof state$1?.active === "number" ? state$1.active : 0,
3910
- pending: typeof state$1?.pending === "number" ? state$1.pending : 0
4552
+ active: typeof state?.active === "number" ? state.active : 0,
4553
+ pending: typeof state?.pending === "number" ? state.pending : 0
3911
4554
  };
3912
4555
  };
3913
4556
  const foregroundMutationActive = () => {
3914
- const state$1 = foregroundMutationState();
3915
- return Boolean(state$1 && (state$1.active > 0 || state$1.pending > 0));
4557
+ const state = foregroundMutationState();
4558
+ return Boolean(state && (state.active > 0 || state.pending > 0));
3916
4559
  };
3917
4560
  const scheduleForegroundAsyncRetry = (delayMs = FOREGROUND_ASYNC_RETRY_MS) => {
3918
4561
  if (disposed || foregroundAsyncRetryTimer) return;
@@ -3938,6 +4581,12 @@ function createSuperDocUI(options) {
3938
4581
  scheduleForegroundAsyncRetry(COLD_ASYNC_READ_START_DELAY_MS);
3939
4582
  return true;
3940
4583
  };
4584
+ /**
4585
+ * A large-selection uniformity read walks every covered paragraph. The first
4586
+ * settled selection reads immediately; later distinct selection keys debounce
4587
+ * so pointer-drag/autoscroll updates supersede one another before worker work
4588
+ * starts. Refreshes of the same selection stay immediate.
4589
+ */
3941
4590
  const shouldDeferEffectiveInlineRead = (key) => {
3942
4591
  if (lastEffectiveInlineReadKey == null || key === lastEffectiveInlineReadKey) {
3943
4592
  if (pendingEffectiveInlineRead?.timer) clearTimeout(pendingEffectiveInlineRead.timer);
@@ -4084,6 +4733,12 @@ function createSuperDocUI(options) {
4084
4733
  });
4085
4734
  return null;
4086
4735
  };
4736
+ /**
4737
+ * Read a doc value through the settled cache. Returns the best-known value
4738
+ * plus a {@link SliceStatus}: `ready` (settled for this token), `stale`
4739
+ * (older settled value while a refresh runs), or `pending` (nothing settled
4740
+ * yet). Never returns or surfaces a promise.
4741
+ */
4087
4742
  const readAsync = (key, token, run, normalize) => {
4088
4743
  const entry = asyncReads.get(key);
4089
4744
  if (entry && entry.token === token && entry.hasSettled) return {
@@ -4182,23 +4837,24 @@ function createSuperDocUI(options) {
4182
4837
  const clearAsyncReadKeys = (predicate) => {
4183
4838
  for (const key of asyncReads.keys()) if (predicate(key)) asyncReads.delete(key);
4184
4839
  };
4185
- const selectionScopedAsyncReadKeys = (selection$1) => {
4186
- const signature = selectionSignature(selection$1);
4187
- const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection$1);
4188
- return new Set([
4840
+ const selectionScopedAsyncReadKeys = (selection) => {
4841
+ const signature = selectionSignature(selection);
4842
+ const effectiveUniformitySignature = selectionEffectiveUniformitySignature(selection);
4843
+ return /* @__PURE__ */ new Set([
4189
4844
  `contentControls:inRange:${signature}`,
4190
4845
  `query:${signature}`,
4191
4846
  ...effectiveUniformitySignature ? [`effInline:${effectiveUniformitySignature}`] : []
4192
4847
  ]);
4193
4848
  };
4194
- const pruneSelectionScopedAsyncReads = (selection$1) => {
4195
- const retained = selectionScopedAsyncReadKeys(selection$1);
4849
+ const pruneSelectionScopedAsyncReads = (selection) => {
4850
+ const retained = selectionScopedAsyncReadKeys(selection);
4196
4851
  clearAsyncReadKeys((key) => SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => key.startsWith(prefix)) && !retained.has(key));
4197
4852
  for (const key of coldAsyncReadDeferrals) {
4198
4853
  const readKey = key.split("\0", 1)[0];
4199
4854
  if (SELECTION_SCOPED_ASYNC_READ_PREFIXES.some((prefix) => readKey.startsWith(prefix)) && !retained.has(readKey)) coldAsyncReadDeferrals.delete(key);
4200
4855
  }
4201
4856
  };
4857
+ /** Drop cached reads when the active editor identity changes (avoids cross-editor leakage). */
4202
4858
  const syncCoordinatorEditor = () => {
4203
4859
  const editor = getEditor();
4204
4860
  if (editor === lastCoordinatorEditor) return;
@@ -4228,12 +4884,20 @@ function createSuperDocUI(options) {
4228
4884
  clearPostDecisionTrackChanges();
4229
4885
  allTrackedChangesResolvedToken = null;
4230
4886
  };
4887
+ /** Bump the document-mutation revision so content reads re-fetch on the next compute. */
4231
4888
  const invalidateDocumentContent = () => {
4232
4889
  const carryAuthoritativeTrackChangesHold = authoritativeTrackChangesPendingToken !== null;
4233
4890
  documentMutationRevision += 1;
4234
4891
  if (carryAuthoritativeTrackChangesHold) authoritativeTrackChangesPendingToken = contentToken();
4235
4892
  clearPostDecisionTrackChanges();
4236
4893
  };
4894
+ /**
4895
+ * Refresh command-derived document slices without discarding an exact
4896
+ * all-resolved catalog received while the async command was settling. Host
4897
+ * mutation events arrive before the command promise resolves; a generic
4898
+ * invalidation at that later boundary would otherwise advance the cache
4899
+ * token and immediately re-list the catalog we already know is empty.
4900
+ */
4237
4901
  const invalidateAfterCommandSettlement = () => {
4238
4902
  const carryAllResolvedCatalog = allTrackedChangesResolvedToken === contentToken();
4239
4903
  invalidateDocumentContent();
@@ -4243,6 +4907,12 @@ function createSuperDocUI(options) {
4243
4907
  }
4244
4908
  recompute();
4245
4909
  };
4910
+ /**
4911
+ * Insert plain text through the public Document API. The narrow built-in /
4912
+ * custom insertion helper exposed on the custom-command callback context.
4913
+ * Fails closed (failure receipt) in viewing mode or when `doc.insert` is
4914
+ * unavailable — never reaching into `activeEditor.commands`.
4915
+ */
4246
4916
  const insertText = (text) => {
4247
4917
  if (typeof text !== "string" || text.length === 0) return failedReceipt("insertText requires a non-empty string.", "INVALID_INPUT");
4248
4918
  if (readDocumentMode() === "viewing") return failedReceipt("The document is read-only.", "DOCUMENT_READONLY");
@@ -4251,8 +4921,8 @@ function createSuperDocUI(options) {
4251
4921
  if (!doc || typeof op !== "function") return failedReceipt("insert is unavailable.");
4252
4922
  const currentSelection = doc.selection?.current;
4253
4923
  if (typeof currentSelection !== "function") return failedReceipt("selection.current is unavailable.");
4254
- const insertAtLiveSelection = (selection$2) => {
4255
- const record = selection$2 && typeof selection$2 === "object" ? selection$2 : null;
4924
+ const insertAtLiveSelection = (selection) => {
4925
+ const record = selection && typeof selection === "object" ? selection : null;
4256
4926
  const target = record?.selectionTarget ?? record?.target ?? null;
4257
4927
  if (!target || typeof target !== "object") return failedReceipt("insertText requires a live selection target.", "PRECONDITION_FAILED");
4258
4928
  return safeCall(() => settleWorkflowReceipt(op.call(doc, {
@@ -4261,15 +4931,16 @@ function createSuperDocUI(options) {
4261
4931
  target
4262
4932
  }), failedReceipt("insert failed.")), failedReceipt("insert failed."));
4263
4933
  };
4264
- let selection$1;
4934
+ let selection;
4265
4935
  try {
4266
- selection$1 = currentSelection.call(doc.selection);
4936
+ selection = currentSelection.call(doc.selection);
4267
4937
  } catch {
4268
4938
  return failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED");
4269
4939
  }
4270
- if (isPromiseLike(selection$1)) return Promise.resolve(selection$1).then(insertAtLiveSelection, () => failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED"));
4271
- return insertAtLiveSelection(selection$1);
4940
+ if (isPromiseLike(selection)) return Promise.resolve(selection).then(insertAtLiveSelection, () => failedReceipt("insertText could not resolve the live selection.", "PRECONDITION_FAILED"));
4941
+ return insertAtLiveSelection(selection);
4272
4942
  };
4943
+ /** Build the shared V2-truthful callback context for a custom command / button. */
4273
4944
  const buildCustomCommandContext = (payload, context) => ({
4274
4945
  payload,
4275
4946
  state,
@@ -4291,10 +4962,11 @@ function createSuperDocUI(options) {
4291
4962
  const editor = getEditor();
4292
4963
  const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
4293
4964
  const fromConfig = normalizeDocumentMode((superdocRecord?.config)?.documentMode);
4294
- const fromRuntime = normalizeDocumentMode(safeCall(typeof superdocRecord?.getActiveRuntime === "function" ? () => {
4965
+ const runtimeSnapshot = safeCall(typeof superdocRecord?.getActiveRuntime === "function" ? () => {
4295
4966
  const runtime = superdocRecord.getActiveRuntime();
4296
4967
  return typeof runtime?.getSnapshot === "function" ? runtime.getSnapshot() : null;
4297
- } : void 0, null)?.documentMode);
4968
+ } : void 0, null);
4969
+ const fromRuntime = normalizeDocumentMode(runtimeSnapshot?.documentMode);
4298
4970
  const fromOptions = normalizeDocumentMode((editor?.options)?.documentMode);
4299
4971
  if (fromConfig && fromRuntime && fromConfig !== fromRuntime) return fromConfig;
4300
4972
  return fromRuntime ?? fromConfig ?? fromOptions;
@@ -4303,15 +4975,20 @@ function createSuperDocUI(options) {
4303
4975
  const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
4304
4976
  const resolved = (superdocRecord?.interactionConfig)?.comments;
4305
4977
  if (resolved && typeof resolved.readOnly === "boolean") return resolved.readOnly === true;
4306
- const comments$1 = ((superdocRecord?.config)?.modules)?.comments;
4307
- return !!comments$1 && typeof comments$1 === "object" && comments$1.readOnly === true;
4308
- };
4978
+ const comments = ((superdocRecord?.config)?.modules)?.comments;
4979
+ return !!comments && typeof comments === "object" && comments.readOnly === true;
4980
+ };
4981
+ /**
4982
+ * Whether `interaction.comments.allowResolve` forbids changing a thread's
4983
+ * resolved state. Same precedence as `readOnly`: the resolved policy first,
4984
+ * the legacy block only when it is absent.
4985
+ */
4309
4986
  const resolveIsForbidden = () => {
4310
4987
  const superdocRecord = superdoc && typeof superdoc === "object" ? superdoc : null;
4311
4988
  const resolved = (superdocRecord?.interactionConfig)?.comments;
4312
4989
  if (resolved && typeof resolved.allowResolve === "boolean") return resolved.allowResolve === false;
4313
- const comments$1 = ((superdocRecord?.config)?.modules)?.comments;
4314
- return !!comments$1 && typeof comments$1 === "object" && comments$1.allowResolve === false;
4990
+ const comments = ((superdocRecord?.config)?.modules)?.comments;
4991
+ return !!comments && typeof comments === "object" && comments.allowResolve === false;
4315
4992
  };
4316
4993
  const reviewMutationsAreReadOnly = () => readDocumentMode() === "viewing" || commentsReadOnlyForReview();
4317
4994
  const normalizeSelectionInfo = (raw) => raw && typeof raw === "object" ? raw : null;
@@ -4345,6 +5022,7 @@ function createSuperDocUI(options) {
4345
5022
  const selectionApi = getDoc()?.selection;
4346
5023
  return selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0;
4347
5024
  };
5025
+ /** True when a host sync snapshot is a resolved collapsed caret (not a range). */
4348
5026
  const isCollapsedCaretSnapshot = (info) => {
4349
5027
  if (info.empty !== true) return false;
4350
5028
  const target = info.selectionTarget;
@@ -4352,6 +5030,21 @@ function createSuperDocUI(options) {
4352
5030
  const end = target?.end;
4353
5031
  return start?.kind === "text" && end?.kind === "text" && typeof start.blockId === "string" && start.blockId === end.blockId && typeof start.offset === "number" && start.offset === end.offset;
4354
5032
  };
5033
+ /**
5034
+ * On a caret move, seed the `selection` read cache synchronously from the
5035
+ * host's local snapshot so the toolbar reflects the NEW caret's block (and the
5036
+ * effective font resolved from it) on the next recompute, instead of serving
5037
+ * the previous selection until the worker `selection.current` round-trip lands
5038
+ * (SD-3652). Only a resolvable COLLAPSED CARET is seeded - a range's snapshot
5039
+ * cannot be resolved synchronously in worker mode, so ranges defer to the async
5040
+ * read (seeding an empty value would wrongly disable range-only commands). The
5041
+ * authoritative async read still overwrites marks/text/review overlap, but
5042
+ * foreground typing defers and coalesces that worker hop through the shared
5043
+ * retry gate. Previous marks are carried forward for toolbar continuity. A
5044
+ * tracked-change id is carried only when the host's painted caret proves the
5045
+ * same carrier, or while an active typing dispatch advances the same caret by
5046
+ * one code point. Pending work alone never carries review command context.
5047
+ */
4355
5048
  const seedCaretSelectionFromHost = (hostSelectionSnapshot) => {
4356
5049
  const host = getHost();
4357
5050
  const read = host?.readLiveSelectionSyncSnapshot;
@@ -4423,15 +5116,15 @@ function createSuperDocUI(options) {
4423
5116
  recompute();
4424
5117
  return state.selection;
4425
5118
  };
4426
- const computeComments = (selection$1) => {
5119
+ const computeComments = (selection) => {
4427
5120
  const commentsApi = getDoc()?.comments;
4428
5121
  const { value, status: listStatus } = readAsync("comments", contentToken(), () => commentsApi?.list ? commentsApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
4429
5122
  const items = value ?? [];
4430
- const activeIds = selection$1.activeCommentIds;
5123
+ const activeIds = selection.activeCommentIds;
4431
5124
  if (explicitActiveCommentId && !items.some((item) => readEntityId(item) === explicitActiveCommentId)) explicitActiveCommentId = null;
4432
5125
  const activeId = explicitActiveCommentId ?? activeIds[0] ?? null;
4433
5126
  return {
4434
- status: combineStatus(listStatus, selection$1.status),
5127
+ status: combineStatus(listStatus, selection.status),
4435
5128
  listStatus,
4436
5129
  items,
4437
5130
  total: items.length,
@@ -4461,7 +5154,7 @@ function createSuperDocUI(options) {
4461
5154
  });
4462
5155
  return status === "ready" ? value : null;
4463
5156
  };
4464
- const computeTrackChanges = (selection$1) => {
5157
+ const computeTrackChanges = (selection) => {
4465
5158
  const tcApi = getDoc()?.trackChanges;
4466
5159
  const v2TrackedChanges = getV2TrackedChanges();
4467
5160
  const listTrackedChanges = typeof v2TrackedChanges?.listTrackedChanges === "function" ? v2TrackedChanges.listTrackedChanges : null;
@@ -4494,51 +5187,58 @@ function createSuperDocUI(options) {
4494
5187
  } else if (!items.some((item) => readEntityId(item) === active.id)) setExplicitActiveChange(null);
4495
5188
  }
4496
5189
  const publicIdItems = allStoryItems ?? items;
4497
- const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection$1));
4498
- const selectionActiveChangeIds = allTrackedChangesResolvedToken === token ? [] : postDecisionIds ? selection$1.activeChangeIds.filter((id) => !postDecisionIds.has(id)) : selection$1.activeChangeIds;
5190
+ const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection));
5191
+ const selectionActiveChangeIds = allTrackedChangesResolvedToken === token ? [] : postDecisionIds ? selection.activeChangeIds.filter((id) => !postDecisionIds.has(id)) : selection.activeChangeIds;
4499
5192
  const selectionPublicChangeIds = selectionActiveChangeIds.map((id) => selectionIdContext.toPublicId(id) ?? id);
4500
5193
  const explicitActiveIdContext = explicitActiveChange?.story ? buildStoryScopedTrackedChangeIdContext(publicIdItems, explicitActiveChange.story) : buildTrackedChangeIdContext(publicIdItems);
4501
5194
  const explicitActiveId = explicitActiveChange ? explicitActiveIdContext.toPublicId(explicitActiveChange.id) ?? explicitActiveChange.id : null;
4502
5195
  const selectionActiveId = resolveSelectionActiveChangeId({
4503
5196
  selectionIds: selectionActiveChangeIds,
4504
5197
  publicIds: selectionPublicChangeIds,
4505
- selection: selection$1,
5198
+ selection,
4506
5199
  bodyItems: items,
4507
5200
  allStoryItems
4508
5201
  });
4509
5202
  const activeId = explicitActiveId ?? selectionActiveId;
4510
5203
  return {
4511
- status: combineStatus(listStatus, selection$1.status),
5204
+ status: combineStatus(listStatus, selection.status),
4512
5205
  items,
4513
5206
  total: items.length,
4514
5207
  activeId,
4515
5208
  authors: [...authors]
4516
5209
  };
4517
5210
  };
4518
- const computeContentControls = (selection$1) => {
5211
+ const computeContentControls = (selection) => {
4519
5212
  const ccApi = getDoc()?.contentControls;
4520
5213
  const { value, status: listStatus } = readAsync("contentControls", contentToken(), () => ccApi?.list ? ccApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
4521
5214
  const items = value ?? [];
4522
- const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection$1);
5215
+ const { ids: activeIds, status: rangeStatus } = computeActiveContentControlIds(ccApi, selection);
4523
5216
  return {
4524
- status: combineStatus(listStatus, rangeStatus, selection$1.status),
5217
+ status: combineStatus(listStatus, rangeStatus, selection.status),
4525
5218
  items,
4526
5219
  total: items.length,
4527
5220
  activeId: activeIds[0] ?? null,
4528
5221
  activeIds
4529
5222
  };
4530
5223
  };
4531
- const computeActiveContentControlLockModes = (ccApi, selection$1) => {
5224
+ /**
5225
+ * Lock mode of every content control overlapping the current selection's
5226
+ * block range, keyed by control id. Powers `contentControlLockReason` so the
5227
+ * toolbar can disable styling controls when the selection touches a
5228
+ * `contentLocked`/`sdtContentLocked` control (SD-3274) — block-range scoped,
5229
+ * same precision as `computeActiveContentControlIds` below.
5230
+ */
5231
+ const computeActiveContentControlLockModes = (ccApi, selection) => {
4532
5232
  if (!ccApi || typeof ccApi.listInRange !== "function") return {
4533
5233
  lockModesById: /* @__PURE__ */ new Map(),
4534
5234
  status: "ready"
4535
5235
  };
4536
- const range = selectionBlockRange(selection$1);
5236
+ const range = selectionBlockRange(selection);
4537
5237
  if (!range) return {
4538
5238
  lockModesById: /* @__PURE__ */ new Map(),
4539
5239
  status: "ready"
4540
5240
  };
4541
- const { value, status } = readAsync(`contentControls:inRange:${selectionSignature(selection$1)}`, contentToken(), () => ccApi.listInRange(range), (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
5241
+ const { value, status } = readAsync(`contentControls:inRange:${selectionSignature(selection)}`, contentToken(), () => ccApi.listInRange(range), (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
4542
5242
  const rows = value ?? [];
4543
5243
  const lockModesById = /* @__PURE__ */ new Map();
4544
5244
  for (const row of rows) if (typeof row?.id === "string") lockModesById.set(row.id, typeof row.lockMode === "string" ? row.lockMode : "unlocked");
@@ -4547,8 +5247,8 @@ function createSuperDocUI(options) {
4547
5247
  status
4548
5248
  };
4549
5249
  };
4550
- const computeActiveContentControlIds = (ccApi, selection$1) => {
4551
- const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection$1);
5250
+ const computeActiveContentControlIds = (ccApi, selection) => {
5251
+ const { lockModesById, status } = computeActiveContentControlLockModes(ccApi, selection);
4552
5252
  return {
4553
5253
  ids: [...lockModesById.keys()],
4554
5254
  status
@@ -4562,14 +5262,14 @@ function createSuperDocUI(options) {
4562
5262
  };
4563
5263
  };
4564
5264
  const computeZoom = () => {
4565
- const state$1 = safeCall(superdoc?.getZoomState ? () => superdoc.getZoomState() : void 0, null);
4566
- const value = typeof state$1?.value === "number" ? state$1.value : 100;
4567
- const rawMode = state$1?.mode;
5265
+ const state = safeCall(superdoc?.getZoomState ? () => superdoc.getZoomState() : void 0, null);
5266
+ const value = typeof state?.value === "number" ? state.value : 100;
5267
+ const rawMode = state?.mode;
4568
5268
  return {
4569
5269
  mode: rawMode === "manual" || rawMode === "fit-width" ? rawMode : rawMode === "fixed" ? "manual" : null,
4570
5270
  value,
4571
- min: typeof state$1?.min === "number" ? state$1.min : 10,
4572
- max: typeof state$1?.max === "number" ? state$1.max : 100
5271
+ min: typeof state?.min === "number" ? state.min : 10,
5272
+ max: typeof state?.max === "number" ? state.max : 100
4573
5273
  };
4574
5274
  };
4575
5275
  const readMeasurementUnit = () => {
@@ -4583,12 +5283,18 @@ function createSuperDocUI(options) {
4583
5283
  dirty: editor ? Boolean(editor.isDirty) : false
4584
5284
  };
4585
5285
  };
4586
- const selectionTextQueryRequest = (selection$1) => {
4587
- const pattern = selection$1.quotedText;
5286
+ /**
5287
+ * Resolve the `query.match` row covering the current selection through the
5288
+ * async read coordinator. The promise-capable browser `query.match` is read
5289
+ * via the cache (keyed by selection signature + content revision), so inline
5290
+ * value projection no longer collapses to empty when the read is async.
5291
+ */
5292
+ const selectionTextQueryRequest = (selection) => {
5293
+ const pattern = selection.quotedText;
4588
5294
  if (typeof pattern !== "string" || pattern.length === 0) return null;
4589
- const segments = selectionTextSegments(selection$1);
5295
+ const segments = selectionTextSegments(selection);
4590
5296
  if (segments.length === 0 || !canProbeEverySelectedBlock(segments.map((segment) => segment.blockId))) return null;
4591
- const target = selection$1.target;
5297
+ const target = selection.target;
4592
5298
  const within = segments.length === 1 ? paragraphTarget(segments[0].blockId, target?.story) : void 0;
4593
5299
  return {
4594
5300
  select: {
@@ -4601,24 +5307,30 @@ function createSuperDocUI(options) {
4601
5307
  require: "any"
4602
5308
  };
4603
5309
  };
4604
- const resolveSelectionTextQueryItem = (selection$1) => {
5310
+ const resolveSelectionTextQueryItem = (selection) => {
4605
5311
  const query = getDoc()?.query;
4606
5312
  if (typeof query?.match !== "function") return null;
4607
- const request = selectionTextQueryRequest(selection$1);
5313
+ const request = selectionTextQueryRequest(selection);
4608
5314
  if (!request) return null;
4609
- const { value } = readAsync(`query:${selectionSignature(selection$1)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
4610
- return pickSelectionTextQueryItem(value, selection$1);
4611
- };
4612
- const resolveEffectiveInlineValuesFromLayout = (selection$1) => {
5315
+ const { value } = readAsync(`query:${selectionSignature(selection)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
5316
+ return pickSelectionTextQueryItem(value, selection);
5317
+ };
5318
+ /**
5319
+ * Effective (cascade-resolved) font family / size for the selection, read from
5320
+ * the mounted layout and matched by source node id (SD-3652). The Document API
5321
+ * query surfaces only DIRECT run properties, so inherited fonts have no
5322
+ * projected value; the layout has already resolved them for painting.
5323
+ */
5324
+ const resolveEffectiveInlineValuesFromLayout = (selection) => {
4613
5325
  const host = getHost();
4614
- const blockIds = new Set(selectionBlockIds(selection$1));
5326
+ const blockIds = new Set(selectionBlockIds(selection));
4615
5327
  if (blockIds.size === 0) return {};
4616
5328
  const readByIds = host?.readMountedProjectionBlocksByIds;
4617
5329
  const readAll = host?.readMountedProjectionBlocks;
4618
5330
  if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
4619
5331
  const blocks = safeCall(() => {
4620
5332
  if (typeof readByIds !== "function") return readAll.call(host);
4621
- const story = selectionStoryLocator(selection$1);
5333
+ const story = selectionStoryLocator(selection);
4622
5334
  return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
4623
5335
  }, null);
4624
5336
  if (!Array.isArray(blocks)) return {};
@@ -4633,7 +5345,7 @@ function createSuperDocUI(options) {
4633
5345
  if (typeof run.highlight === "string" && run.highlight.trim() !== "") out.highlight = run.highlight.trim().toUpperCase();
4634
5346
  return out;
4635
5347
  };
4636
- const caret = collapsedTextAddressFromSelection(selection$1);
5348
+ const caret = collapsedTextAddressFromSelection(selection);
4637
5349
  const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
4638
5350
  if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
4639
5351
  const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
@@ -4657,7 +5369,7 @@ function createSuperDocUI(options) {
4657
5369
  return runFontValues(chosen);
4658
5370
  }
4659
5371
  }
4660
- const segments = selectionTextSegments(selection$1);
5372
+ const segments = selectionTextSegments(selection);
4661
5373
  if (segments.length > 0) {
4662
5374
  const rangeFamilies = /* @__PURE__ */ new Set();
4663
5375
  const rangeSizes = /* @__PURE__ */ new Set();
@@ -4687,10 +5399,10 @@ function createSuperDocUI(options) {
4687
5399
  }
4688
5400
  }
4689
5401
  if (offsetSafe && rangeSawRun) {
4690
- const projection$1 = {};
4691
- if (rangeFamilies.size === 1) projection$1.fontFamily = [...rangeFamilies][0];
4692
- if (rangeSizes.size === 1) projection$1.fontSize = [...rangeSizes][0];
4693
- return projection$1;
5402
+ const projection = {};
5403
+ if (rangeFamilies.size === 1) projection.fontFamily = [...rangeFamilies][0];
5404
+ if (rangeSizes.size === 1) projection.fontSize = [...rangeSizes][0];
5405
+ return projection;
4694
5406
  }
4695
5407
  }
4696
5408
  const families = /* @__PURE__ */ new Set();
@@ -4713,16 +5425,29 @@ function createSuperDocUI(options) {
4713
5425
  if (sizes.size === 1) projection.fontSize = [...sizes][0];
4714
5426
  return projection;
4715
5427
  };
4716
- const resolveEffectiveMarkValuesFromLayout = (selection$1) => {
5428
+ /**
5429
+ * Effective (cascade-resolved) toggle-mark state read from the mounted
5430
+ * projection (SD-3860). `run.bold`/`run.italic`/`run.strike`/`run.underline`
5431
+ * on a projected run are already the FINAL merged value (style cascade +
5432
+ * any direct rPr override) — the same values `DomPainter` paints with — so
5433
+ * unlike the raw-rPr-only `selection.activeMarks` scan, this correctly
5434
+ * reports `false` for an explicit `<w:b w:val="0"/>` override even when a
5435
+ * paragraph/table style says bold. A key is left `undefined` when the
5436
+ * covered runs disagree (genuine mixed selection) or nothing could be read,
5437
+ * so the caller falls through to the worker-side uniformity read rather
5438
+ * than guessing. Mounted-projection `underline` is an object (`{}` /
5439
+ * `{ style: 'single' }`) when active, never a bare `true` — check presence.
5440
+ */
5441
+ const resolveEffectiveMarkValuesFromLayout = (selection) => {
4717
5442
  const host = getHost();
4718
- const blockIds = new Set(selectionBlockIds(selection$1));
5443
+ const blockIds = new Set(selectionBlockIds(selection));
4719
5444
  if (blockIds.size === 0) return {};
4720
5445
  const readByIds = host?.readMountedProjectionBlocksByIds;
4721
5446
  const readAll = host?.readMountedProjectionBlocks;
4722
5447
  if (typeof readByIds !== "function" && typeof readAll !== "function") return {};
4723
5448
  const blocks = safeCall(() => {
4724
5449
  if (typeof readByIds !== "function") return readAll.call(host);
4725
- const story = selectionStoryLocator(selection$1);
5450
+ const story = selectionStoryLocator(selection);
4726
5451
  return story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]);
4727
5452
  }, null);
4728
5453
  if (!Array.isArray(blocks)) return {};
@@ -4741,7 +5466,7 @@ function createSuperDocUI(options) {
4741
5466
  }
4742
5467
  return out;
4743
5468
  };
4744
- const caret = collapsedTextAddressFromSelection(selection$1);
5469
+ const caret = collapsedTextAddressFromSelection(selection);
4745
5470
  const caretOffset = caret && typeof caret.range?.start === "number" ? caret.range.start : null;
4746
5471
  if (caret && caretOffset !== null && blockIds.has(caret.blockId)) {
4747
5472
  const block = flatBlocks.find((b) => projectionBlockMatchesId(b, caret.blockId));
@@ -4765,7 +5490,7 @@ function createSuperDocUI(options) {
4765
5490
  return runMarkValues(chosen);
4766
5491
  }
4767
5492
  }
4768
- const segments = selectionTextSegments(selection$1);
5493
+ const segments = selectionTextSegments(selection);
4769
5494
  if (segments.length > 0) {
4770
5495
  const rangeSets = {
4771
5496
  bold: /* @__PURE__ */ new Set(),
@@ -4819,12 +5544,12 @@ function createSuperDocUI(options) {
4819
5544
  if (!sawWholeRun) return {};
4820
5545
  return collapseMarkSets(wholeSets);
4821
5546
  };
4822
- const completeProjectedInlineValues = (selection$1, direct) => {
5547
+ const completeProjectedInlineValues = (selection, direct) => {
4823
5548
  if (direct.fontFamily !== void 0 && direct.fontSize !== void 0 && direct.color !== void 0 && direct.highlight !== void 0) return {
4824
5549
  values: direct,
4825
5550
  effectiveUniformityStatus: "ready"
4826
5551
  };
4827
- const resolved = resolveEffectiveInlineValuesFromLayout(selection$1);
5552
+ const resolved = resolveEffectiveInlineValuesFromLayout(selection);
4828
5553
  const combined = {
4829
5554
  ...resolved.fontFamily !== void 0 ? { fontFamily: resolved.fontFamily } : {},
4830
5555
  ...resolved.fontSize !== void 0 ? { fontSize: resolved.fontSize } : {},
@@ -4832,8 +5557,8 @@ function createSuperDocUI(options) {
4832
5557
  ...resolved.highlight !== void 0 ? { highlight: resolved.highlight } : {},
4833
5558
  ...direct
4834
5559
  };
4835
- if (!selection$1.empty && (combined.fontFamily === void 0 || combined.fontSize === void 0)) {
4836
- const uniformity = resolveEffectiveInlineUniformityValues(selection$1);
5560
+ if (!selection.empty && (combined.fontFamily === void 0 || combined.fontSize === void 0)) {
5561
+ const uniformity = resolveEffectiveInlineUniformityValues(selection);
4837
5562
  if (uniformity.values) {
4838
5563
  if (combined.fontFamily === void 0 && uniformity.values.fontFamily !== void 0) combined.fontFamily = uniformity.values.fontFamily;
4839
5564
  if (combined.fontSize === void 0 && uniformity.values.fontSize !== void 0) combined.fontSize = uniformity.values.fontSize;
@@ -4848,6 +5573,13 @@ function createSuperDocUI(options) {
4848
5573
  effectiveUniformityStatus: "ready"
4849
5574
  };
4850
5575
  };
5576
+ /**
5577
+ * Cached async access to the INTERNAL `format.readEffectiveInlineUniformity`
5578
+ * read (duck-typed; absent on hosts that do not provide it). Returns only
5579
+ * settled UNIFORM values - mixed and unresolvable keys stay undefined so the
5580
+ * toolbar renders its mixed/blank state, and a pending read serves nothing
5581
+ * (the held-value logic covers the transition).
5582
+ */
4851
5583
  const EFFECTIVE_INLINE_UNIFORMITY_KEYS = [
4852
5584
  "fontFamily",
4853
5585
  "fontSize",
@@ -4856,8 +5588,8 @@ function createSuperDocUI(options) {
4856
5588
  "underline",
4857
5589
  "strikethrough"
4858
5590
  ];
4859
- const readEffectiveInlineUniformityCached = (selection$1) => {
4860
- const target = selection$1.selectionTarget;
5591
+ const readEffectiveInlineUniformityCached = (selection) => {
5592
+ const target = selection.selectionTarget;
4861
5593
  if (!target) return {
4862
5594
  value: null,
4863
5595
  status: "ready"
@@ -4867,14 +5599,15 @@ function createSuperDocUI(options) {
4867
5599
  value: null,
4868
5600
  status: "ready"
4869
5601
  };
4870
- return readAsync(`effInline:${selectionEffectiveUniformitySignature(selection$1) ?? selectionSignature(selection$1)}`, contentToken(), () => op({
5602
+ const signature = selectionEffectiveUniformitySignature(selection) ?? selectionSignature(selection);
5603
+ return readAsync(`effInline:${signature}`, contentToken(), () => op({
4871
5604
  target,
4872
5605
  offsetSpace: "selection",
4873
5606
  keys: EFFECTIVE_INLINE_UNIFORMITY_KEYS
4874
5607
  }), (raw) => raw && typeof raw === "object" ? raw : null);
4875
5608
  };
4876
- const resolveEffectiveInlineUniformityValues = (selection$1) => {
4877
- const { value, status } = readEffectiveInlineUniformityCached(selection$1);
5609
+ const resolveEffectiveInlineUniformityValues = (selection) => {
5610
+ const { value, status } = readEffectiveInlineUniformityCached(selection);
4878
5611
  if (!value || value.success !== true) return {
4879
5612
  values: null,
4880
5613
  status
@@ -4893,8 +5626,9 @@ function createSuperDocUI(options) {
4893
5626
  status
4894
5627
  };
4895
5628
  };
4896
- const resolveEffectiveMarkUniformityValues = (selection$1) => {
4897
- const { value, status } = readEffectiveInlineUniformityCached(selection$1);
5629
+ /** Worker-side cascade-resolved mark uniformity, for selections whose tail isn't mounted (SD-3860). */
5630
+ const resolveEffectiveMarkUniformityValues = (selection) => {
5631
+ const { value, status } = readEffectiveInlineUniformityCached(selection);
4898
5632
  if (!value || value.success !== true) return {
4899
5633
  values: null,
4900
5634
  status
@@ -4910,18 +5644,25 @@ function createSuperDocUI(options) {
4910
5644
  status
4911
5645
  };
4912
5646
  };
4913
- const effectiveMarkActiveState = (descriptor, selection$1) => {
5647
+ /**
5648
+ * Effective active state for a toggle-mark command (bold/italic/underline/
5649
+ * strikethrough), SD-3860: the mounted-layout read is tried first (fast,
5650
+ * synchronous), then the worker uniformity read for unmounted content.
5651
+ * Returns `null` (no opinion) only when neither source has resolved yet,
5652
+ * in which case the caller falls back to the raw direct-only check.
5653
+ */
5654
+ const effectiveMarkActiveState = (descriptor, selection) => {
4914
5655
  const mark = descriptor.activeMark;
4915
5656
  if (!mark || !EFFECTIVE_MARK_KEYS.includes(mark)) return null;
4916
5657
  const key = mark;
4917
- const fromLayout = resolveEffectiveMarkValuesFromLayout(selection$1)[key];
5658
+ const fromLayout = resolveEffectiveMarkValuesFromLayout(selection)[key];
4918
5659
  if (fromLayout !== void 0) return fromLayout;
4919
- const { values } = resolveEffectiveMarkUniformityValues(selection$1);
5660
+ const { values } = resolveEffectiveMarkUniformityValues(selection);
4920
5661
  const fromWorker = values?.[key];
4921
5662
  return fromWorker !== void 0 ? fromWorker : null;
4922
5663
  };
4923
- const readEffectiveInlineUniformityNow = async (selection$1) => {
4924
- const target = selection$1.selectionTarget;
5664
+ const readEffectiveInlineUniformityNow = async (selection) => {
5665
+ const target = selection.selectionTarget;
4925
5666
  if (!target) return null;
4926
5667
  const op = resolveDocOperation(getDoc(), "format.readEffectiveInlineUniformity");
4927
5668
  if (!op) return null;
@@ -4944,23 +5685,30 @@ function createSuperDocUI(options) {
4944
5685
  else delete projected[key];
4945
5686
  }
4946
5687
  };
4947
- const projectSelectionInlineValuesWithStatus = (selection$1) => completeProjectedInlineValues(selection$1, projectInlineValuesFromQueryItem(resolveSelectionTextQueryItem(selection$1), selection$1));
4948
- const projectSelectionInlineValues = (selection$1) => projectSelectionInlineValuesWithStatus(selection$1).values;
4949
- const readFormatPainterInlineValues = async (selection$1) => {
5688
+ const projectSelectionInlineValuesWithStatus = (selection) => completeProjectedInlineValues(selection, projectInlineValuesFromQueryItem(resolveSelectionTextQueryItem(selection), selection));
5689
+ const projectSelectionInlineValues = (selection) => projectSelectionInlineValuesWithStatus(selection).values;
5690
+ /**
5691
+ * Read command-critical inline values for format-painter capture. Reactive
5692
+ * command state intentionally serves stale values while its worker read is
5693
+ * refreshing, but arming the painter must snapshot the mutation that just
5694
+ * settled. A direct, bounded query here makes the async capture authoritative
5695
+ * without changing the non-blocking toolbar snapshot policy.
5696
+ */
5697
+ const readFormatPainterInlineValues = async (selection) => {
4950
5698
  const query = getDoc()?.query;
4951
- const request = selectionTextQueryRequest(selection$1);
4952
- if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection$1);
5699
+ const request = selectionTextQueryRequest(selection);
5700
+ if (typeof query?.match !== "function" || !request) return projectSelectionInlineValues(selection);
4953
5701
  try {
4954
5702
  const raw = await Promise.resolve(query.match(request));
4955
- const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection$1), selection$1);
4956
- const completed = completeProjectedInlineValues(selection$1, direct);
5703
+ const direct = projectInlineValuesFromQueryItem(pickSelectionTextQueryItem(raw && typeof raw === "object" ? raw : null, selection), selection);
5704
+ const completed = completeProjectedInlineValues(selection, direct);
4957
5705
  if (completed.effectiveUniformityStatus !== "ready" && (direct.fontFamily === void 0 || direct.fontSize === void 0)) {
4958
- const effective = await readEffectiveInlineUniformityNow(selection$1);
5706
+ const effective = await readEffectiveInlineUniformityNow(selection);
4959
5707
  if (effective) applyEffectiveInlineUniformity(completed.values, direct, effective);
4960
5708
  }
4961
5709
  return completed.values;
4962
5710
  } catch {
4963
- return projectSelectionInlineValues(selection$1);
5711
+ return projectSelectionInlineValues(selection);
4964
5712
  }
4965
5713
  };
4966
5714
  const readBlockNode = async (address) => {
@@ -4972,21 +5720,35 @@ function createSuperDocUI(options) {
4972
5720
  nodeType: address["nodeType"]
4973
5721
  }));
4974
5722
  };
4975
- const selectionQueryStatus = (selection$1) => {
5723
+ /**
5724
+ * Returns the settled status of the query.match read backing the current
5725
+ * selection's inline-value projection. Called after resolveSelectionTextQueryItem
5726
+ * so readAsync always finds an existing cache entry — this is a status-only lookup.
5727
+ */
5728
+ const selectionQueryStatus = (selection) => {
4976
5729
  const query = getDoc()?.query;
4977
5730
  if (typeof query?.match !== "function") return "ready";
4978
- const request = selectionTextQueryRequest(selection$1);
5731
+ const request = selectionTextQueryRequest(selection);
4979
5732
  if (!request) return "ready";
4980
- const { status } = readAsync(`query:${selectionSignature(selection$1)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
5733
+ const { status } = readAsync(`query:${selectionSignature(selection)}`, contentToken(), () => query.match(request), (raw) => raw && typeof raw === "object" ? raw : null);
4981
5734
  return status;
4982
5735
  };
4983
- const projectInlineValuesWithSettledHold = (selection$1) => {
4984
- const projection = projectSelectionInlineValuesWithStatus(selection$1);
5736
+ /**
5737
+ * Last fully-settled inline projection, keyed by the selection's inline
5738
+ * signature (content-token-free: the SAME selection across content
5739
+ * revisions keeps its key). While a refresh of the same selection is
5740
+ * pending/stale, settled values fill keys the in-flight projection has not
5741
+ * resolved yet, so the toolbar font field does not flicker blank mid-edit.
5742
+ * A NEW selection never reads a previous selection's held values, and a
5743
+ * SETTLED mixed selection overwrites the hold (blank is then correct).
5744
+ */
5745
+ const projectInlineValuesWithSettledHold = (selection) => {
5746
+ const projection = projectSelectionInlineValuesWithStatus(selection);
4985
5747
  const projected = projection.values;
4986
- if (selection$1.empty) return projection;
4987
- const signature = selectionInlineValueSignature(selection$1);
5748
+ if (selection.empty) return projection;
5749
+ const signature = selectionInlineValueSignature(selection);
4988
5750
  if (!signature) return projection;
4989
- if (selection$1.status === "ready" && selectionQueryStatus(selection$1) === "ready" && projection.effectiveUniformityStatus === "ready") {
5751
+ if (selection.status === "ready" && selectionQueryStatus(selection) === "ready" && projection.effectiveUniformityStatus === "ready") {
4990
5752
  heldSettledInlineValues = {
4991
5753
  key: signature,
4992
5754
  values: projected
@@ -5002,39 +5764,56 @@ function createSuperDocUI(options) {
5002
5764
  };
5003
5765
  return projection;
5004
5766
  };
5005
- const computeCommandStates = (selection$1) => {
5767
+ const computeCommandStates = (selection) => {
5006
5768
  const doc = getDoc();
5007
- const projectedInlineRead = projectInlineValuesWithSettledHold(selection$1);
5769
+ const projectedInlineRead = projectInlineValuesWithSettledHold(selection);
5008
5770
  const projectedInlineValues = projectedInlineRead.values;
5009
- if (selection$1.status === "ready" && !selection$1.empty && selectionQueryStatus(selection$1) === "ready" && projectedInlineRead.effectiveUniformityStatus === "ready") {
5771
+ if (selection.status === "ready" && !selection.empty && selectionQueryStatus(selection) === "ready" && projectedInlineRead.effectiveUniformityStatus === "ready") {
5010
5772
  frozenProjectedValues = projectedInlineValues;
5011
- frozenProjectedValuesKey = `${selectionKey(selection$1)}:${contentToken()}`;
5773
+ frozenProjectedValuesKey = `${selectionKey(selection)}:${contentToken()}`;
5012
5774
  }
5013
5775
  const ccApi = doc?.contentControls;
5014
- const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection$1);
5776
+ const { lockModesById } = computeActiveContentControlLockModes(ccApi, selection);
5015
5777
  const states = {};
5016
- for (const id of allCommandIds()) states[id] = computeCommandState(id, doc, selection$1, projectedInlineValues, lockModesById);
5778
+ for (const id of allCommandIds()) states[id] = computeCommandState(id, doc, selection, projectedInlineValues, lockModesById);
5017
5779
  return states;
5018
5780
  };
5781
+ /** Stable reason a routed command cannot reach its Document API operation. */
5019
5782
  const unavailableRouteReason = (doc) => {
5020
5783
  if (doc) return SUPERDOC_UI_REASONS.operationUnavailable;
5021
5784
  return getEditor() ? SUPERDOC_UI_REASONS.documentApiUnavailable : SUPERDOC_UI_REASONS.notReady;
5022
5785
  };
5786
+ /**
5787
+ * Lock modes that block run-level styling mutations (bold/italic/font/etc.),
5788
+ * mirroring the content-mutation axis the content-controls adapter's
5789
+ * `guardContentUnlocked` enforces server-side (`contentLocked` /
5790
+ * `sdtContentLocked`). `sdtLocked` protects only the wrapper, not styling.
5791
+ */
5023
5792
  const blocksContentStyling = (lockMode) => lockMode === "contentLocked" || lockMode === "sdtContentLocked";
5793
+ /**
5794
+ * Stable reason an inline (run-level) styling command is disabled because the
5795
+ * selection overlaps a content control whose lock forbids styling it — Word
5796
+ * parity (SD-3274): SuperDoc must not leave styling controls clickable when
5797
+ * the mutation cannot apply. Block-range scoped (same precision as
5798
+ * `computeActiveContentControlLockModes`), so this can over-disable when an
5799
+ * unrelated selection shares a block with a locked control — accepted for
5800
+ * now; alignment/paragraph-level commands are never gated here.
5801
+ */
5024
5802
  const contentControlLockReason = (lockModesById) => {
5025
5803
  for (const lockMode of lockModesById.values()) if (blocksContentStyling(lockMode)) return SUPERDOC_UI_REASONS.contentControlLocked;
5026
5804
  };
5027
- const trackDecisionReason = (command, supported, readonly, selection$1, doc) => {
5805
+ /** Stable reason a tracked-change decision command is disabled, or undefined when enabled. */
5806
+ const trackDecisionReason = (command, supported, readonly, selection, doc) => {
5028
5807
  if (!supported) return command.scope === "all" ? SUPERDOC_UI_REASONS.bulkDecisionsDisabled : unavailableRouteReason(doc);
5029
5808
  if (readonly) return SUPERDOC_UI_REASONS.documentReadonly;
5030
- if (command.scope !== "all" && selection$1.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
5809
+ if (command.scope !== "all" && selection.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
5031
5810
  };
5032
5811
  const hostCommandSupport = (command) => {
5033
5812
  const host = getHost();
5034
- const commands$1 = safeCall(typeof host?.getCapabilities === "function" ? () => host.getCapabilities() : void 0, null)?.editableSubset?.commands;
5035
- if (Array.isArray(commands$1)) return commands$1.find((entry) => entry?.command === command || entry?.id === command) ?? null;
5036
- if (commands$1 && typeof commands$1 === "object") {
5037
- const record = commands$1[command];
5813
+ const commands = safeCall(typeof host?.getCapabilities === "function" ? () => host.getCapabilities() : void 0, null)?.editableSubset?.commands;
5814
+ if (Array.isArray(commands)) return commands.find((entry) => entry?.command === command || entry?.id === command) ?? null;
5815
+ if (commands && typeof commands === "object") {
5816
+ const record = commands[command];
5038
5817
  return record && typeof record === "object" ? record : null;
5039
5818
  }
5040
5819
  return null;
@@ -5046,7 +5825,7 @@ function createSuperDocUI(options) {
5046
5825
  if (support && (support.status === "supported" || support.enabled === true)) return void 0;
5047
5826
  return SUPERDOC_UI_REASONS.bulkDecisionsDisabled;
5048
5827
  };
5049
- const computeCommandState = (id, doc, selection$1, projectedInlineValues, lockModesById = /* @__PURE__ */ new Map()) => {
5828
+ const computeCommandState = (id, doc, selection, projectedInlineValues, lockModesById = /* @__PURE__ */ new Map()) => {
5050
5829
  if (customCommands.has(id)) return normalizeCommandState({
5051
5830
  supported: true,
5052
5831
  enabled: true,
@@ -5073,7 +5852,7 @@ function createSuperDocUI(options) {
5073
5852
  const supportsSingle = typeof tcApi?.decide === "function" || typeof tcApi?.[trackCommand.kind] === "function";
5074
5853
  const supportsAll = typeof tcApi?.decide === "function" || typeof tcApi?.[`${trackCommand.kind}All`] === "function";
5075
5854
  const supported = trackCommand.scope === "all" ? supportsAll : supportsSingle;
5076
- const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection$1, doc);
5855
+ const reason = bulkTrackDecisionBlockedReason(trackCommand, tcApi) ?? trackDecisionReason(trackCommand, supported, reviewMutationsAreReadOnly(), selection, doc);
5077
5856
  return normalizeCommandState({
5078
5857
  enabled: reason == null,
5079
5858
  active: false,
@@ -5128,7 +5907,7 @@ function createSuperDocUI(options) {
5128
5907
  supported: true,
5129
5908
  reason: lockReason
5130
5909
  }, "builtin");
5131
- const enabled = selectionBlockIds(selection$1).length > 0 || resolveInlineSelectionTarget(selection$1) != null;
5910
+ const enabled = selectionBlockIds(selection).length > 0 || resolveInlineSelectionTarget(selection) != null;
5132
5911
  return normalizeCommandState({
5133
5912
  enabled,
5134
5913
  active: false,
@@ -5171,10 +5950,10 @@ function createSuperDocUI(options) {
5171
5950
  reason
5172
5951
  }, "builtin");
5173
5952
  }
5174
- const active = commandActiveState(descriptor, doc, selection$1);
5175
- const value = routedCommandValue(descriptor, doc, selection$1, projectedInlineValues);
5176
- if (descriptor.list?.mode === "indent" || descriptor.list?.mode === "outdent") return computeHybridIndentCommandState(descriptor, doc, readonly, selection$1, active, value);
5177
- if (descriptor.list?.mode === "toggle-seed") return computeListToggleCommandState(descriptor, doc, readonly, selection$1, active, value);
5953
+ const active = commandActiveState(descriptor, doc, selection);
5954
+ const value = routedCommandValue(descriptor, doc, selection, projectedInlineValues);
5955
+ if (descriptor.list?.mode === "indent" || descriptor.list?.mode === "outdent") return computeHybridIndentCommandState(descriptor, doc, readonly, selection, active, value);
5956
+ if (descriptor.list?.mode === "toggle-seed") return computeListToggleCommandState(descriptor, doc, readonly, selection, active, value);
5178
5957
  if (descriptor.mutates && readonly) return normalizeCommandState({
5179
5958
  enabled: false,
5180
5959
  active,
@@ -5182,7 +5961,7 @@ function createSuperDocUI(options) {
5182
5961
  value,
5183
5962
  reason: SUPERDOC_UI_REASONS.documentReadonly
5184
5963
  }, "builtin");
5185
- if (descriptor.inline && !resolveInlineSelectionTarget(selection$1)) {
5964
+ if (descriptor.inline && !resolveInlineSelectionTarget(selection)) {
5186
5965
  const lockReason = contentControlLockReason(lockModesById);
5187
5966
  if (lockReason) return normalizeCommandState({
5188
5967
  enabled: false,
@@ -5191,7 +5970,7 @@ function createSuperDocUI(options) {
5191
5970
  value,
5192
5971
  reason: lockReason
5193
5972
  }, "builtin");
5194
- return normalizeCommandState(selectionBlockIds(selection$1).length > 0 && typeof getHost()?.setPendingInlineFormat === "function" ? {
5973
+ return normalizeCommandState(selectionBlockIds(selection).length > 0 && typeof getHost()?.setPendingInlineFormat === "function" ? {
5195
5974
  enabled: true,
5196
5975
  active,
5197
5976
  supported: true,
@@ -5204,14 +5983,14 @@ function createSuperDocUI(options) {
5204
5983
  reason: SUPERDOC_UI_REASONS.rangeSelectionRequired
5205
5984
  }, "builtin");
5206
5985
  }
5207
- if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection$1).length === 0) return normalizeCommandState({
5986
+ if ((descriptor.blockParagraph || descriptor.list) && selectionBlockIds(selection).length === 0) return normalizeCommandState({
5208
5987
  enabled: false,
5209
5988
  active,
5210
5989
  supported: true,
5211
5990
  value,
5212
5991
  reason: SUPERDOC_UI_REASONS.selectionRequired
5213
5992
  }, "builtin");
5214
- if (descriptor.link && !active && selectionTextAddresses(selection$1).length === 0 && !collapsedTextAddressFromSelection(selection$1)) return normalizeCommandState({
5993
+ if (descriptor.link && !active && selectionTextAddresses(selection).length === 0 && !collapsedTextAddressFromSelection(selection)) return normalizeCommandState({
5215
5994
  enabled: false,
5216
5995
  active,
5217
5996
  supported: true,
@@ -5234,7 +6013,7 @@ function createSuperDocUI(options) {
5234
6013
  value
5235
6014
  }, "builtin");
5236
6015
  };
5237
- function hyperlinkOverlapsSelection(link, selection$1) {
6016
+ function hyperlinkOverlapsSelection(link, selection) {
5238
6017
  const anchor = link.address?.anchor;
5239
6018
  const start = anchor?.start;
5240
6019
  const end = anchor?.end;
@@ -5244,7 +6023,7 @@ function createSuperDocUI(options) {
5244
6023
  const linkStart = typeof start?.offset === "number" ? start.offset : null;
5245
6024
  const linkEnd = typeof end?.offset === "number" ? end.offset : null;
5246
6025
  if (linkStart == null || linkEnd == null) return false;
5247
- const target = selection$1.target;
6026
+ const target = selection.target;
5248
6027
  const segments = target && Array.isArray(target.segments) ? target.segments : [];
5249
6028
  for (const segment of segments) {
5250
6029
  const segmentBlockId = typeof segment?.blockId === "string" ? segment.blockId : null;
@@ -5267,10 +6046,15 @@ function createSuperDocUI(options) {
5267
6046
  }
5268
6047
  return false;
5269
6048
  }
5270
- const resolveCurrentHyperlink = (doc, selection$1) => {
6049
+ /**
6050
+ * Resolve the hyperlink overlapping the current selection/caret via
6051
+ * `hyperlinks.list({ within })` scoped to the selection's covered blocks.
6052
+ * Returns the first overlapping list row (`{ address, properties }`) or null.
6053
+ */
6054
+ const resolveCurrentHyperlink = (doc, selection) => {
5271
6055
  const linksApi = doc?.hyperlinks;
5272
6056
  if (!linksApi || typeof linksApi.list !== "function") return null;
5273
- const blockIds = selectionBlockIds(selection$1);
6057
+ const blockIds = selectionBlockIds(selection);
5274
6058
  if (!canProbeEverySelectedBlock(blockIds)) return null;
5275
6059
  for (const blockId of blockIds) {
5276
6060
  const within = {
@@ -5279,44 +6063,52 @@ function createSuperDocUI(options) {
5279
6063
  nodeId: blockId
5280
6064
  };
5281
6065
  const { value: result } = readAsync(`hyperlinks:${blockId}`, contentToken(), () => linksApi.list({ within }), (raw) => raw && typeof raw === "object" ? raw : null);
5282
- const hit = (result && Array.isArray(result.items) ? result.items : []).find((item) => hyperlinkOverlapsSelection(item, selection$1));
6066
+ const hit = (result && Array.isArray(result.items) ? result.items : []).find((item) => hyperlinkOverlapsSelection(item, selection));
5283
6067
  if (hit) return hit;
5284
6068
  }
5285
6069
  return null;
5286
6070
  };
5287
- const commandActiveState = (descriptor, doc, selection$1) => {
5288
- if (descriptor.link) return resolveCurrentHyperlink(doc, selection$1) != null;
5289
- if (descriptor.list?.mode === "toggle-seed" && descriptor.list.seed) return readListSeed(doc, selection$1) === descriptor.list.seed;
6071
+ /**
6072
+ * Active state for a routed command. Links resolve by public hyperlink
6073
+ * address overlap; list commands read the current block's list state and
6074
+ * match the seeded kind. Other inline marks use the selection mark set when
6075
+ * the host exposes it.
6076
+ */
6077
+ const commandActiveState = (descriptor, doc, selection) => {
6078
+ if (descriptor.link) return resolveCurrentHyperlink(doc, selection) != null;
6079
+ if (descriptor.list?.mode === "toggle-seed" && descriptor.list.seed) return readListSeed(doc, selection) === descriptor.list.seed;
5290
6080
  const pending = pendingInlineActive(descriptor);
5291
6081
  if (pending !== null) return pending;
5292
6082
  const optimistic = optimisticInlineToggles.get(descriptor.id);
5293
- const selectionSignature$1 = selectionInlineValueSignature(selection$1);
5294
- if (selectionSignature$1 && optimistic?.selectionSignature === selectionSignature$1) return optimistic.active;
5295
- const effective = effectiveMarkActiveState(descriptor, selection$1);
6083
+ const selectionSignature = selectionInlineValueSignature(selection);
6084
+ if (selectionSignature && optimistic?.selectionSignature === selectionSignature) return optimistic.active;
6085
+ const effective = effectiveMarkActiveState(descriptor, selection);
5296
6086
  if (effective !== null) return effective;
5297
- return commandIsActive(descriptor, selection$1);
6087
+ return commandIsActive(descriptor, selection);
5298
6088
  };
5299
- const routedCommandValue = (descriptor, doc, selection$1, projectedInlineValues) => {
6089
+ /** Live `value` for a routed command, when modeled (current paragraph style / link href). */
6090
+ const routedCommandValue = (descriptor, doc, selection, projectedInlineValues) => {
5300
6091
  if (descriptor.inline && isProjectedInlineSelectionValueKey(descriptor.inline.key)) {
5301
6092
  const pending = pendingInlineValueFor(descriptor);
5302
6093
  if (pending !== void 0) return pending;
5303
6094
  const projected = projectedInlineValues[descriptor.inline.key];
5304
6095
  if (projected !== void 0) return projected;
5305
- const signature = selectionInlineValueSignature(selection$1);
6096
+ const signature = selectionInlineValueSignature(selection);
5306
6097
  const cached = signature ? optimisticInlineValues.get(descriptor.inline.key) : void 0;
5307
6098
  if (signature && cached?.selectionSignature === signature) return cached.value;
5308
6099
  }
5309
6100
  if (descriptor.id === "linked-style") {
5310
- const { style: active } = computeActiveParagraphStyle(selection$1, getStyleCatalog().cache);
6101
+ const { style: active } = computeActiveParagraphStyle(selection, getStyleCatalog().cache);
5311
6102
  if (!active.styleId || active.mixed) return void 0;
5312
6103
  return {
5313
6104
  styleId: active.styleId,
5314
6105
  styleName: active.styleName
5315
6106
  };
5316
6107
  }
5317
- if (descriptor.id === "text-align") return readToolbarParagraphAlignment(doc, selection$1);
5318
- if (descriptor.id === "link") return readActiveLinkHref(doc, selection$1) ?? void 0;
6108
+ if (descriptor.id === "text-align") return readToolbarParagraphAlignment(doc, selection);
6109
+ if (descriptor.id === "link") return readActiveLinkHref(doc, selection) ?? void 0;
5319
6110
  };
6111
+ /** Read one block's list state and cache status through the selected story's list-state seam. */
5320
6112
  const readListStateSnapshotForBlock = (doc, blockId, story) => {
5321
6113
  const listsApi = doc?.lists;
5322
6114
  if (!listsApi) return {
@@ -5338,14 +6130,22 @@ function createSuperDocUI(options) {
5338
6130
  status
5339
6131
  };
5340
6132
  };
6133
+ /** Read one block's settled list state through the selected story's list-state seam. */
5341
6134
  const readListStateForBlock = (doc, blockId, story) => {
5342
6135
  return readListStateSnapshotForBlock(doc, blockId, story).value;
5343
6136
  };
6137
+ /** Read one block's list seed (`'bullet'` / `'ordered'`) in the selected story. */
5344
6138
  const readListSeedForBlock = (doc, blockId, story) => {
5345
6139
  const result = readListStateForBlock(doc, blockId, story);
5346
6140
  if (!result || result.isListItem !== true) return null;
5347
6141
  return result.seed === "bullet" || result.seed === "ordered" ? result.seed : null;
5348
6142
  };
6143
+ /**
6144
+ * Read whether a block is a list item. `null` means the authoritative state is
6145
+ * unavailable or has not settled yet; it must never be interpreted as a plain
6146
+ * paragraph. A list item may legitimately have no bullet/ordered seed (for
6147
+ * example, a linked Word numbering definition).
6148
+ */
5349
6149
  const readListMembershipSnapshotForBlock = (doc, blockId, story) => {
5350
6150
  const snapshot = readListStateSnapshotForBlock(doc, blockId, story);
5351
6151
  const result = snapshot.value;
@@ -5354,15 +6154,17 @@ function createSuperDocUI(options) {
5354
6154
  status: snapshot.status
5355
6155
  };
5356
6156
  };
5357
- const readListSeed = (doc, selection$1) => {
5358
- const blockIds = selectionBlockIds(selection$1);
6157
+ /** Read a uniform list seed across the covered blocks, or null when mixed / non-list. */
6158
+ const readListSeed = (doc, selection) => {
6159
+ const blockIds = selectionBlockIds(selection);
5359
6160
  if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return null;
5360
- const story = selectionStory(selection$1);
6161
+ const story = selectionStory(selection);
5361
6162
  const firstSeed = readListSeedForBlock(doc, blockIds[0], story);
5362
6163
  if (firstSeed !== "bullet" && firstSeed !== "ordered") return null;
5363
6164
  for (const blockId of blockIds.slice(1)) if (readListSeedForBlock(doc, blockId, story) !== firstSeed) return null;
5364
6165
  return firstSeed;
5365
6166
  };
6167
+ /** Read a paragraph node in its story through the async read coordinator. */
5366
6168
  const readNodeById = (doc, blockId, story) => {
5367
6169
  if (!doc) return {
5368
6170
  value: null,
@@ -5399,11 +6201,12 @@ function createSuperDocUI(options) {
5399
6201
  const isProjectionResolvedParagraphAlignment = (value) => {
5400
6202
  return value === "start" || value === "end" || value === "distributed" || value === "numTab" || value === "lowKashida" || value === "mediumKashida" || value === "highKashida" || value === "thaiDistribute";
5401
6203
  };
5402
- const readEffectiveParagraphAlignments = (selection$1, blockIds) => {
6204
+ /** Read effective paragraph alignments from the mounted, style-resolved layout. */
6205
+ const readEffectiveParagraphAlignments = (selection, blockIds) => {
5403
6206
  const host = getHost();
5404
6207
  const readByIds = host?.readMountedProjectionBlocksByIds;
5405
6208
  if (typeof readByIds !== "function") return null;
5406
- const story = selectionStoryLocator(selection$1);
6209
+ const story = selectionStoryLocator(selection);
5407
6210
  const blocks = safeCall(() => story ? readByIds.call(host, [...blockIds], story) : readByIds.call(host, [...blockIds]), null);
5408
6211
  if (!Array.isArray(blocks)) return /* @__PURE__ */ new Map();
5409
6212
  const projectionBlocks = collectProjectionTextBlocks(blocks);
@@ -5418,6 +6221,7 @@ function createSuperDocUI(options) {
5418
6221
  }
5419
6222
  return alignments;
5420
6223
  };
6224
+ /** Read public paragraph alignment, then fall back to the resolved projection. */
5421
6225
  const readParagraphAlignment = (doc, blockId, story, effectiveAlignments) => {
5422
6226
  const { value: result, status, refreshing } = readNodeById(doc, blockId, story);
5423
6227
  const effective = effectiveAlignments?.get(blockId);
@@ -5454,11 +6258,12 @@ function createSuperDocUI(options) {
5454
6258
  };
5455
6259
  return { status: "unavailable" };
5456
6260
  };
5457
- const readUniformParagraphAlignment = (doc, selection$1) => {
5458
- const blockIds = selectionBlockIds(selection$1);
6261
+ /** Resolve a uniform effective alignment across every selected paragraph. */
6262
+ const readUniformParagraphAlignment = (doc, selection) => {
6263
+ const blockIds = selectionBlockIds(selection);
5459
6264
  if (blockIds.length === 0 || !canProbeEverySelectedBlock(blockIds)) return { status: "unavailable" };
5460
- const story = selectionStory(selection$1);
5461
- const effectiveAlignments = readEffectiveParagraphAlignments(selection$1, blockIds);
6265
+ const story = selectionStory(selection);
6266
+ const effectiveAlignments = readEffectiveParagraphAlignments(selection, blockIds);
5462
6267
  const first = readParagraphAlignment(doc, blockIds[0], story, effectiveAlignments);
5463
6268
  if (first.status !== "uniform") return first;
5464
6269
  for (const blockId of blockIds.slice(1)) {
@@ -5468,22 +6273,23 @@ function createSuperDocUI(options) {
5468
6273
  }
5469
6274
  return first;
5470
6275
  };
5471
- const paragraphAlignmentSelectionSignature = (selection$1) => {
5472
- const blockIds = selectionBlockIds(selection$1);
6276
+ const paragraphAlignmentSelectionSignature = (selection) => {
6277
+ const blockIds = selectionBlockIds(selection);
5473
6278
  if (blockIds.length === 0) return null;
5474
- return `${storyLocatorSignature(selectionStory(selection$1))}:${blockIds.join(",")}`;
6279
+ return `${storyLocatorSignature(selectionStory(selection))}:${blockIds.join(",")}`;
5475
6280
  };
5476
- const readToolbarParagraphAlignment = (doc, selection$1) => {
5477
- const resolution = readUniformParagraphAlignment(doc, selection$1);
6281
+ /** Keep a toolbar pick stable until its painted paragraph state is readable. */
6282
+ const readToolbarParagraphAlignment = (doc, selection) => {
6283
+ const resolution = readUniformParagraphAlignment(doc, selection);
5478
6284
  const resolved = resolution.status === "uniform" ? resolution.value : void 0;
5479
6285
  const optimistic = optimisticParagraphAlignment;
5480
6286
  if (!optimistic) return resolved;
5481
- const selectionSignature$1 = paragraphAlignmentSelectionSignature(selection$1);
5482
- if (selectionSignature$1 && selectionSignature$1 !== optimistic.selectionSignature) {
6287
+ const selectionSignature = paragraphAlignmentSelectionSignature(selection);
6288
+ if (selectionSignature && selectionSignature !== optimistic.selectionSignature) {
5483
6289
  optimisticParagraphAlignment = null;
5484
6290
  return resolved;
5485
6291
  }
5486
- if (!selectionSignature$1) return optimistic.value;
6292
+ if (!selectionSignature) return optimistic.value;
5487
6293
  if (!optimistic.settled) return optimistic.value;
5488
6294
  if (resolution.status !== "pending") {
5489
6295
  optimisticParagraphAlignment = null;
@@ -5491,14 +6297,14 @@ function createSuperDocUI(options) {
5491
6297
  }
5492
6298
  return optimistic.value;
5493
6299
  };
5494
- const armOptimisticParagraphAlignment = (selection$1, payload) => {
6300
+ const armOptimisticParagraphAlignment = (selection, payload) => {
5495
6301
  const alignment = normalizeParagraphAlignment(payload);
5496
- const selectionSignature$1 = paragraphAlignmentSelectionSignature(selection$1);
5497
- const blockIds = selectionBlockIds(selection$1);
5498
- if (!alignment || !selectionSignature$1 || blockIds.length === 0) return null;
6302
+ const selectionSignature = paragraphAlignmentSelectionSignature(selection);
6303
+ const blockIds = selectionBlockIds(selection);
6304
+ if (!alignment || !selectionSignature || blockIds.length === 0) return null;
5499
6305
  const generation = ++optimisticParagraphAlignmentGeneration;
5500
6306
  optimisticParagraphAlignment = {
5501
- selectionSignature: selectionSignature$1,
6307
+ selectionSignature,
5502
6308
  value: alignment,
5503
6309
  generation,
5504
6310
  settled: false,
@@ -5521,6 +6327,16 @@ function createSuperDocUI(options) {
5521
6327
  }
5522
6328
  optimistic.settled = true;
5523
6329
  };
6330
+ /**
6331
+ * Await a block's list membership authoritatively. Membership comes from
6332
+ * `isListItem`, not `seed`: linked/custom Word numbering can be a valid list
6333
+ * item while returning `seed: null`. Command execution must also fail closed
6334
+ * when membership cannot be resolved instead of assuming a plain paragraph.
6335
+ *
6336
+ * The read resolves directly instead of peeking at the async cache. Only the
6337
+ * first selected block is warmed by the command-state snapshot, so command
6338
+ * execution cannot depend on that cache being warm (SD-3659).
6339
+ */
5524
6340
  const resolveListMembershipForBlockAsync = async (doc, blockId, story) => {
5525
6341
  const listsApi = doc?.lists;
5526
6342
  if (!listsApi) return null;
@@ -5537,6 +6353,7 @@ function createSuperDocUI(options) {
5537
6353
  return null;
5538
6354
  }
5539
6355
  };
6356
+ /** Read a list seed directly for mutation planning instead of trusting the reactive cache. */
5540
6357
  const resolveListSeedForBlock = (doc, blockId, story) => {
5541
6358
  const unavailable = {
5542
6359
  resolved: false,
@@ -5565,6 +6382,7 @@ function createSuperDocUI(options) {
5565
6382
  return unavailable;
5566
6383
  }
5567
6384
  };
6385
+ /** Await a block's current paragraph indentation authoritatively. */
5568
6386
  const resolveParagraphIndentationForBlockAsync = async (doc, blockId, story) => {
5569
6387
  if (!doc) return null;
5570
6388
  try {
@@ -5578,7 +6396,7 @@ function createSuperDocUI(options) {
5578
6396
  return null;
5579
6397
  }
5580
6398
  };
5581
- const computeHybridIndentCommandState = (descriptor, doc, readonly, selection$1, active, value) => {
6399
+ const computeHybridIndentCommandState = (descriptor, doc, readonly, selection, active, value) => {
5582
6400
  if (descriptor.mutates && readonly) return normalizeCommandState({
5583
6401
  enabled: false,
5584
6402
  active,
@@ -5586,7 +6404,7 @@ function createSuperDocUI(options) {
5586
6404
  value,
5587
6405
  reason: SUPERDOC_UI_REASONS.documentReadonly
5588
6406
  }, "builtin");
5589
- const blockIds = selectionBlockIds(selection$1);
6407
+ const blockIds = selectionBlockIds(selection);
5590
6408
  if (blockIds.length === 0) return normalizeCommandState({
5591
6409
  enabled: false,
5592
6410
  active,
@@ -5599,7 +6417,7 @@ function createSuperDocUI(options) {
5599
6417
  const paragraphClearIndent = resolveDocOperation(doc, "format.paragraph.clearIndentation");
5600
6418
  const mode = descriptor.list?.mode;
5601
6419
  const paragraphOpAvailable = mode === "indent" ? paragraphSetIndent != null : paragraphSetIndent != null || paragraphClearIndent != null;
5602
- const story = selectionStory(selection$1);
6420
+ const story = selectionStory(selection);
5603
6421
  let listMembership = null;
5604
6422
  if (story.storyType !== "body") {
5605
6423
  listMembership = [];
@@ -5683,7 +6501,7 @@ function createSuperDocUI(options) {
5683
6501
  reason: unavailableRouteReason(doc)
5684
6502
  }, "builtin");
5685
6503
  };
5686
- const computeListToggleCommandState = (descriptor, doc, readonly, selection$1, active, value) => {
6504
+ const computeListToggleCommandState = (descriptor, doc, readonly, selection, active, value) => {
5687
6505
  if (descriptor.mutates && readonly) return normalizeCommandState({
5688
6506
  enabled: false,
5689
6507
  active,
@@ -5691,7 +6509,7 @@ function createSuperDocUI(options) {
5691
6509
  value,
5692
6510
  reason: SUPERDOC_UI_REASONS.documentReadonly
5693
6511
  }, "builtin");
5694
- const blockIds = selectionBlockIds(selection$1);
6512
+ const blockIds = selectionBlockIds(selection);
5695
6513
  if (blockIds.length === 0) return normalizeCommandState({
5696
6514
  enabled: false,
5697
6515
  active,
@@ -5699,7 +6517,7 @@ function createSuperDocUI(options) {
5699
6517
  value,
5700
6518
  reason: SUPERDOC_UI_REASONS.selectionRequired
5701
6519
  }, "builtin");
5702
- const story = selectionStory(selection$1);
6520
+ const story = selectionStory(selection);
5703
6521
  const listsApi = doc?.lists;
5704
6522
  if (!(typeof listsApi?.getStateInStory === "function" || story.storyType === "body" && typeof listsApi?.getState === "function")) return normalizeCommandState({
5705
6523
  enabled: false,
@@ -5727,7 +6545,7 @@ function createSuperDocUI(options) {
5727
6545
  supported: true,
5728
6546
  value
5729
6547
  }, "builtin");
5730
- if (snapshots.some(({ value: state$1 }) => state$1 === null)) return normalizeCommandState({
6548
+ if (snapshots.some(({ value: state }) => state === null)) return normalizeCommandState({
5731
6549
  enabled: false,
5732
6550
  active,
5733
6551
  supported: false,
@@ -5735,7 +6553,7 @@ function createSuperDocUI(options) {
5735
6553
  reason: SUPERDOC_UI_REASONS.operationUnavailable
5736
6554
  }, "builtin");
5737
6555
  const seed = descriptor.list?.seed;
5738
- const shouldRemove = snapshots.every(({ value: state$1 }) => state$1?.isListItem === true && state$1.seed === seed);
6556
+ const shouldRemove = snapshots.every(({ value: state }) => state?.isListItem === true && state.seed === seed);
5739
6557
  if (story.storyType === "body") return normalizeCommandState(!shouldRemove || resolveDocOperation(doc, "lists.remove") != null ? {
5740
6558
  enabled: true,
5741
6559
  active,
@@ -5763,10 +6581,12 @@ function createSuperDocUI(options) {
5763
6581
  reason: SUPERDOC_UI_REASONS.operationUnavailable
5764
6582
  }, "builtin");
5765
6583
  };
5766
- const readActiveLinkHref = (doc, selection$1) => {
5767
- const href = (resolveCurrentHyperlink(doc, selection$1)?.properties)?.href;
6584
+ /** Read the active hyperlink's href overlapping the selection, when present. */
6585
+ const readActiveLinkHref = (doc, selection) => {
6586
+ const href = (resolveCurrentHyperlink(doc, selection)?.properties)?.href;
5768
6587
  return typeof href === "string" ? href : null;
5769
6588
  };
6589
+ /** State for a command routed through a public SuperDoc-instance method. */
5770
6590
  const computeInstanceCommandState = (descriptor) => {
5771
6591
  if (!getEditor()) return normalizeCommandState({
5772
6592
  enabled: false,
@@ -5788,12 +6608,14 @@ function createSuperDocUI(options) {
5788
6608
  value: descriptorValue(descriptor)
5789
6609
  }, "builtin");
5790
6610
  };
6611
+ /** Live active state for a host-owned chrome control (`ruler`, `formatting-marks`). */
5791
6612
  const chromeActiveState = (descriptor) => {
5792
6613
  const config = superdoc?.config ?? void 0;
5793
6614
  if (descriptor.valueFrom === "ruler") return Boolean(config?.rulers);
5794
6615
  if (descriptor.valueFrom === "formattingMarks") return Boolean((config?.layoutEngineOptions)?.showFormattingMarks);
5795
6616
  return false;
5796
6617
  };
6618
+ /** State for a table cell-context command routed through `tables.*`. */
5797
6619
  const computeTableCommandState = (descriptor, doc, readonly) => {
5798
6620
  if (readonly) return normalizeCommandState({
5799
6621
  enabled: false,
@@ -5820,12 +6642,14 @@ function createSuperDocUI(options) {
5820
6642
  supported: true
5821
6643
  }, "builtin");
5822
6644
  };
6645
+ /** Resolve a descriptor's live `value` from public controller state, when modeled. */
5823
6646
  const descriptorValue = (descriptor) => {
5824
6647
  if (descriptor.valueFrom === "zoom") return computeZoom().value;
5825
6648
  if (descriptor.valueFrom === "documentMode") return readDocumentMode();
5826
6649
  if (descriptor.valueFrom === "measurementUnit") return readMeasurementUnit();
5827
6650
  if (descriptor.valueFrom === "ruler" || descriptor.valueFrom === "formattingMarks") return chromeActiveState(descriptor);
5828
6651
  };
6652
+ /** Validate normalized payloads before invoking public SuperDoc-instance methods. */
5829
6653
  const instanceCommandPayloadIsValid = (descriptor, payload) => {
5830
6654
  if (descriptor.id === "zoom") return typeof payload === "number" && Number.isFinite(payload) && payload > 0;
5831
6655
  if (descriptor.id === "document-mode") return payload === "editing" || payload === "suggesting" || payload === "viewing";
@@ -5833,7 +6657,7 @@ function createSuperDocUI(options) {
5833
6657
  return true;
5834
6658
  };
5835
6659
  const allCommandIds = () => {
5836
- return [...new Set([...ALL_BUILT_IN_COMMAND_IDS, ...customCommands.keys()])];
6660
+ return [.../* @__PURE__ */ new Set([...ALL_BUILT_IN_COMMAND_IDS, ...customCommands.keys()])];
5837
6661
  };
5838
6662
  const DEFAULT_PARAGRAPH_STYLE_ID = "Normal";
5839
6663
  const EMPTY_STYLES_SLICE = {
@@ -5847,6 +6671,14 @@ function createSuperDocUI(options) {
5847
6671
  sourceStatus: null,
5848
6672
  diagnostics: []
5849
6673
  };
6674
+ /**
6675
+ * Read the public Document API style catalogue (`doc.styles.getCatalog`)
6676
+ * through the async read coordinator. A promise-returning browser read
6677
+ * settles into the cache (keyed by content revision) instead of collapsing
6678
+ * the catalogue to `null`. `value` is `null` when the styles surface is
6679
+ * unreachable (worker-backed `doc` is null or the operation is missing); the
6680
+ * accompanying {@link SliceStatus} reports pending/stale/ready.
6681
+ */
5850
6682
  const readStyleCatalogLive = (input) => {
5851
6683
  const stylesApi = getDoc()?.styles;
5852
6684
  if (!stylesApi || typeof stylesApi.getCatalog !== "function") return {
@@ -5855,6 +6687,7 @@ function createSuperDocUI(options) {
5855
6687
  };
5856
6688
  return readAsync(`styles:catalog:${JSON.stringify(input ?? {})}`, contentToken(), () => stylesApi.getCatalog(input), (raw) => raw && typeof raw === "object" ? raw : null);
5857
6689
  };
6690
+ /** Resolve the catalogue (coordinator-cached by content revision) plus its readiness. */
5858
6691
  const getStyleCatalog = () => {
5859
6692
  const full = readStyleCatalogLive({ includePreview: true });
5860
6693
  if (!full.value) return {
@@ -5877,6 +6710,13 @@ function createSuperDocUI(options) {
5877
6710
  status: combineStatus(full.status, quickResult.status)
5878
6711
  };
5879
6712
  };
6713
+ /**
6714
+ * Read one block's paragraph `styleRef` through the public `getNodeById`
6715
+ * read. `ok` distinguishes a successful read (styleRef may be null when the
6716
+ * paragraph has no explicit style) from an unavailable read (no operation, a
6717
+ * promise in worker mode, or a missing node) so active-style resolution can
6718
+ * fail closed precisely.
6719
+ */
5880
6720
  const readBlockStyleRef = (doc, blockId, story) => {
5881
6721
  const { value: result, status } = readNodeById(doc, blockId, story);
5882
6722
  if (!result) return {
@@ -5899,7 +6739,13 @@ function createSuperDocUI(options) {
5899
6739
  status
5900
6740
  };
5901
6741
  };
5902
- const computeActiveParagraphStyle = (selection$1, catalog) => {
6742
+ /**
6743
+ * Resolve the active paragraph style from the current selection. Handles the
6744
+ * uniform, default (no explicit style → document default paragraph style),
6745
+ * and mixed cases, and fails closed with diagnostics when block reads are
6746
+ * unavailable.
6747
+ */
6748
+ const computeActiveParagraphStyle = (selection, catalog) => {
5903
6749
  const diagnostics = [];
5904
6750
  const doc = getDoc();
5905
6751
  if (!doc) {
@@ -5918,7 +6764,7 @@ function createSuperDocUI(options) {
5918
6764
  status: "ready"
5919
6765
  };
5920
6766
  }
5921
- const blockIds = selectionBlockIds(selection$1);
6767
+ const blockIds = selectionBlockIds(selection);
5922
6768
  if (blockIds.length === 0) {
5923
6769
  diagnostics.push({
5924
6770
  severity: "info",
@@ -5957,7 +6803,7 @@ function createSuperDocUI(options) {
5957
6803
  let blockReadFailures = 0;
5958
6804
  let defaultReadFailures = 0;
5959
6805
  let readStatus = "ready";
5960
- const story = selectionStory(selection$1);
6806
+ const story = selectionStory(selection);
5961
6807
  for (const blockId of blockIds) {
5962
6808
  const read = readBlockStyleRef(doc, blockId, story);
5963
6809
  readStatus = combineStatus(readStatus, read.status);
@@ -5969,13 +6815,13 @@ function createSuperDocUI(options) {
5969
6815
  defaultReadFailures += 1;
5970
6816
  continue;
5971
6817
  }
5972
- const styleId$1 = read.styleRef ?? defaultId;
5973
- if (!styleId$1) {
6818
+ const styleId = read.styleRef ?? defaultId;
6819
+ if (!styleId) {
5974
6820
  defaultReadFailures += 1;
5975
6821
  continue;
5976
6822
  }
5977
6823
  resolved += 1;
5978
- resolvedIds.add(styleId$1);
6824
+ resolvedIds.add(styleId);
5979
6825
  }
5980
6826
  if (resolved === 0) {
5981
6827
  const onlyDefaultUnavailable = defaultReadFailures > 0 && blockReadFailures === 0;
@@ -6031,10 +6877,10 @@ function createSuperDocUI(options) {
6031
6877
  status: readStatus
6032
6878
  };
6033
6879
  };
6034
- const computeStyles = (selection$1) => {
6880
+ const computeStyles = (selection) => {
6035
6881
  if (!getEditor()) return EMPTY_STYLES_SLICE;
6036
6882
  const { cache: catalog, status: catalogStatus } = getStyleCatalog();
6037
- const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection$1, catalog);
6883
+ const { style: active, status: activeStatus } = computeActiveParagraphStyle(selection, catalog);
6038
6884
  let diagnostics;
6039
6885
  if (catalog) diagnostics = active.diagnostics.length === 0 ? catalog.full.diagnostics : [...catalog.full.diagnostics, ...active.diagnostics];
6040
6886
  else diagnostics = [{
@@ -6044,7 +6890,7 @@ function createSuperDocUI(options) {
6044
6890
  }, ...active.diagnostics];
6045
6891
  return {
6046
6892
  ready: true,
6047
- status: combineStatus(catalogStatus, activeStatus, selection$1.status),
6893
+ status: combineStatus(catalogStatus, activeStatus, selection.status),
6048
6894
  catalogRevision: catalog?.full.revision ?? null,
6049
6895
  quickGallery: catalog?.quickGallery ?? [],
6050
6896
  activeParagraphStyleId: active.styleId,
@@ -6056,32 +6902,32 @@ function createSuperDocUI(options) {
6056
6902
  };
6057
6903
  const computeState = () => {
6058
6904
  const documentMode = readDocumentMode();
6059
- const selection$1 = computeSelection();
6060
- if (selection$1.status === "ready") pruneSelectionScopedAsyncReads(selection$1);
6061
- reconcilePendingInlineFormat(selection$1);
6062
- const selectionSignature$1 = selectionInlineValueSignature(selection$1);
6063
- for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature$1 || optimistic.selectionSignature !== selectionSignature$1) optimisticInlineToggles.delete(commandId);
6064
- else if (optimistic.settled && selection$1.status === "ready") optimisticInlineToggles.delete(commandId);
6905
+ const selection = computeSelection();
6906
+ if (selection.status === "ready") pruneSelectionScopedAsyncReads(selection);
6907
+ reconcilePendingInlineFormat(selection);
6908
+ const selectionSignature = selectionInlineValueSignature(selection);
6909
+ for (const [commandId, optimistic] of optimisticInlineToggles) if (!selectionSignature || optimistic.selectionSignature !== selectionSignature) optimisticInlineToggles.delete(commandId);
6910
+ else if (optimistic.settled && selection.status === "ready") optimisticInlineToggles.delete(commandId);
6065
6911
  return {
6066
6912
  ready: getEditor() != null,
6067
6913
  documentMode,
6068
6914
  document: computeDocument(),
6069
- selection: selection$1,
6915
+ selection,
6070
6916
  toolbar: {
6071
6917
  context: documentMode,
6072
- commands: computeCommandStates(selection$1),
6918
+ commands: computeCommandStates(selection),
6073
6919
  copyFormatActive: painter.mode !== "idle"
6074
6920
  },
6075
- comments: computeComments(selection$1),
6076
- trackChanges: computeTrackChanges(selection$1),
6077
- contentControls: computeContentControls(selection$1),
6921
+ comments: computeComments(selection),
6922
+ trackChanges: computeTrackChanges(selection),
6923
+ contentControls: computeContentControls(selection),
6078
6924
  zoom: computeZoom(),
6079
6925
  fonts: computeFonts(),
6080
- styles: computeStyles(selection$1)
6926
+ styles: computeStyles(selection)
6081
6927
  };
6082
6928
  };
6083
- const syncOptimisticInlineSelection = (selection$1) => {
6084
- const nextSignature = selectionInlineValueSignature(selection$1);
6929
+ const syncOptimisticInlineSelection = (selection) => {
6930
+ const nextSignature = selectionInlineValueSignature(selection);
6085
6931
  if (nextSignature && lastOptimisticInlineSelectionSignature && nextSignature !== lastOptimisticInlineSelectionSignature) {
6086
6932
  optimisticInlineValues.clear();
6087
6933
  optimisticInlineToggles.clear();
@@ -6129,8 +6975,8 @@ function createSuperDocUI(options) {
6129
6975
  let detachDocumentSelection = null;
6130
6976
  const readHostSelectionSource = () => {
6131
6977
  const host = getEditor()?.host;
6132
- const selection$1 = safeCall(host?.getHandles ? () => host.getHandles() : void 0, null)?.editing?.selection;
6133
- return selection$1 && typeof selection$1.subscribe === "function" ? selection$1 : null;
6978
+ const selection = safeCall(host?.getHandles ? () => host.getHandles() : void 0, null)?.editing?.selection;
6979
+ return selection && typeof selection.subscribe === "function" ? selection : null;
6134
6980
  };
6135
6981
  const syncHostSelectionSubscription = () => {
6136
6982
  const next = readHostSelectionSource();
@@ -6202,7 +7048,8 @@ function createSuperDocUI(options) {
6202
7048
  try {
6203
7049
  const unsubscribe = next.subscribe((snapshot) => {
6204
7050
  const snapshotRecord = snapshot && typeof snapshot === "object" ? snapshot : null;
6205
- if (!syncTrackedChangeFocusFromHostReviewTarget(snapshotRecord && Object.prototype.hasOwnProperty.call(snapshotRecord, "activeReviewTarget") ? snapshotRecord.activeReviewTarget : readHostActiveReviewTarget(next), snapshot) || attaching) return;
7051
+ const target = snapshotRecord && Object.prototype.hasOwnProperty.call(snapshotRecord, "activeReviewTarget") ? snapshotRecord.activeReviewTarget : readHostActiveReviewTarget(next);
7052
+ if (!syncTrackedChangeFocusFromHostReviewTarget(target, snapshot) || attaching) return;
6206
7053
  recompute();
6207
7054
  });
6208
7055
  if (typeof unsubscribe === "function") detachHostReview = unsubscribe;
@@ -6356,7 +7203,29 @@ function createSuperDocUI(options) {
6356
7203
  state = nextState;
6357
7204
  for (const listener of [...listeners]) listener(state);
6358
7205
  };
7206
+ /**
7207
+ * Work that is bound to one specific active editor and cannot survive a swap.
7208
+ * Recomputing aggregate state is not enough for these: a geometry
7209
+ * subscription points at a DOM host that is going away, and an in-flight
7210
+ * async query would publish the old document's result into the new one.
7211
+ *
7212
+ * Populated by the viewport and search handles further down. The handler only
7213
+ * runs on a host event, which cannot happen during construction, so
7214
+ * registering later than `attach` is safe.
7215
+ */
6359
7216
  const activeEditorResetHooks = [];
7217
+ /**
7218
+ * The subset bound to the DOCUMENT rather than to the editor.
7219
+ *
7220
+ * `replaceFile()` swaps content in place: the editor object and its host both
7221
+ * survive, so a geometry subscription is still attached to the thing now
7222
+ * rendering the replacement and must be left alone. Search state describes
7223
+ * content that is gone and must not be.
7224
+ *
7225
+ * Measured rather than assumed — after an in-place replace the geometry
7226
+ * subscription is never detached and keeps firing, while `search.total` still
7227
+ * reports the previous document's match count.
7228
+ */
6360
7229
  const documentResetHooks = [];
6361
7230
  const runHooks = (hooks) => {
6362
7231
  for (const reset of hooks) try {
@@ -6419,6 +7288,20 @@ function createSuperDocUI(options) {
6419
7288
  };
6420
7289
  };
6421
7290
  const sliceHandle = (selector) => select(selector);
7291
+ /**
7292
+ * Wrap an underlying selector {@link Subscribable} (raw `get` + value
7293
+ * `subscribe`) in the main-compatible domain-handle subscription shape so
7294
+ * every customer-facing handle exposes the same contract main does:
7295
+ *
7296
+ * - `getSnapshot()` reads the current slice;
7297
+ * - `observe(listener)` emits the current value immediately, then on each
7298
+ * change (the listener receives the snapshot directly);
7299
+ * - `subscribe(listener)` is the `{ snapshot }`-shaped alias of `observe`,
7300
+ * likewise emitting immediately then on change.
7301
+ *
7302
+ * The generic `select(...)` substrate stays raw-value and unchanged; only the
7303
+ * domain handles are composed from this.
7304
+ */
6422
7305
  const snapshotHandle = (sub) => {
6423
7306
  const observe = (listener) => {
6424
7307
  const safe = (snapshot) => {
@@ -6440,8 +7323,8 @@ function createSuperDocUI(options) {
6440
7323
  function readTrackDecisionTargetId(target) {
6441
7324
  return target && typeof target === "object" && typeof target.id === "string" ? String(target.id) : null;
6442
7325
  }
6443
- function nonBodySelectionStoryLocator(selection$1) {
6444
- const story = selectionStoryLocator(selection$1);
7326
+ function nonBodySelectionStoryLocator(selection) {
7327
+ const story = selectionStoryLocator(selection);
6445
7328
  return story && storyLocatorSignature(story) !== storyLocatorSignature(null) ? story : void 0;
6446
7329
  }
6447
7330
  function trackDecisionIdTarget(changeId, story) {
@@ -6451,15 +7334,24 @@ function createSuperDocUI(options) {
6451
7334
  ...story ? { story } : {}
6452
7335
  };
6453
7336
  }
6454
- function trackDecisionRangeFromSelection(selection$1) {
6455
- if (selection$1.empty) return null;
6456
- const explicit = selection$1.selectionTarget;
7337
+ /**
7338
+ * Build the Document API range target for a partial tracked-change decision.
7339
+ * The explicit selection target is authoritative and labels its coordinates:
7340
+ * ordinary/insertion selections are visible-space, while a selection that
7341
+ * reaches painted deleted text is projected in tracked space by the host.
7342
+ *
7343
+ * A cross-block or malformed selection fails closed. Its intermediate block
7344
+ * coverage cannot be reconstructed from two endpoints without guessing.
7345
+ */
7346
+ function trackDecisionRangeFromSelection(selection) {
7347
+ if (selection.empty) return null;
7348
+ const explicit = selection.selectionTarget;
6457
7349
  const start = explicit?.start;
6458
7350
  const end = explicit?.end;
6459
7351
  if (explicit?.kind !== "selection" || start?.kind !== "text" || end?.kind !== "text" || typeof start.blockId !== "string" || start.blockId !== end.blockId || !Number.isInteger(start.offset) || !Number.isInteger(end.offset) || start.offset < 0 || end.offset < 0 || start.offset === end.offset) return null;
6460
7352
  const rangeStart = Math.min(start.offset, end.offset);
6461
7353
  const rangeEnd = Math.max(start.offset, end.offset);
6462
- const story = selectionStoryLocator(selection$1);
7354
+ const story = selectionStoryLocator(selection);
6463
7355
  return {
6464
7356
  kind: "range",
6465
7357
  coordinateSpace: explicit.coordinateSpace === "tracked" ? "tracked" : "visible",
@@ -6478,17 +7370,17 @@ function createSuperDocUI(options) {
6478
7370
  }
6479
7371
  function readBodyTrackChangeForDecision(activeId, story) {
6480
7372
  if (storyLocatorSignature(story) !== storyLocatorSignature(null)) return void 0;
6481
- const trackChanges$1 = getDoc()?.trackChanges;
6482
- const list = trackChanges$1?.list;
7373
+ const trackChanges = getDoc()?.trackChanges;
7374
+ const list = trackChanges?.list;
6483
7375
  if (typeof list !== "function") return void 0;
6484
- const raw = safeCall(() => list.call(trackChanges$1), null);
7376
+ const raw = safeCall(() => list.call(trackChanges), null);
6485
7377
  if (isPromiseLike(raw) || !raw || typeof raw !== "object") return void 0;
6486
7378
  const items = raw.items;
6487
7379
  if (!Array.isArray(items)) return void 0;
6488
7380
  return items.map(projectTrackChangesItem).find((item) => item != null && entityRowMatchesRequest(item, activeId, story));
6489
7381
  }
6490
- function selectedTextCoversTrackChangeText(selection$1, item) {
6491
- const selected = normalizeTrackDecisionText(selection$1.quotedText);
7382
+ function selectedTextCoversTrackChangeText(selection, item) {
7383
+ const selected = normalizeTrackDecisionText(selection.quotedText);
6492
7384
  if (!selected) return false;
6493
7385
  const payload = trackChangesItemPayload(item);
6494
7386
  return [
@@ -6520,7 +7412,8 @@ function createSuperDocUI(options) {
6520
7412
  if (isPromiseLike(result)) return settleCommandExecution(Promise.resolve(result).then((settled) => {
6521
7413
  return commandResultSucceeded(commandResultFromOperationResult(settled)) ? settled : fallback();
6522
7414
  }));
6523
- return settleCommandExecution(commandResultSucceeded(commandResultFromOperationResult(result)) ? result : fallback());
7415
+ const commandResult = commandResultFromOperationResult(result);
7416
+ return settleCommandExecution(commandResultSucceeded(commandResult) ? result : fallback());
6524
7417
  } catch {
6525
7418
  return false;
6526
7419
  }
@@ -6663,8 +7556,8 @@ function createSuperDocUI(options) {
6663
7556
  const scheduleInlineToggleMutation = (key, mutate) => {
6664
7557
  if (!inlineToggleMutationActive) {
6665
7558
  inlineToggleMutationActive = true;
6666
- inlineToggleMutationIdle = new Promise((resolve$1) => {
6667
- resolveInlineToggleMutationIdle = resolve$1;
7559
+ inlineToggleMutationIdle = new Promise((resolve) => {
7560
+ resolveInlineToggleMutationIdle = resolve;
6668
7561
  });
6669
7562
  try {
6670
7563
  return {
@@ -6701,20 +7594,21 @@ function createSuperDocUI(options) {
6701
7594
  release: releaseInlineToggleMutationOnce()
6702
7595
  };
6703
7596
  };
6704
- const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = new Set(["create.tableOfContents"]);
7597
+ const UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES = /* @__PURE__ */ new Set(["create.tableOfContents"]);
6705
7598
  const unsupportedTrackedMutationReceipt = (route) => failedReceipt(`Tracked authoring is not supported for ${route}.`);
6706
7599
  const editorMutationOptionsForRoute = (route) => {
6707
7600
  if (readDocumentMode() !== "suggesting") return void 0;
6708
7601
  if (UNSUPPORTED_TRACKED_UI_MUTATION_ROUTES.has(route)) return unsupportedTrackedMutationReceipt(route);
6709
7602
  return { changeMode: "tracked" };
6710
7603
  };
7604
+ /** Clear-patch to send for the current document mode (see TRACKED_CLEAR_INLINE_PATCH). */
6711
7605
  const clearInlinePatchForMode = () => readDocumentMode() === "suggesting" ? TRACKED_CLEAR_INLINE_PATCH : CLEAR_INLINE_PATCH;
6712
7606
  const callEditorMutation = (route, op, input) => {
6713
- const options$1 = editorMutationOptionsForRoute(route);
6714
- if (options$1 && options$1.success === false) return options$1;
6715
- return options$1 ? op(input, options$1) : op(input);
7607
+ const options = editorMutationOptionsForRoute(route);
7608
+ if (options && options.success === false) return options;
7609
+ return options ? op(input, options) : op(input);
6716
7610
  };
6717
- const captureOptimisticInlineValueResult = (descriptor, selection$1, input, result) => {
7611
+ const captureOptimisticInlineValueResult = (descriptor, selection, input, result) => {
6718
7612
  const inline = descriptor.inline;
6719
7613
  if (!inline) return;
6720
7614
  const commandResult = commandResultFromOperationResult(result);
@@ -6725,35 +7619,35 @@ function createSuperDocUI(options) {
6725
7619
  return;
6726
7620
  }
6727
7621
  if (inline.kind === "toggle" || !isProjectedInlineSelectionValueKey(inline.key)) return;
6728
- const selectionSignature$1 = selectionInlineValueSignature(selection$1);
6729
- if (!selectionSignature$1) return;
7622
+ const selectionSignature = selectionInlineValueSignature(selection);
7623
+ if (!selectionSignature) return;
6730
7624
  const value = normalizeOptimisticInlineSelectionValue(inline.key, input.value);
6731
7625
  if (value == null) {
6732
7626
  optimisticInlineValues.delete(inline.key);
6733
7627
  return;
6734
7628
  }
6735
7629
  optimisticInlineValues.set(inline.key, {
6736
- selectionSignature: selectionSignature$1,
7630
+ selectionSignature,
6737
7631
  value
6738
7632
  });
6739
7633
  };
6740
- const decorateInlineCommandResult = (descriptor, selection$1, input, result) => {
7634
+ const decorateInlineCommandResult = (descriptor, selection, input, result) => {
6741
7635
  if (!descriptor.inline || descriptor.inline.kind === "toggle") return result;
6742
7636
  if (isPromiseLike(result)) return Promise.resolve(result).then((resolved) => {
6743
- captureOptimisticInlineValueResult(descriptor, selection$1, input, resolved);
7637
+ captureOptimisticInlineValueResult(descriptor, selection, input, resolved);
6744
7638
  return resolved;
6745
7639
  });
6746
7640
  return result;
6747
7641
  };
6748
- const captureOptimisticInlineToggle = (descriptor, selection$1, input, result) => {
7642
+ const captureOptimisticInlineToggle = (descriptor, selection, input, result) => {
6749
7643
  if (descriptor.inline?.kind !== "toggle") return null;
6750
7644
  if (!commandResultSucceeded(commandResultFromOperationResult(result))) return null;
6751
7645
  if (typeof input.value !== "boolean" && input.value !== null) return null;
6752
- const selectionSignature$1 = selectionInlineValueSignature(selection$1);
6753
- if (!selectionSignature$1) return null;
7646
+ const selectionSignature = selectionInlineValueSignature(selection);
7647
+ if (!selectionSignature) return null;
6754
7648
  const generation = ++optimisticInlineToggleGeneration;
6755
7649
  optimisticInlineToggles.set(descriptor.id, {
6756
- selectionSignature: selectionSignature$1,
7650
+ selectionSignature,
6757
7651
  active: input.value === true,
6758
7652
  generation,
6759
7653
  settled: false
@@ -6821,6 +7715,11 @@ function createSuperDocUI(options) {
6821
7715
  recompute();
6822
7716
  return normalizedReceipt;
6823
7717
  };
7718
+ /**
7719
+ * Apply a Document API operation once per covered block, building the input
7720
+ * from the block id. Returns the last success receipt (or `true`), else the
7721
+ * last result (or `false`). Recompute is the caller's responsibility.
7722
+ */
6824
7723
  const applyPerBlock = (route, op, blockIds, buildInput) => {
6825
7724
  const immediateResults = [];
6826
7725
  const settledResults = [];
@@ -6839,6 +7738,7 @@ function createSuperDocUI(options) {
6839
7738
  pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
6840
7739
  return combineCommandResults(immediateResults);
6841
7740
  };
7741
+ /** Apply a Document API operation once per covered text segment. */
6842
7742
  const applyPerTextAddress = (route, op, targets, buildInput) => {
6843
7743
  const immediateResults = [];
6844
7744
  const settledResults = [];
@@ -6857,15 +7757,25 @@ function createSuperDocUI(options) {
6857
7757
  pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
6858
7758
  return combineCommandResults(immediateResults);
6859
7759
  };
6860
- const callInlineFormatMutation = (route, op, input, selection$1 = state.selection) => {
6861
- const options$1 = editorMutationOptionsForRoute(route);
6862
- if (options$1 && options$1.success === false) return options$1;
6863
- if (!selection$1.selectionTarget) return options$1 ? op(input, options$1) : op(input);
7760
+ /**
7761
+ * Call an inline `format.*` operation against the live selection. Mirrors
7762
+ * `callEditorMutation` (suggesting mode adds `changeMode: 'tracked'`), and
7763
+ * when the target is the host's own editable `selectionTarget` it also
7764
+ * carries the PRIVATE V2 option `offsetSpace: 'selection'` (browser
7765
+ * selection offsets count an inline object as one caret position). The
7766
+ * option is not part of public `MutationOptions`; this is the same cast
7767
+ * pattern the delete/replace callers use.
7768
+ */
7769
+ const callInlineFormatMutation = (route, op, input, selection = state.selection) => {
7770
+ const options = editorMutationOptionsForRoute(route);
7771
+ if (options && options.success === false) return options;
7772
+ if (!selection.selectionTarget) return options ? op(input, options) : op(input);
6864
7773
  return op(input, {
6865
- ...options$1 ?? {},
7774
+ ...options ?? {},
6866
7775
  offsetSpace: "selection"
6867
7776
  });
6868
7777
  };
7778
+ /** Build the `format.paragraph.*` / `styles.paragraph.*` input for a block. */
6869
7779
  const buildBlockParagraphInput = (spec, blockId, payload, story) => {
6870
7780
  const target = paragraphTarget(blockId, story);
6871
7781
  switch (spec.kind) {
@@ -6910,6 +7820,14 @@ function createSuperDocUI(options) {
6910
7820
  default: return null;
6911
7821
  }
6912
7822
  };
7823
+ /**
7824
+ * Store a caret pick as a pending mark (SD-3654/SD-3652) so the next typed text
7825
+ * carries it. Mirrors ProseMirror `storedMarks`: a toggle flips the stored
7826
+ * value against the current active state; a value command (color/font/size)
7827
+ * stores the value or clears it on a null/empty ("None") payload; a clear
7828
+ * command drops all stored marks. The host applies the store to the created
7829
+ * span on the next insert; settleCommandExecution drives the recompute.
7830
+ */
6913
7831
  const storePendingInlineFormat = (descriptor, payload) => {
6914
7832
  const spec = descriptor.inline;
6915
7833
  const method = inlineFormatMethod(descriptor);
@@ -6947,9 +7865,9 @@ function createSuperDocUI(options) {
6947
7865
  immediateResults.push(settled.immediate);
6948
7866
  settledResults.push(settled.settled);
6949
7867
  };
6950
- const run = (op, input, options$1) => {
7868
+ const run = (op, input, options) => {
6951
7869
  try {
6952
- record(options$1 ? op(input, options$1) : op(input));
7870
+ record(options ? op(input, options) : op(input));
6953
7871
  } catch {
6954
7872
  immediateResults.push(false);
6955
7873
  settledResults.push(Promise.resolve(false));
@@ -7008,6 +7926,7 @@ function createSuperDocUI(options) {
7008
7926
  pendingCommandSettlement = settledResults.length ? Promise.all(settledResults).then((results) => combineCommandResults(results)) : null;
7009
7927
  return combineCommandResults(immediateResults.length ? immediateResults : [false]);
7010
7928
  };
7929
+ /** Route a block-level paragraph command (`format.paragraph.*` / `styles.paragraph.*`). */
7011
7930
  const executeBlockParagraph = (descriptor, op, payload) => {
7012
7931
  const blockIds = selectionBlockIds(state.selection);
7013
7932
  if (blockIds.length === 0) return false;
@@ -7113,6 +8032,7 @@ function createSuperDocUI(options) {
7113
8032
  })();
7114
8033
  return true;
7115
8034
  };
8035
+ /** Route a list command (`lists.apply` / `lists.remove` / list level changes). */
7116
8036
  const executeListCommand = (descriptor, op) => {
7117
8037
  const spec = descriptor.list;
7118
8038
  const doc = getDoc();
@@ -7150,9 +8070,9 @@ function createSuperDocUI(options) {
7150
8070
  seed
7151
8071
  };
7152
8072
  try {
7153
- const result$1 = settleOperationResult(callEditorMutation(route, operation, input));
7154
- immediateResults.push(result$1.immediate);
7155
- settledResults.push(result$1.settled);
8073
+ const result = settleOperationResult(callEditorMutation(route, operation, input));
8074
+ immediateResults.push(result.immediate);
8075
+ settledResults.push(result.settled);
7156
8076
  } catch {
7157
8077
  immediateResults.push(false);
7158
8078
  settledResults.push(Promise.resolve(false));
@@ -7166,8 +8086,8 @@ function createSuperDocUI(options) {
7166
8086
  const stateReads = blockIds.map((blockId) => resolveListSeedForBlock(doc, blockId, story));
7167
8087
  if (stateReads.some(isPromiseLike)) {
7168
8088
  pendingCommandSettlement = Promise.all(stateReads).then(async (states) => {
7169
- const result$1 = executeResolved(states);
7170
- return result$1.settled ? await result$1.settled : result$1.immediate;
8089
+ const result = executeResolved(states);
8090
+ return result.settled ? await result.settled : result.immediate;
7171
8091
  });
7172
8092
  return true;
7173
8093
  }
@@ -7193,6 +8113,7 @@ function createSuperDocUI(options) {
7193
8113
  const capture = record.capture && typeof record.capture === "object" ? record.capture : null;
7194
8114
  return record.target ?? capture?.target ?? capture?.selectionTarget ?? record.selectionTarget ?? record.textTarget ?? null;
7195
8115
  }
8116
+ /** Route a link command (`hyperlinks.wrap` / `insert` / `patch` / `remove`). */
7196
8117
  const executeLinkCommand = (payload) => {
7197
8118
  const doc = getDoc();
7198
8119
  const linksApi = doc?.hyperlinks;
@@ -7328,8 +8249,8 @@ function createSuperDocUI(options) {
7328
8249
  if (wrapTargets.length > 0) {
7329
8250
  if (typeof wrap !== "function") return false;
7330
8251
  try {
7331
- return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target$1) => ({
7332
- target: target$1,
8252
+ return applyPerTextAddress("hyperlinks.wrap", wrap, wrapTargets, (target) => ({
8253
+ target,
7333
8254
  link: { destination: { href: normalizedHref } }
7334
8255
  }));
7335
8256
  } catch {
@@ -7337,14 +8258,14 @@ function createSuperDocUI(options) {
7337
8258
  }
7338
8259
  }
7339
8260
  const insert = linksApi.insert;
7340
- const text$1 = readLinkPayloadText(payload, normalizedHref);
7341
- if (typeof insert !== "function" || text$1.trim() === "") return false;
8261
+ const text = readLinkPayloadText(payload, normalizedHref);
8262
+ if (typeof insert !== "function" || text.trim() === "") return false;
7342
8263
  const target = collapsedTextAddressFromTarget(payloadTarget) ?? collapsedTextAddressFromSelection(state.selection);
7343
8264
  if (!target) return false;
7344
8265
  try {
7345
8266
  return callEditorMutation("hyperlinks.insert", insert, {
7346
8267
  target,
7347
- text: text$1,
8268
+ text,
7348
8269
  link: { destination: { href: normalizedHref } }
7349
8270
  });
7350
8271
  } catch {
@@ -7353,6 +8274,7 @@ function createSuperDocUI(options) {
7353
8274
  }
7354
8275
  return false;
7355
8276
  };
8277
+ /** Route a create command (`create.table` / `create.image` / `create.tableOfContents`). */
7356
8278
  const executeCreateCommand = (descriptor, op, payload) => {
7357
8279
  const kind = descriptor.create.kind;
7358
8280
  const record = payload && typeof payload === "object" ? payload : {};
@@ -7398,16 +8320,16 @@ function createSuperDocUI(options) {
7398
8320
  }
7399
8321
  };
7400
8322
  }
7401
- const input$1 = {
8323
+ const input = {
7402
8324
  src,
7403
8325
  at
7404
8326
  };
7405
- if (nonBodyStory) input$1.in = at.kind === "inParagraph" ? resolveActiveHeaderFooterSlot(nonBodyStory) ?? nonBodyStory : nonBodyStory;
7406
- if (typeof record.alt === "string") input$1.alt = record.alt;
7407
- if (typeof record.title === "string") input$1.title = record.title;
7408
- if (record.size && typeof record.size === "object") input$1.size = record.size;
8327
+ if (nonBodyStory) input.in = at.kind === "inParagraph" ? resolveActiveHeaderFooterSlot(nonBodyStory) ?? nonBodyStory : nonBodyStory;
8328
+ if (typeof record.alt === "string") input.alt = record.alt;
8329
+ if (typeof record.title === "string") input.title = record.title;
8330
+ if (record.size && typeof record.size === "object") input.size = record.size;
7409
8331
  try {
7410
- return callEditorMutation(descriptor.docRoute, op, input$1);
8332
+ return callEditorMutation(descriptor.docRoute, op, input);
7411
8333
  } catch {
7412
8334
  return false;
7413
8335
  }
@@ -7421,6 +8343,13 @@ function createSuperDocUI(options) {
7421
8343
  return false;
7422
8344
  }
7423
8345
  };
8346
+ /**
8347
+ * Route a table cell-context command (`tables.*`) against the live table
8348
+ * context resolved from the shared facade. Fails closed (`false`) when the
8349
+ * caret is not in a table, the context is incomplete, or the action's
8350
+ * required cell is missing — never calling the operation with a malformed
8351
+ * locator.
8352
+ */
7424
8353
  const executeTableCommand = (descriptor, op) => {
7425
8354
  const spec = descriptor.table;
7426
8355
  const context = resolveTableContext();
@@ -7433,9 +8362,9 @@ function createSuperDocUI(options) {
7433
8362
  const host = getHost();
7434
8363
  const insertRow = ((typeof host?.getHandles === "function" ? host.getHandles() : null)?.editing)?.tables;
7435
8364
  if (typeof insertRow?.insertRow !== "function") return false;
7436
- const options$1 = editorMutationOptionsForRoute(descriptor.docRoute);
7437
- if (options$1 && options$1.success === false) return options$1;
7438
- return options$1 ? insertRow.insertRow(input, options$1) : insertRow.insertRow(input);
8365
+ const options = editorMutationOptionsForRoute(descriptor.docRoute);
8366
+ if (options && options.success === false) return options;
8367
+ return options ? insertRow.insertRow(input, options) : insertRow.insertRow(input);
7439
8368
  }
7440
8369
  return callEditorMutation(descriptor.docRoute, op, input);
7441
8370
  } catch {
@@ -7476,7 +8405,8 @@ function createSuperDocUI(options) {
7476
8405
  if (disposed) return false;
7477
8406
  const custom = customCommands.get(id);
7478
8407
  if (custom) try {
7479
- return settleCommandExecution(custom.execute(buildCustomCommandContext(payload, context)));
8408
+ const result = custom.execute(buildCustomCommandContext(payload, context));
8409
+ return settleCommandExecution(result);
7480
8410
  } catch {
7481
8411
  return false;
7482
8412
  }
@@ -7497,7 +8427,8 @@ function createSuperDocUI(options) {
7497
8427
  if (typeof method !== "function") return false;
7498
8428
  try {
7499
8429
  const arg = descriptor.fixedArg !== void 0 ? descriptor.fixedArg : normalized;
7500
- return settleCommandExecution(method.call(superdoc, arg));
8430
+ const result = method.call(superdoc, arg);
8431
+ return settleCommandExecution(result);
7501
8432
  } catch {
7502
8433
  return false;
7503
8434
  }
@@ -7531,12 +8462,13 @@ function createSuperDocUI(options) {
7531
8462
  if (getEditor() !== commandEditor || getDoc() !== doc || readDocumentMode() !== commandMode) return false;
7532
8463
  return callInlineFormatMutation(route, op, input);
7533
8464
  };
7534
- const selectionSignature$1 = selectionInlineValueSignature(state.selection) ?? selectionKey(state.selection);
7535
- const scheduledMutation = descriptor.inline.kind === "toggle" ? scheduleInlineToggleMutation(`${descriptor.id}:${selectionSignature$1}`, mutate) : null;
8465
+ const selectionSignature = selectionInlineValueSignature(state.selection) ?? selectionKey(state.selection);
8466
+ const scheduledMutation = descriptor.inline.kind === "toggle" ? scheduleInlineToggleMutation(`${descriptor.id}:${selectionSignature}`, mutate) : null;
7536
8467
  releaseScheduledMutation = scheduledMutation?.release ?? null;
7537
8468
  const mutationResult = scheduledMutation ? scheduledMutation.result : mutate();
7538
8469
  const optimisticGeneration = captureOptimisticInlineToggle(descriptor, state.selection, input, mutationResult);
7539
- const immediate = settleCommandExecution(decorateInlineCommandResult(descriptor, state.selection, input, mutationResult), (settled) => {
8470
+ const result = decorateInlineCommandResult(descriptor, state.selection, input, mutationResult);
8471
+ const immediate = settleCommandExecution(result, (settled) => {
7540
8472
  settleOptimisticInlineToggle(descriptor, optimisticGeneration, settled);
7541
8473
  releaseScheduledMutation?.();
7542
8474
  releaseScheduledMutation = null;
@@ -7555,10 +8487,22 @@ function createSuperDocUI(options) {
7555
8487
  settleOptimisticParagraphAlignment(optimisticAlignmentGeneration, settled);
7556
8488
  });
7557
8489
  }
7558
- if (descriptor.list) return settleCommandExecution(executeListCommand(descriptor, op));
7559
- if (descriptor.link) return settleCommandExecution(executeLinkCommand(normalized));
7560
- if (descriptor.create) return settleCommandExecution(executeCreateCommand(descriptor, op, normalized));
7561
- if (descriptor.table) return settleCommandExecution(executeTableCommand(descriptor, op));
8490
+ if (descriptor.list) {
8491
+ const result = executeListCommand(descriptor, op);
8492
+ return settleCommandExecution(result);
8493
+ }
8494
+ if (descriptor.link) {
8495
+ const result = executeLinkCommand(normalized);
8496
+ return settleCommandExecution(result);
8497
+ }
8498
+ if (descriptor.create) {
8499
+ const result = executeCreateCommand(descriptor, op, normalized);
8500
+ return settleCommandExecution(result);
8501
+ }
8502
+ if (descriptor.table) {
8503
+ const result = executeTableCommand(descriptor, op);
8504
+ return settleCommandExecution(result);
8505
+ }
7562
8506
  try {
7563
8507
  return settleCommandExecution(descriptor.mutates ? callEditorMutation(route, op, normalized) : op(normalized));
7564
8508
  } catch {
@@ -7625,8 +8569,8 @@ function createSuperDocUI(options) {
7625
8569
  recompute();
7626
8570
  return true;
7627
8571
  };
7628
- const captureFormatPainter = async (selection$1, captureSlice, captureProjected, visibleActiveMarks = []) => {
7629
- const selTarget = captureSlice?.target ?? selection$1?.target;
8572
+ const captureFormatPainter = async (selection, captureSlice, captureProjected, visibleActiveMarks = []) => {
8573
+ const selTarget = captureSlice?.target ?? selection?.target;
7630
8574
  if (!selTarget || selTarget["kind"] !== "text") return null;
7631
8575
  const segments = Array.isArray(selTarget["segments"]) ? selTarget["segments"] : [];
7632
8576
  if (segments.length === 0) return null;
@@ -7638,7 +8582,8 @@ function createSuperDocUI(options) {
7638
8582
  const node = (await readBlockNode(paragraphTarget(segment["blockId"], sourceStory)))?.["node"] ?? null;
7639
8583
  if (!node) continue;
7640
8584
  const para = getParagraphLikeData(node);
7641
- const slicedProps = sliceAndIntersectRunProps(Array.isArray(para?.["inlines"]) ? para["inlines"] : [], segment["range"]);
8585
+ const runs = Array.isArray(para?.["inlines"]) ? para["inlines"] : [];
8586
+ const slicedProps = sliceAndIntersectRunProps(runs, segment["range"]);
7642
8587
  if (!mergedRunProps) mergedRunProps = slicedProps;
7643
8588
  else mergedRunProps = intersectRunProps(mergedRunProps, slicedProps);
7644
8589
  }
@@ -7649,9 +8594,9 @@ function createSuperDocUI(options) {
7649
8594
  underline: "underline",
7650
8595
  strikethrough: "strike"
7651
8596
  };
7652
- const activeMarks = new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection$1?.["activeMarks"]) ? selection$1["activeMarks"] : []), ...visibleActiveMarks]);
8597
+ const activeMarks = /* @__PURE__ */ new Set([...captureSlice?.activeMarks ?? (Array.isArray(selection?.["activeMarks"]) ? selection["activeMarks"] : []), ...visibleActiveMarks]);
7653
8598
  for (const [markName, patchKey] of Object.entries(MARK_TO_PATCH)) if (activeMarks.has(markName)) inlinePatch[patchKey] = true;
7654
- const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection$1, "ready"));
8599
+ const projectedInlineValues = captureProjected ?? await readFormatPainterInlineValues(captureSlice ?? selectionSliceFromInfo(selection, "ready"));
7655
8600
  if (projectedInlineValues.fontFamily) inlinePatch["fontFamily"] = projectedInlineValues.fontFamily;
7656
8601
  if (projectedInlineValues.color) inlinePatch["color"] = projectedInlineValues.color;
7657
8602
  if (projectedInlineValues.highlight) inlinePatch["highlight"] = projectedInlineValues.highlight;
@@ -7688,12 +8633,12 @@ function createSuperDocUI(options) {
7688
8633
  return r["run"]?.["props"];
7689
8634
  };
7690
8635
  if (range.start === range.end) {
7691
- let offset$1 = 0;
8636
+ let offset = 0;
7692
8637
  for (const run of runs) {
7693
8638
  const text = getRunText(run);
7694
- const runStart = offset$1;
7695
- const runEnd = offset$1 + text.length;
7696
- offset$1 = runEnd;
8639
+ const runStart = offset;
8640
+ const runEnd = offset + text.length;
8641
+ offset = runEnd;
7697
8642
  if (range.start >= runStart && range.start <= runEnd) {
7698
8643
  const rPr = getRunProps(run);
7699
8644
  return rPr ? { ...rPr } : {};
@@ -7770,7 +8715,8 @@ function createSuperDocUI(options) {
7770
8715
  if (painter.pointerSelecting || painter.keyboardSelecting) return;
7771
8716
  const selectionApi = getDoc()?.selection;
7772
8717
  const readSelection = async () => {
7773
- return normalizeSelectionInfo(typeof selectionApi?.["current"] === "function" ? await Promise.resolve(selectionApi["current"]({ includeText: true })) : readSelectionInfoLive().value);
8718
+ const rawSelection = typeof selectionApi?.["current"] === "function" ? await Promise.resolve(selectionApi["current"]({ includeText: true })) : readSelectionInfoLive().value;
8719
+ return normalizeSelectionInfo(rawSelection);
7774
8720
  };
7775
8721
  const waitForSelectionSettle = () => new Promise((resolve) => setTimeout(resolve, 16));
7776
8722
  const MAX_SELECTION_ATTEMPTS = 8;
@@ -7796,7 +8742,7 @@ function createSuperDocUI(options) {
7796
8742
  await applyFormatPainter(caretSel);
7797
8743
  }
7798
8744
  };
7799
- const applyFormatPainter = async (selection$1) => {
8745
+ const applyFormatPainter = async (selection) => {
7800
8746
  const snap = painter.snapshot;
7801
8747
  if (!snap) {
7802
8748
  if (painter.mode === "armed") exitFormatPainter();
@@ -7804,21 +8750,21 @@ function createSuperDocUI(options) {
7804
8750
  }
7805
8751
  const doc = getDoc();
7806
8752
  if (!doc) return;
7807
- const targetSelection = selectionSliceFromInfo(selection$1, "ready");
8753
+ const targetSelection = selectionSliceFromInfo(selection, "ready");
7808
8754
  const { lockModesById } = computeActiveContentControlLockModes(doc.contentControls, targetSelection);
7809
8755
  if (contentControlLockReason(lockModesById)) {
7810
8756
  recompute();
7811
8757
  return;
7812
8758
  }
7813
- const selTarget = selection$1["target"];
7814
- const selectionTarget = selection$1["selectionTarget"];
7815
- if (!(selection$1["empty"] === true) && Object.keys(snap.inline).length > 0) {
8759
+ const selTarget = selection["target"];
8760
+ const selectionTarget = selection["selectionTarget"];
8761
+ if (!(selection["empty"] === true) && Object.keys(snap.inline).length > 0) {
7816
8762
  const inlineOp = resolveDocOperation(doc, "format.apply");
7817
8763
  if (inlineOp && selectionTarget) try {
7818
8764
  if (!commandResultSucceeded(commandResultFromOperationResult(await Promise.resolve(callInlineFormatMutation("format.apply", inlineOp, {
7819
8765
  target: selectionTarget,
7820
8766
  inline: snap.inline
7821
- }, selection$1))))) {
8767
+ }, selection))))) {
7822
8768
  recompute();
7823
8769
  return;
7824
8770
  }
@@ -7854,11 +8800,11 @@ function createSuperDocUI(options) {
7854
8800
  if (spacing) {
7855
8801
  const sp = spacing;
7856
8802
  const rawLr = sp["lineRule"] ?? "auto";
7857
- const lr = new Set([
8803
+ const lr = (/* @__PURE__ */ new Set([
7858
8804
  "auto",
7859
8805
  "exact",
7860
8806
  "atLeast"
7861
- ]).has(rawLr) ? rawLr : "auto";
8807
+ ])).has(rawLr) ? rawLr : "auto";
7862
8808
  const rawSpacing = {
7863
8809
  ...sp["before"] != null ? { before: Math.round(sp["before"] * 20) } : {},
7864
8810
  ...sp["after"] != null ? { after: Math.round(sp["after"] * 20) } : {},
@@ -7948,6 +8894,14 @@ function createSuperDocUI(options) {
7948
8894
  };
7949
8895
  const selectionSub = sliceHandle((s) => s.selection);
7950
8896
  const selectionSnap = snapshotHandle(selectionSub);
8897
+ /**
8898
+ * Resolve the host-owned selection apply helper (`editing.selectionTargets`)
8899
+ * with a truthful failure reason. `host.getHandles()` throws a
8900
+ * `V2EditorHostError` with a lifecycle `reason` while the host is booting or
8901
+ * disposed — that is a readiness condition, not a missing capability, so it
8902
+ * maps to `not-ready` instead of being swallowed into
8903
+ * `host-capability-unavailable`.
8904
+ */
7951
8905
  const getSelectionApplyHelper = () => {
7952
8906
  const host = getHost();
7953
8907
  if (typeof host?.getHandles !== "function") return { reason: SUPERDOC_UI_REASONS.hostCapabilityUnavailable };
@@ -7966,8 +8920,8 @@ function createSuperDocUI(options) {
7966
8920
  const host = getHost();
7967
8921
  if (typeof host?.getHandles !== "function") return;
7968
8922
  try {
7969
- const contentControls$1 = (host.getHandles()?.editing)?.contentControls;
7970
- if (typeof contentControls$1?.activate === "function") contentControls$1.activate({ id });
8923
+ const contentControls = (host.getHandles()?.editing)?.contentControls;
8924
+ if (typeof contentControls?.activate === "function") contentControls.activate({ id });
7971
8925
  } catch {}
7972
8926
  };
7973
8927
  const selection = {
@@ -8095,7 +9049,17 @@ function createSuperDocUI(options) {
8095
9049
  execute: executeCommand,
8096
9050
  executeAsync: executeCommandAsync
8097
9051
  };
8098
- const scrollTargetIntoView = async (target, options$1) => {
9052
+ /**
9053
+ * Scroll a resolved document target into view through the host navigation
9054
+ * surface (`host.scrollTargetIntoView`). Shared by comments / tracked changes /
9055
+ * content controls and the generic `viewport.scrollIntoView`. Alignment
9056
+ * defaults to `block: 'center'` / `behavior: 'smooth'` (matching the generic
9057
+ * method and v1) so every scroll path lands consistently; callers may
9058
+ * override. Fails closed with a stable reason rather than a no-op recompute;
9059
+ * a resolved target that cannot be made visible is `target-not-visible`, not
9060
+ * `target-unresolved`.
9061
+ */
9062
+ const scrollTargetIntoView = async (target, options) => {
8099
9063
  if (!getEditor()) return {
8100
9064
  success: false,
8101
9065
  ok: false,
@@ -8116,10 +9080,10 @@ function createSuperDocUI(options) {
8116
9080
  try {
8117
9081
  const input = {
8118
9082
  target,
8119
- block: options$1?.block ?? "center",
8120
- behavior: options$1?.behavior ?? "smooth"
9083
+ block: options?.block ?? "center",
9084
+ behavior: options?.behavior ?? "smooth"
8121
9085
  };
8122
- const result = await Promise.resolve(options$1?.shouldContinue ? scroll.call(host, input, options$1.shouldContinue) : scroll.call(host, input));
9086
+ const result = await Promise.resolve(options?.shouldContinue ? scroll.call(host, input, options.shouldContinue) : scroll.call(host, input));
8123
9087
  if (result && typeof result === "object" && result.success === false) return {
8124
9088
  success: false,
8125
9089
  ok: false,
@@ -8137,6 +9101,11 @@ function createSuperDocUI(options) {
8137
9101
  };
8138
9102
  }
8139
9103
  };
9104
+ /**
9105
+ * Resolve a document entity (comment / tracked change) to its anchor target.
9106
+ * Prefers the already-loaded list row, falling back to a live `get({ id })`
9107
+ * read when the list does not carry the row.
9108
+ */
8140
9109
  const resolveEntityTarget = async (namespace, id, loaded, request) => {
8141
9110
  const requestedStory = namespace === "trackChanges" ? request?.story : void 0;
8142
9111
  const withRequestedStory = (target) => {
@@ -8189,6 +9158,18 @@ function createSuperDocUI(options) {
8189
9158
  status
8190
9159
  }), fallback), fallback);
8191
9160
  }
9161
+ /**
9162
+ * The refusal every comment write shares, or `null` when the policy permits.
9163
+ *
9164
+ * `readOnly` is policy rather than presentation, so it cannot be left to the
9165
+ * built-in dialog hiding its affordances: a custom comment UI calls
9166
+ * `createFromCapture`, `createFromSelection`, `reply`, and `delete` directly,
9167
+ * and the Document API underneath carries no policy of its own. Missing one
9168
+ * route fails open on exactly the surface the policy exists for.
9169
+ *
9170
+ * `allowResolve` is deliberately not checked here — it forbids only the
9171
+ * resolve/reopen transition, which `patchCommentStatus` owns.
9172
+ */
8192
9173
  const commentWriteRefusal = () => reviewMutationsAreReadOnly() ? failedReceipt("Comments are read-only.", "DOCUMENT_READONLY") : null;
8193
9174
  const filterCommentsSnapshot = (items, query) => {
8194
9175
  const record = query && typeof query === "object" ? query : null;
@@ -8323,7 +9304,7 @@ function createSuperDocUI(options) {
8323
9304
  }
8324
9305
  };
8325
9306
  const trackChangesSub = sliceHandle((s) => s.trackChanges);
8326
- const navigateTrackChange = (step, options$1) => {
9307
+ const navigateTrackChange = (step, options) => {
8327
9308
  const allRows = trackChangesSub.get().items.filter((item) => readEntityId(item) !== null);
8328
9309
  if (allRows.length === 0) return null;
8329
9310
  const currentId = state.trackChanges.activeId;
@@ -8338,7 +9319,7 @@ function createSuperDocUI(options) {
8338
9319
  setExplicitActiveChange(story ? {
8339
9320
  id: activeId,
8340
9321
  story
8341
- } : { id: activeId }, options$1);
9322
+ } : { id: activeId }, options);
8342
9323
  recompute();
8343
9324
  return activeId;
8344
9325
  };
@@ -8559,11 +9540,12 @@ function createSuperDocUI(options) {
8559
9540
  id: input,
8560
9541
  story: void 0
8561
9542
  } : input;
8562
- return executeTrackDecisionTarget(kind, {
9543
+ const target = {
8563
9544
  kind: "id",
8564
9545
  id,
8565
9546
  ...story ? { story } : {}
8566
- }, id, story);
9547
+ };
9548
+ return executeTrackDecisionTarget(kind, target, id, story);
8567
9549
  };
8568
9550
  const executeTrackDecisionTarget = (kind, target, changeId, story) => {
8569
9551
  try {
@@ -8813,9 +9795,9 @@ function createSuperDocUI(options) {
8813
9795
  const hits = [];
8814
9796
  for (const hit of rawHits) {
8815
9797
  if (hit.type !== "trackedChange") {
8816
- const key$1 = `${hit.type}:${hit.id}`;
8817
- if (seen.has(key$1)) continue;
8818
- seen.add(key$1);
9798
+ const key = `${hit.type}:${hit.id}`;
9799
+ if (seen.has(key)) continue;
9800
+ seen.add(key);
8819
9801
  hits.push(hit);
8820
9802
  continue;
8821
9803
  }
@@ -8871,6 +9853,21 @@ function createSuperDocUI(options) {
8871
9853
  }
8872
9854
  entry.boundHost = null;
8873
9855
  };
9856
+ /**
9857
+ * Move every live geometry subscription to the editor that is now active.
9858
+ * Without this, scroll and repaint on the replacement editor never reach a
9859
+ * listener that was registered against the previous one, so overlays stop
9860
+ * tracking the document while still responding to selection and zoom.
9861
+ */
9862
+ /**
9863
+ * Release every geometry subscription once a hostless moment turns out to be
9864
+ * permanent. Deferred rather than immediate because a null host is ambiguous
9865
+ * at the instant it arrives: `registerV2Runtime` unregisters the outgoing
9866
+ * runtime and installs the replacement in ONE synchronous block, so a refresh
9867
+ * shows up here as a null followed by the new host with nothing in between.
9868
+ * By the time a microtask runs, a refresh has already installed its host and a
9869
+ * true clear (`removeDocument()`, a fail-closed projection) still has none.
9870
+ */
8874
9871
  const releaseGeometryObserversIfStillHostless = () => {
8875
9872
  geometryReleaseScheduled = false;
8876
9873
  if (disposed || getHost()) return;
@@ -9007,6 +10004,7 @@ function createSuperDocUI(options) {
9007
10004
  return target && typeof target === "object" ? target : null;
9008
10005
  };
9009
10006
  const metadataResolveKey = (id) => `metadata:resolve:${id}`;
10007
+ /** Resolve a metadata id to its SelectionTarget, or null when unresolved. */
9010
10008
  const resolveMetadataTarget = (id) => {
9011
10009
  const metaApi = getDoc()?.metadata;
9012
10010
  if (!metaApi || typeof metaApi.resolve !== "function") return null;
@@ -9090,16 +10088,16 @@ function createSuperDocUI(options) {
9090
10088
  let searchState = { ...SEARCH_UNAVAILABLE_SLICE };
9091
10089
  const searchListeners = /* @__PURE__ */ new Set();
9092
10090
  const getHostSearch = () => {
9093
- const search$1 = getHost()?.search;
9094
- return search$1 && typeof search$1 === "object" ? search$1 : null;
10091
+ const search = getHost()?.search;
10092
+ return search && typeof search === "object" ? search : null;
9095
10093
  };
9096
10094
  const getEditCommandSearch = () => {
9097
- const search$1 = getEditCommands()?.search;
9098
- return search$1 && typeof search$1 === "object" ? search$1 : null;
10095
+ const search = getEditCommands()?.search;
10096
+ return search && typeof search === "object" ? search : null;
9099
10097
  };
9100
10098
  const searchIsAvailable = () => {
9101
- const search$1 = getHostSearch();
9102
- if (search$1 && typeof search$1.setSession === "function") return true;
10099
+ const search = getHostSearch();
10100
+ if (search && typeof search.setSession === "function") return true;
9103
10101
  const editSearch = getEditCommandSearch();
9104
10102
  return Boolean(editSearch) && typeof editSearch.query === "function" && typeof editSearch.getState === "function";
9105
10103
  };
@@ -9117,7 +10115,21 @@ function createSuperDocUI(options) {
9117
10115
  return searchState;
9118
10116
  };
9119
10117
  let searchRequestGeneration = 0;
10118
+ /**
10119
+ * How to tear down the session that is currently open, captured when it was
10120
+ * opened.
10121
+ *
10122
+ * A closure rather than a reference to the facade, because the two search
10123
+ * paths are released differently: a host session ends with `host.clear()`,
10124
+ * while the worker-backed fallback ends by querying the empty string through
10125
+ * `editCommands.search`. Both bind to the editor that owned them, and by the
10126
+ * time an active-editor change reaches us `activeEditor` already points at the
10127
+ * replacement, so neither is reachable by looking it up again. Capturing the
10128
+ * teardown keeps the two paths from drifting apart: whichever one opens a
10129
+ * session is the one that says how to close it.
10130
+ */
9120
10131
  let releaseActiveSearchSession = null;
10132
+ /** Release whatever session is open, then forget it. */
9121
10133
  const releaseSearchSession = () => {
9122
10134
  const release = releaseActiveSearchSession;
9123
10135
  releaseActiveSearchSession = null;
@@ -9127,6 +10139,23 @@ function createSuperDocUI(options) {
9127
10139
  return null;
9128
10140
  }, null);
9129
10141
  };
10142
+ /**
10143
+ * Drop everything the previous active editor owned.
10144
+ *
10145
+ * Three separate hazards. An in-flight async query still holds a generation
10146
+ * that is current, so without bumping the token it would resolve and publish
10147
+ * the old document's matches into the new one. The slice itself describes a
10148
+ * document that is gone: leaving it in place reports a total and an active
10149
+ * match for content nobody is looking at any more. And the previous host still
10150
+ * holds the session it painted, so without clearing it the old document keeps
10151
+ * its highlights and switching back shows a closed slice over a host that
10152
+ * still has matches.
10153
+ *
10154
+ * This closes the session rather than re-running the query against the
10155
+ * replacement document. Re-running is a plausible product choice and arguably
10156
+ * the nicer one, but it is a feature decision, not the correctness fix, so it
10157
+ * is deliberately not made here.
10158
+ */
9130
10159
  const resetSearchForActiveEditorChange = () => {
9131
10160
  searchRequestGeneration += 1;
9132
10161
  releaseSearchSession();
@@ -9307,11 +10336,11 @@ function createSuperDocUI(options) {
9307
10336
  if (!searchState.available) searchState.reason = SUPERDOC_UI_REASONS.searchUnavailable;
9308
10337
  emitSearch();
9309
10338
  },
9310
- search: (query, options$1) => {
10339
+ search: (query, options) => {
9311
10340
  const host = getHostSearch();
9312
- const caseSensitive = Boolean(options$1?.caseSensitive);
9313
- const includeDeletedText = options$1?.includeDeletedText === true;
9314
- const regex = options$1?.regex === true;
10341
+ const caseSensitive = Boolean(options?.caseSensitive);
10342
+ const includeDeletedText = options?.includeDeletedText === true;
10343
+ const regex = options?.regex === true;
9315
10344
  const generation = ++searchRequestGeneration;
9316
10345
  if (!host || typeof host.setSession !== "function") {
9317
10346
  const editSearch = getEditCommandSearch();
@@ -9371,12 +10400,13 @@ function createSuperDocUI(options) {
9371
10400
  releaseActiveSearchSession = () => {
9372
10401
  if (typeof host.clear === "function") host.clear();
9373
10402
  };
9374
- applyHostSearchResult(safeCall(() => host.setSession(query, {
10403
+ const result = safeCall(() => host.setSession(query, {
9375
10404
  caseSensitive,
9376
10405
  includeDeletedText,
9377
10406
  ...regex ? { regex: true } : {},
9378
10407
  highlight: true
9379
- }), null));
10408
+ }), null);
10409
+ applyHostSearchResult(result);
9380
10410
  if (query.length > 0 && searchState.total === 0 && searchState.reason !== SUPERDOC_UI_REASONS.searchInvalidPattern) {
9381
10411
  const editSearch = getEditCommandSearch();
9382
10412
  if (editSearch && typeof editSearch.query === "function" && typeof editSearch.getState === "function") {
@@ -9506,13 +10536,13 @@ function createSuperDocUI(options) {
9506
10536
  ok: false,
9507
10537
  reason: SUPERDOC_UI_REASONS.operationUnavailable
9508
10538
  };
9509
- const result$1 = safeCall(() => editSearch.replace({ replacement: typeof replacement === "string" ? replacement : "" }), null);
9510
- if (isPromiseLike(result$1)) {
10539
+ const result = safeCall(() => editSearch.replace({ replacement: typeof replacement === "string" ? replacement : "" }), null);
10540
+ if (isPromiseLike(result)) {
9511
10541
  const generation = searchRequestGeneration;
9512
10542
  syncSearchStateFromHost();
9513
10543
  emitSearch();
9514
10544
  const isCurrent = () => generation === searchRequestGeneration;
9515
- return Promise.resolve(result$1).then((resolved) => {
10545
+ return Promise.resolve(result).then((resolved) => {
9516
10546
  if (isCurrent()) {
9517
10547
  applyHostSearchResult(resolved, readEditCommandCanReplace());
9518
10548
  syncSearchStateFromHost();
@@ -9532,7 +10562,7 @@ function createSuperDocUI(options) {
9532
10562
  }
9533
10563
  syncSearchStateFromHost();
9534
10564
  emitSearch();
9535
- return mapHostReplaceResult(result$1);
10565
+ return mapHostReplaceResult(result);
9536
10566
  }
9537
10567
  if (typeof host.replaceCurrent !== "function") return {
9538
10568
  ok: false,
@@ -9555,13 +10585,13 @@ function createSuperDocUI(options) {
9555
10585
  ok: false,
9556
10586
  reason: SUPERDOC_UI_REASONS.operationUnavailable
9557
10587
  };
9558
- const result$1 = safeCall(() => editSearch.replaceAll({ replacement: typeof replacement === "string" ? replacement : "" }), null);
9559
- if (isPromiseLike(result$1)) {
10588
+ const result = safeCall(() => editSearch.replaceAll({ replacement: typeof replacement === "string" ? replacement : "" }), null);
10589
+ if (isPromiseLike(result)) {
9560
10590
  const generation = searchRequestGeneration;
9561
10591
  syncSearchStateFromHost();
9562
10592
  emitSearch();
9563
10593
  const isCurrent = () => generation === searchRequestGeneration;
9564
- return Promise.resolve(result$1).then((resolved) => {
10594
+ return Promise.resolve(result).then((resolved) => {
9565
10595
  if (isCurrent()) {
9566
10596
  applyHostSearchResult(resolved, readEditCommandCanReplace());
9567
10597
  syncSearchStateFromHost();
@@ -9581,7 +10611,7 @@ function createSuperDocUI(options) {
9581
10611
  }
9582
10612
  syncSearchStateFromHost();
9583
10613
  emitSearch();
9584
- return mapHostReplaceResult(result$1);
10614
+ return mapHostReplaceResult(result);
9585
10615
  }
9586
10616
  if (typeof host.replaceAll !== "function") return {
9587
10617
  ok: false,
@@ -9600,7 +10630,7 @@ function createSuperDocUI(options) {
9600
10630
  getSnapshot: stylesSnap.getSnapshot,
9601
10631
  subscribe: stylesSnap.subscribe,
9602
10632
  observe: stylesSnap.observe,
9603
- getCatalog: (options$1) => readStyleCatalogLive(options$1).value,
10633
+ getCatalog: (options) => readStyleCatalogLive(options).value,
9604
10634
  getQuickGallery: () => stylesSub.get().quickGallery,
9605
10635
  getActiveParagraphStyle: () => computeActiveParagraphStyle(state.selection, getStyleCatalog().cache).style
9606
10636
  };
@@ -9747,6 +10777,12 @@ function createSuperDocUI(options) {
9747
10777
  controllerRef = controller;
9748
10778
  return controller;
9749
10779
  }
10780
+ /**
10781
+ * Default font-family offerings, used to backfill the picker when the active document resolves few
10782
+ * (or no) fonts and the runtime picker list is unavailable. Values are logical Word family names —
10783
+ * the formatting command writes them verbatim as logical `w:rFonts` intent; the runtime resolves
10784
+ * substitutes only at measure/paint time.
10785
+ */
9750
10786
  var DEFAULT_FONT_FAMILY_OPTIONS = [
9751
10787
  {
9752
10788
  value: "Arial",
@@ -9779,6 +10815,11 @@ var DEFAULT_FONT_FAMILY_OPTIONS = [
9779
10815
  previewFamily: "Verdana, sans-serif"
9780
10816
  }
9781
10817
  ];
10818
+ /**
10819
+ * The Word-facing logical family: first family of a CSS stack, quotes stripped.
10820
+ * Document / command values sometimes carry `"Cambria, serif"`; the picker must
10821
+ * advertise `Cambria` so it dedupes against picker/default rows.
10822
+ */
9782
10823
  function canonicalFontFamilyName(family) {
9783
10824
  const trimmed = family.trim();
9784
10825
  const quote = trimmed[0];
@@ -9789,6 +10830,10 @@ function canonicalFontFamilyName(family) {
9789
10830
  const comma = trimmed.indexOf(",");
9790
10831
  return (comma === -1 ? trimmed : trimmed.slice(0, comma)).trim().replace(/^["']|["']$/g, "");
9791
10832
  }
10833
+ /**
10834
+ * Normalize document-only rows (`DocumentFontOption` with `logicalFamily`) into the custom-UI
10835
+ * picker shape. Raw document families may be CSS stacks, so canonicalize to the first family.
10836
+ */
9792
10837
  function normalizeDocumentFontOptions(raw) {
9793
10838
  if (!Array.isArray(raw)) return [];
9794
10839
  const out = [];
@@ -9806,6 +10851,11 @@ function normalizeDocumentFontOptions(raw) {
9806
10851
  }
9807
10852
  return out;
9808
10853
  }
10854
+ /**
10855
+ * Normalize picker rows (`FontFamilyOption`) without treating their value as a CSS stack. The font
10856
+ * runtime has already converted quoted document stacks such as `"Acme, Inc Sans", serif` into the
10857
+ * exact apply value (`Acme, Inc Sans`), so preserving `value` avoids truncating internal commas.
10858
+ */
9809
10859
  function normalizePickerFontOptions(raw) {
9810
10860
  if (!Array.isArray(raw)) return [];
9811
10861
  const out = [];
@@ -9824,6 +10874,11 @@ function normalizePickerFontOptions(raw) {
9824
10874
  }
9825
10875
  return out;
9826
10876
  }
10877
+ /**
10878
+ * Compose document font options on top of the picker/default rows: document rows lead and win on
10879
+ * value collisions (case-insensitive), defaults backfill the rest. Order within each group is
10880
+ * preserved so document-used families stay at the front of the custom-UI picker (SD-3887).
10881
+ */
9827
10882
  function composeFontFamilyOptions(documentOptions, pickerOptions = []) {
9828
10883
  const out = [];
9829
10884
  const seen = /* @__PURE__ */ new Set();
@@ -9880,9 +10935,11 @@ var DEFAULT_FONT_SIZE_OPTIONS = [
9880
10935
  label: "36"
9881
10936
  }
9882
10937
  ];
10938
+ /** Active state for a routed command: true when its mark is live at the selection. */
9883
10939
  function commandIsActive(descriptor, selection) {
9884
10940
  return descriptor.activeMark ? selection.activeMarks.includes(descriptor.activeMark) : false;
9885
10941
  }
10942
+ //#endregion
9886
10943
  Object.defineProperty(exports, "BUILT_IN_COMMAND_IDS", {
9887
10944
  enumerable: true,
9888
10945
  get: function() {
@@ -9895,12 +10952,6 @@ Object.defineProperty(exports, "DOM_CLASS_NAMES", {
9895
10952
  return DOM_CLASS_NAMES;
9896
10953
  }
9897
10954
  });
9898
- Object.defineProperty(exports, "composeAuthorColorResolver", {
9899
- enumerable: true,
9900
- get: function() {
9901
- return composeAuthorColorResolver;
9902
- }
9903
- });
9904
10955
  Object.defineProperty(exports, "createSuperDocUI", {
9905
10956
  enumerable: true,
9906
10957
  get: function() {