@beyondwork/docx-react-component 1.0.14 → 1.0.16

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.
@@ -144,17 +144,28 @@ export function __createWordReviewEditorRefBridge(
144
144
  reopenComment: (commentId) => runtime.reopenComment(commentId),
145
145
  addCommentReply: (commentId, body) => runtime.addCommentReply(commentId, body),
146
146
  editCommentBody: (commentId, body) => runtime.editCommentBody(commentId, body),
147
+ deleteComment: (commentId) => {
148
+ applyRuntimeDeleteComment(runtime, commentId);
149
+ },
147
150
  acceptChange: (changeId) => runtime.acceptChange(changeId),
148
151
  rejectChange: (changeId) => runtime.rejectChange(changeId),
149
152
  acceptAllChanges: () => runtime.acceptAllChanges(),
150
153
  rejectAllChanges: () => runtime.rejectAllChanges(),
151
154
  exportDocx: (options) => runtime.exportDocx(options),
152
155
  getSnapshot: () => runtime.getPersistedSnapshot(),
156
+ getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
153
157
  getCompatibilityReport: () => runtime.getCompatibilityReport(),
154
158
  getWarnings: () => runtime.getWarnings(),
155
- getComments: () => runtime.getRenderSnapshot().comments,
156
- getTrackedChanges: () => runtime.getRenderSnapshot().trackedChanges,
159
+ getCommentSidebarSnapshot: () =>
160
+ clonePublicValue(runtime.getRenderSnapshot().comments),
161
+ getTrackedChangesSnapshot: () =>
162
+ clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
163
+ getComments: () => clonePublicValue(runtime.getRenderSnapshot().comments),
164
+ getTrackedChanges: () =>
165
+ clonePublicValue(runtime.getRenderSnapshot().trackedChanges),
166
+ isDirty: () => runtime.getRenderSnapshot().isDirty,
157
167
  getFormattingState: () => getFormattingStateFromRenderSnapshot(runtime.getRenderSnapshot()),
168
+ replaceText: (text, target) => runtime.replaceText(text, target),
158
169
  toggleBold: () => {
159
170
  applyRuntimeFormattingOperation(runtime, { type: "toggle", mark: "bold" });
160
171
  },
@@ -275,6 +286,14 @@ export function __createWordReviewEditorRefBridge(
275
286
  clearSearch: () => {
276
287
  mountedSurface?.clearSearch();
277
288
  },
289
+ setSelection: (selection) => {
290
+ runtime.dispatch({
291
+ type: "selection.set",
292
+ selection: toRuntimeSelectionSnapshot(
293
+ normalizeRequestedSelection(runtime.getRenderSnapshot(), selection),
294
+ ),
295
+ });
296
+ },
278
297
  scrollToRevision: (revisionId: string) => {
279
298
  const revision = runtime.getRenderSnapshot().trackedChanges.revisions.find(
280
299
  (r) => r.revisionId === revisionId,
@@ -671,6 +690,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
671
690
  activeRuntime.addCommentReply(commentId, body, currentUser.userId),
672
691
  editCommentBody: (commentId, body) =>
673
692
  activeRuntime.editCommentBody(commentId, body),
693
+ deleteComment: (commentId) => {
694
+ applyRuntimeDeleteComment(activeRuntime, commentId);
695
+ },
674
696
  acceptChange: (changeId) => activeRuntime.acceptChange(changeId),
675
697
  rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
676
698
  acceptAllChanges: () => activeRuntime.acceptAllChanges(),
@@ -694,12 +716,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
694
716
  onEvent: onEventRef.current,
695
717
  }),
696
718
  getSnapshot: () => activeRuntime.getPersistedSnapshot(),
719
+ getRenderSnapshot: () => clonePublicValue(activeRuntime.getRenderSnapshot()),
697
720
  getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
698
721
  getWarnings: () => activeRuntime.getWarnings(),
699
- getComments: () => activeRuntime.getRenderSnapshot().comments,
700
- getTrackedChanges: () => activeRuntime.getRenderSnapshot().trackedChanges,
722
+ getCommentSidebarSnapshot: () =>
723
+ clonePublicValue(activeRuntime.getRenderSnapshot().comments),
724
+ getTrackedChangesSnapshot: () =>
725
+ clonePublicValue(activeRuntime.getRenderSnapshot().trackedChanges),
726
+ getComments: () =>
727
+ clonePublicValue(activeRuntime.getRenderSnapshot().comments),
728
+ getTrackedChanges: () =>
729
+ clonePublicValue(activeRuntime.getRenderSnapshot().trackedChanges),
730
+ isDirty: () => activeRuntime.getRenderSnapshot().isDirty,
701
731
  getFormattingState: () =>
702
732
  getFormattingStateFromRenderSnapshot(activeRuntime.getRenderSnapshot()),
733
+ replaceText: (text, target) => activeRuntime.replaceText(text, target),
703
734
  toggleBold: () => {
704
735
  applyRuntimeFormattingOperation(activeRuntime, {
705
736
  type: "toggle",
@@ -838,6 +869,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
838
869
  clearSearch: () => {
839
870
  surfaceRef.current?.clearSearch();
840
871
  },
872
+ setSelection: (selection) => {
873
+ activeRuntime.dispatch({
874
+ type: "selection.set",
875
+ selection: toRuntimeSelectionSnapshot(
876
+ normalizeRequestedSelection(activeRuntime.getRenderSnapshot(), selection),
877
+ ),
878
+ });
879
+ },
841
880
  scrollToRevision: (revisionId: string) => {
842
881
  const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
843
882
  (r) => r.revisionId === revisionId,
@@ -1354,6 +1393,70 @@ function dispatchRuntimeDocumentMutation(
1354
1393
  });
1355
1394
  }
1356
1395
 
1396
+ function applyRuntimeDeleteComment(
1397
+ runtime: WordReviewEditorRuntime,
1398
+ commentId: string,
1399
+ ): void {
1400
+ const snapshot = runtime.getRenderSnapshot();
1401
+ if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
1402
+ return;
1403
+ }
1404
+
1405
+ const persistedSnapshot = runtime.getPersistedSnapshot();
1406
+ if (!persistedSnapshot.canonicalDocument.review.comments[commentId]) {
1407
+ return;
1408
+ }
1409
+
1410
+ const nextComments = {
1411
+ ...persistedSnapshot.canonicalDocument.review.comments,
1412
+ };
1413
+ delete nextComments[commentId];
1414
+
1415
+ runtime.dispatch({
1416
+ type: "document.replace",
1417
+ document: {
1418
+ ...persistedSnapshot.canonicalDocument,
1419
+ review: {
1420
+ ...persistedSnapshot.canonicalDocument.review,
1421
+ comments: nextComments,
1422
+ },
1423
+ },
1424
+ selection: toRuntimeSelectionSnapshot(snapshot.selection),
1425
+ origin: {
1426
+ source: "api",
1427
+ timestamp: new Date().toISOString(),
1428
+ },
1429
+ });
1430
+ }
1431
+
1432
+ function normalizeRequestedSelection(
1433
+ snapshot: RuntimeRenderSnapshot,
1434
+ selection: PublicSelectionSnapshot | null,
1435
+ ): PublicSelectionSnapshot {
1436
+ return selection ?? createCollapsedPublicSelection(snapshot.selection.head);
1437
+ }
1438
+
1439
+ function createCollapsedPublicSelection(position: number): PublicSelectionSnapshot {
1440
+ return {
1441
+ anchor: position,
1442
+ head: position,
1443
+ isCollapsed: true,
1444
+ activeRange: {
1445
+ kind: "range",
1446
+ from: position,
1447
+ to: position,
1448
+ assoc: {
1449
+ start: -1,
1450
+ end: 1,
1451
+ },
1452
+ },
1453
+ };
1454
+ }
1455
+
1456
+ function clonePublicValue<T>(value: T): T {
1457
+ return structuredClone(value);
1458
+ }
1459
+
1357
1460
  function searchSnapshotSurface(
1358
1461
  snapshot: RuntimeRenderSnapshot,
1359
1462
  query: string,
@@ -10,6 +10,55 @@ const HEX_COLOR_RE = /^[0-9A-Fa-f]{3,8}$/;
10
10
  const SAFE_FONT_RE = /^[A-Za-z0-9 ,\-'"]+$/;
11
11
  const SAFE_ALIGNMENT = new Set(["left", "center", "right", "justify", "start", "end"]);
12
12
 
13
+ function resolveHeadingLevel(
14
+ styleId: string | null,
15
+ outlineLevel: number | null,
16
+ ): number | null {
17
+ if (styleId) {
18
+ const normalized = styleId.toLowerCase();
19
+ const headingMatch = /^heading([1-6])$/.exec(normalized);
20
+ if (headingMatch) {
21
+ return Number.parseInt(headingMatch[1], 10);
22
+ }
23
+ if (normalized === "title") {
24
+ return 1;
25
+ }
26
+ if (normalized === "subtitle") {
27
+ return 2;
28
+ }
29
+ }
30
+
31
+ if (
32
+ typeof outlineLevel === "number" &&
33
+ Number.isInteger(outlineLevel) &&
34
+ outlineLevel >= 0 &&
35
+ outlineLevel <= 5
36
+ ) {
37
+ return outlineLevel + 1;
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ function headingClassList(level: number): string[] {
44
+ switch (level) {
45
+ case 1:
46
+ return ["text-3xl", "font-semibold", "tracking-tight", "leading-tight"];
47
+ case 2:
48
+ return ["text-2xl", "font-semibold", "tracking-tight"];
49
+ case 3:
50
+ return ["text-xl", "font-medium"];
51
+ case 4:
52
+ return ["text-lg", "font-medium"];
53
+ case 5:
54
+ return ["text-base", "font-semibold", "uppercase", "tracking-[0.12em]"];
55
+ case 6:
56
+ return ["text-sm", "font-semibold", "uppercase", "tracking-[0.16em]"];
57
+ default:
58
+ return [];
59
+ }
60
+ }
61
+
13
62
  /** Validate a raw hex color string from OOXML (no leading #). Returns sanitized `#hex` or null. */
14
63
  function safeHexColor(raw: string | null | undefined): string | null {
15
64
  if (!raw || raw === "auto") return null;
@@ -45,6 +94,7 @@ export const editorSchema = new Schema({
45
94
  styleId: { default: null },
46
95
  numberingInstanceId: { default: null },
47
96
  numberingLevel: { default: null },
97
+ numberingPrefix: { default: null },
48
98
  alignment: { default: null },
49
99
  spacingBefore: { default: null },
50
100
  spacingAfter: { default: null },
@@ -58,6 +108,7 @@ export const editorSchema = new Schema({
58
108
  borderBottom: { default: null },
59
109
  borderLeft: { default: null },
60
110
  borderRight: { default: null },
111
+ outlineLevel: { default: null },
61
112
  bidi: { default: null },
62
113
  pageBreakBefore: { default: null },
63
114
  },
@@ -65,11 +116,10 @@ export const editorSchema = new Schema({
65
116
  toDOM(node) {
66
117
  const classes: string[] = ["leading-relaxed"];
67
118
  const styleId = node.attrs.styleId as string | null;
68
- if (styleId) {
69
- const lower = styleId.toLowerCase();
70
- if (lower === "heading1") classes.push("text-2xl font-medium");
71
- else if (lower === "heading2") classes.push("text-xl font-medium");
72
- else if (lower === "heading3") classes.push("text-lg font-medium");
119
+ const outlineLevel = node.attrs.outlineLevel as number | null;
120
+ const headingLevel = resolveHeadingLevel(styleId, outlineLevel);
121
+ if (headingLevel) {
122
+ classes.push(...headingClassList(headingLevel));
73
123
  }
74
124
  const attrs: Record<string, string> = { class: classes.join(" ") };
75
125
  const styles: string[] = [];
@@ -106,8 +156,50 @@ export const editorSchema = new Schema({
106
156
  if (pageBreak) styles.push("border-top: 2px dashed rgba(0,0,0,0.1); padding-top: 8px; margin-top: 16px");
107
157
  const bidi = node.attrs.bidi as boolean | null;
108
158
  if (bidi) attrs.dir = "rtl";
159
+ if (headingLevel) {
160
+ attrs["data-heading-level"] = String(headingLevel);
161
+ }
109
162
  if (styles.length > 0) attrs.style = styles.join("; ");
110
- return ["p", attrs, 0];
163
+ const numberingPrefix = node.attrs.numberingPrefix as string | null;
164
+ const numberingLevel = node.attrs.numberingLevel as number | null;
165
+ const children: Array<string | number | readonly unknown[]> = [];
166
+ if (pageBreak) {
167
+ children.push([
168
+ "span",
169
+ {
170
+ class:
171
+ "mb-2 inline-flex items-center gap-2 text-[10px] font-medium uppercase tracking-[0.18em] text-tertiary",
172
+ contenteditable: "false",
173
+ "data-page-break-before": "true",
174
+ },
175
+ "Page break",
176
+ ]);
177
+ }
178
+ if (numberingPrefix) {
179
+ const minWidth = Math.min(Math.max(numberingPrefix.length + 1, 4), 14);
180
+ children.push([
181
+ "span",
182
+ {
183
+ class:
184
+ "inline-flex select-none items-center justify-end text-tertiary font-[family-name:var(--font-legal-sans)]",
185
+ contenteditable: "false",
186
+ "data-numbering-prefix": numberingPrefix,
187
+ ...(typeof numberingLevel === "number"
188
+ ? { "data-numbering-level": String(numberingLevel) }
189
+ : {}),
190
+ style: `min-width: ${minWidth}ch; margin-right: 0.75rem; font-variant-numeric: tabular-nums;`,
191
+ },
192
+ numberingPrefix,
193
+ ]);
194
+ }
195
+ children.push([
196
+ "span",
197
+ {
198
+ class: "pm-paragraph-content",
199
+ },
200
+ 0,
201
+ ]);
202
+ return ["p", attrs, ...children];
111
203
  },
112
204
  },
113
205
 
@@ -132,13 +224,42 @@ export const editorSchema = new Schema({
132
224
  selectable: false,
133
225
  attrs: {
134
226
  tabWidth: { default: null },
227
+ leader: { default: null },
228
+ align: { default: null },
135
229
  },
136
230
  toDOM(node) {
137
231
  const width = node.attrs.tabWidth as number | null;
138
- if (width && width > 0) {
139
- return ["span", { style: `display: inline-block; width: ${width}px`, "data-node-type": "tab" }, "\u00A0"];
232
+ const leader = node.attrs.leader as string | null;
233
+ const align = node.attrs.align as string | null;
234
+ const styles = [
235
+ `display: inline-block`,
236
+ `width: ${width && width > 0 ? width : 32}px`,
237
+ `min-width: 8px`,
238
+ ];
239
+ if (leader === "dot" || leader === "middleDot") {
240
+ styles.push(
241
+ `background-image: radial-gradient(circle, currentColor 1px, transparent 1.25px)`,
242
+ `background-size: 6px 3px`,
243
+ `background-repeat: repeat-x`,
244
+ `background-position: left calc(100% - 2px)`,
245
+ `opacity: 0.55`,
246
+ );
247
+ } else if (leader === "hyphen") {
248
+ styles.push(`border-bottom: 1px dashed rgba(107,107,107,0.65)`);
249
+ } else if (leader === "underscore") {
250
+ styles.push(`border-bottom: 1px solid rgba(107,107,107,0.65)`);
251
+ } else if (leader === "heavy") {
252
+ styles.push(`border-bottom: 2px solid rgba(107,107,107,0.75)`);
140
253
  }
141
- return ["span", { class: "inline-block w-8", "data-node-type": "tab" }, "\u00A0"];
254
+ return [
255
+ "span",
256
+ {
257
+ style: styles.join("; "),
258
+ "data-node-type": "tab",
259
+ title: align ? `Tab stop · ${align}` : "Tab stop",
260
+ },
261
+ "\u00A0",
262
+ ];
142
263
  },
143
264
  },
144
265
 
@@ -366,19 +487,33 @@ export const editorSchema = new Schema({
366
487
  detail: { default: "" },
367
488
  },
368
489
  toDOM(node) {
490
+ const fragmentId = node.attrs.fragmentId as string;
491
+ const isPreview = fragmentId.startsWith("preview:");
369
492
  return [
370
493
  "div",
371
494
  {
372
- class: "border-l-2 border-dashed border-warning/30 pl-4 py-2 rounded-r bg-warning-soft/20 my-2",
495
+ class: isPreview
496
+ ? "my-3 rounded-xl border border-primary/15 bg-surface-raised/50 px-4 py-3"
497
+ : "border-l-2 border-dashed border-warning/30 pl-4 py-2 rounded-r bg-warning-soft/20 my-2",
373
498
  contenteditable: "false",
374
499
  "data-node-type": "opaque_block",
375
500
  },
376
501
  [
377
502
  "div",
378
- { class: "flex items-center gap-1.5 text-xs text-tertiary mb-1" },
379
- "\uD83D\uDD12 " + (node.attrs.label as string),
503
+ {
504
+ class: isPreview
505
+ ? "mb-2 text-[11px] uppercase tracking-[0.18em] text-tertiary"
506
+ : "flex items-center gap-1.5 text-xs text-tertiary mb-1",
507
+ },
508
+ `${isPreview ? "" : "\uD83D\uDD12 "}${node.attrs.label as string}`,
509
+ ],
510
+ [
511
+ "p",
512
+ {
513
+ class: isPreview ? "text-sm text-secondary whitespace-pre-wrap leading-relaxed" : "text-sm text-secondary whitespace-pre-wrap",
514
+ },
515
+ node.attrs.detail as string,
380
516
  ],
381
- ["p", { class: "text-sm text-secondary" }, node.attrs.detail as string],
382
517
  ];
383
518
  },
384
519
  },
@@ -100,7 +100,15 @@ function buildParagraph(
100
100
  ? ((prevStop as { pos?: number }).pos ?? (prevStop as { position?: number }).position ?? 0)
101
101
  : 0;
102
102
  const widthPx = Math.round((stopPos - prevPos) / 15);
103
- content.push(editorSchema.nodes.tab_char.create({ tabWidth: widthPx > 8 ? widthPx : null }));
103
+ const leader = (stop as { leader?: string }).leader ?? null;
104
+ const align = (stop as { val?: string }).val ?? null;
105
+ content.push(
106
+ editorSchema.nodes.tab_char.create({
107
+ tabWidth: widthPx > 8 ? widthPx : null,
108
+ leader,
109
+ align,
110
+ }),
111
+ );
104
112
  tabIndex++;
105
113
  } else {
106
114
  const nodes = buildInlineContent(segment);
@@ -113,6 +121,9 @@ function buildParagraph(
113
121
  styleId: block.styleId ?? null,
114
122
  numberingInstanceId: block.numbering?.numberingInstanceId ?? null,
115
123
  numberingLevel: block.numbering?.level ?? null,
124
+ numberingPrefix:
125
+ (block as typeof block & { numberingPrefix?: string }).numberingPrefix ??
126
+ null,
116
127
  alignment: block.alignment ?? null,
117
128
  spacingBefore: block.spacing?.before ?? null,
118
129
  spacingAfter: block.spacing?.after ?? null,
@@ -126,6 +137,7 @@ function buildParagraph(
126
137
  borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
127
138
  borderLeft: (block.borders as Record<string, unknown>)?.left ?? null,
128
139
  borderRight: (block.borders as Record<string, unknown>)?.right ?? null,
140
+ outlineLevel: block.outlineLevel ?? null,
129
141
  bidi: block.bidi ?? null,
130
142
  pageBreakBefore: block.pageBreakBefore ?? null,
131
143
  },
@@ -231,19 +243,9 @@ function buildTable(
231
243
  if (child.kind === "paragraph") {
232
244
  cellContent.push(buildParagraph(child as Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>));
233
245
  } else if (child.kind === "table") {
234
- cellContent.push(buildNestedTablePlaceholder(child as Extract<SurfaceBlockSnapshot, { kind: "table" }>));
246
+ cellContent.push(buildTable(child as Extract<SurfaceBlockSnapshot, { kind: "table" }>));
235
247
  } else if (child.kind === "sdt_block") {
236
- cellContent.push(buildOpaqueBlock({
237
- blockId: child.blockId,
238
- kind: "opaque_block",
239
- from: child.from,
240
- to: child.to,
241
- fragmentId: child.blockId,
242
- warningId: child.blockId,
243
- label: child.alias ?? child.tag ?? "Content control",
244
- detail: "Structured content control remains read-only inside table cells.",
245
- state: "locked-preserve-only",
246
- }));
248
+ cellContent.push(buildSdtBlock(child as Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>));
247
249
  } else if (child.kind === "opaque_block") {
248
250
  cellContent.push(buildOpaqueBlock(child as Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>));
249
251
  }
@@ -276,22 +278,6 @@ function buildTable(
276
278
  );
277
279
  }
278
280
 
279
- function buildNestedTablePlaceholder(
280
- block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
281
- ): PMNode {
282
- return buildOpaqueBlock({
283
- blockId: block.blockId,
284
- kind: "opaque_block",
285
- from: block.from,
286
- to: block.to,
287
- fragmentId: block.blockId,
288
- warningId: block.blockId,
289
- label: "Nested table",
290
- detail: "Nested table remains read-only in the live ProseMirror cell surface.",
291
- state: "locked-preserve-only",
292
- });
293
- }
294
-
295
281
  function buildSdtBlock(
296
282
  block: Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>,
297
283
  ): PMNode {