@beyondwork/docx-react-component 1.0.36 → 1.0.38

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 (107) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -0,0 +1,501 @@
1
+ import type {
2
+ EditorRole,
3
+ WordReviewEditorChromePreset,
4
+ } from "../../api/public-types";
5
+ import type { SelectionToolKind } from "./selection-tool-types";
6
+
7
+ export type ChromeSurface = "top-toolbar" | "selection-tool";
8
+ export type ChromeDensity = "compact";
9
+ export type ToolbarChromePlacement = "inline" | "overflow" | "hidden";
10
+
11
+ export type ToolbarChromeItemId =
12
+ // Universal formatting + history
13
+ | "history"
14
+ | "text-style-selectors"
15
+ | "inline-formatting"
16
+ | "text-colors"
17
+ | "paragraph-alignment"
18
+ | "list-actions"
19
+ | "indentation"
20
+ | "list-continuation"
21
+ | "insert-actions"
22
+ | "update-actions"
23
+ | "scope-status"
24
+ | "story-breadcrumb"
25
+ | "sidebar-toggle"
26
+ | "comment"
27
+ | "tracked-changes-toggle"
28
+ | "workspace-mode"
29
+ | "zoom"
30
+ | "health"
31
+ | "export"
32
+ // R1: role-scoped action region entries
33
+ | "editor-scope-posture-menu"
34
+ | "review-queue-prev"
35
+ | "review-queue-next"
36
+ | "review-queue-counts"
37
+ | "review-queue-active-label"
38
+ | "review-accept"
39
+ | "review-reject"
40
+ | "review-accept-all"
41
+ | "review-reject-all"
42
+ | "review-markup-mode"
43
+ | "workflow-prev"
44
+ | "workflow-next"
45
+ | "workflow-mark-complete"
46
+ | "workflow-claim"
47
+ | "workflow-skip"
48
+ | "workflow-mark-blocked"
49
+ | "workflow-jump-to-scope";
50
+
51
+ export interface ChromeRegistryEntryBase {
52
+ id: string;
53
+ surfaces: ReadonlyArray<ChromeSurface>;
54
+ group: string;
55
+ }
56
+
57
+ export interface SelectionToolRegistryEntry extends ChromeRegistryEntryBase {
58
+ id: SelectionToolKind;
59
+ surfaces: ["selection-tool"];
60
+ precedence: number;
61
+ }
62
+
63
+ export interface ToolbarChromeRegistryEntry extends ChromeRegistryEntryBase {
64
+ id: ToolbarChromeItemId;
65
+ surfaces: ["top-toolbar"];
66
+ presets: ReadonlyArray<WordReviewEditorChromePreset>;
67
+ /**
68
+ * Roles in which this entry appears. Empty = appears in every role.
69
+ * R1 uses this to filter the inline action set down to the per-role
70
+ * matrix described in the chrome-phase plan.
71
+ */
72
+ roles: ReadonlyArray<EditorRole>;
73
+ fullPlacement: Exclude<ToolbarChromePlacement, "hidden">;
74
+ compactPlacement: ToolbarChromePlacement;
75
+ runtimeBehavior: "always" | "formatting" | "structure" | "comment";
76
+ scopeBehavior?: "default" | "scoped-only" | "hidden-when-scoped";
77
+ /**
78
+ * Optional per-role placement override. When a role overrides the
79
+ * placement (for instance, "text-style-selectors" is `inline` in editor
80
+ * but `overflow` in review/workflow), list the role-specific placement
81
+ * here. Missing roles inherit `fullPlacement` / `compactPlacement`.
82
+ */
83
+ rolePlacement?: Partial<
84
+ Record<EditorRole, Exclude<ToolbarChromePlacement, "hidden">>
85
+ >;
86
+ }
87
+
88
+ const ALL_ROLES: ReadonlyArray<EditorRole> = ["editor", "review", "workflow"];
89
+ const EDITOR_ONLY: ReadonlyArray<EditorRole> = ["editor"];
90
+ const REVIEW_ONLY: ReadonlyArray<EditorRole> = ["review"];
91
+ const WORKFLOW_ONLY: ReadonlyArray<EditorRole> = ["workflow"];
92
+ const EDITOR_AND_REVIEW: ReadonlyArray<EditorRole> = ["editor", "review"];
93
+
94
+ export const SELECTION_TOOL_REGISTRY: ReadonlyArray<SelectionToolRegistryEntry> = [
95
+ { id: "suggestion-review", surfaces: ["selection-tool"], group: "review", precedence: 10 },
96
+ { id: "comment-thread", surfaces: ["selection-tool"], group: "review", precedence: 20 },
97
+ { id: "workflow-task", surfaces: ["selection-tool"], group: "workflow", precedence: 30 },
98
+ { id: "structure-context", surfaces: ["selection-tool"], group: "structure", precedence: 40 },
99
+ { id: "formatting-inline", surfaces: ["selection-tool"], group: "formatting", precedence: 50 },
100
+ { id: "blocked-explainer", surfaces: ["selection-tool"], group: "blocked", precedence: 60 },
101
+ ];
102
+
103
+ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry> = [
104
+ {
105
+ id: "history",
106
+ surfaces: ["top-toolbar"],
107
+ group: "history",
108
+ presets: ["simple", "advanced", "review", "workflow"],
109
+ roles: ALL_ROLES,
110
+ fullPlacement: "inline",
111
+ compactPlacement: "inline",
112
+ runtimeBehavior: "always",
113
+ },
114
+ {
115
+ id: "text-style-selectors",
116
+ surfaces: ["top-toolbar"],
117
+ group: "text",
118
+ presets: ["advanced", "workflow"],
119
+ roles: ALL_ROLES,
120
+ fullPlacement: "inline",
121
+ compactPlacement: "overflow",
122
+ runtimeBehavior: "formatting",
123
+ rolePlacement: {
124
+ editor: "inline",
125
+ review: "overflow",
126
+ workflow: "overflow",
127
+ },
128
+ },
129
+ {
130
+ id: "inline-formatting",
131
+ surfaces: ["top-toolbar"],
132
+ group: "text",
133
+ presets: ["simple", "advanced", "review", "workflow"],
134
+ roles: ALL_ROLES,
135
+ fullPlacement: "inline",
136
+ compactPlacement: "inline",
137
+ runtimeBehavior: "formatting",
138
+ rolePlacement: {
139
+ editor: "inline",
140
+ review: "overflow",
141
+ workflow: "overflow",
142
+ },
143
+ },
144
+ {
145
+ id: "text-colors",
146
+ surfaces: ["top-toolbar"],
147
+ group: "text",
148
+ presets: ["simple", "advanced"],
149
+ roles: EDITOR_AND_REVIEW,
150
+ fullPlacement: "inline",
151
+ compactPlacement: "inline",
152
+ runtimeBehavior: "formatting",
153
+ rolePlacement: {
154
+ editor: "inline",
155
+ review: "overflow",
156
+ },
157
+ },
158
+ {
159
+ id: "paragraph-alignment",
160
+ surfaces: ["top-toolbar"],
161
+ group: "paragraph",
162
+ presets: ["simple", "advanced"],
163
+ roles: EDITOR_AND_REVIEW,
164
+ fullPlacement: "inline",
165
+ compactPlacement: "inline",
166
+ runtimeBehavior: "formatting",
167
+ rolePlacement: {
168
+ editor: "inline",
169
+ review: "overflow",
170
+ },
171
+ },
172
+ {
173
+ id: "list-actions",
174
+ surfaces: ["top-toolbar"],
175
+ group: "paragraph",
176
+ presets: ["simple", "advanced"],
177
+ roles: EDITOR_AND_REVIEW,
178
+ fullPlacement: "inline",
179
+ compactPlacement: "overflow",
180
+ runtimeBehavior: "formatting",
181
+ rolePlacement: {
182
+ editor: "inline",
183
+ review: "overflow",
184
+ },
185
+ },
186
+ {
187
+ id: "indentation",
188
+ surfaces: ["top-toolbar"],
189
+ group: "paragraph",
190
+ presets: ["simple", "advanced"],
191
+ roles: EDITOR_AND_REVIEW,
192
+ fullPlacement: "inline",
193
+ compactPlacement: "overflow",
194
+ runtimeBehavior: "formatting",
195
+ rolePlacement: {
196
+ editor: "inline",
197
+ review: "overflow",
198
+ },
199
+ },
200
+ {
201
+ id: "list-continuation",
202
+ surfaces: ["top-toolbar"],
203
+ group: "paragraph",
204
+ presets: ["simple", "advanced"],
205
+ roles: EDITOR_ONLY,
206
+ fullPlacement: "inline",
207
+ compactPlacement: "overflow",
208
+ runtimeBehavior: "formatting",
209
+ },
210
+ {
211
+ id: "insert-actions",
212
+ surfaces: ["top-toolbar"],
213
+ group: "document",
214
+ presets: ["simple", "advanced"],
215
+ roles: EDITOR_ONLY,
216
+ fullPlacement: "inline",
217
+ compactPlacement: "overflow",
218
+ runtimeBehavior: "structure",
219
+ scopeBehavior: "hidden-when-scoped",
220
+ },
221
+ {
222
+ id: "update-actions",
223
+ surfaces: ["top-toolbar"],
224
+ group: "document",
225
+ presets: ["advanced"],
226
+ roles: EDITOR_ONLY,
227
+ fullPlacement: "inline",
228
+ compactPlacement: "overflow",
229
+ runtimeBehavior: "structure",
230
+ scopeBehavior: "hidden-when-scoped",
231
+ },
232
+ {
233
+ id: "scope-status",
234
+ surfaces: ["top-toolbar"],
235
+ group: "scope",
236
+ presets: ["simple", "advanced", "review", "workflow"],
237
+ roles: ALL_ROLES,
238
+ fullPlacement: "inline",
239
+ compactPlacement: "inline",
240
+ runtimeBehavior: "always",
241
+ scopeBehavior: "scoped-only",
242
+ },
243
+ {
244
+ id: "story-breadcrumb",
245
+ surfaces: ["top-toolbar"],
246
+ group: "scope",
247
+ presets: ["simple", "advanced", "review", "workflow"],
248
+ roles: ALL_ROLES,
249
+ fullPlacement: "inline",
250
+ compactPlacement: "inline",
251
+ runtimeBehavior: "always",
252
+ },
253
+ {
254
+ id: "sidebar-toggle",
255
+ surfaces: ["top-toolbar"],
256
+ group: "review",
257
+ presets: ["simple", "advanced", "review", "workflow"],
258
+ roles: ALL_ROLES,
259
+ fullPlacement: "inline",
260
+ compactPlacement: "inline",
261
+ runtimeBehavior: "always",
262
+ },
263
+ {
264
+ id: "comment",
265
+ surfaces: ["top-toolbar"],
266
+ group: "review",
267
+ presets: ["simple", "advanced", "review", "workflow"],
268
+ roles: ALL_ROLES,
269
+ fullPlacement: "inline",
270
+ compactPlacement: "inline",
271
+ runtimeBehavior: "comment",
272
+ },
273
+ {
274
+ id: "tracked-changes-toggle",
275
+ surfaces: ["top-toolbar"],
276
+ group: "review",
277
+ // R1 promotes this toggle to every preset that shows a toolbar so the
278
+ // editor role can see inline tracked changes without switching preset.
279
+ presets: ["simple", "advanced", "review", "workflow"],
280
+ roles: ALL_ROLES,
281
+ fullPlacement: "inline",
282
+ compactPlacement: "inline",
283
+ runtimeBehavior: "always",
284
+ },
285
+ {
286
+ id: "workspace-mode",
287
+ surfaces: ["top-toolbar"],
288
+ group: "view",
289
+ presets: ["simple", "advanced", "review", "workflow"],
290
+ roles: ALL_ROLES,
291
+ fullPlacement: "inline",
292
+ compactPlacement: "inline",
293
+ runtimeBehavior: "always",
294
+ },
295
+ {
296
+ id: "zoom",
297
+ surfaces: ["top-toolbar"],
298
+ group: "view",
299
+ presets: ["simple", "advanced", "review", "workflow"],
300
+ roles: ALL_ROLES,
301
+ fullPlacement: "inline",
302
+ compactPlacement: "inline",
303
+ runtimeBehavior: "always",
304
+ },
305
+ {
306
+ id: "health",
307
+ surfaces: ["top-toolbar"],
308
+ group: "status",
309
+ presets: ["simple", "advanced", "review", "workflow"],
310
+ roles: ALL_ROLES,
311
+ fullPlacement: "inline",
312
+ compactPlacement: "inline",
313
+ runtimeBehavior: "always",
314
+ },
315
+ {
316
+ id: "export",
317
+ surfaces: ["top-toolbar"],
318
+ group: "status",
319
+ presets: ["simple", "advanced", "review", "workflow"],
320
+ roles: ALL_ROLES,
321
+ fullPlacement: "inline",
322
+ compactPlacement: "inline",
323
+ runtimeBehavior: "always",
324
+ },
325
+
326
+ // ───── R1: Editor-role primaries ──────────────────────────────────────
327
+ {
328
+ id: "editor-scope-posture-menu",
329
+ surfaces: ["top-toolbar"],
330
+ group: "scope",
331
+ presets: ["advanced", "review", "workflow"],
332
+ roles: EDITOR_ONLY,
333
+ fullPlacement: "inline",
334
+ compactPlacement: "overflow",
335
+ runtimeBehavior: "always",
336
+ },
337
+
338
+ // ───── R1: Review-role primaries ──────────────────────────────────────
339
+ {
340
+ id: "review-queue-prev",
341
+ surfaces: ["top-toolbar"],
342
+ group: "review-queue",
343
+ presets: ["review", "workflow"],
344
+ roles: REVIEW_ONLY,
345
+ fullPlacement: "inline",
346
+ compactPlacement: "inline",
347
+ runtimeBehavior: "always",
348
+ },
349
+ {
350
+ id: "review-queue-next",
351
+ surfaces: ["top-toolbar"],
352
+ group: "review-queue",
353
+ presets: ["review", "workflow"],
354
+ roles: REVIEW_ONLY,
355
+ fullPlacement: "inline",
356
+ compactPlacement: "inline",
357
+ runtimeBehavior: "always",
358
+ },
359
+ {
360
+ id: "review-queue-counts",
361
+ surfaces: ["top-toolbar"],
362
+ group: "review-queue",
363
+ presets: ["review"],
364
+ roles: REVIEW_ONLY,
365
+ fullPlacement: "inline",
366
+ compactPlacement: "overflow",
367
+ runtimeBehavior: "always",
368
+ },
369
+ {
370
+ id: "review-queue-active-label",
371
+ surfaces: ["top-toolbar"],
372
+ group: "review-queue",
373
+ presets: ["review"],
374
+ roles: REVIEW_ONLY,
375
+ fullPlacement: "inline",
376
+ compactPlacement: "overflow",
377
+ runtimeBehavior: "always",
378
+ },
379
+ {
380
+ id: "review-accept",
381
+ surfaces: ["top-toolbar"],
382
+ group: "review-action",
383
+ presets: ["review"],
384
+ roles: REVIEW_ONLY,
385
+ fullPlacement: "inline",
386
+ compactPlacement: "inline",
387
+ runtimeBehavior: "always",
388
+ },
389
+ {
390
+ id: "review-reject",
391
+ surfaces: ["top-toolbar"],
392
+ group: "review-action",
393
+ presets: ["review"],
394
+ roles: REVIEW_ONLY,
395
+ fullPlacement: "inline",
396
+ compactPlacement: "inline",
397
+ runtimeBehavior: "always",
398
+ },
399
+ {
400
+ id: "review-accept-all",
401
+ surfaces: ["top-toolbar"],
402
+ group: "review-action",
403
+ presets: ["review"],
404
+ roles: REVIEW_ONLY,
405
+ fullPlacement: "inline",
406
+ compactPlacement: "overflow",
407
+ runtimeBehavior: "always",
408
+ },
409
+ {
410
+ id: "review-reject-all",
411
+ surfaces: ["top-toolbar"],
412
+ group: "review-action",
413
+ presets: ["review"],
414
+ roles: REVIEW_ONLY,
415
+ fullPlacement: "inline",
416
+ compactPlacement: "overflow",
417
+ runtimeBehavior: "always",
418
+ },
419
+ {
420
+ id: "review-markup-mode",
421
+ surfaces: ["top-toolbar"],
422
+ group: "review-action",
423
+ presets: ["review"],
424
+ roles: REVIEW_ONLY,
425
+ fullPlacement: "inline",
426
+ compactPlacement: "inline",
427
+ runtimeBehavior: "always",
428
+ },
429
+
430
+ // ───── R1: Workflow-role primaries ────────────────────────────────────
431
+ {
432
+ id: "workflow-prev",
433
+ surfaces: ["top-toolbar"],
434
+ group: "workflow-queue",
435
+ presets: ["workflow"],
436
+ roles: WORKFLOW_ONLY,
437
+ fullPlacement: "inline",
438
+ compactPlacement: "inline",
439
+ runtimeBehavior: "always",
440
+ },
441
+ {
442
+ id: "workflow-next",
443
+ surfaces: ["top-toolbar"],
444
+ group: "workflow-queue",
445
+ presets: ["workflow"],
446
+ roles: WORKFLOW_ONLY,
447
+ fullPlacement: "inline",
448
+ compactPlacement: "inline",
449
+ runtimeBehavior: "always",
450
+ },
451
+ {
452
+ id: "workflow-mark-complete",
453
+ surfaces: ["top-toolbar"],
454
+ group: "workflow-action",
455
+ presets: ["workflow"],
456
+ roles: WORKFLOW_ONLY,
457
+ fullPlacement: "inline",
458
+ compactPlacement: "inline",
459
+ runtimeBehavior: "always",
460
+ },
461
+ {
462
+ id: "workflow-claim",
463
+ surfaces: ["top-toolbar"],
464
+ group: "workflow-action",
465
+ presets: ["workflow"],
466
+ roles: WORKFLOW_ONLY,
467
+ fullPlacement: "inline",
468
+ compactPlacement: "inline",
469
+ runtimeBehavior: "always",
470
+ },
471
+ {
472
+ id: "workflow-skip",
473
+ surfaces: ["top-toolbar"],
474
+ group: "workflow-action",
475
+ presets: ["workflow"],
476
+ roles: WORKFLOW_ONLY,
477
+ fullPlacement: "inline",
478
+ compactPlacement: "overflow",
479
+ runtimeBehavior: "always",
480
+ },
481
+ {
482
+ id: "workflow-mark-blocked",
483
+ surfaces: ["top-toolbar"],
484
+ group: "workflow-action",
485
+ presets: ["workflow"],
486
+ roles: WORKFLOW_ONLY,
487
+ fullPlacement: "inline",
488
+ compactPlacement: "overflow",
489
+ runtimeBehavior: "always",
490
+ },
491
+ {
492
+ id: "workflow-jump-to-scope",
493
+ surfaces: ["top-toolbar"],
494
+ group: "workflow-action",
495
+ presets: ["workflow"],
496
+ roles: WORKFLOW_ONLY,
497
+ fullPlacement: "inline",
498
+ compactPlacement: "overflow",
499
+ runtimeBehavior: "always",
500
+ },
501
+ ];
@@ -0,0 +1,183 @@
1
+ import type {
2
+ ActiveListContext,
3
+ EditorRole,
4
+ InteractionGuardSnapshot,
5
+ WorkflowScopeSnapshot,
6
+ WordReviewEditorChromePreset,
7
+ } from "../../api/public-types";
8
+ import type { SessionCapabilities } from "../../runtime/session-capabilities";
9
+ import {
10
+ TOOLBAR_CHROME_REGISTRY,
11
+ type ToolbarChromeItemId,
12
+ type ToolbarChromePlacement,
13
+ } from "./chrome-registry";
14
+ import type { SelectionToolKind } from "./selection-tool-types";
15
+
16
+ export interface ToolbarChromeItemPolicy {
17
+ visible: boolean;
18
+ enabled: boolean;
19
+ placement: ToolbarChromePlacement;
20
+ }
21
+
22
+ export interface ScopedChromePolicy {
23
+ density: "compact";
24
+ activeScopeId?: string;
25
+ activeWorkItemId?: string | null;
26
+ scopeStatusLabel?: string;
27
+ toolbar: Record<ToolbarChromeItemId, ToolbarChromeItemPolicy>;
28
+ selection: {
29
+ hiddenKinds: ReadonlyArray<SelectionToolKind>;
30
+ };
31
+ }
32
+
33
+ export interface ResolveScopedChromePolicyInput {
34
+ preset: WordReviewEditorChromePreset;
35
+ compactMode: boolean;
36
+ capabilities?: SessionCapabilities;
37
+ interactionGuardSnapshot?: InteractionGuardSnapshot;
38
+ workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
39
+ activeListContext?: ActiveListContext | null;
40
+ /**
41
+ * Active chrome role (spec §6.4). When supplied, the policy filters
42
+ * registry entries whose `roles` array excludes the active role. When
43
+ * omitted, the preset filter alone drives visibility (back-compat for
44
+ * callers that haven't adopted the role model yet).
45
+ */
46
+ role?: EditorRole;
47
+ }
48
+
49
+ export function resolveScopedChromePolicy(
50
+ input: ResolveScopedChromePolicyInput,
51
+ ): ScopedChromePolicy {
52
+ const effectiveMode = input.interactionGuardSnapshot?.effectiveMode ?? "edit";
53
+ const canFormatText = Boolean(input.capabilities?.canEdit) && effectiveMode === "edit";
54
+ const canInsertStructural = Boolean(input.capabilities?.canEdit) && effectiveMode === "edit";
55
+ const canAddComment =
56
+ Boolean(input.capabilities?.canAddComment) &&
57
+ effectiveMode !== "view" &&
58
+ effectiveMode !== "blocked";
59
+ const activeScopeId = input.interactionGuardSnapshot?.matchedScopeId;
60
+ const activeWorkItemId = input.workflowScopeSnapshot?.activeWorkItemId ?? null;
61
+ const hasScopedContext = Boolean(activeScopeId || activeWorkItemId);
62
+ const scopeStatusLabel =
63
+ input.workflowScopeSnapshot?.activeWorkItem?.title ??
64
+ input.workflowScopeSnapshot?.scopes.find((scope) => scope.scopeId === activeScopeId)?.label ??
65
+ (activeScopeId && input.interactionGuardSnapshot?.matchedScopeMode
66
+ ? `Scoped ${input.interactionGuardSnapshot.matchedScopeMode}`
67
+ : undefined);
68
+
69
+ const toolbar = Object.fromEntries(
70
+ TOOLBAR_CHROME_REGISTRY.map((entry) => {
71
+ let visible = entry.presets.includes(input.preset);
72
+
73
+ // Role filter — apply only when the caller supplied a role so existing
74
+ // host apps that haven't adopted the role model keep the pre-R1
75
+ // behavior (preset + capability only).
76
+ if (visible && input.role !== undefined) {
77
+ visible = entry.roles.includes(input.role);
78
+ }
79
+
80
+ if (visible) {
81
+ switch (entry.runtimeBehavior) {
82
+ case "formatting":
83
+ visible = canFormatText;
84
+ break;
85
+ case "structure":
86
+ visible = canInsertStructural;
87
+ break;
88
+ case "comment":
89
+ visible = canAddComment;
90
+ break;
91
+ default:
92
+ visible = true;
93
+ break;
94
+ }
95
+ }
96
+
97
+ if (visible && entry.id === "list-continuation") {
98
+ visible = Boolean(input.activeListContext);
99
+ }
100
+ if (visible && entry.id === "scope-status") {
101
+ visible = Boolean(scopeStatusLabel);
102
+ }
103
+
104
+ if (visible && entry.scopeBehavior === "scoped-only") {
105
+ visible = hasScopedContext;
106
+ }
107
+ if (visible && entry.scopeBehavior === "hidden-when-scoped") {
108
+ visible = !hasScopedContext;
109
+ }
110
+
111
+ // Resolve placement: when the entry overrides placement for the
112
+ // active role, that wins over the preset-density default.
113
+ const fullPlacement =
114
+ (input.role && entry.rolePlacement?.[input.role]) ?? entry.fullPlacement;
115
+ const placement = visible
116
+ ? (input.compactMode ? entry.compactPlacement : fullPlacement)
117
+ : "hidden";
118
+ const enabled =
119
+ visible &&
120
+ (entry.id !== "tracked-changes-toggle" || Boolean(input.capabilities?.trackChangesSupported)) &&
121
+ (entry.id !== "export" || Boolean(input.capabilities?.canExport));
122
+
123
+ return [
124
+ entry.id,
125
+ {
126
+ visible,
127
+ enabled,
128
+ placement,
129
+ } satisfies ToolbarChromeItemPolicy,
130
+ ];
131
+ }),
132
+ ) as Record<ToolbarChromeItemId, ToolbarChromeItemPolicy>;
133
+
134
+ const hiddenKinds = resolveHiddenSelectionToolKinds(effectiveMode);
135
+
136
+ return {
137
+ density: "compact",
138
+ ...(activeScopeId ? { activeScopeId } : {}),
139
+ activeWorkItemId,
140
+ ...(scopeStatusLabel ? { scopeStatusLabel } : {}),
141
+ toolbar,
142
+ selection: {
143
+ hiddenKinds,
144
+ },
145
+ };
146
+ }
147
+
148
+ export function isToolbarChromeItemVisible(
149
+ policy: ScopedChromePolicy,
150
+ itemId: ToolbarChromeItemId,
151
+ ): boolean {
152
+ return policy.toolbar[itemId]?.visible ?? false;
153
+ }
154
+
155
+ export function getToolbarChromePlacement(
156
+ policy: ScopedChromePolicy,
157
+ itemId: ToolbarChromeItemId,
158
+ ): ToolbarChromePlacement {
159
+ return policy.toolbar[itemId]?.placement ?? "hidden";
160
+ }
161
+
162
+ export function shouldRenderSelectionToolKind(
163
+ policy: ScopedChromePolicy | undefined,
164
+ kind: SelectionToolKind,
165
+ ): boolean {
166
+ return !policy?.selection.hiddenKinds.includes(kind);
167
+ }
168
+
169
+ function resolveHiddenSelectionToolKinds(
170
+ effectiveMode: InteractionGuardSnapshot["effectiveMode"] | "edit",
171
+ ): ReadonlyArray<SelectionToolKind> {
172
+ switch (effectiveMode) {
173
+ case "suggest":
174
+ return ["formatting-inline", "structure-context"];
175
+ case "comment":
176
+ return ["formatting-inline", "structure-context"];
177
+ case "view":
178
+ case "blocked":
179
+ return ["formatting-inline", "structure-context", "suggestion-review"];
180
+ default:
181
+ return [];
182
+ }
183
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  StyleCatalogSnapshot,
6
6
  } from "../../api/public-types";
7
7
  import type { SessionCapabilities } from "../../runtime/session-capabilities";
8
+ import type { ScopedChromePolicy } from "./scoped-chrome-policy";
8
9
  import type { SelectionToolResolverContext } from "./selection-tool-types";
9
10
 
10
11
  export interface SelectionToolResolverInput extends SelectionToolResolverContext {
@@ -16,4 +17,5 @@ export interface SelectionToolResolverInput extends SelectionToolResolverContext
16
17
  preferListStructureContext?: boolean;
17
18
  addCommentDisabledReason?: string;
18
19
  suppressedSuggestionRevisionId?: string | null;
20
+ scopedChromePolicy?: ScopedChromePolicy;
19
21
  }