@beyondwork/docx-react-component 1.0.84 → 1.0.85

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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/api/internal/build-ref-projections.ts +3 -0
  3. package/src/api/public-types.ts +38 -0
  4. package/src/api/v3/_runtime-handle.ts +11 -0
  5. package/src/api/v3/runtime/content.ts +148 -1
  6. package/src/api/v3/runtime/formatting.ts +41 -0
  7. package/src/api/v3/runtime/review.ts +98 -0
  8. package/src/core/commands/index.ts +81 -25
  9. package/src/core/state/editor-state.ts +15 -0
  10. package/src/io/ooxml/header-footer-reference.ts +38 -0
  11. package/src/io/ooxml/parse-headers-footers.ts +11 -23
  12. package/src/io/ooxml/parse-main-document.ts +7 -10
  13. package/src/model/canonical-document.ts +9 -0
  14. package/src/model/review/comment-types.ts +2 -0
  15. package/src/runtime/document-runtime.ts +677 -54
  16. package/src/runtime/formatting/field/resolver.ts +73 -8
  17. package/src/runtime/layout/layout-engine-version.ts +31 -12
  18. package/src/runtime/layout/paginated-layout-engine.ts +18 -11
  19. package/src/runtime/layout/public-facet.ts +119 -16
  20. package/src/runtime/layout/resolve-page-fields.ts +68 -6
  21. package/src/runtime/layout/resolve-page-previews.ts +1 -1
  22. package/src/runtime/suggestions-snapshot.ts +24 -0
  23. package/src/runtime/surface-projection.ts +59 -2
  24. package/src/shell/ref-commands.ts +3 -354
  25. package/src/shell/session-bootstrap.ts +8 -0
  26. package/src/ui/WordReviewEditor.tsx +95 -9
  27. package/src/ui/editor-command-bag.ts +3 -1
  28. package/src/ui/headless/revision-decoration-model.ts +13 -0
  29. package/src/ui/headless/selection-tool-types.ts +2 -0
  30. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
  31. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
  32. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
  33. package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
  34. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
  35. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
  36. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
  37. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
  38. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
  39. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
  40. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
  41. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
  42. package/src/ui-tailwind/review-workspace/types.ts +3 -2
  43. package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
  44. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
  45. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
  46. package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
