@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
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.80",
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": [
@@ -5611,17 +5611,16 @@ export interface WordReviewEditorProps {
5611
5611
  import("../ui/headless/chrome-registry").SelectionToolRegistryEntry
5612
5612
  >;
5613
5613
  /**
5614
- * Phase C.γ host-callback bag. When supplied, the workspace mounts
5615
- * `TwWorkspaceChromeHost` which in turn mounts the right-click
5616
- * context menu (`TwContextMenuPortal`) and the Ctrl/Cmd+K command
5617
- * palette (`TwCommandPaletteMount`), dispatching through the shared
5618
- * `editorActionRegistry`. Actions without a wired callback are
5619
- * hidden from every surface progressive disclosure per
5620
- * `designsystem.md §2.1 principle 4`.
5621
- *
5622
- * Omit to preserve pre-Phase-C behavior (no right-click menu, no
5623
- * palette). Required for the default editor to expose the
5624
- * Phase C.γ surfaces shipped in `5fce913a`.
5614
+ * Optional host-callback extension bag for workspace command chrome.
5615
+ * The default `<WordReviewEditor />` path now mounts
5616
+ * `TwWorkspaceChromeHost` with product-backed commands for formatting,
5617
+ * paragraph/list/style/font/color actions, search/navigation host
5618
+ * delegation, comments, and table insertion/structure.
5619
+ * Supplying this bag overrides or extends those defaults for host-owned
5620
+ * actions such as custom table properties, hyperlink handling, or
5621
+ * object metadata. Actions without a wired callback are hidden from
5622
+ * every surface progressive disclosure per `designsystem.md §2.1
5623
+ * principle 4`.
5625
5624
  */
5626
5625
  editorActionHost?: import("../ui-tailwind/chrome/editor-action-registry").EditorActionHostCallbacks;
5627
5626
  /**
@@ -5637,8 +5636,8 @@ export interface WordReviewEditorProps {
5637
5636
  /**
5638
5637
  * Suppress the global Ctrl/Cmd+K palette listener — e.g. when a
5639
5638
  * higher-priority modal captures keyboard focus. Defaults to
5640
- * `false` (palette listener is active when `editorActionHost` is
5641
- * supplied).
5639
+ * `false` (the default product command palette listener is active
5640
+ * unless this prop suppresses it).
5642
5641
  */
5643
5642
  commandPaletteDisabled?: boolean;
5644
5643
  /**
@@ -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);
@@ -131,6 +131,7 @@ export interface ChromeCompositionInput {
131
131
  | "simple"
132
132
  | "all";
133
133
  readonly activeRailTab?: EditorRailTab | null;
134
+ readonly railOpen?: boolean;
134
135
  readonly pinnedRailTabs?: ReadonlySet<EditorRailTab>;
135
136
  readonly density?: ChromeDensity;
136
137
  readonly containerWidth?: number;
@@ -255,13 +256,18 @@ function resolveVisibleRailTabs(
255
256
  railOpen: boolean,
256
257
  diagnosticsSignal: DiagnosticsSignal,
257
258
  pinned: ReadonlySet<EditorRailTab>,
259
+ mode: EditorChromeMode,
258
260
  ): ReadonlySet<EditorRailTab> {
259
261
  const visible = new Set<EditorRailTab>();
260
262
  if (railOpen) {
261
263
  visible.add("comments");
262
264
  visible.add("changes");
263
265
  visible.add("workflow");
264
- if (diagnosticsSignal.severity !== "none" || diagnosticsSignal.count > 0) {
266
+ if (
267
+ diagnosticsSignal.severity !== "none" ||
268
+ diagnosticsSignal.count > 0 ||
269
+ mode === "more"
270
+ ) {
265
271
  visible.add("health");
266
272
  }
267
273
  }
@@ -294,11 +300,13 @@ export function resolveChromeComposition(
294
300
  const density: ChromeDensity = input.density ?? "standard";
295
301
  const pinnedRailTabs: ReadonlySet<EditorRailTab> =
296
302
  input.pinnedRailTabs ?? new Set<EditorRailTab>();
297
- const railOpen = options.showReviewRail && visibility.reviewRail;
303
+ const railOpen =
304
+ input.railOpen ?? (options.showReviewRail && visibility.reviewRail);
298
305
  const visibleTabs = resolveVisibleRailTabs(
299
306
  railOpen,
300
307
  diagnosticsSignal,
301
308
  pinnedRailTabs,
309
+ mode,
302
310
  );
303
311
 
304
312
  // Default active tab: honor caller; otherwise land on the mode-appropriate tab.
@@ -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
  /**
@@ -186,6 +186,7 @@ import { EditorShellView } from "./editor-shell-view.tsx";
186
186
  import { TwDebugPresentation } from "../ui-tailwind/debug/index.ts";
187
187
  import { shellPasteFragmentParser as SHELL_PASTE_FRAGMENT_PARSER } from "../shell/paste-adapter.ts";
188
188
  import { EditorSurfaceController } from "./editor-surface-controller.tsx";
189
+ import type { EditorActionHostCallbacks } from "../ui-tailwind/chrome/editor-action-registry";
189
190
  import type { TwWorkspaceChromeHostController } from "../ui-tailwind/chrome/tw-workspace-chrome-host";
190
191
  import {
191
192
  resolveChromePreset,
@@ -3306,6 +3307,100 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3306
3307
  },
3307
3308
  });
3308
3309
 
3310
+ const productEditorActionHost = useMemo<EditorActionHostCallbacks>(() => {
3311
+ const runClipboardCommand = (command: "copy" | "cut") => {
3312
+ try {
3313
+ globalThis.document?.execCommand?.(command);
3314
+ } catch {
3315
+ // Browser-native clipboard commands are best-effort fallbacks.
3316
+ }
3317
+ };
3318
+
3319
+ const defaultHost: EditorActionHostCallbacks = {
3320
+ onUndo: commands.onUndo,
3321
+ onRedo: commands.onRedo,
3322
+ onCut: () => runClipboardCommand("cut"),
3323
+ onCopy: () => runClipboardCommand("copy"),
3324
+ onPaste: () => {
3325
+ const readText = globalThis.navigator?.clipboard?.readText;
3326
+ if (typeof readText !== "function") return;
3327
+ void readText.call(globalThis.navigator.clipboard)
3328
+ .then((text: string) => {
3329
+ if (!text) return;
3330
+ dispatchTextCommand(
3331
+ activeRuntime,
3332
+ { type: "insert-text", text },
3333
+ DISPATCH_CONTEXT,
3334
+ );
3335
+ })
3336
+ .catch(() => {
3337
+ // Clipboard permission failures should not break the menu.
3338
+ });
3339
+ },
3340
+ onToggleBold: commands.onToggleBold,
3341
+ onToggleItalic: commands.onToggleItalic,
3342
+ onToggleUnderline: commands.onToggleUnderline,
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,
3359
+ onToggleBulletedList: commands.onToggleBulletedList,
3360
+ onToggleNumberedList: commands.onToggleNumberedList,
3361
+ onOutdent: commands.onOutdent,
3362
+ onIndent: commands.onIndent,
3363
+ onSetAlignment: (alignment) => commands.onSetAlignment?.(alignment),
3364
+ onInsertPageBreak: commands.onInsertPageBreak,
3365
+ onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
3366
+ onInsertTable: commands.onInsertTable,
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,
3378
+ onInsertRowAbove: commands.onAddRowBefore,
3379
+ onInsertRowBelow: commands.onAddRowAfter,
3380
+ onInsertColumnBefore: commands.onAddColumnBefore,
3381
+ onInsertColumnAfter: commands.onAddColumnAfter,
3382
+ onDeleteRow: commands.onDeleteRow,
3383
+ onDeleteColumn: commands.onDeleteColumn,
3384
+ onDeleteTable: commands.onDeleteTable,
3385
+ onMergeCells: commands.onMergeCells,
3386
+ onSplitCell: commands.onSplitCell,
3387
+ };
3388
+
3389
+ return editorActionHost
3390
+ ? { ...defaultHost, ...editorActionHost }
3391
+ : defaultHost;
3392
+ }, [
3393
+ activeRuntime,
3394
+ commands,
3395
+ editorActionHost,
3396
+ onFindRequested,
3397
+ onGoToRequested,
3398
+ onPrintRequested,
3399
+ onReplaceRequested,
3400
+ snapshot.selection,
3401
+ styleCatalog,
3402
+ ]);
3403
+
3309
3404
  const harnessShowUnsupportedPreviews = readHarnessDebugPortsFlag(__harnessDebugPorts, "unsupportedObjectPreviews");
3310
3405
  const effectiveShowUnsupportedPreviews = computeEffectiveShowUnsupportedPreviews({
3311
3406
  harnessShowUnsupportedPreviews,
@@ -3342,7 +3437,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3342
3437
  activeWorkflowScopeIds={workflowScopeSnapshot?.activeWorkItem?.scopeIds ?? []}
3343
3438
  workflowMetadata={workflowMarkupSnapshot?.metadata}
3344
3439
  onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
3345
- {...(editorActionHost ? { onContextMenuRequested: handleContextMenuRequested } : {})}
3440
+ onContextMenuRequested={handleContextMenuRequested}
3346
3441
  {...editorCallbacks}
3347
3442
  dispatchRuntimeCommand={(command) =>
3348
3443
  activeRuntime.applyActiveStoryTextCommand(command as never)
@@ -3420,8 +3515,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3420
3515
  interactionGuardSnapshot={interactionGuardSnapshot}
3421
3516
  chromePreset={effectiveChromePreset}
3422
3517
  chromeOptions={chromeOptions}
3423
- {...(editorActionHost ? { editorActionHost } : {})}
3424
- {...(editorActionHost ? { chromeControllerRef: teedChromeControllerRef } : {})}
3518
+ density={density}
3519
+ editorActionHost={productEditorActionHost}
3520
+ chromeControllerRef={teedChromeControllerRef}
3425
3521
  {...(commandPaletteDisabled !== undefined
3426
3522
  ? { commandPaletteDisabled }
3427
3523
  : {})}
@@ -4515,6 +4611,39 @@ function createSelectionToolbarStyleBadge(
4515
4611
  return { label: styleEntry.displayName };
4516
4612
  }
4517
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
+
4518
4647
  function createSelectionToolbarListBadge(
4519
4648
  viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>,
4520
4649
  ): SelectionToolbarModel["badges"][number] | null {
@@ -83,6 +83,7 @@ export interface EditorShellViewProps {
83
83
  interactionGuardSnapshot?: InteractionGuardSnapshot;
84
84
  chromePreset?: WordReviewEditorChromePreset;
85
85
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;
86
+ density?: "compact" | "standard" | "comfortable";
86
87
  editorActionHost?: import("../ui-tailwind/chrome/editor-action-registry.ts").EditorActionHostCallbacks;
87
88
  chromeControllerRef?: React.Ref<
88
89
  import("../ui-tailwind/chrome/tw-workspace-chrome-host.tsx").TwWorkspaceChromeHostController