@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
@@ -34,8 +34,9 @@ export function createPMStateFromSnapshot(
34
34
  selection: SelectionSnapshot,
35
35
  plugins: Plugin[],
36
36
  mediaPreviews: Record<string, MediaPreviewDescriptor> = {},
37
+ showUnsupportedObjectPreviews = true,
37
38
  ): PMStateResult {
38
- const doc = buildPMDoc(surface, mediaPreviews);
39
+ const doc = buildPMDoc(surface, mediaPreviews, showUnsupportedObjectPreviews);
39
40
  const positionMap = buildPositionMap(surface);
40
41
  const pmSelection = createPMSelectionFromSnapshot(doc, positionMap, selection);
41
42
 
@@ -109,8 +110,9 @@ function resolveInlineBoundary(
109
110
  function buildPMDoc(
110
111
  surface: EditorSurfaceSnapshot,
111
112
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
113
+ showUnsupportedObjectPreviews: boolean,
112
114
  ): PMNode {
113
- const blocks = buildPMBlocks(surface.blocks, mediaPreviews);
115
+ const blocks = buildPMBlocks(surface.blocks, mediaPreviews, showUnsupportedObjectPreviews);
114
116
 
115
117
  // Ensure at least one block (PM requires non-empty doc)
116
118
  if (blocks.length === 0) {
@@ -123,6 +125,7 @@ function buildPMDoc(
123
125
  function buildPMBlocks(
124
126
  blocks: SurfaceBlockSnapshot[],
125
127
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
128
+ showUnsupportedObjectPreviews: boolean,
126
129
  ): PMNode[] {
127
130
  const nodes: PMNode[] = [];
128
131
 
@@ -134,11 +137,19 @@ function buildPMBlocks(
134
137
  const nextParagraph = nextBlock?.kind === "paragraph" ? nextBlock : null;
135
138
 
136
139
  if (block.kind === "paragraph") {
137
- nodes.push(buildParagraph(block, previousParagraph, nextParagraph, mediaPreviews));
140
+ nodes.push(
141
+ buildParagraph(
142
+ block,
143
+ previousParagraph,
144
+ nextParagraph,
145
+ mediaPreviews,
146
+ showUnsupportedObjectPreviews,
147
+ ),
148
+ );
138
149
  } else if (block.kind === "table") {
139
- nodes.push(buildTable(block, mediaPreviews));
150
+ nodes.push(buildTable(block, mediaPreviews, showUnsupportedObjectPreviews));
140
151
  } else if (block.kind === "sdt_block") {
141
- nodes.push(buildSdtBlock(block, mediaPreviews));
152
+ nodes.push(buildSdtBlock(block, mediaPreviews, showUnsupportedObjectPreviews));
142
153
  } else {
143
154
  nodes.push(buildOpaqueBlock(block));
144
155
  }
@@ -152,10 +163,22 @@ function buildParagraph(
152
163
  previousParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
153
164
  nextParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
154
165
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
166
+ showUnsupportedObjectPreviews: boolean,
155
167
  ): PMNode {
156
168
  const content: PMNode[] = [];
157
- const tabStops = block.tabStops ?? [];
169
+ const paragraphLayout = resolveParagraphLayout(block);
170
+ const tabStops = paragraphLayout.tabStops;
158
171
  let tabIndex = 0;
172
+ const textSegments = block.segments.filter(
173
+ (segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> => segment.kind === "text",
174
+ );
175
+ const fullyVanishedParagraph =
176
+ textSegments.length > 0 &&
177
+ textSegments.every((segment) => segment.marks?.includes("vanish")) &&
178
+ block.segments.every((segment) => segment.kind === "text");
179
+
180
+ if (fullyVanishedParagraph) {
181
+ }
159
182
 
160
183
  for (const segment of block.segments) {
161
184
  if (segment.kind === "tab" && tabIndex < tabStops.length) {
@@ -177,7 +200,7 @@ function buildParagraph(
177
200
  );
178
201
  tabIndex++;
179
202
  } else {
180
- const nodes = buildInlineContent(segment, mediaPreviews);
203
+ const nodes = buildInlineContent(segment, mediaPreviews, showUnsupportedObjectPreviews);
181
204
  content.push(...nodes);
182
205
  }
183
206
  }
@@ -211,18 +234,20 @@ function buildParagraph(
211
234
  (block as typeof block & { numberingSuffix?: string }).numberingSuffix ??
212
235
  null,
213
236
  alignment: block.alignment ?? null,
214
- spacingBefore: block.spacing?.before ?? null,
215
- spacingAfter: block.spacing?.after ?? null,
216
- lineSpacing: block.spacing?.line ?? null,
217
- lineRule: block.spacing?.lineRule ?? null,
237
+ spacingBefore: paragraphLayout.spacing?.before ?? null,
238
+ spacingAfter: paragraphLayout.spacing?.after ?? null,
239
+ lineSpacing: paragraphLayout.spacing?.line ?? null,
240
+ lineRule: paragraphLayout.spacing?.lineRule ?? null,
218
241
  contextualSpacing: block.contextualSpacing ?? null,
219
242
  listContinuation: listContinuation || null,
220
243
  contextualSpacingBefore: contextualSpacingBefore || null,
221
244
  contextualSpacingAfter: contextualSpacingAfter || null,
222
- indentLeft: block.indentation?.left ?? null,
223
- indentRight: block.indentation?.right ?? null,
224
- indentFirstLine: block.indentation?.firstLine ?? null,
225
- indentHanging: block.indentation?.hanging ?? null,
245
+ indentLeft: paragraphLayout.indentation?.left ?? null,
246
+ indentRight: paragraphLayout.indentation?.right ?? null,
247
+ indentFirstLine: paragraphLayout.indentation?.firstLine ?? null,
248
+ indentHanging: paragraphLayout.indentation?.hanging ?? null,
249
+ numberingMarkerWidth: paragraphLayout.markerLane?.width ?? null,
250
+ numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
226
251
  shadingFill: block.shading?.fill ?? null,
227
252
  borderTop: (block.borders as Record<string, unknown>)?.top ?? null,
228
253
  borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
@@ -231,14 +256,38 @@ function buildParagraph(
231
256
  outlineLevel: block.outlineLevel ?? null,
232
257
  bidi: block.bidi ?? null,
233
258
  pageBreakBefore: block.pageBreakBefore ?? null,
259
+ hiddenTextOnly: fullyVanishedParagraph || null,
234
260
  },
235
261
  content.length > 0 ? Fragment.from(content) : undefined,
236
262
  );
237
263
  }
238
264
 
265
+ function resolveParagraphLayout(
266
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
267
+ ): {
268
+ spacing: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["spacing"];
269
+ indentation: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"];
270
+ tabStops: Array<{ pos: number; val?: string; leader?: string }>;
271
+ markerLane: NonNullable<
272
+ NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["resolvedNumbering"]>["geometry"]["markerLane"]
273
+ > | undefined;
274
+ markerJustification: NonNullable<
275
+ NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["resolvedNumbering"]>["geometry"]["markerJustification"]
276
+ > | undefined;
277
+ } {
278
+ return {
279
+ spacing: block.resolvedNumbering?.geometry.spacing ?? block.spacing,
280
+ indentation: block.resolvedNumbering?.geometry.indentation ?? block.indentation,
281
+ tabStops: block.resolvedNumbering?.geometry.tabStops ?? block.tabStops ?? [],
282
+ markerLane: block.resolvedNumbering?.geometry.markerLane,
283
+ markerJustification: block.resolvedNumbering?.geometry.markerJustification,
284
+ };
285
+ }
286
+
239
287
  function buildInlineContent(
240
288
  segment: SurfaceInlineSegment,
241
289
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
290
+ showUnsupportedObjectPreviews: boolean,
242
291
  ): PMNode[] {
243
292
  switch (segment.kind) {
244
293
  case "text": {
@@ -324,7 +373,7 @@ function buildInlineContent(
324
373
  }
325
374
 
326
375
  case "opaque_inline":
327
- return [buildOpaqueInlineOrComplexAtom(segment)];
376
+ return [buildOpaqueInlineOrComplexAtom(segment, showUnsupportedObjectPreviews)];
328
377
 
329
378
  case "note_ref": {
330
379
  const text = editorSchema.text(
@@ -353,18 +402,26 @@ function buildInlineContent(
353
402
  function buildTable(
354
403
  block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
355
404
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
405
+ showUnsupportedObjectPreviews: boolean,
356
406
  ): PMNode {
357
407
  const rows: PMNode[] = [];
358
408
  for (const row of block.rows) {
359
409
  const cells: PMNode[] = [];
360
410
  for (const cell of row.cells) {
361
- const cellContent = buildPMBlocks(cell.content, mediaPreviews);
411
+ const cellContent = buildPMBlocks(
412
+ cell.content,
413
+ mediaPreviews,
414
+ showUnsupportedObjectPreviews,
415
+ );
362
416
  // Ensure at least one paragraph in cell (PM requires non-empty)
363
417
  if (cellContent.length === 0) {
364
418
  cellContent.push(editorSchema.nodes.paragraph.create());
365
419
  }
420
+ const cellNodeType = row.isHeader
421
+ ? editorSchema.nodes.table_header_cell
422
+ : editorSchema.nodes.table_cell;
366
423
  cells.push(
367
- editorSchema.nodes.table_cell.create(
424
+ cellNodeType.create(
368
425
  {
369
426
  colspan: cell.colspan,
370
427
  rowspan: cell.rowspan,
@@ -411,8 +468,13 @@ function buildTable(
411
468
  function buildSdtBlock(
412
469
  block: Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>,
413
470
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
471
+ showUnsupportedObjectPreviews: boolean,
414
472
  ): PMNode {
415
- const children = buildPMBlocks(block.children, mediaPreviews);
473
+ const children = buildPMBlocks(
474
+ block.children,
475
+ mediaPreviews,
476
+ showUnsupportedObjectPreviews,
477
+ );
416
478
 
417
479
  if (children.length === 0) {
418
480
  children.push(editorSchema.nodes.paragraph.create());
@@ -441,17 +503,18 @@ function buildSdtBlock(
441
503
  */
442
504
  function buildOpaqueInlineOrComplexAtom(
443
505
  segment: Extract<import("../../api/public-types").SurfaceInlineSegment, { kind: "opaque_inline" }>,
506
+ showUnsupportedObjectPreviews: boolean,
444
507
  ): PMNode {
445
508
  const label = segment.label;
446
509
  const detail = segment.detail;
447
510
 
448
- if (label === "Embedded chart") {
511
+ if (showUnsupportedObjectPreviews && label === "Embedded chart") {
449
512
  return editorSchema.nodes.chart_atom.create({ detail });
450
513
  }
451
- if (label === "SmartArt diagram") {
514
+ if (showUnsupportedObjectPreviews && label === "SmartArt diagram") {
452
515
  return editorSchema.nodes.smartart_atom.create({ detail });
453
516
  }
454
- if (label === "Drawing shape" || label === "Text box") {
517
+ if (showUnsupportedObjectPreviews && (label === "Drawing shape" || label === "Text box")) {
455
518
  const textMatch = /(?:Text content|Content): "([^"]+)"/.exec(detail);
456
519
  const geometryMatch = /Geometry: ([^.]+)\./.exec(detail);
457
520
  return editorSchema.nodes.shape_atom.create({
@@ -460,7 +523,7 @@ function buildOpaqueInlineOrComplexAtom(
460
523
  detail,
461
524
  });
462
525
  }
463
- if (label === "WordArt") {
526
+ if (showUnsupportedObjectPreviews && label === "WordArt") {
464
527
  const textMatch = /Text: "([^"]+)"/.exec(detail);
465
528
  const effectMatch = /Effect: ([^.]+)\./.exec(detail);
466
529
  return editorSchema.nodes.wordart_atom.create({
@@ -469,7 +532,7 @@ function buildOpaqueInlineOrComplexAtom(
469
532
  detail,
470
533
  });
471
534
  }
472
- if (label === "Legacy VML drawing") {
535
+ if (showUnsupportedObjectPreviews && label === "Legacy VML drawing") {
473
536
  const textMatch = /Text content: "([^"]+)"/.exec(detail);
474
537
  const typeMatch = /Type: ([^.]+)\./.exec(detail);
475
538
  return editorSchema.nodes.vml_atom.create({
@@ -21,6 +21,7 @@ export function createSurfaceDocumentBuildKey(input: {
21
21
  surface: EditorSurfaceSnapshot | null | undefined;
22
22
  activeStory: EditorStoryTarget;
23
23
  mediaPreviewKey: string;
24
+ showUnsupportedObjectPreviews?: boolean;
24
25
  }): string {
25
26
  return JSON.stringify({
26
27
  surfaceIdentity:
@@ -29,6 +30,7 @@ export function createSurfaceDocumentBuildKey(input: {
29
30
  : getSurfaceIdentity(input.surface),
30
31
  activeStory: input.activeStory,
31
32
  mediaPreviewKey: input.mediaPreviewKey,
33
+ showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ?? true,
32
34
  });
33
35
  }
34
36
 
@@ -41,6 +43,7 @@ export function createSurfaceDecorationKey(input: {
41
43
  workflowScopeSignature?: string;
42
44
  workflowCandidateSignature?: string;
43
45
  workflowBlockedSignature?: string;
46
+ workflowMetadataSignature?: string;
44
47
  activeWorkflowWorkItemId?: string | null;
45
48
  activeWorkflowScopeIds?: readonly string[];
46
49
  suggestionsEnabled?: boolean;
@@ -54,6 +57,7 @@ export function createSurfaceDecorationKey(input: {
54
57
  workflowScopeSignature: input.workflowScopeSignature ?? null,
55
58
  workflowCandidateSignature: input.workflowCandidateSignature ?? null,
56
59
  workflowBlockedSignature: input.workflowBlockedSignature ?? null,
60
+ workflowMetadataSignature: input.workflowMetadataSignature ?? null,
57
61
  activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
58
62
  activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
59
63
  suggestionsEnabled: input.suggestionsEnabled ?? false,
@@ -18,6 +18,7 @@ import type {
18
18
  SelectionSnapshot,
19
19
  WorkflowBlockedCommandReason,
20
20
  WorkflowCandidateRange,
21
+ WorkflowMetadataMarkup,
21
22
  WorkflowScope,
22
23
  } from "../../api/public-types";
23
24
  import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
@@ -35,6 +36,7 @@ import {
35
36
  createPMSelectionFromSnapshot,
36
37
  createPMStateFromSnapshot,
37
38
  } from "./pm-state-from-snapshot";
39
+ import type { ActiveSelectionToolModel } from "../../ui/headless/selection-tool-types";
38
40
  import {
39
41
  createCommandBridgePlugins,
40
42
  type CommandBridgeCallbacks,
@@ -73,9 +75,10 @@ export interface TwProseMirrorSurfaceProps {
73
75
  documentNavigation: DocumentNavigationSnapshot;
74
76
  reviewMode: "editing" | "review";
75
77
  markupDisplay: MarkupDisplay;
78
+ showUnsupportedObjectPreviews?: boolean;
76
79
  activeRevisionId?: string;
80
+ activeSelectionToolKind?: ActiveSelectionToolModel["kind"] | null;
77
81
  showTrackedChanges?: boolean;
78
- suggestionsEnabled?: boolean;
79
82
  /** When true, the surface renders inside the page workspace (vs canvas). */
80
83
  isPageWorkspace?: boolean;
81
84
  onFocus: FocusEventHandler<HTMLDivElement>;
@@ -100,6 +103,7 @@ export interface TwProseMirrorSurfaceProps {
100
103
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
101
104
  activeWorkflowWorkItemId?: string | null;
102
105
  activeWorkflowScopeIds?: readonly string[];
106
+ workflowMetadata?: readonly WorkflowMetadataMarkup[];
103
107
  }
104
108
 
105
109
  export interface TwProseMirrorSurfaceRef {
@@ -202,10 +206,10 @@ export const TwProseMirrorSurface = forwardRef<
202
206
  surface,
203
207
  activeStory: snapshot.activeStory,
204
208
  mediaPreviewKey,
209
+ showUnsupportedObjectPreviews: props.showUnsupportedObjectPreviews,
205
210
  }),
206
- [mediaPreviewKey, snapshot.activeStory, surface],
211
+ [mediaPreviewKey, props.showUnsupportedObjectPreviews, snapshot.activeStory, surface],
207
212
  );
208
- const suggestionsEnabled = props.suggestionsEnabled === true;
209
213
  const decorationBuildKey = useMemo(
210
214
  () =>
211
215
  createSurfaceDecorationKey({
@@ -217,9 +221,9 @@ export const TwProseMirrorSurface = forwardRef<
217
221
  workflowScopeSignature: createWorkflowScopeSignature(props.workflowScopes),
218
222
  workflowCandidateSignature: createWorkflowCandidateSignature(props.workflowCandidates),
219
223
  workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
224
+ workflowMetadataSignature: createWorkflowMetadataSignature(props.workflowMetadata),
220
225
  activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
221
226
  activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
222
- suggestionsEnabled,
223
227
  }),
224
228
  [
225
229
  canEdit,
@@ -227,12 +231,12 @@ export const TwProseMirrorSurface = forwardRef<
227
231
  props.activeRevisionId,
228
232
  props.workflowCandidates,
229
233
  props.workflowBlockedReasons,
234
+ props.workflowMetadata,
230
235
  props.activeWorkflowWorkItemId,
231
236
  props.activeWorkflowScopeIds,
232
237
  props.workflowScopes,
233
238
  showTrackedChanges,
234
239
  snapshot.comments.activeCommentId,
235
- suggestionsEnabled,
236
240
  ],
237
241
  );
238
242
 
@@ -278,7 +282,7 @@ export const TwProseMirrorSurface = forwardRef<
278
282
  props.workflowBlockedReasons,
279
283
  props.activeWorkflowWorkItemId,
280
284
  props.activeWorkflowScopeIds,
281
- suggestionsEnabled,
285
+ props.workflowMetadata,
282
286
  );
283
287
  view.setProps({
284
288
  editable: () => canEdit,
@@ -296,6 +300,7 @@ export const TwProseMirrorSurface = forwardRef<
296
300
  props.activeWorkflowScopeIds,
297
301
  props.activeWorkflowWorkItemId,
298
302
  props.workflowBlockedReasons,
303
+ props.workflowMetadata,
299
304
  props.workflowCandidates,
300
305
  props.workflowScopes,
301
306
  revisionModel,
@@ -316,6 +321,7 @@ export const TwProseMirrorSurface = forwardRef<
316
321
  snapshot.selection,
317
322
  plugins,
318
323
  props.mediaPreviews,
324
+ props.showUnsupportedObjectPreviews,
319
325
  );
320
326
  positionMapRef.current = positionMap;
321
327
  const decorations = buildDecorations(
@@ -331,6 +337,7 @@ export const TwProseMirrorSurface = forwardRef<
331
337
  props.workflowBlockedReasons,
332
338
  props.activeWorkflowWorkItemId,
333
339
  props.activeWorkflowScopeIds,
340
+ props.workflowMetadata,
334
341
  );
335
342
  recordPerfSample("pm.rebuild");
336
343
  incrementInvalidationCounter("pm.laneA.rebuilds");
@@ -598,8 +605,13 @@ export const TwProseMirrorSurface = forwardRef<
598
605
  const workspaceLabel = props.isPageWorkspace ? "Document page" : "Document canvas";
599
606
 
600
607
  const selectionToolbarMeasurementKey = useMemo(
601
- () => buildSelectionToolbarMeasurementKey(snapshot.selection, snapshot.activeStory),
602
- [snapshot.activeStory, snapshot.selection],
608
+ () =>
609
+ buildSelectionToolbarMeasurementKey(
610
+ snapshot.selection,
611
+ snapshot.activeStory,
612
+ props.activeSelectionToolKind,
613
+ ),
614
+ [props.activeSelectionToolKind, snapshot.activeStory, snapshot.selection],
603
615
  );
604
616
 
605
617
  const emitSelectionToolbarAnchor = useCallback((): void => {
@@ -652,6 +664,7 @@ export const TwProseMirrorSurface = forwardRef<
652
664
  useEffect(() => {
653
665
  scheduleSelectionToolbarAnchorUpdate();
654
666
  }, [
667
+ props.activeSelectionToolKind,
655
668
  scheduleSelectionToolbarAnchorUpdate,
656
669
  snapshot.revisionToken,
657
670
  snapshot.selection,
@@ -692,6 +705,7 @@ export const TwProseMirrorSurface = forwardRef<
692
705
  resizeObserver?.disconnect();
693
706
  };
694
707
  }, [
708
+ props.activeSelectionToolKind,
695
709
  props.onSelectionToolbarAnchorChange,
696
710
  scheduleSelectionToolbarAnchorUpdate,
697
711
  snapshot.revisionToken,
@@ -793,7 +807,7 @@ export const TwProseMirrorSurface = forwardRef<
793
807
  const positionMap = positionMapRef.current;
794
808
  const range = snapshot.selection.activeRange;
795
809
 
796
- if (!callback || !view || !mount || !positionMap || snapshot.selection.isCollapsed || range.kind !== "range") {
810
+ if (!callback || !view || !mount || !positionMap || range.kind === "detached") {
797
811
  return null;
798
812
  }
799
813
 
@@ -803,29 +817,33 @@ export const TwProseMirrorSurface = forwardRef<
803
817
  }
804
818
 
805
819
  try {
806
- const pmFrom = positionMap.runtimeToPm(range.from);
807
- const pmTo = positionMap.runtimeToPm(range.to);
808
- const startRect = view.coordsAtPos(pmFrom);
809
- const endRect = view.coordsAtPos(pmTo);
810
- const left = Math.max(rootRect.left, Math.min(startRect.left, endRect.left));
811
- const right = Math.min(rootRect.right, Math.max(startRect.right, endRect.right));
812
- const top = Math.max(rootRect.top, Math.min(startRect.top, endRect.top));
813
- const bottom = Math.min(rootRect.bottom, Math.max(startRect.bottom, endRect.bottom));
820
+ if (!snapshot.selection.isCollapsed && range.kind === "range") {
821
+ const pmFrom = positionMap.runtimeToPm(range.from);
822
+ const pmTo = positionMap.runtimeToPm(range.to);
823
+ return createSelectionToolbarAnchor(rootRect, view.coordsAtPos(pmFrom), view.coordsAtPos(pmTo));
824
+ }
814
825
 
815
826
  if (
816
- !Number.isFinite(left) ||
817
- !Number.isFinite(right) ||
818
- !Number.isFinite(top) ||
819
- !Number.isFinite(bottom) ||
820
- right <= left ||
821
- bottom <= top ||
822
- bottom < rootRect.top ||
823
- top > rootRect.bottom
827
+ range.kind === "node" ||
828
+ (
829
+ snapshot.selection.isCollapsed &&
830
+ range.kind === "range" &&
831
+ (
832
+ props.activeSelectionToolKind === "comment-thread" ||
833
+ props.activeSelectionToolKind === "structure-context"
834
+ )
835
+ )
824
836
  ) {
825
- return null;
837
+ const runtimePosition = range.kind === "node" ? range.at : range.from;
838
+ const pmAt = positionMap.runtimeToPm(runtimePosition);
839
+ return createSelectionToolbarAnchor(
840
+ rootRect,
841
+ view.coordsAtPos(pmAt, -1),
842
+ view.coordsAtPos(pmAt, 1),
843
+ );
826
844
  }
827
845
 
828
- return { left, right, top, bottom };
846
+ return null;
829
847
  } catch {
830
848
  return null;
831
849
  }
@@ -880,7 +898,33 @@ function createWorkflowBlockedSignature(
880
898
  ).join("|");
881
899
  }
882
900
 
883
- function serializeAnchorSignature(anchor: WorkflowScope["anchor"] | WorkflowCandidateRange["anchor"] | WorkflowBlockedCommandReason["anchor"] | undefined): string {
901
+ function createWorkflowMetadataSignature(
902
+ metadata: readonly WorkflowMetadataMarkup[] | undefined,
903
+ ): string {
904
+ if (!metadata || metadata.length === 0) {
905
+ return "";
906
+ }
907
+ return metadata.map((entry) =>
908
+ [
909
+ entry.entryId,
910
+ entry.metadataId,
911
+ entry.color ?? "",
912
+ entry.persistence,
913
+ serializeAnchorSignature(entry.anchor),
914
+ serializeStoryTargetSignature(entry.storyTarget),
915
+ JSON.stringify(entry.value ?? {}),
916
+ ].join(":")
917
+ ).join("|");
918
+ }
919
+
920
+ function serializeAnchorSignature(
921
+ anchor:
922
+ | WorkflowScope["anchor"]
923
+ | WorkflowCandidateRange["anchor"]
924
+ | WorkflowBlockedCommandReason["anchor"]
925
+ | WorkflowMetadataMarkup["anchor"]
926
+ | undefined,
927
+ ): string {
884
928
  if (!anchor) {
885
929
  return "";
886
930
  }
@@ -894,7 +938,13 @@ function serializeAnchorSignature(anchor: WorkflowScope["anchor"] | WorkflowCand
894
938
  }
895
939
  }
896
940
 
897
- function serializeStoryTargetSignature(storyTarget: WorkflowScope["storyTarget"] | WorkflowCandidateRange["storyTarget"] | WorkflowBlockedCommandReason["storyTarget"]): string {
941
+ function serializeStoryTargetSignature(
942
+ storyTarget:
943
+ | WorkflowScope["storyTarget"]
944
+ | WorkflowCandidateRange["storyTarget"]
945
+ | WorkflowBlockedCommandReason["storyTarget"]
946
+ | WorkflowMetadataMarkup["storyTarget"],
947
+ ): string {
898
948
  if (!storyTarget) {
899
949
  return "";
900
950
  }
@@ -913,18 +963,71 @@ function serializeStoryTargetSignature(storyTarget: WorkflowScope["storyTarget"]
913
963
  function buildSelectionToolbarMeasurementKey(
914
964
  selection: SelectionSnapshot,
915
965
  activeStory: RuntimeRenderSnapshot["activeStory"],
966
+ activeSelectionToolKind?: ActiveSelectionToolModel["kind"] | null,
916
967
  ): string | null {
917
- if (selection.isCollapsed || selection.activeRange.kind !== "range") {
968
+ if (!activeSelectionToolKind || selection.activeRange.kind === "detached") {
918
969
  return null;
919
970
  }
920
971
 
921
972
  return JSON.stringify({
922
973
  story: activeStory,
923
- from: selection.activeRange.from,
924
- to: selection.activeRange.to,
974
+ tool: activeSelectionToolKind,
975
+ ...(selection.activeRange.kind === "node"
976
+ ? { nodeAt: selection.activeRange.at }
977
+ : {
978
+ from: selection.activeRange.from,
979
+ to: selection.activeRange.to,
980
+ collapsed: selection.isCollapsed,
981
+ }),
925
982
  });
926
983
  }
927
984
 
985
+ function createSelectionToolbarAnchor(
986
+ rootRect: DOMRect,
987
+ ...rects: Array<Pick<DOMRect, "left" | "right" | "top" | "bottom">>
988
+ ): SelectionToolbarAnchor | null {
989
+ const validRects = rects.filter((rect) =>
990
+ Number.isFinite(rect.left) &&
991
+ Number.isFinite(rect.right) &&
992
+ Number.isFinite(rect.top) &&
993
+ Number.isFinite(rect.bottom),
994
+ );
995
+ if (validRects.length === 0) {
996
+ return null;
997
+ }
998
+
999
+ let left = Math.max(rootRect.left, Math.min(...validRects.map((rect) => Math.min(rect.left, rect.right))));
1000
+ let right = Math.min(rootRect.right, Math.max(...validRects.map((rect) => Math.max(rect.left, rect.right))));
1001
+ let top = Math.max(rootRect.top, Math.min(...validRects.map((rect) => Math.min(rect.top, rect.bottom))));
1002
+ let bottom = Math.min(rootRect.bottom, Math.max(...validRects.map((rect) => Math.max(rect.top, rect.bottom))));
1003
+
1004
+ if (right <= left) {
1005
+ const centerX = Math.min(rootRect.right, Math.max(rootRect.left, (left + right) / 2 || left));
1006
+ left = Math.max(rootRect.left, centerX - 1);
1007
+ right = Math.min(rootRect.right, centerX + 1);
1008
+ }
1009
+ if (bottom <= top) {
1010
+ const centerY = Math.min(rootRect.bottom, Math.max(rootRect.top, (top + bottom) / 2 || top));
1011
+ top = Math.max(rootRect.top, centerY - 1);
1012
+ bottom = Math.min(rootRect.bottom, centerY + 1);
1013
+ }
1014
+
1015
+ if (
1016
+ !Number.isFinite(left) ||
1017
+ !Number.isFinite(right) ||
1018
+ !Number.isFinite(top) ||
1019
+ !Number.isFinite(bottom) ||
1020
+ right <= left ||
1021
+ bottom <= top ||
1022
+ bottom < rootRect.top ||
1023
+ top > rootRect.bottom
1024
+ ) {
1025
+ return null;
1026
+ }
1027
+
1028
+ return { left, right, top, bottom };
1029
+ }
1030
+
928
1031
  function selectionToolbarAnchorsEqual(
929
1032
  left: SelectionToolbarAnchor | null,
930
1033
  right: SelectionToolbarAnchor | null,