@beyondwork/docx-react-component 1.0.21 → 1.0.23

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 (33) hide show
  1. package/README.md +763 -38
  2. package/package.json +25 -36
  3. package/src/api/public-types.ts +66 -1
  4. package/src/core/commands/index.ts +574 -5
  5. package/src/index.ts +5 -0
  6. package/src/io/docx-session.ts +181 -2
  7. package/src/io/export/serialize-main-document.ts +21 -1
  8. package/src/io/normalize/normalize-text.ts +4 -0
  9. package/src/io/ooxml/parse-main-document.ts +88 -7
  10. package/src/model/canonical-document.ts +22 -0
  11. package/src/review/store/revision-store.ts +1 -0
  12. package/src/review/store/revision-types.ts +2 -0
  13. package/src/runtime/document-runtime.ts +503 -51
  14. package/src/runtime/session-capabilities.ts +6 -5
  15. package/src/runtime/surface-projection.ts +2 -0
  16. package/src/runtime/table-schema.ts +2 -0
  17. package/src/runtime/workflow-markup.ts +5 -1
  18. package/src/ui/WordReviewEditor.tsx +661 -132
  19. package/src/ui/editor-runtime-boundary.ts +10 -1
  20. package/src/ui/editor-shell-view.tsx +8 -0
  21. package/src/ui/editor-surface-controller.tsx +5 -0
  22. package/src/ui/headless/selection-toolbar-model.ts +12 -0
  23. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  24. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
  25. package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
  26. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
  27. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
  29. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
  30. package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
  31. package/src/ui-tailwind/theme/editor-theme.css +10 -0
  32. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
  33. package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
@@ -33,6 +33,7 @@ import {
33
33
  insertText,
34
34
  splitParagraph,
35
35
  } from "./text-commands.ts";
36
+ import type { RevisionRecord as CanonicalRevisionRecord } from "../../model/canonical-document.ts";
36
37
  import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
37
38
  import { applyRevisionRuntimeCommand } from "../../runtime/revision-runtime.ts";
38
39
  import type { RevisionStore } from "../../review/store/revision-store.ts";
@@ -180,6 +181,8 @@ export interface TransactionEffects {
180
181
  commentBodyEdited?: { commentId: string };
181
182
  changeAccepted?: { changeId: string };
182
183
  changeRejected?: { changeId: string };
184
+ revisionAuthored?: { changeId: string; kind: "insertion" | "deletion" };
185
+ commandBlocked?: { code: string; message: string };
183
186
  }
184
187
 
