@beyondwork/docx-react-component 1.0.24-rc → 1.0.24

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.24rc",
4
+ "version": "1.0.24",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -1398,7 +1398,6 @@ export interface WordReviewEditorProps {
1398
1398
  loadSourcePolicy?: LoadSourcePolicy;
1399
1399
  readOnly?: boolean;
1400
1400
  reviewMode?: "editing" | "review";
1401
- suggestionsEnabled?: boolean;
1402
1401
  markupDisplay?: "clean" | "simple" | "all";
1403
1402
  showReviewPanel?: boolean;
1404
1403
  chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
@@ -1309,7 +1309,7 @@ function applySuggestingInsert(
1309
1309
  }
1310
1310
 
1311
1311
  if (isCollapsed) {
1312
- // Pure insertion at cursor: apply normally, then create or extend an insertion revision.
1312
+ // Pure insertion at cursor: apply normally, then create insertion revision
1313
1313
  const result = insertText(state.document, selection, text, { timestamp: context.timestamp });
1314
1314
  const insertedFrom = from;
1315
1315
  const insertedTo = from + Array.from(text).length;
@@ -1321,53 +1321,25 @@ function applySuggestingInsert(
1321
1321
  result.mapping,
1322
1322
  );
1323
1323
 
1324
- // If there is an authored insertion revision that ends exactly where this
1325
- // text was inserted (i.e. the cursor was at the end of a prior insertion),
1326
- // extend that revision to cover the new text rather than creating a new one.
1327
- // This groups consecutive keystrokes into a single tracked change.
1328
- const adjacentInsertion = findAdjacentAuthoredInsertion(
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(
1329
1327
  reviewState.document.review.revisions,
1328
+ "insertion",
1330
1329
  insertedFrom,
1330
+ insertedTo,
1331
+ authorId,
1332
+ context.timestamp,
1331
1333
  );
1332
1334
 
1333
- let finalRevisionId: string;
1334
- let finalRevisions: Record<string, CanonicalRevisionRecord>;
1335
-
1336
- if (adjacentInsertion && adjacentInsertion.anchor.kind === "range") {
1337
- const extended: CanonicalRevisionRecord = {
1338
- ...adjacentInsertion,
1339
- anchor: {
1340
- kind: "range",
1341
- range: { from: adjacentInsertion.anchor.range.from, to: insertedTo },
1342
- assoc: { start: 1, end: -1 },
1343
- },
1344
- };
1345
- finalRevisionId = extended.changeId;
1346
- finalRevisions = {
1347
- ...reviewState.document.review.revisions,
1348
- [extended.changeId]: extended,
1349
- };
1350
- } else {
1351
- const revision = createAuthoredRevision(
1352
- reviewState.document.review.revisions,
1353
- "insertion",
1354
- insertedFrom,
1355
- insertedTo,
1356
- authorId,
1357
- context.timestamp,
1358
- );
1359
- finalRevisionId = revision.changeId;
1360
- finalRevisions = {
1361
- ...reviewState.document.review.revisions,
1362
- [revision.changeId]: revision,
1363
- };
1364
- }
1365
-
1366
1335
  const finalDocument: CanonicalDocumentEnvelope = {
1367
1336
  ...reviewState.document,
1368
1337
  review: {
1369
1338
  ...reviewState.document.review,
1370
- revisions: finalRevisions,
1339
+ revisions: {
1340
+ ...reviewState.document.review.revisions,
1341
+ [revision.changeId]: revision,
1342
+ },
1371
1343
  },
1372
1344
  };
1373
1345
 
@@ -1388,7 +1360,7 @@ function applySuggestingInsert(
1388
1360
  mapping: result.mapping,
1389
1361
  effects: {
1390
1362
  ...reviewState.effects,
1391
- revisionAuthored: { changeId: finalRevisionId, kind: "insertion" },
1363
+ revisionAuthored: { changeId: revision.changeId, kind: "insertion" },
1392
1364
  },
1393
1365
  },
1394
1366
  );
@@ -1732,26 +1704,3 @@ function findOverlappingAuthoredDeletion(
1732
1704
  }
1733
1705
  return undefined;
1734
1706
  }
1735
-
1736
- /**
1737
- * Find an open authored insertion revision whose end position equals `cursorAt`.
1738
- * Used to extend an existing insertion when the user keeps typing at the same
1739
- * position rather than creating a new revision for every character.
1740
- */
1741
- function findAdjacentAuthoredInsertion(
1742
- revisions: Record<string, CanonicalRevisionRecord>,
1743
- cursorAt: number,
1744
- ): CanonicalRevisionRecord | undefined {
1745
- for (const revision of Object.values(revisions)) {
1746
- if (
1747
- revision.kind === "insertion" &&
1748
- revision.status === "open" &&
1749
- revision.metadata?.source === "runtime" &&
1750
- revision.anchor.kind === "range" &&
1751
- revision.anchor.range.to === cursorAt
1752
- ) {
1753
- return revision;
1754
- }
1755
- }
1756
- return undefined;
1757
- }
@@ -520,7 +520,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
520
520
  onWarning,
521
521
  readOnly = false,
522
522
  reviewMode = "review",
523
- suggestionsEnabled = false,
524
523
  showReviewPanel = true,
525
524
  chromeVisibility,
526
525
  } = props;
