@beyondwork/docx-react-component 1.0.41 → 1.0.43

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 (118) hide show
  1. package/package.json +38 -37
  2. package/src/api/awareness-identity-types.ts +35 -0
  3. package/src/api/comment-negotiation-types.ts +130 -0
  4. package/src/api/comment-presentation-types.ts +106 -0
  5. package/src/api/editor-state-types.ts +110 -0
  6. package/src/api/external-custody-types.ts +74 -0
  7. package/src/api/participants-types.ts +18 -0
  8. package/src/api/public-types.ts +541 -5
  9. package/src/api/scope-metadata-resolver-types.ts +88 -0
  10. package/src/core/commands/formatting-commands.ts +1 -1
  11. package/src/core/commands/index.ts +601 -9
  12. package/src/core/search/search-text.ts +15 -2
  13. package/src/index.ts +131 -1
  14. package/src/io/docx-session.ts +672 -2
  15. package/src/io/export/escape-xml-attribute.ts +26 -0
  16. package/src/io/export/external-send.ts +188 -0
  17. package/src/io/export/serialize-comments.ts +13 -16
  18. package/src/io/export/serialize-footnotes.ts +17 -24
  19. package/src/io/export/serialize-headers-footers.ts +17 -24
  20. package/src/io/export/serialize-main-document.ts +59 -62
  21. package/src/io/export/serialize-numbering.ts +20 -27
  22. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  23. package/src/io/export/serialize-tables.ts +8 -15
  24. package/src/io/export/table-properties-xml.ts +25 -32
  25. package/src/io/import/external-reimport.ts +40 -0
  26. package/src/io/load-scheduler.ts +230 -0
  27. package/src/io/normalize/normalize-text.ts +83 -0
  28. package/src/io/ooxml/bw-xml.ts +244 -0
  29. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  30. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  31. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  32. package/src/io/ooxml/external-custody-payload.ts +102 -0
  33. package/src/io/ooxml/participants-payload.ts +97 -0
  34. package/src/io/ooxml/payload-signature.ts +112 -0
  35. package/src/io/ooxml/workflow-payload-validator.ts +367 -0
  36. package/src/io/ooxml/workflow-payload.ts +317 -7
  37. package/src/runtime/awareness-identity.ts +173 -0
  38. package/src/runtime/collab/event-types.ts +27 -0
  39. package/src/runtime/collab-session-bridge.ts +157 -0
  40. package/src/runtime/collab-session-facet.ts +193 -0
  41. package/src/runtime/collab-session.ts +273 -0
  42. package/src/runtime/comment-negotiation-sync.ts +91 -0
  43. package/src/runtime/comment-negotiation.ts +158 -0
  44. package/src/runtime/comment-presentation.ts +223 -0
  45. package/src/runtime/document-runtime.ts +639 -124
  46. package/src/runtime/editor-state-channel.ts +544 -0
  47. package/src/runtime/editor-state-integration.ts +217 -0
  48. package/src/runtime/external-send-runtime.ts +117 -0
  49. package/src/runtime/layout/docx-font-loader.ts +11 -30
  50. package/src/runtime/layout/index.ts +2 -0
  51. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  52. package/src/runtime/layout/layout-engine-instance.ts +139 -14
  53. package/src/runtime/layout/page-graph.ts +79 -7
  54. package/src/runtime/layout/paginated-layout-engine.ts +441 -48
  55. package/src/runtime/layout/public-facet.ts +585 -14
  56. package/src/runtime/layout/table-row-split.ts +316 -0
  57. package/src/runtime/markdown-sanitizer.ts +132 -0
  58. package/src/runtime/participants.ts +134 -0
  59. package/src/runtime/perf-counters.ts +28 -0
  60. package/src/runtime/render/render-frame-types.ts +17 -0
  61. package/src/runtime/render/render-kernel.ts +172 -29
  62. package/src/runtime/resign-payload.ts +120 -0
  63. package/src/runtime/surface-projection.ts +10 -5
  64. package/src/runtime/tamper-gate.ts +157 -0
  65. package/src/runtime/workflow-markup.ts +80 -16
  66. package/src/runtime/workflow-rail-segments.ts +244 -5
  67. package/src/ui/WordReviewEditor.tsx +654 -45
  68. package/src/ui/editor-command-bag.ts +14 -0
  69. package/src/ui/editor-runtime-boundary.ts +111 -11
  70. package/src/ui/editor-shell-view.tsx +21 -0
  71. package/src/ui/editor-surface-controller.tsx +5 -0
  72. package/src/ui/headless/selection-helpers.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  74. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  75. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  76. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  77. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  78. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  79. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  80. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  81. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  82. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  83. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  84. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  85. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  86. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  87. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  88. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  89. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  90. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
  91. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  92. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  93. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  94. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  95. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  96. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  97. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  98. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  99. package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
  100. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
  101. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  102. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  103. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
  104. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
  105. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  106. package/src/ui-tailwind/index.ts +37 -1
  107. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  108. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  109. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  110. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  111. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  112. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  113. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  114. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  115. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  116. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  117. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  118. package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