185
188
  export interface EditorTransaction {
@@ -192,6 +195,8 @@ export interface EditorTransaction {
192
195
 
193
196
  export interface CommandExecutionContext {
194
197
  timestamp: string;
198
+ documentMode?: "editing" | "suggesting" | "viewing";
199
+ defaultAuthorId?: string;
195
200
  }
196
201
 
197
202
  export function executeEditorCommand(
@@ -253,27 +258,64 @@ export function executeEditorCommand(
253
258
  },
254
259
  );
255
260
  }
256
- case "text.insert":
261
+ case "text.insert": {
262
+ const suggestingResult = context.documentMode === "suggesting"
263
+ ? applySuggestingInsert(state, command.text, context)
264
+ : undefined;
265
+ if (suggestingResult) return suggestingResult;
257
266
  return applyTextCommand(state, context.timestamp, (document, selection) =>
258
267
  insertText(document, selection, command.text, context),
259
268
  );
260
- case "text.delete-backward":
269
+ }
270
+ case "text.delete-backward": {
271
+ const suggestingResult = context.documentMode === "suggesting"
272
+ ? applySuggestingDelete(state, "backward", context)
273
+ : undefined;
274
+ if (suggestingResult) return suggestingResult;
261
275
  return applyTextCommand(state, context.timestamp, (document, selection) =>
262
276
  deleteSelectionOrBackward(document, selection, context),
263
277
  );
264
- case "text.delete-forward":
278
+ }
279
+ case "text.delete-forward": {
280
+ const suggestingResult = context.documentMode === "suggesting"
281
+ ? applySuggestingDelete(state, "forward", context)
282
+ : undefined;
283
+ if (suggestingResult) return suggestingResult;
265
284
  return applyTextCommand(state, context.timestamp, (document, selection) =>
266
285
  deleteSelectionOrForward(document, selection, context),
267
286
  );
268
- case "text.insert-tab":
287
+ }
288
+ case "text.insert-tab": {
289
+ const suggestingResult = context.documentMode === "suggesting"
290
+ ? applySuggestingInsertUnit(state, "tab", context)
291
+ : undefined;
292
+ if (suggestingResult) return suggestingResult;
269
293
  return applyTextCommand(state, context.timestamp, (document, selection) =>
270
294
  insertTab(document, selection, context),
271
295
  );
272
- case "text.insert-hard-break":
296
+ }
297
+ case "text.insert-hard-break": {
298
+ const suggestingResult = context.documentMode === "suggesting"
299
+ ? applySuggestingInsertUnit(state, "hard_break", context)
300
+ : undefined;
301
+ if (suggestingResult) return suggestingResult;
273
302
  return applyTextCommand(state, context.timestamp, (document, selection) =>
274
303
  insertHardBreak(document, selection, context),
275
304
  );
305
+ }
276
306
  case "paragraph.split":
307
+ if (context.documentMode === "suggesting") {
308
+ return createTransaction(state, {
309
+ historyBoundary: "skip",
310
+ markDirty: false,
311
+ effects: {
312
+ commandBlocked: {
313
+ code: "suggesting_unsupported",
314
+ message: "Paragraph splits are not supported in suggesting mode.",
315
+ },
316
+ },
317
+ });
318
+ }
277
319
  return applyTextCommand(state, context.timestamp, (document, selection) =>
278
320
  splitParagraph(document, selection, context),
279
321
  );
@@ -666,6 +708,8 @@ function createTransaction(
666
708
  commentResolved: options.effects?.commentResolved,
667
709
  changeAccepted: options.effects?.changeAccepted,
668
710
  changeRejected: options.effects?.changeRejected,
711
+ revisionAuthored: options.effects?.revisionAuthored,
712
+ commandBlocked: options.effects?.commandBlocked,
669
713
  },
670
714
  };
671
715
  }
