@beyondwork/docx-react-component 1.0.42 → 1.0.45

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 (82) hide show
  1. package/README.md +17 -0
  2. package/package.json +5 -4
  3. package/src/api/editor-state-types.ts +110 -0
  4. package/src/api/public-types.ts +333 -4
  5. package/src/core/commands/formatting-commands.ts +7 -1
  6. package/src/core/commands/index.ts +60 -10
  7. package/src/core/commands/text-commands.ts +59 -0
  8. package/src/core/search/search-text.ts +15 -2
  9. package/src/core/selection/review-anchors.ts +131 -21
  10. package/src/index.ts +29 -1
  11. package/src/io/chart-preview-resolver.ts +281 -0
  12. package/src/io/docx-session.ts +692 -2
  13. package/src/io/export/build-app-properties-xml.ts +1 -1
  14. package/src/io/export/serialize-comments.ts +38 -9
  15. package/src/io/export/twip.ts +1 -1
  16. package/src/io/load-scheduler.ts +230 -0
  17. package/src/io/normalize/normalize-text.ts +116 -0
  18. package/src/io/ooxml/parse-comments.ts +0 -33
  19. package/src/io/ooxml/parse-complex-content.ts +14 -0
  20. package/src/io/ooxml/parse-main-document.ts +4 -0
  21. package/src/io/ooxml/workflow-payload-validator.ts +97 -1
  22. package/src/io/ooxml/workflow-payload.ts +172 -1
  23. package/src/preservation/opaque-region.ts +5 -0
  24. package/src/review/store/comment-remapping.ts +2 -2
  25. package/src/runtime/collab-session.ts +1 -1
  26. package/src/runtime/document-runtime.ts +661 -42
  27. package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
  28. package/src/runtime/edit-dispatch/index.ts +2 -0
  29. package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
  30. package/src/runtime/editor-state-channel.ts +544 -0
  31. package/src/runtime/editor-state-integration.ts +217 -0
  32. package/src/runtime/editor-surface/capabilities.ts +411 -0
  33. package/src/runtime/layout/index.ts +2 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +63 -2
  36. package/src/runtime/layout/layout-engine-version.ts +41 -0
  37. package/src/runtime/layout/paginated-layout-engine.ts +211 -14
  38. package/src/runtime/layout/public-facet.ts +430 -1
  39. package/src/runtime/perf-counters.ts +28 -0
  40. package/src/runtime/prerender/cache-envelope.ts +29 -0
  41. package/src/runtime/prerender/cache-key.ts +66 -0
  42. package/src/runtime/prerender/font-fingerprint.ts +17 -0
  43. package/src/runtime/prerender/graph-canonicalize.ts +121 -0
  44. package/src/runtime/prerender/indexeddb-cache.ts +184 -0
  45. package/src/runtime/prerender/prerender-document.ts +145 -0
  46. package/src/runtime/render/block-fragment-projection.ts +2 -0
  47. package/src/runtime/render/render-frame-types.ts +17 -0
  48. package/src/runtime/render/render-kernel.ts +172 -29
  49. package/src/runtime/selection/post-edit-validator.ts +77 -0
  50. package/src/runtime/surface-projection.ts +45 -7
  51. package/src/runtime/workflow-markup.ts +71 -16
  52. package/src/ui/WordReviewEditor.tsx +142 -237
  53. package/src/ui/editor-command-bag.ts +14 -0
  54. package/src/ui/editor-runtime-boundary.ts +115 -12
  55. package/src/ui/editor-shell-view.tsx +10 -0
  56. package/src/ui/editor-surface-controller.tsx +5 -0
  57. package/src/ui/headless/selection-helpers.ts +10 -0
  58. package/src/ui/runtime-shortcut-dispatch.ts +28 -68
  59. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  60. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
  62. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  63. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  64. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
  65. package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
  66. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
  67. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  68. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  69. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
  70. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
  71. package/src/ui-tailwind/index.ts +5 -1
  72. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  73. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  74. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  75. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  76. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  77. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  78. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
  79. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  80. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  81. package/src/ui-tailwind/theme/editor-theme.css +47 -14
  82. package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
