@beyondwork/docx-react-component 1.0.109 → 1.0.111
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 +1 -1
- package/src/api/public-types.ts +3 -0
- package/src/model/layout/page-graph-types.ts +33 -0
- package/src/model/layout/runtime-page-graph-types.ts +25 -0
- package/src/runtime/document-runtime.ts +46 -0
- package/src/runtime/geometry/adjacent-geometry-intake.ts +820 -15
- package/src/runtime/geometry/caret-geometry.ts +219 -7
- package/src/runtime/geometry/geometry-index.ts +52 -12
- package/src/runtime/geometry/object-handles.ts +42 -1
- package/src/runtime/layout/index.ts +3 -0
- package/src/runtime/layout/inert-layout-facet.ts +13 -0
- package/src/runtime/layout/layout-engine-instance.ts +233 -4
- package/src/runtime/layout/layout-engine-version.ts +47 -2
- package/src/runtime/layout/layout-facet-types.ts +3 -0
- package/src/runtime/layout/page-graph.ts +88 -7
- package/src/runtime/layout/paginated-layout-engine.ts +34 -0
- package/src/runtime/layout/project-block-fragments.ts +144 -1
- package/src/runtime/layout/public-facet.ts +228 -9
- package/src/runtime/layout/resolve-page-previews.ts +46 -8
- package/src/runtime/scopes/adjacent-geometry-evidence.ts +456 -0
- package/src/runtime/scopes/compile-scope-bundle.ts +8 -0
- package/src/runtime/scopes/evidence.ts +16 -0
- package/src/runtime/scopes/index.ts +13 -0
- package/src/runtime/scopes/semantic-scope-types.ts +67 -0
- package/src/ui-tailwind/chrome-overlay/tw-table-split-row-carry-overlay.tsx +62 -0
- package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +104 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +50 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +27 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +62 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +1 -0
- package/src/README.md +0 -85
- package/src/api/README.md +0 -26
- package/src/api/v3/README.md +0 -91
- package/src/component-inventory.md +0 -99
- package/src/core/README.md +0 -10
- package/src/core/commands/README.md +0 -3
- package/src/core/schema/README.md +0 -3
- package/src/core/selection/README.md +0 -3
- package/src/core/state/README.md +0 -3
- package/src/io/README.md +0 -10
- package/src/io/export/README.md +0 -3
- package/src/io/normalize/README.md +0 -3
- package/src/io/ooxml/README.md +0 -3
- package/src/io/opc/README.md +0 -3
- package/src/model/README.md +0 -3
- package/src/preservation/README.md +0 -3
- package/src/review/README.md +0 -16
- package/src/review/store/README.md +0 -3
- package/src/runtime/README.md +0 -3
- package/src/ui/README.md +0 -30
- package/src/ui/comments/README.md +0 -3
- package/src/ui/compatibility/README.md +0 -3
- package/src/ui/editor-surface/README.md +0 -3
- package/src/ui/review/README.md +0 -3
- package/src/ui/status/README.md +0 -3
- package/src/ui/theme/README.md +0 -3
- package/src/ui/toolbar/README.md +0 -3
- package/src/ui-tailwind/debug/README.md +0 -22
- package/src/validation/README.md +0 -3
|
@@ -289,6 +289,12 @@ export interface ScopeBundleEvidence {
|
|
|
289
289
|
* `requires-rehydration` or `unavailable`; Layer 08 never fabricates rects.
|
|
290
290
|
*/
|
|
291
291
|
readonly geometry?: ScopeGeometryEvidence;
|
|
292
|
+
/**
|
|
293
|
+
* Bounded Layer-05 adjacent geometry evidence. This is compositor/read
|
|
294
|
+
* evidence only; it does not make a scope replaceable. Rows are present only
|
|
295
|
+
* when L05 marked them `compositorReady` in `frame-px` space.
|
|
296
|
+
*/
|
|
297
|
+
readonly adjacentGeometry?: ScopeAdjacentGeometryEvidence;
|
|
292
298
|
/**
|
|
293
299
|
* Presentation hint only. Consumers may use this to choose a cheap inline
|
|
294
300
|
* treatment for field-like scopes versus a broader overlay treatment for
|
|
@@ -449,6 +455,67 @@ export interface ScopeGeometryEvidence {
|
|
|
449
455
|
};
|
|
450
456
|
}
|
|
451
457
|
|
|
458
|
+
export type ScopeAdjacentGeometryAxis = "numbering-marker" | "field-region";
|
|
459
|
+
|
|
460
|
+
export interface ScopeAdjacentGeometryFramePixelRect {
|
|
461
|
+
readonly leftPx: number;
|
|
462
|
+
readonly topPx: number;
|
|
463
|
+
readonly widthPx: number;
|
|
464
|
+
readonly heightPx: number;
|
|
465
|
+
readonly coordinateSpace: "frame-px";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface ScopeAdjacentGeometryFramePixelPoint {
|
|
469
|
+
readonly xPx: number;
|
|
470
|
+
readonly yPx: number;
|
|
471
|
+
readonly coordinateSpace: "frame-px";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export interface ScopeAdjacentGeometryRowEvidence {
|
|
475
|
+
readonly axis: ScopeAdjacentGeometryAxis;
|
|
476
|
+
readonly source: "l05-adjacent-geometry-intake";
|
|
477
|
+
readonly docId?: string;
|
|
478
|
+
readonly canonicalBlockId?: string;
|
|
479
|
+
readonly runtimeFragmentId?: string;
|
|
480
|
+
readonly pageId?: string;
|
|
481
|
+
readonly pageIndex?: number;
|
|
482
|
+
readonly frameId?: string;
|
|
483
|
+
readonly precision: "word-page-local-calibration";
|
|
484
|
+
readonly coordinateSpace: "frame-px";
|
|
485
|
+
readonly compositorReady: true;
|
|
486
|
+
readonly scaleSource?: "runtime-render-frame-page-rect";
|
|
487
|
+
readonly renderFrameRevision?: number;
|
|
488
|
+
readonly numbering?: {
|
|
489
|
+
readonly runtimeNumberingId?: string;
|
|
490
|
+
readonly canonicalNumberingInstanceId?: string;
|
|
491
|
+
readonly canonicalLevel?: number;
|
|
492
|
+
readonly markerKind?: string;
|
|
493
|
+
readonly markerSuffix?: string;
|
|
494
|
+
readonly markerLane?: ScopeAdjacentGeometryFramePixelRect;
|
|
495
|
+
readonly textColumn?: ScopeAdjacentGeometryFramePixelRect;
|
|
496
|
+
};
|
|
497
|
+
readonly fieldRegion?: {
|
|
498
|
+
readonly canonicalFieldId?: string;
|
|
499
|
+
readonly runtimeFieldRegionId?: string;
|
|
500
|
+
readonly instructionFamily?: string;
|
|
501
|
+
readonly fieldStartAnchorPx?: ScopeAdjacentGeometryFramePixelPoint;
|
|
502
|
+
readonly fieldEndAnchorPx?: ScopeAdjacentGeometryFramePixelPoint;
|
|
503
|
+
readonly fieldResultRangePx?: ScopeAdjacentGeometryFramePixelRect;
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export interface ScopeAdjacentGeometryEvidence {
|
|
508
|
+
readonly status: "available" | "unavailable";
|
|
509
|
+
readonly source: "l05-adjacent-geometry-intake";
|
|
510
|
+
readonly schemaVersion?: "layer-05-adjacent-geometry-intake/v2";
|
|
511
|
+
readonly rowCount: number;
|
|
512
|
+
readonly rows?: readonly ScopeAdjacentGeometryRowEvidence[];
|
|
513
|
+
readonly reason?:
|
|
514
|
+
| "adjacent-geometry-provider-unavailable"
|
|
515
|
+
| "scope-adjacent-frame-pixel-row-unavailable"
|
|
516
|
+
| "l05-adjacent-intake-not-compositor-ready";
|
|
517
|
+
}
|
|
518
|
+
|
|
452
519
|
export type ScopeVisualizationClass = "field" | "broad";
|
|
453
520
|
|
|
454
521
|
export interface ScopeVisualizationHint {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface TableSplitRowCarryOverlayEntry {
|
|
4
|
+
blockId: string;
|
|
5
|
+
rowIndex: number;
|
|
6
|
+
topPx: number;
|
|
7
|
+
leftPx: number;
|
|
8
|
+
widthPx: number;
|
|
9
|
+
heightPx: number;
|
|
10
|
+
continuesFromPreviousPage?: boolean;
|
|
11
|
+
continuesToNextPage?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TwTableSplitRowCarryOverlayProps {
|
|
15
|
+
entries: readonly TableSplitRowCarryOverlayEntry[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function TwTableSplitRowCarryOverlayInner({
|
|
19
|
+
entries,
|
|
20
|
+
}: TwTableSplitRowCarryOverlayProps): React.ReactElement | null {
|
|
21
|
+
if (entries.length === 0) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{entries.map((entry) => (
|
|
26
|
+
<div
|
|
27
|
+
key={`${entry.blockId}:${entry.rowIndex}:${entry.topPx}:${entry.heightPx}`}
|
|
28
|
+
aria-hidden
|
|
29
|
+
data-table-split-row-carry=""
|
|
30
|
+
data-block-id={entry.blockId}
|
|
31
|
+
data-row-index={entry.rowIndex}
|
|
32
|
+
data-continues-from-previous-page={
|
|
33
|
+
entry.continuesFromPreviousPage ? "true" : undefined
|
|
34
|
+
}
|
|
35
|
+
data-continues-to-next-page={
|
|
36
|
+
entry.continuesToNextPage ? "true" : undefined
|
|
37
|
+
}
|
|
38
|
+
style={{
|
|
39
|
+
position: "absolute",
|
|
40
|
+
top: `${entry.topPx}px`,
|
|
41
|
+
left: `${entry.leftPx}px`,
|
|
42
|
+
width: `${entry.widthPx}px`,
|
|
43
|
+
height: `${entry.heightPx}px`,
|
|
44
|
+
pointerEvents: "none",
|
|
45
|
+
boxSizing: "border-box",
|
|
46
|
+
borderTop: entry.continuesFromPreviousPage
|
|
47
|
+
? "1px dashed var(--color-border-accent)"
|
|
48
|
+
: undefined,
|
|
49
|
+
borderBottom: entry.continuesToNextPage
|
|
50
|
+
? "1px dashed var(--color-border-accent)"
|
|
51
|
+
: undefined,
|
|
52
|
+
backgroundColor: "color-mix(in srgb, var(--color-accent) 8%, transparent)",
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const TwTableSplitRowCarryOverlay = React.memo(
|
|
61
|
+
TwTableSplitRowCarryOverlayInner,
|
|
62
|
+
);
|
|
@@ -215,6 +215,38 @@ export interface Layer11RenderCalibrationSummary {
|
|
|
215
215
|
route: "presentation-check" | "lower-layer-fact-required" | "insufficient-evidence";
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
export type Layer11AdjacentGeometryAxis = "numbering-marker" | "field-region";
|
|
219
|
+
|
|
220
|
+
export interface Layer11AdjacentGeometryIntakeLike {
|
|
221
|
+
readonly schemaVersion?: unknown;
|
|
222
|
+
readonly totals?: {
|
|
223
|
+
readonly numberingCompositorReadyRows?: unknown;
|
|
224
|
+
readonly fieldRegionCompositorReadyRows?: unknown;
|
|
225
|
+
};
|
|
226
|
+
readonly pageLocalNormalization?: {
|
|
227
|
+
readonly framePixelCoordinateSpace?: unknown;
|
|
228
|
+
readonly framePixelPrecision?: unknown;
|
|
229
|
+
readonly compositorReady?: unknown;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface Layer11AdjacentGeometryAxisReadiness {
|
|
234
|
+
axis: Layer11AdjacentGeometryAxis;
|
|
235
|
+
compositorReadyRows: number;
|
|
236
|
+
assertionReady: boolean;
|
|
237
|
+
route: "presentation-consumer-ready" | "lower-layer-fact-required";
|
|
238
|
+
reason: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface Layer11AdjacentGeometryConsumerSummary {
|
|
242
|
+
schemaVersion: string | null;
|
|
243
|
+
framePixelCoordinateSpace: string | null;
|
|
244
|
+
framePixelPrecision: string | null;
|
|
245
|
+
assertionReadyCount: number;
|
|
246
|
+
lowerLayerFactRequiredCount: number;
|
|
247
|
+
entries: readonly Layer11AdjacentGeometryAxisReadiness[];
|
|
248
|
+
}
|
|
249
|
+
|
|
218
250
|
const PRIMARY_RENDER_EVIDENCE = new Set<Layer11RenderEvidenceKind>([
|
|
219
251
|
"word-oracle",
|
|
220
252
|
"render-word",
|
|
@@ -270,3 +302,75 @@ export function summarizeWordFirstRenderCalibration(
|
|
|
270
302
|
route: "presentation-check",
|
|
271
303
|
};
|
|
272
304
|
}
|
|
305
|
+
|
|
306
|
+
export function summarizeLayer11AdjacentGeometryConsumer(
|
|
307
|
+
intake: Layer11AdjacentGeometryIntakeLike,
|
|
308
|
+
): Layer11AdjacentGeometryConsumerSummary {
|
|
309
|
+
const schemaVersion =
|
|
310
|
+
typeof intake.schemaVersion === "string" ? intake.schemaVersion : null;
|
|
311
|
+
const normalization = intake.pageLocalNormalization;
|
|
312
|
+
const framePixelCoordinateSpace =
|
|
313
|
+
typeof normalization?.framePixelCoordinateSpace === "string"
|
|
314
|
+
? normalization.framePixelCoordinateSpace
|
|
315
|
+
: null;
|
|
316
|
+
const framePixelPrecision =
|
|
317
|
+
typeof normalization?.framePixelPrecision === "string"
|
|
318
|
+
? normalization.framePixelPrecision
|
|
319
|
+
: null;
|
|
320
|
+
const normalizationReady =
|
|
321
|
+
schemaVersion === "layer-05-adjacent-geometry-intake/v2" &&
|
|
322
|
+
framePixelCoordinateSpace === "frame-px" &&
|
|
323
|
+
normalization?.compositorReady === true;
|
|
324
|
+
const entries: Layer11AdjacentGeometryAxisReadiness[] = [
|
|
325
|
+
summarizeAdjacentGeometryAxis(
|
|
326
|
+
"numbering-marker",
|
|
327
|
+
numberValue(intake.totals?.numberingCompositorReadyRows),
|
|
328
|
+
normalizationReady,
|
|
329
|
+
),
|
|
330
|
+
summarizeAdjacentGeometryAxis(
|
|
331
|
+
"field-region",
|
|
332
|
+
numberValue(intake.totals?.fieldRegionCompositorReadyRows),
|
|
333
|
+
normalizationReady,
|
|
334
|
+
),
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
schemaVersion,
|
|
339
|
+
framePixelCoordinateSpace,
|
|
340
|
+
framePixelPrecision,
|
|
341
|
+
assertionReadyCount: entries.filter((entry) => entry.assertionReady).length,
|
|
342
|
+
lowerLayerFactRequiredCount: entries.filter((entry) => !entry.assertionReady)
|
|
343
|
+
.length,
|
|
344
|
+
entries,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function summarizeAdjacentGeometryAxis(
|
|
349
|
+
axis: Layer11AdjacentGeometryAxis,
|
|
350
|
+
compositorReadyRows: number,
|
|
351
|
+
normalizationReady: boolean,
|
|
352
|
+
): Layer11AdjacentGeometryAxisReadiness {
|
|
353
|
+
const assertionReady = normalizationReady && compositorReadyRows > 0;
|
|
354
|
+
if (assertionReady) {
|
|
355
|
+
return {
|
|
356
|
+
axis,
|
|
357
|
+
compositorReadyRows,
|
|
358
|
+
assertionReady,
|
|
359
|
+
route: "presentation-consumer-ready",
|
|
360
|
+
reason:
|
|
361
|
+
"L05 published frame-pixel rows with compositorReady provenance; L11 may consume this bounded subset without reconstructing geometry.",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
axis,
|
|
366
|
+
compositorReadyRows,
|
|
367
|
+
assertionReady,
|
|
368
|
+
route: "lower-layer-fact-required",
|
|
369
|
+
reason:
|
|
370
|
+
"L11 must wait for L05 frame-pixel rows marked compositorReady before treating this axis as a presentation assertion.",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function numberValue(value: unknown): number {
|
|
375
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
376
|
+
}
|
|
@@ -89,6 +89,15 @@ export interface PageBreakDecorationInput {
|
|
|
89
89
|
* heuristic.
|
|
90
90
|
*/
|
|
91
91
|
blockIndexRangeByPageIndex?: ReadonlyMap<number, { first: number; last: number }>;
|
|
92
|
+
/**
|
|
93
|
+
* Page indexes whose boundary/content widgets would be inserted at a document
|
|
94
|
+
* position inside a table. PM widgets inherit that table-cell containing
|
|
95
|
+
* block, so visible chrome there paints page numbers inside the cell and
|
|
96
|
+
* block-level invisible anchors can perturb table layout. Keep the diagnostic
|
|
97
|
+
* markers but suppress the visible spacer/seam and shrink anchors to inline
|
|
98
|
+
* zero-size markers.
|
|
99
|
+
*/
|
|
100
|
+
suppressChromeForPageIndex?: ReadonlySet<number>;
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export function buildPageBreakDecorations(
|
|
@@ -111,7 +120,13 @@ export function buildPageBreakDecorations(
|
|
|
111
120
|
// (coord-11 §19) are emitted in a second pass at the end of this
|
|
112
121
|
// function.
|
|
113
122
|
if (graph.pages.length < 2) {
|
|
114
|
-
return buildPageAnchorDecorationsInto(
|
|
123
|
+
return buildPageAnchorDecorationsInto(
|
|
124
|
+
decorations,
|
|
125
|
+
graph,
|
|
126
|
+
posture,
|
|
127
|
+
runtimeToPmOffset,
|
|
128
|
+
input.suppressChromeForPageIndex,
|
|
129
|
+
);
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
for (let i = 1; i < graph.pages.length; i += 1) {
|
|
@@ -133,6 +148,8 @@ export function buildPageBreakDecorations(
|
|
|
133
148
|
input.headerPreviewByPageId?.get(next.pageId) ?? "";
|
|
134
149
|
|
|
135
150
|
const nextBlockRange = input.blockIndexRangeByPageIndex?.get(next.pageIndex);
|
|
151
|
+
const suppressVisibleChrome =
|
|
152
|
+
input.suppressChromeForPageIndex?.has(next.pageIndex) === true;
|
|
136
153
|
|
|
137
154
|
decorations.push(
|
|
138
155
|
Decoration.widget(
|
|
@@ -155,6 +172,7 @@ export function buildPageBreakDecorations(
|
|
|
155
172
|
nextHeaderPreview,
|
|
156
173
|
nextPageFirstBlockIndex: nextBlockRange?.first ?? -1,
|
|
157
174
|
nextPageLastBlockIndex: nextBlockRange?.last ?? -1,
|
|
175
|
+
suppressVisibleChrome,
|
|
158
176
|
}),
|
|
159
177
|
{
|
|
160
178
|
side: -1,
|
|
@@ -171,7 +189,13 @@ export function buildPageBreakDecorations(
|
|
|
171
189
|
),
|
|
172
190
|
);
|
|
173
191
|
}
|
|
174
|
-
return buildPageAnchorDecorationsInto(
|
|
192
|
+
return buildPageAnchorDecorationsInto(
|
|
193
|
+
decorations,
|
|
194
|
+
graph,
|
|
195
|
+
posture,
|
|
196
|
+
runtimeToPmOffset,
|
|
197
|
+
input.suppressChromeForPageIndex,
|
|
198
|
+
);
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
/**
|
|
@@ -197,6 +221,7 @@ function buildPageAnchorDecorationsInto(
|
|
|
197
221
|
graph: RuntimePageGraph,
|
|
198
222
|
posture: "page" | "canvas",
|
|
199
223
|
runtimeToPmOffset: ((runtimeOffset: number) => number | null) | undefined,
|
|
224
|
+
tableInteriorPageIndex?: ReadonlySet<number>,
|
|
200
225
|
): Decoration[] {
|
|
201
226
|
let contentPageOrdinal = 0;
|
|
202
227
|
for (const page of graph.pages) {
|
|
@@ -214,6 +239,7 @@ function buildPageAnchorDecorationsInto(
|
|
|
214
239
|
() => buildPageAnchorWidgetDom({
|
|
215
240
|
pageNumber: anchorPageNumber,
|
|
216
241
|
pageId: anchorPageId,
|
|
242
|
+
tableInterior: tableInteriorPageIndex?.has(page.pageIndex) === true,
|
|
217
243
|
}),
|
|
218
244
|
{
|
|
219
245
|
side: 1,
|
|
@@ -268,6 +294,7 @@ interface ChromeWidgetInput {
|
|
|
268
294
|
* -1 when block-index info was not supplied to the decoration builder.
|
|
269
295
|
*/
|
|
270
296
|
nextPageLastBlockIndex: number;
|
|
297
|
+
suppressVisibleChrome?: boolean;
|
|
271
298
|
}
|
|
272
299
|
|
|
273
300
|
// P14.c — cache the widget DOM by input identity. PM rebuilds the
|
|
@@ -299,6 +326,7 @@ function widgetCacheKey(input: ChromeWidgetInput): string {
|
|
|
299
326
|
input.nextHeaderPreview,
|
|
300
327
|
input.nextPageFirstBlockIndex,
|
|
301
328
|
input.nextPageLastBlockIndex,
|
|
329
|
+
input.suppressVisibleChrome ? "1" : "0",
|
|
302
330
|
].join("\x1f");
|
|
303
331
|
}
|
|
304
332
|
|
|
@@ -337,22 +365,29 @@ export function __resetPageBreakWidgetCache(): void {
|
|
|
337
365
|
*
|
|
338
366
|
* Emitted as a PM widget via `buildPageBreakDecorations` so it lives
|
|
339
367
|
* on the content layer (present under chrome=none) rather than on an
|
|
340
|
-
* absolute-positioned chrome overlay.
|
|
368
|
+
* absolute-positioned chrome overlay. When the page starts inside a table,
|
|
369
|
+
* the marker stays inline and zero-width so it does not create a block box
|
|
370
|
+
* inside the table cell.
|
|
341
371
|
*/
|
|
342
372
|
function buildPageAnchorWidgetDom(input: {
|
|
343
373
|
pageNumber: number;
|
|
344
374
|
pageId: string;
|
|
375
|
+
tableInterior?: boolean;
|
|
345
376
|
}): HTMLElement {
|
|
346
377
|
const root = document.createElement("span");
|
|
347
378
|
root.setAttribute("data-kind", "page-content-anchor");
|
|
348
379
|
root.setAttribute("data-page-content-wrapper", "");
|
|
349
380
|
root.setAttribute("data-page-number", String(input.pageNumber));
|
|
350
381
|
root.setAttribute("data-page-id", input.pageId);
|
|
382
|
+
if (input.tableInterior) {
|
|
383
|
+
root.setAttribute("data-page-anchor-suppressed", "table-interior");
|
|
384
|
+
}
|
|
351
385
|
root.setAttribute("aria-hidden", "true");
|
|
352
386
|
root.contentEditable = "false";
|
|
353
|
-
root.style.display = "block";
|
|
387
|
+
root.style.display = input.tableInterior ? "inline-block" : "block";
|
|
354
388
|
root.style.height = "0";
|
|
355
|
-
root.style.width = "100%";
|
|
389
|
+
root.style.width = input.tableInterior ? "0" : "100%";
|
|
390
|
+
root.style.overflow = "hidden";
|
|
356
391
|
root.style.userSelect = "none";
|
|
357
392
|
return root;
|
|
358
393
|
}
|
|
@@ -391,6 +426,16 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
|
|
|
391
426
|
root.style.width = "100%";
|
|
392
427
|
root.style.userSelect = "none";
|
|
393
428
|
|
|
429
|
+
if (input.suppressVisibleChrome) {
|
|
430
|
+
root.setAttribute("data-page-chrome-suppressed", "table-interior");
|
|
431
|
+
root.setAttribute("aria-hidden", "true");
|
|
432
|
+
root.style.height = "0";
|
|
433
|
+
root.style.width = "0";
|
|
434
|
+
root.style.overflow = "hidden";
|
|
435
|
+
root.style.pointerEvents = "none";
|
|
436
|
+
return root;
|
|
437
|
+
}
|
|
438
|
+
|
|
394
439
|
if (input.posture === "canvas") {
|
|
395
440
|
// Single dotted horizontal line with an unframed page-number label.
|
|
396
441
|
root.style.height = `${input.interGapPx + 1}px`;
|
|
@@ -151,6 +151,7 @@ function buildPageBreakDecorationsFromProps(
|
|
|
151
151
|
pageIndex: p.page.pageIndex,
|
|
152
152
|
startOffset: p.page.startOffset,
|
|
153
153
|
isBlankFiller: p.page.isBlankFiller,
|
|
154
|
+
frame: p.page.frame,
|
|
154
155
|
stories: {
|
|
155
156
|
displayPageNumber: p.page.stories.displayPageNumber,
|
|
156
157
|
header: p.page.stories.header,
|
|
@@ -184,6 +185,7 @@ function buildPageBreakDecorationsFromProps(
|
|
|
184
185
|
// carry `data-page-first-block-index` / `data-page-last-block-index`
|
|
185
186
|
// attributes needed by `useVisibleBlockRange`.
|
|
186
187
|
let blockIndexRangeByPageIndex: Map<number, { first: number; last: number }> | undefined;
|
|
188
|
+
let suppressChromeForPageIndex: Set<number> | undefined;
|
|
187
189
|
if (surfaceBlocks && surfaceBlocks.length > 0 && frame.pages.length > 0) {
|
|
188
190
|
blockIndexRangeByPageIndex = new Map();
|
|
189
191
|
for (let pi = 0; pi < frame.pages.length; pi++) {
|
|
@@ -193,6 +195,13 @@ function buildPageBreakDecorationsFromProps(
|
|
|
193
195
|
if (range) {
|
|
194
196
|
blockIndexRangeByPageIndex.set(page.page.pageIndex, range);
|
|
195
197
|
}
|
|
198
|
+
if (
|
|
199
|
+
pi > 0 &&
|
|
200
|
+
isRuntimeOffsetInsideTableBlock(surfaceBlocks, page.page.startOffset)
|
|
201
|
+
) {
|
|
202
|
+
if (!suppressChromeForPageIndex) suppressChromeForPageIndex = new Set();
|
|
203
|
+
suppressChromeForPageIndex.add(page.page.pageIndex);
|
|
204
|
+
}
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
|
|
@@ -206,9 +215,27 @@ function buildPageBreakDecorationsFromProps(
|
|
|
206
215
|
headerPreviewByPageId: previews?.headerPreviewByPageId,
|
|
207
216
|
footerPreviewByPageId: previews?.footerPreviewByPageId,
|
|
208
217
|
blockIndexRangeByPageIndex,
|
|
218
|
+
suppressChromeForPageIndex,
|
|
209
219
|
});
|
|
210
220
|
}
|
|
211
221
|
|
|
222
|
+
function isRuntimeOffsetInsideTableBlock(
|
|
223
|
+
blocks: readonly import("../../api/public-types.ts").SurfaceBlockSnapshot[],
|
|
224
|
+
offset: number,
|
|
225
|
+
): boolean {
|
|
226
|
+
for (const block of blocks) {
|
|
227
|
+
if (offset <= block.from || offset >= block.to) continue;
|
|
228
|
+
if (block.kind === "table") return true;
|
|
229
|
+
if (
|
|
230
|
+
block.kind === "sdt_block" &&
|
|
231
|
+
isRuntimeOffsetInsideTableBlock(block.children, offset)
|
|
232
|
+
) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
212
239
|
function extractDecorations(
|
|
213
240
|
set: DecorationSet,
|
|
214
241
|
_doc: unknown,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import React from "react";
|
|
17
17
|
import type {
|
|
18
18
|
EditorStoryTarget,
|
|
19
|
+
GeometryFacet,
|
|
19
20
|
PublicPageNode,
|
|
20
21
|
SurfaceTableRowSnapshot,
|
|
21
22
|
WordReviewEditorLayoutFacet,
|
|
@@ -23,6 +24,10 @@ import type {
|
|
|
23
24
|
import { buildPageAnchorAttributes } from "../../api/v3/_page-anchor-id.ts";
|
|
24
25
|
import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
25
26
|
import { TwTableContinuationHeader } from "../chrome-overlay/tw-table-continuation-header.tsx";
|
|
27
|
+
import {
|
|
28
|
+
TwTableSplitRowCarryOverlay,
|
|
29
|
+
type TableSplitRowCarryOverlayEntry,
|
|
30
|
+
} from "../chrome-overlay/tw-table-split-row-carry-overlay.tsx";
|
|
26
31
|
import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
|
|
27
32
|
import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
|
|
28
33
|
import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
|
|
@@ -35,6 +40,7 @@ export interface TwPageChromeEntryProps {
|
|
|
35
40
|
pageIndex: number;
|
|
36
41
|
page: PublicPageNode;
|
|
37
42
|
facet: WordReviewEditorLayoutFacet;
|
|
43
|
+
geometryFacet?: GeometryFacet;
|
|
38
44
|
activeStory: EditorStoryTarget;
|
|
39
45
|
activeStoryPageIndex?: number | null;
|
|
40
46
|
onOpenStory?: (target: EditorStoryTarget, pageIndex: number) => void;
|
|
@@ -60,6 +66,7 @@ function TwPageChromeEntryInner({
|
|
|
60
66
|
pageIndex,
|
|
61
67
|
page,
|
|
62
68
|
facet,
|
|
69
|
+
geometryFacet,
|
|
63
70
|
activeStory,
|
|
64
71
|
activeStoryPageIndex,
|
|
65
72
|
onOpenStory,
|
|
@@ -134,6 +141,16 @@ function TwPageChromeEntryInner({
|
|
|
134
141
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
142
|
}, [facet, pageIndex, page, renderFrameRevision]);
|
|
136
143
|
|
|
144
|
+
const splitRowCarryEntries = React.useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
collectSplitRowCarryOverlayEntries({
|
|
147
|
+
geometryFacet,
|
|
148
|
+
pageIndex,
|
|
149
|
+
pageTopPx: rect.topPx,
|
|
150
|
+
}),
|
|
151
|
+
[geometryFacet, pageIndex, rect.topPx, renderFrameRevision],
|
|
152
|
+
);
|
|
153
|
+
|
|
137
154
|
const handleHeaderDoubleClick = React.useCallback(
|
|
138
155
|
() => headerStory && onOpenStory?.(headerStory, pageIndex),
|
|
139
156
|
[onOpenStory, headerStory, pageIndex],
|
|
@@ -257,6 +274,7 @@ function TwPageChromeEntryInner({
|
|
|
257
274
|
bodyOriginTopPx={px(layout.marginTop)}
|
|
258
275
|
/>
|
|
259
276
|
))}
|
|
277
|
+
<TwTableSplitRowCarryOverlay entries={splitRowCarryEntries} />
|
|
260
278
|
</div>
|
|
261
279
|
);
|
|
262
280
|
}
|
|
@@ -269,6 +287,7 @@ function propsAreEqual(
|
|
|
269
287
|
prev.pageIndex === next.pageIndex &&
|
|
270
288
|
prev.page === next.page &&
|
|
271
289
|
prev.facet === next.facet &&
|
|
290
|
+
prev.geometryFacet === next.geometryFacet &&
|
|
272
291
|
prev.activeStory === next.activeStory &&
|
|
273
292
|
prev.activeStoryPageIndex === next.activeStoryPageIndex &&
|
|
274
293
|
prev.onOpenStory === next.onOpenStory &&
|
|
@@ -285,6 +304,49 @@ function propsAreEqual(
|
|
|
285
304
|
|
|
286
305
|
export const TwPageChromeEntry = React.memo(TwPageChromeEntryInner, propsAreEqual);
|
|
287
306
|
|
|
307
|
+
function collectSplitRowCarryOverlayEntries(input: {
|
|
308
|
+
geometryFacet?: GeometryFacet;
|
|
309
|
+
pageIndex: number;
|
|
310
|
+
pageTopPx: number;
|
|
311
|
+
}): readonly TableSplitRowCarryOverlayEntry[] {
|
|
312
|
+
const index = input.geometryFacet?.getGeometryIndex();
|
|
313
|
+
if (!index) return [];
|
|
314
|
+
|
|
315
|
+
const entries: TableSplitRowCarryOverlayEntry[] = [];
|
|
316
|
+
const seen = new Set<string>();
|
|
317
|
+
for (const entry of index.semanticEntries) {
|
|
318
|
+
if (entry.kind !== "table-row") continue;
|
|
319
|
+
if (entry.pageIndex !== input.pageIndex) continue;
|
|
320
|
+
if (entry.rect.space !== "frame") continue;
|
|
321
|
+
const carries = entry.tableContinuation?.splitRowCarry ?? [];
|
|
322
|
+
if (carries.length === 0) continue;
|
|
323
|
+
const blockId = entry.blockId;
|
|
324
|
+
if (!blockId) continue;
|
|
325
|
+
for (const carry of carries) {
|
|
326
|
+
const rowIndex = entry.rowIndex ?? carry.rowIndex;
|
|
327
|
+
if (rowIndex === undefined) continue;
|
|
328
|
+
const topPx = Math.max(0, entry.rect.topPx - input.pageTopPx);
|
|
329
|
+
const leftPx = Math.max(0, entry.rect.leftPx);
|
|
330
|
+
const widthPx = Math.max(1, entry.rect.widthPx);
|
|
331
|
+
const heightPx = Math.max(1, entry.rect.heightPx);
|
|
332
|
+
const key = `${blockId}:${rowIndex}:${topPx}:${leftPx}:${widthPx}:${heightPx}`;
|
|
333
|
+
if (seen.has(key)) continue;
|
|
334
|
+
seen.add(key);
|
|
335
|
+
entries.push({
|
|
336
|
+
blockId,
|
|
337
|
+
rowIndex,
|
|
338
|
+
topPx,
|
|
339
|
+
leftPx,
|
|
340
|
+
widthPx,
|
|
341
|
+
heightPx,
|
|
342
|
+
continuesFromPreviousPage: carry.continuesFromPreviousPage,
|
|
343
|
+
continuesToNextPage: carry.continuesToNextPage,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return entries;
|
|
348
|
+
}
|
|
349
|
+
|
|
288
350
|
function isActiveStoryMatch(
|
|
289
351
|
active: EditorStoryTarget,
|
|
290
352
|
candidate: EditorStoryTarget,
|
|
@@ -503,6 +503,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
503
503
|
pageIndex={rect.pageIndex}
|
|
504
504
|
page={page}
|
|
505
505
|
facet={facet}
|
|
506
|
+
geometryFacet={geometryFacet}
|
|
506
507
|
activeStory={activeStory}
|
|
507
508
|
activeStoryPageIndex={activeStoryPageIndex}
|
|
508
509
|
onOpenStory={handleOpenStoryForPage}
|
package/src/README.md
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# Source Layout
|
|
2
|
-
|
|
3
|
-
The landed source tree is still organized around the active docx implementation, not around the future target office-wide layout.
|
|
4
|
-
|
|
5
|
-
## Current Landed Layout
|
|
6
|
-
|
|
7
|
-
```text
|
|
8
|
-
src/
|
|
9
|
-
api/
|
|
10
|
-
model/
|
|
11
|
-
core/
|
|
12
|
-
schema/
|
|
13
|
-
state/
|
|
14
|
-
commands/
|
|
15
|
-
selection/
|
|
16
|
-
review/
|
|
17
|
-
store/
|
|
18
|
-
io/
|
|
19
|
-
opc/
|
|
20
|
-
ooxml/
|
|
21
|
-
normalize/
|
|
22
|
-
export/
|
|
23
|
-
preservation/
|
|
24
|
-
validation/
|
|
25
|
-
runtime/
|
|
26
|
-
ui/
|
|
27
|
-
headless/ # Shared pure logic (framework-free)
|
|
28
|
-
shared/ # Shared utilities (revision-filters)
|
|
29
|
-
WordReviewEditor.tsx # Entry point
|
|
30
|
-
ui-tailwind/ # Default rendering (Tailwind + Radix + Lucide)
|
|
31
|
-
editor-surface/
|
|
32
|
-
toolbar/
|
|
33
|
-
review/
|
|
34
|
-
status/
|
|
35
|
-
chrome/
|
|
36
|
-
theme/
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
This is the truthful current layout for implementation work today.
|
|
40
|
-
|
|
41
|
-
## Target Broader Layout
|
|
42
|
-
|
|
43
|
-
The broader repo story now targets a future layout like this:
|
|
44
|
-
|
|
45
|
-
```text
|
|
46
|
-
src/
|
|
47
|
-
platform/
|
|
48
|
-
formats/
|
|
49
|
-
docx/
|
|
50
|
-
xlsx/
|
|
51
|
-
pdf/
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
That layout is not landed yet. Use it as a planning direction, not as evidence that the current code has already been reorganized.
|
|
55
|
-
|
|
56
|
-
## UI Layer Strategy
|
|
57
|
-
|
|
58
|
-
- `ui/headless/` — Pure logic: keyboard handling, decoration models, selection utilities. No React dependencies.
|
|
59
|
-
- `ui-tailwind/` — Default rendering: Tailwind CSS, Radix UI primitives, Lucide icons. All styling via CSS custom properties.
|
|
60
|
-
- `ui/WordReviewEditor.tsx` — Public entry point that bridges the runtime to the Tailwind layer.
|
|
61
|
-
|
|
62
|
-
Legacy inline-CSSProperties components have been removed. See `docs/reference/word-review-editor-frontend-architecture.md` for the canonical frontend architecture.
|
|
63
|
-
|
|
64
|
-
Keep business rules close to the subsystem that owns them. Do not centralize unrelated logic into generic utility layers.
|
|
65
|
-
|
|
66
|
-
Ownership rules:
|
|
67
|
-
|
|
68
|
-
- `api` exposes public contracts only.
|
|
69
|
-
- `model`, `core`, `review`, `io`, `preservation`, and `validation` should remain React-free when possible.
|
|
70
|
-
- `runtime` is the only mutation boundary the UI calls into.
|
|
71
|
-
- `ui` consumes runtime contracts and design tokens; it does not own canonical document truth.
|
|
72
|
-
|
|
73
|
-
## Broader Repo Direction
|
|
74
|
-
|
|
75
|
-
As the repo broadens:
|
|
76
|
-
|
|
77
|
-
- shared package and preservation concerns should move toward `src/platform/`
|
|
78
|
-
- current docx runtime code remains the active implementation track until a deliberate source move lands
|
|
79
|
-
- future xlsx work should gain its own source area rather than widening docx-specific modules by implication
|
|
80
|
-
- future pdf work should remain separate from the first OOXML platform layer unless architecture decisions intentionally broaden it
|
|
81
|
-
|
|
82
|
-
Wave 0 inventory references:
|
|
83
|
-
|
|
84
|
-
- `component-inventory.md`
|
|
85
|
-
Wave-owned inventory for the major editor subsystems, their boundaries, and the promotion target for this scaffold phase.
|
package/src/api/README.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# API
|
|
2
|
-
|
|
3
|
-
Public TypeScript contracts for `WordReviewEditor` belong here.
|
|
4
|
-
|
|
5
|
-
This layer should expose:
|
|
6
|
-
|
|
7
|
-
- component props and ref types
|
|
8
|
-
- session-state and snapshot compatibility types
|
|
9
|
-
- host adapter and datastore adapter interfaces
|
|
10
|
-
- runtime-derived position and selection projection types used by the public API
|
|
11
|
-
- discriminated event unions
|
|
12
|
-
- warning and error payloads
|
|
13
|
-
- persisted snapshot contracts
|
|
14
|
-
- export and compatibility report types
|
|
15
|
-
|
|
16
|
-
Do not place runtime logic here.
|
|
17
|
-
|
|
18
|
-
Frozen naming and boundary rules:
|
|
19
|
-
|
|
20
|
-
- the shipped component name is `WordReviewEditor`
|
|
21
|
-
- `DocumentRuntime` is an internal runtime contract, not a public React component name
|
|
22
|
-
- `EditorSessionState` is the canonical host-facing live-session contract
|
|
23
|
-
- `PersistedEditorSnapshot` is the legacy/store compatibility envelope
|
|
24
|
-
- `EditorHostAdapter` is the preferred persistence boundary; `EditorDatastoreAdapter` remains the snapshot-compatible bridge
|
|
25
|
-
- public range references are canonical-position projections, not DOM-path handles
|
|
26
|
-
- render snapshots stay in `src/runtime`; `src/api` only exposes persisted host-facing snapshot types
|