@beyondwork/docx-react-component 1.0.18 → 1.0.20

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 (105) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +710 -4
  5. package/src/api/session-state.ts +60 -0
  6. package/src/core/commands/formatting-commands.ts +2 -1
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +19 -3
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +357 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +4 -1
  16. package/src/index.ts +51 -0
  17. package/src/io/docx-session.ts +623 -56
  18. package/src/io/export/serialize-comments.ts +104 -34
  19. package/src/io/export/serialize-footnotes.ts +198 -1
  20. package/src/io/export/serialize-headers-footers.ts +203 -10
  21. package/src/io/export/serialize-main-document.ts +285 -8
  22. package/src/io/export/serialize-numbering.ts +28 -7
  23. package/src/io/export/split-review-boundaries.ts +181 -19
  24. package/src/io/normalize/normalize-text.ts +144 -32
  25. package/src/io/ooxml/highlight-colors.ts +39 -0
  26. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  27. package/src/io/ooxml/parse-comments.ts +85 -19
  28. package/src/io/ooxml/parse-fields.ts +396 -0
  29. package/src/io/ooxml/parse-footnotes.ts +452 -22
  30. package/src/io/ooxml/parse-headers-footers.ts +657 -29
  31. package/src/io/ooxml/parse-inline-media.ts +30 -0
  32. package/src/io/ooxml/parse-main-document.ts +807 -20
  33. package/src/io/ooxml/parse-numbering.ts +7 -0
  34. package/src/io/ooxml/parse-revisions.ts +317 -38
  35. package/src/io/ooxml/parse-settings.ts +184 -0
  36. package/src/io/ooxml/parse-shapes.ts +25 -0
  37. package/src/io/ooxml/parse-styles.ts +463 -0
  38. package/src/io/ooxml/parse-theme.ts +32 -0
  39. package/src/legal/bookmarks.ts +44 -0
  40. package/src/legal/cross-references.ts +59 -1
  41. package/src/model/canonical-document.ts +250 -4
  42. package/src/model/cds-1.0.0.ts +13 -0
  43. package/src/model/snapshot.ts +87 -2
  44. package/src/review/store/revision-store.ts +6 -0
  45. package/src/review/store/revision-types.ts +1 -0
  46. package/src/runtime/document-layout.ts +332 -0
  47. package/src/runtime/document-navigation.ts +603 -0
  48. package/src/runtime/document-runtime.ts +1754 -78
  49. package/src/runtime/document-search.ts +145 -0
  50. package/src/runtime/numbering-prefix.ts +47 -26
  51. package/src/runtime/page-layout-estimation.ts +212 -0
  52. package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
  53. package/src/runtime/session-capabilities.ts +35 -3
  54. package/src/runtime/story-context.ts +164 -0
  55. package/src/runtime/story-targeting.ts +162 -0
  56. package/src/runtime/surface-projection.ts +324 -36
  57. package/src/runtime/table-schema.ts +89 -7
  58. package/src/runtime/view-state.ts +477 -0
  59. package/src/runtime/workflow-markup.ts +349 -0
  60. package/src/ui/WordReviewEditor.tsx +2469 -1344
  61. package/src/ui/browser-export.ts +52 -0
  62. package/src/ui/editor-command-bag.ts +120 -0
  63. package/src/ui/editor-runtime-boundary.ts +1422 -0
  64. package/src/ui/editor-shell-view.tsx +134 -0
  65. package/src/ui/editor-surface-controller.tsx +51 -0
  66. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  67. package/src/ui/headless/revision-decoration-model.ts +4 -4
  68. package/src/ui/headless/selection-helpers.ts +20 -0
  69. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  70. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  71. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  72. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  73. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  74. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  75. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  76. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  77. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
  78. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  79. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  80. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
  81. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  82. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  83. package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
  84. package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
  85. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
  86. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  87. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  88. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  89. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  90. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
  91. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  92. package/src/ui-tailwind/index.ts +2 -1
  93. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  94. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  95. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  96. package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
  97. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  98. package/src/ui-tailwind/theme/editor-theme.css +127 -0
  99. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  100. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
  101. package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
  102. package/src/validation/compatibility-engine.ts +119 -24
  103. package/src/validation/compatibility-report.ts +1 -0
  104. package/src/validation/diagnostics.ts +1 -0
  105. package/src/validation/docx-comment-proof.ts +707 -0
