@beyondwork/docx-react-component 1.0.80 → 1.0.82

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +12 -13
  3. package/src/api/v3/_runtime-handle.ts +4 -0
  4. package/src/api/v3/runtime/document.ts +61 -3
  5. package/src/api/v3/runtime/review.ts +55 -2
  6. package/src/api/v3/ui/chrome-composition.ts +10 -2
  7. package/src/io/normalize/normalize-text.ts +4 -1
  8. package/src/io/ooxml/parse-drawing.ts +4 -0
  9. package/src/model/canonical-document.ts +2 -0
  10. package/src/ui/WordReviewEditor.tsx +132 -3
  11. package/src/ui/editor-shell-view.tsx +1 -0
  12. package/src/ui-tailwind/chrome/editor-action-registry.ts +373 -0
  13. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +59 -35
  14. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +2 -0
  15. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  16. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +15 -10
  17. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
  18. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +256 -37
  19. package/src/ui-tailwind/editor-surface/pm-schema.ts +54 -1
  20. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +31 -1
  21. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -0
  22. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +24 -5
  23. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +35 -6
  24. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +333 -43
  25. package/src/ui-tailwind/review-workspace/types.ts +1 -0
  26. package/src/ui-tailwind/review-workspace/use-page-markers.ts +273 -24
  27. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +46 -6
  28. package/src/ui-tailwind/theme/editor-theme.css +3 -5
  29. package/src/ui-tailwind/tw-review-workspace.tsx +117 -14
@@ -24,6 +24,8 @@ import type { EditorChromeMode } from "../../api/v3/ui/chrome-composition";
24
24
  import type { ContextMenuGroupId } from "./tw-context-menu";
25
25
  import type { ShortcutKey } from "./tw-shortcut-hint";
26
26
 
27
+ type ProductSectionBreakType = "nextPage" | "continuous" | "evenPage" | "oddPage";
28
+
27
29
  /**
28
30
  * Target kinds roughly mirror DESIGN-EDITOR.md §4 "Local context" cells.
29
31
  * A contextmenu event resolves its target to one (or more) of these
@@ -66,11 +68,44 @@ export interface EditorActionDispatchContext {
66
68
  * `WordReviewEditorRef`; the surface is intentionally minimal now.
67
69
  */
