@beyondwork/docx-react-component 1.0.43 → 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.
- package/README.md +17 -0
- package/package.json +44 -32
- package/src/api/public-types.ts +139 -3
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +27 -2
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +16 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +21 -1
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/document-runtime.ts +316 -25
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +46 -0
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/public-facet.ts +30 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +35 -2
- package/src/ui/WordReviewEditor.tsx +75 -192
- package/src/ui/editor-runtime-boundary.ts +5 -1
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +18 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +23 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +38 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +131 -29
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorSessionState,
|
|
3
|
+
EditorStoryTarget,
|
|
4
|
+
RuntimeRenderSnapshot,
|
|
5
|
+
SelectionSnapshot as PublicSelectionSnapshot,
|
|
6
|
+
SurfaceBlockSnapshot,
|
|
7
|
+
} from "../../api/public-types";
|
|
8
|
+
import { type SelectionSnapshot as InternalSelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
9
|
+
import {
|
|
10
|
+
backspaceAtListStart,
|
|
11
|
+
indentListItems,
|
|
12
|
+
outdentListItems,
|
|
13
|
+
splitListParagraph,
|
|
14
|
+
} from "../../core/commands/list-commands.ts";
|
|
15
|
+
|
|
16
|
+
export type DispatchTextCommand =
|
|
17
|
+
| { type: "insert-text"; text: string }
|
|
18
|
+
| { type: "delete-backward" }
|
|
19
|
+
| { type: "delete-forward" }
|
|
20
|
+
| { type: "insert-tab" }
|
|
21
|
+
| { type: "outdent-tab" }
|
|
22
|
+
| { type: "insert-hard-break" }
|
|
23
|
+
| { type: "split-paragraph" };
|
|
24
|
+
|
|
25
|
+
export interface ListAwareParagraphContext {
|
|
26
|
+
paragraphIndex: number;
|
|
27
|
+
paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
|
|
28
|
+
atParagraphStart: boolean;
|
|
29
|
+
isEmpty: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StoryMutationContext {
|
|
33
|
+
timestamp: string;
|
|
34
|
+
activeStory: EditorStoryTarget;
|
|
35
|
+
persistedDocument: EditorSessionState["canonicalDocument"];
|
|
36
|
+
localDocument: EditorSessionState["canonicalDocument"];
|
|
37
|
+
localSnapshot: RuntimeRenderSnapshot;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ListAwareMutationResult {
|
|
41
|
+
changed: boolean;
|
|
42
|
+
document: EditorSessionState["canonicalDocument"];
|
|
43
|
+
selection: InternalSelectionSnapshot;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ListAwareDispatchDeps {
|
|
47
|
+
resolveActiveParagraphContext(
|
|
48
|
+
snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
|
|
49
|
+
): ListAwareParagraphContext | null;
|
|
50
|
+
toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot): InternalSelectionSnapshot;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function applyListAwareTextCommand(
|
|
54
|
+
context: StoryMutationContext,
|
|
55
|
+
command: DispatchTextCommand,
|
|
56
|
+
deps: ListAwareDispatchDeps,
|
|
57
|
+
): ListAwareMutationResult | null {
|
|
58
|
+
const paragraphContext = deps.resolveActiveParagraphContext(context.localSnapshot);
|
|
59
|
+
if (!paragraphContext?.paragraph.numbering) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch (command.type) {
|
|
64
|
+
case "insert-tab": {
|
|
65
|
+
const result = indentListItems(
|
|
66
|
+
context.localDocument,
|
|
67
|
+
[paragraphContext.paragraphIndex],
|
|
68
|
+
{ timestamp: context.timestamp },
|
|
69
|
+
);
|
|
70
|
+
return createListMutationResult(result, context.localSnapshot.selection, deps);
|
|
71
|
+
}
|
|
72
|
+
case "outdent-tab": {
|
|
73
|
+
const result = outdentListItems(
|
|
74
|
+
context.localDocument,
|
|
75
|
+
[paragraphContext.paragraphIndex],
|
|
76
|
+
{ timestamp: context.timestamp },
|
|
77
|
+
);
|
|
78
|
+
return createListMutationResult(result, context.localSnapshot.selection, deps);
|
|
79
|
+
}
|
|
80
|
+
case "delete-backward": {
|
|
81
|
+
if (!paragraphContext.atParagraphStart || !context.localSnapshot.selection.isCollapsed) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const result = backspaceAtListStart(
|
|
85
|
+
context.localDocument,
|
|
86
|
+
paragraphContext.paragraphIndex,
|
|
87
|
+
{ timestamp: context.timestamp },
|
|
88
|
+
);
|
|
89
|
+
return result.handled
|
|
90
|
+
? createListMutationResult(result, context.localSnapshot.selection, deps)
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
case "split-paragraph": {
|
|
94
|
+
if (!context.localSnapshot.selection.isCollapsed || !paragraphContext.isEmpty) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const result = splitListParagraph(
|
|
98
|
+
context.localDocument,
|
|
99
|
+
paragraphContext.paragraphIndex,
|
|
100
|
+
true,
|
|
101
|
+
{ timestamp: context.timestamp },
|
|
102
|
+
);
|
|
103
|
+
return result.action === "split"
|
|
104
|
+
? null
|
|
105
|
+
: createListMutationResult(result, context.localSnapshot.selection, deps);
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createListMutationResult(
|
|
113
|
+
result: {
|
|
114
|
+
document: EditorSessionState["canonicalDocument"];
|
|
115
|
+
affectedParagraphIndexes: number[];
|
|
116
|
+
},
|
|
117
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
118
|
+
deps: ListAwareDispatchDeps,
|
|
119
|
+
): ListAwareMutationResult {
|
|
120
|
+
return {
|
|
121
|
+
changed: result.affectedParagraphIndexes.length > 0,
|
|
122
|
+
document: result.document,
|
|
123
|
+
selection: deps.toRuntimeSelectionSnapshot(selection),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -61,6 +61,8 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
61
61
|
getTableRenderPlan: () => null,
|
|
62
62
|
getDirtyFieldFamilies: () => [],
|
|
63
63
|
getFieldDirtinessReport: () => emptyReport,
|
|
64
|
+
setVisibleBlockRange: () => undefined,
|
|
65
|
+
requestViewportRefresh: () => undefined,
|
|
64
66
|
subscribe: (_listener: (event: LayoutFacetEvent) => void) => () => undefined,
|
|
65
67
|
};
|
|
66
68
|
}
|
|
@@ -182,6 +182,35 @@ export interface LayoutEngineInstance {
|
|
|
182
182
|
* glyphs, and the cached page graph keeps its stale page boundaries.
|
|
183
183
|
*/
|
|
184
184
|
invalidateMeasurementCache(): void;
|
|
185
|
+
|
|
186
|
+
// ---- cache rehydration (L7 Phase 2.5) ---------------------------------
|
|
187
|
+
/**
|
|
188
|
+
* Seed the engine's cached graph from a prerendered `RuntimePageGraph`
|
|
189
|
+
* (read from IndexedDB or customXml). Subsequent `getPageGraph()` calls
|
|
190
|
+
* against the same `document` return the seeded graph directly —
|
|
191
|
+
* skipping `fullRebuild` and the pagination/measurement work that
|
|
192
|
+
* dominates cold-open on large docs.
|
|
193
|
+
*
|
|
194
|
+
* Both the graph and the source document must be seeded so the
|
|
195
|
+
* engine's internal cache-key (`content`, `styles`, `subParts`
|
|
196
|
+
* reference-equality tuple) compares equal on the next query. Passing
|
|
197
|
+
* only the graph leaves `cachedKey` null, the next query would run a
|
|
198
|
+
* full rebuild, and the seed would be discarded.
|
|
199
|
+
*
|
|
200
|
+
* Caller contract:
|
|
201
|
+
* - The seeded graph must have been produced by the same
|
|
202
|
+
* LAYOUT_ENGINE_VERSION as the current engine instance. Stale reads
|
|
203
|
+
* are prevented by the cache-key scheme in
|
|
204
|
+
* src/runtime/prerender/cache-key.ts (engine version is part of the
|
|
205
|
+
* key), not by this method.
|
|
206
|
+
* - The next runtime mutation triggers a normal invalidation path;
|
|
207
|
+
* the seeded graph is treated as a fresh cache entry from the
|
|
208
|
+
* engine's perspective.
|
|
209
|
+
*/
|
|
210
|
+
seedCachedGraph(
|
|
211
|
+
graph: RuntimePageGraph,
|
|
212
|
+
document: CanonicalDocumentEnvelope,
|
|
213
|
+
): void;
|
|
185
214
|
}
|
|
186
215
|
|
|
187
216
|
// ---------------------------------------------------------------------------
|
|
@@ -737,6 +766,23 @@ export function createLayoutEngine(
|
|
|
737
766
|
cachedFormatting = null;
|
|
738
767
|
cachedMapper = null;
|
|
739
768
|
},
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* L7 Phase 2.5 — seed the cached graph from a prerender envelope.
|
|
772
|
+
* Populates both `cachedGraph` and `cachedKey` (keyed on the provided
|
|
773
|
+
* document's identity-equal slots) so the next getPageGraph query
|
|
774
|
+
* returns the seeded graph directly. Any subsequent mutation
|
|
775
|
+
* invalidates normally through the existing path.
|
|
776
|
+
*/
|
|
777
|
+
seedCachedGraph(graph: RuntimePageGraph, document: CanonicalDocumentEnvelope) {
|
|
778
|
+
cachedGraph = graph;
|
|
779
|
+
cachedKey = {
|
|
780
|
+
content: document.content,
|
|
781
|
+
styles: document.styles,
|
|
782
|
+
subParts: document.subParts,
|
|
783
|
+
};
|
|
784
|
+
previousPageCount = graph.contentPageCount;
|
|
785
|
+
},
|
|
740
786
|
};
|
|
741
787
|
}
|
|
742
788
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout engine version marker — bump when **any** file under
|
|
3
|
+
* `src/runtime/layout/**` or `src/runtime/render/**` changes, or when
|
|
4
|
+
* the page-break widget DOM shape under `src/ui-tailwind/editor-surface/`
|
|
5
|
+
* changes in a way that affects cached render-frame diffs or persisted
|
|
6
|
+
* layout caches.
|
|
7
|
+
*
|
|
8
|
+
* Enforcement: `scripts/ci-check-layout-engine-version.mjs` inspects the
|
|
9
|
+
* PR diff; if any file in the watched trees is touched without this
|
|
10
|
+
* constant being co-touched, CI fails. See CLAUDE.md → *Performance
|
|
11
|
+
* Invariants* for the full contract.
|
|
12
|
+
*
|
|
13
|
+
* Persisted caches should key their stored snapshots on this version so a
|
|
14
|
+
* bump automatically invalidates stale entries — no corruption path
|
|
15
|
+
* exists because the version is the cache's top-level discriminator.
|
|
16
|
+
*
|
|
17
|
+
* History:
|
|
18
|
+
* 1 — initial materialization (L8 Phase B).
|
|
19
|
+
* 2 — L8 Phase B retired the page-posture widget's inline band-gap-band
|
|
20
|
+
* DOM in favor of a transparent spacer. Widget DOM shape changed;
|
|
21
|
+
* cached render frames from version 1 are incompatible.
|
|
22
|
+
* 3 — L7 Phase 2.5 Plan A. Adds `seedCachedGraph(graph, document)` to
|
|
23
|
+
* `LayoutEngineInstance` so prerender-cache consumers can seed the
|
|
24
|
+
* internal cachedGraph + cachedKey without triggering fullRebuild.
|
|
25
|
+
* Does not change geometry — but the public interface changed, so
|
|
26
|
+
* persisted envelopes MUST re-derive their cacheKey under 3.
|
|
27
|
+
*/
|
|
28
|
+
export const LAYOUT_ENGINE_VERSION = 3 as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serialization schema version for the LayCache payload (the cache envelope
|
|
32
|
+
* stored in IndexedDB, and — post Plan B — the customXml editor-state
|
|
33
|
+
* namespace). Bump independently of LAYOUT_ENGINE_VERSION when the
|
|
34
|
+
* envelope shape changes but the layout engine itself has not.
|
|
35
|
+
*
|
|
36
|
+
* History:
|
|
37
|
+
* 1 — initial envelope shape: { schemaVersion, engineVersion,
|
|
38
|
+
* fontFingerprint, structuralHash, graph, surface }. Ships with
|
|
39
|
+
* L7 Phase 2.5 Plan A.
|
|
40
|
+
*/
|
|
41
|
+
export const LAYCACHE_SCHEMA_VERSION = 1 as const;
|
|
@@ -564,6 +564,19 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
564
564
|
getDirtyFieldFamilies(): readonly string[];
|
|
565
565
|
getFieldDirtinessReport(): PublicFieldDirtinessReport;
|
|
566
566
|
|
|
567
|
+
// Viewport culling (L7 Phase 2) ----------------------------------------
|
|
568
|
+
/**
|
|
569
|
+
* Notifies the runtime that the visible block range changed. Delegates to
|
|
570
|
+
* `DocumentRuntime.setVisibleBlockRange`. Safe to call at any frequency;
|
|
571
|
+
* identical ranges are a no-op inside the runtime.
|
|
572
|
+
*/
|
|
573
|
+
setVisibleBlockRange(range: { start: number; end: number }): void;
|
|
574
|
+
/**
|
|
575
|
+
* Triggers a surface-only refresh applying the latest visible block range.
|
|
576
|
+
* Delegates to `DocumentRuntime.requestViewportRefresh`.
|
|
577
|
+
*/
|
|
578
|
+
requestViewportRefresh(): void;
|
|
579
|
+
|
|
567
580
|
// Events ---------------------------------------------------------------
|
|
568
581
|
subscribe(listener: (event: LayoutFacetEvent) => void): () => void;
|
|
569
582
|
}
|
|
@@ -614,6 +627,14 @@ export interface CreateLayoutFacetInput {
|
|
|
614
627
|
| readonly import("../../api/public-types.ts").WorkflowMetadataMarkup[]
|
|
615
628
|
| null
|
|
616
629
|
| undefined;
|
|
630
|
+
/**
|
|
631
|
+
* L7 Phase 2 — optional viewport culling hooks wired from the owning
|
|
632
|
+
* `DocumentRuntime`. When supplied, `facet.setVisibleBlockRange` /
|
|
633
|
+
* `facet.requestViewportRefresh` delegate directly to these; when omitted
|
|
634
|
+
* (e.g. tests, inert facet, standalone use) both methods are no-ops.
|
|
635
|
+
*/
|
|
636
|
+
setVisibleBlockRange?: (range: { start: number; end: number }) => void;
|
|
637
|
+
requestViewportRefresh?: () => void;
|
|
617
638
|
/**
|
|
618
639
|
* R3 — optional suggestions snapshot accessor. Used by
|
|
619
640
|
* `getAllScopeCardModels` to attach `SuggestionGroup` entries whose
|
|
@@ -1231,6 +1252,15 @@ export function createLayoutFacet(
|
|
|
1231
1252
|
};
|
|
1232
1253
|
},
|
|
1233
1254
|
|
|
1255
|
+
// L7 Phase 2 — viewport culling delegates
|
|
1256
|
+
setVisibleBlockRange(range) {
|
|
1257
|
+
input.setVisibleBlockRange?.(range);
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
requestViewportRefresh() {
|
|
1261
|
+
input.requestViewportRefresh?.();
|
|
1262
|
+
},
|
|
1263
|
+
|
|
1234
1264
|
subscribe(listener) {
|
|
1235
1265
|
listeners.add(listener);
|
|
1236
1266
|
return () => {
|