@beyondwork/docx-react-component 1.0.31 → 1.0.33

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.
@@ -354,7 +354,12 @@ export function createDocumentRuntime(
354
354
  enforcedRangeCount: 0,
355
355
  preservedRangeCount: 0,
356
356
  };
357
- let workflowOverlay: WorkflowOverlay | null = null;
357
+ let workflowOverlay: WorkflowOverlay | null =
358
+ structuredClone(
359
+ options.initialSessionState?.workflowOverlay ??
360
+ options.initialSnapshot?.workflowOverlay ??
361
+ null,
362
+ );
358
363
  let workflowMetadataDefinitions: WorkflowMetadataDefinition[] =
359
364
  options.initialSessionState?.workflowMetadata?.definitions
360
365
  ?? options.initialSnapshot?.workflowMetadata?.definitions
@@ -1995,6 +2000,7 @@ export function createDocumentRuntime(
1995
2000
  compatibility,
1996
2001
  protectionSnapshot,
1997
2002
  }) as unknown as PersistedEditorSnapshot),
2003
+ workflowOverlay: workflowOverlay ?? undefined,
1998
2004
  workflowMetadata: deriveWorkflowMetadataSnapshot(),
1999
2005
  },
2000
2006
  );
@@ -3484,39 +3490,155 @@ function refreshDocumentFields(
3484
3490
  let changedFrom: number | undefined;
3485
3491
  let changedTo: number | undefined;
3486
3492
 
3487
- const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
3488
- if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
3489
- return field;
3490
- }
3491
- if (supportedOnly && field.fieldFamily === "TOC") {
3492
- return field;
3493
+ const refreshStoryFields = (
3494
+ blocks: readonly BlockNode[],
3495
+ storyTarget: EditorStoryTarget,
3496
+ ): { blocks: BlockNode[]; changed: boolean } => {
3497
+ let storyChanged = false;
3498
+ let storyChangedFrom: number | undefined;
3499
+ let storyChangedTo: number | undefined;
3500
+ const refreshed = refreshBlocksWithCursor(blocks, (field, range) => {
3501
+ if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
3502
+ return field;
3503
+ }
3504
+ if (supportedOnly && field.fieldFamily === "TOC") {
3505
+ return field;
3506
+ }
3507
+ const display = resolveSupportedFieldDisplay(
3508
+ field,
3509
+ document,
3510
+ bookmarkMap,
3511
+ paragraphs,
3512
+ navigation,
3513
+ storyTarget,
3514
+ );
3515
+ if (!display) {
3516
+ return field;
3517
+ }
3518
+ updatedCount += 1;
3519
+ const nextField: FieldNode = {
3520
+ ...field,
3521
+ children: buildInlineNodesFromDisplayText(display.displayText),
3522
+ refreshStatus: display.refreshStatus,
3523
+ };
3524
+ if (
3525
+ nextField.refreshStatus !== field.refreshStatus ||
3526
+ flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)
3527
+ ) {
3528
+ storyChanged = true;
3529
+ storyChangedFrom = storyChangedFrom === undefined ? range.from : Math.min(storyChangedFrom, range.from);
3530
+ storyChangedTo = storyChangedTo === undefined ? range.to : Math.max(storyChangedTo, range.to);
3531
+ }
3532
+ return nextField;
3533
+ });
3534
+ if (storyChanged) {
3535
+ changed = true;
3536
+ if (storyTargetsEqual(activeStory, storyTarget)) {
3537
+ if (storyChangedFrom !== undefined) {
3538
+ changedFrom = changedFrom === undefined ? storyChangedFrom : Math.min(changedFrom, storyChangedFrom);
3539
+ }
3540
+ if (storyChangedTo !== undefined) {
3541
+ changedTo = changedTo === undefined ? storyChangedTo : Math.max(changedTo, storyChangedTo);
3542
+ }
3543
+ }
3493
3544
  }
