@beyondwork/docx-react-component 1.0.19 → 1.0.21

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 (71) hide show
  1. package/package.json +44 -25
  2. package/src/api/public-types.ts +336 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/formatting-commands.ts +1 -1
  5. package/src/core/commands/index.ts +14 -2
  6. package/src/core/search/search-text.ts +28 -0
  7. package/src/core/state/editor-state.ts +3 -0
  8. package/src/index.ts +21 -0
  9. package/src/io/docx-session.ts +363 -17
  10. package/src/io/export/serialize-comments.ts +104 -34
  11. package/src/io/export/serialize-footnotes.ts +198 -1
  12. package/src/io/export/serialize-headers-footers.ts +203 -10
  13. package/src/io/export/serialize-main-document.ts +83 -3
  14. package/src/io/export/split-review-boundaries.ts +181 -19
  15. package/src/io/normalize/normalize-text.ts +82 -8
  16. package/src/io/ooxml/highlight-colors.ts +39 -0
  17. package/src/io/ooxml/parse-comments.ts +85 -19
  18. package/src/io/ooxml/parse-fields.ts +396 -0
  19. package/src/io/ooxml/parse-footnotes.ts +240 -2
  20. package/src/io/ooxml/parse-headers-footers.ts +431 -7
  21. package/src/io/ooxml/parse-inline-media.ts +15 -1
  22. package/src/io/ooxml/parse-main-document.ts +396 -14
  23. package/src/io/ooxml/parse-revisions.ts +317 -38
  24. package/src/legal/bookmarks.ts +44 -0
  25. package/src/legal/cross-references.ts +59 -1
  26. package/src/model/canonical-document.ts +117 -1
  27. package/src/model/snapshot.ts +85 -1
  28. package/src/review/store/revision-store.ts +6 -0
  29. package/src/review/store/revision-types.ts +1 -0
  30. package/src/runtime/document-navigation.ts +52 -13
  31. package/src/runtime/document-runtime.ts +1521 -75
  32. package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
  33. package/src/runtime/session-capabilities.ts +33 -3
  34. package/src/runtime/surface-projection.ts +86 -25
  35. package/src/runtime/table-schema.ts +2 -2
  36. package/src/runtime/view-state.ts +24 -6
  37. package/src/runtime/workflow-markup.ts +349 -0
  38. package/src/ui/WordReviewEditor.tsx +915 -1314
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1448 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +55 -0
  43. package/src/ui/headless/revision-decoration-model.ts +4 -4
  44. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  45. package/src/ui/workflow-surface-blocked-rails.ts +94 -0
  46. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  47. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  48. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  49. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  52. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  53. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  54. package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
  55. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  56. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  57. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  58. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
  59. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
  61. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  62. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  63. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  64. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  65. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  66. package/src/ui-tailwind/theme/editor-theme.css +130 -0
  67. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  68. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  69. package/src/validation/compatibility-engine.ts +27 -4
  70. package/src/validation/compatibility-report.ts +1 -0
  71. package/src/validation/docx-comment-proof.ts +220 -0
@@ -193,6 +193,14 @@ function createDiagnosticsRenderSnapshot(
193
193
  canRedo: false,
194
194
  readOnly: true,
195
195
  },
196
+ documentMode: "viewing",
197
+ protectionSnapshot: {
198
+ hasDocumentProtection: false,
199
+ enforcementActive: false,
200
+ ranges: [],
201
+ enforcedRangeCount: 0,
202
+ preservedRangeCount: 0,
203
+ },
196
204
  };
197
205
  }
198
206
 
