@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
package/README.md
CHANGED
|
@@ -216,9 +216,26 @@ Current integration honesty:
|
|
|
216
216
|
|
|
217
217
|
- [`docs/maintainers/README.md`](docs/maintainers/README.md)
|
|
218
218
|
- [`docs/ccep-contract-templates/README.md`](docs/ccep-contract-templates/README.md)
|
|
219
|
+
- [`docs/plans/README.md`](docs/plans/README.md) — active development lanes + open-issues register
|
|
219
220
|
|
|
220
221
|
The CCEP corpus is kept on `main` as a maintainer-safe smoke-doc source set for agreement-heavy validation and wrapper or agent benchmarking.
|
|
221
222
|
|
|
223
|
+
### Active Development Lanes
|
|
224
|
+
|
|
225
|
+
Engineering work is organized into 9 lanes (5 active now + 2 later polish + 2 final) — full plan index, status, and worktree map at [`docs/plans/README.md`](docs/plans/README.md). Each lane has a single self-contained doc at `docs/plans/lane-N-<name>.md`:
|
|
226
|
+
|
|
227
|
+
| Lane | Mission | Status |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| 1. [**Editing Foundation**](docs/plans/lane-1-editing-foundation.md) | Close I1–I6 + ship the SelectionLayer / EditLayer / StructureLayer / SurfaceLayer split | active |
|
|
230
|
+
| 2. [**Render Performance**](docs/plans/lane-2-render-performance.md) | <32 ms typing P50 + <1 s cold-open on 200-page CCEP; never stuck on loading | active |
|
|
231
|
+
| 3. [**Layout Engine + OOXML Fidelity**](docs/plans/lane-3-layout-engine-ooxml-fidelity.md) | Land P9 anchor-index + P10 incremental relayout + drain O2/O3/O4 round-trip + tables/TOC/page numbers/front pages/image structures | active |
|
|
232
|
+
| 4. [**Collab + CLM/Vallor Integration**](docs/plans/lane-4-collab-clm-vallor.md) | Promote collab plumbing → first-class user mode; wire Sunday CLM × React E2E demo | active |
|
|
233
|
+
| 5. [**Charts (independent)**](docs/plans/lane-5-charts.md) | Move charts from preserve-only opaque preview to Word-accurate native rendering (Stages 1–7) | active |
|
|
234
|
+
| 6. [**Visual Chrome / Layout Polish**](docs/plans/lane-6-visual-chrome-layout-polish.md) | True-to-Word visuals: discrete paper cards, native page chrome, float-wrap, validation bar | LATER |
|
|
235
|
+
| 7. [**Bugs / Gaps / Cross-cutting**](docs/plans/lane-7-bugs-gaps-cross-cutting.md) | Drain V#/O#/X# register; trigger-gated work; infrastructure hardening | LATER |
|
|
236
|
+
| 8. [**API Ergonomics / Errors / Backwards Compatibility**](docs/plans/lane-8-api-ergonomics.md) | Refactor `docs/reference/public-api.md` end-to-end with groups + per-code error catalog + ergonomics fixes; maintain backwards compat | LATER (Tracks A+C shipped 2026-04-19) |
|
|
237
|
+
| 9. [**Shipping**](docs/plans/lane-9-shipping.md) | Production-readiness: API freeze, semver discipline, changelog, telemetry, customer migration guides, doc completeness audit | FINAL |
|
|
238
|
+
|
|
222
239
|
### Technical Wiki
|
|
223
240
|
|
|
224
241
|
- [`docs/wiki/`](docs/wiki/) — Feature-by-feature technical documentation (25+ topics covering OOXML, ProseMirror, runtime, and platform)
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.45",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
+
"packageManager": "pnpm@10.30.3",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"sideEffects": [
|
|
8
9
|
"**/*.css"
|
|
@@ -92,6 +93,35 @@
|
|
|
92
93
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
93
94
|
},
|
|
94
95
|
"types": "./src/index.ts",
|
|
96
|
+
"scripts": {
|
|
97
|
+
"build": "tsup",
|
|
98
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
99
|
+
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
100
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
101
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
102
|
+
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
103
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
104
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
105
|
+
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
106
|
+
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
107
|
+
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
108
|
+
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
109
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
110
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
111
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
112
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
113
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
114
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
115
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
116
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
117
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
118
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
119
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
120
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
121
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
122
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
123
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
124
|
+
},
|
|
95
125
|
"keywords": [
|
|
96
126
|
"docx",
|
|
97
127
|
"word",
|
|
@@ -135,9 +165,9 @@
|
|
|
135
165
|
"react": "^19.2.0",
|
|
136
166
|
"react-dom": "^19.2.0",
|
|
137
167
|
"tailwindcss": "^4.2.2",
|
|
138
|
-
"yjs": "^13.6.0",
|
|
139
168
|
"y-prosemirror": "^1.2.0",
|
|
140
|
-
"y-protocols": "^1.0.0"
|
|
169
|
+
"y-protocols": "^1.0.0",
|
|
170
|
+
"yjs": "^13.6.0"
|
|
141
171
|
},
|
|
142
172
|
"peerDependenciesMeta": {
|
|
143
173
|
"yjs": {
|
|
@@ -156,6 +186,7 @@
|
|
|
156
186
|
"@types/react": "19.2.14",
|
|
157
187
|
"@types/react-dom": "19.2.3",
|
|
158
188
|
"@typescript/native-preview": "7.0.0-dev.20260409.1",
|
|
189
|
+
"fake-indexeddb": "^6.2.5",
|
|
159
190
|
"jsdom": "^29.0.1",
|
|
160
191
|
"pixelmatch": "^7.1.0",
|
|
161
192
|
"pngjs": "^7.0.0",
|
|
@@ -174,33 +205,14 @@
|
|
|
174
205
|
"y-protocols": "^1.0.7",
|
|
175
206
|
"yjs": "^13.6.30"
|
|
176
207
|
},
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
187
|
-
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
188
|
-
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
189
|
-
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
190
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
191
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
192
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
193
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
194
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
195
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
196
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
197
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
198
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
199
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
200
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
201
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
202
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
203
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
204
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
208
|
+
"pnpm": {
|
|
209
|
+
"onlyBuiltDependencies": [
|
|
210
|
+
"esbuild",
|
|
211
|
+
"sharp"
|
|
212
|
+
],
|
|
213
|
+
"overrides": {
|
|
214
|
+
"react": "19.2.4",
|
|
215
|
+
"react-dom": "19.2.4"
|
|
216
|
+
}
|
|
205
217
|
}
|
|
206
|
-
}
|
|
218
|
+
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -981,7 +981,16 @@ export type SurfaceBlockSnapshot =
|
|
|
981
981
|
detail: string;
|
|
982
982
|
featureKey?: string;
|
|
983
983
|
blockedReasonCode?: "workflow_preserve_only" | "workflow_blocked_import";
|
|
984
|
-
|
|
984
|
+
/**
|
|
985
|
+
* When set, this opaque_block is a size-preserving placeholder generated
|
|
986
|
+
* by viewport culling, NOT a real preserved fragment. The PM schema uses
|
|
987
|
+
* this to claim the original block's position span without rendering
|
|
988
|
+
* content.
|
|
989
|
+
*
|
|
990
|
+
* See docs/plans/lane-2-render-performance.md.
|
|
991
|
+
*/
|
|
992
|
+
placeholderSize?: number;
|
|
993
|
+
state: "locked-preserve-only" | "placeholder-culled";
|
|
985
994
|
};
|
|
986
995
|
|
|
987
996
|
export interface SecondaryStorySurface {
|
|
@@ -997,6 +1006,15 @@ export interface EditorSurfaceSnapshot {
|
|
|
997
1006
|
blocks: SurfaceBlockSnapshot[];
|
|
998
1007
|
lockedFragmentIds: string[];
|
|
999
1008
|
secondaryStories: SecondaryStorySurface[];
|
|
1009
|
+
/**
|
|
1010
|
+
* Block index range rendered as real (non-placeholder) in this snapshot.
|
|
1011
|
+
* Blocks outside this range are placeholder opaque_blocks carrying the
|
|
1012
|
+
* original position range but no content. `null` (default) = all blocks
|
|
1013
|
+
* are real (legacy behavior).
|
|
1014
|
+
*
|
|
1015
|
+
* See docs/plans/lane-2-render-performance.md.
|
|
1016
|
+
*/
|
|
1017
|
+
viewportBlockRange: { start: number; end: number } | null;
|
|
1000
1018
|
}
|
|
1001
1019
|
|
|
1002
1020
|
export type EditorWarningCode =
|
|
@@ -1669,6 +1687,16 @@ export interface AddCommentParams {
|
|
|
1669
1687
|
authorId?: string;
|
|
1670
1688
|
}
|
|
1671
1689
|
|
|
1690
|
+
export interface AddCommentResult {
|
|
1691
|
+
commentId: string;
|
|
1692
|
+
anchor: EditorAnchorProjection;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
export interface AddCommentReplyResult {
|
|
1696
|
+
commentId: string;
|
|
1697
|
+
entryId: string;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1672
1700
|
export interface ExportDocxOptions {
|
|
1673
1701
|
fileName?: string;
|
|
1674
1702
|
reason?: string;
|
|
@@ -2693,11 +2721,50 @@ export interface EditorTelemetryEvent {
|
|
|
2693
2721
|
detail?: Record<string, unknown>;
|
|
2694
2722
|
}
|
|
2695
2723
|
|
|
2724
|
+
/**
|
|
2725
|
+
* Stage 0B.1 — parameters passed to `EditorHostAdapter.renderChartPreview`
|
|
2726
|
+
* when the importer encounters a `c:chartSpace` that has no cached
|
|
2727
|
+
* `mc:Fallback` bitmap. Hosts receive the raw chart-part XML plus the
|
|
2728
|
+
* theme XML and the intended display size, and return either preview
|
|
2729
|
+
* bytes (typically PNG or SVG) or `null` to fall back to the typed
|
|
2730
|
+
* badge.
|
|
2731
|
+
*
|
|
2732
|
+
* Field stability: only additive changes; no field is ever renamed or
|
|
2733
|
+
* removed. Host implementations should use structural narrowing to
|
|
2734
|
+
* ignore unknown fields in future versions.
|
|
2735
|
+
*/
|
|
2736
|
+
export interface ChartPreviewResolveParams {
|
|
2737
|
+
/** Chart part body (`word/charts/chartN.xml`). UTF-8 string. */
|
|
2738
|
+
chartXml: string;
|
|
2739
|
+
/** Absolute package path of the chart part (e.g. `/word/charts/chart1.xml`). Useful as a cache key. */
|
|
2740
|
+
chartPartPath: string;
|
|
2741
|
+
/** Body of `theme1.xml` when the package ships one; `undefined` otherwise. */
|
|
2742
|
+
themeXml: string | undefined;
|
|
2743
|
+
/** Intended display width in EMU (extracted from the drawing's `wp:extent`). */
|
|
2744
|
+
widthEmu: number;
|
|
2745
|
+
/** Intended display height in EMU. */
|
|
2746
|
+
heightEmu: number;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2696
2749
|
export interface EditorHostAdapter {
|
|
2697
2750
|
load?(params: LoadRequest): Promise<LoadResult>;
|
|
2698
2751
|
saveSession?(params: SaveSessionParams): Promise<SaveSessionResult>;
|
|
2699
2752
|
saveExport?(params: SaveExportParams): Promise<SaveExportResult>;
|
|
2700
2753
|
logEvent?(event: EditorTelemetryEvent): void;
|
|
2754
|
+
/**
|
|
2755
|
+
* Stage 0B.1: render a chart to bitmap/SVG preview bytes. Called at
|
|
2756
|
+
* import time for every `c:chartSpace` that ships without a cached
|
|
2757
|
+
* `mc:Fallback` blip. Return `null` to fall back to the typed badge
|
|
2758
|
+
* (Stage 0 behavior). Return a `Uint8Array` to inject the bytes as
|
|
2759
|
+
* a synthetic `MediaItem` and render the preview through the
|
|
2760
|
+
* existing `chart_atom` path.
|
|
2761
|
+
*
|
|
2762
|
+
* Content type is inferred from the first few bytes: `image/png`
|
|
2763
|
+
* for a PNG magic number, `image/svg+xml` otherwise. Hosts that
|
|
2764
|
+
* need a different content type should extend this contract in a
|
|
2765
|
+
* follow-up.
|
|
2766
|
+
*/
|
|
2767
|
+
renderChartPreview?(params: ChartPreviewResolveParams): Promise<Uint8Array | null>;
|
|
2701
2768
|
}
|
|
2702
2769
|
|
|
2703
2770
|
export interface EditorDatastoreAdapter {
|
|
@@ -2712,11 +2779,11 @@ export interface WordReviewEditorRef {
|
|
|
2712
2779
|
blur(): void;
|
|
2713
2780
|
undo(): void;
|
|
2714
2781
|
redo(): void;
|
|
2715
|
-
addComment(params: AddCommentParams):
|
|
2782
|
+
addComment(params: AddCommentParams): AddCommentResult;
|
|
2716
2783
|
openComment(commentId: string): void;
|
|
2717
2784
|
resolveComment(commentId: string): void;
|
|
2718
2785
|
reopenComment(commentId: string): void;
|
|
2719
|
-
addCommentReply(commentId: string, body: string):
|
|
2786
|
+
addCommentReply(commentId: string, body: string): AddCommentReplyResult;
|
|
2720
2787
|
editCommentBody(commentId: string, body: string): void;
|
|
2721
2788
|
deleteComment(commentId: string): void;
|
|
2722
2789
|
acceptChange(changeId: string): void;
|
|
@@ -3073,6 +3140,20 @@ export interface WordReviewEditorProps {
|
|
|
3073
3140
|
chromePreset?: WordReviewEditorChromePreset;
|
|
3074
3141
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
3075
3142
|
markupDisplay?: "clean" | "simple" | "all";
|
|
3143
|
+
/**
|
|
3144
|
+
* Harness/debug-only preview toggle for preserve-only features
|
|
3145
|
+
* (charts, SmartArt, shapes, WordArt, VML rendered as typed atoms;
|
|
3146
|
+
* the "N preserve-only features detected" banner; lock-callouts).
|
|
3147
|
+
*
|
|
3148
|
+
* MUST default to `false`. Consumer hosts MUST NOT hard-code `true` —
|
|
3149
|
+
* this is a dev opt-in gated by the harness dev drawer's
|
|
3150
|
+
* `debugMode && showUnsupportedObjectPreviews` AND-gate. Flipping the
|
|
3151
|
+
* default or dropping the AND-gate is a regression that has landed
|
|
3152
|
+
* three times (PRs #124, #131, #160); the invariant is locked by
|
|
3153
|
+
* `test/ui/unsupported-previews-invariant.test.ts`.
|
|
3154
|
+
*
|
|
3155
|
+
* @default false
|
|
3156
|
+
*/
|
|
3076
3157
|
showUnsupportedObjectPreviews?: boolean;
|
|
3077
3158
|
showReviewPanel?: boolean;
|
|
3078
3159
|
chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
|
|
@@ -3097,6 +3178,61 @@ export interface WordReviewEditorProps {
|
|
|
3097
3178
|
* inline "Comments panel" icon appears only when a callback is wired.
|
|
3098
3179
|
*/
|
|
3099
3180
|
onReviewSidebarComments?: () => void;
|
|
3181
|
+
/**
|
|
3182
|
+
* Optional: fires when the user invokes the Find shortcut
|
|
3183
|
+
* (Ctrl/Cmd+F) with the editor focused. When a host wires this
|
|
3184
|
+
* callback, the editor treats the shortcut as host-delegated and
|
|
3185
|
+
* suppresses the browser's native Find flow; when omitted, the
|
|
3186
|
+
* shortcut falls through to the browser default. The callback
|
|
3187
|
+
* receives the current selection so the host can pre-populate its
|
|
3188
|
+
* Find panel with the selected text.
|
|
3189
|
+
*
|
|
3190
|
+
* Capability id: `shortcut.find`. See
|
|
3191
|
+
* `src/runtime/editor-surface/capabilities.ts` for the full
|
|
3192
|
+
* capability contract.
|
|
3193
|
+
*/
|
|
3194
|
+
onFindRequested?: (context: ShortcutDelegationContext) => void;
|
|
3195
|
+
/**
|
|
3196
|
+
* Optional: fires when the user invokes Print (Ctrl/Cmd+P) with
|
|
3197
|
+
* the editor focused. When wired, the editor calls this callback
|
|
3198
|
+
* and suppresses the browser's native print dialog — the host is
|
|
3199
|
+
* expected to open its own print flow (e.g. a PDF export pipeline
|
|
3200
|
+
* that preserves review chrome). When omitted, Ctrl/Cmd+P falls
|
|
3201
|
+
* through to the browser's print dialog.
|
|
3202
|
+
*
|
|
3203
|
+
* Capability id: `shortcut.print`.
|
|
3204
|
+
*/
|
|
3205
|
+
onPrintRequested?: () => void;
|
|
3206
|
+
/**
|
|
3207
|
+
* Optional: fires when the user invokes a zoom shortcut
|
|
3208
|
+
* (Ctrl/Cmd+Plus, Ctrl/Cmd+Minus, Ctrl/Cmd+0). When wired, the
|
|
3209
|
+
* editor suppresses the browser's native zoom and delegates the
|
|
3210
|
+
* direction to the host. When omitted, the browser handles zoom
|
|
3211
|
+
* as usual.
|
|
3212
|
+
*
|
|
3213
|
+
* Capability ids: `shortcut.zoom-in`, `shortcut.zoom-out`,
|
|
3214
|
+
* `shortcut.zoom-reset`.
|
|
3215
|
+
*/
|
|
3216
|
+
onZoomRequested?: (direction: "in" | "out" | "reset") => void;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
/**
|
|
3220
|
+
* Selection context handed to host-delegated shortcut callbacks
|
|
3221
|
+
* (`onFindRequested`, future `onReplaceRequested`, etc.) so the host
|
|
3222
|
+
* can pre-populate its own UI with the user's current selection.
|
|
3223
|
+
*
|
|
3224
|
+
* - `selectionText` is truncated to the first 500 characters —
|
|
3225
|
+
* Find / Replace panels typically only need a snippet, and unbounded
|
|
3226
|
+
* text would be wasteful for large selections.
|
|
3227
|
+
* - `selectionRange` is the same shape exposed via the
|
|
3228
|
+
* `selection_changed` editor event, so hosts can reuse selection
|
|
3229
|
+
* plumbing.
|
|
3230
|
+
*/
|
|
3231
|
+
export interface ShortcutDelegationContext {
|
|
3232
|
+
/** The user-visible text of the selection, truncated to 500 chars. Empty string when collapsed. */
|
|
3233
|
+
selectionText: string;
|
|
3234
|
+
/** The selection range as a SelectionSnapshot. */
|
|
3235
|
+
selectionRange: SelectionSnapshot;
|
|
3100
3236
|
}
|
|
3101
3237
|
|
|
3102
3238
|
export interface WordReviewEditorChromeVisibility {
|
|
@@ -624,7 +624,13 @@ function applyAlignment(
|
|
|
624
624
|
return true;
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Adjusts the paragraph's indentation (or list level if the paragraph carries
|
|
629
|
+
* `numbering`) by ±INDENT_STEP_TWIPS. Mutates `paragraph` in place — caller
|
|
630
|
+
* must clone first if the source is shared. Returns `false` when no change
|
|
631
|
+
* occurred (already at the 0 / 8 bound, or no-op).
|
|
632
|
+
*/
|
|
633
|
+
export function applyIndentation(paragraph: ParagraphNode, delta: -1 | 1): boolean {
|
|
628
634
|
if (paragraph.numbering) {
|
|
629
635
|
const nextLevel = clamp(paragraph.numbering.level + delta, 0, 8);
|
|
630
636
|
if (nextLevel === paragraph.numbering.level) {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
insertHardBreak,
|
|
32
32
|
insertTab,
|
|
33
33
|
insertText,
|
|
34
|
+
outdentParagraphAtSelection,
|
|
34
35
|
splitParagraph,
|
|
35
36
|
} from "./text-commands.ts";
|
|
36
37
|
import type { RevisionRecord as CanonicalRevisionRecord } from "../../model/canonical-document.ts";
|
|
@@ -128,6 +129,10 @@ export type EditorCommand =
|
|
|
128
129
|
type: "text.insert-tab";
|
|
129
130
|
origin?: CommandOrigin;
|
|
130
131
|
}
|
|
132
|
+
| {
|
|
133
|
+
type: "text.outdent-tab";
|
|
134
|
+
origin?: CommandOrigin;
|
|
135
|
+
}
|
|
131
136
|
| {
|
|
132
137
|
type: "text.insert-hard-break";
|
|
133
138
|
origin?: CommandOrigin;
|
|
@@ -367,7 +372,7 @@ export interface TransactionEffects {
|
|
|
367
372
|
commentAdded?: { commentId: string; anchor: EditorAnchorProjection };
|
|
368
373
|
commentResolved?: { commentId: string };
|
|
369
374
|
commentReopened?: { commentId: string };
|
|
370
|
-
commentReplyAdded?: { commentId: string };
|
|
375
|
+
commentReplyAdded?: { commentId: string; entryId: string };
|
|
371
376
|
commentBodyEdited?: { commentId: string };
|
|
372
377
|
changeAccepted?: { changeId: string };
|
|
373
378
|
changeRejected?: { changeId: string };
|
|
@@ -513,6 +518,26 @@ export function executeEditorCommand(
|
|
|
513
518
|
insertTab(document, selection, context),
|
|
514
519
|
);
|
|
515
520
|
}
|
|
521
|
+
case "text.outdent-tab": {
|
|
522
|
+
// No suggesting-mode branch: outdent is a paragraph-format change, not a
|
|
523
|
+
// text-content change, so it bypasses the suggesting/track-changes pipeline.
|
|
524
|
+
// (If a tracked-changes test fails, copy the suggesting branch from
|
|
525
|
+
// text.insert-tab.)
|
|
526
|
+
//
|
|
527
|
+
// We use `buildDocumentReplaceTransaction` (not `applyTextCommand`) so the
|
|
528
|
+
// no-op cases — already at zero indent or list paragraph — skip the
|
|
529
|
+
// transaction entirely and do not bump revisionToken. This matches the
|
|
530
|
+
// pattern used by the formatting-indent path.
|
|
531
|
+
const result = outdentParagraphAtSelection(state.document, state.selection, {
|
|
532
|
+
timestamp: context.timestamp,
|
|
533
|
+
});
|
|
534
|
+
return buildDocumentReplaceTransaction(state, context, {
|
|
535
|
+
changed: result.changed,
|
|
536
|
+
document: result.document,
|
|
537
|
+
selection: result.selection,
|
|
538
|
+
mapping: result.mapping,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
516
541
|
case "text.insert-hard-break": {
|
|
517
542
|
const suggestingResult = context.documentMode === "suggesting"
|
|
518
543
|
? applySuggestingInsertUnit(state, "hard_break", context)
|
|
@@ -785,7 +810,7 @@ export function executeEditorCommand(
|
|
|
785
810
|
{
|
|
786
811
|
historyBoundary: "push",
|
|
787
812
|
markDirty: true,
|
|
788
|
-
effects: { commentReplyAdded: { commentId: command.commentId } },
|
|
813
|
+
effects: { commentReplyAdded: { commentId: command.commentId, entryId } },
|
|
789
814
|
},
|
|
790
815
|
);
|
|
791
816
|
}
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
resolveParagraphScope,
|
|
24
24
|
type StructuralMutationResult,
|
|
25
25
|
} from "./structural-helpers.ts";
|
|
26
|
+
import { applyIndentation } from "./formatting-commands.ts";
|
|
27
|
+
import { createEmptyMapping, type TransactionMapping } from "../selection/mapping.ts";
|
|
26
28
|
import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
|
|
27
29
|
|
|
28
30
|
export interface TextCommandContext {
|
|
@@ -197,6 +199,63 @@ export function insertTab(
|
|
|
197
199
|
);
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Reduce the active paragraph's left indent by one half-inch step (720 twips).
|
|
204
|
+
* Mirrors Word's Shift+Tab behavior on a non-list paragraph.
|
|
205
|
+
*
|
|
206
|
+
* Defensive list-check: if the paragraph carries `numbering`, this is a no-op —
|
|
207
|
+
* list-level changes are owned by the list-aware dispatcher in
|
|
208
|
+
* `src/runtime/edit-dispatch/list-aware-dispatch.ts`, which intercepts before
|
|
209
|
+
* we are called. The runtime command itself never mutates list level.
|
|
210
|
+
*
|
|
211
|
+
* No text positions change, so `mapping` is empty and the selection is preserved.
|
|
212
|
+
*/
|
|
213
|
+
export function outdentParagraphAtSelection(
|
|
214
|
+
document: CanonicalDocumentEnvelope,
|
|
215
|
+
selection: SelectionSnapshot,
|
|
216
|
+
context: TextCommandContext,
|
|
217
|
+
): {
|
|
218
|
+
changed: boolean;
|
|
219
|
+
document: CanonicalDocumentEnvelope;
|
|
220
|
+
selection: SelectionSnapshot;
|
|
221
|
+
mapping: TransactionMapping;
|
|
222
|
+
} {
|
|
223
|
+
const noop = {
|
|
224
|
+
changed: false,
|
|
225
|
+
document,
|
|
226
|
+
selection,
|
|
227
|
+
mapping: createEmptyMapping(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const scope = resolveParagraphScope(document, selection);
|
|
231
|
+
if (!scope) {
|
|
232
|
+
return noop;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Defensive: list paragraphs are owned by the list-aware dispatcher.
|
|
236
|
+
if (scope.paragraph.numbering) {
|
|
237
|
+
return noop;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// `resolveParagraphScope` already returns a cloned paragraph, so it is
|
|
241
|
+
// safe to mutate in place.
|
|
242
|
+
const changed = applyIndentation(scope.paragraph, -1);
|
|
243
|
+
if (!changed) {
|
|
244
|
+
return noop;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nextDocument = replaceParagraphScope(document, scope, [scope.paragraph]);
|
|
248
|
+
return {
|
|
249
|
+
changed: true,
|
|
250
|
+
document: {
|
|
251
|
+
...nextDocument,
|
|
252
|
+
updatedAt: context.timestamp,
|
|
253
|
+
},
|
|
254
|
+
selection,
|
|
255
|
+
mapping: createEmptyMapping(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
200
259
|
export function insertHardBreak(
|
|
201
260
|
document: CanonicalDocumentEnvelope,
|
|
202
261
|
selection: SelectionSnapshot,
|