@@ -38,6 +38,52 @@ import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
38
38
  import { collectScopeTagTouches } from "../../review/store/scope-tag-diff.ts";
39
39
  import { applyRevisionRuntimeCommand } from "../../runtime/revision-runtime.ts";
40
40
  import type { RevisionStore } from "../../review/store/revision-store.ts";
41
+ import type {
42
+ HeaderFooterLinkPatch,
43
+ HostAnnotationOverlay,
44
+ InsertTableOptions,
45
+ RuntimeRenderSnapshot,
46
+ SectionBreakType,
47
+ SectionLayoutPatch,
48
+ SectionPageNumberingPatch,
49
+ WorkflowMetadataDefinition,
50
+ WorkflowMetadataEntry,
51
+ WorkflowOverlay,
52
+ } from "../../api/public-types.ts";
53
+ import {
54
+ applyFormattingOperationToDocument,
55
+ type FormattingOperation,
56
+ } from "./formatting-commands.ts";
57
+ import {
58
+ applyParagraphStyleToDocument,
59
+ applyTableStyleToDocument,
60
+ } from "./style-commands.ts";
61
+ import {
62
+ continueNumbering,
63
+ indentListItems,
64
+ outdentListItems,
65
+ restartNumbering,
66
+ toggleBulletedList,
67
+ toggleNumberedList,
68
+ } from "./list-commands.ts";
69
+ import {
70
+ applyTableStructureOperation,
71
+ type TableStructureOperation,
72
+ } from "./table-structure-commands.ts";
73
+ import {
74
+ insertImage,
75
+ repositionFloatingImage,
76
+ resizeImage,
77
+ } from "./image-commands.ts";
78
+ import {
79
+ insertSectionBreakAfterSectionIndex,
80
+ deleteSectionBreakAtSectionIndex,
81
+ updateSectionLayoutAtSectionIndex,
82
+ setSectionPageNumberingAtSectionIndex,
83
+ setHeaderFooterLinkAtSectionIndex,
84
+ } from "./section-layout-commands.ts";
85
+ import { insertPageBreak, insertTable } from "./text-commands.ts";
86
+ import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
41
87
 
