@beyondwork/docx-react-component 1.0.47 → 1.0.49
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 +16 -11
- package/package.json +30 -41
- package/src/api/public-types.ts +199 -13
- package/src/compare/diff-engine.ts +4 -0
- package/src/core/commands/add-scope.ts +257 -0
- package/src/core/commands/formatting-commands.ts +2 -0
- package/src/core/commands/index.ts +9 -1
- package/src/core/commands/text-commands.ts +3 -1
- package/src/core/schema/text-schema.ts +95 -1
- package/src/core/selection/anchor-conversion.ts +112 -0
- package/src/core/selection/review-anchors.ts +108 -3
- package/src/core/state/text-transaction.ts +103 -7
- package/src/internal/harness-debug-ports.ts +168 -0
- package/src/io/chart-preview-resolver.ts +59 -1
- package/src/io/docx-session.ts +226 -38
- package/src/io/export/serialize-main-document.ts +46 -0
- package/src/io/export/serialize-paragraph-formatting.ts +8 -0
- package/src/io/export/serialize-run-formatting.ts +10 -1
- package/src/io/export/serialize-settings.ts +421 -0
- package/src/io/export/serialize-styles.ts +10 -0
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/chart/chart-style-table.ts +543 -0
- package/src/io/ooxml/chart/color-palette.ts +101 -0
- package/src/io/ooxml/chart/compose-series-color.ts +147 -0
- package/src/io/ooxml/chart/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +885 -0
- package/src/io/ooxml/chart/parse-series.ts +635 -0
- package/src/io/ooxml/chart/resolve-color.ts +261 -0
- package/src/io/ooxml/chart/types.ts +439 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +90 -2
- package/src/io/ooxml/parse-main-document.ts +156 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +46 -0
- package/src/io/ooxml/parse-run-formatting.ts +49 -0
- package/src/io/ooxml/parse-scope-markers.ts +184 -0
- package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
- package/src/io/ooxml/parse-settings.ts +97 -1
- package/src/io/ooxml/parse-styles.ts +65 -0
- package/src/io/ooxml/parse-theme.ts +2 -127
- package/src/io/ooxml/property-grab-bag.ts +211 -0
- package/src/io/ooxml/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +160 -0
- package/src/model/scope-markers.ts +144 -0
- package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
- package/src/runtime/collab/checkpoint-election.ts +75 -0
- package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
- package/src/runtime/collab/checkpoint-store.ts +115 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab/index.ts +29 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +330 -0
- package/src/runtime/collab/workflow-shared.ts +247 -0
- package/src/runtime/document-locations.ts +1 -9
- package/src/runtime/document-outline.ts +1 -9
- package/src/runtime/document-runtime.ts +288 -65
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- package/src/runtime/hyperlink-color-resolver.ts +119 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/prerender/cache-envelope.ts +19 -7
- package/src/runtime/prerender/cache-key.ts +25 -14
- package/src/runtime/prerender/canonical-document-hash.ts +63 -0
- package/src/runtime/prerender/customxml-cache.ts +211 -0
- package/src/runtime/prerender/customxml-probe.ts +78 -0
- package/src/runtime/prerender/prerender-document.ts +74 -7
- package/src/runtime/scope-resolver.ts +148 -0
- package/src/runtime/scope-tag-registry.ts +10 -0
- package/src/runtime/surface-projection.ts +102 -37
- package/src/runtime/theme-color-resolver.ts +188 -0
- package/src/runtime/workflow-markup.ts +7 -18
- package/src/ui/WordReviewEditor.tsx +48 -2
- package/src/ui/editor-runtime-boundary.ts +42 -1
- package/src/ui/headless/selection-helpers.ts +10 -23
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
- package/src/ui/unsupported-previews-policy.ts +23 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +10 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +47 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +88 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +16 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a (`<w:color>`-shaped) theme-color reference to a concrete hex
|
|
3
|
+
* string, applying the `w:themeTint` / `w:themeShade` HSL-luminance
|
|
4
|
+
* modulation per ECMA-376 §17.18.85 / §17.18.83.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* - The IO layer (`parse-run-formatting.ts`) captures
|
|
8
|
+
* `colorThemeSlot` / `colorThemeTint` / `colorThemeShade` on
|
|
9
|
+
* `CanonicalRunFormatting` as raw strings (preserved for
|
|
10
|
+
* byte-stable round-trip).
|
|
11
|
+
* - The render/cascade layer calls `resolveThemeColorHex(rPr, theme)`
|
|
12
|
+
* to collapse the reference to the actual hex colour the browser
|
|
13
|
+
* should paint. The original fields stay intact so export still
|
|
14
|
+
* round-trips the theme reference (not the computed hex).
|
|
15
|
+
*
|
|
16
|
+
* Lane 3 L2.c. Mirrors LibreOffice's `Color::ApplyTintOrShade` at
|
|
17
|
+
* `vendor/libreoffice/include/tools/color.hxx:200-240` (shape only; no
|
|
18
|
+
* code copied — our implementation is pure TypeScript against the
|
|
19
|
+
* ECMA-376 formulae).
|
|
20
|
+
*
|
|
21
|
+
* **Scope boundary:** this resolver handles the standard tint/shade
|
|
22
|
+
* pair. Theme-color-mapping (clrSchemeMapping — how "accent1" maps to a
|
|
23
|
+
* different `<w:clrScheme>` slot in older Word versions) is NOT applied
|
|
24
|
+
* here — `resolveThemeColor` from `src/io/ooxml/parse-theme.ts` is the
|
|
25
|
+
* authoritative slot→hex lookup and future work will layer scheme
|
|
26
|
+
* mapping into it.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { CanonicalRunFormatting, ResolvedTheme } from "../model/canonical-document.ts";
|
|
30
|
+
import { resolveThemeColor } from "../io/ooxml/parse-theme.ts";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Collapse `<w:color>`-style theme-slot + tint + shade references into a
|
|
34
|
+
* single resolved hex colour. Returns:
|
|
35
|
+
* - the raw `colorHex` as-is when it's a direct hex (the theme fields
|
|
36
|
+
* are ignored because direct colour wins per ECMA-376 cascade).
|
|
37
|
+
* - `"auto"` when the source declared `w:color w:val="auto"` (sentinel
|
|
38
|
+
* kept verbatim per our long-standing A.9 round-trip rule).
|
|
39
|
+
* - the theme slot's hex — tint/shade applied — when the source
|
|
40
|
+
* declared `w:themeColor="accent1"` etc. and the theme is available.
|
|
41
|
+
* - `undefined` otherwise (no colour declared, or theme slot absent).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveThemeColorHex(
|
|
44
|
+
rPr: Pick<
|
|
45
|
+
CanonicalRunFormatting,
|
|
46
|
+
"colorHex" | "colorThemeSlot" | "colorThemeTint" | "colorThemeShade"
|
|
47
|
+
>,
|
|
48
|
+
theme: ResolvedTheme | undefined,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (rPr.colorHex === "auto") return "auto";
|
|
51
|
+
if (rPr.colorHex) return rPr.colorHex;
|
|
52
|
+
if (!rPr.colorThemeSlot) return undefined;
|
|
53
|
+
|
|
54
|
+
const baseHex = resolveThemeColor(theme, rPr.colorThemeSlot);
|
|
55
|
+
if (!baseHex) return undefined;
|
|
56
|
+
|
|
57
|
+
return applyThemeTintShade(baseHex, rPr.colorThemeTint, rPr.colorThemeShade);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Apply `w:themeTint` (shift toward white) and/or `w:themeShade` (shift
|
|
62
|
+
* toward black) to a base hex colour. Pure function.
|
|
63
|
+
*
|
|
64
|
+
* Tint/shade are each a 0x00–0xFF hex byte representing a fraction of
|
|
65
|
+
* 255 (ECMA-376 §17.18.85 / §17.18.83). Only one may be present per
|
|
66
|
+
* `<w:color>`; when both are omitted, the base hex is returned.
|
|
67
|
+
*
|
|
68
|
+
* Formulae (per Microsoft's Word/OOXML guidance mirrored in LibreOffice):
|
|
69
|
+
* - tint byte T → frac = T / 255; newL = frac * L + (1 - frac) * 1.0
|
|
70
|
+
* (at T=0 the colour becomes pure white; at T=255 it stays unchanged)
|
|
71
|
+
* - shade byte S → frac = S / 255; newL = frac * L
|
|
72
|
+
* (at S=0 the colour becomes pure black; at S=255 it stays unchanged)
|
|
73
|
+
*
|
|
74
|
+
* These are LUMINANCE modulations in the HSL colour space (hue + saturation
|
|
75
|
+
* preserved). The conversion hex → HSL → hex is the standard 0–1 / 0–360
|
|
76
|
+
* form.
|
|
77
|
+
*/
|
|
78
|
+
export function applyThemeTintShade(
|
|
79
|
+
baseHex: string,
|
|
80
|
+
tint: string | undefined,
|
|
81
|
+
shade: string | undefined,
|
|
82
|
+
): string {
|
|
83
|
+
const tintByte = parseHexByte(tint);
|
|
84
|
+
const shadeByte = parseHexByte(shade);
|
|
85
|
+
if (tintByte === undefined && shadeByte === undefined) {
|
|
86
|
+
return baseHex;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rgb = parseHexColor(baseHex);
|
|
90
|
+
if (!rgb) return baseHex;
|
|
91
|
+
|
|
92
|
+
const hsl = rgbToHsl(rgb);
|
|
93
|
+
let newL = hsl.l;
|
|
94
|
+
if (tintByte !== undefined) {
|
|
95
|
+
const frac = tintByte / 255;
|
|
96
|
+
newL = frac * hsl.l + (1 - frac);
|
|
97
|
+
} else if (shadeByte !== undefined) {
|
|
98
|
+
const frac = shadeByte / 255;
|
|
99
|
+
newL = frac * hsl.l;
|
|
100
|
+
}
|
|
101
|
+
newL = Math.max(0, Math.min(1, newL));
|
|
102
|
+
|
|
103
|
+
const out = hslToRgb({ h: hsl.h, s: hsl.s, l: newL });
|
|
104
|
+
return formatHexColor(out);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseHexByte(value: string | undefined): number | undefined {
|
|
108
|
+
if (!value) return undefined;
|
|
109
|
+
const n = Number.parseInt(value, 16);
|
|
110
|
+
if (!Number.isFinite(n)) return undefined;
|
|
111
|
+
return Math.max(0, Math.min(255, n));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseHexColor(hex: string): { r: number; g: number; b: number } | undefined {
|
|
115
|
+
const normalized = hex.replace(/^#/u, "").trim();
|
|
116
|
+
if (normalized.length !== 6) return undefined;
|
|
117
|
+
const n = Number.parseInt(normalized, 16);
|
|
118
|
+
if (!Number.isFinite(n)) return undefined;
|
|
119
|
+
return {
|
|
120
|
+
r: (n >> 16) & 0xff,
|
|
121
|
+
g: (n >> 8) & 0xff,
|
|
122
|
+
b: n & 0xff,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatHexColor(rgb: { r: number; g: number; b: number }): string {
|
|
127
|
+
const to2 = (n: number): string => Math.round(n).toString(16).padStart(2, "0").toUpperCase();
|
|
128
|
+
return `${to2(rgb.r)}${to2(rgb.g)}${to2(rgb.b)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rgbToHsl(rgb: { r: number; g: number; b: number }): {
|
|
132
|
+
h: number;
|
|
133
|
+
s: number;
|
|
134
|
+
l: number;
|
|
135
|
+
} {
|
|
136
|
+
const r = rgb.r / 255;
|
|
137
|
+
const g = rgb.g / 255;
|
|
138
|
+
const b = rgb.b / 255;
|
|
139
|
+
const max = Math.max(r, g, b);
|
|
140
|
+
const min = Math.min(r, g, b);
|
|
141
|
+
const l = (max + min) / 2;
|
|
142
|
+
let h = 0;
|
|
143
|
+
let s = 0;
|
|
144
|
+
if (max !== min) {
|
|
145
|
+
const d = max - min;
|
|
146
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
147
|
+
switch (max) {
|
|
148
|
+
case r:
|
|
149
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
150
|
+
break;
|
|
151
|
+
case g:
|
|
152
|
+
h = ((b - r) / d + 2) * 60;
|
|
153
|
+
break;
|
|
154
|
+
default:
|
|
155
|
+
h = ((r - g) / d + 4) * 60;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { h, s, l };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hslToRgb(hsl: { h: number; s: number; l: number }): {
|
|
162
|
+
r: number;
|
|
163
|
+
g: number;
|
|
164
|
+
b: number;
|
|
165
|
+
} {
|
|
166
|
+
const { h, s, l } = hsl;
|
|
167
|
+
if (s === 0) {
|
|
168
|
+
const gray = l * 255;
|
|
169
|
+
return { r: gray, g: gray, b: gray };
|
|
170
|
+
}
|
|
171
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
172
|
+
const p = 2 * l - q;
|
|
173
|
+
const hueToRgb = (t: number): number => {
|
|
174
|
+
let tt = t;
|
|
175
|
+
if (tt < 0) tt += 1;
|
|
176
|
+
if (tt > 1) tt -= 1;
|
|
177
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
178
|
+
if (tt < 1 / 2) return q;
|
|
179
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
180
|
+
return p;
|
|
181
|
+
};
|
|
182
|
+
const hFrac = h / 360;
|
|
183
|
+
return {
|
|
184
|
+
r: hueToRgb(hFrac + 1 / 3) * 255,
|
|
185
|
+
g: hueToRgb(hFrac) * 255,
|
|
186
|
+
b: hueToRgb(hFrac - 1 / 3) * 255,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
WorkflowCommentMarkup,
|
|
23
23
|
} from "../api/public-types";
|
|
24
24
|
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
25
|
+
import { createPublicRangeAnchor } from "../core/selection/anchor-conversion.ts";
|
|
25
26
|
import {
|
|
26
27
|
projectSurfaceText,
|
|
27
28
|
searchProjectedSurfaceText,
|
|
@@ -145,7 +146,7 @@ export function collectWorkflowMarkupSnapshot(input: {
|
|
|
145
146
|
markupId: `protected-range:${range.rangeId}`,
|
|
146
147
|
kind: "protected_range",
|
|
147
148
|
rangeId: range.rangeId,
|
|
148
|
-
anchor:
|
|
149
|
+
anchor: createPublicRangeAnchor(range.start, range.end),
|
|
149
150
|
storyTarget: MAIN_STORY_TARGET,
|
|
150
151
|
label: `Protected range ${range.rangeId}`,
|
|
151
152
|
excerpt: range.enforcementReason,
|
|
@@ -283,7 +284,7 @@ function collectSurfaceMarkup(
|
|
|
283
284
|
kind: "opaque_fragment",
|
|
284
285
|
fragmentId: block.fragmentId,
|
|
285
286
|
warningId: block.warningId,
|
|
286
|
-
anchor:
|
|
287
|
+
anchor: createPublicRangeAnchor(block.from, block.to),
|
|
287
288
|
storyTarget,
|
|
288
289
|
label: block.label,
|
|
289
290
|
excerpt: block.detail,
|
|
@@ -308,7 +309,7 @@ function collectSegmentMarkup(
|
|
|
308
309
|
highlights.push({
|
|
309
310
|
markupId: `highlight:${storyTargetKey(storyTarget)}:${segment.from}:${segment.to}:${segment.markAttrs.backgroundColor}`,
|
|
310
311
|
kind: "highlight",
|
|
311
|
-
anchor:
|
|
312
|
+
anchor: createPublicRangeAnchor(segment.from, segment.to),
|
|
312
313
|
storyTarget,
|
|
313
314
|
label: `Highlight ${segment.markAttrs.backgroundColor}`,
|
|
314
315
|
excerpt: segment.text,
|
|
@@ -326,7 +327,7 @@ function collectSegmentMarkup(
|
|
|
326
327
|
kind: "opaque_fragment",
|
|
327
328
|
fragmentId: segment.fragmentId,
|
|
328
329
|
warningId: segment.warningId,
|
|
329
|
-
anchor:
|
|
330
|
+
anchor: createPublicRangeAnchor(segment.from, segment.to),
|
|
330
331
|
storyTarget,
|
|
331
332
|
label: segment.label,
|
|
332
333
|
excerpt: segment.detail,
|
|
@@ -380,7 +381,7 @@ function collectFieldMarkup(
|
|
|
380
381
|
{
|
|
381
382
|
markupId: `field:${field.index}`,
|
|
382
383
|
kind: "field",
|
|
383
|
-
anchor:
|
|
384
|
+
anchor: createPublicRangeAnchor(match.from, match.to),
|
|
384
385
|
storyTarget: story.storyTarget,
|
|
385
386
|
label: displayText,
|
|
386
387
|
excerpt: field.instruction,
|
|
@@ -417,7 +418,7 @@ function collectOpaqueFragmentMarkup(
|
|
|
417
418
|
kind: "opaque_fragment",
|
|
418
419
|
fragmentId: fragment.fragmentId,
|
|
419
420
|
warningId: fragment.warningId,
|
|
420
|
-
anchor:
|
|
421
|
+
anchor: createPublicRangeAnchor(fragment.lastKnownRange.from, fragment.lastKnownRange.to),
|
|
421
422
|
storyTarget: MAIN_STORY_TARGET,
|
|
422
423
|
label: descriptor.label,
|
|
423
424
|
excerpt: descriptor.detail,
|
|
@@ -427,18 +428,6 @@ function collectOpaqueFragmentMarkup(
|
|
|
427
428
|
});
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
function createRangeAnchor(from: number, to: number): EditorAnchorProjection {
|
|
431
|
-
return {
|
|
432
|
-
kind: "range",
|
|
433
|
-
from,
|
|
434
|
-
to,
|
|
435
|
-
assoc: {
|
|
436
|
-
start: -1,
|
|
437
|
-
end: 1,
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
431
|
function storyTargetKey(storyTarget: EditorStoryTarget): string {
|
|
443
432
|
switch (storyTarget.kind) {
|
|
444
433
|
case "main":
|
|
@@ -75,6 +75,7 @@ import type {
|
|
|
75
75
|
ZoomLevel,
|
|
76
76
|
} from "../api/public-types";
|
|
77
77
|
import { MetadataResolverMissingError } from "../api/public-types";
|
|
78
|
+
import { readHarnessDebugPortsFlag } from "../internal/harness-debug-ports.ts";
|
|
78
79
|
import type { ScopeMetadataResolver } from "../api/scope-metadata-resolver-types.ts";
|
|
79
80
|
import {
|
|
80
81
|
editorSessionStateFromPersistedSnapshot,
|
|
@@ -178,6 +179,7 @@ import {
|
|
|
178
179
|
useRuntimeSnapshotSlice,
|
|
179
180
|
useRuntimeValue,
|
|
180
181
|
} from "./runtime-snapshot-selectors.ts";
|
|
182
|
+
import { computeEffectiveShowUnsupportedPreviews } from "./unsupported-previews-policy.ts";
|
|
181
183
|
import type { MarkupDisplay } from "./headless/comment-decoration-model";
|
|
182
184
|
import { resolveScopedChromePolicy } from "./headless/scoped-chrome-policy";
|
|
183
185
|
import type {
|
|
@@ -494,6 +496,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
494
496
|
deleteComment: (commentId) => {
|
|
495
497
|
applyRuntimeDeleteComment(runtime, commentId);
|
|
496
498
|
},
|
|
499
|
+
addScope: (params) => runtime.addScope(params),
|
|
500
|
+
getScope: (scopeId) => runtime.getScope(scopeId),
|
|
501
|
+
removeScope: (scopeId) => runtime.removeScope(scopeId),
|
|
497
502
|
acceptChange: (changeId) => runtime.acceptChange(changeId),
|
|
498
503
|
rejectChange: (changeId) => runtime.rejectChange(changeId),
|
|
499
504
|
acceptAllChanges: () => runtime.acceptAllChanges(),
|
|
@@ -776,6 +781,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
776
781
|
clearWorkflowOverlay: () => {
|
|
777
782
|
runtime.clearWorkflowOverlay();
|
|
778
783
|
},
|
|
784
|
+
setSharedWorkflowState: (state) => {
|
|
785
|
+
runtime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
786
|
+
},
|
|
779
787
|
getWorkflowScopeSnapshot: () => {
|
|
780
788
|
return clonePublicValue(runtime.getWorkflowScopeSnapshot());
|
|
781
789
|
},
|
|
@@ -1033,7 +1041,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1033
1041
|
chromePreset,
|
|
1034
1042
|
chromeOptions,
|
|
1035
1043
|
markupDisplay,
|
|
1036
|
-
|
|
1044
|
+
__harnessDebugPorts,
|
|
1045
|
+
unsupportedPreviewsPolicy = "never",
|
|
1037
1046
|
onError,
|
|
1038
1047
|
onEvent,
|
|
1039
1048
|
onWarning,
|
|
@@ -1042,6 +1051,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1042
1051
|
onFindRequested,
|
|
1043
1052
|
onPrintRequested,
|
|
1044
1053
|
onZoomRequested,
|
|
1054
|
+
onReplaceRequested,
|
|
1055
|
+
onGoToRequested,
|
|
1056
|
+
onSpellRequested,
|
|
1057
|
+
onThesaurusRequested,
|
|
1058
|
+
onExtendSelectionRequested,
|
|
1059
|
+
onLastEditRequested,
|
|
1045
1060
|
readOnly = false,
|
|
1046
1061
|
reviewMode = "review",
|
|
1047
1062
|
suggestionsEnabled = false,
|
|
@@ -1477,6 +1492,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1477
1492
|
deleteComment: (commentId) => {
|
|
1478
1493
|
applyRuntimeDeleteComment(activeRuntime, commentId);
|
|
1479
1494
|
},
|
|
1495
|
+
addScope: (params) => activeRuntime.addScope(params),
|
|
1496
|
+
getScope: (scopeId) => activeRuntime.getScope(scopeId),
|
|
1497
|
+
removeScope: (scopeId) => activeRuntime.removeScope(scopeId),
|
|
1480
1498
|
acceptChange: (changeId) => activeRuntime.acceptChange(changeId),
|
|
1481
1499
|
rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
|
|
1482
1500
|
acceptAllChanges: () => activeRuntime.acceptAllChanges(),
|
|
@@ -1813,6 +1831,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1813
1831
|
clearWorkflowOverlay: () => {
|
|
1814
1832
|
activeRuntime.clearWorkflowOverlay();
|
|
1815
1833
|
},
|
|
1834
|
+
setSharedWorkflowState: (state) => {
|
|
1835
|
+
activeRuntime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
1836
|
+
},
|
|
1816
1837
|
getWorkflowScopeSnapshot: () => {
|
|
1817
1838
|
return clonePublicValue(activeRuntime.getWorkflowScopeSnapshot());
|
|
1818
1839
|
},
|
|
@@ -2608,6 +2629,24 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2608
2629
|
shortcut.shortcut === "zoom-out" ? "out" : "reset";
|
|
2609
2630
|
onZoomRequested(direction);
|
|
2610
2631
|
handled = true;
|
|
2632
|
+
} else if (shortcut.shortcut === "replace" && onReplaceRequested) {
|
|
2633
|
+
onReplaceRequested({ selectionText: "", selectionRange: snapshot.selection });
|
|
2634
|
+
handled = true;
|
|
2635
|
+
} else if (shortcut.shortcut === "go-to" && onGoToRequested) {
|
|
2636
|
+
onGoToRequested({ selectionText: "", selectionRange: snapshot.selection });
|
|
2637
|
+
handled = true;
|
|
2638
|
+
} else if (shortcut.shortcut === "spell" && onSpellRequested) {
|
|
2639
|
+
onSpellRequested({ selectionText: "", selectionRange: snapshot.selection });
|
|
2640
|
+
handled = true;
|
|
2641
|
+
} else if (shortcut.shortcut === "thesaurus" && onThesaurusRequested) {
|
|
2642
|
+
onThesaurusRequested({ selectionText: "", selectionRange: snapshot.selection });
|
|
2643
|
+
handled = true;
|
|
2644
|
+
} else if (shortcut.shortcut === "extend-selection" && onExtendSelectionRequested) {
|
|
2645
|
+
onExtendSelectionRequested({ selectionText: "", selectionRange: snapshot.selection });
|
|
2646
|
+
handled = true;
|
|
2647
|
+
} else if (shortcut.shortcut === "last-edit" && onLastEditRequested) {
|
|
2648
|
+
onLastEditRequested({ selectionText: "", selectionRange: snapshot.selection });
|
|
2649
|
+
handled = true;
|
|
2611
2650
|
}
|
|
2612
2651
|
if (handled) {
|
|
2613
2652
|
event.preventDefault();
|
|
@@ -2908,6 +2947,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2908
2947
|
},
|
|
2909
2948
|
});
|
|
2910
2949
|
|
|
2950
|
+
const harnessShowUnsupportedPreviews = readHarnessDebugPortsFlag(__harnessDebugPorts, "unsupportedObjectPreviews");
|
|
2951
|
+
const effectiveShowUnsupportedPreviews = computeEffectiveShowUnsupportedPreviews({
|
|
2952
|
+
harnessShowUnsupportedPreviews,
|
|
2953
|
+
unsupportedPreviewsPolicy,
|
|
2954
|
+
reviewMode,
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2911
2957
|
const documentElement = (
|
|
2912
2958
|
<EditorSurfaceController
|
|
2913
2959
|
ref={surfaceRef}
|
|
@@ -2918,7 +2964,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2918
2964
|
documentNavigation={documentNavigation}
|
|
2919
2965
|
reviewMode={reviewMode}
|
|
2920
2966
|
markupDisplay={liveMarkupDisplay}
|
|
2921
|
-
showUnsupportedObjectPreviews={
|
|
2967
|
+
showUnsupportedObjectPreviews={effectiveShowUnsupportedPreviews}
|
|
2922
2968
|
activeRevisionId={activeRevisionId}
|
|
2923
2969
|
activeSelectionToolKind={activeSelectionTool?.kind ?? null}
|
|
2924
2970
|
showTrackedChanges={showTrackedChanges}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
loadDocxEditorSession,
|
|
45
45
|
loadDocxEditorSessionAsync,
|
|
46
46
|
} from "../io/docx-session.ts";
|
|
47
|
+
import { tryReadLaycacheEnvelope } from "../runtime/prerender/customxml-probe.ts";
|
|
47
48
|
import {
|
|
48
49
|
createLoadScheduler,
|
|
49
50
|
type LoadScheduler,
|
|
@@ -79,6 +80,14 @@ export interface ResolvedSource {
|
|
|
79
80
|
* does the classic synchronous load.
|
|
80
81
|
*/
|
|
81
82
|
preloadedDocxSession?: ReturnType<typeof loadDocxEditorSession>;
|
|
83
|
+
/**
|
|
84
|
+
* L7 Phase 2.7 — when the editor-runtime-boundary probe finds a
|
|
85
|
+
* laycache envelope in `/customXml/item1.xml`, its `graph` is stashed
|
|
86
|
+
* here and forwarded to `createDocumentRuntime` as `seedLayoutCache`.
|
|
87
|
+
* This is the Plan A (graph-seed) win layered on top of the Plan B
|
|
88
|
+
* (loader short-circuit) win. Undefined when no envelope is available.
|
|
89
|
+
*/
|
|
90
|
+
preloadedLaycacheGraph?: import("../runtime/layout/page-graph.ts").RuntimePageGraph;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
export interface CreateRuntimeArgs {
|
|
@@ -404,6 +413,18 @@ export function useEditorRuntimeBoundary(
|
|
|
404
413
|
// can paint the skeleton while the rest of the parse finishes.
|
|
405
414
|
// SSR / Node tests fall through to the synchronous load inside
|
|
406
415
|
// `createRuntime`.
|
|
416
|
+
//
|
|
417
|
+
// L7 Phase 2.7 — Plan B read-path wiring. Before invoking the
|
|
418
|
+
// async loader, probe the docx for a laycache envelope stashed
|
|
419
|
+
// in `/customXml/item1.xml` (shipped by Plan B Slice 2). When the
|
|
420
|
+
// probe returns a validated envelope, pass it to the loader so
|
|
421
|
+
// the five expensive parse stages short-circuit against the
|
|
422
|
+
// cached `canonicalDocument`. Measured win on 138-pp extra-large
|
|
423
|
+
// CCEP: ~947 ms cold → ~590 ms warm (Δ 376 ms), same delta the
|
|
424
|
+
// Node-side `prerender-cache.bench.ts` records. Probe is ~20–50 ms;
|
|
425
|
+
// `tryReadLaycacheEnvelope` returns null on any rejection path
|
|
426
|
+
// (missing part, stale structural hash, schema mismatch) and the
|
|
427
|
+
// loader falls through to the full parse.
|
|
407
428
|
if (
|
|
408
429
|
source.initialDocx !== undefined &&
|
|
409
430
|
source.preloadedDocxSession === undefined &&
|
|
@@ -411,12 +432,20 @@ export function useEditorRuntimeBoundary(
|
|
|
411
432
|
) {
|
|
412
433
|
const scheduler = createLoadScheduler();
|
|
413
434
|
loadSchedulerRef.current = scheduler;
|
|
435
|
+
const probeResult = await tryReadLaycacheEnvelope(source.initialDocx);
|
|
436
|
+
if (cancelled) {
|
|
437
|
+
scheduler.dispose();
|
|
438
|
+
loadSchedulerRef.current = null;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
recordPerfSample("loadSession.laycacheProbe");
|
|
414
442
|
const preloaded = await loadDocxEditorSessionAsync({
|
|
415
443
|
documentId,
|
|
416
444
|
sourceLabel: source.sourceLabel,
|
|
417
445
|
bytes: source.initialDocx,
|
|
418
446
|
editorBuild: "dev",
|
|
419
447
|
scheduler,
|
|
448
|
+
...(probeResult ? { laycacheEnvelope: probeResult.envelope } : {}),
|
|
420
449
|
});
|
|
421
450
|
if (cancelled) {
|
|
422
451
|
scheduler.dispose();
|
|
@@ -424,6 +453,9 @@ export function useEditorRuntimeBoundary(
|
|
|
424
453
|
return;
|
|
425
454
|
}
|
|
426
455
|
source.preloadedDocxSession = preloaded;
|
|
456
|
+
if (probeResult) {
|
|
457
|
+
source.preloadedLaycacheGraph = probeResult.envelope.graph;
|
|
458
|
+
}
|
|
427
459
|
}
|
|
428
460
|
|
|
429
461
|
const nextRuntime = createRuntime(
|
|
@@ -662,6 +694,9 @@ function createRuntime(
|
|
|
662
694
|
editorBuild: runtimeSessionState.editorBuild,
|
|
663
695
|
fatalError: docxSession?.fatalError,
|
|
664
696
|
protectionSnapshot: docxSession?.protectionSnapshot,
|
|
697
|
+
...(args.source.preloadedLaycacheGraph
|
|
698
|
+
? { seedLayoutCache: args.source.preloadedLaycacheGraph }
|
|
699
|
+
: {}),
|
|
665
700
|
exportDocx: async (sessionState, options) => {
|
|
666
701
|
if (docxSession) {
|
|
667
702
|
return docxSession.exportDocx(sessionState, options);
|
|
@@ -900,6 +935,11 @@ function createLoadingRuntimeBridge(input: {
|
|
|
900
935
|
throw createLoadingBoundaryError(input.snapshot.documentId, "comment");
|
|
901
936
|
},
|
|
902
937
|
editCommentBody: () => undefined,
|
|
938
|
+
addScope: () => {
|
|
939
|
+
throw createLoadingBoundaryError(input.snapshot.documentId, "scope");
|
|
940
|
+
},
|
|
941
|
+
getScope: () => null,
|
|
942
|
+
removeScope: () => undefined,
|
|
903
943
|
acceptChange: () => undefined,
|
|
904
944
|
rejectChange: () => undefined,
|
|
905
945
|
acceptAllChanges: () => undefined,
|
|
@@ -960,6 +1000,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
960
1000
|
Promise.reject(createLoadingBoundaryError(input.snapshot.documentId, "export")),
|
|
961
1001
|
setWorkflowOverlay: () => undefined,
|
|
962
1002
|
clearWorkflowOverlay: () => undefined,
|
|
1003
|
+
setSharedWorkflowState: () => undefined,
|
|
963
1004
|
getWorkflowOverlay: () => null,
|
|
964
1005
|
getWorkflowScopeSnapshot: () => null,
|
|
965
1006
|
getInteractionGuardSnapshot: () => ({ effectiveMode: "edit", blockedReasons: [] }),
|
|
@@ -1016,7 +1057,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1016
1057
|
|
|
1017
1058
|
function createLoadingBoundaryError(
|
|
1018
1059
|
documentId: string,
|
|
1019
|
-
target: "comment" | "session" | "snapshot" | "export",
|
|
1060
|
+
target: "comment" | "session" | "snapshot" | "export" | "scope",
|
|
1020
1061
|
): EditorError {
|
|
1021
1062
|
return {
|
|
1022
1063
|
errorId: `${documentId}-loading-${target}`,
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
import type { SelectionSnapshot } from "../../api/public-types";
|
|
2
|
+
import {
|
|
3
|
+
createPublicNodeAnchor,
|
|
4
|
+
createPublicRangeAnchor,
|
|
5
|
+
} from "../../core/selection/anchor-conversion.ts";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Headless-UI-side `createSelectionSnapshot` that produces the **public**
|
|
5
|
-
* `EditorAnchorProjection` shape
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
* `EditorAnchorProjection` definitions in `src/api/public-types.ts` vs
|
|
10
|
-
* `src/core/selection/mapping.ts`. Do not merge without first unifying
|
|
11
|
-
* those two definitions.
|
|
9
|
+
* `EditorAnchorProjection` shape via the canonical
|
|
10
|
+
* `createPublicRangeAnchor` constructor. The runtime-facing twin at
|
|
11
|
+
* `src/core/state/editor-state.ts` produces the internal `RangeAnchor`
|
|
12
|
+
* shape (`range: { from, to }`) — the two are not interchangeable.
|
|
12
13
|
*/
|
|
13
14
|
export function createSelectionSnapshot(anchor: number, head = anchor): SelectionSnapshot {
|
|
14
|
-
const from = Math.min(anchor, head);
|
|
15
|
-
const to = Math.max(anchor, head);
|
|
16
15
|
return {
|
|
17
16
|
anchor,
|
|
18
17
|
head,
|
|
19
18
|
isCollapsed: anchor === head,
|
|
20
|
-
activeRange:
|
|
21
|
-
kind: "range",
|
|
22
|
-
from,
|
|
23
|
-
to,
|
|
24
|
-
assoc: {
|
|
25
|
-
start: -1,
|
|
26
|
-
end: 1,
|
|
27
|
-
},
|
|
28
|
-
},
|
|
19
|
+
activeRange: createPublicRangeAnchor(anchor, head),
|
|
29
20
|
};
|
|
30
21
|
}
|
|
31
22
|
|
|
@@ -34,11 +25,7 @@ export function createNodeSelectionSnapshot(at: number, assoc: -1 | 1 = 1): Sele
|
|
|
34
25
|
anchor: at,
|
|
35
26
|
head: at,
|
|
36
27
|
isCollapsed: true,
|
|
37
|
-
activeRange:
|
|
38
|
-
kind: "node",
|
|
39
|
-
at,
|
|
40
|
-
assoc,
|
|
41
|
-
},
|
|
28
|
+
activeRange: createPublicNodeAnchor(at, assoc),
|
|
42
29
|
};
|
|
43
30
|
}
|
|
44
31
|
|
|
@@ -23,7 +23,12 @@ export interface SurfaceShortcutContext {
|
|
|
23
23
|
|
|
24
24
|
export type ShellShortcutResolution =
|
|
25
25
|
| { kind: "none" }
|
|
26
|
-
| { kind: "delegate"; shortcut:
|
|
26
|
+
| { kind: "delegate"; shortcut:
|
|
27
|
+
| "find" | "print"
|
|
28
|
+
| "zoom-in" | "zoom-out" | "zoom-reset"
|
|
29
|
+
| "replace" | "go-to"
|
|
30
|
+
| "spell" | "thesaurus"
|
|
31
|
+
| "extend-selection" | "last-edit" }
|
|
27
32
|
| { kind: "block"; command: string; reason: WorkflowBlockedCommandReason }
|
|
28
33
|
| { kind: "history"; history: "undo" | "redo" }
|
|
29
34
|
| { kind: "focus-region"; direction: 1 | -1 }
|
|
@@ -115,14 +120,14 @@ export function resolveShellShortcut(
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
if (isReplaceShortcut(input, key)) {
|
|
118
|
-
return
|
|
123
|
+
return { kind: "delegate", shortcut: "replace" };
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
if (
|
|
122
127
|
isGoToShortcut(input, key) ||
|
|
123
128
|
(key === "f5" && !input.shiftKey)
|
|
124
129
|
) {
|
|
125
|
-
return
|
|
130
|
+
return { kind: "delegate", shortcut: "go-to" };
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
if (isModShiftShortcut(input, key, "e")) {
|
|
@@ -130,19 +135,19 @@ export function resolveShellShortcut(
|
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
if (key === "f7" && !input.shiftKey) {
|
|
133
|
-
return
|
|
138
|
+
return { kind: "delegate", shortcut: "spell" };
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
if (key === "f7" && input.shiftKey) {
|
|
137
|
-
return
|
|
142
|
+
return { kind: "delegate", shortcut: "thesaurus" };
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
if (key === "f8") {
|
|
141
|
-
return
|
|
146
|
+
return { kind: "delegate", shortcut: "extend-selection" };
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
if (key === "f5" && input.shiftKey) {
|
|
145
|
-
return
|
|
150
|
+
return { kind: "delegate", shortcut: "last-edit" };
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
return { kind: "none" };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I4 — Effective-visibility rule for preserve-only previews.
|
|
3
|
+
*
|
|
4
|
+
* Combines the harness-only opaque-token flag (from
|
|
5
|
+
* `__harnessDebugPorts`, read via `readHarnessDebugPortsFlag(..., "unsupportedObjectPreviews")`)
|
|
6
|
+
* with the public sibling `unsupportedPreviewsPolicy` prop (default
|
|
7
|
+
* `"never"`). The harness token remains unreachable from consumer
|
|
8
|
+
* apps (see `test/ui/unsupported-previews-invariant.test.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export interface EffectiveShowUnsupportedPreviewsInput {
|
|
11
|
+
harnessShowUnsupportedPreviews: boolean;
|
|
12
|
+
unsupportedPreviewsPolicy: "never" | "review-only" | "always";
|
|
13
|
+
reviewMode: "editing" | "review";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function computeEffectiveShowUnsupportedPreviews(
|
|
17
|
+
input: EffectiveShowUnsupportedPreviewsInput,
|
|
18
|
+
): boolean {
|
|
19
|
+
if (input.harnessShowUnsupportedPreviews === true) return true;
|
|
20
|
+
if (input.unsupportedPreviewsPolicy === "always") return true;
|
|
21
|
+
if (input.unsupportedPreviewsPolicy === "review-only" && input.reviewMode === "review") return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
@@ -152,6 +152,14 @@ export interface TwChromeOverlayProps {
|
|
|
152
152
|
* handle leaves focus-restore as a no-op — DOM reparent still runs.
|
|
153
153
|
*/
|
|
154
154
|
pmView?: PmPortalView | null;
|
|
155
|
+
/**
|
|
156
|
+
* L7 Phase 2.8 — viewport cull for `TwPageStackChromeLayer`. Sequential
|
|
157
|
+
* page-index range (plus overscan) that should render full chrome
|
|
158
|
+
* bands; pages outside the range render empty frame wrappers only.
|
|
159
|
+
* When omitted, every page's chrome mounts (pre-Phase-2.8 behavior).
|
|
160
|
+
* See `useVisiblePageIndexRange` in `src/ui-tailwind/page-stack/use-visible-block-range.ts`.
|
|
161
|
+
*/
|
|
162
|
+
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
/**
|
|
@@ -188,6 +196,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
188
196
|
onOpenStory,
|
|
189
197
|
pmSurfaceElement,
|
|
190
198
|
pmView,
|
|
199
|
+
visiblePageIndexRange,
|
|
191
200
|
}) => {
|
|
192
201
|
return (
|
|
193
202
|
<div
|
|
@@ -211,6 +220,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
211
220
|
onOpenStory={onOpenStory}
|
|
212
221
|
pmSurfaceElement={pmSurfaceElement}
|
|
213
222
|
pmView={pmView}
|
|
223
|
+
visiblePageIndexRange={visiblePageIndexRange ?? null}
|
|
214
224
|
/>
|
|
215
225
|
) : null}
|
|
216
226
|
<TwScopeRailLayer
|