@beyondwork/docx-react-component 1.0.82 → 1.0.84

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.
@@ -393,6 +393,8 @@ export function __createWordReviewEditorRefBridge(
393
393
  addInvisibleScope: (params) => runtime.addInvisibleScope(params),
394
394
  setScopeVisibility: (scopeId, visibility) => runtime.setScopeVisibility(scopeId, visibility),
395
395
  getScopeVisibility: (scopeId) => runtime.getScopeVisibility(scopeId),
396
+ setScopeGuardPolicy: (scopeId, guardPolicy) => runtime.setScopeGuardPolicy(scopeId, guardPolicy),
397
+ getScopeGuardPolicy: (scopeId) => runtime.getScopeGuardPolicy(scopeId),
396
398
  setScopeChromeVisibility: (state) => runtime.setScopeChromeVisibility(state),
397
399
  getScopeChromeVisibility: () => runtime.getScopeChromeVisibility(),
398
400
  subscribeToScopeQuery: (filter, callback) => runtime.subscribeToScopeQuery(filter, callback),
@@ -1654,6 +1656,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1654
1656
  addInvisibleScope: (params) => activeRuntime.addInvisibleScope(params),
1655
1657
  setScopeVisibility: (scopeId, visibility) => activeRuntime.setScopeVisibility(scopeId, visibility),
1656
1658
  getScopeVisibility: (scopeId) => activeRuntime.getScopeVisibility(scopeId),
1659
+ setScopeGuardPolicy: (scopeId, guardPolicy) => activeRuntime.setScopeGuardPolicy(scopeId, guardPolicy),
1660
+ getScopeGuardPolicy: (scopeId) => activeRuntime.getScopeGuardPolicy(scopeId),
1657
1661
  setScopeChromeVisibility: (state) => activeRuntime.setScopeChromeVisibility(state),
1658
1662
  getScopeChromeVisibility: () => activeRuntime.getScopeChromeVisibility(),
1659
1663
  subscribeToScopeQuery: (filter, callback) => activeRuntime.subscribeToScopeQuery(filter, callback),
@@ -2414,9 +2418,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2414
2418
  function addReviewComment(): string | null {
2415
2419
  try {
2416
2420
  const { commentId } = activeRuntime.addComment({
2417
- anchor: snapshot.selection.activeRange,
2421
+ anchor: resolveCommentCommandAnchor(snapshot),
2418
2422
  body: "",
2419
2423
  authorId: currentUser.userId,
2424
+ snapToSafeBoundary: true,
2420
2425
  });
2421
2426
  activeRuntime.openComment(commentId);
2422
2427
  setActiveRailTab("comments");
@@ -4192,6 +4197,176 @@ function summarizeSelectionPreview(snapshot: RuntimeRenderSnapshot): string | nu
4192
4197
  return preview.length > 48 ? `${preview.slice(0, 45)}...` : preview;
4193
4198
  }
4194
4199
 
4200
+ function resolveCommentCommandAnchor(
4201
+ snapshot: RuntimeRenderSnapshot,
4202
+ ): PublicSelectionSnapshot["activeRange"] {
4203
+ const selection = snapshot.selection;
4204
+ if (!selection.isCollapsed && selection.activeRange.kind === "range") {
4205
+ return selection.activeRange;
4206
+ }
4207
+
4208
+ const collapsedRange = resolveCollapsedCommentRange(snapshot.surface, selection);
4209
+ return collapsedRange
4210
+ ? {
4211
+ kind: "range",
4212
+ from: collapsedRange.from,
4213
+ to: collapsedRange.to,
4214
+ assoc: { start: -1, end: 1 },
4215
+ }
4216
+ : selection.activeRange;
4217
+ }
4218
+
4219
+ function resolveCollapsedCommentRange(
4220
+ surface: RuntimeRenderSnapshot["surface"],
4221
+ selection: RuntimeRenderSnapshot["selection"],
4222
+ ): { from: number; to: number } | null {
4223
+ if (!surface) {
4224
+ return null;
4225
+ }
4226
+
4227
+ const position = selection.activeRange.kind === "node"
4228
+ ? selection.activeRange.at
4229
+ : selection.head;
4230
+ const paragraph =
4231
+ findParagraphRangeAtPosition(surface.blocks, position) ??
4232
+ findFirstNonEmptyParagraphRange(surface.blocks);
4233
+ return paragraph
4234
+ ? resolveWordRangeInsideParagraph(surface.plainText, paragraph, position)
4235
+ : null;
4236
+ }
4237
+
4238
+ function findParagraphRangeAtPosition(
4239
+ blocks: readonly SurfaceBlockSnapshot[],
4240
+ position: number,
4241
+ ): { from: number; to: number } | null {
4242
+ for (const block of blocks) {
4243
+ if (
4244
+ block.kind === "paragraph" &&
4245
+ block.to > block.from &&
4246
+ position >= block.from &&
4247
+ position <= block.to
4248
+ ) {
4249
+ return { from: block.from, to: block.to };
4250
+ }
4251
+ const nested = findParagraphRangeInNestedBlocks(block, position);
4252
+ if (nested) {
4253
+ return nested;
4254
+ }
4255
+ }
4256
+ return null;
4257
+ }
4258
+
4259
+ function findFirstNonEmptyParagraphRange(
4260
+ blocks: readonly SurfaceBlockSnapshot[],
4261
+ ): { from: number; to: number } | null {
4262
+ for (const block of blocks) {
4263
+ if (block.kind === "paragraph" && block.to > block.from) {
4264
+ return { from: block.from, to: block.to };
4265
+ }
4266
+ const nested = findFirstNonEmptyParagraphInNestedBlocks(block);
4267
+ if (nested) {
4268
+ return nested;
4269
+ }
4270
+ }
4271
+ return null;
4272
+ }
4273
+
4274
+ function findParagraphRangeInNestedBlocks(
4275
+ block: SurfaceBlockSnapshot,
4276
+ position: number,
4277
+ ): { from: number; to: number } | null {
4278
+ if (block.kind === "sdt_block") {
4279
+ return findParagraphRangeAtPosition(block.children, position);
4280
+ }
4281
+ if (block.kind === "table") {
4282
+ for (const row of block.rows) {
4283
+ for (const cell of row.cells) {
4284
+ const nested = findParagraphRangeAtPosition(cell.content, position);
4285
+ if (nested) {
4286
+ return nested;
4287
+ }
4288
+ }
4289
+ }
4290
+ }
4291
+ return null;
4292
+ }
4293
+
4294
+ function findFirstNonEmptyParagraphInNestedBlocks(
4295
+ block: SurfaceBlockSnapshot,
4296
+ ): { from: number; to: number } | null {
4297
+ if (block.kind === "sdt_block") {
4298
+ return findFirstNonEmptyParagraphRange(block.children);
4299
+ }
4300
+ if (block.kind === "table") {
4301
+ for (const row of block.rows) {
4302
+ for (const cell of row.cells) {
4303
+ const nested = findFirstNonEmptyParagraphRange(cell.content);
4304
+ if (nested) {
4305
+ return nested;
4306
+ }
4307
+ }
4308
+ }
4309
+ }
4310
+ return null;
4311
+ }
4312
+
4313
+ function resolveWordRangeInsideParagraph(
4314
+ plainText: string,
4315
+ paragraph: { from: number; to: number },
4316
+ position: number,
4317
+ ): { from: number; to: number } {
4318
+ const paragraphFrom = paragraph.from;
4319
+ const paragraphTo = paragraph.to;
4320
+ const fallback = { from: paragraphFrom, to: paragraphTo };
4321
+ if (paragraphTo <= paragraphFrom) {
4322
+ return fallback;
4323
+ }
4324
+
4325
+ let cursor = Math.max(paragraphFrom, Math.min(position, paragraphTo - 1));
4326
+ if (isCommentWordBoundary(plainText.charAt(cursor))) {
4327
+ const next = findNearestCommentTextOffset(plainText, cursor, paragraphFrom, paragraphTo);
4328
+ if (next === null) {
4329
+ return fallback;
4330
+ }
4331
+ cursor = next;
4332
+ }
4333
+
4334
+ let from = cursor;
4335
+ while (from > paragraphFrom && !isCommentWordBoundary(plainText.charAt(from - 1))) {
4336
+ from -= 1;
4337
+ }
4338
+
4339
+ let to = cursor + 1;
4340
+ while (to < paragraphTo && !isCommentWordBoundary(plainText.charAt(to))) {
4341
+ to += 1;
4342
+ }
4343
+
4344
+ return to > from ? { from, to } : fallback;
4345
+ }
4346
+
4347
+ function findNearestCommentTextOffset(
4348
+ plainText: string,
4349
+ cursor: number,
4350
+ from: number,
4351
+ to: number,
4352
+ ): number | null {
4353
+ for (let distance = 1; distance < to - from; distance += 1) {
4354
+ const right = cursor + distance;
4355
+ if (right < to && !isCommentWordBoundary(plainText.charAt(right))) {
4356
+ return right;
4357
+ }
4358
+ const left = cursor - distance;
4359
+ if (left >= from && !isCommentWordBoundary(plainText.charAt(left))) {
4360
+ return left;
4361
+ }
4362
+ }
4363
+ return null;
4364
+ }
4365
+
4366
+ function isCommentWordBoundary(value: string): boolean {
4367
+ return value.length === 0 || /\s/.test(value);
4368
+ }
4369
+
4195
4370
  function selectionToolbarAnchorsEqual(
4196
4371
  left: SelectionToolbarAnchor | null,
4197
4372
  right: SelectionToolbarAnchor | null,
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
 
3
3
  import type {
4
4
  ActiveListContext,
5
+ EditorStoryTarget,
5
6
  EditorViewStateSnapshot,
6
7
  InteractionGuardSnapshot,
7
8
  WordReviewEditorChromePreset,
@@ -24,6 +25,7 @@ export interface UseChromePolicyOptions {
24
25
  role: EditorViewStateSnapshot["editorRole"];
25
26
  hasSidebarPanelAccess: boolean;
26
27
  effectiveSelectionMode: ToolbarInteractionPolicy["mode"];
28
+ activeStoryKind?: EditorStoryTarget["kind"];
27
29
  }
28
30
 
29
31
  export interface ChromePolicy {
@@ -52,8 +54,22 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
52
54
  role,
53
55
  hasSidebarPanelAccess,
54
56
  effectiveSelectionMode,
57
+ activeStoryKind,
55
58
  } = options;
56
59
 
60
+ const canRunCommentCommand = resolveTopBarCommentAvailability({
61
+ caps,
62
+ effectiveSelectionMode,
63
+ activeStoryKind,
64
+ });
65
+ const chromeCaps = useMemo(
66
+ () =>
67
+ caps && caps.canAddComment !== canRunCommentCommand
68
+ ? { ...caps, canAddComment: canRunCommentCommand }
69
+ : caps,
70
+ [canRunCommentCommand, caps],
71
+ );
72
+
57
73
  const responsiveChrome = useMemo(
58
74
  () =>
59
75
  resolveResponsiveChromeState({
@@ -69,7 +85,7 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
69
85
  resolveScopedChromePolicy({
70
86
  preset: chromePreset,
71
87
  compactMode: responsiveChrome.isNarrow,
72
- capabilities: caps,
88
+ capabilities: chromeCaps,
73
89
  interactionGuardSnapshot,
74
90
  workflowScopeSnapshot,
75
91
  activeListContext,
@@ -77,7 +93,7 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
77
93
  hasSidebarPanelAccess,
78
94
  }),
79
95
  [
80
- caps,
96
+ chromeCaps,
81
97
  chromePreset,
82
98
  hasSidebarPanelAccess,
83
99
  activeListContext,
@@ -93,12 +109,22 @@ export function useChromePolicy(options: UseChromePolicyOptions): ChromePolicy {
93
109
  mode: effectiveSelectionMode,
94
110
  canFormatText: caps.canEdit && effectiveSelectionMode === "edit",
95
111
  canInsertStructural: caps.canEdit && effectiveSelectionMode === "edit",
96
- canAddComment:
97
- caps.canAddComment &&
98
- effectiveSelectionMode !== "view" &&
99
- effectiveSelectionMode !== "blocked",
112
+ canAddComment: canRunCommentCommand,
100
113
  }
101
114
  : undefined;
102
115
 
103
116
  return { responsiveChrome, scopedChromePolicy, toolbarInteractionPolicy };
104
117
  }
118
+
119
+ export function resolveTopBarCommentAvailability(input: {
120
+ caps: SessionCapabilities | undefined;
121
+ effectiveSelectionMode: ToolbarInteractionPolicy["mode"];
122
+ activeStoryKind?: EditorStoryTarget["kind"];
123
+ }): boolean {
124
+ const { caps, effectiveSelectionMode, activeStoryKind } = input;
125
+ if (!caps) return false;
126
+ if (caps.phase !== "ready" || caps.hasFatalError) return false;
127
+ if (caps.documentMode === "viewing") return false;
128
+ if (activeStoryKind !== undefined && activeStoryKind !== "main") return false;
129
+ return effectiveSelectionMode !== "view" && effectiveSelectionMode !== "blocked";
130
+ }
@@ -139,6 +139,7 @@ export function TwRoleActionRegion(
139
139
  for (const id of order) {
140
140
  const entry = props.policy.toolbar[id];
141
141
  if (!entry?.visible) continue;
142
+ if (!isRoleActionRenderable(id, props)) continue;
142
143
  if (entry.placement === "overflow") {
143
144
  overflowIds.push(id);
144
145
  } else if (entry.placement === "inline") {
@@ -171,6 +172,26 @@ export function TwRoleActionRegion(
171
172
  );
172
173
  }
173
174
 
175
+ function isRoleActionRenderable(
176
+ id: ToolbarChromeItemId,
177
+ props: TwRoleActionRegionProps,
178
+ ): boolean {
179
+ const reviewQueueTotal = props.reviewQueue?.totalCount ?? 0;
180
+ switch (id) {
181
+ case "review-queue-prev":
182
+ case "review-queue-next":
183
+ case "review-queue-counts":
184
+ case "review-queue-active-label":
185
+ case "review-accept":
186
+ case "review-reject":
187
+ case "review-accept-all":
188
+ case "review-reject-all":
189
+ return reviewQueueTotal > 0;
190
+ default:
191
+ return true;
192
+ }
193
+ }
194
+
174
195
  interface RoleActionButtonProps {
175
196
  id: ToolbarChromeItemId;
176
197
  compact: boolean;
@@ -357,6 +357,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
357
357
  role: viewState.editorRole,
358
358
  hasSidebarPanelAccess,
359
359
  effectiveSelectionMode,
360
+ activeStoryKind: snapshot.activeStory.kind,
360
361
  });
361
362
 
362
363
  // L7 Phase 2 Task 2.2.4a — viewport-scroll wiring. Page marker