@@ -1010,6 +1054,7 @@ function createRevisionStoreFromState(
1010
1054
  warningIds: [...(revision.warningIds ?? [])],
1011
1055
  metadata: {
1012
1056
  source: revision.metadata?.source ?? "runtime",
1057
+ storyTarget: revision.metadata?.storyTarget,
1013
1058
  preserveOnlyReason: revision.metadata?.preserveOnlyReason,
1014
1059
  importedRevisionForm: revision.metadata?.importedRevisionForm,
1015
1060
  originalRevisionType: revision.metadata?.originalRevisionType,
@@ -1036,6 +1081,7 @@ function toEditorRevisionRecords(
1036
1081
  warningIds: [...revision.warningIds],
1037
1082
  metadata: {
1038
1083
  source: revision.metadata.source,
1084
+ storyTarget: revision.metadata.storyTarget,
1039
1085
  preserveOnlyReason: revision.metadata.preserveOnlyReason,
1040
1086
  importedRevisionForm: revision.metadata.importedRevisionForm,
1041
1087
  originalRevisionType: revision.metadata.originalRevisionType,
@@ -1135,3 +1181,526 @@ function combineMappingSteps(
1135
1181
  }
1136
1182
  : createEmptyMapping();
1137
1183
  }
1184
+
1185
+ // ---------------------------------------------------------------------------
1186
+ // Suggesting mode: creates revision records instead of (or alongside) text mutations
1187
+ // ---------------------------------------------------------------------------
1188
+
1189
+ let suggestingRevisionCounter = 0;
1190
+
1191
+ function createSuggestingRevisionId(
1192
+ existing: Record<string, unknown>,
1193
+ timestamp: string,
1194
+ ): string {
1195
+ suggestingRevisionCounter += 1;
1196
+ const ts = timestamp.replace(/[^0-9]/gu, "");
1197
+ let id = `change-${ts}-s${suggestingRevisionCounter}`;
1198
+ while (existing[id]) {
1199
+ suggestingRevisionCounter += 1;
1200
+ id = `change-${ts}-s${suggestingRevisionCounter}`;
1201
+ }
1202
+ return id;
1203
+ }
1204
+
1205
+ function createAuthoredRevision(
1206
+ existing: Record<string, unknown>,
1207
+ kind: "insertion" | "deletion",
1208
+ from: number,
1209
+ to: number,
1210
+ authorId: string,
1211
+ timestamp: string,
1212
+ ): CanonicalRevisionRecord {
1213
+ const changeId = createSuggestingRevisionId(existing, timestamp);
1214
+ return {
1215
+ changeId,
1216
+ kind,
1217
+ anchor: {
1218
+ kind: "range",
1219
+ range: { from, to },
1220
+ assoc: { start: 1, end: -1 },
1221
+ },
1222
+ authorId,
1223
+ createdAt: timestamp,
1224
+ warningIds: [],
1225
+ metadata: {
1226
+ source: "runtime",
1227
+ },
1228
+ status: "open",
1229
+ };
1230
+ }
1231
+
1232
+ function createSuggestingUnsupportedTransaction(
1233
+ state: EditorState,
1234
+ message: string,
1235
+ ): EditorTransaction {
1236
+ return createTransaction(state, {
1237
+ historyBoundary: "skip",
1238
+ markDirty: false,
1239
+ effects: {
1240
+ commandBlocked: {
1241
+ code: "suggesting_unsupported",
1242
+ message,
1243
+ },
1244
+ },
1245
+ });
1246
+ }
1247
+
1248
+ function isSingleParagraphSuggestingRange(
1249
+ document: CanonicalDocumentEnvelope,
1250
+ from: number,
1251
+ to: number,
1252
+ ): boolean {
1253
+ const ranges: Array<{ start: number; end: number }> = [];
1254
+ let cursor = 0;
1255
+ let previousWasParagraph = false;
1256
+
1257
+ for (const block of document.content.children) {
1258
+ if (block.type === "paragraph") {
1259
+ if (previousWasParagraph) {
1260
+ cursor += 1;
1261
+ }
1262
+ const start = cursor;
1263
+ cursor += block.children.reduce<number>((size, child) => {
1264
+ if (child.type === "text") {
1265
+ return size + child.text.length;
1266
+ }
1267
+ if (child.type === "hyperlink") {
1268
+ return size + child.children.reduce<number>((childSize, entry) => {
1269
+ if (entry.type === "text") {
1270
+ return childSize + entry.text.length;
1271
+ }
1272
+ return childSize + 1;
1273
+ }, 0);
1274
+ }
1275
+ return size + 1;
1276
+ }, 0);
1277
+ ranges.push({ start, end: cursor });
1278
+ previousWasParagraph = true;
1279
+ continue;
1280
+ }
1281
+
1282
+ cursor += 1;
1283
+ previousWasParagraph = false;
1284
+ }
1285
+
1286
+ return ranges.some((range) => from >= range.start && to <= range.end);
1287
+ }
1288
+
1289
+ function applySuggestingInsert(
1290
+ state: EditorState,
1291
+ text: string,
1292
+ context: CommandExecutionContext,
1293
+ ): EditorTransaction | undefined {
1294
+ if (state.readOnly) {
1295
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1296
+ }
1297
+
1298
+ const authorId = context.defaultAuthorId ?? "unknown";
1299
+ const selection = state.selection;
1300
+ const from = Math.min(selection.anchor, selection.head);
1301
+ const to = Math.max(selection.anchor, selection.head);
1302
+ const isCollapsed = from === to;
1303
+
1304
+ if (!isCollapsed && !isSingleParagraphSuggestingRange(state.document, from, to)) {
1305
+ return createSuggestingUnsupportedTransaction(
1306
+ state,
1307
+ "Suggesting mode does not yet support multi-paragraph replacement ranges.",
1308
+ );
1309
+ }
1310
+
1311
+ if (isCollapsed) {
1312
+ // Pure insertion at cursor: apply normally, then create insertion revision
1313
+ const result = insertText(state.document, selection, text, { timestamp: context.timestamp });
1314
+ const insertedFrom = from;
1315
+ const insertedTo = from + Array.from(text).length;
1316
+
1317
+ // Remap existing review state first (comments, pre-existing revisions)
1318
+ const reviewState = remapReviewStateAfterContentChange(
1319
+ state,
1320
+ result.document,
1321
+ result.mapping,
1322
+ );
1323
+
1324
+ // Create the revision with pre-mapping positions — it refers to content
1325
+ // that was just inserted, so its anchors are already correct in the new document
1326
+ const revision = createAuthoredRevision(
1327
+ reviewState.document.review.revisions,
1328
+ "insertion",
1329
+ insertedFrom,
1330
+ insertedTo,
1331
+ authorId,
1332
+ context.timestamp,
1333
+ );
1334
+
1335
+ const finalDocument: CanonicalDocumentEnvelope = {
1336
+ ...reviewState.document,
1337
+ review: {
1338
+ ...reviewState.document.review,
1339
+ revisions: {
1340
+ ...reviewState.document.review.revisions,
1341
+ [revision.changeId]: revision,
1342
+ },
1343
+ },
1344
+ };
1345
+
1346
+ return createTransaction(
1347
+ {
1348
+ ...state,
1349
+ document: finalDocument,
1350
+ selection: result.selection,
1351
+ warnings: reviewState.warnings,
1352
+ runtime: {
1353
+ ...state.runtime,
1354
+ activeCommentId: reviewState.activeCommentId,
1355
+ },
1356
+ },
1357
+ {
1358
+ historyBoundary: "push",
1359
+ markDirty: true,
1360
+ mapping: result.mapping,
1361
+ effects: {
1362
+ ...reviewState.effects,
1363
+ revisionAuthored: { changeId: revision.changeId, kind: "insertion" },
1364
+ },
1365
+ },
1366
+ );
1367
+ }
1368
+
1369
+ // Non-collapsed selection: mark selected text as deletion, insert new text after selection
1370
+ // Step 1: Insert new text at the end of the selection
1371
+ const insertSelection = createSelectionSnapshot(to, to);
1372
+ const result = insertText(state.document, insertSelection, text, { timestamp: context.timestamp });
1373
+ const insertedFrom = to;
1374
+ const insertedTo = to + Array.from(text).length;
1375
+
1376
+ // Step 2: Remap existing review state through the mapping first
1377
+ const reviewState = remapReviewStateAfterContentChange(
1378
+ state,
1379
+ result.document,
1380
+ result.mapping,
1381
+ );
1382
+
1383
+ // Step 3: Create deletion revision for the selected range (text stays in place).
1384
+ // Deletion range uses pre-mapping positions since content was not removed.
1385
+ const deletionRevision = createAuthoredRevision(
1386
+ reviewState.document.review.revisions,
1387
+ "deletion",
1388
+ from,
1389
+ to,
1390
+ authorId,
1391
+ context.timestamp,
1392
+ );
1393
+
1394
+ // Step 4: Create insertion revision for the new text (positions already correct)
1395
+ const insertionRevision = createAuthoredRevision(
1396
+ {
1397
+ ...reviewState.document.review.revisions,
1398
+ [deletionRevision.changeId]: deletionRevision,
1399
+ },
1400
+ "insertion",
1401
+ insertedFrom,
1402
+ insertedTo,
1403
+ authorId,
1404
+ context.timestamp,
1405
+ );
1406
+
1407
+ const finalDocument: CanonicalDocumentEnvelope = {
1408
+ ...reviewState.document,
1409
+ review: {
1410
+ ...reviewState.document.review,
1411
+ revisions: {
1412
+ ...reviewState.document.review.revisions,
1413
+ [deletionRevision.changeId]: deletionRevision,
1414
+ [insertionRevision.changeId]: insertionRevision,
1415
+ },
1416
+ },
1417
+ };
1418
+
1419
+ // Move cursor to after inserted text
1420
+ const caretPos = insertedTo;
1421
+
1422
+ return createTransaction(
1423
+ {
1424
+ ...state,
1425
+ document: finalDocument,
1426
+ selection: createSelectionSnapshot(caretPos, caretPos),
1427
+ warnings: reviewState.warnings,
1428
+ runtime: {
1429
+ ...state.runtime,
1430
+ activeCommentId: reviewState.activeCommentId,
1431
+ },
1432
+ },
1433
+ {
1434
+ historyBoundary: "push",
1435
+ markDirty: true,
1436
+ mapping: result.mapping,
1437
+ effects: {
1438
+ ...reviewState.effects,
1439
+ revisionAuthored: { changeId: insertionRevision.changeId, kind: "insertion" },
1440
+ },
1441
+ },
1442
+ );
1443
+ }
1444
+
1445
+ function applySuggestingDelete(
1446
+ state: EditorState,
1447
+ direction: "backward" | "forward",
1448
+ context: CommandExecutionContext,
1449
+ ): EditorTransaction | undefined {
1450
+ if (state.readOnly) {
1451
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1452
+ }
1453
+
1454
+ const authorId = context.defaultAuthorId ?? "unknown";
1455
+ const selection = state.selection;
1456
+ const from = Math.min(selection.anchor, selection.head);
1457
+ const to = Math.max(selection.anchor, selection.head);
1458
+ const isCollapsed = from === to;
1459
+
1460
+ if (!isCollapsed && !isSingleParagraphSuggestingRange(state.document, from, to)) {
1461
+ return createSuggestingUnsupportedTransaction(
1462
+ state,
1463
+ "Suggesting mode does not yet support multi-paragraph deletion ranges.",
1464
+ );
1465
+ }
1466
+
1467
+ let deleteFrom: number;
1468
+ let deleteTo: number;
1469
+
1470
+ if (isCollapsed) {
1471
+ if (direction === "backward") {
1472
+ deleteFrom = Math.max(0, from - 1);
1473
+ deleteTo = from;
1474
+ } else {
1475
+ deleteFrom = from;
1476
+ deleteTo = from + 1;
1477
+ }
1478
+ } else {
1479
+ deleteFrom = from;
1480
+ deleteTo = to;
1481
+ }
1482
+
1483
+ // Validate the deletion range doesn't cross protected content
1484
+ if (deleteFrom === deleteTo) {
1485
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1486
+ }
1487
+
1488
+ // Check if the range overlaps an existing deletion revision from this suggesting session
1489
+ // If so, extend the existing revision rather than creating a new one
1490
+ const existingDeletion = findOverlappingAuthoredDeletion(
1491
+ state.document.review.revisions,
1492
+ deleteFrom,
1493
+ deleteTo,
1494
+ );
1495
+
1496
+ if (existingDeletion) {
1497
+ // Extend the existing deletion revision to cover the new range
1498
+ const extendedFrom = Math.min(
1499
+ existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.range.from : deleteFrom,
1500
+ deleteFrom,
1501
+ );
1502
+ const extendedTo = Math.max(
1503
+ existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.range.to : deleteTo,
1504
+ deleteTo,
1505
+ );
1506
+
1507
+ const updatedRevision: CanonicalRevisionRecord = {
1508
+ ...existingDeletion,
1509
+ anchor: {
1510
+ kind: "range",
1511
+ range: { from: extendedFrom, to: extendedTo },
1512
+ assoc: { start: 1, end: -1 },
1513
+ },
1514
+ };
1515
+
1516
+ const nextDocument: CanonicalDocumentEnvelope = {
1517
+ ...state.document,
1518
+ updatedAt: context.timestamp,
1519
+ review: {
1520
+ ...state.document.review,
1521
+ revisions: {
1522
+ ...state.document.review.revisions,
1523
+ [updatedRevision.changeId]: updatedRevision,
1524
+ },
1525
+ },
1526
+ };
1527
+
1528
+ const caretPos = direction === "backward" ? deleteFrom : deleteTo;
1529
+
1530
+ return createTransaction(
1531
+ {
1532
+ ...state,
1533
+ document: nextDocument,
1534
+ selection: createSelectionSnapshot(caretPos, caretPos),
1535
+ },
1536
+ {
1537
+ historyBoundary: "push",
1538
+ markDirty: true,
1539
+ effects: {
1540
+ revisionAuthored: { changeId: updatedRevision.changeId, kind: "deletion" },
1541
+ },
1542
+ },
1543
+ );
1544
+ }
1545
+
1546
+ // Create a new deletion revision — text stays in the document
1547
+ const revision = createAuthoredRevision(
1548
+ state.document.review.revisions,
1549
+ "deletion",
1550
+ deleteFrom,
1551
+ deleteTo,
1552
+ authorId,
1553
+ context.timestamp,
1554
+ );
1555
+
1556
+ const nextDocument: CanonicalDocumentEnvelope = {
1557
+ ...state.document,
1558
+ updatedAt: context.timestamp,
1559
+ review: {
1560
+ ...state.document.review,
1561
+ revisions: {
1562
+ ...state.document.review.revisions,
1563
+ [revision.changeId]: revision,
1564
+ },
1565
+ },
1566
+ };
1567
+
1568
+ const caretPos = direction === "backward" ? deleteFrom : deleteTo;
1569
+
1570
+ return createTransaction(
1571
+ {
1572
+ ...state,
1573
+ document: nextDocument,
1574
+ selection: createSelectionSnapshot(caretPos, caretPos),
1575
+ },
1576
+ {
1577
+ historyBoundary: "push",
1578
+ markDirty: true,
1579
+ effects: {
1580
+ revisionAuthored: { changeId: revision.changeId, kind: "deletion" },
1581
+ },
1582
+ },
1583
+ );
1584
+ }
1585
+
1586
+ function applySuggestingInsertUnit(
1587
+ state: EditorState,
1588
+ unitType: "tab" | "hard_break",
1589
+ context: CommandExecutionContext,
1590
+ ): EditorTransaction | undefined {
1591
+ if (state.readOnly) {
1592
+ return createTransaction(state, { historyBoundary: "skip", markDirty: false });
1593
+ }
1594
+
1595
+ const authorId = context.defaultAuthorId ?? "unknown";
1596
+ const selection = state.selection;
1597
+ const from = Math.min(selection.anchor, selection.head);
1598
+ const to = Math.max(selection.anchor, selection.head);
1599
+
1600
+ if (from !== to && !isSingleParagraphSuggestingRange(state.document, from, to)) {
1601
+ return createSuggestingUnsupportedTransaction(
1602
+ state,
1603
+ "Suggesting mode does not yet support multi-paragraph replacement ranges.",
1604
+ );
1605
+ }
1606
+
1607
+ // Insert the unit at the end of the selection (or at cursor if collapsed)
1608
+ const insertPos = to;
1609
+ const insertSelection = createSelectionSnapshot(insertPos, insertPos);
1610
+ const applyFn = unitType === "tab" ? insertTab : insertHardBreak;
1611
+ const result = applyFn(state.document, insertSelection, { timestamp: context.timestamp });
1612
+
1613
+ // Remap existing review state first, before adding new revisions
1614
+ const reviewState = remapReviewStateAfterContentChange(
1615
+ state,
1616
+ result.document,
1617
+ result.mapping,
1618
+ );
1619
+
1620
+ // If non-collapsed, mark selected range as deletion (positions are pre-mapping, content preserved)
1621
+ const deletionRevision = from !== to
1622
+ ? createAuthoredRevision(
1623
+ reviewState.document.review.revisions,
1624
+ "deletion",
1625
+ from,
1626
+ to,
1627
+ authorId,
1628
+ context.timestamp,
1629
+ )
1630
+ : undefined;
1631
+
1632
+ // Create insertion revision for the single inserted unit (positions already correct)
1633
+ const insertionRevision = createAuthoredRevision(
1634
+ {
1635
+ ...reviewState.document.review.revisions,
1636
+ ...(deletionRevision ? { [deletionRevision.changeId]: deletionRevision } : {}),
1637
+ },
1638
+ "insertion",
1639
+ insertPos,
1640
+ insertPos + 1,
1641
+ authorId,
1642
+ context.timestamp,
1643
+ );
1644
+
1645
+ const finalDocument: CanonicalDocumentEnvelope = {
1646
+ ...reviewState.document,
1647
+ review: {
1648
+ ...reviewState.document.review,
1649
+ revisions: {
1650
+ ...reviewState.document.review.revisions,
1651
+ ...(deletionRevision ? { [deletionRevision.changeId]: deletionRevision } : {}),
1652
+ [insertionRevision.changeId]: insertionRevision,
1653
+ },
1654
+ },
1655
+ };
1656
+
1657
+ return createTransaction(
1658
+ {
1659
+ ...state,
1660
+ document: finalDocument,
1661
+ selection: result.selection,
1662
+ warnings: reviewState.warnings,
1663
+ runtime: {
1664
+ ...state.runtime,
1665
+ activeCommentId: reviewState.activeCommentId,
1666
+ },
1667
+ },
1668
+ {
1669
+ historyBoundary: "push",
1670
+ markDirty: true,
1671
+ mapping: result.mapping,
1672
+ effects: {
1673
+ ...reviewState.effects,
1674
+ revisionAuthored: { changeId: insertionRevision.changeId, kind: "insertion" },
1675
+ },
1676
+ },
1677
+ );
1678
+ }
1679
+
1680
+ function findOverlappingAuthoredDeletion(
1681
+ revisions: Record<string, CanonicalRevisionRecord>,
1682
+ from: number,
1683
+ to: number,
1684
+ ): CanonicalRevisionRecord | undefined {
1685
+ for (const revision of Object.values(revisions)) {
1686
+ if (
1687
+ revision.kind === "deletion" &&
1688
+ revision.status === "open" &&
1689
+ revision.metadata?.source === "runtime" &&
1690
+ revision.anchor.kind === "range"
1691
+ ) {
1692
+ const revFrom = revision.anchor.range.from;
1693
+ const revTo = revision.anchor.range.to;
1694
+ // Adjacent or overlapping: the deletion is directly next to or overlapping the range
1695
+ if (
1696
+ (from >= revFrom && from <= revTo) ||
1697
+ (to >= revFrom && to <= revTo) ||
1698
+ (from === revTo) ||
1699
+ (to === revFrom)
1700
+ ) {
1701
+ return revision;
1702
+ }
1703
+ }
1704
+ }
1705
+ return undefined;
1706
+ }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export type {
11
11
  EditorSessionState,
12
12
  EditorHostAdapter,
13
13
  WordReviewEditorProps,
14
+ WordReviewEditorChromeVisibility,
14
15
  WordReviewEditorRef,
15
16
  EditorUser,
16
17
  EditorDatastoreAdapter,
@@ -93,4 +94,8 @@ export type {
93
94
  WorkflowMarkupItem,
94
95
  WorkflowMarkupSnapshot,
95
96
  WorkflowCandidateRangeOptions,
97
+ HostAnnotationKind,
98
+ HostAnnotationItem,
99
+ HostAnnotationOverlay,
100
+ HostAnnotationSnapshot,
96
101
  } from "./api/public-types.ts";