@beyondwork/docx-react-component 1.0.28 → 1.0.30

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 (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -14,6 +14,7 @@ import type {
14
14
  } from "../model/canonical-document.ts";
15
15
  import {
16
16
  describeOpaqueFragment,
17
+ describeStructuredWrapperBlock,
17
18
  listOpaqueFragments,
18
19
  listPreservedPackageParts,
19
20
  } from "../preservation/store.ts";
@@ -40,6 +41,8 @@ export function buildCompatibilityReport(
40
41
  }
41
42
  const featureEntries: CompatibilityFeatureEntry[] = [
42
43
  ...contentFeatures,
44
+ ...collectStructuredWrapperFeatures(content),
45
+ ...collectReviewFeatures(input.document),
43
46
  ...collectPreservationFeatures(input.document),
44
47
  ];
45
48
  const warnings = dedupeWarnings([
@@ -69,6 +72,110 @@ function hasSupportedRuntimeComments(
69
72
  return Object.values(comments).some((comment) => comment.anchor.kind !== "detached");
70
73
  }
71
74
 
75
+ function collectReviewFeatures(
76
+ document: CanonicalDocumentEnvelope,
77
+ ): CompatibilityFeatureEntry[] {
78
+ const entries: CompatibilityFeatureEntry[] = [];
79
+ const comments = Object.values(document.review.comments ?? {});
80
+ const revisions = Object.values(document.review.revisions ?? {});
81
+
82
+ const multiParagraphComment = comments.find(
83
+ (comment) => comment.metadata?.detachedReason === "multi-paragraph" && comment.anchor.kind === "detached",
84
+ );
85
+ if (multiParagraphComment?.anchor.kind === "detached") {
86
+ entries.push({
87
+ featureEntryId: "feature:comments-multi-paragraph-ranges",
88
+ featureKey: "comments-multi-paragraph-ranges",
89
+ featureClass: "preserve-only",
90
+ message:
91
+ "Cross-paragraph comment anchors remain visible but detached to preserve package truth and export safety.",
92
+ affectedAnchor: createRangeAnchor(
93
+ multiParagraphComment.anchor.lastKnownRange.from,
94
+ multiParagraphComment.anchor.lastKnownRange.to,
95
+ ),
96
+ details: {
97
+ commentId: multiParagraphComment.commentId,
98
+ detachedReason: multiParagraphComment.metadata?.detachedReason,
99
+ actionabilityNote: multiParagraphComment.metadata?.actionabilityNote,
100
+ },
101
+ });
102
+ }
103
+
104
+ const moveRevision = revisions.find((revision) => revision.kind === "move");
105
+ if (moveRevision) {
106
+ entries.push({
107
+ featureEntryId: "feature:tracked-moves",
108
+ featureKey: "tracked-moves",
109
+ featureClass: "preserve-only",
110
+ message:
111
+ "Tracked move revisions are preserved for round-trip and review visibility but remain non-actionable in the current runtime.",
112
+ affectedAnchor: toRevisionAffectedAnchor(moveRevision),
113
+ details: {
114
+ revisionId: moveRevision.changeId,
115
+ moveDirection: moveRevision.metadata?.moveData?.direction,
116
+ linkedRevisionId: moveRevision.metadata?.moveData?.linkedRevisionId,
117
+ },
118
+ });
119
+ }
120
+
121
+ const formattingRevisions = revisions.filter(
122
+ (revision) =>
123
+ revision.kind === "formatting" ||
124
+ (revision.kind === "property-change" && revision.metadata?.propertyChangeData?.xmlTag === "rPrChange"),
125
+ );
126
+ if (formattingRevisions.length > 0) {
127
+ const featureClass = formattingRevisions.some(
128
+ (revision) => typeof revision.metadata?.preserveOnlyReason === "string" && revision.metadata.preserveOnlyReason.length > 0,
129
+ )
130
+ ? "preserve-only"
131
+ : "supported-roundtrip";
132
+ entries.push({
133
+ featureEntryId: "feature:formatting-revisions",
134
+ featureKey: "formatting-revisions",
135
+ featureClass,
136
+ message:
137
+ featureClass === "preserve-only"
138
+ ? "Formatting revisions remain visible for review, but at least one formatting-change slice is still preserve-only."
139
+ : "Formatting-change revisions remain structured and round-trip through the runtime review model.",
140
+ affectedAnchor: toRevisionAffectedAnchor(formattingRevisions[0]!),
141
+ details: {
142
+ revisionCount: formattingRevisions.length,
143
+ },
144
+ });
145
+ }
146
+
147
+ const structuralRevisions = revisions.filter(
148
+ (revision) =>
149
+ revision.kind === "property-change" &&
150
+ revision.metadata?.propertyChangeData?.xmlTag !== "rPrChange",
151
+ );
152
+ if (structuralRevisions.length > 0) {
153
+ const featureClass = structuralRevisions.some(
154
+ (revision) => typeof revision.metadata?.preserveOnlyReason === "string" && revision.metadata.preserveOnlyReason.length > 0,
155
+ )
156
+ ? "preserve-only"
157
+ : "supported-roundtrip";
158
+ entries.push({
159
+ featureEntryId: "feature:structural-revisions",
160
+ featureKey: "structural-revisions",
161
+ featureClass,
162
+ message:
163
+ featureClass === "preserve-only"
164
+ ? "Structural property revisions are preserved for round-trip, but at least one slice remains non-actionable."
165
+ : "Structured paragraph, table, or section property revisions remain mapped in the runtime review model.",
166
+ affectedAnchor: toRevisionAffectedAnchor(structuralRevisions[0]!),
167
+ details: {
168
+ revisionCount: structuralRevisions.length,
169
+ revisionKinds: structuralRevisions.map(
170
+ (revision) => revision.metadata?.propertyChangeData?.xmlTag ?? revision.kind,
171
+ ),
172
+ },
173
+ });
174
+ }
175
+
176
+ return entries;
177
+ }
178
+
72
179
  function normalizeDocumentRoot(content: unknown): DocumentRootNode {
73
180
  if (content && typeof content === "object" && (content as { type?: string }).type === "doc") {
74
181
  return content as DocumentRootNode;
@@ -105,6 +212,7 @@ function collectContentFeatures(
105
212
  hyperlinks: false,
106
213
  images: false,
107
214
  tables: false,
215
+ mergedCells: false,
108
216
  sections: false,
109
217
  contentControls: false,
110
218
  };
@@ -139,7 +247,15 @@ function collectContentFeatures(
139
247
  entries.push(
140
248
  supportedEntry(
141
249
  "tables",
142
- "Structured tables keep cell topology and common visual properties through runtime editing and export.",
250
+ "Structured tables keep bounded topology and common visual properties through runtime editing and export.",
251
+ ),
252
+ );
253
+ }
254
+ if (flags.mergedCells) {
255
+ entries.push(
256
+ supportedEntry(
257
+ "merged-cells",
258
+ "Merged-cell topology survives through the supported table runtime and export path.",
143
259
  ),
144
260
  );
145
261
  }
@@ -173,6 +289,7 @@ function measureBlock(
173
289
  hyperlinks: boolean;
174
290
  images: boolean;
175
291
  tables: boolean;
292
+ mergedCells: boolean;
176
293
  sections: boolean;
177
294
  contentControls: boolean;
178
295
  },
@@ -189,6 +306,13 @@ function measureBlock(
189
306
  return measureParagraph(block, flags);
190
307
  case "table":
191
308
  flags.tables = true;
309
+ if (
310
+ block.rows.some((row) =>
311
+ row.cells.some((cell) => (cell.gridSpan ?? 1) > 1 || cell.verticalMerge != null),
312
+ )
313
+ ) {
314
+ flags.mergedCells = true;
315
+ }
192
316
  return block.rows.reduce(
193
317
  (size, row) =>
194
318
  size +
@@ -200,10 +324,15 @@ function measureBlock(
200
324
  0,
201
325
  );
202
326
  case "sdt":
327
+ if (describeStructuredWrapperBlock(block)?.featureKey === "content-controls") {
328
+ return 1;
329
+ }
203
330
  flags.contentControls = true;
204
331
  return block.children.reduce((size, child) => size + measureBlock(child, flags), 0);
205
332
  case "custom_xml":
206
- return block.children.reduce((size, child) => size + measureBlock(child, flags), 0);
333
+ return 1;
334
+ case "alt_chunk":
335
+ return 1;
207
336
  case "section_break":
208
337
  flags.sections = true;
209
338
  return 1;
@@ -212,6 +341,54 @@ function measureBlock(
212
341
  }
213
342
  }
214
343
 
344
+ function collectStructuredWrapperFeatures(
345
+ content: DocumentRootNode,
346
+ ): CompatibilityFeatureEntry[] {
347
+ const entries: CompatibilityFeatureEntry[] = [];
348
+ let structuredWrapperIndex = 0;
349
+
350
+ function visitBlock(block: BlockNode): void {
351
+ const descriptor = describeStructuredWrapperBlock(block);
352
+ if (descriptor) {
353
+ entries.push({
354
+ featureEntryId: `feature:structured-wrapper:${structuredWrapperIndex}`,
355
+ featureKey: descriptor.featureKey,
356
+ featureClass: "preserve-only",
357
+ message: descriptor.label,
358
+ details: {
359
+ detail: descriptor.detail,
360
+ surface: "structured-wrapper",
361
+ },
362
+ });
363
+ structuredWrapperIndex += 1;
364
+ return;
365
+ }
366
+
367
+ if (block.type === "table") {
368
+ for (const row of block.rows) {
369
+ for (const cell of row.cells) {
370
+ for (const child of cell.children) {
371
+ visitBlock(child);
372
+ }
373
+ }
374
+ }
375
+ return;
376
+ }
377
+
378
+ if (block.type === "sdt") {
379
+ for (const child of block.children) {
380
+ visitBlock(child);
381
+ }
382
+ }
383
+ }
384
+
385
+ for (const block of content.children) {
386
+ visitBlock(block);
387
+ }
388
+
389
+ return entries;
390
+ }
391
+
215
392
  function measureParagraph(
216
393
  paragraph: ParagraphNode,
217
394
  flags: {
@@ -381,6 +558,16 @@ function collectSubPartFeatures(
381
558
  const noteCount =
382
559
  Object.keys(subParts.footnoteCollection?.footnotes ?? {}).length +
383
560
  Object.keys(subParts.footnoteCollection?.endnotes ?? {}).length;
561
+ const headerFooterBookmarkCount =
562
+ [...(subParts.headers ?? []), ...(subParts.footers ?? [])].reduce(
563
+ (count, subPart) => count + countBookmarksInBlocks(subPart.blocks),
564
+ 0,
565
+ );
566
+ const noteBookmarkCount =
567
+ [
568
+ ...Object.values(subParts.footnoteCollection?.footnotes ?? {}),
569
+ ...Object.values(subParts.footnoteCollection?.endnotes ?? {}),
570
+ ].reduce((count, note) => count + countBookmarksInBlocks(note.blocks), 0);
384
571
 
385
572
  if (hasHeaderFooterContent && !entries.some((entry) => entry.featureKey === "headers-footers-lossy")) {
386
573
  entries.push({
@@ -391,6 +578,7 @@ function collectSubPartFeatures(
391
578
  details: {
392
579
  headerCount: subParts.headers?.length ?? 0,
393
580
  footerCount: subParts.footers?.length ?? 0,
581
+ bookmarkCount: headerFooterBookmarkCount,
394
582
  },
395
583
  });
396
584
  }
@@ -404,6 +592,7 @@ function collectSubPartFeatures(
404
592
  details: {
405
593
  footnoteCount: Object.keys(subParts.footnoteCollection?.footnotes ?? {}).length,
406
594
  endnoteCount: Object.keys(subParts.footnoteCollection?.endnotes ?? {}).length,
595
+ bookmarkCount: noteBookmarkCount,
407
596
  },
408
597
  });
409
598
  }
@@ -472,6 +661,45 @@ function collectLossySubPartFeatures(
472
661
  return entries;
473
662
  }
474
663
 
664
+ function countBookmarksInBlocks(blocks: readonly BlockNode[]): number {
665
+ return blocks.reduce((count, block) => count + countBookmarksInBlock(block), 0);
666
+ }
667
+
668
+ function countBookmarksInBlock(block: BlockNode): number {
669
+ switch (block.type) {
670
+ case "paragraph":
671
+ return (block.children ?? []).reduce((count, child) => count + countBookmarksInInlineNode(child), 0);
672
+ case "table":
673
+ return block.rows.reduce(
674
+ (count, row) =>
675
+ count +
676
+ row.cells.reduce(
677
+ (cellCount, cell) =>
678
+ cellCount + cell.children.reduce((childCount, child) => childCount + countBookmarksInBlock(child), 0),
679
+ 0,
680
+ ),
681
+ 0,
682
+ );
683
+ case "sdt":
684
+ case "custom_xml":
685
+ return block.children.reduce((count, child) => count + countBookmarksInBlock(child), 0);
686
+ default:
687
+ return 0;
688
+ }
689
+ }
690
+
691
+ function countBookmarksInInlineNode(node: InlineNode): number {
692
+ switch (node.type) {
693
+ case "bookmark_start":
694
+ return 1;
695
+ case "hyperlink":
696
+ case "field":
697
+ return (node.children ?? []).reduce((count, child) => count + countBookmarksInInlineNode(child), 0);
698
+ default:
699
+ return 0;
700
+ }
701
+ }
702
+
475
703
  function collectLossyBlocks(
476
704
  blocks: readonly BlockNode[],
477
705
  surface: string,
@@ -613,6 +841,22 @@ function supportedEntry(
613
841
  };
614
842
  }
615
843
 
844
+ function toRevisionAffectedAnchor(
845
+ revision: CanonicalDocumentEnvelope["review"]["revisions"][string],
846
+ ) {
847
+ switch (revision.anchor.kind) {
848
+ case "range":
849
+ return createRangeAnchor(revision.anchor.range.from, revision.anchor.range.to);
850
+ case "node":
851
+ return createRangeAnchor(revision.anchor.at, revision.anchor.at + 1);
852
+ case "detached":
853
+ return createRangeAnchor(
854
+ revision.anchor.lastKnownRange.from,
855
+ revision.anchor.lastKnownRange.to,
856
+ );
857
+ }
858
+ }
859
+
616
860
  function dedupeWarnings(warnings: EditorWarning[]): EditorWarning[] {
617
861
  const byId = new Map<string, EditorWarning>();
618
862
 
@@ -26,18 +26,31 @@ export interface ClosureValidationContext {
26
26
  harnessShowcase?: HarnessShowcaseValidationContext;
27
27
  }
28
28
 
29
+ export const HARNESS_SHOWCASE_AREAS = [
30
+ "workflow-overlay",
31
+ "blocked-commands",
32
+ "preserve-only",
33
+ "search-candidates",
34
+ "right-rail",
35
+ "multi-scope",
36
+ "public-api",
37
+ "sidebar-integration",
38
+ "policy-yaml",
39
+ "harness-lifecycle",
40
+ "export-continuity",
41
+ "main-story-text",
42
+ "paragraph-structure",
43
+ "secondary-story-text",
44
+ "formatting-property-list",
45
+ "table-field-slices",
46
+ "fail-closed-boundaries",
47
+ "media-section-fail-closed",
48
+ "continuity-proof",
49
+ "harness-suggestion-telemetry",
50
+ ] as const;
51
+
29
52
  export type HarnessShowcaseArea =
30
- | "workflow-overlay"
31
- | "blocked-commands"
32
- | "preserve-only"
33
- | "search-candidates"
34
- | "right-rail"
35
- | "multi-scope"
36
- | "public-api"
37
- | "sidebar-integration"
38
- | "policy-yaml"
39
- | "harness-lifecycle"
40
- | "export-continuity";
53
+ (typeof HARNESS_SHOWCASE_AREAS)[number];
41
54
 
42
55
  export interface HarnessShowcaseAreaProof {
43
56
  demonstrated: boolean;