@@ -1,5 +1,5 @@
1
1
  import { Fragment, type Node as PMNode } from "prosemirror-model";
2
- import { EditorState, type Plugin, Selection, TextSelection } from "prosemirror-state";
2
+ import { EditorState, NodeSelection, type Plugin, Selection, TextSelection } from "prosemirror-state";
3
3
 
4
4
  import type {
5
5
  EditorSurfaceSnapshot,
@@ -17,6 +17,12 @@ export interface PMStateResult {
17
17
  positionMap: PositionMap;
18
18
  }
19
19
 
20
+ export interface MediaPreviewDescriptor {
21
+ src: string;
22
+ widthEmu?: number;
23
+ heightEmu?: number;
24
+ }
25
+
20
26
  /**
21
27
  * Create a ProseMirror EditorState from a runtime surface snapshot.
22
28
  *
@@ -27,30 +33,11 @@ export function createPMStateFromSnapshot(
27
33
  surface: EditorSurfaceSnapshot,
28
34
  selection: SelectionSnapshot,
29
35
  plugins: Plugin[],
36
+ mediaPreviews: Record<string, MediaPreviewDescriptor> = {},
30
37
  ): PMStateResult {
31
- const doc = buildPMDoc(surface);
38
+ const doc = buildPMDoc(surface, mediaPreviews);
32
39
  const positionMap = buildPositionMap(surface);
33
-
34
- // Convert runtime selection to PM selection
35
- const pmAnchor = clamp(
36
- positionMap.runtimeToPm(selection.anchor),
37
- 1,
38
- positionMap.pmDocSize - 1,
39
- );
40
- const pmHead = clamp(
41
- positionMap.runtimeToPm(selection.head),
42
- 1,
43
- positionMap.pmDocSize - 1,
44
- );
45
-
46
- let pmSelection: Selection;
47
- try {
48
- pmSelection = TextSelection.between(doc.resolve(pmAnchor), doc.resolve(pmHead));
49
- } catch {
50
- // If the mapped runtime selection is invalid or lands in a non-text block,
51
- // let ProseMirror choose the nearest valid starting selection.
52
- pmSelection = Selection.atStart(doc);
53
- }
40
+ const pmSelection = createPMSelectionFromSnapshot(doc, positionMap, selection);
54
41
 
55
42
  const state = EditorState.create({
56
43
  doc,
@@ -61,21 +48,70 @@ export function createPMStateFromSnapshot(
61
48
  return { state, positionMap };
62
49
  }
63
50
 
64
- function buildPMDoc(surface: EditorSurfaceSnapshot): PMNode {
65
- const blocks: PMNode[] = [];
51
+ export function createPMSelectionFromSnapshot(
52
+ doc: PMNode,
53
+ positionMap: PositionMap,
54
+ selection: SelectionSnapshot,
55
+ ): Selection {
56
+ const pmAnchor = clamp(positionMap.runtimeToPm(selection.anchor), 1, positionMap.pmDocSize - 1);
57
+ const pmHead = clamp(positionMap.runtimeToPm(selection.head), 1, positionMap.pmDocSize - 1);
58
+ try {
59
+ if (selection.activeRange.kind === "node") {
60
+ const pmNodePos = clamp(pmAnchor - 1, 0, positionMap.pmDocSize - 2);
61
+ return NodeSelection.create(doc, pmNodePos);
62
+ }
66
63
 
67
- for (const block of surface.blocks) {
68
- if (block.kind === "paragraph") {
69
- blocks.push(buildParagraph(block));
70
- } else if (block.kind === "table") {
71
- blocks.push(buildTable(block));
72
- } else if (block.kind === "sdt_block") {
73
- blocks.push(buildSdtBlock(block));
74
- } else {
75
- blocks.push(buildOpaqueBlock(block));
64
+ const forward = selection.head >= selection.anchor;
65
+ const resolvedAnchor = resolveInlineBoundary(doc, pmAnchor, forward ? 1 : -1);
66
+ const resolvedHead = resolveInlineBoundary(doc, pmHead, forward ? -1 : 1);
67
+ if (resolvedAnchor !== null && resolvedHead !== null) {
68
+ return TextSelection.create(doc, resolvedAnchor, resolvedHead);
76
69
  }
70
+
71
+ const $anchor = doc.resolve(pmAnchor);
72
+ const $head = doc.resolve(pmHead);
73
+ return (
74
+ Selection.findFrom($anchor, 1, true) ??
75
+ Selection.findFrom($anchor, -1, true) ??
76
+ Selection.findFrom($head, 1, true) ??
77
+ Selection.findFrom($head, -1, true) ??
78
+ Selection.near($anchor, 1)
79
+ );
80
+ } catch {
81
+ // If the mapped runtime selection is invalid or lands in a non-text block,
82
+ // let ProseMirror choose the nearest valid starting selection.
83
+ return Selection.atStart(doc);
84
+ }
85
+ }
86
+
87
+ function resolveInlineBoundary(
88
+ doc: PMNode,
89
+ pos: number,
90
+ bias: -1 | 1,
91
+ ): number | null {
92
+ const $pos = doc.resolve(pos);
93
+ if ($pos.parent.inlineContent) {
94
+ return pos;
77
95
  }
78
96
 
97
+ const candidate =
98
+ Selection.findFrom($pos, bias, true) ??
99
+ Selection.findFrom($pos, -bias, true) ??
100
+ Selection.near($pos, bias);
101
+
102
+ if (candidate.$from.parent.inlineContent || candidate.$to.parent.inlineContent) {
103
+ return bias < 0 ? candidate.to : candidate.from;
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ function buildPMDoc(
110
+ surface: EditorSurfaceSnapshot,
111
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
112
+ ): PMNode {
113
+ const blocks = buildPMBlocks(surface.blocks, mediaPreviews);
114
+
79
115
  // Ensure at least one block (PM requires non-empty doc)
80
116
  if (blocks.length === 0) {
81
117
  blocks.push(editorSchema.nodes.paragraph.create());
@@ -84,8 +120,38 @@ function buildPMDoc(surface: EditorSurfaceSnapshot): PMNode {
84
120
  return editorSchema.nodes.doc.create(null, Fragment.from(blocks));
85
121
  }
86
122
 
123
+ function buildPMBlocks(
124
+ blocks: SurfaceBlockSnapshot[],
125
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
126
+ ): PMNode[] {
127
+ const nodes: PMNode[] = [];
128
+
129
+ for (let index = 0; index < blocks.length; index += 1) {
130
+ const block = blocks[index];
131
+ const previousBlock = index > 0 ? blocks[index - 1] : null;
132
+ const nextBlock = blocks[index + 1];
133
+ const previousParagraph = previousBlock?.kind === "paragraph" ? previousBlock : null;
134
+ const nextParagraph = nextBlock?.kind === "paragraph" ? nextBlock : null;
135
+
136
+ if (block.kind === "paragraph") {
137
+ nodes.push(buildParagraph(block, previousParagraph, nextParagraph, mediaPreviews));
138
+ } else if (block.kind === "table") {
139
+ nodes.push(buildTable(block, mediaPreviews));
140
+ } else if (block.kind === "sdt_block") {
141
+ nodes.push(buildSdtBlock(block, mediaPreviews));
142
+ } else {
143
+ nodes.push(buildOpaqueBlock(block));
144
+ }
145
+ }
146
+
147
+ return nodes;
148
+ }
149
+
87
150
  function buildParagraph(
88
151
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
152
+ previousParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
153
+ nextParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
154
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
89
155
  ): PMNode {
90
156
  const content: PMNode[] = [];
91
157
  const tabStops = block.tabStops ?? [];
@@ -111,11 +177,28 @@ function buildParagraph(
111
177
  );
112
178
  tabIndex++;
113
179
  } else {
114
- const nodes = buildInlineContent(segment);
180
+ const nodes = buildInlineContent(segment, mediaPreviews);
115
181
  content.push(...nodes);
116
182
  }
117
183
  }
118
184
 
185
+ const listContinuation = Boolean(
186
+ block.numbering &&
187
+ nextParagraph?.numbering &&
188
+ nextParagraph.numbering.numberingInstanceId === block.numbering.numberingInstanceId &&
189
+ nextParagraph.numbering.level === block.numbering.level,
190
+ );
191
+ const contextualSpacingAfter = Boolean(
192
+ block.contextualSpacing &&
193
+ nextParagraph?.contextualSpacing &&
194
+ (nextParagraph.styleId ?? "__default__") === (block.styleId ?? "__default__"),
195
+ );
196
+ const contextualSpacingBefore = Boolean(
197
+ block.contextualSpacing &&
198
+ previousParagraph?.contextualSpacing &&
199
+ (previousParagraph.styleId ?? "__default__") === (block.styleId ?? "__default__"),
200
+ );
201
+
119
202
  return editorSchema.nodes.paragraph.create(
120
203
  {
121
204
  styleId: block.styleId ?? null,
@@ -124,14 +207,22 @@ function buildParagraph(
124
207
  numberingPrefix:
125
208
  (block as typeof block & { numberingPrefix?: string }).numberingPrefix ??
126
209
  null,
210
+ numberingSuffix:
211
+ (block as typeof block & { numberingSuffix?: string }).numberingSuffix ??
212
+ null,
127
213
  alignment: block.alignment ?? null,
128
214
  spacingBefore: block.spacing?.before ?? null,
129
215
  spacingAfter: block.spacing?.after ?? null,
130
216
  lineSpacing: block.spacing?.line ?? null,
131
217
  lineRule: block.spacing?.lineRule ?? null,
218
+ contextualSpacing: block.contextualSpacing ?? null,
219
+ listContinuation: listContinuation || null,
220
+ contextualSpacingBefore: contextualSpacingBefore || null,
221
+ contextualSpacingAfter: contextualSpacingAfter || null,
132
222
  indentLeft: block.indentation?.left ?? null,
133
223
  indentRight: block.indentation?.right ?? null,
134
224
  indentFirstLine: block.indentation?.firstLine ?? null,
225
+ indentHanging: block.indentation?.hanging ?? null,
135
226
  shadingFill: block.shading?.fill ?? null,
136
227
  borderTop: (block.borders as Record<string, unknown>)?.top ?? null,
137
228
  borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
@@ -145,7 +236,10 @@ function buildParagraph(
145
236
  );
146
237
  }
147
238
 
148
- function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
239
+ function buildInlineContent(
240
+ segment: SurfaceInlineSegment,
241
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
242
+ ): PMNode[] {
149
243
  switch (segment.kind) {
150
244
  case "text": {
151
245
  if (!segment.text) return [];
@@ -213,6 +307,8 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
213
307
  return [editorSchema.nodes.tab_char.create()];
214
308
 
215
309
  case "image":
310
+ {
311
+ const preview = mediaPreviews[segment.mediaId];
216
312
  return [
217
313
  editorSchema.nodes.image_atom.create({
218
314
  mediaId: segment.mediaId,
@@ -220,12 +316,35 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
220
316
  state: segment.state,
221
317
  display: segment.display ?? "inline",
222
318
  detail: segment.detail ?? null,
319
+ src: preview?.src ?? null,
320
+ widthEmu: preview?.widthEmu ?? null,
321
+ heightEmu: preview?.heightEmu ?? null,
223
322
  }),
224
323
  ];
324
+ }
225
325
 
226
326
  case "opaque_inline":
227
327
  return [buildOpaqueInlineOrComplexAtom(segment)];
228
328
 
329
+ case "note_ref": {
330
+ const text = editorSchema.text(
331
+ segment.label,
332
+ [editorSchema.marks.superscript.create()],
333
+ );
334
+ return [text];
335
+ }
336
+
337
+ case "field_ref":
338
+ return [
339
+ editorSchema.nodes.field_ref_atom.create({
340
+ fieldFamily: segment.fieldFamily,
341
+ fieldTarget: segment.fieldTarget ?? null,
342
+ instruction: segment.instruction,
343
+ refreshStatus: segment.refreshStatus,
344
+ label: segment.label,
345
+ }),
346
+ ];
347
+
229
348
  default:
230
349
  return [];
231
350
  }
@@ -233,23 +352,13 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
233
352
 
234
353
  function buildTable(
235
354
  block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
355
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
236
356
  ): PMNode {
237
357
  const rows: PMNode[] = [];
238
358
  for (const row of block.rows) {
239
359
  const cells: PMNode[] = [];
240
360
  for (const cell of row.cells) {
241
- const cellContent: PMNode[] = [];
242
- for (const child of cell.content) {
243
- if (child.kind === "paragraph") {
244
- cellContent.push(buildParagraph(child as Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>));
245
- } else if (child.kind === "table") {
246
- cellContent.push(buildTable(child as Extract<SurfaceBlockSnapshot, { kind: "table" }>));
247
- } else if (child.kind === "sdt_block") {
248
- cellContent.push(buildSdtBlock(child as Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>));
249
- } else if (child.kind === "opaque_block") {
250
- cellContent.push(buildOpaqueBlock(child as Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>));
251
- }
252
- }
361
+ const cellContent = buildPMBlocks(cell.content, mediaPreviews);
253
362
  // Ensure at least one paragraph in cell (PM requires non-empty)
254
363
  if (cellContent.length === 0) {
255
364
  cellContent.push(editorSchema.nodes.paragraph.create());
@@ -262,17 +371,36 @@ function buildTable(
262
371
  gridSpan: cell.gridSpan,
263
372
  verticalMerge: cell.verticalMerge,
264
373
  backgroundColor: cell.backgroundColor ?? null,
374
+ verticalAlign: cell.verticalAlign ?? null,
375
+ borderTop: cell.borderTop ?? null,
376
+ borderRight: cell.borderRight ?? null,
377
+ borderBottom: cell.borderBottom ?? null,
378
+ borderLeft: cell.borderLeft ?? null,
265
379
  },
266
380
  Fragment.from(cellContent),
267
381
  ),
268
382
  );
269
383
  }
270
- rows.push(editorSchema.nodes.table_row.create(null, Fragment.from(cells)));
384
+ rows.push(editorSchema.nodes.table_row.create(
385
+ {
386
+ height: row.height ?? null,
387
+ heightRule: row.heightRule ?? null,
388
+ isHeader: row.isHeader ?? false,
389
+ },
390
+ Fragment.from(cells),
391
+ ));
271
392
  }
272
393
  return editorSchema.nodes.table.create(
273
394
  {
274
395
  styleId: block.styleId ?? null,
275
396
  gridColumns: block.gridColumns,
397
+ alignment: block.alignment ?? null,
398
+ tblLookFirstRow: block.tblLook?.firstRow ?? false,
399
+ tblLookLastRow: block.tblLook?.lastRow ?? false,
400
+ tblLookFirstColumn: block.tblLook?.firstColumn ?? false,
401
+ tblLookLastColumn: block.tblLook?.lastColumn ?? false,
402
+ tblLookNoHBand: block.tblLook?.noHBand ?? false,
403
+ tblLookNoVBand: block.tblLook?.noVBand ?? false,
276
404
  },
277
405
  Fragment.from(rows),
278
406
  );
@@ -280,19 +408,9 @@ function buildTable(
280
408
 
281
409
  function buildSdtBlock(
282
410
  block: Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>,
411
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
283
412
  ): PMNode {
284
- const children = block.children.map((child) => {
285
- if (child.kind === "paragraph") {
286
- return buildParagraph(child);
287
- }
288
- if (child.kind === "table") {
289
- return buildTable(child);
290
- }
291
- if (child.kind === "sdt_block") {
292
- return buildSdtBlock(child);
293
- }
294
- return buildOpaqueBlock(child);
295
- });
413
+ const children = buildPMBlocks(block.children, mediaPreviews);
296
414
 
297
415
  if (children.length === 0) {
298
416
  children.push(editorSchema.nodes.paragraph.create());
@@ -304,6 +422,11 @@ function buildSdtBlock(
304
422
  alias: block.alias ?? null,
305
423
  tag: block.tag ?? null,
306
424
  lock: block.lock ?? null,
425
+ checkboxChecked: block.checkboxChecked ?? null,
426
+ dateValue: block.dateValue ?? null,
427
+ dropdownItems: block.dropdownItems ?? null,
428
+ comboBoxItems: block.comboBoxItems ?? null,
429
+ showingPlcHdr: block.showingPlcHdr ?? false,
307
430
  },
308
431
  Fragment.from(children),
309
432
  );
@@ -320,15 +443,14 @@ function buildOpaqueInlineOrComplexAtom(
320
443
  const label = segment.label;
321
444
  const detail = segment.detail;
322
445
 
323
- if (label === "Chart") {
446
+ if (label === "Embedded chart") {
324
447
  return editorSchema.nodes.chart_atom.create({ detail });
325
448
  }
326
- if (label === "SmartArt") {
449
+ if (label === "SmartArt diagram") {
327
450
  return editorSchema.nodes.smartart_atom.create({ detail });
328
451
  }
329
- if (label === "Shape") {
330
- // Extract text hint from detail if present
331
- const textMatch = /Text: "([^"]+)"/.exec(detail);
452
+ if (label === "Drawing shape" || label === "Text box") {
453
+ const textMatch = /(?:Text content|Content): "([^"]+)"/.exec(detail);
332
454
  const geometryMatch = /Geometry: ([^.]+)\./.exec(detail);
333
455
  return editorSchema.nodes.shape_atom.create({
334
456
  text: textMatch ? textMatch[1] : null,
@@ -345,8 +467,8 @@ function buildOpaqueInlineOrComplexAtom(
345
467
  detail,
346
468
  });
347
469
  }
348
- if (label === "VML shape") {
349
- const textMatch = /Text: "([^"]+)"/.exec(detail);
470
+ if (label === "Legacy VML drawing") {
471
+ const textMatch = /Text content: "([^"]+)"/.exec(detail);
350
472
  const typeMatch = /Type: ([^.]+)\./.exec(detail);
351
473
  return editorSchema.nodes.vml_atom.create({
352
474
  text: textMatch ? textMatch[1] : null,
@@ -360,6 +482,7 @@ function buildOpaqueInlineOrComplexAtom(
360
482
  warningId: segment.warningId,
361
483
  label,
362
484
  detail,
485
+ presentation: segment.presentation ?? "inline-chip",
363
486
  });
364
487
  }
365
488
 
@@ -15,7 +15,17 @@ import { Plugin, PluginKey } from "prosemirror-state";
15
15
  import type { EditorState, Transaction } from "prosemirror-state";
16
16
  import { Decoration, DecorationSet } from "prosemirror-view";
17
17
 
18
- import type { SearchOptions as PublicSearchOptions } from "../../api/public-types";
18
+ import type {
19
+ SearchOptions as PublicSearchOptions,
20
+ } from "../../api/public-types";
21
+ import {
22
+ buildSearchPattern,
23
+ createSearchExcerpt,
24
+ findSearchMatches,
25
+ searchSecondaryStories,
26
+ type SecondaryStorySearchResult,
27
+ type SearchTextOptions,
28
+ } from "../../core/search/search-text.ts";
19
29
 
20
30
  // ---------------------------------------------------------------------------
21
31
  // Public types
@@ -28,8 +38,7 @@ export interface SearchResult {
28
38
  index: number;
29
39
  }
30
40
 
31
- export interface SearchOptions extends PublicSearchOptions {
32
- caseSensitive?: boolean;
41
+ export interface SearchOptions extends PublicSearchOptions, SearchTextOptions {
33
42
  regex?: boolean;
34
43
  highlightColor?: string;
35
44
  }
@@ -133,71 +142,6 @@ export function performSearch(
133
142
  return results;
134
143
  }
135
144
 
136
- export function findSearchMatches(
137
- text: string,
138
- query: string,
139
- options: SearchOptions = {},
140
- ): SearchResult[] {
141
- const pattern = buildSearchPattern(query, options);
142
- if (!pattern) return [];
143
-
144
- const results: SearchResult[] = [];
145
- let match: RegExpExecArray | null;
146
- pattern.lastIndex = 0;
147
- while ((match = pattern.exec(text)) !== null) {
148
- results.push({
149
- from: match.index,
150
- to: match.index + match[0].length,
151
- text: match[0],
152
- index: results.length,
153
- });
154
-
155
- if (match[0].length === 0) {
156
- pattern.lastIndex += 1;
157
- }
158
- }
159
-
160
- return results;
161
- }
162
-
163
- export function createSearchExcerpt(
164
- text: string,
165
- from: number,
166
- to: number,
167
- radius = 24,
168
- ): string {
169
- const safeFrom = Math.max(0, Math.min(from, text.length));
170
- const safeTo = Math.max(safeFrom, Math.min(to, text.length));
171
- const start = Math.max(0, safeFrom - radius);
172
- const end = Math.min(text.length, safeTo + radius);
173
- const prefix = start > 0 ? "…" : "";
174
- const suffix = end < text.length ? "…" : "";
175
- return `${prefix}${text.slice(start, end)}${suffix}`;
176
- }
177
-
178
- function buildSearchPattern(
179
- query: string,
180
- options: SearchOptions,
181
- ): RegExp | null {
182
- if (!query) {
183
- return null;
184
- }
185
-
186
- const caseSensitive = options.matchCase ?? options.caseSensitive ?? false;
187
- const regex = options.regex ?? false;
188
- const wholeWord = options.wholeWord ?? false;
189
-
190
- try {
191
- const source = regex
192
- ? query
193
- : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194
- const wrapped = wholeWord ? `\\b${source}\\b` : source;
195
- return new RegExp(wrapped, caseSensitive ? "g" : "gi");
196
- } catch {
197
- return null;
198
- }
199
- }
200
-
201
145
  // ---------------------------------------------------------------------------
202
146
  // Clear helper (ProseMirror Command signature)
203
147
  // ---------------------------------------------------------------------------
@@ -215,3 +159,10 @@ export function clearSearch(
215
159
  );
216
160
  return true;
217
161
  }
162
+
163
+ export {
164
+ createSearchExcerpt,
165
+ findSearchMatches,
166
+ searchSecondaryStories,
167
+ };
168
+ export type { SecondaryStorySearchResult };
@@ -0,0 +1,51 @@
1
+ import type {
2
+ EditorStoryTarget,
3
+ EditorSurfaceSnapshot,
4
+ } from "../../api/public-types.ts";
5
+
6
+ const surfaceIdentityMap = new WeakMap<EditorSurfaceSnapshot, number>();
7
+ let nextSurfaceIdentity = 0;
8
+
9
+ function getSurfaceIdentity(surface: EditorSurfaceSnapshot): number {
10
+ const cached = surfaceIdentityMap.get(surface);
11
+ if (cached !== undefined) {
12
+ return cached;
13
+ }
14
+ const identity = nextSurfaceIdentity;
15
+ nextSurfaceIdentity += 1;
16
+ surfaceIdentityMap.set(surface, identity);
17
+ return identity;
18
+ }
19
+
20
+ export function createSurfaceDocumentBuildKey(input: {
21
+ surface: EditorSurfaceSnapshot | null | undefined;
22
+ activeStory: EditorStoryTarget;
23
+ mediaPreviewKey: string;
24
+ }): string {
25
+ return JSON.stringify({
26
+ surfaceIdentity:
27
+ input.surface === undefined || input.surface === null
28
+ ? "loading"
29
+ : getSurfaceIdentity(input.surface),
30
+ activeStory: input.activeStory,
31
+ mediaPreviewKey: input.mediaPreviewKey,
32
+ });
33
+ }
34
+
35
+ export function createSurfaceDecorationKey(input: {
36
+ markupDisplay: string;
37
+ showTrackedChanges: boolean;
38
+ canEdit: boolean;
39
+ activeCommentId?: string;
40
+ activeRevisionId?: string;
41
+ workflowScopeSignature?: string;
42
+ }): string {
43
+ return JSON.stringify({
44
+ markupDisplay: input.markupDisplay,
45
+ showTrackedChanges: input.showTrackedChanges,
46
+ canEdit: input.canEdit,
47
+ activeCommentId: input.activeCommentId ?? null,
48
+ activeRevisionId: input.activeRevisionId ?? null,
49
+ workflowScopeSignature: input.workflowScopeSignature ?? null,
50
+ });
51
+ }
@@ -95,6 +95,17 @@ export function TwInlineToken(props: TwInlineTokenProps) {
95
95
 
96
96
  // opaque_inline
97
97
  if (segment.kind === "opaque_inline") {
98
+ if (segment.presentation === "quiet-marker") {
99
+ return (
100
+ <span
101
+ aria-label={segment.label}
102
+ title={segment.detail}
103
+ className="inline-block h-0 w-0 overflow-hidden align-baseline"
104
+ data-inline-presentation="quiet-marker"
105
+ />
106
+ );
107
+ }
108
+
98
109
  return (
99
110
  <button
100
111
  type="button"
@@ -7,13 +7,14 @@ export interface TwOpaqueBlockProps {
7
7
  block: Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>;
8
8
  selection: SelectionSnapshot;
9
9
  onSelectionChange?: (selection: SelectionSnapshot) => void;
10
+ workflowTargeted?: boolean;
10
11
  }
11
12
 
12
13
  const focusRingClass =
13
14
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
14
15
 
15
16
  export function TwOpaqueBlock(props: TwOpaqueBlockProps) {
16
- const { block, selection } = props;
17
+ const { block, selection, workflowTargeted } = props;
17
18
  const selected = selectionTouchesRange(selection, block.from, block.to);
18
19
 
19
20
  return (
@@ -44,6 +45,11 @@ export function TwOpaqueBlock(props: TwOpaqueBlockProps) {
44
45
  <span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-semibold text-comment bg-warning-soft">
45
46
  preserve-only
46
47
  </span>
48
+ {workflowTargeted && (
49
+ <span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-semibold text-amber-700 bg-amber-100">
50
+ workflow-targeted
51
+ </span>
52
+ )}
47
53
  </div>
48
54
  <p className="text-sm text-secondary">{block.detail}</p>
49
55
  </button>