42
88
  export interface CommandOrigin {
43
89
  source:
@@ -170,6 +216,149 @@ export type EditorCommand =
170
216
  | {
171
217
  type: "history.redo";
172
218
  origin?: CommandOrigin;
219
+ }
220
+ | {
221
+ type: "workflow.set-overlay";
222
+ overlay: WorkflowOverlay;
223
+ origin?: CommandOrigin;
224
+ }
225
+ | {
226
+ type: "workflow.clear-overlay";
227
+ origin?: CommandOrigin;
228
+ }
229
+ | {
230
+ type: "workflow.set-metadata-definitions";
231
+ definitions: WorkflowMetadataDefinition[];
232
+ origin?: CommandOrigin;
233
+ }
234
+ | {
235
+ type: "workflow.clear-metadata-definitions";
236
+ origin?: CommandOrigin;
237
+ }
238
+ | {
239
+ type: "workflow.set-metadata-entries";
240
+ entries: WorkflowMetadataEntry[];
241
+ origin?: CommandOrigin;
242
+ }
243
+ | {
244
+ type: "workflow.clear-metadata-entries";
245
+ origin?: CommandOrigin;
246
+ }
247
+ | {
248
+ type: "host-annotation.set-overlay";
249
+ overlay: HostAnnotationOverlay;
250
+ origin?: CommandOrigin;
251
+ }
252
+ | {
253
+ type: "host-annotation.clear-overlay";
254
+ origin?: CommandOrigin;
255
+ }
256
+ | {
257
+ type: "formatting.apply";
258
+ operation: FormattingOperation;
259
+ origin?: CommandOrigin;
260
+ }
261
+ | {
262
+ type: "style.set-paragraph";
263
+ styleId: string | null;
264
+ origin?: CommandOrigin;
265
+ }
266
+ | {
267
+ type: "style.set-table";
268
+ styleId: string | null;
269
+ origin?: CommandOrigin;
270
+ }
271
+ | {
272
+ type: "list.toggle";
273
+ kind: "bulleted" | "numbered";
274
+ paragraphIndexes: readonly number[];
275
+ origin?: CommandOrigin;
276
+ }
277
+ | {
278
+ type: "list.indent";
279
+ paragraphIndexes: readonly number[];
280
+ origin?: CommandOrigin;
281
+ }
282
+ | {
283
+ type: "list.outdent";
284
+ paragraphIndexes: readonly number[];
285
+ origin?: CommandOrigin;
286
+ }
287
+ | {
288
+ type: "list.restart-numbering";
289
+ paragraphIndex: number;
290
+ startAt?: number;
291
+ origin?: CommandOrigin;
292
+ }
293
+ | {
294
+ type: "list.continue-numbering";
295
+ paragraphIndex: number;
296
+ origin?: CommandOrigin;
297
+ }
298
+ | {
299
+ type: "table.apply-structure";
300
+ operation: TableStructureOperation;
301
+ selectionDescriptor: TableSelectionDescriptor | null;
302
+ origin?: CommandOrigin;
303
+ }
304
+ | {
305
+ type: "image.insert";
306
+ data: string;
307
+ mimeType: string;
308
+ width?: number;
309
+ height?: number;
310
+ altText?: string;
311
+ origin?: CommandOrigin;
312
+ }
313
+ | {
314
+ type: "image.set-layout";
315
+ mediaId: string;
316
+ dimensions: { widthEmu: number; heightEmu: number };
317
+ origin?: CommandOrigin;
318
+ }
319
+ | {
320
+ type: "image.set-frame";
321
+ mediaId: string;
322
+ offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number };
323
+ origin?: CommandOrigin;
324
+ }
325
+ | {
326
+ type: "section.insert-break";
327
+ sectionIndex: number;
328
+ breakType: SectionBreakType;
329
+ origin?: CommandOrigin;
330
+ }
331
+ | {
332
+ type: "section.delete-break";
333
+ sectionIndex: number;
334
+ origin?: CommandOrigin;
335
+ }
336
+ | {
337
+ type: "section.update-layout";
338
+ sectionIndex: number;
339
+ patch: SectionLayoutPatch;
340
+ origin?: CommandOrigin;
341
+ }
342
+ | {
343
+ type: "section.set-page-numbering";
344
+ sectionIndex: number;
345
+ patch: SectionPageNumberingPatch | null;
346
+ origin?: CommandOrigin;
347
+ }
348
+ | {
349
+ type: "section.set-header-footer-link";
350
+ sectionIndex: number;
351
+ params: HeaderFooterLinkPatch;
352
+ origin?: CommandOrigin;
353
+ }
354
+ | {
355
+ type: "content.insert-page-break";
356
+ origin?: CommandOrigin;
357
+ }
358
+ | {
359
+ type: "content.insert-table";
360
+ options: InsertTableOptions;
361
+ origin?: CommandOrigin;
173
362
  };
174
363
 
