@beyondwork/docx-react-component 1.0.81 → 1.0.83

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.
@@ -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
@@ -80,6 +82,11 @@ export interface EditorActionHostCallbacks {
80
82
  readonly onToggleItalic?: () => void;
81
83
  readonly onToggleUnderline?: () => void;
82
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;
83
90
  readonly onToggleBulletedList?: () => void;
84
91
  readonly onToggleNumberedList?: () => void;
85
92
  readonly onOutdent?: () => void;
@@ -87,9 +94,18 @@ export interface EditorActionHostCallbacks {
87
94
  readonly onSetAlignment?: (
88
95
  alignment: "left" | "center" | "right" | "justify",
89
96
  ) => void;
97
+ readonly onInsertPageBreak?: () => void;
98
+ readonly onInsertSectionBreak?: (type: ProductSectionBreakType) => void;
90
99
  readonly onInsertTable?: () => void;
100
+ readonly onInsertImage?: () => void;
91
101
  readonly onAddComment?: () => void;
92
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
+
93
109
  // Tracked-change operations (suggestion target)
94
110
  readonly onAcceptSuggestion?: () => void;
95
111
  readonly onRejectSuggestion?: () => void;
@@ -134,6 +150,7 @@ export interface EditorActionHostCallbacks {
134
150
  export interface EditorAction {
135
151
  readonly id: string;
136
152
  readonly label: string;
153
+ readonly description?: string;
137
154
  readonly shortcut?: readonly ShortcutKey[];
138
155
  readonly group: ContextMenuGroupId;
139
156
  /** Targets for which this action is relevant. */
@@ -167,6 +184,7 @@ export interface EditorAction {
167
184
  function mk<K extends keyof EditorActionHostCallbacks>(opts: {
168
185
  id: string;
169
186
  label: string;
187
+ description?: string;
170
188
  shortcut?: readonly ShortcutKey[];
171
189
  group: ContextMenuGroupId;
172
190
  targetKinds: readonly TargetKind[];
@@ -178,6 +196,7 @@ function mk<K extends keyof EditorActionHostCallbacks>(opts: {
178
196
  return {
179
197
  id: opts.id,
180
198
  label: opts.label,
199
+ ...(opts.description ? { description: opts.description } : {}),
181
200
  ...(opts.shortcut ? { shortcut: opts.shortcut } : {}),
182
201
  group: opts.group,
183
202
  targetKinds: new Set(opts.targetKinds),
@@ -211,6 +230,7 @@ function mk<K extends keyof EditorActionHostCallbacks>(opts: {
211
230
  function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
212
231
  id: string;
213
232
  label: string;
233
+ description?: string;
214
234
  shortcut?: readonly ShortcutKey[];
215
235
  group: ContextMenuGroupId;
216
236
  targetKinds: readonly TargetKind[];
@@ -222,6 +242,7 @@ function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
222
242
  return {
223
243
  id: opts.id,
224
244
  label: opts.label,
245
+ ...(opts.description ? { description: opts.description } : {}),
225
246
  ...(opts.shortcut ? { shortcut: opts.shortcut } : {}),
226
247
  group: opts.group,
227
248
  targetKinds: new Set(opts.targetKinds),
@@ -239,6 +260,41 @@ function mkArg<A, K extends keyof EditorActionHostCallbacks>(opts: {
239
260
  };
240
261
  }
241
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
+
242
298
  export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
243
299
  // -------- History --------
244
300
  mk({
@@ -341,6 +397,105 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
341
397
  targetKinds: ["plain-text", "table-cell"],
342
398
  callback: "onToggleStrikethrough",
343
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
+ }),
344
499
  mk({
345
500
  id: "list-bulleted",
346
501
  label: "Bulleted list",
@@ -401,6 +556,31 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
401
556
  callback: "onSetAlignment",
402
557
  payload: "justify",
403
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
+ }),
404
584
  mk({
405
585
  id: "insert-table",
406
586
  label: "Insert table",
@@ -416,6 +596,46 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
416
596
  callback: "onAddComment",
417
597
  }),
418
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
+
419
639
  // -------- Suggestion / tracked change --------
420
640
  mk({
421
641
  id: "accept-suggestion",
@@ -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[] = [];
@@ -251,6 +251,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
251
251
  {pageStackScrollRoot !== undefined ? (
252
252
  <TwPageStackChromeLayer
253
253
  facet={facet}
254
+ geometryFacet={geometryFacet}
254
255
  scrollRoot={pageStackScrollRoot}
255
256
  renderFrameRevision={renderFrameRevision ?? 0}
256
257
  activeStory={activeStory ?? { kind: "main" }}