@@ -1536,7 +1536,34 @@ function appendInlineSegments(
1536
1536
  node.fieldFamily === "NOTEREF" ||
1537
1537
  node.fieldFamily === "TOC" ||
1538
1538
  node.fieldFamily === "PAGE" ||
1539
- node.fieldFamily === "NUMPAGES";
1539
+ node.fieldFamily === "NUMPAGES" ||
1540
+ node.fieldFamily === "SECTIONPAGES";
1541
+ const isPageScopedField =
1542
+ node.fieldFamily === "PAGE" ||
1543
+ node.fieldFamily === "NUMPAGES" ||
1544
+ node.fieldFamily === "SECTIONPAGES";
1545
+ if (isPageScopedField) {
1546
+ const fieldLabel =
1547
+ node.fieldFamily === "PAGE"
1548
+ ? "Current page number"
1549
+ : node.fieldFamily === "NUMPAGES"
1550
+ ? "Total pages"
1551
+ : "Section pages";
1552
+ const displayText = flattenSurfaceFieldDisplayText(node.children);
1553
+ paragraph.segments.push({
1554
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
1555
+ kind: "field_ref",
1556
+ from: start,
1557
+ to: start + 1,
1558
+ fieldFamily: node.fieldFamily,
1559
+ fieldTarget: node.fieldTarget,
1560
+ instruction: node.instruction,
1561
+ refreshStatus: node.refreshStatus ?? "stale",
1562
+ label: fieldLabel,
1563
+ ...(displayText ? { displayText } : {}),
1564
+ } as SurfaceInlineSegment);
1565
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
1566
+ }
1540
1567
  if (node.children && node.children.length > 0) {
1541
1568
  // For REF \h, pass the bookmark as a hyperlink href so child text gets hyperlink styling
1542
1569
  const refHyperlinkHref =
@@ -1574,7 +1601,9 @@ function appendInlineSegments(
1574
1601
  ? "Current page number"
1575
1602
  : node.fieldFamily === "NUMPAGES"
1576
1603
  ? "Total pages"
1577
- : `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
1604
+ : node.fieldFamily === "SECTIONPAGES"
1605
+ ? "Section pages"
1606
+ : `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
1578
1607
  paragraph.segments.push({
1579
1608
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
1580
1609
  kind: "field_ref",
@@ -1743,6 +1772,34 @@ function normalizeSafeCssHexColor(value: string | undefined): string | undefined
1743
1772
  return trimmed.replace(/^#/, "").toUpperCase();
1744
1773
  }
1745
1774
 
1775
+ function flattenSurfaceFieldDisplayText(
1776
+ children: readonly InlineNode[] | undefined,
1777
+ ): string {
1778
+ if (!children || children.length === 0) return "";
1779
+ const parts: string[] = [];
1780
+ for (const child of children) {
1781
+ switch (child.type) {
1782
+ case "text":
1783
+ parts.push(child.text);
1784
+ break;
1785
+ case "tab":
1786
+ case "hard_break":
1787
+ parts.push(" ");
1788
+ break;
1789
+ case "symbol":
1790
+ parts.push(child.char ? String.fromCodePoint(parseInt(child.char, 16)) : "\uFFFD");
1791
+ break;
1792
+ case "field":
1793
+ case "hyperlink":
1794
+ parts.push(flattenSurfaceFieldDisplayText(child.children));
1795
+ break;
1796
+ default:
1797
+ break;
1798
+ }
1799
+ }
1800
+ return parts.join("");
1801
+ }
1802
+
1746
1803
  /**
1747
1804
  * V2c.5 — Extract the first paragraph's plain text from a parsed
1748
1805
  * `txbxBlocks` tree for the `txbxText` segment preview. The recursion
@@ -30,7 +30,6 @@
30
30
  import type {
31
31
  EditorSessionState,
32
32
  EditorStoryTarget,
33
- FormattingAlignment,
34
33
  HeaderFooterLinkPatch,
35
34
  InsertImageOptions,
36
35
  InsertTableOptions,
@@ -41,7 +40,6 @@ import type {
41
40
  SelectionSnapshot as PublicSelectionSnapshot,
42
41
  StyleCatalogSnapshot,
43
42
  SurfaceBlockSnapshot,
44
- SurfaceInlineSegment,
45
43
  TableOp,
46
44
  } from "../api/public-types";
47
45
  import {
@@ -51,7 +49,7 @@ import {
51
49
  storyTargetsEqual,
52
50
  type TransactionMapping,
53
51
  } from "../core/selection/mapping.ts";
54
- import { applyFormattingOperationToDocument } from "../core/commands/formatting-commands.ts";
52
+ import type { FormattingOperation } from "../core/commands/formatting-commands.ts";
55
53
  import {
56
54
  applyParagraphStyleToDocument,
57
55
  applyTableStyleToDocument,
@@ -220,178 +218,15 @@ export function getRuntimeStyleCatalog(
220
218
  // Formatting (flat + suggesting-mode)
221
219
  // ---------------------------------------------------------------------------
222
220
 
223
- type FormattingOperation =
224
- | { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
225
- | { type: "set-font-family"; fontFamily: string | null }
226
- | { type: "set-font-size"; size: number | null }
227
- | { type: "set-text-color"; color: string | null }
228
- | { type: "set-highlight-color"; color: string | null }
229
- | { type: "set-alignment"; alignment: FormattingAlignment }
230
- | { type: "indent" }
231
- | { type: "outdent" };
232
-
233
221
  export function applyRuntimeFormattingOperation(
234
222
  runtime: WordReviewEditorRuntime,
235
223
  operation: FormattingOperation,
236
224
  ): void {
237
- if (isSelectionSuggesting(runtime)) {
238
- if (applySuggestingFormattingOperation(runtime, operation)) {
239
- return;
240
- }
241
- }
242
- if (emitSuggestingUnsupportedMutation(runtime, getFormattingOperationCommandName(operation))) {
243
- return;
244
- }
245
- const context = getStoryMutationContext(runtime, getFormattingOperationCommandName(operation));
246
- if (!context) {
247
- return;
248
- }
249
-
250
- const result = applyFormattingOperationToDocument(
251
- context.localDocument,
252
- context.localSnapshot,
253
- operation,
254
- );
255
- dispatchStoryMutationResult(
256
- runtime,
257
- context,
258
- {
259
- ...result,
260
- selection: toRuntimeSelectionSnapshot(result.selection),
261
- },
262
- context.timestamp,
263
- );
264
- }
265
-
266
- function applySuggestingFormattingOperation(
267
- runtime: WordReviewEditorRuntime,
268
- operation: FormattingOperation,
269
- ): boolean {
270
- const commandName = getFormattingOperationCommandName(operation);
271
- const context = getStoryMutationContext(runtime, commandName);
272
- if (!context) {
273
- return true;
274
- }
275
- if (context.activeStory.kind !== "main") {
276
- runtime.emitBlockedCommand(commandName, [{
277
- code: "suggesting_unsupported",
278
- message: `"${commandName}" is not supported in suggesting mode for this story.`,
279
- }]);
280
- return true;
281
- }
282
-
283
- if (operation.type === "set-alignment" || operation.type === "indent" || operation.type === "outdent") {
284
- const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
285
- if (!paragraphContext) {
286
- return true;
287
- }
288
- const beforeXml = buildParagraphPropertyBeforeXml(paragraphContext.paragraph);
289
- const result = applyFormattingOperationToDocument(
290
- context.localDocument,
291
- context.localSnapshot,
292
- operation,
293
- );
294
- if (!result.changed) {
295
- return true;
296
- }
297
- const nextDocument = appendPropertyChangeSuggestion(
298
- result.document,
299
- {
300
- from: paragraphContext.paragraph.from,
301
- to: paragraphContext.paragraph.to,
302
- },
303
- {
304
- originalRevisionType: "pPrChange",
305
- xmlTag: "pPrChange",
306
- beforeXml,
307
- semanticKind: "paragraph-property-change",
308
- storyTarget: context.activeStory,
309
- authorId: runtime.getDefaultAuthorId?.(),
310
- },
311
- context.timestamp,
312
- );
313
- dispatchStoryMutationResult(
314
- runtime,
315
- context,
316
- {
317
- changed: true,
318
- document: nextDocument,
319
- selection: toRuntimeSelectionSnapshot(result.selection),
320
- },
321
- context.timestamp,
322
- );
323
- return true;
324
- }
325
-
326
- const segment = findSingleSelectedTextSegment(context.localSnapshot);
327
- if (!segment) {
328
- runtime.emitBlockedCommand(commandName, [{
329
- code: "suggesting_unsupported",
330
- message: `"${commandName}" requires one bounded text segment in suggesting mode.`,
331
- }]);
332
- return true;
333
- }
334
- const beforeXml = buildRunPropertyBeforeXml(segment);
335
- const result = applyFormattingOperationToDocument(
336
- context.localDocument,
337
- context.localSnapshot,
338
- operation,
339
- );
340
- if (!result.changed) {
341
- return true;
342
- }
343
- const nextDocument = appendPropertyChangeSuggestion(
344
- result.document,
345
- {
346
- from: segment.from,
347
- to: segment.to,
348
- },
349
- {
350
- originalRevisionType: "rPrChange",
351
- xmlTag: "rPrChange",
352
- beforeXml,
353
- semanticKind: "formatting-change",
354
- storyTarget: context.activeStory,
355
- authorId: runtime.getDefaultAuthorId?.(),
356
- },
357
- context.timestamp,
358
- );
359
- dispatchStoryMutationResult(
360
- runtime,
361
- context,
362
- {
363
- changed: true,
364
- document: nextDocument,
365
- selection: toRuntimeSelectionSnapshot(result.selection),
366
- },
367
- context.timestamp,
368
- );
369
- return true;
370
- }
371
-
372
- function getFormattingOperationCommandName(operation: FormattingOperation): string {
373
- switch (operation.type) {
374
- case "toggle":
375
- return `toggle${operation.mark.charAt(0).toUpperCase()}${operation.mark.slice(1)}`;
376
- case "set-font-family":
377
- return "setFontFamily";
378
- case "set-font-size":
379
- return "setFontSize";
380
- case "set-text-color":
381
- return "setTextColor";
382
- case "set-highlight-color":
383
- return "setHighlightColor";
384
- case "set-alignment":
385
- return "setAlignment";
386
- case "indent":
387
- return "indent";
388
- case "outdent":
389
- return "outdent";
390
- }
225
+ runtime.applyFormattingOperation(operation);
391
226
  }
392
227
 
393
228
  // ---------------------------------------------------------------------------
394
- // Suggesting-mode property-change plumbing
229
+ // Suggesting-mode guard helper for commands without bounded redline support
395
230
  // ---------------------------------------------------------------------------
396
231
 
397
232
  function emitSuggestingUnsupportedMutation(
@@ -409,192 +244,6 @@ function emitSuggestingUnsupportedMutation(
409
244
  return true;
410
245
  }
411
246
 
412
- function appendPropertyChangeSuggestion(
413
- document: EditorSessionState["canonicalDocument"],
414
- anchor: { from: number; to: number },
415
- input: {
416
- originalRevisionType: "rPrChange" | "pPrChange";
417
- xmlTag: "rPrChange" | "pPrChange";
418
- beforeXml: string;
419
- semanticKind: "formatting-change" | "paragraph-property-change";
420
- storyTarget: EditorStoryTarget;
421
- authorId?: string;
422
- },
423
- timestamp: string,
424
- ): EditorSessionState["canonicalDocument"] {
425
- const existing = document.review.revisions;
426
- const changeId = createRuntimeSuggestionChangeId(existing, timestamp);
427
- const resolvedAuthorId = input.authorId ?? "unknown";
428
- return {
429
- ...document,
430
- review: {
431
- ...document.review,
432
- revisions: {
433
- ...existing,
434
- [changeId]: {
435
- changeId,
436
- kind: "property-change",
437
- anchor: createRangeAnchor(anchor.from, anchor.to, { start: 1, end: -1 }),
438
- authorId: resolvedAuthorId,
439
- createdAt: timestamp,
440
- warningIds: [],
441
- metadata: {
442
- source: "runtime",
443
- storyTarget: input.storyTarget,
444
- suggestionId: changeId,
445
- semanticKind: input.semanticKind,
446
- originalRevisionType: input.originalRevisionType,
447
- propertyChangeData: {
448
- xmlTag: input.xmlTag,
449
- beforeXml: input.beforeXml,
450
- },
451
- },
452
- status: "open",
453
- },
454
- },
455
- },
456
- };
457
- }
458
-
459
- function createRuntimeSuggestionChangeId(
460
- existing: EditorSessionState["canonicalDocument"]["review"]["revisions"],
461
- timestamp: string,
462
- ): string {
463
- const base = `change-${timestamp.replace(/[^0-9]/gu, "")}`;
464
- let counter = Object.keys(existing).length + 1;
465
- let candidate = `${base}-p${counter}`;
466
- while (existing[candidate]) {
467
- counter += 1;
468
- candidate = `${base}-p${counter}`;
469
- }
470
- return candidate;
471
- }
472
-
473
- function findSingleSelectedTextSegment(
474
- snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
475
- ): Extract<SurfaceInlineSegment, { kind: "text" }> | null {
476
- if (!snapshot.surface || snapshot.selection.activeRange.kind !== "range" || snapshot.selection.isCollapsed) {
477
- return null;
478
- }
479
- const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
480
- const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
481
- const segments = collectSelectedTextSegments(snapshot.surface.blocks, selectionFrom, selectionTo);
482
- if (segments.length !== 1) {
483
- return null;
484
- }
485
- const [segment] = segments;
486
- if (!segment || segment.from !== selectionFrom || segment.to !== selectionTo) {
487
- return null;
488
- }
489
- return segment;
490
- }
491
-
492
- function collectSelectedTextSegments(
493
- blocks: readonly SurfaceBlockSnapshot[],
494
- selectionFrom: number,
495
- selectionTo: number,
496
- output: Array<Extract<SurfaceInlineSegment, { kind: "text" }>> = [],
497
- ): Array<Extract<SurfaceInlineSegment, { kind: "text" }>> {
498
- for (const block of blocks) {
499
- if (block.kind === "paragraph") {
500
- for (const segment of block.segments) {
501
- if (
502
- segment.kind === "text" &&
503
- rangesOverlap(selectionFrom, selectionTo, segment.from, segment.to)
504
- ) {
505
- output.push(segment);
506
- }
507
- }
508
- continue;
509
- }
510
- if (block.kind === "table") {
511
- for (const row of block.rows) {
512
- for (const cell of row.cells) {
513
- collectSelectedTextSegments(cell.content, selectionFrom, selectionTo, output);
514
- }
515
- }
516
- continue;
517
- }
518
- if (block.kind === "sdt_block") {
519
- collectSelectedTextSegments(block.children, selectionFrom, selectionTo, output);
520
- }
521
- }
522
- return output;
523
- }
524
-
525
- function rangesOverlap(
526
- leftFrom: number,
527
- leftTo: number,
528
- rightFrom: number,
529
- rightTo: number,
530
- ): boolean {
531
- return leftFrom < rightTo && rightFrom < leftTo;
532
- }
533
-
534
- function buildRunPropertyBeforeXml(
535
- segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
536
- ): string {
537
- const parts: string[] = [];
538
- const marks = new Set(segment.marks ?? []);
539
- if (marks.has("bold")) parts.push("<w:b/>");
540
- if (marks.has("italic")) parts.push("<w:i/>");
541
- if (marks.has("underline")) parts.push("<w:u w:val=\"single\"/>");
542
- if (marks.has("strikethrough")) parts.push("<w:strike/>");
543
- if (marks.has("superscript")) parts.push("<w:vertAlign w:val=\"superscript\"/>");
544
- if (marks.has("subscript")) parts.push("<w:vertAlign w:val=\"subscript\"/>");
545
- if (segment.markAttrs?.fontFamily) {
546
- parts.push(`<w:rFonts w:ascii="${escapeAttributeXml(segment.markAttrs.fontFamily)}" w:hAnsi="${escapeAttributeXml(segment.markAttrs.fontFamily)}"/>`);
547
- }
548
- if (segment.markAttrs?.fontSize !== undefined) {
549
- parts.push(`<w:sz w:val="${segment.markAttrs.fontSize}"/>`);
550
- }
551
- if (segment.markAttrs?.textColor) {
552
- parts.push(`<w:color w:val="${escapeAttributeXml(segment.markAttrs.textColor)}"/>`);
553
- }
554
- if (segment.markAttrs?.backgroundColor) {
555
- parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttributeXml(segment.markAttrs.backgroundColor)}"/>`);
556
- }
557
- return `<w:rPr>${parts.join("")}</w:rPr>`;
558
- }
559
-
560
- function buildParagraphPropertyBeforeXml(
561
- paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
562
- ): string {
563
- const parts: string[] = [];
564
- if (paragraph.styleId) {
565
- parts.push(`<w:pStyle w:val="${escapeAttributeXml(paragraph.styleId)}"/>`);
566
- }
567
- if (paragraph.numbering) {
568
- parts.push(
569
- `<w:numPr><w:ilvl w:val="${paragraph.numbering.level}"/><w:numId w:val="${escapeAttributeXml(
570
- paragraph.numbering.numberingInstanceId.replace(/^num:/u, ""),
571
- )}"/></w:numPr>`,
572
- );
573
- }
574
- if (paragraph.alignment) {
575
- parts.push(`<w:jc w:val="${escapeAttributeXml(paragraph.alignment)}"/>`);
576
- }
577
- if (paragraph.indentation) {
578
- const attrs: string[] = [];
579
- if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
580
- if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
581
- if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
582
- if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
583
- if (attrs.length > 0) {
584
- parts.push(`<w:ind ${attrs.join(" ")}/>`);
585
- }
586
- }
587
- return `<w:pPr>${parts.join("")}</w:pPr>`;
588
- }
589
-
590
- function escapeAttributeXml(value: string): string {
591
- return value
592
- .replace(/&/g, "&amp;")
593
- .replace(/</g, "&lt;")
594
- .replace(/>/g, "&gt;")
595
- .replace(/"/g, "&quot;");
596
- }
597
-
598
247
  // ---------------------------------------------------------------------------
599
248
  // List toggle / paragraph style / table style
600
249
  // ---------------------------------------------------------------------------
@@ -1098,6 +1098,7 @@ function createLoadingRuntimeBridge(input: {
1098
1098
  getFontEntry: (name: string) =>
1099
1099
  input.sessionState.canonicalDocument.fontTable?.fonts[name],
1100
1100
  replaceText: () => undefined,
1101
+ applyFormattingOperation: () => undefined,
1101
1102
  applyScopeReplacement: () => undefined,
1102
1103
  insertFragment: () => undefined,
1103
1104
  copy: () => undefined,
@@ -1133,6 +1134,13 @@ function createLoadingRuntimeBridge(input: {
1133
1134
  addCommentReply: () => {
1134
1135
  throw createLoadingBoundaryError(input.snapshot.documentId, "comment");
1135
1136
  },
1137
+ getCommentThreadForChange: () => null,
1138
+ ensureCommentThreadForChange: () => {
1139
+ throw createLoadingBoundaryError(input.snapshot.documentId, "comment");
1140
+ },
1141
+ addReplyToChange: () => {
1142
+ throw createLoadingBoundaryError(input.snapshot.documentId, "comment");
1143
+ },
1136
1144
  editCommentBody: () => undefined,
1137
1145
  addScope: () => {
1138
1146
  throw createLoadingBoundaryError(input.snapshot.documentId, "scope");
@@ -66,6 +66,7 @@ import type {
66
66
  ViewMode as EditorViewMode,
67
67
  WorkflowBlockedCommandReason,
68
68
  WorkflowMarkupSnapshot,
69
+ WorkflowMarkupMode,
69
70
  WorkflowScopeSnapshot,
70
71
  WordReviewEditorChromeOptions,
71
72
  WordReviewEditorChromePreset,
@@ -250,6 +251,21 @@ function normalizeHostMarkupDisplay(
250
251
  return "markup";
251
252
  }
252
253
 
254
+ function toWorkflowMarkupMode(mode: MarkupDisplay): WorkflowMarkupMode {
255
+ switch (mode) {
256
+ case "all":
257
+ case "all-markup":
258
+ return "all";
259
+ case "simple":
260
+ case "simple-markup":
261
+ return "simple";
262
+ case "clean":
263
+ case "no-markup":
264
+ case "original":
265
+ return "clean";
266
+ }
267
+ }
268
+
253
269
  const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
254
270
  position: "absolute",
255
271
  width: "1px",
@@ -383,6 +399,11 @@ export function __createWordReviewEditorRefBridge(
383
399
  resolveComment: (commentId) => runtime.resolveComment(commentId),
384
400
  reopenComment: (commentId) => runtime.reopenComment(commentId),
385
401
  addCommentReply: (commentId, body) => runtime.addCommentReply(commentId, body),
402
+ getCommentThreadForChange: (changeId) =>
403
+ clonePublicValue(runtime.getCommentThreadForChange(changeId)),
404
+ ensureCommentThreadForChange: (changeId) =>
405
+ runtime.ensureCommentThreadForChange(changeId),
406
+ addReplyToChange: (changeId, body) => runtime.addReplyToChange(changeId, body),
386
407
  editCommentBody: (commentId, body) => runtime.editCommentBody(commentId, body),
387
408
  deleteComment: (commentId) => {
388
409
  applyRuntimeDeleteComment(runtime, commentId);
@@ -1041,7 +1062,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1041
1062
  } = props;
1042
1063
 
1043
1064
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
1044
- const [showTrackedChanges, setShowTrackedChanges] = useState(false);
1065
+ const [showTrackedChanges, setShowTrackedChanges] = useState(() => suggestionsEnabled);
1066
+ const [localMarkupDisplay, setLocalMarkupDisplay] =
1067
+ useState<WorkflowMarkupMode | null>(() => suggestionsEnabled ? "all" : null);
1045
1068
  const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
1046
1069
  const [suppressedSuggestionRevisionId, setSuppressedSuggestionRevisionId] = useState<string | null>(null);
1047
1070
  const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
@@ -1343,7 +1366,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1343
1366
  loadingViewState,
1344
1367
  );
1345
1368
  const isPageWorkspace = viewState.workspaceMode === "page";
1346
- const liveMarkupDisplay = __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
1369
+ const liveMarkupDisplay = localMarkupDisplay ??
1370
+ __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
1347
1371
  const documentNavigation = useRuntimeValue(
1348
1372
  runtime
1349
1373
  ? {
@@ -1483,9 +1507,29 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1483
1507
  activeRuntime.setViewMode(effectiveViewMode);
1484
1508
  }, [activeRuntime, effectiveViewMode]);
1485
1509
 
1510
+ const setReviewMarkupMode = useCallback((mode: MarkupDisplay) => {
1511
+ const workflowMode = toWorkflowMarkupMode(mode);
1512
+ setLocalMarkupDisplay(workflowMode);
1513
+ setShowTrackedChanges(workflowMode !== "clean");
1514
+ api.ui?.viewport.setLocalMarkupMode(workflowMode);
1515
+ }, [api]);
1516
+
1517
+ const setTrackedChangesAuthoring = useCallback((enabled: boolean) => {
1518
+ const workflowMode: WorkflowMarkupMode = enabled ? "all" : "clean";
1519
+ setShowTrackedChanges(enabled);
1520
+ setLocalMarkupDisplay(workflowMode);
1521
+ api.runtime.document.setMode(enabled ? "suggesting" : "editing");
1522
+ api.ui?.viewport.setLocalMarkupMode(workflowMode);
1523
+ }, [api]);
1524
+
1486
1525
  useEffect(() => {
1487
- activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
1488
- }, [activeRuntime, suggestionsEnabled]);
1526
+ if (suggestionsEnabled) {
1527
+ setTrackedChangesAuthoring(true);
1528
+ return;
1529
+ }
1530
+ setShowTrackedChanges(false);
1531
+ api.runtime.document.setMode("editing");
1532
+ }, [api, setTrackedChangesAuthoring, suggestionsEnabled]);
1489
1533
 
1490
1534
  // design-close-chrome Phase 2 — density contract (designsystem §4.2).
1491
1535
  // When the `density` prop is supplied, drive the root `data-density`
@@ -1645,6 +1689,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1645
1689
  reopenComment: (commentId) => activeRuntime.reopenComment(commentId),
1646
1690
  addCommentReply: (commentId, body) =>
1647
1691
  activeRuntime.addCommentReply(commentId, body, currentUser.userId),
1692
+ getCommentThreadForChange: (changeId) =>
1693
+ clonePublicValue(activeRuntime.getCommentThreadForChange(changeId)),
1694
+ ensureCommentThreadForChange: (changeId) =>
1695
+ activeRuntime.ensureCommentThreadForChange(changeId, currentUser.userId),
1696
+ addReplyToChange: (changeId, body) =>
1697
+ activeRuntime.addReplyToChange(changeId, body, currentUser.userId),
1648
1698
  editCommentBody: (commentId, body) =>
1649
1699
  activeRuntime.editCommentBody(commentId, body),
1650
1700
  deleteComment: (commentId) => {
@@ -2740,7 +2790,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2740
2790
  );
2741
2791
 
2742
2792
  const addSelectionToolbarComment = useCallback(() => {
2743
- const commentId = addReviewComment();
2793
+ let commentId: string | null = null;
2794
+ if (activeSelectionTool?.kind === "suggestion-review") {
2795
+ const primaryChangeId = activeSelectionTool.changeIds[0];
2796
+ const linkedThread = primaryChangeId
2797
+ ? activeRuntime.ensureCommentThreadForChange(primaryChangeId, currentUser.userId)
2798
+ : null;
2799
+ commentId = linkedThread?.commentId ?? null;
2800
+ if (commentId) {
2801
+ activeRuntime.openComment(commentId);
2802
+ setActiveRailTab("comments");
2803
+ }
2804
+ } else {
2805
+ commentId = addReviewComment();
2806
+ }
2744
2807
  if (!commentId) {
2745
2808
  return;
2746
2809
  }
@@ -2748,7 +2811,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2748
2811
  queueMicrotask(() => {
2749
2812
  focusDocumentSurface();
2750
2813
  });
2751
- }, [dismissSelectionToolbar, focusDocumentSurface]);
2814
+ }, [
2815
+ activeRuntime,
2816
+ activeSelectionTool,
2817
+ currentUser.userId,
2818
+ dismissSelectionToolbar,
2819
+ focusDocumentSurface,
2820
+ ]);
2752
2821
 
2753
2822
  const handleSelectionToolbarAnchorChange = useCallback(
2754
2823
  (nextAnchor: SelectionToolbarAnchor | null) => {
@@ -3166,7 +3235,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3166
3235
  onWorkspaceModeChange: (mode) => activeRuntime.setWorkspaceMode(mode),
3167
3236
  onZoomChange: (level) => activeRuntime.setZoom(level),
3168
3237
  onActiveRailTabChange: setActiveRailTab,
3169
- onShowTrackedChangesChange: setShowTrackedChanges,
3238
+ onShowTrackedChangesChange: setTrackedChangesAuthoring,
3239
+ onReviewMarkupModeChange: setReviewMarkupMode,
3170
3240
  onToggleBold: () =>
3171
3241
  applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
3172
3242
  onToggleItalic: () =>
@@ -3263,10 +3333,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3263
3333
  // with a mount-time `console.warn`; hosts that still pass them can
3264
3334
  // migrate to `onOpenStory` at their leisure.
3265
3335
  onOpenHeaderStoryForPage: (pageIndex: number) =>
3266
- openStoryForPage(activeRuntime, pageIndex, "header"),
3336
+ isPageWorkspace
3337
+ ? openStoryForPage(activeRuntime, pageIndex, "header")
3338
+ : undefined,
3267
3339
  onOpenFooterStoryForPage: (pageIndex: number) =>
3268
- openStoryForPage(activeRuntime, pageIndex, "footer"),
3340
+ isPageWorkspace
3341
+ ? openStoryForPage(activeRuntime, pageIndex, "footer")
3342
+ : undefined,
3269
3343
  onOpenStory: (target) => {
3344
+ if (
3345
+ !isPageWorkspace &&
3346
+ (target.kind === "header" || target.kind === "footer")
3347
+ ) {
3348
+ return;
3349
+ }
3270
3350
  activeRuntime.openStory(target);
3271
3351
  },
3272
3352
  onDeleteSectionBreak: (sectionIndex) =>
@@ -4740,6 +4820,12 @@ function buildSuggestionCardModel(args: {
4740
4820
  canReject: canReviewSuggestion && capabilities.canRejectChange && focusedSuggestion.canReject,
4741
4821
  canEditSuggestion: canReviewSuggestion && focusedSuggestion.editable,
4742
4822
  canAddComment,
4823
+ ...(focusedSuggestion.commentThreadIds
4824
+ ? { commentThreadIds: focusedSuggestion.commentThreadIds }
4825
+ : {}),
4826
+ ...(focusedSuggestion.replyCount !== undefined
4827
+ ? { replyCount: focusedSuggestion.replyCount }
4828
+ : {}),
4743
4829
  ...(disabledReason ? { disabledReason } : {}),
4744
4830
  };
4745
4831
  }