68
70
  export interface EditorActionHostCallbacks {
71
+ // Command history
72
+ readonly onUndo?: () => void;
73
+ readonly onRedo?: () => void;
74
+
69
75
  // Clipboard (always available — browser-native fallback acceptable)
70
76
  readonly onCut?: () => void;
71
77
  readonly onCopy?: () => void;
72
78
  readonly onPaste?: () => void;
73
79
 
80
+ // Formatting / paragraph operations (plain text + table-cell targets)
81
+ readonly onToggleBold?: () => void;
82
+ readonly onToggleItalic?: () => void;
83
+ readonly onToggleUnderline?: () => void;
84
+ readonly onToggleStrikethrough?: () => void;
85
+ readonly onSetParagraphStyle?: (styleId: string) => void;
86
+ readonly onSetFontFamily?: (fontFamily: string) => void;
87
+ readonly onSetFontSize?: (fontSize: number) => void;
88
+ readonly onSetTextColor?: (color: string) => void;
89
+ readonly onSetHighlightColor?: (color: string | null) => void;
90
+ readonly onToggleBulletedList?: () => void;
91
+ readonly onToggleNumberedList?: () => void;
92
+ readonly onOutdent?: () => void;
93
+ readonly onIndent?: () => void;
94
+ readonly onSetAlignment?: (
95
+ alignment: "left" | "center" | "right" | "justify",
96
+ ) => void;
97
+ readonly onInsertPageBreak?: () => void;
98
+ readonly onInsertSectionBreak?: (type: ProductSectionBreakType) => void;
99
+ readonly onInsertTable?: () => void;
100
+ readonly onInsertImage?: () => void;
101
+ readonly onAddComment?: () => void;
102
+
103
+ // Search / app-level host-delegated commands
104
+ readonly onFindRequested?: () => void;
105
+ readonly onReplaceRequested?: () => void;
106
+ readonly onPrintRequested?: () => void;
107
+ readonly onGoToRequested?: () => void;
108
+
74
109
  // Tracked-change operations (suggestion target)
75
110
  readonly onAcceptSuggestion?: () => void;
76
111
  readonly onRejectSuggestion?: () => void;
@@ -87,6 +122,7 @@ export interface EditorActionHostCallbacks {
87
122
  readonly onInsertColumnAfter?: () => void;
88
123
  readonly onDeleteRow?: () => void;
89
124
  readonly onDeleteColumn?: () => void;
125
+ readonly onDeleteTable?: () => void;
90
126
  readonly onMergeCells?: () => void;
91
127
  readonly onSplitCell?: () => void;
92
128
  readonly onOpenTableProperties?: () => void;
@@ -114,6 +150,7 @@ export interface EditorActionHostCallbacks {
114
150
  export interface EditorAction {
115
151
  readonly id: string;
116
152
  readonly label: string;
153
+ readonly description?: string;
117
154
  readonly shortcut?: readonly ShortcutKey[];
118
155
  readonly group: ContextMenuGroupId;
119
156
  /** Targets for which this action is relevant. */
@@ -147,6 +184,7 @@ export interface EditorAction {
147
184
  function mk<K extends keyof EditorActionHostCallbacks>(opts: {
148
185
  id: string;
149
186
  label: string;
187
+ description?: string;
150
188
  shortcut?: readonly ShortcutKey[];
151
189
  group: ContextMenuGroupId;
152
190
  targetKinds: readonly TargetKind[];
@@ -158,6 +196,7 @@ function mk<K extends keyof EditorActionHostCallbacks>(opts: {
158
196
  return {
159
197
  id: opts.id,
160
198
  label: opts.label,
199
+ ...(opts.description ? { description: opts.description } : {}),
161
200
  ...(opts.shortcut ? { shortcut: opts.shortcut } : {}),
162
201
  group: opts.group,
163
202
  targetKinds: new Set(opts.targetKinds),
@@ -191,6 +230,7 @@ function mk<K extends keyof EditorActionHostCallbacks>(opts: {
191
230
  function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
192
231
  id: string;
193
232
  label: string;
233
+ description?: string;
194
234
  shortcut?: readonly ShortcutKey[];
195
235
  group: ContextMenuGroupId;
196
236
  targetKinds: readonly TargetKind[];
@@ -202,6 +242,7 @@ function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
202
242
  return {
203
243
  id: opts.id,
204
244
  label: opts.label,
245
+ ...(opts.description ? { description: opts.description } : {}),
205
246
  ...(opts.shortcut ? { shortcut: opts.shortcut } : {}),
206
247
  group: opts.group,
207
248
  targetKinds: new Set(opts.targetKinds),
@@ -219,7 +260,60 @@ function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
219
260
  };
220
261
  }
221
262
 
263
+ /**
264
+ * Build an important command that should remain visible as a disabled
265
+ * row when the host/runtime has not wired the implementation yet. Use
266
+ * sparingly for product-signpost commands such as Insert Image or
267
+ * Replace; most callback-less registry rows should stay hidden.
268
+ */
269
+ function mkImportant<K extends keyof EditorActionHostCallbacks>(opts: {
270
+ id: string;
271
+ label: string;
272
+ description: string;
273
+ shortcut?: readonly ShortcutKey[];
274
+ group: ContextMenuGroupId;
275
+ targetKinds: readonly TargetKind[];
276
+ modes?: readonly EditorChromeMode[];
277
+ callback: K;
278
+ }): EditorAction {
279
+ return {
280
+ id: opts.id,
281
+ label: opts.label,
282
+ description: opts.description,
283
+ ...(opts.shortcut ? { shortcut: opts.shortcut } : {}),
284
+ group: opts.group,
285
+ targetKinds: new Set(opts.targetKinds),
286
+ ...(opts.modes ? { modes: new Set(opts.modes) } : {}),
287
+ when: (ctx) =>
288
+ typeof ctx.host[opts.callback] === "function" ? true : "disabled",
289
+ run: (ctx) => {
290
+ const fn = ctx.host[opts.callback] as (() => void) | undefined;
291
+ if (!fn) return;
292
+ fn();
293
+ ctx.dismiss();
294
+ },
295
+ };
296
+ }
297
+
222
298
  export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
299
+ // -------- History --------
300
+ mk({
301
+ id: "undo",
302
+ label: "Undo",
303
+ shortcut: ["Mod", "Z"],
304
+ group: "misc",
305
+ targetKinds: ["plain-text", "table-cell"],
306
+ callback: "onUndo",
307
+ }),
308
+ mk({
309
+ id: "redo",
310
+ label: "Redo",
311
+ shortcut: ["Mod", "Shift", "Z"],
312
+ group: "misc",
313
+ targetKinds: ["plain-text", "table-cell"],
314
+ callback: "onRedo",
315
+ }),
316
+
223
317
  // -------- Clipboard (always available across every target kind) --------
224
318
  mk({
225
319
  id: "cut",
@@ -271,6 +365,277 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
271
365
  callback: "onPaste",
272
366
  }),
273
367
 
368
+ // -------- Formatting / paragraph --------
369
+ mk({
370
+ id: "format-bold",
371
+ label: "Bold",
372
+ shortcut: ["Mod", "B"],
373
+ group: "formatting",
374
+ targetKinds: ["plain-text", "table-cell"],
375
+ callback: "onToggleBold",
376
+ }),
377
+ mk({
378
+ id: "format-italic",
379
+ label: "Italic",
380
+ shortcut: ["Mod", "I"],
381
+ group: "formatting",
382
+ targetKinds: ["plain-text", "table-cell"],
383
+ callback: "onToggleItalic",
384
+ }),
385
+ mk({
386
+ id: "format-underline",
387
+ label: "Underline",
388
+ shortcut: ["Mod", "U"],
389
+ group: "formatting",
390
+ targetKinds: ["plain-text", "table-cell"],
391
+ callback: "onToggleUnderline",
392
+ }),
393
+ mk({
394
+ id: "format-strikethrough",
395
+ label: "Strikethrough",
396
+ group: "formatting",
397
+ targetKinds: ["plain-text", "table-cell"],
398
+ callback: "onToggleStrikethrough",
399
+ }),
400
+ mkArg<string, "onSetParagraphStyle">({
401
+ id: "style-normal",
402
+ label: "Normal text",
403
+ group: "formatting",
404
+ targetKinds: ["plain-text", "table-cell"],
405
+ callback: "onSetParagraphStyle",
406
+ payload: "Normal",
407
+ }),
408
+ mkArg<string, "onSetParagraphStyle">({
409
+ id: "style-heading-1",
410
+ label: "Heading 1",
411
+ shortcut: ["Mod", "Alt", "1"],
412
+ group: "formatting",
413
+ targetKinds: ["plain-text", "table-cell"],
414
+ callback: "onSetParagraphStyle",
415
+ payload: "Heading1",
416
+ }),
417
+ mkArg<string, "onSetParagraphStyle">({
418
+ id: "style-heading-2",
419
+ label: "Heading 2",
420
+ shortcut: ["Mod", "Alt", "2"],
421
+ group: "formatting",
422
+ targetKinds: ["plain-text", "table-cell"],
423
+ callback: "onSetParagraphStyle",
424
+ payload: "Heading2",
425
+ }),
426
+ mkArg<string, "onSetParagraphStyle">({
427
+ id: "style-heading-3",
428
+ label: "Heading 3",
429
+ shortcut: ["Mod", "Alt", "3"],
430
+ group: "formatting",
431
+ targetKinds: ["plain-text", "table-cell"],
432
+ callback: "onSetParagraphStyle",
433
+ payload: "Heading3",
434
+ }),
435
+ mkArg<string, "onSetFontFamily">({
436
+ id: "font-family-aptos",
437
+ label: "Font: Aptos",
438
+ group: "formatting",
439
+ targetKinds: ["plain-text", "table-cell"],
440
+ callback: "onSetFontFamily",
441
+ payload: "Aptos",
442
+ }),
443
+ mkArg<string, "onSetFontFamily">({
444
+ id: "font-family-calibri",
445
+ label: "Font: Calibri",
446
+ group: "formatting",
447
+ targetKinds: ["plain-text", "table-cell"],
448
+ callback: "onSetFontFamily",
449
+ payload: "Calibri",
450
+ }),
451
+ mkArg<number, "onSetFontSize">({
452
+ id: "font-size-11",
453
+ label: "Font size: 11",
454
+ group: "formatting",
455
+ targetKinds: ["plain-text", "table-cell"],
456
+ callback: "onSetFontSize",
457
+ payload: 11,
458
+ }),
459
+ mkArg<number, "onSetFontSize">({
460
+ id: "font-size-12",
461
+ label: "Font size: 12",
462
+ group: "formatting",
463
+ targetKinds: ["plain-text", "table-cell"],
464
+ callback: "onSetFontSize",
465
+ payload: 12,
466
+ }),
467
+ mkArg<string, "onSetTextColor">({
468
+ id: "text-color-black",
469
+ label: "Text color: Black",
470
+ group: "formatting",
471
+ targetKinds: ["plain-text", "table-cell"],
472
+ callback: "onSetTextColor",
473
+ payload: "#000000",
474
+ }),
475
+ mkArg<string, "onSetTextColor">({
476
+ id: "text-color-accent",
477
+ label: "Text color: Accent",
478
+ group: "formatting",
479
+ targetKinds: ["plain-text", "table-cell"],
480
+ callback: "onSetTextColor",
481
+ payload: "#1F6B4F",
482
+ }),
483
+ mkArg<string | null, "onSetHighlightColor">({
484
+ id: "highlight-yellow",
485
+ label: "Highlight: Yellow",
486
+ group: "formatting",
487
+ targetKinds: ["plain-text", "table-cell"],
488
+ callback: "onSetHighlightColor",
489
+ payload: "#FFF2A8",
490
+ }),
491
+ mkArg<string | null, "onSetHighlightColor">({
492
+ id: "highlight-clear",
493
+ label: "Clear highlight",
494
+ group: "formatting",
495
+ targetKinds: ["plain-text", "table-cell"],
496
+ callback: "onSetHighlightColor",
497
+ payload: null,
498
+ }),
499
+ mk({
500
+ id: "list-bulleted",
501
+ label: "Bulleted list",
502
+ group: "formatting",
503
+ targetKinds: ["plain-text", "table-cell"],
504
+ callback: "onToggleBulletedList",
505
+ }),
506
+ mk({
507
+ id: "list-numbered",
508
+ label: "Numbered list",
509
+ group: "formatting",
510
+ targetKinds: ["plain-text", "table-cell"],
511
+ callback: "onToggleNumberedList",
512
+ }),
513
+ mk({
514
+ id: "paragraph-outdent",
515
+ label: "Decrease indent",
516
+ group: "formatting",
517
+ targetKinds: ["plain-text", "table-cell"],
518
+ callback: "onOutdent",
519
+ }),
520
+ mk({
521
+ id: "paragraph-indent",
522
+ label: "Increase indent",
523
+ group: "formatting",
524
+ targetKinds: ["plain-text", "table-cell"],
525
+ callback: "onIndent",
526
+ }),
527
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
528
+ id: "align-left",
529
+ label: "Align left",
530
+ group: "formatting",
531
+ targetKinds: ["plain-text", "table-cell"],
532
+ callback: "onSetAlignment",
533
+ payload: "left",
534
+ }),
535
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
536
+ id: "align-center",
537
+ label: "Align center",
538
+ group: "formatting",
539
+ targetKinds: ["plain-text", "table-cell"],
540
+ callback: "onSetAlignment",
541
+ payload: "center",
542
+ }),
543
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
544
+ id: "align-right",
545
+ label: "Align right",
546
+ group: "formatting",
547
+ targetKinds: ["plain-text", "table-cell"],
548
+ callback: "onSetAlignment",
549
+ payload: "right",
550
+ }),
551
+ mkArg<"left" | "center" | "right" | "justify", "onSetAlignment">({
552
+ id: "align-justify",
553
+ label: "Justify",
554
+ group: "formatting",
555
+ targetKinds: ["plain-text", "table-cell"],
556
+ callback: "onSetAlignment",
557
+ payload: "justify",
558
+ }),
559
+ mk({
560
+ id: "insert-page-break",
561
+ label: "Insert page break",
562
+ shortcut: ["Mod", "Enter"],
563
+ group: "misc",
564
+ targetKinds: ["plain-text", "table-cell"],
565
+ callback: "onInsertPageBreak",
566
+ }),
567
+ mkArg<ProductSectionBreakType, "onInsertSectionBreak">({
568
+ id: "insert-section-break-next-page",
569
+ label: "Insert section break",
570
+ description: "Next page section break",
571
+ group: "misc",
572
+ targetKinds: ["plain-text", "table-cell"],
573
+ callback: "onInsertSectionBreak",
574
+ payload: "nextPage",
575
+ }),
576
+ mkImportant({
577
+ id: "insert-image",
578
+ label: "Insert image…",
579
+ description: "Needs a host file picker before it can run.",
580
+ group: "misc",
581
+ targetKinds: [],
582
+ callback: "onInsertImage",
583
+ }),
584
+ mk({
585
+ id: "insert-table",
586
+ label: "Insert table",
587
+ group: "table",
588
+ targetKinds: ["plain-text"],
589
+ callback: "onInsertTable",
590
+ }),
591
+ mk({
592
+ id: "comment-add",
593
+ label: "Add comment",
594
+ group: "comment",
595
+ targetKinds: ["plain-text", "table-cell"],
596
+ callback: "onAddComment",
597
+ }),
598
+
599
+ // -------- Search / navigation / app-level commands --------
600
+ // Empty targetKinds keeps these out of right-click local menus. The
601
+ // global command palette still projects them from the shared registry.
602
+ mkImportant({
603
+ id: "find",
604
+ label: "Find…",
605
+ description: "Host search panel is not wired.",
606
+ shortcut: ["Mod", "F"],
607
+ group: "misc",
608
+ targetKinds: [],
609
+ callback: "onFindRequested",
610
+ }),
611
+ mkImportant({
612
+ id: "replace",
613
+ label: "Replace…",
614
+ description: "Host replace panel is not wired.",
615
+ shortcut: ["Ctrl", "H"],
616
+ group: "misc",
617
+ targetKinds: [],
618
+ callback: "onReplaceRequested",
619
+ }),
620
+ mkImportant({
621
+ id: "go-to",
622
+ label: "Go to…",
623
+ description: "Host navigation panel is not wired.",
624
+ shortcut: ["Ctrl", "G"],
625
+ group: "misc",
626
+ targetKinds: [],
627
+ callback: "onGoToRequested",
628
+ }),
629
+ mkImportant({
630
+ id: "print",
631
+ label: "Print…",
632
+ description: "Host print/export flow is not wired.",
633
+ shortcut: ["Mod", "P"],
634
+ group: "misc",
635
+ targetKinds: [],
636
+ callback: "onPrintRequested",
637
+ }),
638
+
274
639
  // -------- Suggestion / tracked change --------
275
640
  mk({
276
641
  id: "accept-suggestion",
@@ -379,6 +744,14 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
379
744
  targetKinds: ["table-column", "table-whole"],
380
745
  callback: "onDeleteColumn",
381
746
  }),
747
+ // ── Whole-table destructive op ──
748
+ mk({
749
+ id: "table-delete-table",
750
+ label: "Delete table",
751
+ group: "table",
752
+ targetKinds: ["table-whole"],
753
+ callback: "onDeleteTable",
754
+ }),
382
755
  // ── Whole-table — properties / borders surface at any tier so
383
756
  // they are reachable from a single right-click in a cell ──
384
757
  mk({
@@ -12,9 +12,10 @@
12
12
  *
13
13
  * Progressive-disclosure discipline (designsystem.md §2.1 principle 4):
14
14
  * - Actions without a wired callback are hidden, not rendered
15
- * disabled. The palette is the escape valve for low-frequency
16
- * actions; an empty surface is better than a wall of disabled
17
- * rows.
15
+ * disabled unless the registry marks them as important signposts
16
+ * by returning `"disabled"` from `when()`. Those rows stay visible
17
+ * with an explanation so users can see that the product recognizes
18
+ * the workflow even when the host has not wired it yet.
18
19
  * - Mode-filtered actions (e.g. `scope-open-card` is
19
20
  * workflow/review-only) only appear when the composition mode
20
21
  * allows them.
@@ -33,7 +34,6 @@ import type {
33
34
  CommandPaletteGroup,
34
35
  CommandPaletteItem,
35
36
  } from "./tw-command-palette";
36
- import type { ContextMenuGroupId } from "./tw-context-menu";
37
37
  import type { ShortcutKey } from "./tw-shortcut-hint";
38
38
 
39
39
  /**
@@ -90,43 +90,63 @@ export function formatShortcutForPalette(
90
90
  }
91
91
 
92
92
  /**
93
- * Human-friendly group labels. Kept here because the palette renders
94
- * section headings and the registry's group ids are terse internal
95
- * identifiers.
93
+ * Product-facing command palette buckets. These are intentionally not
94
+ * the context-menu `ContextMenuGroupId`s: right-click menus group by
95
+ * local action family, while the palette groups by user intent.
96
96
  */
97
- const GROUP_LABELS: Record<ContextMenuGroupId, string> = {
98
- clipboard: "Clipboard",
99
- suggestion: "Review",
100
- comment: "Comments",
101
- table: "Table",
102
- formatting: "Formatting",
103
- misc: "More",
97
+ type ProductPaletteGroupId =
98
+ | "commands"
99
+ | "search"
100
+ | "navigation"
101
+ | "mode"
102
+ | "diagnostics";
103
+
104
+ const GROUP_LABELS: Record<ProductPaletteGroupId, string> = {
105
+ commands: "Commands",
106
+ search: "Search",
107
+ navigation: "Navigation",
108
+ mode: "Mode",
109
+ diagnostics: "Diagnostics",
104
110
  };
105
111
 
106
112
  /**
107
- * Stable group order matches the progressive-disclosure priority in
108
- * designsystem.md §2.1 (clipboard first because it's always available,
109
- * review + comment next because they're the primary legal-review
110
- * actions, table + formatting after, misc last).
113
+ * Stable product group order per editor-product Slice 3.
111
114
  */
112
- const GROUP_ORDER: readonly ContextMenuGroupId[] = [
113
- "clipboard",
114
- "suggestion",
115
- "comment",
116
- "table",
117
- "formatting",
118
- "misc",
115
+ const GROUP_ORDER: readonly ProductPaletteGroupId[] = [
116
+ "commands",
117
+ "search",
118
+ "navigation",
119
+ "mode",
120
+ "diagnostics",
119
121
  ];
120
122
 
121
- function isActionAvailable(
123
+ function actionAvailability(
122
124
  action: EditorAction,
123
125
  ctx: EditorActionDispatchContext,
124
- ): boolean {
126
+ ): false | "enabled" | "disabled" {
125
127
  const verdict = action.when ? action.when(ctx) : true;
126
- // "disabled" actions are hidden from the palette. The palette is a
127
- // search-first surface; surfacing disabled rows dilutes the query
128
- // space and clutters the empty state.
129
- return verdict === true;
128
+ if (verdict === false) return false;
129
+ return verdict === "disabled" ? "disabled" : "enabled";
130
+ }
131
+
132
+ function paletteGroupForAction(action: EditorAction): ProductPaletteGroupId {
133
+ switch (action.id) {
134
+ case "find":
135
+ case "replace":
136
+ return "search";
137
+ case "go-to":
138
+ case "jump-to-comment-in-rail":
139
+ case "scope-jump-in-rail":
140
+ case "scope-open-card":
141
+ return "navigation";
142
+ case "scope-mark-resolved":
143
+ return "mode";
144
+ case "object-info":
145
+ case "print":
146
+ return "diagnostics";
147
+ default:
148
+ return "commands";
149
+ }
130
150
  }
131
151
 
132
152
  export interface BuildPaletteGroupsInput {
@@ -145,15 +165,18 @@ export function buildPaletteGroupsFromRegistry(
145
165
  dismiss: input.dismiss,
146
166
  };
147
167
 
148
- const byGroup = new Map<ContextMenuGroupId, CommandPaletteItem[]>();
168
+ const byGroup = new Map<ProductPaletteGroupId, CommandPaletteItem[]>();
149
169
 
150
170
  for (const action of EDITOR_ACTION_REGISTRY) {
151
171
  if (action.modes && !action.modes.has(input.mode)) continue;
152
- if (!isActionAvailable(action, ctx)) continue;
172
+ const availability = actionAvailability(action, ctx);
173
+ if (availability === false) continue;
153
174
 
154
175
  const item: CommandPaletteItem = {
155
176
  id: action.id,
156
177
  label: action.label,
178
+ ...(action.description ? { description: action.description } : {}),
179
+ ...(availability === "disabled" ? { disabled: true } : {}),
157
180
  // Chrome Closure Pass · Task 4 (designsystem.md §6.25) —
158
181
  // preserve the registry shortcut so the palette row shows
159
182
  // the same hint as the matching context-menu / tooltip.
@@ -163,9 +186,10 @@ export function buildPaletteGroupsFromRegistry(
163
186
  onInvoke: () => action.run(ctx),
164
187
  };
165
188
 
166
- const bucket = byGroup.get(action.group) ?? [];
189
+ const groupId = paletteGroupForAction(action);
190
+ const bucket = byGroup.get(groupId) ?? [];
167
191
  bucket.push(item);
168
- byGroup.set(action.group, bucket);
192
+ byGroup.set(groupId, bucket);
169
193
  }
170
194
 
171
195
  const groups: CommandPaletteGroup[] = [];
@@ -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} tableStyles={props.model.tableStyles ?? []}
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 tierKinds =
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