@beyondwork/docx-react-component 1.0.52 → 1.0.54
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/package.json +31 -40
- package/src/api/public-types.ts +67 -7
- package/src/io/chart-preview-resolver.ts +41 -0
- package/src/io/docx-session.ts +217 -23
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +88 -8
- package/src/runtime/document-runtime.ts +182 -9
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +97 -2
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +70 -1
- package/src/runtime/prerender/cache-envelope.ts +30 -0
- package/src/runtime/prerender/customxml-cache.ts +17 -3
- package/src/runtime/prerender/prerender-document.ts +17 -1
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/render/render-kernel.ts +67 -19
- package/src/runtime/surface-projection.ts +28 -0
- package/src/runtime/table-schema.ts +27 -0
- package/src/runtime/table-style-resolver.ts +51 -0
- package/src/ui/WordReviewEditor.tsx +6 -3
- package/src/ui/editor-runtime-boundary.ts +39 -2
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
- package/src/ui-tailwind/theme/editor-theme.css +275 -46
- package/src/ui-tailwind/theme/tokens.css +345 -0
- package/src/ui-tailwind/theme/tokens.ts +313 -0
- package/src/ui-tailwind/theme/use-density.ts +60 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
} from "../layout/layout-engine-version.ts";
|
|
11
11
|
import { createLayoutEngine } from "../layout/layout-engine-instance.ts";
|
|
12
12
|
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
13
|
-
import
|
|
13
|
+
import { buildCompatibilityReport } from "../../validation/compatibility-engine.ts";
|
|
14
|
+
import {
|
|
15
|
+
CACHE_NORMALIZED_GENERATED_AT,
|
|
16
|
+
type CacheEnvelope,
|
|
17
|
+
} from "./cache-envelope.ts";
|
|
14
18
|
import {
|
|
15
19
|
computeStructuralHash,
|
|
16
20
|
deriveCacheKey,
|
|
@@ -167,6 +171,17 @@ export async function prerenderDocument(
|
|
|
167
171
|
canonicalDocumentHash,
|
|
168
172
|
});
|
|
169
173
|
|
|
174
|
+
// Phase 2 Finale C3: pre-compute `compatibilityReport` so the warm Plan B
|
|
175
|
+
// short-circuit in `loadDocxEditorSessionAsync` can skip the live
|
|
176
|
+
// `buildCompatibilityReport` call (~60–100 ms on extra-large). The report
|
|
177
|
+
// is a pure function of `canonicalDocument` + `generatedAt`; pinning
|
|
178
|
+
// `generatedAt` to `CACHE_NORMALIZED_GENERATED_AT` keeps the envelope
|
|
179
|
+
// byte-identical across two sequential prerenders on identical input.
|
|
180
|
+
const compatibilityReport = buildCompatibilityReport({
|
|
181
|
+
document: envelope,
|
|
182
|
+
generatedAt: CACHE_NORMALIZED_GENERATED_AT,
|
|
183
|
+
});
|
|
184
|
+
|
|
170
185
|
const cacheBlob: CacheEnvelope = {
|
|
171
186
|
schemaVersion: LAYCACHE_SCHEMA_VERSION,
|
|
172
187
|
engineVersion: LAYOUT_ENGINE_VERSION,
|
|
@@ -176,6 +191,7 @@ export async function prerenderDocument(
|
|
|
176
191
|
graph,
|
|
177
192
|
surface,
|
|
178
193
|
canonicalDocument: envelope,
|
|
194
|
+
compatibilityReport,
|
|
179
195
|
};
|
|
180
196
|
|
|
181
197
|
// Plan B B.5 — persistToCustomXml: inject the envelope into the docx's
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
RenderPage,
|
|
23
23
|
RenderStoryRegion,
|
|
24
24
|
DecorationIndex,
|
|
25
|
+
PageChromeReservations,
|
|
25
26
|
} from "./render-frame-types.ts";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -46,6 +47,14 @@ export interface ChangedPageEntry {
|
|
|
46
47
|
*/
|
|
47
48
|
changedBlockIds: readonly string[];
|
|
48
49
|
}[];
|
|
50
|
+
/**
|
|
51
|
+
* R1: Set when the page's physical frame rect changed but no individual
|
|
52
|
+
* block regions changed. Consumers that iterate `regions` for targeted
|
|
53
|
+
* re-render targets must also honour this flag to avoid silently skipping
|
|
54
|
+
* pages whose geometry shifted without block-level changes (e.g. zoom
|
|
55
|
+
* change or page margin edit that shifts every page frame uniformly).
|
|
56
|
+
*/
|
|
57
|
+
pageFrameChanged?: boolean;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export interface RenderFrameDiff {
|
|
@@ -110,10 +119,21 @@ export function diffRenderFrames(
|
|
|
110
119
|
continue;
|
|
111
120
|
}
|
|
112
121
|
const regions = diffPage(prevPage, nextPage, prev.decorationIndex, next.decorationIndex);
|
|
113
|
-
|
|
122
|
+
// R1: track page-frame geometry changes independently of block regions.
|
|
123
|
+
const frameChanged = !rectEquals(prevPage.frame, nextPage.frame);
|
|
124
|
+
// R2: track chrome-reservation changes so chrome re-projection isn't skipped.
|
|
125
|
+
const reservationsChanged = !reservationsEqual(
|
|
126
|
+
prevPage.chromeReservations,
|
|
127
|
+
nextPage.chromeReservations,
|
|
128
|
+
);
|
|
129
|
+
if (regions.length === 0 && !frameChanged && !reservationsChanged) {
|
|
114
130
|
unchangedPages.push(pageIndex);
|
|
115
131
|
} else {
|
|
116
|
-
changedPages.push({
|
|
132
|
+
changedPages.push({
|
|
133
|
+
pageIndex,
|
|
134
|
+
regions,
|
|
135
|
+
...(frameChanged ? { pageFrameChanged: true } : {}),
|
|
136
|
+
});
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
|
|
@@ -268,6 +288,22 @@ function decorationHashForBlock(
|
|
|
268
288
|
return tokens.join("|");
|
|
269
289
|
}
|
|
270
290
|
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Chrome reservations compare
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
// R2: Compare all five reservation fields so changes to rail/balloon lanes
|
|
296
|
+
// or footnote area trigger changedPages, not unchangedPages.
|
|
297
|
+
function reservationsEqual(a: PageChromeReservations, b: PageChromeReservations): boolean {
|
|
298
|
+
return (
|
|
299
|
+
a.railLaneTwips === b.railLaneTwips &&
|
|
300
|
+
a.balloonLaneTwips === b.balloonLaneTwips &&
|
|
301
|
+
a.footnoteAreaTwips === b.footnoteAreaTwips &&
|
|
302
|
+
a.pageFrameWidthPx === b.pageFrameWidthPx &&
|
|
303
|
+
a.pageFrameHeightPx === b.pageFrameHeightPx
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
271
307
|
// ---------------------------------------------------------------------------
|
|
272
308
|
// Geometry compare
|
|
273
309
|
// ---------------------------------------------------------------------------
|
|
@@ -23,12 +23,8 @@ import type {
|
|
|
23
23
|
PublicRegionBlock,
|
|
24
24
|
WordReviewEditorLayoutFacet,
|
|
25
25
|
} from "../layout/public-facet.ts";
|
|
26
|
-
import type {
|
|
27
|
-
|
|
28
|
-
} from "../../ui/headless/comment-decoration-model.ts";
|
|
29
|
-
import type {
|
|
30
|
-
RevisionDecorationModel,
|
|
31
|
-
} from "../../ui/headless/revision-decoration-model.ts";
|
|
26
|
+
import type { CommentDecorationModel } from "../../ui/headless/comment-decoration-model.ts";
|
|
27
|
+
import type { RevisionDecorationModel } from "../../ui/headless/revision-decoration-model.ts";
|
|
32
28
|
import type { ScopeRailSegment } from "../workflow-rail-segments.ts";
|
|
33
29
|
import {
|
|
34
30
|
resolveDecorationIndex,
|
|
@@ -148,6 +144,24 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
|
|
|
148
144
|
// is an optimizer for repeat reads at the same revision.
|
|
149
145
|
let lastEmittedFrame: RenderFrame | null = null;
|
|
150
146
|
|
|
147
|
+
// P14.c — single-slot DecorationIndex cache. `resolveDecorationIndex`
|
|
148
|
+
// is O(n × m) in decorations × offsets; it is safe to reuse the prior
|
|
149
|
+
// result when the layout revision, active story, zoom, and every source
|
|
150
|
+
// reference are all unchanged from the previous build. The frame-level
|
|
151
|
+
// `cache` already prevents rebuilds for repeat reads; this slot covers
|
|
152
|
+
// the post-invalidate rebuild where layout changed but decorations did not.
|
|
153
|
+
let diCacheKey: {
|
|
154
|
+
revision: number;
|
|
155
|
+
activeStoryKind: string;
|
|
156
|
+
zoomPxPerTwip: number;
|
|
157
|
+
workflowSegments: readonly ScopeRailSegment[] | undefined;
|
|
158
|
+
comments: CommentDecorationModel | null | undefined;
|
|
159
|
+
revisions: RevisionDecorationModel | null | undefined;
|
|
160
|
+
searchMatches: readonly SearchMatchRange[] | undefined;
|
|
161
|
+
lockedRanges: readonly LockedRangeInput[] | undefined;
|
|
162
|
+
} | null = null;
|
|
163
|
+
let diCacheValue: DecorationIndex | null = null;
|
|
164
|
+
|
|
151
165
|
const listeners = new Set<(event: RenderKernelEvent) => void>();
|
|
152
166
|
const unsubscribeFacet = facet.subscribe((event) => {
|
|
153
167
|
// Any layout-changing event invalidates the cached frame. We rely on
|
|
@@ -208,6 +222,14 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
|
|
|
208
222
|
pendingDeltas,
|
|
209
223
|
zoom.pxPerTwip,
|
|
210
224
|
);
|
|
225
|
+
// Revision: keyed off the engine's current page graph so repeated reads
|
|
226
|
+
// at the same revision return the same cached frame. We derive it
|
|
227
|
+
// from the first page since the engine stamps every page with the
|
|
228
|
+
// graph's revision indirectly via pageId; fall back to 0 if empty.
|
|
229
|
+
const revision = filteredPages[0]
|
|
230
|
+
? Number(extractRevisionFromPageId(filteredPages[0].pageId))
|
|
231
|
+
: 0;
|
|
232
|
+
|
|
211
233
|
const includeDecorations = options?.includeDecorations ?? true;
|
|
212
234
|
const sources = input.getDecorationSources?.();
|
|
213
235
|
const hasSources =
|
|
@@ -217,11 +239,45 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
|
|
|
217
239
|
(sources.revisions?.revisions?.length ?? 0) > 0 ||
|
|
218
240
|
(sources.searchMatches && sources.searchMatches.length > 0) ||
|
|
219
241
|
(sources.lockedRanges && sources.lockedRanges.length > 0));
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
242
|
+
// P14.c — DecorationIndex single-slot cache. Check reference equality
|
|
243
|
+
// on every source before calling the O(n × m) resolver.
|
|
244
|
+
const newDIKey = {
|
|
245
|
+
revision: Number.isFinite(revision) ? revision : 0,
|
|
246
|
+
activeStoryKind: activeStory.kind,
|
|
247
|
+
zoomPxPerTwip: zoom.pxPerTwip,
|
|
248
|
+
workflowSegments: sources?.workflowSegments,
|
|
249
|
+
comments: sources?.comments,
|
|
250
|
+
revisions: sources?.revisions,
|
|
251
|
+
searchMatches: sources?.searchMatches,
|
|
252
|
+
lockedRanges: sources?.lockedRanges,
|
|
253
|
+
};
|
|
254
|
+
const diCacheHit =
|
|
255
|
+
hasSources &&
|
|
256
|
+
diCacheKey !== null &&
|
|
257
|
+
diCacheValue !== null &&
|
|
258
|
+
diCacheKey.revision === newDIKey.revision &&
|
|
259
|
+
diCacheKey.activeStoryKind === newDIKey.activeStoryKind &&
|
|
260
|
+
diCacheKey.zoomPxPerTwip === newDIKey.zoomPxPerTwip &&
|
|
261
|
+
diCacheKey.workflowSegments === newDIKey.workflowSegments &&
|
|
262
|
+
diCacheKey.comments === newDIKey.comments &&
|
|
263
|
+
diCacheKey.revisions === newDIKey.revisions &&
|
|
264
|
+
diCacheKey.searchMatches === newDIKey.searchMatches &&
|
|
265
|
+
diCacheKey.lockedRanges === newDIKey.lockedRanges;
|
|
266
|
+
|
|
267
|
+
let decorationIndex: DecorationIndex;
|
|
268
|
+
if (!includeDecorations) {
|
|
269
|
+
decorationIndex = EMPTY_DECORATION_INDEX;
|
|
270
|
+
} else if (hasSources) {
|
|
271
|
+
if (diCacheHit) {
|
|
272
|
+
decorationIndex = diCacheValue!;
|
|
273
|
+
} else {
|
|
274
|
+
decorationIndex = resolveDecorationIndex({ anchorIndex: baseAnchorIndex, ...sources });
|
|
275
|
+
diCacheKey = newDIKey;
|
|
276
|
+
diCacheValue = decorationIndex;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
decorationIndex = buildDecorationIndex(renderPages);
|
|
280
|
+
}
|
|
225
281
|
const anchorIndex = buildAnchorIndex(
|
|
226
282
|
renderPages,
|
|
227
283
|
pendingDeltas,
|
|
@@ -229,14 +285,6 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
|
|
|
229
285
|
decorationIndex,
|
|
230
286
|
);
|
|
231
287
|
|
|
232
|
-
// Revision: keyed off the engine's current page graph so repeated reads
|
|
233
|
-
// at the same revision return the same cached frame. We derive it
|
|
234
|
-
// from the first page since the engine stamps every page with the
|
|
235
|
-
// graph's revision indirectly via pageId; fall back to 0 if empty.
|
|
236
|
-
const revision = filteredPages[0]
|
|
237
|
-
? Number(extractRevisionFromPageId(filteredPages[0].pageId))
|
|
238
|
-
: 0;
|
|
239
|
-
|
|
240
288
|
const frame: RenderFrame = {
|
|
241
289
|
revision: Number.isFinite(revision) ? revision : 0,
|
|
242
290
|
measurementFidelity,
|
|
@@ -419,6 +419,9 @@ function createTableBlock(
|
|
|
419
419
|
...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
|
|
420
420
|
...(cellBorders.borderBottom ? { borderBottom: cellBorders.borderBottom } : {}),
|
|
421
421
|
...(cellBorders.borderLeft ? { borderLeft: cellBorders.borderLeft } : {}),
|
|
422
|
+
// R3.a Phase 2: per-cell text-flow direction copied from canonical
|
|
423
|
+
// TableCellNode. Node-view renders `tbRl` / `btLr` as CSS writing-mode.
|
|
424
|
+
...(cell.textDirection ? { textDirection: cell.textDirection } : {}),
|
|
422
425
|
...(bandClasses ? { bandClasses } : {}),
|
|
423
426
|
content: cellContent,
|
|
424
427
|
});
|
|
@@ -437,6 +440,30 @@ function createTableBlock(
|
|
|
437
440
|
});
|
|
438
441
|
}
|
|
439
442
|
|
|
443
|
+
// R3.a Phase 2 — fold the resolver's table-level bundle onto the surface
|
|
444
|
+
// block when any field is populated. Borders carry the typed BorderSpec per
|
|
445
|
+
// side; the node-view converts the four outer sides to CSS shorthand at
|
|
446
|
+
// render time. insideH / insideV flow through for round-trip + future use,
|
|
447
|
+
// but their visual effect is realized via per-cell borders today.
|
|
448
|
+
const tr = resolvedTable.tableResolved;
|
|
449
|
+
const trHasContent =
|
|
450
|
+
tr.width !== undefined ||
|
|
451
|
+
tr.layoutMode !== undefined ||
|
|
452
|
+
tr.cellSpacing !== undefined ||
|
|
453
|
+
tr.borders !== undefined;
|
|
454
|
+
const tableResolvedAttr = trHasContent
|
|
455
|
+
? {
|
|
456
|
+
...(tr.width !== undefined ? { width: tr.width } : {}),
|
|
457
|
+
...(tr.widthType !== undefined ? { widthType: tr.widthType } : {}),
|
|
458
|
+
...(tr.layoutMode !== undefined ? { layoutMode: tr.layoutMode } : {}),
|
|
459
|
+
...(tr.cellSpacing !== undefined ? { cellSpacing: tr.cellSpacing } : {}),
|
|
460
|
+
...(tr.cellSpacingType !== undefined
|
|
461
|
+
? { cellSpacingType: tr.cellSpacingType }
|
|
462
|
+
: {}),
|
|
463
|
+
...(tr.borders ? { borders: tr.borders } : {}),
|
|
464
|
+
}
|
|
465
|
+
: undefined;
|
|
466
|
+
|
|
440
467
|
return {
|
|
441
468
|
block: {
|
|
442
469
|
blockId: `table-${tableIndex}`,
|
|
@@ -447,6 +474,7 @@ function createTableBlock(
|
|
|
447
474
|
gridColumns: table.gridColumns,
|
|
448
475
|
...(resolvedTable.table?.alignment ? { alignment: resolvedTable.table.alignment } : {}),
|
|
449
476
|
tblLook: resolvedTable.effectiveTblLook,
|
|
477
|
+
...(tableResolvedAttr ? { tableResolved: tableResolvedAttr } : {}),
|
|
450
478
|
rows,
|
|
451
479
|
},
|
|
452
480
|
lockedFragmentIds,
|
|
@@ -42,6 +42,8 @@ type TableCellAttrs = {
|
|
|
42
42
|
borderRight?: string | null;
|
|
43
43
|
borderBottom?: string | null;
|
|
44
44
|
borderLeft?: string | null;
|
|
45
|
+
/** R3.a Phase 2 — per-cell text-flow direction. */
|
|
46
|
+
textDirection?: "lrTb" | "tbRl" | "btLr" | null;
|
|
45
47
|
bandClasses?: string | null;
|
|
46
48
|
};
|
|
47
49
|
|
|
@@ -188,6 +190,13 @@ const tableCellSpecAttrs = {
|
|
|
188
190
|
borderRight: { default: null },
|
|
189
191
|
borderBottom: { default: null },
|
|
190
192
|
borderLeft: { default: null },
|
|
193
|
+
/**
|
|
194
|
+
* R3.a Phase 2 — per-cell text-flow direction copied from
|
|
195
|
+
* `TableCellNode.textDirection` ("lrTb" | "tbRl" | "btLr"). Node-view maps
|
|
196
|
+
* `tbRl` → `writing-mode: vertical-rl`, `btLr` → `writing-mode: vertical-lr`,
|
|
197
|
+
* `lrTb` (or null) → unset.
|
|
198
|
+
*/
|
|
199
|
+
textDirection: { default: null },
|
|
191
200
|
/** R2b: space-joined band classes ("band-firstRow band-band1Horz") from the resolved style. */
|
|
192
201
|
bandClasses: { default: null },
|
|
193
202
|
} as const;
|
|
@@ -210,6 +219,24 @@ export const tableNodeSpec: NodeSpec = {
|
|
|
210
219
|
tblLookNoVBand: { default: false },
|
|
211
220
|
/** R2d: raw `w:tblLook/@w:val` hex preserved verbatim so vendor-extended bits survive round-trip. */
|
|
212
221
|
tblLookVal: { default: null },
|
|
222
|
+
// R3.a Phase 2 — table-level resolved properties projected from
|
|
223
|
+
// ResolvedTableLevelProperties. Each is nullable so PM's null-vs-undefined
|
|
224
|
+
// preference is satisfied. Width is in twips (dxa) or fiftieths-of-percent
|
|
225
|
+
// (pct); spacing is twips (dxa). Borders are CSS shorthand strings (e.g.
|
|
226
|
+
// "1px solid #000000") for the four outer sides; insideH / insideV are
|
|
227
|
+
// realized through per-cell border attrs (CSS has no native
|
|
228
|
+
// table-inside-border property — see applyTableAttrs comment).
|
|
229
|
+
tableWidth: { default: null },
|
|
230
|
+
tableWidthType: { default: null },
|
|
231
|
+
tableLayoutMode: { default: null },
|
|
232
|
+
tableCellSpacing: { default: null },
|
|
233
|
+
tableCellSpacingType: { default: null },
|
|
234
|
+
tableBorderTop: { default: null },
|
|
235
|
+
tableBorderRight: { default: null },
|
|
236
|
+
tableBorderBottom: { default: null },
|
|
237
|
+
tableBorderLeft: { default: null },
|
|
238
|
+
tableBorderInsideH: { default: null },
|
|
239
|
+
tableBorderInsideV: { default: null },
|
|
213
240
|
},
|
|
214
241
|
parseDOM: [{ tag: "table" }],
|
|
215
242
|
toDOM(node) {
|
|
@@ -27,10 +27,31 @@ export interface ResolvedTableRowStyle {
|
|
|
27
27
|
activeConditionalRegions: TableStyleConditionalRegion[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* R3.a Phase 2 — typed bundle of table-level resolved properties projected onto
|
|
32
|
+
* the surface snapshot so the PM schema + node-view can render them inline at
|
|
33
|
+
* commit time.
|
|
34
|
+
*
|
|
35
|
+
* Borders are kept as the typed `TableBorders` shape here (BorderSpec per side);
|
|
36
|
+
* the surface-projection step converts them to per-side CSS shorthand strings
|
|
37
|
+
* (e.g. "1px solid #000") to mirror the existing `resolveCellBorderStyles`
|
|
38
|
+
* pattern at `surface-projection.ts:506`.
|
|
39
|
+
*/
|
|
40
|
+
export interface ResolvedTableLevelProperties {
|
|
41
|
+
width?: number;
|
|
42
|
+
widthType?: TableWidth["type"];
|
|
43
|
+
layoutMode?: "fixed" | "autofit";
|
|
44
|
+
cellSpacing?: number;
|
|
45
|
+
cellSpacingType?: TableWidth["type"];
|
|
46
|
+
borders?: TableBorders;
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
export interface ResolvedTableStyleResolution {
|
|
31
50
|
rawTblLook?: TableLook;
|
|
32
51
|
effectiveTblLook: TableLook;
|
|
33
52
|
table?: TableStyleFormatting["table"];
|
|
53
|
+
/** R3.a Phase 2 — table-level resolved properties (width / layout / spacing / borders). */
|
|
54
|
+
tableResolved: ResolvedTableLevelProperties;
|
|
34
55
|
rows: Array<{
|
|
35
56
|
style: ResolvedTableRowStyle;
|
|
36
57
|
cells: ResolvedTableCellStyle[];
|
|
@@ -140,10 +161,40 @@ export function resolveTableStyleResolution(
|
|
|
140
161
|
},
|
|
141
162
|
);
|
|
142
163
|
|
|
164
|
+
// R3.a Phase 2 — collect the table-level resolved property bundle. Direct
|
|
165
|
+
// TableNode properties win over the resolved table-style formatting; both
|
|
166
|
+
// paths still populate the optional fields independently so a partial style
|
|
167
|
+
// (e.g. style provides borders, direct provides width) merges cleanly.
|
|
168
|
+
const resolvedTableLevel: ResolvedTableLevelProperties = {};
|
|
169
|
+
const styleTable = resolvedStyle.formatting?.table;
|
|
170
|
+
const directWidth = table.width;
|
|
171
|
+
const styleWidth = styleTable?.width;
|
|
172
|
+
if (directWidth) {
|
|
173
|
+
resolvedTableLevel.width = directWidth.value;
|
|
174
|
+
resolvedTableLevel.widthType = directWidth.type;
|
|
175
|
+
} else if (styleWidth) {
|
|
176
|
+
resolvedTableLevel.width = styleWidth.value;
|
|
177
|
+
resolvedTableLevel.widthType = styleWidth.type;
|
|
178
|
+
}
|
|
179
|
+
if (table.layoutMode) {
|
|
180
|
+
resolvedTableLevel.layoutMode = table.layoutMode;
|
|
181
|
+
}
|
|
182
|
+
if (table.cellSpacing) {
|
|
183
|
+
resolvedTableLevel.cellSpacing = table.cellSpacing.value;
|
|
184
|
+
resolvedTableLevel.cellSpacingType = table.cellSpacing.type;
|
|
185
|
+
}
|
|
186
|
+
// Borders: prefer direct TableNode.borders, fall back to merged style borders.
|
|
187
|
+
// Both shapes are TableBorders so we can hand the side-bag straight back.
|
|
188
|
+
const mergedBorders = mergeBorderMap<TableBorders>(styleTable?.borders, table.borders);
|
|
189
|
+
if (mergedBorders) {
|
|
190
|
+
resolvedTableLevel.borders = mergedBorders;
|
|
191
|
+
}
|
|
192
|
+
|
|
143
193
|
return {
|
|
144
194
|
...(table.tblLook ? { rawTblLook: table.tblLook } : {}),
|
|
145
195
|
effectiveTblLook,
|
|
146
196
|
...(tableFormatting ? { table: tableFormatting } : {}),
|
|
197
|
+
tableResolved: resolvedTableLevel,
|
|
147
198
|
rows,
|
|
148
199
|
};
|
|
149
200
|
}
|
|
@@ -782,7 +782,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
782
782
|
clearWorkflowOverlay: () => {
|
|
783
783
|
runtime.clearWorkflowOverlay();
|
|
784
784
|
},
|
|
785
|
-
getWorkflowOverlay: () =>
|
|
785
|
+
getWorkflowOverlay: () => {
|
|
786
|
+
return clonePublicValue(runtime.getWorkflowOverlay());
|
|
787
|
+
},
|
|
786
788
|
setSharedWorkflowState: (state) => {
|
|
787
789
|
runtime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
788
790
|
},
|
|
@@ -1834,8 +1836,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1834
1836
|
clearWorkflowOverlay: () => {
|
|
1835
1837
|
activeRuntime.clearWorkflowOverlay();
|
|
1836
1838
|
},
|
|
1837
|
-
getWorkflowOverlay: () =>
|
|
1838
|
-
clonePublicValue(activeRuntime.getWorkflowOverlay())
|
|
1839
|
+
getWorkflowOverlay: () => {
|
|
1840
|
+
return clonePublicValue(activeRuntime.getWorkflowOverlay());
|
|
1841
|
+
},
|
|
1839
1842
|
setSharedWorkflowState: (state) => {
|
|
1840
1843
|
activeRuntime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
1841
1844
|
},
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
EditorError,
|
|
9
9
|
EditorHostAdapter,
|
|
10
10
|
EditorSessionState,
|
|
11
|
+
EditorSurfaceSnapshot,
|
|
11
12
|
EditorViewStateSnapshot,
|
|
12
13
|
EditorWarning,
|
|
13
14
|
ExportDocxOptions,
|
|
@@ -134,6 +135,8 @@ export interface EditorRuntimeBoundaryState {
|
|
|
134
135
|
runtime: WordReviewEditorRuntime | null;
|
|
135
136
|
loadError: EditorError | null;
|
|
136
137
|
activeRuntime: WordReviewEditorRuntime;
|
|
138
|
+
/** C2c: partial surface snapshot from body-normalize stage; null once full runtime is ready. */
|
|
139
|
+
progressiveSurface: EditorSurfaceSnapshot | null;
|
|
137
140
|
commandAppliedBridge: RuntimeCommandAppliedBridge;
|
|
138
141
|
fallbackSnapshot: RuntimeRenderSnapshot;
|
|
139
142
|
loadingSessionState: EditorSessionState;
|
|
@@ -302,6 +305,12 @@ export function useEditorRuntimeBoundary(
|
|
|
302
305
|
|
|
303
306
|
const [runtime, setRuntime] = useState<WordReviewEditorRuntime | null>(null);
|
|
304
307
|
const [loadError, setLoadError] = useState<EditorError | null>(null);
|
|
308
|
+
// C2c: progressive initial mount — transient surface snapshot from the
|
|
309
|
+
// body-normalize stage. Cleared when the full runtime commits (setRuntime).
|
|
310
|
+
const [progressiveSurface, setProgressiveSurface] =
|
|
311
|
+
useState<EditorSurfaceSnapshot | null>(null);
|
|
312
|
+
const progressiveSurfaceRef = useRef<EditorSurfaceSnapshot | null>(null);
|
|
313
|
+
progressiveSurfaceRef.current = progressiveSurface;
|
|
305
314
|
const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
|
|
306
315
|
const pendingReadySourceRef = useRef<"docx" | "session" | "snapshot" | null>(null);
|
|
307
316
|
const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -445,7 +454,20 @@ export function useEditorRuntimeBoundary(
|
|
|
445
454
|
bytes: source.initialDocx,
|
|
446
455
|
editorBuild: "dev",
|
|
447
456
|
scheduler,
|
|
448
|
-
...(probeResult ? { laycacheEnvelope: probeResult.envelope } : {
|
|
457
|
+
...(probeResult ? { laycacheEnvelope: probeResult.envelope } : {
|
|
458
|
+
// C2c: on the cold path (no cached envelope), emit the first
|
|
459
|
+
// viewport surface as soon as body-normalize completes so the
|
|
460
|
+
// UI can show the first page before the full session is ready.
|
|
461
|
+
// Wrapped in startTransition so React treats this as a
|
|
462
|
+
// low-priority update — the full setRuntime commit (urgent)
|
|
463
|
+
// will preempt it if it arrives before React flushes the
|
|
464
|
+
// transition.
|
|
465
|
+
onProgressiveSnapshot: (partial) => {
|
|
466
|
+
React.startTransition(() => {
|
|
467
|
+
setProgressiveSurface(partial.surface);
|
|
468
|
+
});
|
|
469
|
+
},
|
|
470
|
+
}),
|
|
449
471
|
});
|
|
450
472
|
if (cancelled) {
|
|
451
473
|
scheduler.dispose();
|
|
@@ -478,6 +500,10 @@ export function useEditorRuntimeBoundary(
|
|
|
478
500
|
recordPerfSample("runtime.create");
|
|
479
501
|
runtimeRef.current = nextRuntime;
|
|
480
502
|
pendingReadySourceRef.current = source.source;
|
|
503
|
+
// C2c: clear the transient progressive surface — the full runtime
|
|
504
|
+
// snapshot supersedes it. No need for startTransition here since
|
|
505
|
+
// this is the higher-priority commit that preempts the transition.
|
|
506
|
+
setProgressiveSurface(null);
|
|
481
507
|
setRuntime(nextRuntime);
|
|
482
508
|
} catch (error) {
|
|
483
509
|
if (cancelled) {
|
|
@@ -627,6 +653,9 @@ export function useEditorRuntimeBoundary(
|
|
|
627
653
|
sessionState: loadingSessionState,
|
|
628
654
|
viewState: loadingViewState,
|
|
629
655
|
navigation: loadingNavigation,
|
|
656
|
+
// C2c: ref so the bridge can serve the progressive surface in
|
|
657
|
+
// getRenderSnapshot().surface without recreating on each update.
|
|
658
|
+
progressiveSurfaceRef,
|
|
630
659
|
}),
|
|
631
660
|
[fallbackSnapshot, loadingNavigation, loadingSessionState, loadingViewState],
|
|
632
661
|
);
|
|
@@ -635,6 +664,7 @@ export function useEditorRuntimeBoundary(
|
|
|
635
664
|
runtime,
|
|
636
665
|
loadError,
|
|
637
666
|
activeRuntime: runtime ?? loadingRuntimeBridge,
|
|
667
|
+
progressiveSurface,
|
|
638
668
|
commandAppliedBridge,
|
|
639
669
|
fallbackSnapshot,
|
|
640
670
|
loadingSessionState,
|
|
@@ -876,6 +906,8 @@ function createLoadingRuntimeBridge(input: {
|
|
|
876
906
|
sessionState: EditorSessionState;
|
|
877
907
|
viewState: EditorViewStateSnapshot;
|
|
878
908
|
navigation: DocumentNavigationSnapshot;
|
|
909
|
+
/** C2c: when present, getRenderSnapshot() injects the progressive surface. */
|
|
910
|
+
progressiveSurfaceRef?: React.RefObject<EditorSurfaceSnapshot | null>;
|
|
879
911
|
}): WordReviewEditorRuntime {
|
|
880
912
|
const inertLayoutFacet = createInertLayoutFacet();
|
|
881
913
|
const emptyFieldSnapshot: FieldSnapshot = {
|
|
@@ -908,7 +940,11 @@ function createLoadingRuntimeBridge(input: {
|
|
|
908
940
|
subscribe: () => () => undefined,
|
|
909
941
|
subscribeToEvents: () => () => undefined,
|
|
910
942
|
emitBlockedCommand: () => undefined,
|
|
911
|
-
getRenderSnapshot: () =>
|
|
943
|
+
getRenderSnapshot: () => {
|
|
944
|
+
const progressive = input.progressiveSurfaceRef?.current;
|
|
945
|
+
if (progressive == null) return input.snapshot;
|
|
946
|
+
return { ...input.snapshot, surface: progressive };
|
|
947
|
+
},
|
|
912
948
|
getCanonicalDocument: () => input.sessionState.canonicalDocument,
|
|
913
949
|
getSourcePackage: () => input.sessionState.sourcePackage,
|
|
914
950
|
replaceText: () => undefined,
|
|
@@ -922,6 +958,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
922
958
|
}),
|
|
923
959
|
dispatch: () => undefined,
|
|
924
960
|
applyRemoteCommand: () => undefined,
|
|
961
|
+
applyRemoteCommandBatch: () => undefined,
|
|
925
962
|
undo: () => undefined,
|
|
926
963
|
redo: () => undefined,
|
|
927
964
|
focus: () => undefined,
|
|
@@ -77,7 +77,61 @@ export function getCommentRangeState(
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Markup display mode — controls how tracked changes and comments render.
|
|
82
|
+
*
|
|
83
|
+
* L6d.N2 widens the union to Word's 4-mode grammar (`"all-markup"`,
|
|
84
|
+
* `"simple-markup"`, `"no-markup"`, `"original"`) while keeping the
|
|
85
|
+
* legacy aliases `"clean" | "simple" | "all"` for backward compat with
|
|
86
|
+
* hosts that already pass the old values. Callers that switch on this
|
|
87
|
+
* value should either:
|
|
88
|
+
* - handle both sets of names (the decoration models do), or
|
|
89
|
+
* - normalize via `normalizeMarkupDisplay()` before matching.
|
|
90
|
+
*
|
|
91
|
+
* New in 6d.N2:
|
|
92
|
+
* - `"original"` — hide insertions entirely, render deletions as
|
|
93
|
+
* plain body text. Shows what the document looked like before the
|
|
94
|
+
* current batch of tracked changes.
|
|
95
|
+
*/
|
|
96
|
+
export type MarkupDisplay =
|
|
97
|
+
| "all-markup"
|
|
98
|
+
| "simple-markup"
|
|
99
|
+
| "no-markup"
|
|
100
|
+
| "original"
|
|
101
|
+
// Legacy aliases — accepted on the public API; prefer the canonical names above.
|
|
102
|
+
| "clean"
|
|
103
|
+
| "simple"
|
|
104
|
+
| "all";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Collapse legacy `MarkupDisplay` aliases to their canonical Word-grammar
|
|
108
|
+
* name:
|
|
109
|
+
* - `"all"` → `"all-markup"`
|
|
110
|
+
* - `"simple"` → `"simple-markup"`
|
|
111
|
+
* - `"clean"` → `"no-markup"`
|
|
112
|
+
* - `"original"` → `"original"` (identity)
|
|
113
|
+
*
|
|
114
|
+
* The two sets remain interchangeable inside the decoration models, but
|
|
115
|
+
* external surfaces (selector labels, telemetry, host events) should
|
|
116
|
+
* normalize first so Word's grammar is the single source of truth.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeMarkupDisplay(
|
|
119
|
+
value: MarkupDisplay,
|
|
120
|
+
): "all-markup" | "simple-markup" | "no-markup" | "original" {
|
|
121
|
+
switch (value) {
|
|
122
|
+
case "all":
|
|
123
|
+
case "all-markup":
|
|
124
|
+
return "all-markup";
|
|
125
|
+
case "simple":
|
|
126
|
+
case "simple-markup":
|
|
127
|
+
return "simple-markup";
|
|
128
|
+
case "clean":
|
|
129
|
+
case "no-markup":
|
|
130
|
+
return "no-markup";
|
|
131
|
+
case "original":
|
|
132
|
+
return "original";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
81
135
|
|
|
82
136
|
export function getCommentHighlightClass(
|
|
83
137
|
model: CommentDecorationModel | undefined,
|
|
@@ -90,10 +144,11 @@ export function getCommentHighlightClass(
|
|
|
90
144
|
return "";
|
|
91
145
|
}
|
|
92
146
|
|
|
93
|
-
switch (markupDisplay) {
|
|
94
|
-
case "
|
|
147
|
+
switch (normalizeMarkupDisplay(markupDisplay)) {
|
|
148
|
+
case "no-markup":
|
|
149
|
+
case "original":
|
|
95
150
|
return state.hasActive ? "bg-comment-soft" : "";
|
|
96
|
-
case "simple":
|
|
151
|
+
case "simple-markup":
|
|
97
152
|
if (state.hasActive) {
|
|
98
153
|
return "underline decoration-comment decoration-2 underline-offset-4";
|
|
99
154
|
}
|
|
@@ -101,7 +156,7 @@ export function getCommentHighlightClass(
|
|
|
101
156
|
return "underline decoration-comment/60 decoration-1 underline-offset-4";
|
|
102
157
|
}
|
|
103
158
|
return "underline decoration-comment/40 decoration-1 underline-offset-4";
|
|
104
|
-
case "all":
|
|
159
|
+
case "all-markup":
|
|
105
160
|
if (state.hasActive) {
|
|
106
161
|
return "bg-comment-strong";
|
|
107
162
|
}
|