175
364
  export interface TransactionEffects {
@@ -198,11 +387,40 @@ export interface CommandExecutionContext {
198
387
  timestamp: string;
199
388
  documentMode?: "editing" | "suggesting" | "viewing" | "commenting";
200
389
  defaultAuthorId?: string;
390
+ /**
391
+ * Runtime-owned render snapshot made available for document-mutating
392
+ * commands that need per-paragraph surface lookups (formatting, styles,
393
+ * section layout). Populated by the runtime at dispatch time; absent for
394
+ * callers that cannot produce it.
395
+ */
396
+ renderSnapshot?: RuntimeRenderSnapshot;
201
397
  }
202
398
 
399
+ /**
400
+ * Commands that are surfaced on the event log but do NOT mutate `EditorState`.
401
+ * They mutate runtime-scoped overlays (workflow, host annotations) and are
402
+ * applied directly by the runtime rather than through `executeEditorCommand`.
403
+ */
404
+ export type RuntimeStateCommandType =
405
+ | "history.undo"
406
+ | "history.redo"
407
+ | "workflow.set-overlay"
408
+ | "workflow.clear-overlay"
409
+ | "workflow.set-metadata-definitions"
410
+ | "workflow.clear-metadata-definitions"
411
+ | "workflow.set-metadata-entries"
412
+ | "workflow.clear-metadata-entries"
413
+ | "host-annotation.set-overlay"
414
+ | "host-annotation.clear-overlay";
415
+
416
+ export type StateMutatingCommand = Exclude<
417
+ EditorCommand,
418
+ { type: RuntimeStateCommandType }
419
+ >;
420
+
203
421
  export function executeEditorCommand(
204
422
  state: EditorState,
205
- command: Exclude<EditorCommand, { type: "history.undo" } | { type: "history.redo" }>,
423
+ command: StateMutatingCommand,
206
424
  context: CommandExecutionContext,
207
425
  ): EditorTransaction {
208
426
  switch (command.type) {
@@ -633,7 +851,356 @@ export function executeEditorCommand(
633
851
  },
634
852
  context.timestamp,
635
853
  );
854
+ case "formatting.apply": {
855
+ if (!context.renderSnapshot) {
856
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
857
+ }
858
+ const result = applyFormattingOperationToDocument(
859
+ state.document,
860
+ context.renderSnapshot,
861
+ command.operation,
862
+ );
863
+ return buildDocumentReplaceTransaction(state, context, {
864
+ changed: result.changed,
865
+ document: result.document,
866
+ selection: toInternalSelection(result.selection, state.selection),
867
+ });
868
+ }
869
+ case "style.set-paragraph": {
870
+ if (!context.renderSnapshot) {
871
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
872
+ }
873
+ const result = applyParagraphStyleToDocument(
874
+ state.document,
875
+ context.renderSnapshot,
876
+ command.styleId,
877
+ );
878
+ return buildDocumentReplaceTransaction(state, context, {
879
+ changed: result.changed,
880
+ document: result.document,
881
+ selection: toInternalSelection(result.selection, state.selection),
882
+ });
883
+ }
884
+ case "style.set-table": {
885
+ if (!context.renderSnapshot) {
886
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
887
+ }
888
+ const result = applyTableStyleToDocument(
889
+ state.document,
890
+ context.renderSnapshot,
891
+ command.styleId,
892
+ );
893
+ return buildDocumentReplaceTransaction(state, context, {
894
+ changed: result.changed,
895
+ document: result.document,
896
+ selection: toInternalSelection(result.selection, state.selection),
897
+ });
898
+ }
899
+ case "list.toggle": {
900
+ const result = command.kind === "bulleted"
901
+ ? toggleBulletedList(state.document, command.paragraphIndexes, { timestamp: context.timestamp })
902
+ : toggleNumberedList(state.document, command.paragraphIndexes, { timestamp: context.timestamp });
903
+ return buildDocumentReplaceTransaction(state, context, {
904
+ changed: result.affectedParagraphIndexes.length > 0,
905
+ document: result.document,
906
+ selection: state.selection,
907
+ });
908
+ }
909
+ case "list.indent": {
910
+ const result = indentListItems(state.document, command.paragraphIndexes, { timestamp: context.timestamp });
911
+ return buildDocumentReplaceTransaction(state, context, {
912
+ changed: result.affectedParagraphIndexes.length > 0,
913
+ document: result.document,
914
+ selection: state.selection,
915
+ });
916
+ }
917
+ case "list.outdent": {
918
+ const result = outdentListItems(state.document, command.paragraphIndexes, { timestamp: context.timestamp });
919
+ return buildDocumentReplaceTransaction(state, context, {
920
+ changed: result.affectedParagraphIndexes.length > 0,
921
+ document: result.document,
922
+ selection: state.selection,
923
+ });
924
+ }
925
+ case "list.restart-numbering": {
926
+ const result = restartNumbering(
927
+ state.document,
928
+ command.paragraphIndex,
929
+ { timestamp: context.timestamp },
930
+ command.startAt,
931
+ );
932
+ return buildDocumentReplaceTransaction(state, context, {
933
+ changed: result.affectedParagraphIndexes.length > 0,
934
+ document: result.document,
935
+ selection: state.selection,
936
+ });
937
+ }
938
+ case "list.continue-numbering": {
939
+ const result = continueNumbering(
940
+ state.document,
941
+ command.paragraphIndex,
942
+ { timestamp: context.timestamp },
943
+ );
944
+ return buildDocumentReplaceTransaction(state, context, {
945
+ changed: result.affectedParagraphIndexes.length > 0,
946
+ document: result.document,
947
+ selection: state.selection,
948
+ });
949
+ }
950
+ case "table.apply-structure": {
951
+ if (!context.renderSnapshot) {
952
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
953
+ }
954
+ const result = applyTableStructureOperation(
955
+ state.document,
956
+ context.renderSnapshot,
957
+ command.selectionDescriptor,
958
+ command.operation,
959
+ );
960
+ return buildDocumentReplaceTransaction(state, context, {
961
+ changed: result.changed,
962
+ document: result.document,
963
+ selection: result.selection,
964
+ mapping: result.mapping,
965
+ });
966
+ }
967
+ case "image.insert": {
968
+ const dataBytes = decodeBase64ToBytes(command.data);
969
+ const result = insertImage(
970
+ state.document,
971
+ state.selection,
972
+ dataBytes,
973
+ command.mimeType,
974
+ command.width,
975
+ command.height,
976
+ {
977
+ timestamp: context.timestamp,
978
+ ...(command.altText ? { altText: command.altText } : {}),
979
+ },
980
+ );
981
+ return buildDocumentReplaceTransaction(state, context, {
982
+ changed: true,
983
+ document: result.document,
984
+ selection: result.selection,
985
+ mapping: result.mapping,
986
+ });
987
+ }
988
+ case "image.set-layout": {
989
+ try {
990
+ const result = resizeImage(
991
+ state.document,
992
+ command.mediaId,
993
+ command.dimensions,
994
+ context.timestamp,
995
+ );
996
+ return buildDocumentReplaceTransaction(state, context, {
997
+ changed: true,
998
+ document: result.document,
999
+ selection: state.selection,
1000
+ });
1001
+ } catch {
1002
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1003
+ }
1004
+ }
1005
+ case "image.set-frame": {
1006
+ try {
1007
+ const result = repositionFloatingImage(
1008
+ state.document,
1009
+ command.mediaId,
1010
+ command.offsets,
1011
+ context.timestamp,
1012
+ );
1013
+ return buildDocumentReplaceTransaction(state, context, {
1014
+ changed: true,
1015
+ document: result.document,
1016
+ selection: state.selection,
1017
+ });
1018
+ } catch {
1019
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1020
+ }
1021
+ }
1022
+ case "section.insert-break": {
1023
+ const result = insertSectionBreakAfterSectionIndex(
1024
+ state.document,
1025
+ command.sectionIndex,
1026
+ command.breakType,
1027
+ { timestamp: context.timestamp },
1028
+ );
1029
+ return buildDocumentReplaceTransaction(state, context, {
1030
+ changed: result.changed,
1031
+ document: result.document,
1032
+ selection: state.selection,
1033
+ });
1034
+ }
1035
+ case "section.delete-break": {
1036
+ const result = deleteSectionBreakAtSectionIndex(
1037
+ state.document,
1038
+ command.sectionIndex,
1039
+ { timestamp: context.timestamp },
1040
+ );
1041
+ return buildDocumentReplaceTransaction(state, context, {
1042
+ changed: result.changed,
1043
+ document: result.document,
1044
+ selection: state.selection,
1045
+ });
1046
+ }
1047
+ case "section.update-layout": {
1048
+ const result = updateSectionLayoutAtSectionIndex(
1049
+ state.document,
1050
+ command.sectionIndex,
1051
+ command.patch,
1052
+ { timestamp: context.timestamp },
1053
+ );
1054
+ return buildDocumentReplaceTransaction(state, context, {
1055
+ changed: result.changed,
1056
+ document: result.document,
1057
+ selection: state.selection,
1058
+ });
1059
+ }
1060
+ case "section.set-page-numbering": {
1061
+ const normalizedPatch = command.patch === null
1062
+ ? null
1063
+ : stripNullsFromRecord(command.patch as unknown as Record<string, unknown>);
1064
+ const result = setSectionPageNumberingAtSectionIndex(
1065
+ state.document,
1066
+ command.sectionIndex,
1067
+ normalizedPatch as never,
1068
+ { timestamp: context.timestamp },
1069
+ );
1070
+ return buildDocumentReplaceTransaction(state, context, {
1071
+ changed: result.changed,
1072
+ document: result.document,
1073
+ selection: state.selection,
1074
+ });
1075
+ }
1076
+ case "section.set-header-footer-link": {
1077
+ const result = setHeaderFooterLinkAtSectionIndex(
1078
+ state.document,
1079
+ command.sectionIndex,
1080
+ command.params,
1081
+ { timestamp: context.timestamp },
1082
+ );
1083
+ return buildDocumentReplaceTransaction(state, context, {
1084
+ changed: result.changed,
1085
+ document: result.document,
1086
+ selection: state.selection,
1087
+ });
1088
+ }
1089
+ case "content.insert-page-break": {
1090
+ const result = insertPageBreak(
1091
+ state.document,
1092
+ state.selection,
1093
+ { timestamp: context.timestamp },
1094
+ );
1095
+ return buildDocumentReplaceTransaction(state, context, {
1096
+ changed: result.changed,
1097
+ document: result.document,
1098
+ selection: result.selection,
1099
+ mapping: result.mapping,
1100
+ });
1101
+ }
1102
+ case "content.insert-table": {
1103
+ const result = insertTable(
1104
+ state.document,
1105
+ state.selection,
1106
+ command.options,
1107
+ { timestamp: context.timestamp },
1108
+ );
1109
+ return buildDocumentReplaceTransaction(state, context, {
1110
+ changed: result.changed,
1111
+ document: result.document,
1112
+ selection: result.selection,
1113
+ mapping: result.mapping,
1114
+ });
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ function buildDocumentReplaceTransaction(
1120
+ state: EditorState,
1121
+ context: CommandExecutionContext,
1122
+ result: {
1123
+ changed: boolean;
1124
+ document: EditorState["document"];
1125
+ selection: SelectionSnapshot;
1126
+ mapping?: TransactionMapping;
1127
+ },
1128
+ ): EditorTransaction {
1129
+ if (!result.changed) {
1130
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1131
+ }
1132
+ const mapping = result.mapping ?? createEmptyMapping();
1133
+ const reviewState = remapReviewStateAfterContentChange(
1134
+ state,
1135
+ result.document,
1136
+ mapping,
1137
+ );
1138
+ return createTransaction(
1139
+ {
1140
+ ...state,
1141
+ document: reviewState.document,
1142
+ selection: result.selection,
1143
+ warnings: reviewState.warnings,
1144
+ runtime: {
1145
+ ...state.runtime,
1146
+ activeCommentId: reviewState.activeCommentId,
1147
+ },
1148
+ compatibility: {
1149
+ ...state.compatibility,
1150
+ generatedAt: context.timestamp,
1151
+ warnings: reviewState.warnings,
1152
+ featureEntries: state.compatibility.featureEntries.map((entry) =>
1153
+ entry.affectedAnchor
1154
+ ? {
1155
+ ...entry,
1156
+ affectedAnchor: mapAnchor(entry.affectedAnchor, mapping),
1157
+ }
1158
+ : entry,
1159
+ ),
1160
+ },
1161
+ },
1162
+ {
1163
+ historyBoundary: "push",
1164
+ markDirty: true,
1165
+ mapping,
1166
+ effects: reviewState.effects,
1167
+ },
1168
+ );
1169
+ }
1170
+
1171
+ function decodeBase64ToBytes(value: string): Uint8Array {
1172
+ const binary = atob(value);
1173
+ const bytes = new Uint8Array(binary.length);
1174
+ for (let i = 0; i < binary.length; i++) {
1175
+ bytes[i] = binary.charCodeAt(i);
636
1176
  }
1177
+ return bytes;
1178
+ }
1179
+
1180
+ function toInternalSelection(
1181
+ publicSelection: RuntimeRenderSnapshot["selection"],
1182
+ fallback: SelectionSnapshot,
1183
+ ): SelectionSnapshot {
1184
+ if (
1185
+ publicSelection.anchor === fallback.anchor &&
1186
+ publicSelection.head === fallback.head
1187
+ ) {
1188
+ return fallback;
1189
+ }
1190
+ return createSelectionSnapshot(publicSelection.anchor, publicSelection.head);
1191
+ }
1192
+
1193
+ function stripNullsFromRecord<T extends Record<string, unknown>>(
1194
+ value: T,
1195
+ ): { [K in keyof T]?: Exclude<T[K], null> } {
1196
+ const out: Record<string, unknown> = {};
1197
+ for (const key of Object.keys(value) as Array<keyof T>) {
1198
+ const v = value[key];
1199
+ if (v !== null && v !== undefined) {
1200
+ out[key as string] = v;
1201
+ }
1202
+ }
1203
+ return out as { [K in keyof T]?: Exclude<T[K], null> };
637
1204
  }
638
1205
 
639
1206
  export function remapSelection(
@@ -1224,22 +1791,46 @@ function combineMappingSteps(
1224
1791
  // Suggesting mode: creates revision records instead of (or alongside) text mutations
1225
1792
  // ---------------------------------------------------------------------------
1226
1793
 
1227
- let suggestingRevisionCounter = 0;
1228
-
1794
+ /**
1795
+ * Builds a suggesting-mode revision id that is deterministic across
1796
+ * clients. The id is a pure function of `(existing, timestamp, authorId)`
1797
+ * so that origin and replica — both of whom see the same `existing`
1798
+ * revisions record after a Yjs-ordered replay and receive the same
1799
+ * `timestamp` + `authorId` on the broadcast command event — produce
1800
+ * the same id for the same authored revision.
1801
+ *
1802
+ * Previously this used a module-level counter; that counter was shared
1803
+ * across every `DocumentRuntime` in a single process and drifted between
1804
+ * origin and replica whenever either side had other suggesting-mode work
1805
+ * bump the counter before replay. Cross-client `change.accept(changeId)`
1806
+ * / `change.reject(changeId)` then missed because the target id only
1807
+ * existed on one side.
1808
+ */
1229
1809
  function createSuggestingRevisionId(
1230
1810
  existing: Record<string, unknown>,
1231
1811
  timestamp: string,
1812
+ authorId: string,
1232
1813
  ): string {
1233
- suggestingRevisionCounter += 1;
1234
1814
  const ts = timestamp.replace(/[^0-9]/gu, "");
1235
- let id = `change-${ts}-s${suggestingRevisionCounter}`;
1815
+ const authorMarker = sanitizeAuthorMarker(authorId);
1816
+ const prefix = `change-${authorMarker}-${ts}-s`;
1817
+ let n = 1;
1818
+ let id = `${prefix}${n}`;
1236
1819
  while (existing[id]) {
1237
- suggestingRevisionCounter += 1;
1238
- id = `change-${ts}-s${suggestingRevisionCounter}`;
1820
+ n += 1;
1821
+ id = `${prefix}${n}`;
1239
1822
  }
1240
1823
  return id;
1241
1824
  }
1242
1825
 
1826
+ function sanitizeAuthorMarker(authorId: string): string {
1827
+ // OOXML `w:id` accepts alphanumerics plus `._-`; keep within that set so
1828
+ // the id survives round-trips unchanged (see `sanitizeRevisionId` on the
1829
+ // serializer side).
1830
+ const cleaned = authorId.replace(/[^A-Za-z0-9._-]/g, "-");
1831
+ return cleaned.length > 0 ? cleaned : "u";
1832
+ }
1833
+
1243
1834
  function createAuthoredRevision(
1244
1835
  existing: Record<string, unknown>,
1245
1836
  kind: "insertion" | "deletion",
@@ -1249,7 +1840,7 @@ function createAuthoredRevision(
1249
1840
  timestamp: string,
1250
1841
  metadata: Partial<NonNullable<CanonicalRevisionRecord["metadata"]>> = {},
1251
1842
  ): CanonicalRevisionRecord {
1252
- const changeId = createSuggestingRevisionId(existing, timestamp);
1843
+ const changeId = createSuggestingRevisionId(existing, timestamp, authorId);
1253
1844
  return {
1254
1845
  changeId,
1255
1846
  kind,
@@ -1499,6 +2090,7 @@ function applySuggestingInsert(
1499
2090
  const replacementSuggestionId = createSuggestingRevisionId(
1500
2091
  reviewState.document.review.revisions,
1501
2092
  context.timestamp,
2093
+ authorId,
1502
2094
  );
1503
2095
 
1504
2096
  // Step 3: Create deletion revision for the selected range (text stays in place).
@@ -1757,7 +2349,7 @@ function applySuggestingInsertUnit(
1757
2349
  result.mapping,
1758
2350
  );
1759
2351
  const replacementSuggestionId = from !== to
1760
- ? createSuggestingRevisionId(reviewState.document.review.revisions, context.timestamp)
2352
+ ? createSuggestingRevisionId(reviewState.document.review.revisions, context.timestamp, authorId)
1761
2353
  : undefined;
1762
2354
 
1763
2355
  // If non-collapsed, mark selected range as deletion (positions are pre-mapping, content preserved)
@@ -26,7 +26,7 @@ export interface SecondaryStorySearchResult extends SurfaceSearchResult {
26
26
  storyTarget: EditorStoryTarget;
27
27
  }
28
28
 
29
- interface ProjectedSurfaceText {
29
+ export interface ProjectedSurfaceText {
30
30
  text: string;
31
31
  offsetMap: Array<number | null>;
32
32
  }
@@ -112,7 +112,20 @@ export function searchSurfaceBlocks(
112
112
  query: string,
113
113
  options: SearchTextOptions = {},
114
114
  ): SurfaceSearchResult[] {
115
- const projection = projectSurfaceText(blocks);
115
+ return searchProjectedSurfaceText(projectSurfaceText(blocks), query, options);
116
+ }
117
+
118
+ /**
119
+ * Search a pre-projected surface text. Hoist `projectSurfaceText(blocks)` out
120
+ * of a per-query loop to avoid rebuilding the projection N times for N queries
121
+ * against the same surface — L7 Phase 1.5 discovered this cost dominates
122
+ * `collectFieldMarkup` on the CCEP large-tables fixture.
123
+ */
124
+ export function searchProjectedSurfaceText(
125
+ projection: ProjectedSurfaceText,
126
+ query: string,
127
+ options: SearchTextOptions = {},
128
+ ): SurfaceSearchResult[] {
116
129
  return findSearchMatches(projection.text, query, options)
117
130
  .map((match) => {
118
131
  const range = resolveProjectedRuntimeRange(