@beyondwork/docx-react-component 1.0.71 → 1.0.73
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 +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +280 -1
- package/src/api/v3/_create.ts +16 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/evaluate.ts +113 -0
- package/src/api/v3/ai/outline.ts +140 -0
- package/src/api/v3/ai/policy.ts +31 -0
- package/src/api/v3/ai/replacement.ts +8 -0
- package/src/api/v3/ai/review.ts +342 -0
- package/src/api/v3/ai/stats.ts +62 -0
- package/src/api/v3/runtime/viewport.ts +181 -0
- package/src/api/v3/runtime/workflow.ts +114 -1
- package/src/api/v3/ui/_types.ts +35 -0
- package/src/api/v3/ui/chrome-preset-model.ts +6 -0
- package/src/api/v3/ui/index.ts +1 -0
- package/src/api/v3/ui/viewport.ts +112 -0
- package/src/compare/diff-engine.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/table-structure-commands.ts +1 -0
- package/src/core/state/editor-state.ts +49 -6
- package/src/io/export/serialize-footnotes.ts +6 -0
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +20 -0
- package/src/io/export/serialize-paragraph-formatting.ts +34 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/normalize/normalize-text.ts +49 -2
- package/src/io/ooxml/parse-headers-footers.ts +31 -0
- package/src/io/ooxml/parse-main-document.ts +148 -7
- package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
- package/src/model/canonical-document.ts +401 -1
- package/src/runtime/formatting/formatting-context.ts +2 -1
- package/src/runtime/geometry/overlay-rects.ts +7 -10
- package/src/runtime/layout/layout-engine-version.ts +278 -1
- package/src/runtime/layout/paginated-layout-engine.ts +181 -8
- package/src/runtime/layout/resolved-formatting-state.ts +108 -13
- package/src/runtime/markdown-sanitizer.ts +21 -4
- package/src/runtime/render/render-kernel.ts +21 -1
- package/src/runtime/scopes/action-validation.ts +30 -4
- package/src/runtime/scopes/audit-bundle.ts +8 -0
- package/src/runtime/scopes/compiler-service.ts +1 -0
- package/src/runtime/scopes/enumerate-scopes.ts +61 -3
- package/src/runtime/scopes/replacement/apply.ts +50 -3
- package/src/runtime/scopes/scope-kinds/paragraph.ts +170 -7
- package/src/runtime/scopes/semantic-scope-types.ts +27 -0
- package/src/runtime/surface-projection.ts +77 -0
- package/src/runtime/workflow/coordinator.ts +3 -0
- package/src/runtime/workflow/scope-writer.ts +34 -0
- package/src/session/export/embedded-reconstitute.ts +37 -3
- package/src/session/import/embedded-offload.ts +26 -1
- package/src/session/import/loader-types.ts +18 -0
- package/src/session/import/loader.ts +2 -0
- package/src/shell/media-previews.ts +8 -6
- package/src/ui/WordReviewEditor.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +11 -0
- package/src/ui/headless/selection-helpers.ts +2 -2
- package/src/ui/runtime-shortcut-dispatch.ts +4 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +50 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +6 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
- package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +114 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +12 -4
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
- package/src/ui-tailwind/index.ts +4 -2
- package/src/ui-tailwind/page-chrome-model.ts +5 -7
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +54 -34
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +8 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +11 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +139 -10
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
- package/src/ui-tailwind/theme/editor-theme.css +15 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +22 -14
|
@@ -6,10 +6,28 @@ import React, {
|
|
|
6
6
|
} from "react";
|
|
7
7
|
|
|
8
8
|
import type { WordReviewEditorRef } from "../../api/public-types";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Opaque runtime handle for the REPL. The REPL hands the value to
|
|
12
|
+
* `new Function("runtime", ...)` for user-supplied JS evaluation and
|
|
13
|
+
* does not introspect its shape — so the dialog's prop type is
|
|
14
|
+
* deliberately structurally loose (`object | null`) rather than the
|
|
15
|
+
* non-public `DocumentRuntime`.
|
|
16
|
+
*
|
|
17
|
+
* Callers inside the component tree that actually hold a
|
|
18
|
+
* `DocumentRuntime` / `RuntimeApiHandle` / `ApiV3` can pass it here
|
|
19
|
+
* unchanged; the widening only affects compile-time autocomplete for
|
|
20
|
+
* the REPL's internal use, not the live eval target.
|
|
21
|
+
*
|
|
22
|
+
* Retired handoff §4.8 residual: the previous `DocumentRuntime`
|
|
23
|
+
* prop type was the last direct import of the non-public runtime
|
|
24
|
+
* class name in `src/ui-tailwind/**` per refactor/11 handoff §4.8
|
|
25
|
+
* remainder row.
|
|
26
|
+
*/
|
|
27
|
+
export type TwRuntimeReplTarget = object;
|
|
10
28
|
|
|
11
29
|
export interface TwRuntimeReplDialogProps {
|
|
12
|
-
runtime:
|
|
30
|
+
runtime: TwRuntimeReplTarget | null;
|
|
13
31
|
/**
|
|
14
32
|
* Optional editor ref. When provided, the REPL exposes it to evaluated
|
|
15
33
|
* expressions as `ref` — e.g. `ref.getRenderSnapshot()`. The REPL reads
|
|
@@ -370,11 +388,11 @@ export function isReplToggleShortcut(event: KeyboardEvent): boolean {
|
|
|
370
388
|
|
|
371
389
|
export async function evaluateReplExpression(
|
|
372
390
|
code: string,
|
|
373
|
-
runtime:
|
|
391
|
+
runtime: TwRuntimeReplTarget,
|
|
374
392
|
ref: WordReviewEditorRef | null = null,
|
|
375
393
|
): Promise<unknown> {
|
|
376
394
|
type ReplFn = (
|
|
377
|
-
runtime:
|
|
395
|
+
runtime: TwRuntimeReplTarget,
|
|
378
396
|
ref: WordReviewEditorRef | null,
|
|
379
397
|
) => Promise<unknown>;
|
|
380
398
|
let fn: ReplFn | null = null;
|
|
@@ -214,17 +214,17 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
214
214
|
capability={tableContext?.operations.setTableAlignment}
|
|
215
215
|
disabled={props.disabled}
|
|
216
216
|
onClick={() => props.onSetTableAlignment?.(align)}
|
|
217
|
-
//
|
|
218
|
-
// `
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
active={
|
|
217
|
+
// Refactor/11 handoff §4.13 — L07 shipped
|
|
218
|
+
// `TableStructureContextSnapshot.alignment` in
|
|
219
|
+
// `4cfe52a3` (2026-04-24), landing alignment on the
|
|
220
|
+
// structure context alongside the existing
|
|
221
|
+
// `PublicTableSummary.alignment`. Toggle the active
|
|
222
|
+
// state against the live value. `null` means no
|
|
223
|
+
// explicit alignment declared (parent default, typically
|
|
224
|
+
// left) — all three buttons remain inactive in that
|
|
225
|
+
// case; setting any alignment via the callback produces
|
|
226
|
+
// a subsequent snapshot with a non-null value.
|
|
227
|
+
active={tableContext?.alignment === align}
|
|
228
228
|
>
|
|
229
229
|
{align[0]!.toUpperCase()}
|
|
230
230
|
</ToolbarButton>
|
|
@@ -28,9 +28,9 @@ import type {
|
|
|
28
28
|
WordReviewEditorLayoutFacet,
|
|
29
29
|
} from "../../api/public-types";
|
|
30
30
|
import type { GeometryFacet } from "../../api/public-types";
|
|
31
|
+
import { DEFAULT_PX_PER_TWIP } from "../../api/public-types";
|
|
31
32
|
import type { OverlayCoordinateSpace } from "../chrome-overlay/chrome-overlay-projector";
|
|
32
33
|
import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector";
|
|
33
|
-
import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
|
|
34
34
|
import { forwardNonDragClick } from "./forward-non-drag-click";
|
|
35
35
|
|
|
36
36
|
const GRIP_PX = 2;
|
|
@@ -183,6 +183,9 @@ export interface TwChromeOverlayProps {
|
|
|
183
183
|
* See `useVisiblePageIndexRange` in `src/ui-tailwind/page-stack/use-visible-block-range.ts`.
|
|
184
184
|
*/
|
|
185
185
|
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
186
|
+
/** Preview catalog threaded into the page-stack chrome so header /
|
|
187
|
+
* footer / footnote / endnote regions render real <img>s. */
|
|
188
|
+
mediaPreviews?: Record<string, import("../editor-surface/pm-state-from-snapshot").MediaPreviewDescriptor>;
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
/**
|
|
@@ -226,6 +229,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
226
229
|
pmSurfaceElement,
|
|
227
230
|
pmView,
|
|
228
231
|
visiblePageIndexRange,
|
|
232
|
+
mediaPreviews,
|
|
229
233
|
}) => {
|
|
230
234
|
return (
|
|
231
235
|
<div
|
|
@@ -243,6 +247,7 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
243
247
|
pmSurfaceElement={pmSurfaceElement}
|
|
244
248
|
pmView={pmView}
|
|
245
249
|
visiblePageIndexRange={visiblePageIndexRange ?? null}
|
|
250
|
+
mediaPreviews={mediaPreviews}
|
|
246
251
|
/>
|
|
247
252
|
) : null}
|
|
248
253
|
<TwScopeRailLayer
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { RenderBlockDecoration } from "../../api/public-types.ts";
|
|
3
3
|
import { TwCommentPreview } from "../chrome/tw-comment-preview";
|
|
4
|
+
import {
|
|
5
|
+
projectRectToOverlay,
|
|
6
|
+
type OverlayCoordinateSpace,
|
|
7
|
+
} from "./chrome-overlay-projector.ts";
|
|
4
8
|
|
|
5
9
|
const WIDE_BREAKPOINT_PX = 1024;
|
|
6
10
|
const CONNECTOR_GAP_PX = 16;
|
|
@@ -24,14 +28,26 @@ export interface TwCommentBalloonLayerProps {
|
|
|
24
28
|
viewportWidthPx: number;
|
|
25
29
|
/** Right edge of the page frame in overlay coordinates (px). */
|
|
26
30
|
pageRightEdgePx: number;
|
|
31
|
+
/**
|
|
32
|
+
* Overlay coordinate space — subtracted from each decoration rect so
|
|
33
|
+
* balloon `top` is relative to the overlay's own top-left, not the
|
|
34
|
+
* document column. Matches `TwScopeCardLayer` / `TwTableGripLayer`
|
|
35
|
+
* precedent. Defaults to the zero-origin space so existing callers
|
|
36
|
+
* that mount the layer at the document column's origin continue to
|
|
37
|
+
* work unchanged.
|
|
38
|
+
*/
|
|
39
|
+
space?: OverlayCoordinateSpace;
|
|
27
40
|
onOpenThread?: (commentId: string) => void;
|
|
28
41
|
}
|
|
29
42
|
|
|
43
|
+
const ZERO_SPACE: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 };
|
|
44
|
+
|
|
30
45
|
export const TwCommentBalloonLayer = React.memo(function TwCommentBalloonLayer({
|
|
31
46
|
commentDecorations,
|
|
32
47
|
commentDataById,
|
|
33
48
|
viewportWidthPx,
|
|
34
49
|
pageRightEdgePx,
|
|
50
|
+
space = ZERO_SPACE,
|
|
35
51
|
onOpenThread,
|
|
36
52
|
}: TwCommentBalloonLayerProps) {
|
|
37
53
|
if (viewportWidthPx < WIDE_BREAKPOINT_PX) return null;
|
|
@@ -42,12 +58,13 @@ export const TwCommentBalloonLayer = React.memo(function TwCommentBalloonLayer({
|
|
|
42
58
|
{commentDecorations.map((dec) => {
|
|
43
59
|
const data = commentDataById.get(dec.refId);
|
|
44
60
|
if (!data) return null;
|
|
61
|
+
const projected = projectRectToOverlay(dec.frame, space);
|
|
45
62
|
return (
|
|
46
63
|
<div
|
|
47
64
|
key={dec.refId}
|
|
48
65
|
style={{
|
|
49
66
|
position: "absolute",
|
|
50
|
-
top:
|
|
67
|
+
top: projected.top,
|
|
51
68
|
left: pageRightEdgePx + CONNECTOR_GAP_PX,
|
|
52
69
|
width: BALLOON_MAX_WIDTH_PX,
|
|
53
70
|
pointerEvents: "auto",
|
|
@@ -176,6 +176,7 @@ export function resolvePageOverlayRects(
|
|
|
176
176
|
if (!scrollRoot || count <= 0) return [];
|
|
177
177
|
widgets = measureWidgetsViaOffsetChain(scrollRoot);
|
|
178
178
|
pageCount = count;
|
|
179
|
+
// geometry:allow-dom-fallback
|
|
179
180
|
scrollHeight = scrollRoot.clientHeight;
|
|
180
181
|
} else if (
|
|
181
182
|
input !== null &&
|
|
@@ -195,6 +196,7 @@ export function resolvePageOverlayRects(
|
|
|
195
196
|
if (legacyPageCount <= 0) return [];
|
|
196
197
|
widgets = measureWidgetsViaOffsetChain(scrollRoot);
|
|
197
198
|
pageCount = legacyPageCount;
|
|
199
|
+
// geometry:allow-dom-fallback
|
|
198
200
|
scrollHeight = scrollRoot.clientHeight;
|
|
199
201
|
} else {
|
|
200
202
|
return [];
|
|
@@ -275,6 +277,10 @@ export function measureWidgetsViaBoundingRect(
|
|
|
275
277
|
},
|
|
276
278
|
): PageBoundaryMeasurement[] {
|
|
277
279
|
if (!queryRoot || !originElement) return [];
|
|
280
|
+
// Cold-open / pre-paint DOM fallback — warm path flows through
|
|
281
|
+
// `geometryFacet.getPage(i)` at the caller; this branch fires only
|
|
282
|
+
// before the first render frame.
|
|
283
|
+
// geometry:allow-dom-fallback
|
|
278
284
|
const originRect = originElement.getBoundingClientRect();
|
|
279
285
|
const normalizedVisiblePageIndexRange = normalizeVisiblePageIndexRange(
|
|
280
286
|
options?.visiblePageIndexRange,
|
|
@@ -301,6 +307,7 @@ export function measureWidgetsViaBoundingRect(
|
|
|
301
307
|
const prevPageId = widget.getAttribute("data-page-frame-end");
|
|
302
308
|
const nextPageId = widget.getAttribute("data-page-frame-start");
|
|
303
309
|
if (!prevPageId || !nextPageId) continue;
|
|
310
|
+
// geometry:allow-dom-fallback
|
|
304
311
|
const rect = widget.getBoundingClientRect();
|
|
305
312
|
out.push({
|
|
306
313
|
prevPageId,
|
|
@@ -380,10 +387,14 @@ function resolveOffsetTop(
|
|
|
380
387
|
// still defined and defaults to 0. Browsers set it relative to the
|
|
381
388
|
// offsetParent. We walk up the offset chain until we reach the scroll
|
|
382
389
|
// root (or exit the document) so the result is scroll-root-relative.
|
|
390
|
+
// Cold-open / pre-paint DOM fallback — warm path flows through
|
|
391
|
+
// `geometryFacet.getPage(i)` at the caller.
|
|
383
392
|
let node: HTMLElement | null = widget;
|
|
384
393
|
let top = 0;
|
|
385
394
|
while (node) {
|
|
395
|
+
// geometry:allow-dom-fallback
|
|
386
396
|
top += node.offsetTop ?? 0;
|
|
397
|
+
// geometry:allow-dom-fallback
|
|
387
398
|
const parent = node.offsetParent as HTMLElement | null;
|
|
388
399
|
if (parent === scrollRoot || parent === null) break;
|
|
389
400
|
node = parent;
|
|
@@ -392,6 +403,7 @@ function resolveOffsetTop(
|
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
function resolveOffsetHeight(widget: HTMLElement): number {
|
|
406
|
+
// geometry:allow-dom-fallback
|
|
395
407
|
return widget.offsetHeight ?? 0;
|
|
396
408
|
}
|
|
397
409
|
|
|
@@ -437,12 +449,10 @@ export interface TwPageStackOverlayLayerProps {
|
|
|
437
449
|
* owns the projection math; the overlay component re-exports from this
|
|
438
450
|
* module for back-compat with existing imports.
|
|
439
451
|
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* a `geometryFacet` to this overlay — the DOM-measurement path remains
|
|
445
|
-
* the default.
|
|
452
|
+
* **Slice 3c reconciliation shipped 2026-04-23** — kernel `PAGE_GAP_PX`
|
|
453
|
+
* (16 → 48) now matches the DOM chrome's `interGapPx`, so `geometryFacet`
|
|
454
|
+
* is the production warm path. The DOM-measurement branch below stays as
|
|
455
|
+
* the cold-open fallback only.
|
|
446
456
|
*/
|
|
447
457
|
export const resolvePageOverlayRectsFromGeometry =
|
|
448
458
|
resolvePageOverlayRectsFromGeometryImpl;
|
|
@@ -628,17 +638,22 @@ export const TwPageStackOverlayLayer: React.FC<TwPageStackOverlayLayerProps> = (
|
|
|
628
638
|
}
|
|
629
639
|
}
|
|
630
640
|
|
|
641
|
+
// Cold-open / pre-paint DOM fallback — warm path early-returned
|
|
642
|
+
// above via `geometryFacet` or the UI-API resolver. Lines below
|
|
643
|
+
// fire only before the first render frame.
|
|
631
644
|
if (origin) {
|
|
632
645
|
const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
|
|
633
646
|
pageCount,
|
|
634
647
|
visiblePageIndexRange,
|
|
635
648
|
});
|
|
649
|
+
// geometry:allow-dom-fallback
|
|
636
650
|
const originRect = origin.getBoundingClientRect();
|
|
637
651
|
setRects(
|
|
638
652
|
resolvePageOverlayRects({
|
|
639
653
|
widgets,
|
|
640
654
|
pageCount,
|
|
641
655
|
scrollHeight:
|
|
656
|
+
// geometry:allow-dom-fallback
|
|
642
657
|
origin.clientHeight > 0 ? origin.clientHeight : originRect.height,
|
|
643
658
|
visiblePageIndexRange,
|
|
644
659
|
}),
|
|
@@ -652,6 +667,7 @@ export const TwPageStackOverlayLayer: React.FC<TwPageStackOverlayLayerProps> = (
|
|
|
652
667
|
resolvePageOverlayRects({
|
|
653
668
|
widgets,
|
|
654
669
|
pageCount,
|
|
670
|
+
// geometry:allow-dom-fallback
|
|
655
671
|
scrollHeight: scrollRoot.clientHeight,
|
|
656
672
|
visiblePageIndexRange,
|
|
657
673
|
}),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { RenderBlockDecoration } from "../../api/public-types.ts";
|
|
3
3
|
import { AUTHOR_PALETTE } from "../../ui/headless/revision-decoration-model";
|
|
4
|
+
import {
|
|
5
|
+
projectRectToOverlay,
|
|
6
|
+
type OverlayCoordinateSpace,
|
|
7
|
+
} from "./chrome-overlay-projector.ts";
|
|
4
8
|
|
|
5
9
|
const BAR_WIDTH_PX = 3;
|
|
6
10
|
const BAR_LEFT_OFFSET_PX = 8;
|
|
@@ -15,12 +19,24 @@ export interface TwRevisionMarginBarLayerProps {
|
|
|
15
19
|
authorPaletteIndexById: ReadonlyMap<string, number>;
|
|
16
20
|
/** Left edge of the page body in overlay coordinates (px). */
|
|
17
21
|
pageBodyLeftPx: number;
|
|
22
|
+
/**
|
|
23
|
+
* Overlay coordinate space — subtracted from each decoration rect so
|
|
24
|
+
* bar `top` is relative to the overlay's own top-left, not the
|
|
25
|
+
* document column. Matches `TwScopeCardLayer` / `TwTableGripLayer`
|
|
26
|
+
* precedent. Defaults to the zero-origin space so existing callers
|
|
27
|
+
* that mount the layer at the document column's origin continue to
|
|
28
|
+
* work unchanged.
|
|
29
|
+
*/
|
|
30
|
+
space?: OverlayCoordinateSpace;
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
const ZERO_SPACE: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 };
|
|
34
|
+
|
|
20
35
|
export const TwRevisionMarginBarLayer = React.memo(function TwRevisionMarginBarLayer({
|
|
21
36
|
revisionDecorations,
|
|
22
37
|
authorPaletteIndexById,
|
|
23
38
|
pageBodyLeftPx,
|
|
39
|
+
space = ZERO_SPACE,
|
|
24
40
|
}: TwRevisionMarginBarLayerProps) {
|
|
25
41
|
if (revisionDecorations.length === 0) return null;
|
|
26
42
|
|
|
@@ -29,13 +45,14 @@ export const TwRevisionMarginBarLayer = React.memo(function TwRevisionMarginBarL
|
|
|
29
45
|
{revisionDecorations.map((dec, idx) => {
|
|
30
46
|
const paletteIdx = authorPaletteIndexById.get(dec.refId) ?? 0;
|
|
31
47
|
const color = AUTHOR_PALETTE[paletteIdx % AUTHOR_PALETTE.length];
|
|
48
|
+
const projected = projectRectToOverlay(dec.frame, space);
|
|
32
49
|
return (
|
|
33
50
|
<div
|
|
34
51
|
key={`rev-bar-${dec.refId}-${idx}`}
|
|
35
52
|
aria-hidden
|
|
36
53
|
style={{
|
|
37
54
|
position: "absolute",
|
|
38
|
-
top:
|
|
55
|
+
top: projected.top,
|
|
39
56
|
left: pageBodyLeftPx - BAR_LEFT_OFFSET_PX - BAR_WIDTH_PX,
|
|
40
57
|
width: BAR_WIDTH_PX,
|
|
41
58
|
height: dec.frame.heightPx,
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { Decoration } from "prosemirror-view";
|
|
27
|
-
import
|
|
28
|
-
|
|
27
|
+
import {
|
|
28
|
+
type RuntimePageGraph,
|
|
29
|
+
resolvePageFieldDisplayText,
|
|
30
|
+
} from "../../api/public-types.ts";
|
|
29
31
|
|
|
30
32
|
export const PAGE_CHROME_DEFAULTS = {
|
|
31
33
|
headerBandPx: 32,
|
|
@@ -93,7 +95,7 @@ export function buildPageBreakDecorations(
|
|
|
93
95
|
input: PageBreakDecorationInput,
|
|
94
96
|
): Decoration[] {
|
|
95
97
|
const { graph, posture, runtimeToPmOffset } = input;
|
|
96
|
-
if (!graph
|
|
98
|
+
if (!graph) return [];
|
|
97
99
|
|
|
98
100
|
const headerBandPx =
|
|
99
101
|
input.headerBandPx ?? PAGE_CHROME_DEFAULTS.headerBandPx;
|
|
@@ -103,6 +105,15 @@ export function buildPageBreakDecorations(
|
|
|
103
105
|
|
|
104
106
|
const decorations: Decoration[] = [];
|
|
105
107
|
|
|
108
|
+
// Inter-page boundary widgets (existing) — emitted first so
|
|
109
|
+
// `decorations[0..N-2]` remain boundary widgets for any legacy
|
|
110
|
+
// callers that index by position. Per-page content-anchor widgets
|
|
111
|
+
// (coord-11 §19) are emitted in a second pass at the end of this
|
|
112
|
+
// function.
|
|
113
|
+
if (graph.pages.length < 2) {
|
|
114
|
+
return buildPageAnchorDecorationsInto(decorations, graph, posture, runtimeToPmOffset);
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
for (let i = 1; i < graph.pages.length; i += 1) {
|
|
107
118
|
const prev = graph.pages[i - 1]!;
|
|
108
119
|
const next = graph.pages[i]!;
|
|
@@ -160,6 +171,58 @@ export function buildPageBreakDecorations(
|
|
|
160
171
|
),
|
|
161
172
|
);
|
|
162
173
|
}
|
|
174
|
+
return buildPageAnchorDecorationsInto(decorations, graph, posture, runtimeToPmOffset);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Append per-page content-anchor widgets to `decorations` for coord-11
|
|
179
|
+
* §19. One zero-height anchor per non-filler page at the page's content-
|
|
180
|
+
* start offset. Chrome-preset-independent (PM widget = content layer) +
|
|
181
|
+
* markup-mode stable (document-offset-anchored, not chrome-dependent).
|
|
182
|
+
*
|
|
183
|
+
* Each anchor carries `data-page-content-wrapper` + `data-page-number`
|
|
184
|
+
* (1-based, skipping blank fillers) + `data-page-id` (from
|
|
185
|
+
* `RuntimePageNode.pageId` — stable across chrome/markup transitions).
|
|
186
|
+
* Consumed by `runtime.viewport.getPageAnchor(n)` (coord-07 §2.9) and
|
|
187
|
+
* `ui.viewport.scrollToPage(n)` (coord-10 §γ); also by the visual-
|
|
188
|
+
* fidelity harness at `test/visual-fidelity/` per coord-11 §20.
|
|
189
|
+
*
|
|
190
|
+
* Anchors use `side: 1` so they sit AFTER the document position
|
|
191
|
+
* (inside page N's content). Boundary widgets use `side: -1` so they
|
|
192
|
+
* sit BEFORE the same offset (at the end of page N-1's content + gap).
|
|
193
|
+
* Ordering places each anchor at the top of its page's content area.
|
|
194
|
+
*/
|
|
195
|
+
function buildPageAnchorDecorationsInto(
|
|
196
|
+
decorations: Decoration[],
|
|
197
|
+
graph: RuntimePageGraph,
|
|
198
|
+
posture: "page" | "canvas",
|
|
199
|
+
runtimeToPmOffset: ((runtimeOffset: number) => number | null) | undefined,
|
|
200
|
+
): Decoration[] {
|
|
201
|
+
let contentPageOrdinal = 0;
|
|
202
|
+
for (const page of graph.pages) {
|
|
203
|
+
if (page.isBlankFiller) continue;
|
|
204
|
+
contentPageOrdinal += 1;
|
|
205
|
+
const pagePmOffset = runtimeToPmOffset
|
|
206
|
+
? runtimeToPmOffset(page.startOffset)
|
|
207
|
+
: page.startOffset;
|
|
208
|
+
if (pagePmOffset === null || pagePmOffset === undefined) continue;
|
|
209
|
+
const anchorPageNumber = contentPageOrdinal;
|
|
210
|
+
const anchorPageId = page.pageId;
|
|
211
|
+
decorations.push(
|
|
212
|
+
Decoration.widget(
|
|
213
|
+
pagePmOffset,
|
|
214
|
+
() => buildPageAnchorWidgetDom({
|
|
215
|
+
pageNumber: anchorPageNumber,
|
|
216
|
+
pageId: anchorPageId,
|
|
217
|
+
}),
|
|
218
|
+
{
|
|
219
|
+
side: 1,
|
|
220
|
+
key: `pa-${page.pageId}-${posture}`,
|
|
221
|
+
ignoreSelection: true,
|
|
222
|
+
},
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
163
226
|
return decorations;
|
|
164
227
|
}
|
|
165
228
|
|
|
@@ -262,6 +325,38 @@ export function __resetPageBreakWidgetCache(): void {
|
|
|
262
325
|
widgetDomCache.clear();
|
|
263
326
|
}
|
|
264
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Build the per-page content-anchor widget DOM for coord-11 §19.
|
|
330
|
+
*
|
|
331
|
+
* Zero-height, non-visual marker carrying `data-page-content-wrapper`,
|
|
332
|
+
* `data-page-number` (1-based), and `data-page-id` (stable across
|
|
333
|
+
* chrome preset + markup mode transitions — sourced from
|
|
334
|
+
* `RuntimePageNode.pageId`). Consumed by `runtime.viewport.getPageAnchor(n)`
|
|
335
|
+
* (coord-07 §2.9) + `ui.viewport.scrollToPage(n)` (coord-10 §γ) and by
|
|
336
|
+
* the visual-fidelity harness.
|
|
337
|
+
*
|
|
338
|
+
* Emitted as a PM widget via `buildPageBreakDecorations` so it lives
|
|
339
|
+
* on the content layer (present under chrome=none) rather than on an
|
|
340
|
+
* absolute-positioned chrome overlay.
|
|
341
|
+
*/
|
|
342
|
+
function buildPageAnchorWidgetDom(input: {
|
|
343
|
+
pageNumber: number;
|
|
344
|
+
pageId: string;
|
|
345
|
+
}): HTMLElement {
|
|
346
|
+
const root = document.createElement("span");
|
|
347
|
+
root.setAttribute("data-kind", "page-content-anchor");
|
|
348
|
+
root.setAttribute("data-page-content-wrapper", "");
|
|
349
|
+
root.setAttribute("data-page-number", String(input.pageNumber));
|
|
350
|
+
root.setAttribute("data-page-id", input.pageId);
|
|
351
|
+
root.setAttribute("aria-hidden", "true");
|
|
352
|
+
root.contentEditable = "false";
|
|
353
|
+
root.style.display = "block";
|
|
354
|
+
root.style.height = "0";
|
|
355
|
+
root.style.width = "100%";
|
|
356
|
+
root.style.userSelect = "none";
|
|
357
|
+
return root;
|
|
358
|
+
}
|
|
359
|
+
|
|
265
360
|
function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
|
|
266
361
|
const root = document.createElement("div");
|
|
267
362
|
root.className = "wre-page-chrome-widget";
|
|
@@ -200,6 +200,15 @@ export const editorSchema = new Schema({
|
|
|
200
200
|
hiddenTextOnly: { default: null },
|
|
201
201
|
placeholderCulled: { default: null },
|
|
202
202
|
blockId: { default: null },
|
|
203
|
+
/**
|
|
204
|
+
* `<w:framePr>` projection from `SurfaceBlockFragment.frameProperties`
|
|
205
|
+
* (ECMA-376 §17.3.1.11). When set (and `dropCap` is neither `"drop"`
|
|
206
|
+
* nor `"margin"`), `toDOM` emits `position: absolute` with the
|
|
207
|
+
* xTwips/yTwips/widthTwips/heightTwips fields; the inline flow
|
|
208
|
+
* already treats the paragraph as zero-height per L04
|
|
209
|
+
* `measureBlockHeight` short-circuit (a298391e / coord-04 §1.19.d).
|
|
210
|
+
*/
|
|
211
|
+
frameProperties: { default: null },
|
|
203
212
|
},
|
|
204
213
|
parseDOM: [{ tag: "p" }],
|
|
205
214
|
toDOM(node) {
|
|
@@ -253,17 +262,54 @@ export const editorSchema = new Schema({
|
|
|
253
262
|
else if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}pt`);
|
|
254
263
|
const shadingColor = safeHexColor(node.attrs.shadingFill as string | null);
|
|
255
264
|
if (shadingColor) styles.push(`background-color: ${shadingColor}`);
|
|
265
|
+
// Paragraph borders. Reads the PublicBorderSpec shape
|
|
266
|
+
// (`{value, size, space, color}`) that `pm-state-from-snapshot.ts`
|
|
267
|
+
// forwards verbatim from `SurfaceBlockFragment.borders`. `size` is
|
|
268
|
+
// in eighths of a point (ECMA-376 §17.18.88 ST_Border); `value`
|
|
269
|
+
// follows ECMA-376 §17.18.2 ST_Border enumeration.
|
|
256
270
|
for (const [side, attrName] of [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]] as const) {
|
|
257
|
-
const border = node.attrs[attrName] as
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
const border = node.attrs[attrName] as
|
|
272
|
+
| { color?: string; size?: number; space?: number; value?: string }
|
|
273
|
+
| null;
|
|
274
|
+
if (border && border.value && border.value !== "none" && border.value !== "nil") {
|
|
275
|
+
const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
|
|
260
276
|
const color = safeHexColor(border.color ?? null) ?? "#000000";
|
|
261
|
-
const bStyle =
|
|
277
|
+
const bStyle =
|
|
278
|
+
border.value === "double"
|
|
279
|
+
? "double"
|
|
280
|
+
: border.value === "dashed" || border.value === "dashSmallGap"
|
|
281
|
+
? "dashed"
|
|
282
|
+
: border.value === "dotted"
|
|
283
|
+
? "dotted"
|
|
284
|
+
: "solid";
|
|
262
285
|
styles.push(`border-${side}: ${width} ${bStyle} ${color}`);
|
|
263
286
|
}
|
|
264
287
|
}
|
|
265
288
|
const pageBreak = node.attrs.pageBreakBefore as boolean | null;
|
|
266
289
|
if (pageBreak) styles.push("border-top: 2px dashed rgba(0,0,0,0.1); padding-top: 8px; margin-top: 16px");
|
|
290
|
+
// `<w:framePr>` out-of-flow frame — mirror the static-path branch in
|
|
291
|
+
// tw-page-block-view.helpers.ts so PM-rendered page 1 absolutely
|
|
292
|
+
// positions the frame identically to pages 2+.
|
|
293
|
+
const framePr = node.attrs.frameProperties as
|
|
294
|
+
| {
|
|
295
|
+
xTwips?: number;
|
|
296
|
+
yTwips?: number;
|
|
297
|
+
widthTwips?: number;
|
|
298
|
+
heightTwips?: number;
|
|
299
|
+
hRule?: "auto" | "atLeast" | "exact";
|
|
300
|
+
dropCap?: "none" | "drop" | "margin";
|
|
301
|
+
}
|
|
302
|
+
| null;
|
|
303
|
+
if (framePr && framePr.dropCap !== "drop" && framePr.dropCap !== "margin") {
|
|
304
|
+
styles.push("position: absolute");
|
|
305
|
+
if (typeof framePr.xTwips === "number") styles.push(`left: ${framePr.xTwips / 20}pt`);
|
|
306
|
+
if (typeof framePr.yTwips === "number") styles.push(`top: ${framePr.yTwips / 20}pt`);
|
|
307
|
+
if (typeof framePr.widthTwips === "number") styles.push(`width: ${framePr.widthTwips / 20}pt`);
|
|
308
|
+
if (typeof framePr.heightTwips === "number") {
|
|
309
|
+
if (framePr.hRule === "exact") styles.push(`height: ${framePr.heightTwips / 20}pt`);
|
|
310
|
+
else if (framePr.hRule === "atLeast") styles.push(`min-height: ${framePr.heightTwips / 20}pt`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
267
313
|
const hiddenTextOnly = node.attrs.hiddenTextOnly as boolean | null;
|
|
268
314
|
if (hiddenTextOnly) {
|
|
269
315
|
attrs["data-hidden-paragraph"] = "true";
|
|
@@ -426,6 +426,12 @@ function buildParagraph(
|
|
|
426
426
|
bidi: block.bidi ?? cascade?.bidi ?? null,
|
|
427
427
|
pageBreakBefore: block.pageBreakBefore ?? cascade?.pageBreakBefore ?? null,
|
|
428
428
|
hiddenTextOnly: fullyVanishedParagraph || null,
|
|
429
|
+
// `<w:framePr>` out-of-flow frame — forward to the PM paragraph node
|
|
430
|
+
// so `pm-schema.ts::paragraph.toDOM` emits the absolute positioning
|
|
431
|
+
// that matches the static `buildParagraphStyle` path (L04
|
|
432
|
+
// short-circuits inline flow height in measureBlockHeight; L11 owns
|
|
433
|
+
// the absolute render on BOTH the PM path and the static path).
|
|
434
|
+
frameProperties: block.frameProperties ?? null,
|
|
429
435
|
},
|
|
430
436
|
content.length > 0 ? Fragment.from(content) : undefined,
|
|
431
437
|
);
|
|
@@ -101,10 +101,13 @@ export function findScrollAnchor(
|
|
|
101
101
|
|
|
102
102
|
// Cold-open / pre-paint fallback — the single permitted DOM-origin
|
|
103
103
|
// branch under `src/ui-tailwind/editor-surface/scroll-anchor.ts` per
|
|
104
|
-
// the Slice-3 plan.
|
|
104
|
+
// the Slice-3 plan. Warm path flows through `geometryFacet.getBlock`
|
|
105
|
+
// above; this fallback fires only before the first render frame.
|
|
106
|
+
// geometry:allow-dom-fallback
|
|
105
107
|
const rootRect = root.getBoundingClientRect();
|
|
106
108
|
const rootTop = rootRect.top;
|
|
107
109
|
for (const block of blocks) {
|
|
110
|
+
// geometry:allow-dom-fallback
|
|
108
111
|
const rect = block.getBoundingClientRect();
|
|
109
112
|
if (rect.bottom < rootTop) continue;
|
|
110
113
|
const blockId = block.getAttribute("data-block-id");
|
|
@@ -153,7 +156,11 @@ export function restoreScrollAnchor(
|
|
|
153
156
|
const selector = `[data-block-id="${cssEscape(anchor.blockId)}"]`;
|
|
154
157
|
const block = root.querySelector<HTMLElement>(selector);
|
|
155
158
|
if (!block) return;
|
|
159
|
+
// Cold-open / pre-paint DOM fallback — same rationale as
|
|
160
|
+
// findScrollAnchor's fallback above.
|
|
161
|
+
// geometry:allow-dom-fallback
|
|
156
162
|
const rootRect = root.getBoundingClientRect();
|
|
163
|
+
// geometry:allow-dom-fallback
|
|
157
164
|
const blockRect = block.getBoundingClientRect();
|
|
158
165
|
// We want, post-restore, `blockRect.top === rootRect.top - offsetWithinBlock`
|
|
159
166
|
// (i.e. the block's leading edge sits `offsetWithinBlock` px above the
|
|
@@ -15,17 +15,15 @@ import { Plugin, PluginKey } from "prosemirror-state";
|
|
|
15
15
|
import type { EditorState, Transaction } from "prosemirror-state";
|
|
16
16
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
17
17
|
|
|
18
|
-
import type {
|
|
19
|
-
SearchOptions as PublicSearchOptions,
|
|
20
|
-
} from "../../api/public-types";
|
|
21
18
|
import {
|
|
19
|
+
type SearchOptions as PublicSearchOptions,
|
|
22
20
|
buildSearchPattern,
|
|
23
21
|
createSearchExcerpt,
|
|
24
22
|
findSearchMatches,
|
|
25
23
|
searchSecondaryStories,
|
|
26
24
|
type SecondaryStorySearchResult,
|
|
27
25
|
type SearchTextOptions,
|
|
28
|
-
} from "../../
|
|
26
|
+
} from "../../api/public-types";
|
|
29
27
|
|
|
30
28
|
// ---------------------------------------------------------------------------
|
|
31
29
|
// Public types
|