@@ -694,10 +693,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
694
693
  activeRuntime.setViewMode(effectiveViewMode);
695
694
  }, [activeRuntime, effectiveViewMode]);
696
695
 
697
- useEffect(() => {
698
- activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
699
- }, [activeRuntime, suggestionsEnabled]);
700
-
701
696
  useEffect(() => {
702
697
  runtimeViewStateSeedRef.current = {
703
698
  workspaceMode: viewState.workspaceMode,
@@ -1712,7 +1707,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1712
1707
  markupDisplay={liveMarkupDisplay}
1713
1708
  activeRevisionId={activeRevisionId}
1714
1709
  showTrackedChanges={showTrackedChanges}
1715
- suggestionsEnabled={suggestionsEnabled}
1716
1710
  mediaPreviews={mediaPreviews}
1717
1711
  isPageWorkspace={isPageWorkspace}
1718
1712
  workflowScopes={workflowScopeSnapshot?.scopes}
@@ -27,7 +27,6 @@ export interface EditorSurfaceControllerProps {
27
27
  markupDisplay: MarkupDisplay;
28
28
  activeRevisionId?: string;
29
29
  showTrackedChanges?: boolean;
30
- suggestionsEnabled?: boolean;
31
30
  mediaPreviews?: Record<string, MediaPreviewDescriptor>;
32
31
  isPageWorkspace?: boolean;
33
32
  onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
@@ -21,7 +21,22 @@ type RailDecorationSpec = {
21
21
  attrs: Record<string, string>;
22
22
  };
23
23
 
24
- function getWorkflowInlineClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
24
+ function isSelectionZoneScope(scope: WorkflowScope): boolean {
25
+ return (
26
+ scope.scopeId.startsWith("scope-lab-") ||
27
+ scope.scopeId.startsWith("metadata-lab-") ||
28
+ scope.workItemId?.startsWith("scope-lab-") === true ||
29
+ scope.workItemId?.startsWith("metadata-lab-") === true ||
30
+ scope.label?.toLowerCase().includes("scope lab") === true ||
31
+ scope.label?.toLowerCase().includes("metadata lab") === true
32
+ );
33
+ }
34
+
35
+ function getWorkflowInlineClass(
36
+ scope: WorkflowScope,
37
+ isActiveWorkItem: boolean,
38
+ isSelectionZone: boolean,
39
+ ): string {
25
40
  const base =
26
41
  scope.mode === "edit"
27
42
  ? "wre-workflow-inline wre-workflow-inline-edit"
@@ -30,10 +45,15 @@ function getWorkflowInlineClass(scope: WorkflowScope, isActiveWorkItem: boolean)
30
45
  : scope.mode === "comment"
31
46
  ? "wre-workflow-inline wre-workflow-inline-comment"
32
47
  : "wre-workflow-inline wre-workflow-inline-view";
33
- return isActiveWorkItem ? `${base} wre-workflow-inline-active` : base;
48
+ const withZone = isSelectionZone ? `${base} wre-workflow-inline-zone` : base;
49
+ return isActiveWorkItem ? `${withZone} wre-workflow-inline-active` : withZone;
34
50
  }
35
51
 
36
- function getWorkflowRailClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
52
+ function getWorkflowRailClass(
53
+ scope: WorkflowScope,
54
+ isActiveWorkItem: boolean,
55
+ isSelectionZone: boolean,
56
+ ): string {
37
57
  const base =
38
58
  scope.mode === "edit"
39
59
  ? "wre-workflow-rail wre-workflow-rail-edit"
@@ -42,7 +62,8 @@ function getWorkflowRailClass(scope: WorkflowScope, isActiveWorkItem: boolean):
42
62
  : scope.mode === "comment"
43
63
  ? "wre-workflow-rail wre-workflow-rail-comment"
44
64
  : "wre-workflow-rail wre-workflow-rail-view";
45
- return isActiveWorkItem ? `${base} wre-workflow-rail-active` : base;
65
+ const withZone = isSelectionZone ? `${base} wre-workflow-rail-selection-zone` : base;
66
+ return isActiveWorkItem ? `${withZone} wre-workflow-rail-active` : withZone;
46
67
  }
47
68
 
48
69
  function getWorkflowCandidateInlineClass(): string {
@@ -190,13 +211,10 @@ export function buildDecorations(
190
211
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
191
212
  activeWorkflowWorkItemId?: string | null,
192
213
  activeWorkflowScopeIds?: readonly string[],
193
- suggestionsEnabled = false,
194
214
  ): DecorationSet {
195
215
  const decorations: Decoration[] = [];
196
216
  const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
197
217
  const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
198
- // In suggestions mode, tracked changes are always shown regardless of the toggle.
199
- const effectiveShowTracked = suggestionsEnabled ? true : showTrackedChanges;
200
218
 
201
219
  // Walk comment threads and create inline decorations
202
220
  if (commentModel) {
@@ -230,7 +248,7 @@ export function buildDecorations(
230
248
  // Always hide deletions in clean mode (final-text semantics).
231
249
  // This is the critical behavior: "hide tracked changes" must show
232
250
  // the document as if accepted, not show deleted text as kept text.
233
- if (markupDisplay === "clean" && rev.kind === "deletion" && !suggestionsEnabled) {
251
+ if (markupDisplay === "clean" && rev.kind === "deletion") {
234
252
  const pmFrom = positionMap.runtimeToPm(rev.from);
235
253
  const pmTo = positionMap.runtimeToPm(rev.to);
236
254
  if (pmFrom < pmTo) {
@@ -245,48 +263,7 @@ export function buildDecorations(
245
263
  }
246
264
 
247
265
  // Skip visual styling when tracked changes display is off
248
- if (!effectiveShowTracked) continue;
249
-
250
- const pmFrom = positionMap.runtimeToPm(rev.from);
251
- const pmTo = positionMap.runtimeToPm(rev.to);
252
- if (pmFrom >= pmTo) continue;
253
-
254
- if (suggestionsEnabled) {
255
- if (rev.kind === "insertion") {
256
- decorations.push(
257
- Decoration.inline(pmFrom, pmTo, {
258
- class: "text-insert",
259
- "data-revision-id": rev.revisionId,
260
- }),
261
- );
262
- decorations.push(
263
- Decoration.widget(pmFrom, () => {
264
- const el = document.createElement("span");
265
- el.textContent = "[";
266
- el.className = "text-insert";
267
- el.setAttribute("contenteditable", "false");
268
- return el;
269
- }, { side: -1, key: `${rev.revisionId}-open` }),
270
- );
271
- decorations.push(
272
- Decoration.widget(pmTo, () => {
273
- const el = document.createElement("span");
274
- el.textContent = "]";
275
- el.className = "text-insert";
276
- el.setAttribute("contenteditable", "false");
277
- return el;
278
- }, { side: 1, key: `${rev.revisionId}-close` }),
279
- );
280
- } else if (rev.kind === "deletion") {
281
- decorations.push(
282
- Decoration.inline(pmFrom, pmTo, {
283
- class: "text-danger line-through decoration-danger/80 decoration-1",
284
- "data-revision-id": rev.revisionId,
285
- }),
286
- );
287
- }
288
- continue;
289
- }
266
+ if (!showTrackedChanges) continue;
290
267
 
291
268
  const cls = getRevisionHighlightClass(
292
269
  revisionModel,
@@ -296,12 +273,16 @@ export function buildDecorations(
296
273
  );
297
274
  if (!cls) continue;
298
275
 
299
- decorations.push(
300
- Decoration.inline(pmFrom, pmTo, {
301
- class: cls,
302
- "data-revision-id": rev.revisionId,
303
- }),
304
- );
276
+ const pmFrom = positionMap.runtimeToPm(rev.from);
277
+ const pmTo = positionMap.runtimeToPm(rev.to);
278
+ if (pmFrom < pmTo) {
279
+ decorations.push(
280
+ Decoration.inline(pmFrom, pmTo, {
281
+ class: cls,
282
+ "data-revision-id": rev.revisionId,
283
+ }),
284
+ );
285
+ }
305
286
  }
306
287
  }
307
288
 
@@ -311,6 +292,7 @@ export function buildDecorations(
311
292
  if (!storyTargetsEqual(scopeStoryTarget, activeStory)) continue;
312
293
  const pmRange = buildAnchorPmRange(scope.anchor, positionMap);
313
294
  if (!pmRange) continue;
295
+ const isSelectionZone = isSelectionZoneScope(scope);
314
296
  const isActiveWorkItem =
315
297
  Boolean(activeWorkflowWorkItemId) &&
316
298
  (
@@ -321,21 +303,23 @@ export function buildDecorations(
321
303
  if (pmRange.allowInline && pmRange.from < pmRange.to) {
322
304
  decorations.push(
323
305
  Decoration.inline(pmRange.from, pmRange.to, {
324
- class: getWorkflowInlineClass(scope, isActiveWorkItem),
306
+ class: getWorkflowInlineClass(scope, isActiveWorkItem, isSelectionZone),
325
307
  "data-workflow-scope-id": scope.scopeId,
326
308
  "data-workflow-scope-mode": scope.mode,
327
309
  "data-workflow-active": isActiveWorkItem ? "true" : "false",
310
+ ...(isSelectionZone ? { "data-workflow-zone": "selection" } : {}),
328
311
  }),
329
312
  );
330
313
  }
331
314
 
332
315
  pushRailDecorations(decorations, doc, pmRange.from, pmRange.to, {
333
316
  railKind: "scope",
334
- className: getWorkflowRailClass(scope, isActiveWorkItem),
317
+ className: getWorkflowRailClass(scope, isActiveWorkItem, isSelectionZone),
335
318
  attrs: {
336
319
  "data-workflow-scope-id": scope.scopeId,
337
320
  "data-workflow-scope-mode": scope.mode,
338
321
  "data-workflow-active": isActiveWorkItem ? "true" : "false",
322
+ ...(isSelectionZone ? { "data-workflow-zone": "selection" } : {}),
339
323
  },
340
324
  }, railRangeCache);
341
325
  }
@@ -43,7 +43,6 @@ export function createSurfaceDecorationKey(input: {
43
43
  workflowBlockedSignature?: string;
44
44
  activeWorkflowWorkItemId?: string | null;
45
45
  activeWorkflowScopeIds?: readonly string[];
46
- suggestionsEnabled?: boolean;
47
46
  }): string {
48
47
  return JSON.stringify({
49
48
  markupDisplay: input.markupDisplay,
@@ -56,6 +55,5 @@ export function createSurfaceDecorationKey(input: {
56
55
  workflowBlockedSignature: input.workflowBlockedSignature ?? null,
57
56
  activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
58
57
  activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
59
- suggestionsEnabled: input.suggestionsEnabled ?? false,
60
58
  });
61
59
  }
@@ -75,7 +75,6 @@ export interface TwProseMirrorSurfaceProps {
75
75
  markupDisplay: MarkupDisplay;
76
76
  activeRevisionId?: string;
77
77
  showTrackedChanges?: boolean;
78
- suggestionsEnabled?: boolean;
79
78
  /** When true, the surface renders inside the page workspace (vs canvas). */
80
79
  isPageWorkspace?: boolean;
81
80
  onFocus: FocusEventHandler<HTMLDivElement>;
@@ -205,7 +204,6 @@ export const TwProseMirrorSurface = forwardRef<
205
204
  }),
206
205
  [mediaPreviewKey, snapshot.activeStory, surface],
207
206
  );
208
- const suggestionsEnabled = props.suggestionsEnabled === true;
209
207
  const decorationBuildKey = useMemo(
210
208
  () =>
211
209
  createSurfaceDecorationKey({
@@ -219,7 +217,6 @@ export const TwProseMirrorSurface = forwardRef<
219
217
  workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
220
218
  activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
221
219
  activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
222
- suggestionsEnabled,
223
220
  }),
224
221
  [
225
222
  canEdit,
@@ -232,7 +229,6 @@ export const TwProseMirrorSurface = forwardRef<
232
229
  props.workflowScopes,
233
230
  showTrackedChanges,
234
231
  snapshot.comments.activeCommentId,
235
- suggestionsEnabled,
236
232
  ],
237
233
  );
238
234
 
@@ -278,7 +274,6 @@ export const TwProseMirrorSurface = forwardRef<
278
274
  props.workflowBlockedReasons,
279
275
  props.activeWorkflowWorkItemId,
280
276
  props.activeWorkflowScopeIds,
281
- suggestionsEnabled,
282
277
  );
283
278
  view.setProps({
284
279
  editable: () => canEdit,
@@ -261,6 +261,8 @@
261
261
  .prosemirror-surface .ProseMirror .wre-workflow-inline {
262
262
  border-radius: 0.25rem;
263
263
  box-shadow: inset 0 0 0 1px transparent;
264
+ -webkit-box-decoration-break: clone;
265
+ box-decoration-break: clone;
264
266
  }
265
267
 
266
268
  .prosemirror-surface .ProseMirror .wre-workflow-inline-edit {
@@ -333,6 +335,13 @@
333
335
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 28%, transparent);
334
336
  }
335
337
 
338
+ .prosemirror-surface .ProseMirror .wre-workflow-inline-zone {
339
+ border-radius: 0.35rem;
340
+ box-shadow:
341
+ inset 0 0 0 1px color-mix(in srgb, var(--wre-workflow-rail-color, var(--color-accent)) 18%, transparent),
342
+ inset 0 0 0 999px color-mix(in srgb, var(--wre-workflow-rail-color, var(--color-accent)) 8%, transparent);
343
+ }
344
+
336
345
  .prosemirror-surface .ProseMirror .wre-workflow-rail-edit {
337
346
  --wre-workflow-rail-color: var(--color-accent);
338
347
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
@@ -394,6 +403,15 @@
394
403
  );
395
404
  }
396
405
 
406
+ .prosemirror-surface .ProseMirror .wre-workflow-rail-selection-zone {
407
+ background: transparent;
408
+ }
409
+
410
+ .prosemirror-surface .ProseMirror .wre-workflow-rail-selection-zone::before {
411
+ width: 0.25rem;
412
+ opacity: 0.95;
413
+ }
414
+
397
415
  .prosemirror-surface:focus-visible {
398
416
  outline: none;
399
417
  }