3494
- const display = resolveSupportedFieldDisplay(
3495
- field,
3496
- document,
3497
- bookmarkMap,
3498
- paragraphs,
3499
- navigation,
3500
- );
3501
- if (!display) {
3502
- return field;
3545
+ return { blocks: refreshed.blocks, changed: storyChanged };
3546
+ };
3547
+
3548
+ const refreshedMain = refreshStoryFields(document.content.children, MAIN_STORY_TARGET);
3549
+ let nextSubParts = document.subParts;
3550
+
3551
+ if (document.subParts) {
3552
+ let subPartsChanged = false;
3553
+ const nextHeaders = document.subParts.headers.map((header) => {
3554
+ const storyTarget: EditorStoryTarget = {
3555
+ kind: "header",
3556
+ relationshipId: header.relationshipId,
3557
+ variant: header.variant,
3558
+ ...(header.sectionIndex !== undefined ? { sectionIndex: header.sectionIndex } : {}),
3559
+ };
3560
+ const refreshed = refreshStoryFields(header.blocks, storyTarget);
3561
+ if (refreshed.changed) {
3562
+ subPartsChanged = true;
3563
+ return {
3564
+ ...header,
3565
+ blocks: refreshed.blocks,
3566
+ };
3567
+ }
3568
+ return header;
3569
+ });
3570
+ const nextFooters = document.subParts.footers.map((footer) => {
3571
+ const storyTarget: EditorStoryTarget = {
3572
+ kind: "footer",
3573
+ relationshipId: footer.relationshipId,
3574
+ variant: footer.variant,
3575
+ ...(footer.sectionIndex !== undefined ? { sectionIndex: footer.sectionIndex } : {}),
3576
+ };
3577
+ const refreshed = refreshStoryFields(footer.blocks, storyTarget);
3578
+ if (refreshed.changed) {
3579
+ subPartsChanged = true;
3580
+ return {
3581
+ ...footer,
3582
+ blocks: refreshed.blocks,
3583
+ };
3584
+ }
3585
+ return footer;
3586
+ });
3587
+
3588
+ let nextFootnoteCollection = document.subParts.footnoteCollection;
3589
+ if (document.subParts.footnoteCollection) {
3590
+ let noteCollectionChanged = false;
3591
+ const nextFootnotes = Object.fromEntries(
3592
+ Object.entries(document.subParts.footnoteCollection.footnotes).map(([noteId, note]) => {
3593
+ const refreshed = refreshStoryFields(note.blocks, { kind: "footnote", noteId });
3594
+ if (refreshed.changed) {
3595
+ noteCollectionChanged = true;
3596
+ return [
3597
+ noteId,
3598
+ {
3599
+ ...note,
3600
+ blocks: refreshed.blocks,
3601
+ },
3602
+ ];
3603
+ }
3604
+ return [noteId, note];
3605
+ }),
3606
+ );
3607
+ const nextEndnotes = Object.fromEntries(
3608
+ Object.entries(document.subParts.footnoteCollection.endnotes).map(([noteId, note]) => {
3609
+ const refreshed = refreshStoryFields(note.blocks, { kind: "endnote", noteId });
3610
+ if (refreshed.changed) {
3611
+ noteCollectionChanged = true;
3612
+ return [
3613
+ noteId,
3614
+ {
3615
+ ...note,
3616
+ blocks: refreshed.blocks,
3617
+ },
3618
+ ];
3619
+ }
3620
+ return [noteId, note];
3621
+ }),
3622
+ );
3623
+ if (noteCollectionChanged) {
3624
+ subPartsChanged = true;
3625
+ nextFootnoteCollection = {
3626
+ footnotes: nextFootnotes,
3627
+ endnotes: nextEndnotes,
3628
+ };
3629
+ }
3503
3630
  }
3504
- updatedCount += 1;
3505
- const nextField: FieldNode = {
3506
- ...field,
3507
- children: buildInlineNodesFromDisplayText(display.displayText),
3508
- refreshStatus: display.refreshStatus,
3509
- };
3510
- if (
3511
- nextField.refreshStatus !== field.refreshStatus ||
3512
- flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)
3513
- ) {
3514
- changed = true;
3515
- changedFrom = changedFrom === undefined ? range.from : Math.min(changedFrom, range.from);
3516
- changedTo = changedTo === undefined ? range.to : Math.max(changedTo, range.to);
3631
+
3632
+ if (subPartsChanged) {
3633
+ nextSubParts = {
3634
+ ...document.subParts,
3635
+ headers: nextHeaders,
3636
+ footers: nextFooters,
3637
+ ...(nextFootnoteCollection ? { footnoteCollection: nextFootnoteCollection } : {}),
3638
+ };
3517
3639
  }
3518
- return nextField;
3519
- }).blocks;
3640
+ }
3641
+
3520
3642
  if (!changed) {
3521
3643
  return { document, updatedCount, changed: false };
3522
3644
  }