@@ -1,4 +1,4 @@
1
- import type { RuntimeRenderSnapshot } from "../api/public-types";
1
+ import type { RuntimeRenderSnapshot, WorkflowScopeSnapshot } from "../api/public-types";
2
2
  import {
3
3
  createDetachedAnchor,
4
4
  createNodeAnchor,
@@ -21,6 +21,10 @@ export interface SessionCapabilities {
21
21
  /** Effective editor mode after accounting for runtime state. */
22
22
  mode: "editing" | "review" | "read-only-diagnostics";
23
23
 
24
+ // ── Document mode ──
25
+ /** Runtime document mode — editing authority, distinct from view/workspace mode. */
26
+ documentMode: "editing" | "suggesting" | "viewing";
27
+
24
28
  // ── Command capabilities ──
25
29
  canUndo: boolean;
26
30
  canRedo: boolean;
@@ -44,10 +48,22 @@ export interface SessionCapabilities {
44
48
  preserveOnlyCount: number;
45
49
  unsupportedFatalCount: number;
46
50
 
51
+ // ── Protection posture ──
52
+ /** Whether the source package declares document-level protection. */
53
+ hasDocumentProtection: boolean;
54
+ /** Count of permission ranges from the source package. */
55
+ protectedRangeCount: number;
56
+
47
57
  // ── Health ──
48
58
  /** Total count of health issues (preserve-only + unsupported-fatal + warnings). */
49
59
  healthIssueCount: number;
50
60
 
61
+ // ── Workflow ──
62
+ /** Whether a workflow overlay is currently applied. */
63
+ workflowOverlayPresent: boolean;
64
+ /** Whether the current selection is blocked by workflow scope enforcement. */
65
+ workflowBlocked: boolean;
66
+
51
67
  // ── Status ──
52
68
  isDirty: boolean;
53
69
  isReady: boolean;
@@ -63,12 +79,14 @@ export interface SessionCapabilities {
63
79
  export function deriveCapabilities(
64
80
  snapshot: RuntimeRenderSnapshot,
65
81
  reviewMode: "editing" | "review",
82
+ workflowScope?: WorkflowScopeSnapshot | null,
66
83
  ): SessionCapabilities {
67
84
  const hasFatalError = Boolean(snapshot.fatalError);
68
85
  const isReady = snapshot.isReady;
69
86
  const isReadOnly = snapshot.readOnly;
70
87
  const exportBlocked = snapshot.compatibility.blockExport;
71
88
  const activeStory = snapshot.activeStory ?? { kind: "main" as const };
89
+ const documentMode = snapshot.documentMode ?? "editing";
72
90
 
73
91
  // Phase derivation
74
92
  const phase: SessionCapabilities["phase"] = !isReady
@@ -83,8 +101,8 @@ export function deriveCapabilities(
83
101
  ? "read-only-diagnostics"
84
102
  : reviewMode;
85
103
 
86
- // Command capabilities
87
- const canEdit = isReady && !isReadOnly && !hasFatalError;
104
+ // Command capabilities — document mode "viewing" disables editing
105
+ const canEdit = isReady && !isReadOnly && !hasFatalError && documentMode !== "viewing";
88
106
  const canUndo = snapshot.commandState.canUndo && canEdit;
89
107
  const canRedo = snapshot.commandState.canRedo && canEdit;
90
108
  const canAddComment =
@@ -125,9 +143,17 @@ export function deriveCapabilities(
125
143
 
126
144
  const healthIssueCount = preserveOnlyCount + unsupportedFatalCount + snapshot.warnings.length;
127
145
 
146
+ const protection = snapshot.protectionSnapshot;
147
+ const hasDocumentProtection = protection?.hasDocumentProtection ?? false;
148
+ const protectedRangeCount = protection?.ranges?.length ?? 0;
149
+
150
+ const workflowOverlayPresent = workflowScope?.overlayPresent ?? false;
151
+ const workflowBlocked = (workflowScope?.blockedReasons?.length ?? 0) > 0;
152
+
128
153
  return {
129
154
  phase,
130
155
  mode,
156
+ documentMode,
131
157
  canUndo,
132
158
  canRedo,
133
159
  canEdit,
@@ -142,7 +168,11 @@ export function deriveCapabilities(
142
168
  exportBlocked,
143
169
  preserveOnlyCount,
144
170
  unsupportedFatalCount,
171
+ hasDocumentProtection,
172
+ protectedRangeCount,
145
173
  healthIssueCount,
174
+ workflowOverlayPresent,
175
+ workflowBlocked,
146
176
  isDirty: snapshot.isDirty,
147
177
  isReady,
148
178
  hasFatalError,
@@ -52,6 +52,7 @@ interface ParagraphAccumulator {
52
52
  numbering?: ParagraphNode["numbering"];
53
53
  numberingPrefix?: string;
54
54
  numberingSuffix?: "tab" | "space" | "nothing";
55
+ contextualSpacing?: boolean;
55
56
  segments: SurfaceInlineSegment[];
56
57
  }
57
58
 
@@ -469,6 +470,9 @@ function createParagraphBlock(
469
470
  : {}),
470
471
  ...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
471
472
  ...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
473
+ ...(paragraph.contextualSpacing !== undefined
474
+ ? { contextualSpacing: paragraph.contextualSpacing }
475
+ : {}),
472
476
  ...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
473
477
  ...(paragraph.borders ? { borders: paragraph.borders } : {}),
474
478
  ...(paragraph.shading ? { shading: paragraph.shading } : {}),
@@ -592,15 +596,16 @@ function appendInlineSegments(
592
596
  return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
593
597
  }
594
598
  case "chart_preview":
595
- return appendComplexPreviewSegment(paragraph, node, start, "Chart", createChartDetail(node));
599
+ return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node));
596
600
  case "smartart_preview":
597
- return appendComplexPreviewSegment(paragraph, node, start, "SmartArt", createSmartArtDetail(node));
601
+ return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node));
598
602
  case "shape":
599
- return appendComplexPreviewSegment(paragraph, node, start, "Shape", createShapeDetail(node));
603
+ return appendComplexPreviewSegment(paragraph, node, start,
604
+ node.isTextBox ? "Text box" : "Drawing shape", createShapeDetail(node));
600
605
  case "wordart":
601
606
  return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
602
607
  case "vml_shape":
603
- return appendComplexPreviewSegment(paragraph, node, start, "VML shape", createVmlDetail(node));
608
+ return appendComplexPreviewSegment(paragraph, node, start, "Legacy VML drawing", createVmlDetail(node));
604
609
  case "symbol":
605
610
  paragraph.segments.push({
606
611
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -635,6 +640,11 @@ function appendInlineSegments(
635
640
  });
636
641
  return { nextCursor: start + 1, lockedFragmentIds: [] };
637
642
  case "field": {
643
+ const isSupportedField =
644
+ node.fieldFamily === "REF" ||
645
+ node.fieldFamily === "PAGEREF" ||
646
+ node.fieldFamily === "NOTEREF" ||
647
+ node.fieldFamily === "TOC";
638
648
  if (node.children && node.children.length > 0) {
639
649
  let cursor = start;
640
650
  const lockedIds: string[] = [];
@@ -645,6 +655,25 @@ function appendInlineSegments(
645
655
  }
646
656
  return { nextCursor: cursor, lockedFragmentIds: lockedIds };
647
657
  }
658
+ if (isSupportedField) {
659
+ // Supported field with no resolved content — show as field chip
660
+ const fieldLabel =
661
+ node.fieldFamily === "TOC"
662
+ ? "Table of Contents"
663
+ : `${node.fieldFamily ?? "Field"}: ${node.fieldTarget ?? node.instruction.trim()}`;
664
+ paragraph.segments.push({
665
+ segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
666
+ kind: "field_ref",
667
+ from: start,
668
+ to: start + 1,
669
+ fieldFamily: node.fieldFamily!,
670
+ fieldTarget: node.fieldTarget,
671
+ instruction: node.instruction,
672
+ refreshStatus: node.refreshStatus ?? "stale",
673
+ label: fieldLabel,
674
+ } as SurfaceInlineSegment);
675
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
676
+ }
648
677
  paragraph.segments.push({
649
678
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
650
679
  kind: "opaque_inline",
@@ -653,7 +682,7 @@ function appendInlineSegments(
653
682
  fragmentId: "",
654
683
  warningId: "",
655
684
  label: "Field",
656
- detail: "Field code preserved for export.",
685
+ detail: `Preserve-only field: ${node.instruction.trim()}`,
657
686
  state: "locked-preserve-only",
658
687
  });
659
688
  return { nextCursor: start + 1, lockedFragmentIds: [] };
@@ -689,38 +718,54 @@ function appendComplexPreviewSegment(
689
718
  }
690
719
 
691
720
  function createChartDetail(node: ChartPreviewNode): string {
692
- return node.previewMediaId
693
- ? `Chart read-only preview. Fallback image ${node.previewMediaId}. Original XML preserved for export.`
694
- : "Chart read-only preview. Original XML preserved for export.";
721
+ const parts = ["Embedded chart."];
722
+ if (node.previewMediaId) {
723
+ parts.push(`Preview available via fallback image (${node.previewMediaId}).`);
724
+ } else {
725
+ parts.push("No fallback preview image found; chart data preserved in package.");
726
+ }
727
+ parts.push("Edit in Word to modify chart data. Original DrawingML preserved for lossless export.");
728
+ return parts.join(" ");
695
729
  }
696
730
 
697
731
  function createSmartArtDetail(node: SmartArtPreviewNode): string {
698
- return node.previewMediaId
699
- ? `SmartArt diagram read-only preview. Fallback image ${node.previewMediaId}. Original XML preserved for export.`
700
- : "SmartArt diagram read-only preview. Original XML preserved for export.";
732
+ const parts = ["SmartArt diagram."];
733
+ if (node.previewMediaId) {
734
+ parts.push(`Preview available via fallback image (${node.previewMediaId}).`);
735
+ } else {
736
+ parts.push("No fallback preview image found; diagram structure preserved in package.");
737
+ }
738
+ parts.push("Edit in Word to modify diagram layout and content. Original DrawingML preserved for lossless export.");
739
+ return parts.join(" ");
701
740
  }
702
741
 
703
742
  function createShapeDetail(node: ShapeNode): string {
704
- const parts = [node.isTextBox ? "Text box read-only preview." : "Shape read-only preview."];
743
+ if (node.isTextBox) {
744
+ const parts = ["Text box."];
745
+ if (node.text) parts.push(`Content: "${node.text}".`);
746
+ parts.push("Text content is visible; formatting and geometry preserved for export.");
747
+ return parts.join(" ");
748
+ }
749
+ const parts = ["Drawing shape."];
705
750
  if (node.geometry) parts.push(`Geometry: ${node.geometry}.`);
706
- if (node.text) parts.push(`Text: "${node.text}".`);
707
- parts.push("Original XML preserved for export.");
751
+ if (node.text) parts.push(`Text content: "${node.text}".`);
752
+ parts.push("Visual geometry is preview-only. Original DrawingML preserved for export.");
708
753
  return parts.join(" ");
709
754
  }
710
755
 
711
756
  function createWordArtDetail(node: WordArtNode): string {
712
- const parts = ["WordArt read-only preview."];
757
+ const parts = ["WordArt decorative text."];
713
758
  if (node.text) parts.push(`Text: "${node.text}".`);
714
759
  if (node.geometry) parts.push(`Effect: ${node.geometry}.`);
715
- parts.push("Original XML preserved for export.");
760
+ parts.push("Text effect is preview-only. Edit in Word for full formatting. Original DrawingML preserved for export.");
716
761
  return parts.join(" ");
717
762
  }
718
763
 
719
764
  function createVmlDetail(node: VmlShapeNode): string {
720
- const parts = ["VML shape read-only preview."];
765
+ const parts = ["Legacy VML drawing."];
721
766
  if (node.shapeType) parts.push(`Type: ${node.shapeType}.`);
722
- if (node.text) parts.push(`Text: "${node.text}".`);
723
- parts.push("Legacy VML; original XML preserved for export.");
767
+ if (node.text) parts.push(`Text content: "${node.text}".`);
768
+ parts.push("VML content is preview-only. Edit in Word for full control. Original VML XML preserved for export.");
724
769
  return parts.join(" ");
725
770
  }
726
771
 
@@ -1039,15 +1084,16 @@ function summarizePreviewInline(node: InlineNode): string {
1039
1084
  case "column_break":
1040
1085
  return "[Column break]";
1041
1086
  case "chart_preview":
1042
- return "[Chart]";
1087
+ return "[Embedded chart]";
1043
1088
  case "smartart_preview":
1044
- return "[SmartArt]";
1089
+ return "[SmartArt diagram]";
1045
1090
  case "shape":
1046
- return node.text ? `[Shape: ${node.text}]` : "[Shape]";
1091
+ if (node.isTextBox && node.text) return `[Text box: ${node.text}]`;
1092
+ return node.text ? `[Shape: ${node.text}]` : "[Drawing shape]";
1047
1093
  case "wordart":
1048
1094
  return node.text ? `[WordArt: ${node.text}]` : "[WordArt]";
1049
1095
  case "vml_shape":
1050
- return node.text ? `[VML: ${node.text}]` : "[VML shape]";
1096
+ return node.text ? `[VML: ${node.text}]` : "[Legacy VML drawing]";
1051
1097
  }
1052
1098
  }
1053
1099
 
@@ -1139,8 +1185,15 @@ function describePreservedInlinePreview(
1139
1185
  .join("")
1140
1186
  .trim();
1141
1187
  const instruction = (simpleInstruction ?? complexInstruction ?? "").trim();
1188
+ const family = /^([A-Z]+)/i.exec(instruction)?.[1]?.toUpperCase();
1189
+ const label =
1190
+ family === "TOC"
1191
+ ? "Table of Contents field"
1192
+ : family
1193
+ ? `${family} field`
1194
+ : "Field";
1142
1195
  return {
1143
- label: "Field",
1196
+ label,
1144
1197
  detail:
1145
1198
  instruction.length > 0
1146
1199
  ? `Read-only field preserved for export safety. Instruction: ${instruction}.`
@@ -1182,6 +1235,8 @@ function cloneMarks(marks: TextMark[]): {
1182
1235
  fontSize?: number;
1183
1236
  textColor?: string;
1184
1237
  } = {};
1238
+ let shadingColor: string | undefined;
1239
+ let highlightColor: string | undefined;
1185
1240
  for (const mark of marks) {
1186
1241
  switch (mark.type) {
1187
1242
  case "bold":
@@ -1200,7 +1255,10 @@ function cloneMarks(marks: TextMark[]): {
1200
1255
  else if (mark.val < 0) supported.push("subscript");
1201
1256
  break;
1202
1257
  case "backgroundColor":
1203
- attrs.backgroundColor = mark.color;
1258
+ shadingColor = mark.color;
1259
+ break;
1260
+ case "highlight":
1261
+ highlightColor = mark.color;
1204
1262
  break;
1205
1263
  case "charSpacing":
1206
1264
  attrs.charSpacing = mark.val;
@@ -1231,6 +1289,9 @@ function cloneMarks(marks: TextMark[]): {
1231
1289
  break;
1232
1290
  }
1233
1291
  }
1292
+ if (highlightColor || shadingColor) {
1293
+ attrs.backgroundColor = highlightColor ?? shadingColor;
1294
+ }
1234
1295
  const hasAttrs = Object.keys(attrs).length > 0;
1235
1296
  return hasAttrs ? { marks: supported, markAttrs: attrs } : { marks: supported };
1236
1297
  }
@@ -239,7 +239,7 @@ export const tableRowNodeSpec: NodeSpec = {
239
239
  };
240
240
 
241
241
  export const tableCellNodeSpec: NodeSpec = {
242
- content: "paragraph+",
242
+ content: "block+",
243
243
  tableRole: "cell",
244
244
  isolating: true,
245
245
  attrs: tableCellSpecAttrs,
@@ -264,7 +264,7 @@ export const tableCellNodeSpec: NodeSpec = {
264
264
  };
265
265
 
266
266
  export const tableHeaderCellNodeSpec: NodeSpec = {
267
- content: "paragraph+",
267
+ content: "block+",
268
268
  tableRole: "header_cell",
269
269
  isolating: true,
270
270
  attrs: tableCellSpecAttrs,
@@ -19,6 +19,7 @@ import type {
19
19
  ActiveListContext,
20
20
  ActiveNoteContext,
21
21
  CaretAffinity,
22
+ DocumentMode,
22
23
  EditorStoryTarget,
23
24
  EditorSurfaceSnapshot,
24
25
  EditorViewStateSnapshot,
@@ -36,6 +37,7 @@ import type { NumberingCatalog } from "../model/canonical-document.ts";
36
37
 
37
38
  export interface ViewState {
38
39
  viewMode: ViewMode;
40
+ documentMode: DocumentMode;
39
41
  workspaceMode: WorkspaceMode;
40
42
  zoomLevel: ZoomLevel;
41
43
  isFocused: boolean;
@@ -49,6 +51,7 @@ const MAX_ZOOM_PERCENT = 200;
49
51
 
50
52
  const DEFAULT_VIEW_STATE: ViewState = {
51
53
  viewMode: "editing",
54
+ documentMode: "editing",
52
55
  workspaceMode: "canvas",
53
56
  zoomLevel: 100,
54
57
  isFocused: false,
@@ -66,6 +69,11 @@ export function setViewMode(state: ViewState, mode: ViewMode): ViewState {
66
69
  return { ...state, viewMode: mode };
67
70
  }
68
71
 
72
+ export function setDocumentMode(state: ViewState, mode: DocumentMode): ViewState {
73
+ if (state.documentMode === mode) return state;
74
+ return { ...state, documentMode: mode };
75
+ }
76
+
69
77
  export function setWorkspaceMode(
70
78
  state: ViewState,
71
79
  workspaceMode: WorkspaceMode,
@@ -199,6 +207,7 @@ export function createEditorViewStateSnapshot(
199
207
 
200
208
  return {
201
209
  viewMode: derivedViewState.viewMode,
210
+ documentMode: derivedViewState.documentMode,
202
211
  workspaceMode: derivedViewState.workspaceMode,
203
212
  zoomLevel: derivedViewState.zoomLevel,
204
213
  activeStory,
@@ -227,6 +236,14 @@ function findBlockAtPosition(
227
236
  const inner = findBlockAtPosition(block.children, position);
228
237
  if (inner) return inner;
229
238
  }
239
+ if (block.kind === "table") {
240
+ for (const row of block.rows) {
241
+ for (const cell of row.cells) {
242
+ const inner = findBlockAtPosition(cell.content, position);
243
+ if (inner) return inner;
244
+ }
245
+ }
246
+ }
230
247
  return block;
231
248
  }
232
249
  }
@@ -262,7 +279,7 @@ function deriveInteractionViewState(
262
279
  pageLayout: PageLayoutSnapshot | null | undefined,
263
280
  ): ViewState {
264
281
  const activePageRegion = deriveActivePageRegion(activeStory, pageLayout) ?? viewState.activePageRegion;
265
- const activeObjectFrame = deriveActiveObjectFrame(surface, selection) ?? viewState.activeObjectFrame;
282
+ const activeObjectFrame = deriveActiveObjectFrame(surface, selection);
266
283
  const caretAffinity = deriveCaretAffinity(surface, selection);
267
284
 
268
285
  return {
@@ -434,12 +451,13 @@ function isObjectLikeSegment(
434
451
  function inferOpaqueObjectKind(
435
452
  segment: Extract<SurfaceInlineSegment, { kind: "opaque_inline" }>,
436
453
  ): "textbox" | "shape" | null {
437
- if (segment.label === "Shape") {
438
- return segment.detail.startsWith("Text box")
439
- ? "textbox"
440
- : "shape";
454
+ if (segment.label === "Text box") {
455
+ return "textbox";
456
+ }
457
+ if (segment.label === "Drawing shape") {
458
+ return "shape";
441
459
  }
442
- if (segment.label === "VML shape") {
460
+ if (segment.label === "Legacy VML drawing") {
443
461
  return segment.detail.includes("#_x0000_t202")
444
462
  ? "textbox"
445
463
  : "shape";