@@ -0,0 +1,217 @@
1
+ /**
2
+ * editor-state-integration.ts — Schema 1.2 Task D + E helpers.
3
+ *
4
+ * Factored out of document-runtime.ts / docx-session.ts to keep
5
+ * those files focused. Two entry points:
6
+ *
7
+ * - `hydrateEditorStateFromEnvelope`: called right after the runtime
8
+ * is created and the envelope parsed; drives the load-path.
9
+ * - `collectEditorStateForSerialize`: called inside exportDocx before
10
+ * `buildWorkflowPayloadParts`; drives the save-path.
11
+ */
12
+
13
+ import type { EditorStateNamespace } from "../api/editor-state-types.ts";
14
+ import type { EditorStateChannel } from "./editor-state-channel.ts";
15
+ import type {
16
+ EditorStatePayload,
17
+ EditorStatePayloadNamespaceEntry,
18
+ } from "../io/ooxml/workflow-payload.ts";
19
+
20
+ // All namespaces the runtime currently knows about.
21
+ export const ALL_EDITOR_STATE_NAMESPACES: readonly EditorStateNamespace[] = [
22
+ "hostAnnotations",
23
+ "workflowOverlay",
24
+ "workflowMetadata",
25
+ "workItems",
26
+ ] as const;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Load-path: hydrateEditorStateFromEnvelope
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface HydrateEditorStateArgs {
33
+ /** The editorState block parsed from the workflow-payload envelope. */
34
+ editorState: EditorStatePayload;
35
+ channel: EditorStateChannel;
36
+ /**
37
+ * Called for each namespace whose blob has been resolved/loaded.
38
+ * Responsible for applying the blob to the appropriate subsystem store
39
+ * (e.g. calling runtime.setHostAnnotationOverlay).
40
+ */
41
+ applyBlob: (namespace: EditorStateNamespace, data: unknown) => void;
42
+ }
43
+
44
+ /**
45
+ * Drives the load-path for schema 1.2 editor-state entries.
46
+ *
47
+ * - Unknown namespaces → preserved opaquely, `unknown_namespace` event.
48
+ * - Inline entries under in-document policy → applied directly.
49
+ * - Keyed entries → resolver called; result applied (or failure handled).
50
+ * - Policy mismatch → `policy_migrated` event.
51
+ *
52
+ * Returns a promise that resolves when all namespaces have been
53
+ * processed. Rejects only when `onResolveError` is `"block"` for a
54
+ * namespace and the resolver fails — the caller should fail the load.
55
+ */
56
+ export async function hydrateEditorStateFromEnvelope(
57
+ args: HydrateEditorStateArgs,
58
+ ): Promise<void> {
59
+ const { editorState, channel, applyBlob } = args;
60
+
61
+ // Record unknown namespaces first — they are preserved opaquely by
62
+ // the payload layer; we emit the warning event AND hand the raw XML
63
+ // to the channel so the next save round-trips it verbatim.
64
+ for (const unknown of editorState.unknownNamespaces ?? []) {
65
+ channel.recordUnknownNamespace(unknown.name, { rawXml: unknown.rawXml });
66
+ }
67
+
68
+ for (const entry of editorState.entries) {
69
+ await hydrateEntry({ entry, channel, applyBlob });
70
+ }
71
+ }
72
+
73
+ async function hydrateEntry(args: {
74
+ entry: EditorStatePayloadNamespaceEntry;
75
+ channel: EditorStateChannel;
76
+ applyBlob: (namespace: EditorStateNamespace, data: unknown) => void;
77
+ }): Promise<void> {
78
+ const { entry, channel, applyBlob } = args;
79
+ const ns = entry.namespace;
80
+ const policyEntry = channel.getPolicyEntry(ns);
81
+
82
+ // Malformed inline JSON: surface as a load failure; don't apply.
83
+ if (entry.malformedInline) {
84
+ channel.recordLoadFailure({
85
+ namespace: ns,
86
+ error: new Error(`Malformed inline JSON for namespace "${ns}"`),
87
+ fallback: policyEntry.onResolveError === "block" ? "empty" : policyEntry.onResolveError,
88
+ });
89
+ return;
90
+ }
91
+
92
+ // Policy-migration detection: compare payload-written location vs
93
+ // current policy location.
94
+ const payloadLocation: string = entry.storageRef
95
+ ? entry.storageRef.location
96
+ : "in-document";
97
+ if (payloadLocation !== policyEntry.location) {
98
+ channel.recordPolicyMigration({
99
+ namespace: ns,
100
+ from: payloadLocation as import("../api/editor-state-types.ts").EditorStateLocation,
101
+ to: policyEntry.location,
102
+ key: entry.storageRef?.entryKey,
103
+ });
104
+ }
105
+
106
+ // Inline path: apply directly when both payload and policy agree on
107
+ // in-document.
108
+ if (entry.inline !== undefined && policyEntry.location === "in-document") {
109
+ applyBlob(ns, entry.inline);
110
+ channel.recordLoaded(ns, {
111
+ namespace: ns,
112
+ schemaVersion: entry.schemaVersion,
113
+ data: entry.inline,
114
+ });
115
+ return;
116
+ }
117
+
118
+ // Keyed path: set the key from the payload, then call the resolver.
119
+ if (entry.storageRef) {
120
+ channel.setKey(ns, entry.storageRef.entryKey);
121
+ // Under keyed policy the resolver wins over any inline blob.
122
+ const result = await channel.resolve(ns, entry.storageRef.entryKey);
123
+ if (result.blob !== null) {
124
+ applyBlob(ns, result.blob.data);
125
+ if (!result.appliedFallback) {
126
+ channel.recordLoaded(ns, result.blob);
127
+ }
128
+ }
129
+ // result.blob === null → failure already handled by channel (event
130
+ // emitted, fallback mode applied). Nothing more to apply.
131
+ return;
132
+ }
133
+
134
+ // Mismatch: payload is inline but policy is keyed. Apply inline as
135
+ // fallback (no key to resolve against).
136
+ if (entry.inline !== undefined) {
137
+ applyBlob(ns, entry.inline);
138
+ channel.recordLoaded(ns, {
139
+ namespace: ns,
140
+ schemaVersion: entry.schemaVersion,
141
+ data: entry.inline,
142
+ });
143
+ }
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Save-path: collectEditorStateForSerialize
148
+ // ---------------------------------------------------------------------------
149
+
150
+ export interface CollectEditorStateArgs {
151
+ channel: EditorStateChannel;
152
+ /**
153
+ * Returns the current in-memory blob for a namespace, or null if
154
+ * the namespace has no data to persist.
155
+ */
156
+ getNamespaceData: (ns: EditorStateNamespace) => { schemaVersion: string; data: unknown } | null;
157
+ }
158
+
159
+ /**
160
+ * Builds the `EditorStatePayload` for the serializer.
161
+ *
162
+ * 1. Flushes any pending debounced persists so the docx captures
163
+ * the last-known-good state for rowstore namespaces.
164
+ * 2. For each namespace with data: emits inline or storageRef per policy.
165
+ * 3. Returns undefined when no namespaces have data — the serializer
166
+ * then omits `<bw:editorState>` entirely (downgrade to 1.1/1.0).
167
+ */
168
+ export async function collectEditorStateForSerialize(
169
+ args: CollectEditorStateArgs,
170
+ ): Promise<EditorStatePayload | undefined> {
171
+ const { channel, getNamespaceData } = args;
172
+
173
+ // Flush pending debounced persists before serialize resolves.
174
+ await channel.flush();
175
+
176
+ const entries: EditorStatePayloadNamespaceEntry[] = [];
177
+
178
+ for (const ns of ALL_EDITOR_STATE_NAMESPACES) {
179
+ const current = getNamespaceData(ns);
180
+ if (!current) continue;
181
+
182
+ const policyEntry = channel.getPolicyEntry(ns);
183
+
184
+ if (policyEntry.location === "in-document") {
185
+ entries.push({
186
+ namespace: ns,
187
+ schemaVersion: current.schemaVersion,
188
+ inline: current.data,
189
+ });
190
+ } else {
191
+ const key = channel.getKey(ns);
192
+ if (!key) continue; // Keyed policy without a key — can't serialize ref.
193
+ entries.push({
194
+ namespace: ns,
195
+ schemaVersion: current.schemaVersion,
196
+ storageRef: {
197
+ location: policyEntry.location as Exclude<
198
+ import("../api/editor-state-types.ts").EditorStateLocation,
199
+ "in-document"
200
+ >,
201
+ entryKey: key,
202
+ },
203
+ });
204
+ }
205
+ }
206
+
207
+ const unknownEntries = channel.getUnknownEntries();
208
+ const unknownNamespaces = unknownEntries
209
+ .filter((u): u is typeof u & { rawXml: string } => typeof u.rawXml === "string")
210
+ .map((u) => ({ name: u.name, rawXml: u.rawXml }));
211
+
212
+ if (entries.length === 0 && unknownNamespaces.length === 0) return undefined;
213
+ return {
214
+ entries,
215
+ ...(unknownNamespaces.length > 0 ? { unknownNamespaces } : {}),
216
+ };
217
+ }
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Editor capability table — the single source of truth for every
3
+ * keyboard shortcut, context-menu action, and surface dispatch the
4
+ * mounted editor knows about.
5
+ *
6
+ * An `EditorCapability` classifies a binding into one of four kinds:
7
+ * - `supported` — the editor owns the mutation (runtime command
8
+ * or shell op).
9
+ * - `host-delegated` — the editor recognizes the input but hands it
10
+ * off to the host via a typed callback prop.
11
+ * Host event name lives in `hostEvent`.
12
+ * - `blocked` — the editor explicitly rejects the input. The
13
+ * host sees a `command_blocked` event with the
14
+ * `blockReason.code` and `blockReason.message`.
15
+ * - `passthrough` — the editor does not intercept; the browser
16
+ * default behavior wins.
17
+ *
18
+ * This table is consumed by:
19
+ * - `src/ui/runtime-shortcut-dispatch.ts` (I3 C.2) — each resolution
20
+ * branch looks up by id rather than hard-coding block reasons.
21
+ * - `docs/reference/editor-capabilities.md` (I3 C.4) — generated
22
+ * from this table so the wiki + reference doc stay in sync.
23
+ * - `src/ui/WordReviewEditor.tsx` (I3 C.3) — `host-delegated`
24
+ * entries wire their `hostEvent` to the matching prop on
25
+ * `WordReviewEditorProps`.
26
+ *
27
+ * C.1 (this file): enumerate every shortcut already present in
28
+ * `resolveShellShortcut` / `resolveSurfaceShortcut` / the PM
29
+ * Alt+Shift+arrow bindings. No behavior change — C.2 is the
30
+ * dispatcher refactor.
31
+ *
32
+ * Scope: keyboard shortcuts only in C.1. Context-menu capabilities
33
+ * land alongside the right-click hook (Phase F) in a later commit.
34
+ */
35
+
36
+ export type CapabilityKind = "supported" | "host-delegated" | "blocked" | "passthrough";
37
+
38
+ export type CapabilityCategory =
39
+ | "text-formatting"
40
+ | "paragraph-formatting"
41
+ | "navigation"
42
+ | "selection"
43
+ | "clipboard"
44
+ | "history"
45
+ | "tracked-changes"
46
+ | "comments"
47
+ | "structure"
48
+ | "system";
49
+
50
+ export interface CapabilityShortcut {
51
+ /** Canonical Windows / Linux binding string (e.g. "Ctrl+B", "Shift+Tab"). */
52
+ winLinux: string;
53
+ /** Canonical Mac binding string (e.g. "Cmd+B", "Shift+Tab"). */
54
+ mac: string;
55
+ }
56
+
57
+ export interface CapabilityBlockReason {
58
+ code: string;
59
+ message: string;
60
+ }
61
+
62
+ export interface EditorCapability {
63
+ /** Stable symbolic identifier. Used for lookup + wire-format. */
64
+ id: string;
65
+ kind: CapabilityKind;
66
+ category: CapabilityCategory;
67
+ /** Human-readable description for docs + a11y help. */
68
+ label: string;
69
+ /** Present when the capability is keyboard-triggered. */
70
+ shortcut?: CapabilityShortcut;
71
+ /** For `host-delegated`: the `WordReviewEditorProps` callback name. */
72
+ hostEvent?: string;
73
+ /** For `blocked`: the reason returned in the `command_blocked` event. */
74
+ blockReason?: CapabilityBlockReason;
75
+ }
76
+
77
+ const UNSUPPORTED_SURFACE = "unsupported_surface";
78
+
79
+ export const EDITOR_CAPABILITIES: readonly EditorCapability[] = [
80
+ // ---------------------------------------------------------------
81
+ // System — shell-level UX affordances
82
+ // ---------------------------------------------------------------
83
+ {
84
+ id: "shell.focus-region",
85
+ kind: "supported",
86
+ category: "system",
87
+ label: "Cycle focus between editor regions",
88
+ shortcut: { winLinux: "F6", mac: "F6" },
89
+ },
90
+ {
91
+ id: "shell.dismiss-selection-toolbar",
92
+ kind: "supported",
93
+ category: "system",
94
+ label: "Dismiss the selection toolbar",
95
+ shortcut: { winLinux: "Escape", mac: "Escape" },
96
+ },
97
+
98
+ // ---------------------------------------------------------------
99
+ // History
100
+ // ---------------------------------------------------------------
101
+ {
102
+ id: "history.undo",
103
+ kind: "supported",
104
+ category: "history",
105
+ label: "Undo",
106
+ shortcut: { winLinux: "Ctrl+Z", mac: "Cmd+Z" },
107
+ },
108
+ {
109
+ id: "history.redo",
110
+ kind: "supported",
111
+ category: "history",
112
+ label: "Redo",
113
+ shortcut: { winLinux: "Ctrl+Y", mac: "Cmd+Shift+Z" },
114
+ },
115
+
116
+ // ---------------------------------------------------------------
117
+ // Text formatting — shell-dispatched marks
118
+ // ---------------------------------------------------------------
119
+ {
120
+ id: "mark.toggle-bold",
121
+ kind: "supported",
122
+ category: "text-formatting",
123
+ label: "Toggle bold",
124
+ shortcut: { winLinux: "Ctrl+B", mac: "Cmd+B" },
125
+ },
126
+ {
127
+ id: "mark.toggle-italic",
128
+ kind: "supported",
129
+ category: "text-formatting",
130
+ label: "Toggle italic",
131
+ shortcut: { winLinux: "Ctrl+I", mac: "Cmd+I" },
132
+ },
133
+ {
134
+ id: "mark.toggle-underline",
135
+ kind: "supported",
136
+ category: "text-formatting",
137
+ label: "Toggle underline",
138
+ shortcut: { winLinux: "Ctrl+U", mac: "Cmd+U" },
139
+ },
140
+
141
+ // ---------------------------------------------------------------
142
+ // Comments
143
+ // ---------------------------------------------------------------
144
+ {
145
+ id: "comment.add",
146
+ kind: "supported",
147
+ category: "comments",
148
+ label: "Insert comment at selection",
149
+ shortcut: { winLinux: "Ctrl+Alt+M", mac: "Cmd+Option+A" },
150
+ },
151
+
152
+ // ---------------------------------------------------------------
153
+ // Paragraph-formatting — heading levels
154
+ // ---------------------------------------------------------------
155
+ {
156
+ id: "heading.set-level-1",
157
+ kind: "supported",
158
+ category: "paragraph-formatting",
159
+ label: "Apply Heading 1 style",
160
+ shortcut: { winLinux: "Ctrl+Alt+1", mac: "Cmd+Option+1" },
161
+ },
162
+ {
163
+ id: "heading.set-level-2",
164
+ kind: "supported",
165
+ category: "paragraph-formatting",
166
+ label: "Apply Heading 2 style",
167
+ shortcut: { winLinux: "Ctrl+Alt+2", mac: "Cmd+Option+2" },
168
+ },
169
+ {
170
+ id: "heading.set-level-3",
171
+ kind: "supported",
172
+ category: "paragraph-formatting",
173
+ label: "Apply Heading 3 style",
174
+ shortcut: { winLinux: "Ctrl+Alt+3", mac: "Cmd+Option+3" },
175
+ },
176
+
177
+ // ---------------------------------------------------------------
178
+ // Surface-dispatched text commands (PM-focus path)
179
+ // ---------------------------------------------------------------
180
+ {
181
+ id: "text.delete-backward",
182
+ kind: "supported",
183
+ category: "text-formatting",
184
+ label: "Delete character before the cursor",
185
+ shortcut: { winLinux: "Backspace", mac: "Backspace" },
186
+ },
187
+ {
188
+ id: "text.delete-forward",
189
+ kind: "supported",
190
+ category: "text-formatting",
191
+ label: "Delete character after the cursor",
192
+ shortcut: { winLinux: "Delete", mac: "Delete" },
193
+ },
194
+ {
195
+ id: "paragraph.split",
196
+ kind: "supported",
197
+ category: "structure",
198
+ label: "Split paragraph at cursor",
199
+ shortcut: { winLinux: "Enter", mac: "Enter" },
200
+ },
201
+ {
202
+ id: "text.insert-hard-break",
203
+ kind: "supported",
204
+ category: "structure",
205
+ label: "Insert hard line break",
206
+ shortcut: { winLinux: "Shift+Enter", mac: "Shift+Enter" },
207
+ },
208
+ {
209
+ id: "text.insert-tab",
210
+ kind: "supported",
211
+ category: "paragraph-formatting",
212
+ label: "Indent list item; in table, move to next cell; otherwise insert tab",
213
+ shortcut: { winLinux: "Tab", mac: "Tab" },
214
+ },
215
+ {
216
+ id: "text.outdent-tab",
217
+ kind: "supported",
218
+ category: "paragraph-formatting",
219
+ label: "Outdent list item or paragraph; in table, move to previous cell",
220
+ shortcut: { winLinux: "Shift+Tab", mac: "Shift+Tab" },
221
+ },
222
+ {
223
+ id: "list.indent",
224
+ kind: "supported",
225
+ category: "paragraph-formatting",
226
+ label: "Indent current list item (Word Alt+Shift binding)",
227
+ shortcut: { winLinux: "Alt+Shift+Right", mac: "Option+Shift+Right" },
228
+ },
229
+ {
230
+ id: "list.outdent",
231
+ kind: "supported",
232
+ category: "paragraph-formatting",
233
+ label: "Outdent current list item (Word Alt+Shift binding)",
234
+ shortcut: { winLinux: "Alt+Shift+Left", mac: "Option+Shift+Left" },
235
+ },
236
+ {
237
+ id: "table.navigate-cell",
238
+ kind: "supported",
239
+ category: "navigation",
240
+ label: "Move to next / previous table cell (Tab / Shift+Tab while inside a table)",
241
+ shortcut: { winLinux: "Tab", mac: "Tab" },
242
+ },
243
+
244
+ // ---------------------------------------------------------------
245
+ // Clipboard — force-plain-text shortcut
246
+ //
247
+ // Ctrl/Cmd+Shift+V resolves to `{kind: "none"}` at the shell layer
248
+ // and falls through to the browser's native paste flow, which PM's
249
+ // handlePaste routes through the same plain-text path as Ctrl/Cmd+V.
250
+ // Classified as `passthrough` here: we recognize the shortcut but
251
+ // don't intercept — the browser + PM surface handle it.
252
+ // ---------------------------------------------------------------
253
+ {
254
+ id: "clipboard.paste-plain-text",
255
+ kind: "passthrough",
256
+ category: "clipboard",
257
+ label: "Paste as plain text (today identical to Ctrl/Cmd+V — force-plain-text semantics land with Tier B rich paste)",
258
+ shortcut: { winLinux: "Ctrl+Shift+V", mac: "Cmd+Shift+V" },
259
+ },
260
+
261
+ // ---------------------------------------------------------------
262
+ // Host-delegated — browser-owned gestures
263
+ // ---------------------------------------------------------------
264
+ {
265
+ id: "shortcut.find",
266
+ kind: "host-delegated",
267
+ category: "navigation",
268
+ label: "Find in document",
269
+ shortcut: { winLinux: "Ctrl+F", mac: "Cmd+F" },
270
+ hostEvent: "onFindRequested",
271
+ },
272
+ {
273
+ id: "shortcut.print",
274
+ kind: "host-delegated",
275
+ category: "system",
276
+ label: "Print document",
277
+ shortcut: { winLinux: "Ctrl+P", mac: "Cmd+P" },
278
+ hostEvent: "onPrintRequested",
279
+ },
280
+ {
281
+ id: "shortcut.zoom-in",
282
+ kind: "host-delegated",
283
+ category: "system",
284
+ label: "Zoom in",
285
+ shortcut: { winLinux: "Ctrl+Plus", mac: "Cmd+Plus" },
286
+ hostEvent: "onZoomRequested",
287
+ },
288
+ {
289
+ id: "shortcut.zoom-out",
290
+ kind: "host-delegated",
291
+ category: "system",
292
+ label: "Zoom out",
293
+ shortcut: { winLinux: "Ctrl+Minus", mac: "Cmd+Minus" },
294
+ hostEvent: "onZoomRequested",
295
+ },
296
+ {
297
+ id: "shortcut.zoom-reset",
298
+ kind: "host-delegated",
299
+ category: "system",
300
+ label: "Reset zoom",
301
+ shortcut: { winLinux: "Ctrl+0", mac: "Cmd+0" },
302
+ hostEvent: "onZoomRequested",
303
+ },
304
+
305
+ // ---------------------------------------------------------------
306
+ // Blocked — Word shortcuts the mounted editor does not implement
307
+ // ---------------------------------------------------------------
308
+ {
309
+ id: "replaceText",
310
+ kind: "blocked",
311
+ category: "navigation",
312
+ label: "Find and replace",
313
+ shortcut: { winLinux: "Ctrl+H", mac: "Ctrl+H" },
314
+ blockReason: {
315
+ code: UNSUPPORTED_SURFACE,
316
+ message: "Replace shortcuts are not supported in the mounted editor yet.",
317
+ },
318
+ },
319
+ {
320
+ id: "goTo",
321
+ kind: "blocked",
322
+ category: "navigation",
323
+ label: "Go to",
324
+ shortcut: { winLinux: "Ctrl+G", mac: "Cmd+Option+G" },
325
+ blockReason: {
326
+ code: UNSUPPORTED_SURFACE,
327
+ message: "Go To shortcuts are not supported in the mounted editor yet.",
328
+ },
329
+ },
330
+ {
331
+ id: "toggleTrackChanges",
332
+ kind: "blocked",
333
+ category: "tracked-changes",
334
+ label: "Toggle track-changes authoring mode",
335
+ shortcut: { winLinux: "Ctrl+Shift+E", mac: "Cmd+Shift+E" },
336
+ blockReason: {
337
+ code: UNSUPPORTED_SURFACE,
338
+ message: "Track changes authoring shortcuts are not supported in the mounted editor.",
339
+ },
340
+ },
341
+ {
342
+ id: "checkSpelling",
343
+ kind: "blocked",
344
+ category: "system",
345
+ label: "Check spelling",
346
+ shortcut: { winLinux: "F7", mac: "F7" },
347
+ blockReason: {
348
+ code: UNSUPPORTED_SURFACE,
349
+ message: "Spelling shortcuts are not supported in the mounted editor.",
350
+ },
351
+ },
352
+ {
353
+ id: "openThesaurus",
354
+ kind: "blocked",
355
+ category: "system",
356
+ label: "Open thesaurus",
357
+ shortcut: { winLinux: "Shift+F7", mac: "Shift+F7" },
358
+ blockReason: {
359
+ code: UNSUPPORTED_SURFACE,
360
+ message: "Thesaurus shortcuts are not supported in the mounted editor.",
361
+ },
362
+ },
363
+ {
364
+ id: "extendSelection",
365
+ kind: "blocked",
366
+ category: "selection",
367
+ label: "Extend-selection mode",
368
+ shortcut: { winLinux: "F8", mac: "F8" },
369
+ blockReason: {
370
+ code: UNSUPPORTED_SURFACE,
371
+ message: "Extend-selection shortcuts are not supported in the mounted editor.",
372
+ },
373
+ },
374
+ {
375
+ id: "lastEdit",
376
+ kind: "blocked",
377
+ category: "navigation",
378
+ label: "Return to last edit",
379
+ shortcut: { winLinux: "Shift+F5", mac: "Shift+F5" },
380
+ blockReason: {
381
+ code: UNSUPPORTED_SURFACE,
382
+ message: "Last-edit shortcuts are not supported in the mounted editor.",
383
+ },
384
+ },
385
+ ];
386
+
387
+ /**
388
+ * O(1) lookup by capability id. Derived from `EDITOR_CAPABILITIES`
389
+ * at module-load; freezes the map so accidental mutation throws.
390
+ */
391
+ export const CAPABILITY_BY_ID: ReadonlyMap<string, EditorCapability> = (() => {
392
+ const map = new Map<string, EditorCapability>();
393
+ for (const cap of EDITOR_CAPABILITIES) {
394
+ map.set(cap.id, cap);
395
+ }
396
+ return map;
397
+ })();
398
+
399
+ /** Return every capability in the given category (preserves table order). */
400
+ export function capabilitiesByCategory(
401
+ category: CapabilityCategory,
402
+ ): readonly EditorCapability[] {
403
+ return EDITOR_CAPABILITIES.filter((cap) => cap.category === category);
404
+ }
405
+
406
+ /** Return every capability with the given kind (preserves table order). */
407
+ export function capabilitiesByKind(
408
+ kind: CapabilityKind,
409
+ ): readonly EditorCapability[] {
410
+ return EDITOR_CAPABILITIES.filter((cap) => cap.kind === kind);
411
+ }
@@ -172,6 +172,8 @@ export {
172
172
  type PublicPageNode,
173
173
  type PublicPageRegions,
174
174
  type PublicPageRegion,
175
+ type PublicRegionBlock,
176
+ type PublicRegionKind,
175
177
  type PublicBlockFragment,
176
178
  type PublicLineBox,
177
179
  type PublicNoteAllocation,
@@ -36,6 +36,8 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
36
36
  getLineBoxes: () => [],
37
37
  getLineBoxesForRegion: () => [],
38
38
  getStoryRegionsOnPage: () => [],
39
+ getStoryBlocksForRegion: () => [],
40
+ getDocumentEndnoteBlocks: () => [],
39
41
  getFragmentsForPage: () => [],
40
42
  getPageFormatCatalog: () => PAGE_FORMAT_CATALOG,
41
43
  getActivePageFormat: () => null,
@@ -59,6 +61,8 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
59
61
  getTableRenderPlan: () => null,
60
62
  getDirtyFieldFamilies: () => [],
61
63
  getFieldDirtinessReport: () => emptyReport,
64
+ setVisibleBlockRange: () => undefined,
65
+ requestViewportRefresh: () => undefined,
62
66
  subscribe: (_listener: (event: LayoutFacetEvent) => void) => () => undefined,
63
67
  };
64
68
  }