@@ -3525,8 +3647,9 @@ function refreshDocumentFields(
3525
3647
  ...document,
3526
3648
  content: {
3527
3649
  ...document.content,
3528
- children: nextChildren,
3650
+ children: refreshedMain.blocks,
3529
3651
  },
3652
+ ...(nextSubParts ? { subParts: nextSubParts } : {}),
3530
3653
  };
3531
3654
  const nextRegistry = buildFieldRegistry({
3532
3655
  content: nextDocument.content,
@@ -3821,6 +3944,7 @@ function resolveSupportedFieldDisplay(
3821
3944
  bookmarkMap: Map<string, { bookmarkId: string; paragraphIndex: number }>,
3822
3945
  paragraphs: readonly ParagraphContext[],
3823
3946
  navigation: DocumentNavigationSnapshot,
3947
+ storyTarget: EditorStoryTarget,
3824
3948
  ): { displayText: string; refreshStatus: FieldRefreshStatus } | undefined {
3825
3949
  if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
3826
3950
  return undefined;
@@ -3846,7 +3970,10 @@ function resolveSupportedFieldDisplay(
3846
3970
  return { displayText: "", refreshStatus: "unresolvable" };
3847
3971
  }
3848
3972
  const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
3849
- return { displayText: String(pageIndex + 1), refreshStatus: "current" };
3973
+ const page = navigation.pages[pageIndex] ?? navigation.pages[0];
3974
+ return page
3975
+ ? { displayText: String(resolveDisplayedPageNumber(page)), refreshStatus: "current" }
3976
+ : { displayText: "", refreshStatus: "unresolvable" };
3850
3977
  }
3851
3978
  if (field.fieldFamily === "NOTEREF") {
3852
3979
  const paragraph = paragraphs[bookmark.paragraphIndex]?.paragraph;
@@ -3861,6 +3988,53 @@ function resolveSupportedFieldDisplay(
3861
3988
  return undefined;
3862
3989
  }
3863
3990
 
3991
+ function resolveRepresentativePageForStory(
3992
+ navigation: DocumentNavigationSnapshot,
3993
+ storyTarget: EditorStoryTarget,
3994
+ ): DocumentNavigationSnapshot["pages"][number] | undefined {
3995
+ if (storyTarget.kind === "main") {
3996
+ return navigation.pages[navigation.activePageIndex] ?? navigation.pages[0];
3997
+ }
3998
+
3999
+ if (storyTarget.kind === "header" || storyTarget.kind === "footer") {
4000
+ const sectionIndex = storyTarget.sectionIndex ?? navigation.activeSectionIndex;
4001
+ const sectionPages = navigation.pages.filter((page) => page.sectionIndex === sectionIndex);
4002
+ if (sectionPages.length === 0) {
4003
+ return navigation.pages[0];
4004
+ }
4005
+ if (storyTarget.variant === "first") {
4006
+ return sectionPages[0];
4007
+ }
4008
+ if (storyTarget.variant === "even") {
4009
+ return sectionPages.find((page) => (page.pageInSection + 1) % 2 === 0) ?? sectionPages[0];
4010
+ }
4011
+ return (
4012
+ sectionPages.find((page) => isDefaultHeaderFooterPage(page)) ??
4013
+ sectionPages[0]
4014
+ );
4015
+ }
4016
+
4017
+ return navigation.pages[navigation.activePageIndex] ?? navigation.pages[0];
4018
+ }
4019
+
4020
+ function isDefaultHeaderFooterPage(
4021
+ page: DocumentNavigationSnapshot["pages"][number],
4022
+ ): boolean {
4023
+ if (page.layout.differentFirstPage && page.pageInSection === 0) {
4024
+ return false;
4025
+ }
4026
+ if (page.layout.differentOddEvenPages) {
4027
+ return (page.pageInSection + 1) % 2 === 1;
4028
+ }
4029
+ return true;
4030
+ }
4031
+
4032
+ function resolveDisplayedPageNumber(
4033
+ page: DocumentNavigationSnapshot["pages"][number],
4034
+ ): number {
4035
+ return (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
4036
+ }
4037
+
3864
4038
  interface ParagraphContext {
3865
4039
  paragraph: ParagraphNode;
3866
4040
  startOffset: number;
@@ -90,6 +90,7 @@ export function createEditorSurfaceSnapshot(
90
90
  cursor,
91
91
  counters,
92
92
  numberingPrefixResolver,
93
+ activeStory.kind !== "main",
93
94
  );
94
95
  blocks.push(surfaceBlock.block);
95
96
  lockedFragmentIds.push(...surfaceBlock.lockedFragmentIds);
@@ -123,6 +124,7 @@ function createSurfaceBlock(
123
124
  altChunk: number;
124
125
  },
125
126
  numberingPrefixResolver: NumberingPrefixResolver,
127
+ promoteSecondaryStoryTextBoxes: boolean,
126
128
  ): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
127
129
  if (block.type === "opaque_block") {
128
130
  const fragment = getOpaqueFragment(document.preservation as never, block.fragmentId);
@@ -162,6 +164,7 @@ function createSurfaceBlock(
162
164
  cursor,
163
165
  counters,
164
166
  numberingPrefixResolver,
167
+ promoteSecondaryStoryTextBoxes,
165
168
  );
166
169
  }
167
170
 
@@ -200,6 +203,7 @@ function createSurfaceBlock(
200
203
  cursor,
201
204
  counters,
202
205
  numberingPrefixResolver,
206
+ promoteSecondaryStoryTextBoxes,
203
207
  );
204
208
  }
205
209
 
@@ -291,6 +295,7 @@ function createSurfaceBlock(
291
295
  document,
292
296
  cursor,
293
297
  numberingPrefixResolver,
298
+ promoteSecondaryStoryTextBoxes,
294
299
  );
295
300
  }
296
301
 
@@ -308,6 +313,7 @@ function createTableBlock(
308
313
  altChunk: number;
309
314
  },
310
315
  numberingPrefixResolver: NumberingPrefixResolver,
316
+ promoteSecondaryStoryTextBoxes: boolean,
311
317
  ): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
312
318
  const lockedFragmentIds: string[] = [];
313
319
  let innerCursor = cursor;
@@ -327,6 +333,7 @@ function createTableBlock(
327
333
  innerCursor,
328
334
  counters,
329
335
  numberingPrefixResolver,
336
+ promoteSecondaryStoryTextBoxes,
330
337
  );
331
338
  cellContent.push(result.block);
332
339
  lockedFragmentIds.push(...result.lockedFragmentIds);
@@ -458,6 +465,7 @@ function createSdtBlock(
458
465
  altChunk: number;
459
466
  },
460
467
  numberingPrefixResolver: NumberingPrefixResolver,
468
+ promoteSecondaryStoryTextBoxes: boolean,
461
469
  ): { block: SurfaceBlockSnapshot; lockedFragmentIds: string[]; nextCursor: number } {
462
470
  const children: SurfaceBlockSnapshot[] = [];
463
471
  const lockedFragmentIds: string[] = [];
@@ -470,6 +478,7 @@ function createSdtBlock(
470
478
  innerCursor,
471
479
  counters,
472
480
  numberingPrefixResolver,
481
+ promoteSecondaryStoryTextBoxes,
473
482
  );
474
483
  children.push(result.block);
475
484
  lockedFragmentIds.push(...result.lockedFragmentIds);
@@ -504,6 +513,7 @@ function createParagraphBlock(
504
513
  document: CanonicalDocumentEnvelope,
505
514
  start: number,
506
515
  numberingPrefixResolver: NumberingPrefixResolver,
516
+ promoteSecondaryStoryTextBoxes: boolean,
507
517
  ): {
508
518
  block: SurfaceBlockSnapshot;
509
519
  nextCursor: number;
@@ -553,7 +563,13 @@ function createParagraphBlock(
553
563
  const children = Array.isArray(paragraph.children) ? paragraph.children : [];
554
564
 
555
565
  for (const child of children) {
556
- const result = appendInlineSegments(accumulator, child, document, cursor);
566
+ const result = appendInlineSegments(
567
+ accumulator,
568
+ child,
569
+ document,
570
+ cursor,
571
+ promoteSecondaryStoryTextBoxes,
572
+ );
557
573
  cursor = result.nextCursor;
558
574
  lockedFragmentIds.push(...result.lockedFragmentIds);
559
575
  }
@@ -671,6 +687,7 @@ function appendInlineSegments(
671
687
  node: InlineNode,
672
688
  document: CanonicalDocumentEnvelope,
673
689
  start: number,
690
+ promoteSecondaryStoryTextBoxes: boolean,
674
691
  hyperlinkHref?: string,
675
692
  ): { nextCursor: number; lockedFragmentIds: string[] } {
676
693
  switch (node.type) {
@@ -712,7 +729,14 @@ function appendInlineSegments(
712
729
  case "hyperlink": {
713
730
  let cursor = start;
714
731
  for (const child of node.children) {
715
- const result = appendInlineSegments(paragraph, child, document, cursor, node.href);
732
+ const result = appendInlineSegments(
733
+ paragraph,
734
+ child,
735
+ document,
736
+ cursor,
737
+ promoteSecondaryStoryTextBoxes,
738
+ node.href,
739
+ );
716
740
  cursor = result.nextCursor;
717
741
  }
718
742
  return { nextCursor: cursor, lockedFragmentIds: [] };
@@ -756,6 +780,7 @@ function appendInlineSegments(
756
780
  "Locked whole-unit to keep unsupported inline OOXML intact through export.",
757
781
  ...(descriptor ? { featureKey: descriptor.featureKey, blockedReasonCode } : {}),
758
782
  ...(preview?.presentation ? { presentation: preview.presentation } : {}),
783
+ ...(preview?.displayText ? { displayText: preview.displayText } : {}),
759
784
  state: "locked-preserve-only",
760
785
  });
761
786
  return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
@@ -765,11 +790,29 @@ function appendInlineSegments(
765
790
  case "smartart_preview":
766
791
  return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node));
767
792
  case "shape":
793
+ if (promoteSecondaryStoryTextBoxes && node.isTextBox && node.text) {
794
+ return appendTextBoxSegment(
795
+ paragraph,
796
+ start,
797
+ "Text box",
798
+ createShapeDetail(node),
799
+ node.text,
800
+ );
801
+ }
768
802
  return appendComplexPreviewSegment(paragraph, node, start,
769
803
  node.isTextBox ? "Text box" : "Drawing shape", createShapeDetail(node));
770
804
  case "wordart":
771
805
  return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
772
806
  case "vml_shape":
807
+ if (promoteSecondaryStoryTextBoxes && shouldRenderSecondaryStoryVmlTextBox(node)) {
808
+ return appendTextBoxSegment(
809
+ paragraph,
810
+ start,
811
+ "Text box",
812
+ createVmlDetail(node),
813
+ node.text!,
814
+ );
815
+ }
773
816
  return appendComplexPreviewSegment(paragraph, node, start, "Legacy VML drawing", createVmlDetail(node));
774
817
  case "symbol":
775
818
  paragraph.segments.push({
@@ -810,12 +853,20 @@ function appendInlineSegments(
810
853
  node.fieldFamily === "REF" ||
811
854
  node.fieldFamily === "PAGEREF" ||
812
855
  node.fieldFamily === "NOTEREF" ||
813
- node.fieldFamily === "TOC";
856
+ node.fieldFamily === "TOC" ||
857
+ node.fieldFamily === "PAGE" ||
858
+ node.fieldFamily === "NUMPAGES";
814
859
  if (node.children && node.children.length > 0) {
815
860
  let cursor = start;
816
861
  const lockedIds: string[] = [];
817
862
  for (const child of node.children) {
818
- const result = appendInlineSegments(paragraph, child, document, cursor);
863
+ const result = appendInlineSegments(
864
+ paragraph,
865
+ child,
866
+ document,
867
+ cursor,
868
+ promoteSecondaryStoryTextBoxes,
869
+ );
819
870
  cursor = result.nextCursor;
820
871
  lockedIds.push(...result.lockedFragmentIds);
821
872
  }
@@ -826,7 +877,11 @@ function appendInlineSegments(
826
877
  const fieldLabel =
827
878
  node.fieldFamily === "TOC"
828
879
  ? "Table of Contents"
829
- : `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
880
+ : node.fieldFamily === "PAGE"
881
+ ? "Current page number"
882
+ : node.fieldFamily === "NUMPAGES"
883
+ ? "Total pages"
884
+ : `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
830
885
  paragraph.segments.push({
831
886
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
832
887
  kind: "field_ref",
@@ -883,6 +938,33 @@ function appendComplexPreviewSegment(
883
938
  return { nextCursor: start + 1, lockedFragmentIds: [] };
884
939
  }
885
940
 
941
+ function appendTextBoxSegment(
942
+ paragraph: ParagraphAccumulator,
943
+ start: number,
944
+ label: string,
945
+ detail: string,
946
+ displayText: string,
947
+ ): { nextCursor: number; lockedFragmentIds: string[] } {
948
+ paragraph.segments.push({
949
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
950
+ kind: "opaque_inline",
951
+ from: start,
952
+ to: start + 1,
953
+ fragmentId: `complex:text-box:${start}`,
954
+ warningId: `warning:text-box:${start}`,
955
+ label,
956
+ detail,
957
+ presentation: "text-box",
958
+ displayText,
959
+ state: "locked-preserve-only",
960
+ });
961
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
962
+ }
963
+
964
+ function shouldRenderSecondaryStoryVmlTextBox(node: VmlShapeNode): boolean {
965
+ return Boolean(node.text) && (!node.shapeType || /_x0000_t202$/iu.test(node.shapeType));
966
+ }
967
+
886
968
  function createChartDetail(node: ChartPreviewNode): string {
887
969
  const parts = ["Embedded chart."];
888
970
  if (node.previewMediaId) {
@@ -977,7 +1059,11 @@ function createPlainText(
977
1059
  text.push("\uFFFC");
978
1060
  break;
979
1061
  case "opaque_inline":
980
- text.push("\uFFF9");
1062
+ if ((segment.presentation === "text-box" || segment.presentation === "checkbox") && segment.displayText) {
1063
+ text.push(segment.displayText);
1064
+ } else {
1065
+ text.push("\uFFF9");
1066
+ }
981
1067
  break;
982
1068
  }
983
1069
  }
@@ -1095,6 +1181,7 @@ function createStorySurface(
1095
1181
  cursor,
1096
1182
  counters,
1097
1183
  numberingPrefixResolver,
1184
+ true,
1098
1185
  );
1099
1186
  surfaceBlocks.push(surfaceBlock.block);
1100
1187
  cursor = surfaceBlock.nextCursor;
@@ -1317,7 +1404,8 @@ function describePreservedInlinePreview(
1317
1404
  ): {
1318
1405
  label: string;
1319
1406
  detail: string;
1320
- presentation?: "inline-chip" | "quiet-marker";
1407
+ presentation?: "inline-chip" | "quiet-marker" | "text-box" | "checkbox";
1408
+ displayText?: string;
1321
1409
  } | null {
1322
1410
  if (/\b(?:w:)?proofErr\b/u.test(payloadReference)) {
1323
1411
  const proofType = /\bw:type="([^"]+)"/u.exec(payloadReference)?.[1];
@@ -1359,6 +1447,11 @@ function describePreservedInlinePreview(
1359
1447
  };
1360
1448
  }
1361
1449
 
1450
+ const checkboxPreview = describePreservedCheckboxPreview(payloadReference);
1451
+ if (checkboxPreview) {
1452
+ return checkboxPreview;
1453
+ }
1454
+
1362
1455
  if (/\b(?:w:)?bookmarkStart\b/u.test(payloadReference)) {
1363
1456
  const name = /\bw:name="([^"]+)"/u.exec(payloadReference)?.[1];
1364
1457
  return {
@@ -1402,6 +1495,62 @@ function describePreservedInlinePreview(
1402
1495
  return null;
1403
1496
  }
1404
1497
 
1498
+ function describePreservedCheckboxPreview(
1499
+ payloadReference: string,
1500
+ ): {
1501
+ label: string;
1502
+ detail: string;
1503
+ presentation: "checkbox";
1504
+ displayText: string;
1505
+ } | null {
1506
+ if (!/\b(?:w:)?sdt\b/u.test(payloadReference) || !/\b(?:w14:)?checkbox\b/u.test(payloadReference)) {
1507
+ return null;
1508
+ }
1509
+
1510
+ const checkedValue = /\b(?:w14:)?checked\b[^>]*\b(?:w14:)?val="([^"]+)"/u.exec(payloadReference)?.[1];
1511
+ const isChecked = checkedValue === "1" || /^true$/iu.test(checkedValue ?? "");
1512
+ const checkedState = /\b(?:w14:)?checkedState\b[^>]*\b(?:w14:)?val="([^"]+)"/u.exec(payloadReference)?.[1];
1513
+ const uncheckedState = /\b(?:w14:)?uncheckedState\b[^>]*\b(?:w14:)?val="([^"]+)"/u.exec(payloadReference)?.[1];
1514
+ const displayText =
1515
+ extractCheckboxGlyph(payloadReference) ??
1516
+ decodeCheckboxCodePoint(isChecked ? checkedState : uncheckedState) ??
1517
+ (isChecked ? "☒" : "☐");
1518
+
1519
+ return {
1520
+ label: isChecked ? "Checked checkbox" : "Unchecked checkbox",
1521
+ detail: `Checkbox content control is visible while the original OOXML wrapper remains preserve-backed for export. State: ${isChecked ? "checked" : "unchecked"}.`,
1522
+ presentation: "checkbox",
1523
+ displayText,
1524
+ };
1525
+ }
1526
+
1527
+ function extractCheckboxGlyph(payloadReference: string): string | null {
1528
+ const content = /<(?:\w+:)?sdtContent\b[^>]*>([\s\S]*?)<\/(?:\w+:)?sdtContent>/u.exec(payloadReference)?.[1];
1529
+ if (!content) {
1530
+ return null;
1531
+ }
1532
+ const text = [...content.matchAll(/<(?:\w+:)?t\b[^>]*>([\s\S]*?)<\/(?:\w+:)?t>/gu)]
1533
+ .map((match) => decodeXmlEntities(match[1] ?? ""))
1534
+ .join("")
1535
+ .trim();
1536
+ return text.length > 0 ? text : null;
1537
+ }
1538
+
1539
+ function decodeCheckboxCodePoint(value: string | undefined): string | null {
1540
+ if (!value) {
1541
+ return null;
1542
+ }
1543
+ const codePoint = Number.parseInt(value, 16);
1544
+ if (!Number.isFinite(codePoint)) {
1545
+ return null;
1546
+ }
1547
+ try {
1548
+ return String.fromCodePoint(codePoint);
1549
+ } catch {
1550
+ return null;
1551
+ }
1552
+ }
1553
+
1405
1554
  function decodeXmlEntities(text: string): string {
1406
1555
  return text
1407
1556
  .replace(/&lt;/g, "<")
@@ -176,7 +176,10 @@ import {
176
176
  resolveHeadingShortcutStyleId,
177
177
  resolveShellShortcut,
178
178
  } from "./runtime-shortcut-dispatch";
179
- import { deriveVisibleWorkflowBlockedRails } from "./workflow-surface-blocked-rails.ts";
179
+ import {
180
+ deriveVisibleWorkflowBlockedRails,
181
+ deriveVisibleWorkflowLockedZones,
182
+ } from "./workflow-surface-blocked-rails.ts";
180
183
  import {
181
184
  type WordReviewEditorRuntime,
182
185
  persistAndExport as persistAndExportFromBoundary,
@@ -869,6 +872,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
869
872
  () => deriveVisibleWorkflowBlockedRails(snapshot.surface, workflowMarkupSnapshot),
870
873
  [snapshot.surface, workflowMarkupSnapshot],
871
874
  );
875
+ const workflowLockedZones = useMemo(
876
+ () => deriveVisibleWorkflowLockedZones(snapshot.surface, workflowMarkupSnapshot),
877
+ [snapshot.surface, workflowMarkupSnapshot],
878
+ );
872
879
  const canonicalDocument = useMemo(
873
880
  () => (runtime ? runtime.getCanonicalDocument() : loadingSessionState.canonicalDocument),
874
881
  [loadingSessionState.canonicalDocument, runtime, snapshot.revisionToken],
@@ -902,6 +909,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
902
909
  activeRuntime.setViewMode(effectiveViewMode);
903
910
  }, [activeRuntime, effectiveViewMode]);
904
911
 
912
+ useEffect(() => {
913
+ activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
914
+ }, [activeRuntime, suggestionsEnabled]);
915
+
905
916
  const markCurrentSectionForReview = useCallback((input?: {
906
917
  sectionIndex?: number;
907
918
  label?: string;
@@ -971,10 +982,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
971
982
  [activeReviewQueueItemId, activeRuntime, reviewQueueSnapshot],
972
983
  );
973
984
 
974
- useEffect(() => {
975
- activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
976
- }, [activeRuntime, suggestionsEnabled]);
977
-
978
985
  useEffect(() => {
979
986
  runtimeViewStateSeedRef.current = {
980
987
  workspaceMode: viewState.workspaceMode,
@@ -2217,6 +2224,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2217
2224
  workflowScopes={workflowScopeSnapshot?.scopes}
2218
2225
  workflowCandidates={workflowScopeSnapshot?.candidates}
2219
2226
  workflowBlockedReasons={workflowBlockedRails}
2227
+ workflowLockedZones={workflowLockedZones}
2220
2228
  activeWorkflowWorkItemId={workflowScopeSnapshot?.activeWorkItemId ?? null}
2221
2229
  activeWorkflowScopeIds={workflowScopeSnapshot?.activeWorkItem?.scopeIds ?? []}
2222
2230
  workflowMetadata={workflowMarkupSnapshot?.metadata}
@@ -7,6 +7,7 @@ import type {
7
7
  SelectionSnapshot,
8
8
  WorkflowBlockedCommandReason,
9
9
  WorkflowCandidateRange,
10
+ WorkflowLockedZone,
10
11
  WorkflowMetadataMarkup,
11
12
  WorkflowScope,
12
13
  } from "../api/public-types.ts";
@@ -53,6 +54,7 @@ export interface EditorSurfaceControllerProps {
53
54
  workflowScopes?: readonly WorkflowScope[];
54
55
  workflowCandidates?: readonly WorkflowCandidateRange[];
55
56
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
57
+ workflowLockedZones?: readonly WorkflowLockedZone[];
56
58
  activeWorkflowWorkItemId?: string | null;
57
59
  activeWorkflowScopeIds?: readonly string[];
58
60
  workflowMetadata?: readonly WorkflowMetadataMarkup[];
@@ -357,7 +357,10 @@ export function buildStructureContextSelectionToolModel(
357
357
  kind: "structure-context",
358
358
  structureKind: "table",
359
359
  badges,
360
- tableStyles: input.styleCatalog.tables,
360
+ tableStyles: input.styleCatalog.tables.map((entry) => ({
361
+ styleId: entry.styleId,
362
+ displayName: entry.displayName,
363
+ })),
361
364
  activeTable: input.activeTableContext ?? null,
362
365
  canMutate,
363
366
  ...(disabledReason ? { disabledReason } : {}),