@beyondwork/docx-react-component 1.0.81 → 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.81",
4
+ "version": "1.0.82",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -5614,7 +5614,8 @@ export interface WordReviewEditorProps {
5614
5614
  * Optional host-callback extension bag for workspace command chrome.
5615
5615
  * The default `<WordReviewEditor />` path now mounts
5616
5616
  * `TwWorkspaceChromeHost` with product-backed commands for formatting,
5617
- * paragraph/list actions, comments, and table insertion/structure.
5617
+ * paragraph/list/style/font/color actions, search/navigation host
5618
+ * delegation, comments, and table insertion/structure.
5618
5619
  * Supplying this bag overrides or extends those defaults for host-owned
5619
5620
  * actions such as custom table properties, hyperlink handling, or
5620
5621
  * object metadata. Actions without a wired callback are hidden from
@@ -43,6 +43,7 @@ export type RuntimeApiHandle = Pick<
43
43
  DocumentRuntime,
44
44
  // Session + export (runtime.document family)
45
45
  | "getSessionState"
46
+ | "setDocumentMode"
46
47
  | "exportDocx"
47
48
  | "getCompatibilityReport"
48
49
  | "getWarnings"
@@ -54,6 +55,7 @@ export type RuntimeApiHandle = Pick<
54
55
  | "findAllText"
55
56
  // Review (runtime.review family)
56
57
  | "getReviewWorkSnapshot"
58
+ | "getSuggestionsSnapshot"
57
59
  | "acceptChange"
58
60
  | "rejectChange"
59
61
  | "resolveComment"
@@ -136,6 +138,7 @@ export type RuntimeApiHandle = Pick<
136
138
  */
137
139
  export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true> = {
138
140
  getSessionState: true,
141
+ setDocumentMode: true,
139
142
  exportDocx: true,
140
143
  getCompatibilityReport: true,
141
144
  getWarnings: true,
@@ -143,6 +146,7 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
143
146
  getCanonicalDocument: true,
144
147
  findAllText: true,
145
148
  getReviewWorkSnapshot: true,
149
+ getSuggestionsSnapshot: true,
146
150
  acceptChange: true,
147
151
  rejectChange: true,
148
152
  resolveComment: true,
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * @endStateApi v3 — `runtime.document` family.
3
3
  *
4
- * See docs/reference/public-api.md § runtime.document. Three functions:
4
+ * See docs/reference/public-api.md § runtime.document.
5
5
  * `load` (partial — runtime pre-load is the caller's responsibility; v3
6
- * exposes a re-mount semantic later), `export` (live; delegates to
7
- * `runtime.exportDocx`), `validate` (partial; read live, write mock).
6
+ * exposes a re-mount semantic later), `getMode` / `setMode` (live;
7
+ * delegates to the runtime view-state posture), `export` (live; delegates
8
+ * to `runtime.exportDocx`), `validate` (partial; read live, write mock).
8
9
  */
9
10
 
10
11
  import type { RuntimeApiHandle } from "../_runtime-handle.ts";
11
12
  import type {
13
+ DocumentMode,
12
14
  EditorError,
13
15
  ExportDocxOptions,
14
16
  ExportResult,
@@ -88,6 +90,42 @@ export const loadMetadata: ApiV3FnMetadata = {
88
90
  "§Runtime API § runtime.document.load. Graduation (2026-04-22, post-eb7d14fa): `live` via direct delegation to `loadDocxSessionAsync` (src/session/import/loader.ts). Returns a PersistedEditorSnapshot the caller can pass to DocxSession.reopenFromSnapshot or persist for later rehydrate. Note per arch §R8 Option B: v3 does NOT construct the receiving DocumentRuntime — that's the caller's job via createDocumentRuntime(initialSessionState).",
89
91
  };
90
92
 
93
+ /* ================================================================== */
94
+ /* mode */
95
+ /* ================================================================== */
96
+
97
+ export const getModeMetadata: ApiV3FnMetadata = {
98
+ name: "runtime.document.getMode",
99
+ status: "live",
100
+ sourceLayer: "runtime-core",
101
+ liveEvidence: {
102
+ runnerTest: "test/api/v3/create-accepts-handle.test.ts",
103
+ commit: "refactor-03-tracked-changes-v1-api-adapter",
104
+ },
105
+ uxIntent: { uiVisible: false, expectsUxResponse: "none" },
106
+ agentMetadata: { readOrMutate: "read", boundedScope: "document", auditCategory: "document-mode" },
107
+ stateClass: "C-local",
108
+ persistsTo: "none",
109
+ rwdReference:
110
+ "§Runtime API § runtime.document.getMode. Live adapter over the runtime render snapshot's DocumentMode; suggesting remains the tracked-change authoring posture.",
111
+ };
112
+
113
+ export const setModeMetadata: ApiV3FnMetadata = {
114
+ name: "runtime.document.setMode",
115
+ status: "live",
116
+ sourceLayer: "runtime-core",
117
+ liveEvidence: {
118
+ runnerTest: "test/api/v3/create-accepts-handle.test.ts",
119
+ commit: "refactor-03-tracked-changes-v1-api-adapter",
120
+ },
121
+ uxIntent: { uiVisible: true, expectsUxResponse: "surface-refresh", expectedDelta: "document mode changes" },
122
+ agentMetadata: { readOrMutate: "mutate", boundedScope: "document", auditCategory: "document-mode" },
123
+ stateClass: "C-local",
124
+ persistsTo: "none",
125
+ rwdReference:
126
+ "§Runtime API § runtime.document.setMode. Live adapter over runtime.setDocumentMode(); mode 'suggesting' is the v3 entry to tracked-change authoring.",
127
+ };
128
+
91
129
  /* ================================================================== */
92
130
  /* export */
93
131
  /* ================================================================== */
@@ -196,6 +234,26 @@ export function createDocumentFamily(runtime: RuntimeApiHandle) {
196
234
  return result;
197
235
  },
198
236
 
237
+ getMode(): DocumentMode {
238
+ // @endStateApi — live. Reads the runtime view-state posture that
239
+ // render snapshots already expose.
240
+ return runtime.getRenderSnapshot().documentMode;
241
+ },
242
+
243
+ setMode(mode: DocumentMode): void {
244
+ // @endStateApi — live. Delegates to the runtime's document-mode
245
+ // setter; `suggesting` is the tracked-change authoring posture.
246
+ runtime.setDocumentMode(mode);
247
+ emitUxResponse(runtime, {
248
+ apiFn: setModeMetadata.name,
249
+ intent: setModeMetadata.uxIntent.expectedDelta ?? "",
250
+ mockOrLive: "live",
251
+ uiVisible: true,
252
+ expectedDelta: setModeMetadata.uxIntent.expectedDelta,
253
+ actualDelta: { kind: "surface-refresh", payload: { mode } },
254
+ });
255
+ },
256
+
199
257
  async export(options?: ExportDocxOptions): Promise<ExportResult> {
200
258
  // @endStateApi — live. Delegates to the shipped runtime export path.
201
259
  const result = await runtime.exportDocx(options);
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * @endStateApi v3 — `runtime.review` family.
3
3
  *
4
- * getComments (live) / getChanges (live) / acceptChange (live) /
5
- * resolveComment (live).
4
+ * getComments (live) / getChanges (live) / getSuggestions (live) /
5
+ * acceptChange (live) / rejectChange (live) / resolveComment (live).
6
6
  */
7
7
 
8
8
  import type { RuntimeApiHandle } from "../_runtime-handle.ts";
9
9
  import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
10
10
  import type {
11
11
  CommentSidebarThreadSnapshot,
12
+ SuggestionsSnapshot,
12
13
  TrackedChangeEntrySnapshot,
13
14
  } from "../../public-types.ts";
14
15
  import { emitUxResponse } from "../_ux-response.ts";
@@ -51,6 +52,22 @@ export const getChangesMetadata: ApiV3FnMetadata = {
51
52
  rwdReference: "§Runtime API § runtime.review.getChanges",
52
53
  };
53
54
 
55
+ export const getSuggestionsMetadata: ApiV3FnMetadata = {
56
+ name: "runtime.review.getSuggestions",
57
+ status: "live",
58
+ sourceLayer: "workflow-review",
59
+ liveEvidence: {
60
+ runnerTest: "test/api/v3/create-accepts-handle.test.ts",
61
+ commit: "refactor-03-tracked-changes-v1-api-adapter",
62
+ },
63
+ uxIntent: { uiVisible: false, expectsUxResponse: "none" },
64
+ agentMetadata: { readOrMutate: "read", boundedScope: "document", auditCategory: "review-read" },
65
+ stateClass: "A-canonical",
66
+ persistsTo: "canonical",
67
+ rwdReference:
68
+ "§Runtime API § runtime.review.getSuggestions. Live adapter over runtime.getSuggestionsSnapshot(); semantic suggestion readback is grouped by the runtime, not by v3.",
69
+ };
70
+
54
71
  export const acceptChangeMetadata: ApiV3FnMetadata = {
55
72
  name: "runtime.review.acceptChange",
56
73
  status: "live",
@@ -70,6 +87,23 @@ export const acceptChangeMetadata: ApiV3FnMetadata = {
70
87
  rwdReference: "§Runtime API § runtime.review.acceptChange",
71
88
  };
72
89
 
90
+ export const rejectChangeMetadata: ApiV3FnMetadata = {
91
+ name: "runtime.review.rejectChange",
92
+ status: "live",
93
+ sourceLayer: "workflow-review",
94
+ liveEvidence: {
95
+ runnerTest: "test/api/v3/create-accepts-handle.test.ts",
96
+ commit: "refactor-03-tracked-changes-v1-api-adapter",
97
+ },
98
+ uxIntent: { uiVisible: true, expectsUxResponse: "inline-change", expectedDelta: "change mark disappears and text restores" },
99
+ agentMetadata: { readOrMutate: "mutate", boundedScope: "scope", auditCategory: "change-reject" },
100
+ stateClass: "A-canonical",
101
+ persistsTo: "canonical",
102
+ broadcastsVia: "crdt",
103
+ rwdReference:
104
+ "§Runtime API § runtime.review.rejectChange. Live adapter over runtime.rejectChange; mirrors acceptChange for individual tracked-change review.",
105
+ };
106
+
73
107
  export const resolveCommentMetadata: ApiV3FnMetadata = {
74
108
  name: "runtime.review.resolveComment",
75
109
  status: "live",
@@ -100,6 +134,12 @@ export function createReviewFamily(runtime: RuntimeApiHandle) {
100
134
  return runtime.getRenderSnapshot().trackedChanges.revisions;
101
135
  },
102
136
 
137
+ getSuggestions(): SuggestionsSnapshot {
138
+ // @endStateApi — live. Delegates to the runtime's semantic
139
+ // suggestion grouping rather than regrouping raw revisions here.
140
+ return runtime.getSuggestionsSnapshot();
141
+ },
142
+
103
143
  acceptChange(changeId: string): void {
104
144
  // @endStateApi — live. Delegates.
105
145
  runtime.acceptChange(changeId);
@@ -113,6 +153,19 @@ export function createReviewFamily(runtime: RuntimeApiHandle) {
113
153
  });
114
154
  },
115
155
 
156
+ rejectChange(changeId: string): void {
157
+ // @endStateApi — live. Delegates.
158
+ runtime.rejectChange(changeId);
159
+ emitUxResponse(runtime, {
160
+ apiFn: rejectChangeMetadata.name,
161
+ intent: rejectChangeMetadata.uxIntent.expectedDelta ?? "",
162
+ mockOrLive: "live",
163
+ uiVisible: true,
164
+ expectedDelta: rejectChangeMetadata.uxIntent.expectedDelta,
165
+ actualDelta: { kind: "inline-change", payload: { changeId } },
166
+ });
167
+ },
168
+
116
169
  resolveComment(commentId: string): void {
117
170
  // @endStateApi — live.
118
171
  runtime.resolveComment(commentId);
@@ -671,7 +671,10 @@ function normalizeDrawingFrameNode(
671
671
  const filename = packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin";
672
672
  state.media.items[node.content.mediaId] = {
673
673
  mediaId: node.content.mediaId,
674
- contentType: existingMediaItem?.contentType ?? "application/octet-stream",
674
+ contentType:
675
+ node.content.contentType ??
676
+ existingMediaItem?.contentType ??
677
+ "application/octet-stream",
675
678
  filename,
676
679
  packagePartName,
677
680
  relationshipId: node.content.blipRef,
@@ -188,8 +188,12 @@ function resolveContent(
188
188
  const partPath = normalizePartPath(
189
189
  resolveRelationshipTarget(opts.sourcePartPath ?? "/word/document.xml", rel),
190
190
  );
191
+ const mediaPart = opts.mediaParts?.get(partPath);
191
192
  pic.packagePartName = partPath;
192
193
  pic.mediaId = `media:${partPath.slice(1)}`;
194
+ if (mediaPart?.contentType) {
195
+ pic.contentType = mediaPart.contentType;
196
+ }
193
197
  }
194
198
  // F4.1 — preserve outer drawing XML for lossless round-trip serialization
195
199
  pic.rawXml = rawXml;
@@ -1922,6 +1922,8 @@ export interface PictureContent {
1922
1922
  mediaId?: string;
1923
1923
  /** Absolute package path for media catalog lookup. */
1924
1924
  packagePartName?: string;
1925
+ /** MIME resolved from the OPC media part, when known. */
1926
+ contentType?: string;
1925
1927
  srcRect?: { top: number; bottom: number; left: number; right: number };
1926
1928
  stretch?: boolean;
1927
1929
  /**
@@ -3341,13 +3341,40 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3341
3341
  onToggleItalic: commands.onToggleItalic,
3342
3342
  onToggleUnderline: commands.onToggleUnderline,
3343
3343
  onToggleStrikethrough: commands.onToggleStrikethrough,
3344
+ onSetParagraphStyle: (styleId) => {
3345
+ const resolvedStyleId = resolveProductParagraphStyleId(styleCatalog, styleId);
3346
+ if (!resolvedStyleId) {
3347
+ activeRuntime.emitBlockedCommand("setParagraphStyle", [{
3348
+ code: "unsupported_surface",
3349
+ message: `${styleId} is not available in this document's style catalog.`,
3350
+ }]);
3351
+ return;
3352
+ }
3353
+ commands.onSetParagraphStyle?.(resolvedStyleId);
3354
+ },
3355
+ onSetFontFamily: commands.onSetFontFamily,
3356
+ onSetFontSize: commands.onSetFontSize,
3357
+ onSetTextColor: commands.onSetTextColor,
3358
+ onSetHighlightColor: commands.onSetHighlightColor,
3344
3359
  onToggleBulletedList: commands.onToggleBulletedList,
3345
3360
  onToggleNumberedList: commands.onToggleNumberedList,
3346
3361
  onOutdent: commands.onOutdent,
3347
3362
  onIndent: commands.onIndent,
3348
3363
  onSetAlignment: (alignment) => commands.onSetAlignment?.(alignment),
3364
+ onInsertPageBreak: commands.onInsertPageBreak,
3365
+ onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
3349
3366
  onInsertTable: commands.onInsertTable,
3350
3367
  onAddComment: commands.onAddComment,
3368
+ onFindRequested: onFindRequested
3369
+ ? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
3370
+ : undefined,
3371
+ onReplaceRequested: onReplaceRequested
3372
+ ? () => onReplaceRequested({ selectionText: "", selectionRange: snapshot.selection })
3373
+ : undefined,
3374
+ onPrintRequested,
3375
+ onGoToRequested: onGoToRequested
3376
+ ? () => onGoToRequested({ selectionText: "", selectionRange: snapshot.selection })
3377
+ : undefined,
3351
3378
  onInsertRowAbove: commands.onAddRowBefore,
3352
3379
  onInsertRowBelow: commands.onAddRowAfter,
3353
3380
  onInsertColumnBefore: commands.onAddColumnBefore,
@@ -3362,7 +3389,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3362
3389
  return editorActionHost
3363
3390
  ? { ...defaultHost, ...editorActionHost }
3364
3391
  : defaultHost;
3365
- }, [activeRuntime, commands, editorActionHost]);
3392
+ }, [
3393
+ activeRuntime,
3394
+ commands,
3395
+ editorActionHost,
3396
+ onFindRequested,
3397
+ onGoToRequested,
3398
+ onPrintRequested,
3399
+ onReplaceRequested,
3400
+ snapshot.selection,
3401
+ styleCatalog,
3402
+ ]);
3366
3403
 
3367
3404
  const harnessShowUnsupportedPreviews = readHarnessDebugPortsFlag(__harnessDebugPorts, "unsupportedObjectPreviews");
3368
3405
  const effectiveShowUnsupportedPreviews = computeEffectiveShowUnsupportedPreviews({
@@ -4574,6 +4611,39 @@ function createSelectionToolbarStyleBadge(
4574
4611
  return { label: styleEntry.displayName };
4575
4612
  }
4576
4613
 
4614
+ function resolveProductParagraphStyleId(
4615
+ styleCatalog: StyleCatalogSnapshot,
4616
+ requestedStyleId: string,
4617
+ ): string | null {
4618
+ switch (requestedStyleId) {
4619
+ case "Heading1":
4620
+ return resolveHeadingShortcutStyleId(styleCatalog, 1);
4621
+ case "Heading2":
4622
+ return resolveHeadingShortcutStyleId(styleCatalog, 2);
4623
+ case "Heading3":
4624
+ return resolveHeadingShortcutStyleId(styleCatalog, 3);
4625
+ case "Normal": {
4626
+ const defaultStyle = styleCatalog.paragraphs.find((entry) => entry.isDefault);
4627
+ if (defaultStyle) return defaultStyle.styleId;
4628
+ break;
4629
+ }
4630
+ default:
4631
+ break;
4632
+ }
4633
+
4634
+ const requestedToken = normalizeProductStyleToken(requestedStyleId);
4635
+ const match = styleCatalog.paragraphs.find(
4636
+ (entry) =>
4637
+ normalizeProductStyleToken(entry.styleId) === requestedToken ||
4638
+ normalizeProductStyleToken(entry.displayName) === requestedToken,
4639
+ );
4640
+ return match?.styleId ?? null;
4641
+ }
4642
+
4643
+ function normalizeProductStyleToken(value: string): string {
4644
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
4645
+ }
4646
+
4577
4647
  function createSelectionToolbarListBadge(
4578
4648
  viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>,
4579
4649
  ): SelectionToolbarModel["badges"][number] | null {
@@ -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",