@beyondwork/docx-react-component 1.0.79 → 1.0.81
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 +1 -1
- package/src/api/public-types.ts +28 -21
- package/src/api/v3/ai/resolve.ts +13 -7
- package/src/api/v3/runtime/workflow.ts +0 -9
- package/src/api/v3/ui/chrome-composition.ts +10 -2
- package/src/core/commands/add-scope.ts +110 -84
- package/src/runtime/formatting/formatting-types.ts +16 -0
- package/src/runtime/formatting/revision-display.ts +16 -10
- package/src/runtime/scopes/compile-scope-bundle.ts +9 -1
- package/src/runtime/scopes/compile-scope.ts +16 -0
- package/src/runtime/scopes/enumerate-scopes.ts +116 -3
- package/src/runtime/scopes/replaceability.ts +16 -0
- package/src/runtime/scopes/replacement/apply.ts +13 -3
- package/src/runtime/scopes/resolve-reference.ts +5 -0
- package/src/runtime/scopes/scope-kinds/scope.ts +87 -0
- package/src/runtime/scopes/scope-range.ts +11 -0
- package/src/runtime/workflow/coordinator.ts +3 -6
- package/src/runtime/workflow/scope-writer.ts +5 -26
- package/src/ui/WordReviewEditor.tsx +62 -3
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +10 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +153 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
- package/src/ui-tailwind/chrome/use-context-menu-controller.ts +15 -10
- package/src/ui-tailwind/review-workspace/types.ts +1 -0
- package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +46 -6
- package/src/ui-tailwind/theme/editor-theme.css +10 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +114 -14
|
@@ -66,11 +66,30 @@ export interface EditorActionDispatchContext {
|
|
|
66
66
|
* `WordReviewEditorRef`; the surface is intentionally minimal now.
|
|
67
67
|
*/
|
|
68
68
|
export interface EditorActionHostCallbacks {
|
|
69
|
+
// Command history
|
|
70
|
+
readonly onUndo?: () => void;
|
|
71
|
+
readonly onRedo?: () => void;
|
|
72
|
+
|
|
69
73
|
// Clipboard (always available — browser-native fallback acceptable)
|
|
70
74
|
readonly onCut?: () => void;
|
|
71
75
|
readonly onCopy?: () => void;
|
|
72
76
|
readonly onPaste?: () => void;
|
|
73
77
|
|
|
78
|
+
// Formatting / paragraph operations (plain text + table-cell targets)
|
|
79
|
+
readonly onToggleBold?: () => void;
|
|
80
|
+
readonly onToggleItalic?: () => void;
|
|
81
|
+
readonly onToggleUnderline?: () => void;
|
|
82
|
+
readonly onToggleStrikethrough?: () => void;
|
|
83
|
+
readonly onToggleBulletedList?: () => void;
|
|
84
|
+
readonly onToggleNumberedList?: () => void;
|
|
85
|
+
readonly onOutdent?: () => void;
|
|
86
|
+
readonly onIndent?: () => void;
|
|
87
|
+
readonly onSetAlignment?: (
|
|
88
|
+
alignment: "left" | "center" | "right" | "justify",
|
|
89
|
+
) => void;
|
|
90
|
+
readonly onInsertTable?: () => void;
|
|
91
|
+
readonly onAddComment?: () => void;
|
|
92
|
+
|
|
74
93
|
// Tracked-change operations (suggestion target)
|
|
75
94
|
readonly onAcceptSuggestion?: () => void;
|
|
76
95
|
readonly onRejectSuggestion?: () => void;
|
|
@@ -87,6 +106,7 @@ export interface EditorActionHostCallbacks {
|
|
|
87
106
|
readonly onInsertColumnAfter?: () => void;
|
|
88
107
|
readonly onDeleteRow?: () => void;
|
|
89
108
|
readonly onDeleteColumn?: () => void;
|
|
109
|
+
readonly onDeleteTable?: () => void;
|
|
90
110
|
readonly onMergeCells?: () => void;
|
|
91
111
|
readonly onSplitCell?: () => void;
|
|
92
112
|
readonly onOpenTableProperties?: () => void;
|
|
@@ -220,6 +240,24 @@ function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
|
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
243
|
+
// -------- History --------
|
|
244
|
+
mk({
|
|
245
|
+
id: "undo",
|
|
246
|
+
label: "Undo",
|
|
247
|
+
shortcut: ["Mod", "Z"],
|
|
248
|
+
group: "misc",
|
|
249
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
250
|
+
callback: "onUndo",
|
|
251
|
+
}),
|
|
252
|
+
mk({
|
|
253
|
+
id: "redo",
|
|
254
|
+
label: "Redo",
|
|
255
|
+
shortcut: ["Mod", "Shift", "Z"],
|
|
256
|
+
group: "misc",
|
|
257
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
258
|
+
callback: "onRedo",
|
|
259
|
+
}),
|
|
260
|
+
|
|
223
261
|
// -------- Clipboard (always available across every target kind) --------
|
|
224
262
|
mk({
|
|
225
263
|
id: "cut",
|
|
@@ -271,6 +309,113 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
|
271
309
|
callback: "onPaste",
|
|
272
310
|
}),
|
|
273
311
|
|
|
312
|
+
// -------- Formatting / paragraph --------
|
|
313
|
+
mk({
|
|
314
|
+
id: "format-bold",
|
|
315
|
+
label: "Bold",
|
|
316
|
+
shortcut: ["Mod", "B"],
|
|
317
|
+
group: "formatting",
|
|
318
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
319
|
+
callback: "onToggleBold",
|
|
320
|
+
}),
|
|
321
|
+
mk({
|
|
322
|
+
id: "format-italic",
|
|
323
|
+
label: "Italic",
|
|
324
|
+
shortcut: ["Mod", "I"],
|
|
325
|
+
group: "formatting",
|
|
326
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
327
|
+
callback: "onToggleItalic",
|
|
328
|
+
}),
|
|
329
|
+
mk({
|
|
330
|
+
id: "format-underline",
|
|
331
|
+
label: "Underline",
|
|
332
|
+
shortcut: ["Mod", "U"],
|
|
333
|
+
group: "formatting",
|
|
334
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
335
|
+
callback: "onToggleUnderline",
|
|
336
|
+
}),
|
|
337
|
+
mk({
|
|
338
|
+
id: "format-strikethrough",
|
|
339
|
+
label: "Strikethrough",
|
|
340
|
+
group: "formatting",
|
|
341
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
342
|
+
callback: "onToggleStrikethrough",
|
|
343
|
+
}),
|
|
344
|
+
mk({
|
|
345
|
+
id: "list-bulleted",
|
|
346
|
+
label: "Bulleted list",
|
|
347
|
+
group: "formatting",
|
|
348
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
349
|
+
callback: "onToggleBulletedList",
|
|
350
|
+
}),
|
|
351
|
+
mk({
|
|
352
|
+
id: "list-numbered",
|
|
353
|
+
label: "Numbered list",
|
|
354
|
+
group: "formatting",
|
|
355
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
356
|
+
callback: "onToggleNumberedList",
|
|
357
|
+
}),
|
|
358
|
+
mk({
|
|
359
|
+
id: "paragraph-outdent",
|
|
360
|
+
label: "Decrease indent",
|
|
361
|
+
group: "formatting",
|
|
362
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
363
|
+
callback: "onOutdent",
|
|
364
|
+
}),
|
|
365
|
+
mk({
|
|
366
|
+
id: "paragraph-indent",
|
|
367
|
+
label: "Increase indent",
|
|
368
|
+
group: "formatting",
|
|
369
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
370
|
+
callback: "onIndent",
|
|
371
|
+
}),
|
|
372
|
+
mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
|
|
373
|
+
id: "align-left",
|
|
374
|
+
label: "Align left",
|
|
375
|
+
group: "formatting",
|
|
376
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
377
|
+
callback: "onSetAlignment",
|
|
378
|
+
payload: "left",
|
|
379
|
+
}),
|
|
380
|
+
mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
|
|
381
|
+
id: "align-center",
|
|
382
|
+
label: "Align center",
|
|
383
|
+
group: "formatting",
|
|
384
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
385
|
+
callback: "onSetAlignment",
|
|
386
|
+
payload: "center",
|
|
387
|
+
}),
|
|
388
|
+
mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
|
|
389
|
+
id: "align-right",
|
|
390
|
+
label: "Align right",
|
|
391
|
+
group: "formatting",
|
|
392
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
393
|
+
callback: "onSetAlignment",
|
|
394
|
+
payload: "right",
|
|
395
|
+
}),
|
|
396
|
+
mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
|
|
397
|
+
id: "align-justify",
|
|
398
|
+
label: "Justify",
|
|
399
|
+
group: "formatting",
|
|
400
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
401
|
+
callback: "onSetAlignment",
|
|
402
|
+
payload: "justify",
|
|
403
|
+
}),
|
|
404
|
+
mk({
|
|
405
|
+
id: "insert-table",
|
|
406
|
+
label: "Insert table",
|
|
407
|
+
group: "table",
|
|
408
|
+
targetKinds: ["plain-text"],
|
|
409
|
+
callback: "onInsertTable",
|
|
410
|
+
}),
|
|
411
|
+
mk({
|
|
412
|
+
id: "comment-add",
|
|
413
|
+
label: "Add comment",
|
|
414
|
+
group: "comment",
|
|
415
|
+
targetKinds: ["plain-text", "table-cell"],
|
|
416
|
+
callback: "onAddComment",
|
|
417
|
+
}),
|
|
418
|
+
|
|
274
419
|
// -------- Suggestion / tracked change --------
|
|
275
420
|
mk({
|
|
276
421
|
id: "accept-suggestion",
|
|
@@ -379,6 +524,14 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
|
379
524
|
targetKinds: ["table-column", "table-whole"],
|
|
380
525
|
callback: "onDeleteColumn",
|
|
381
526
|
}),
|
|
527
|
+
// ── Whole-table destructive op ──
|
|
528
|
+
mk({
|
|
529
|
+
id: "table-delete-table",
|
|
530
|
+
label: "Delete table",
|
|
531
|
+
group: "table",
|
|
532
|
+
targetKinds: ["table-whole"],
|
|
533
|
+
callback: "onDeleteTable",
|
|
534
|
+
}),
|
|
382
535
|
// ── Whole-table — properties / borders surface at any tier so
|
|
383
536
|
// they are reachable from a single right-click in a cell ──
|
|
384
537
|
mk({
|
|
@@ -75,6 +75,7 @@ export interface TwSelectionToolHostProps {
|
|
|
75
75
|
onDistributeColumnsEvenly?: () => void;
|
|
76
76
|
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
77
77
|
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
78
|
+
onOpenTableMore?: (coords: { clientX: number; clientY: number }) => void;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -299,6 +300,7 @@ function renderTool(
|
|
|
299
300
|
onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
|
|
300
301
|
onSetTableAlignment={props.onSetTableAlignment}
|
|
301
302
|
onSetCellVerticalAlign={props.onSetCellVerticalAlign}
|
|
303
|
+
onOpenTableMore={props.onOpenTableMore}
|
|
302
304
|
/>
|
|
303
305
|
);
|
|
304
306
|
case "comment-thread":
|
|
@@ -35,6 +35,7 @@ export interface TwSelectionToolStructureProps {
|
|
|
35
35
|
onDistributeColumnsEvenly?: () => void;
|
|
36
36
|
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
37
37
|
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
38
|
+
onOpenTableMore?: (coords: { clientX: number; clientY: number }) => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
|
|
@@ -58,7 +59,10 @@ export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
|
|
|
58
59
|
return (
|
|
59
60
|
<TwTableContextToolbar
|
|
60
61
|
disabled={!props.model.canMutate}
|
|
61
|
-
tableContext={props.model.activeTable ?? null}
|
|
62
|
+
tableContext={props.model.activeTable ?? null}
|
|
63
|
+
tableStyles={props.model.tableStyles ?? []}
|
|
64
|
+
compact={true}
|
|
65
|
+
onOpenMore={props.onOpenTableMore}
|
|
62
66
|
onSetTableStyle={props.onSetTableStyle}
|
|
63
67
|
onAddRowBefore={props.onAddRowBefore}
|
|
64
68
|
onAddRowAfter={props.onAddRowAfter}
|
|
@@ -58,6 +58,18 @@ function tableContextToKinds(
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function mergeTableTierKinds(
|
|
62
|
+
kinds: readonly TargetKind[],
|
|
63
|
+
tableContext: TableStructureContextSnapshot | null | undefined,
|
|
64
|
+
): readonly TargetKind[] {
|
|
65
|
+
if (!kinds.includes("table-cell") || !tableContext) return kinds;
|
|
66
|
+
const merged = [...kinds];
|
|
67
|
+
for (const k of tableContextToKinds(tableContext)) {
|
|
68
|
+
if (!merged.includes(k)) merged.push(k);
|
|
69
|
+
}
|
|
70
|
+
return merged;
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export interface ContextMenuRequest {
|
|
62
74
|
readonly clientX: number;
|
|
63
75
|
readonly clientY: number;
|
|
@@ -149,14 +161,7 @@ export function useContextMenuController(
|
|
|
149
161
|
// is in a table cell. Cell-level ops carry every table-* kind
|
|
150
162
|
// (so they are still visible) and tier-only ops (delete row /
|
|
151
163
|
// delete column) gate on the matching kind.
|
|
152
|
-
const
|
|
153
|
-
baseKinds.includes("table-cell") && tableContext
|
|
154
|
-
? tableContextToKinds(tableContext)
|
|
155
|
-
: [];
|
|
156
|
-
const merged = [...baseKinds];
|
|
157
|
-
for (const k of tierKinds) {
|
|
158
|
-
if (!merged.includes(k)) merged.push(k);
|
|
159
|
-
}
|
|
164
|
+
const merged = mergeTableTierKinds(baseKinds, tableContext);
|
|
160
165
|
const entries = buildEntries(merged);
|
|
161
166
|
// Progressive disclosure: if no actions resolve (every host callback
|
|
162
167
|
// absent + no scope-anchor + etc), don't open an empty menu. Let
|
|
@@ -180,7 +185,7 @@ export function useContextMenuController(
|
|
|
180
185
|
clientY: number;
|
|
181
186
|
kinds: readonly TargetKind[];
|
|
182
187
|
}) => {
|
|
183
|
-
const entries = buildEntries(args.kinds);
|
|
188
|
+
const entries = buildEntries(mergeTableTierKinds(args.kinds, tableContext));
|
|
184
189
|
if (entries.length === 0) return;
|
|
185
190
|
setState({
|
|
186
191
|
open: true,
|
|
@@ -189,7 +194,7 @@ export function useContextMenuController(
|
|
|
189
194
|
entries,
|
|
190
195
|
});
|
|
191
196
|
},
|
|
192
|
-
[buildEntries],
|
|
197
|
+
[buildEntries, tableContext],
|
|
193
198
|
);
|
|
194
199
|
|
|
195
200
|
// D.5.Nit1 — the return value is consumed directly by the caller
|
|
@@ -150,6 +150,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
150
150
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
151
151
|
chromePreset?: WordReviewEditorChromePreset;
|
|
152
152
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
153
|
+
density?: "compact" | "standard" | "comfortable";
|
|
153
154
|
/** P9g — live collab session for the `"collab"` chrome preset's top nav. */
|
|
154
155
|
collabSession?: import("../../runtime/collab-session.ts").CollabSession;
|
|
155
156
|
collabTransportStatus?: import("../../api/awareness-identity-types.ts").TransportStatus;
|
|
@@ -10,10 +10,18 @@ export type UseWorkspaceCompositionOptions = Pick<
|
|
|
10
10
|
ChromeCompositionInput,
|
|
11
11
|
| "chromePreset"
|
|
12
12
|
| "chromeOptions"
|
|
13
|
+
| "chromeVisibility"
|
|
13
14
|
| "reviewMode"
|
|
14
15
|
| "role"
|
|
16
|
+
| "readOnly"
|
|
15
17
|
| "markupDisplay"
|
|
18
|
+
| "activeRailTab"
|
|
19
|
+
| "railOpen"
|
|
20
|
+
| "pinnedRailTabs"
|
|
21
|
+
| "density"
|
|
22
|
+
| "containerWidth"
|
|
16
23
|
| "diagnosticsSignal"
|
|
24
|
+
| "modeOverride"
|
|
17
25
|
>;
|
|
18
26
|
|
|
19
27
|
/**
|
|
@@ -49,11 +57,11 @@ export type UseWorkspaceCompositionOptions = Pick<
|
|
|
49
57
|
* downstream. Review M6 (2026-04-22) flagged this as a silent perf
|
|
50
58
|
* regression risk.
|
|
51
59
|
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
* Product-lane note: this hook intentionally accepts the full set of
|
|
61
|
+
* mounted composition signals the resolver already models. Leaving these
|
|
62
|
+
* out makes `resolveChromeComposition()` look canonical in tests while the
|
|
63
|
+
* shipped editor silently falls back to stale defaults.
|
|
64
|
+
*/
|
|
57
65
|
export function useWorkspaceComposition(
|
|
58
66
|
options: UseWorkspaceCompositionOptions,
|
|
59
67
|
): ReturnType<typeof resolveChromeComposition> {
|
|
@@ -61,10 +69,18 @@ export function useWorkspaceComposition(
|
|
|
61
69
|
const {
|
|
62
70
|
chromePreset,
|
|
63
71
|
chromeOptions,
|
|
72
|
+
chromeVisibility,
|
|
64
73
|
reviewMode,
|
|
65
74
|
role,
|
|
75
|
+
readOnly,
|
|
66
76
|
markupDisplay,
|
|
77
|
+
activeRailTab,
|
|
78
|
+
railOpen,
|
|
79
|
+
pinnedRailTabs,
|
|
80
|
+
density,
|
|
81
|
+
containerWidth,
|
|
67
82
|
diagnosticsSignal,
|
|
83
|
+
modeOverride,
|
|
68
84
|
} = options;
|
|
69
85
|
|
|
70
86
|
return useMemo(
|
|
@@ -72,15 +88,39 @@ export function useWorkspaceComposition(
|
|
|
72
88
|
const input: ChromeCompositionInput = {
|
|
73
89
|
chromePreset,
|
|
74
90
|
chromeOptions,
|
|
91
|
+
chromeVisibility,
|
|
75
92
|
reviewMode,
|
|
76
93
|
role,
|
|
94
|
+
readOnly,
|
|
77
95
|
markupDisplay,
|
|
96
|
+
activeRailTab,
|
|
97
|
+
railOpen,
|
|
98
|
+
pinnedRailTabs,
|
|
99
|
+
density,
|
|
100
|
+
containerWidth,
|
|
78
101
|
diagnosticsSignal,
|
|
102
|
+
modeOverride,
|
|
79
103
|
};
|
|
80
104
|
return ui
|
|
81
105
|
? ui.chrome.getComposition(input)
|
|
82
106
|
: resolveChromeComposition(input);
|
|
83
107
|
},
|
|
84
|
-
[
|
|
108
|
+
[
|
|
109
|
+
ui,
|
|
110
|
+
chromePreset,
|
|
111
|
+
chromeOptions,
|
|
112
|
+
chromeVisibility,
|
|
113
|
+
reviewMode,
|
|
114
|
+
role,
|
|
115
|
+
readOnly,
|
|
116
|
+
markupDisplay,
|
|
117
|
+
activeRailTab,
|
|
118
|
+
railOpen,
|
|
119
|
+
pinnedRailTabs,
|
|
120
|
+
density,
|
|
121
|
+
containerWidth,
|
|
122
|
+
diagnosticsSignal,
|
|
123
|
+
modeOverride,
|
|
124
|
+
],
|
|
85
125
|
);
|
|
86
126
|
}
|
|
@@ -1075,7 +1075,16 @@
|
|
|
1075
1075
|
}
|
|
1076
1076
|
|
|
1077
1077
|
.wre-page-band:hover {
|
|
1078
|
-
|
|
1078
|
+
/*
|
|
1079
|
+
* N3 audit (2026-04-24): the prior `color-mix(bg-muted 80%, surface 20%)`
|
|
1080
|
+
* blended two near-adjacent tokens — in dark mode (#17211C vs #182420)
|
|
1081
|
+
* the result moved ≤2 RGB units from the base, leaving no visible
|
|
1082
|
+
* hover affordance. `--color-bg-hover` is the token the design system
|
|
1083
|
+
* already dedicates to this signal (light #EAF6EF vs muted #F7FAF8;
|
|
1084
|
+
* dark #21342A vs muted #17211C) and resolves the dark-mode legibility
|
|
1085
|
+
* gap without regressing light-mode subtlety.
|
|
1086
|
+
*/
|
|
1087
|
+
background-color: var(--color-bg-hover);
|
|
1079
1088
|
}
|
|
1080
1089
|
|
|
1081
1090
|
.wre-page-band[data-active="true"] {
|
|
@@ -43,7 +43,10 @@ import {
|
|
|
43
43
|
import { TwContextBand } from "./chrome/tw-context-band";
|
|
44
44
|
import { TwRoleActionRegion } from "./toolbar/tw-role-action-region";
|
|
45
45
|
import { LocalSurfaceArbiterContext } from "./chrome/local-surface-arbiter";
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
TwWorkspaceChromeHost,
|
|
48
|
+
type TwWorkspaceChromeHostController,
|
|
49
|
+
} from "./chrome/tw-workspace-chrome-host";
|
|
47
50
|
import { TwChromeOverlay, TwPageStackOverlayLayer } from "./chrome-overlay";
|
|
48
51
|
import { TwFloatingImageLayer } from "./page-stack/tw-floating-image-layer.tsx";
|
|
49
52
|
import { shouldHidePageBorderForSelection } from "./review-workspace/paragraph-layout.ts";
|
|
@@ -85,16 +88,14 @@ export type {
|
|
|
85
88
|
import type { EditorRole } from "../api/public-types.ts";
|
|
86
89
|
|
|
87
90
|
// Default shell-header modes for the workspace's default composition.
|
|
88
|
-
// Designsystem §6.1 prescribes a 4-mode switcher
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// Slice 5 (Phase Q debug UX). Hosts that want a fully-wired 4-mode set
|
|
92
|
-
// supply their own `shellHeader` prop.
|
|
91
|
+
// Designsystem §6.1 prescribes a 4-mode switcher; all four are reachable in
|
|
92
|
+
// the product path. "More" is a shell-owned diagnostics/search posture, so it
|
|
93
|
+
// uses `ChromeCompositionInput.modeOverride` instead of changing runtime role.
|
|
93
94
|
const DEFAULT_WORKSPACE_SHELL_MODES: readonly ShellHeaderModeOption[] = [
|
|
94
95
|
{ id: "edit", label: "Edit" },
|
|
95
96
|
{ id: "review", label: "Review" },
|
|
96
97
|
{ id: "workflow", label: "Workflow" },
|
|
97
|
-
{ id: "more", label: "More"
|
|
98
|
+
{ id: "more", label: "More" },
|
|
98
99
|
];
|
|
99
100
|
|
|
100
101
|
function editorRoleToShellMode(role: EditorRole): ShellHeaderMode {
|
|
@@ -121,6 +122,47 @@ function shellModeToEditorRole(mode: ShellHeaderMode): EditorRole | null {
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function TwMoreContextBandContent(props: {
|
|
126
|
+
onOpenDiagnostics: () => void;
|
|
127
|
+
onOpenCompatibility: () => void;
|
|
128
|
+
onOpenOutline: () => void;
|
|
129
|
+
onOpenSearch?: () => void;
|
|
130
|
+
}): React.JSX.Element {
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
className="flex min-w-0 items-center gap-1"
|
|
134
|
+
data-testid="more-context-band-content"
|
|
135
|
+
>
|
|
136
|
+
<ContextBandButton label="Diagnostics" onClick={props.onOpenDiagnostics} />
|
|
137
|
+
<ContextBandButton label="Compatibility" onClick={props.onOpenCompatibility} />
|
|
138
|
+
<ContextBandButton label="Outline" onClick={props.onOpenOutline} />
|
|
139
|
+
<ContextBandButton
|
|
140
|
+
label="Search"
|
|
141
|
+
onClick={props.onOpenSearch}
|
|
142
|
+
disabled={!props.onOpenSearch}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ContextBandButton(props: {
|
|
149
|
+
label: string;
|
|
150
|
+
onClick?: () => void;
|
|
151
|
+
disabled?: boolean;
|
|
152
|
+
}): React.JSX.Element {
|
|
153
|
+
return (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
disabled={props.disabled}
|
|
157
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
158
|
+
onClick={props.onClick}
|
|
159
|
+
className="inline-flex h-7 items-center rounded-[var(--radius-md)] px-2.5 text-xs font-medium text-secondary transition-colors hover:bg-hover focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] disabled:cursor-not-allowed disabled:opacity-40"
|
|
160
|
+
>
|
|
161
|
+
{props.label}
|
|
162
|
+
</button>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
124
166
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
125
167
|
const props = {
|
|
126
168
|
...inputProps,
|
|
@@ -128,6 +170,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
128
170
|
} as TwReviewWorkspaceProps & EditorCommandBag;
|
|
129
171
|
const { snapshot, viewState } = props;
|
|
130
172
|
const selectionToolbarRootRef = useRef<HTMLDivElement>(null);
|
|
173
|
+
const workspaceChromeControllerRef =
|
|
174
|
+
useRef<TwWorkspaceChromeHostController | null>(null);
|
|
175
|
+
const [shellModeOverride, setShellModeOverride] =
|
|
176
|
+
useState<ShellHeaderMode | null>(null);
|
|
131
177
|
// P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
|
|
132
178
|
// root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
|
|
133
179
|
// measure per-page rects and to reparent PM's DOM node across band
|
|
@@ -382,18 +428,46 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
382
428
|
props.onSetParagraphTabStops,
|
|
383
429
|
]);
|
|
384
430
|
|
|
431
|
+
const setWorkspaceChromeController = useCallback(
|
|
432
|
+
(instance: TwWorkspaceChromeHostController | null) => {
|
|
433
|
+
workspaceChromeControllerRef.current = instance;
|
|
434
|
+
const ref = props.chromeControllerRef;
|
|
435
|
+
if (!ref) return;
|
|
436
|
+
if (typeof ref === "function") {
|
|
437
|
+
ref(instance);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
(ref as React.MutableRefObject<TwWorkspaceChromeHostController | null>).current =
|
|
441
|
+
instance;
|
|
442
|
+
},
|
|
443
|
+
[props.chromeControllerRef],
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const openTableMoreMenu = useCallback(
|
|
447
|
+
(coords: { clientX: number; clientY: number }) => {
|
|
448
|
+
workspaceChromeControllerRef.current?.openWithKinds({
|
|
449
|
+
...coords,
|
|
450
|
+
kinds: ["table-cell"],
|
|
451
|
+
});
|
|
452
|
+
},
|
|
453
|
+
[],
|
|
454
|
+
);
|
|
455
|
+
|
|
385
456
|
// Audit §2.4 — the shell header is ALWAYS present in default composition.
|
|
386
457
|
// When the host does not supply a pre-assembled shell node, fall back to
|
|
387
458
|
// a default TwShellHeader wired to the workspace's editor-role state so
|
|
388
459
|
// the mode tabs actually change the active role instead of being
|
|
389
|
-
// decorative.
|
|
390
|
-
//
|
|
460
|
+
// decorative. The "more" tab is shell-owned diagnostics/search posture:
|
|
461
|
+
// it drives composition mode via `modeOverride` and does not mutate the
|
|
462
|
+
// runtime editor role.
|
|
391
463
|
// Host-supplied shells continue to win (back-compat).
|
|
392
464
|
const defaultShellModes: readonly ShellHeaderModeOption[] =
|
|
393
465
|
DEFAULT_WORKSPACE_SHELL_MODES;
|
|
394
466
|
const defaultShellActiveMode: ShellHeaderMode = editorRoleToShellMode(
|
|
395
467
|
viewState.editorRole,
|
|
396
468
|
);
|
|
469
|
+
const activeShellMode: ShellHeaderMode =
|
|
470
|
+
shellModeOverride === "more" ? "more" : defaultShellActiveMode;
|
|
397
471
|
// coord-11 §21 — host-supplied shellHeader wins; otherwise the default
|
|
398
472
|
// TwShellHeader mounts only when `chromeVisibility.shellHeader === true`
|
|
399
473
|
// (default for every preset except `selection`). The `selection` preset
|
|
@@ -406,8 +480,15 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
406
480
|
? (
|
|
407
481
|
<TwShellHeader
|
|
408
482
|
modes={defaultShellModes}
|
|
409
|
-
activeMode={
|
|
483
|
+
activeMode={activeShellMode}
|
|
410
484
|
onModeChange={(mode) => {
|
|
485
|
+
if (mode === "more") {
|
|
486
|
+
setShellModeOverride("more");
|
|
487
|
+
setReviewRailOpen(true);
|
|
488
|
+
props.onActiveRailTabChange?.("health");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
setShellModeOverride(null);
|
|
411
492
|
const nextRole = shellModeToEditorRole(mode);
|
|
412
493
|
if (nextRole !== null && nextRole !== viewState.editorRole) {
|
|
413
494
|
props.onEditorRoleChange?.(nextRole);
|
|
@@ -444,10 +525,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
444
525
|
const composition = useWorkspaceComposition({
|
|
445
526
|
chromePreset,
|
|
446
527
|
chromeOptions: props.chromeOptions,
|
|
528
|
+
chromeVisibility: props.chromeVisibility,
|
|
447
529
|
reviewMode: props.reviewMode,
|
|
448
530
|
role: viewState.editorRole,
|
|
531
|
+
readOnly: snapshot.readOnly,
|
|
449
532
|
markupDisplay: props.markupDisplay,
|
|
533
|
+
activeRailTab: props.activeRailTab,
|
|
534
|
+
railOpen: reviewRailOpen,
|
|
535
|
+
density: props.density,
|
|
536
|
+
containerWidth: viewportWidth,
|
|
450
537
|
diagnosticsSignal,
|
|
538
|
+
modeOverride: shellModeOverride === "more" ? "more" : undefined,
|
|
451
539
|
});
|
|
452
540
|
const showHealthRailTab = composition.rail.visibleTabs.has("health");
|
|
453
541
|
|
|
@@ -481,7 +569,20 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
481
569
|
*/}
|
|
482
570
|
{chromeVisibility.toolbar ? (
|
|
483
571
|
<TwContextBand mode={composition.mode}>
|
|
484
|
-
{
|
|
572
|
+
{composition.mode === "more" ? (
|
|
573
|
+
<TwMoreContextBandContent
|
|
574
|
+
onOpenDiagnostics={() => {
|
|
575
|
+
setReviewRailOpen(true);
|
|
576
|
+
props.onActiveRailTabChange?.("health");
|
|
577
|
+
}}
|
|
578
|
+
onOpenCompatibility={() => {
|
|
579
|
+
setReviewRailOpen(true);
|
|
580
|
+
props.onActiveRailTabChange?.("health");
|
|
581
|
+
}}
|
|
582
|
+
onOpenOutline={() => setNavOpen(true)}
|
|
583
|
+
onOpenSearch={props.reviewRailFooter?.onSearch}
|
|
584
|
+
/>
|
|
585
|
+
) : viewState.editorRole ? (
|
|
485
586
|
<TwRoleActionRegion
|
|
486
587
|
role={viewState.editorRole}
|
|
487
588
|
policy={scopedChromePolicy}
|
|
@@ -574,9 +675,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
574
675
|
<TwWorkspaceChromeHost
|
|
575
676
|
mode={composition.mode}
|
|
576
677
|
editorActionHost={props.editorActionHost}
|
|
577
|
-
{
|
|
578
|
-
? { controllerRef: props.chromeControllerRef }
|
|
579
|
-
: {})}
|
|
678
|
+
controllerRef={setWorkspaceChromeController}
|
|
580
679
|
{...(props.commandPaletteDisabled !== undefined
|
|
581
680
|
? { paletteDisabled: props.commandPaletteDisabled }
|
|
582
681
|
: {})}
|
|
@@ -875,6 +974,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
875
974
|
onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
|
|
876
975
|
onSetTableAlignment={props.onSetTableAlignment}
|
|
877
976
|
onSetCellVerticalAlign={props.onSetCellVerticalAlign}
|
|
977
|
+
onOpenTableMore={openTableMoreMenu}
|
|
878
978
|
chromePins={viewState.chromePins}
|
|
879
979
|
onChromePinChange={props.onChromePinChange}
|
|
880
980
|
/>
|