@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
|
@@ -85,6 +85,54 @@ export function resolveMarkerAlignCss(raw: string | undefined): React.CSSPropert
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Precompute per-tab-segment widths from the paragraph's tabStops.
|
|
90
|
+
*
|
|
91
|
+
* Returns a map `segmentId -> widthPt`. The N-th tab segment gets the
|
|
92
|
+
* width `(tabStops[N].pos - (tabStops[N-1]?.pos ?? 0)) / 20` points —
|
|
93
|
+
* the horizontal span from the previous stop (or the paragraph's
|
|
94
|
+
* content-left edge) to the current stop.
|
|
95
|
+
*
|
|
96
|
+
* This is a conservative approximation of Word's real tab algorithm
|
|
97
|
+
* (which advances to the next stop >= cumulative X considering prior
|
|
98
|
+
* content width). Without run-width measurement, the approximation
|
|
99
|
+
* holds best when the text in each zone is short relative to the zone
|
|
100
|
+
* width — which is exactly the three-zone-header / TOC case L11 needs
|
|
101
|
+
* to render correctly.
|
|
102
|
+
*
|
|
103
|
+
* Direct `block.tabStops` wins over `block.resolvedParagraphFormatting.tabStops`
|
|
104
|
+
* (consistent with other resolved fields).
|
|
105
|
+
*/
|
|
106
|
+
export function computeTabWidthsInPoints(
|
|
107
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
108
|
+
): Map<string, number> {
|
|
109
|
+
const widths = new Map<string, number>();
|
|
110
|
+
// Direct `block.tabStops` uses the public `{pos, val?, leader?}` shape;
|
|
111
|
+
// `block.resolvedParagraphFormatting.tabStops` uses the canonical
|
|
112
|
+
// `{position, align, leader?}` shape. Normalize to a single accessor.
|
|
113
|
+
const readPos = (stop: { pos?: number; position?: number }): number =>
|
|
114
|
+
stop.pos ?? stop.position ?? 0;
|
|
115
|
+
|
|
116
|
+
const rawStops: Array<{ pos?: number; position?: number }> | undefined =
|
|
117
|
+
block.tabStops ?? block.resolvedParagraphFormatting?.tabStops;
|
|
118
|
+
if (!rawStops || rawStops.length === 0) return widths;
|
|
119
|
+
|
|
120
|
+
let tabIndex = 0;
|
|
121
|
+
for (const seg of block.segments) {
|
|
122
|
+
if (seg.kind !== "tab") continue;
|
|
123
|
+
const stop = rawStops[tabIndex];
|
|
124
|
+
if (stop) {
|
|
125
|
+
const prevPos = tabIndex > 0 ? readPos(rawStops[tabIndex - 1]) : 0;
|
|
126
|
+
const widthTwips = readPos(stop) - prevPos;
|
|
127
|
+
if (widthTwips > 0) {
|
|
128
|
+
widths.set(seg.segmentId, widthTwips / 20);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
tabIndex += 1;
|
|
132
|
+
}
|
|
133
|
+
return widths;
|
|
134
|
+
}
|
|
135
|
+
|
|
88
136
|
/** Build CSSProperties for a paragraph block from spacing/indent/alignment. */
|
|
89
137
|
export function buildParagraphStyle(
|
|
90
138
|
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
@@ -159,6 +207,43 @@ export function buildParagraphStyle(
|
|
|
159
207
|
const shadingColor = safeHexColor(shadingFill);
|
|
160
208
|
if (shadingColor) style.backgroundColor = shadingColor;
|
|
161
209
|
|
|
210
|
+
// Paragraph borders (`w:pBdr`). Mirror `pm-schema.ts::paragraph.toDOM`
|
|
211
|
+
// so the static render path (pages 2+, non-PM) paints the same
|
|
212
|
+
// borders as the PM-rendered page 1. Reads the PublicBorderSpec shape
|
|
213
|
+
// (`{value, size, space, color}`); `size` is eighths of a point.
|
|
214
|
+
const borders = block.borders;
|
|
215
|
+
if (borders) {
|
|
216
|
+
const sideToCss: Array<["top" | "bottom" | "left" | "right", keyof NonNullable<typeof borders>]> = [
|
|
217
|
+
["top", "top"],
|
|
218
|
+
["bottom", "bottom"],
|
|
219
|
+
["left", "left"],
|
|
220
|
+
["right", "right"],
|
|
221
|
+
];
|
|
222
|
+
for (const [cssSide, key] of sideToCss) {
|
|
223
|
+
const border = borders[key];
|
|
224
|
+
if (!border || !border.value || border.value === "none" || border.value === "nil") continue;
|
|
225
|
+
const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
|
|
226
|
+
const color = safeHexColor(border.color ?? null) ?? "#000000";
|
|
227
|
+
const bStyle =
|
|
228
|
+
border.value === "double"
|
|
229
|
+
? "double"
|
|
230
|
+
: border.value === "dashed" || border.value === "dashSmallGap"
|
|
231
|
+
? "dashed"
|
|
232
|
+
: border.value === "dotted"
|
|
233
|
+
? "dotted"
|
|
234
|
+
: "solid";
|
|
235
|
+
const cssKey =
|
|
236
|
+
cssSide === "top"
|
|
237
|
+
? "borderTop"
|
|
238
|
+
: cssSide === "bottom"
|
|
239
|
+
? "borderBottom"
|
|
240
|
+
: cssSide === "left"
|
|
241
|
+
? "borderLeft"
|
|
242
|
+
: "borderRight";
|
|
243
|
+
(style as Record<string, string>)[cssKey] = `${width} ${bStyle} ${color}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
162
247
|
// Page break visual indicator
|
|
163
248
|
if (block.pageBreakBefore) {
|
|
164
249
|
style.borderTop = "2px dashed rgba(0,0,0,0.1)";
|
|
@@ -166,6 +251,35 @@ export function buildParagraphStyle(
|
|
|
166
251
|
style.marginTop = "16px";
|
|
167
252
|
}
|
|
168
253
|
|
|
254
|
+
// `<w:framePr>` out-of-flow frame (ECMA-376 §17.3.1.11). L04 returns 0
|
|
255
|
+
// from measureBlockHeight for these paragraphs (a298391e) so the inline
|
|
256
|
+
// flow doesn't double-count; L11 renders them absolutely positioned.
|
|
257
|
+
// Drop-cap (dropCap="drop"|"margin") is in-flow — only the initial
|
|
258
|
+
// letter is framed — so skip the absolute switch there.
|
|
259
|
+
const framePr = block.frameProperties;
|
|
260
|
+
if (framePr && framePr.dropCap !== "drop" && framePr.dropCap !== "margin") {
|
|
261
|
+
style.position = "absolute";
|
|
262
|
+
if (typeof framePr.xTwips === "number") {
|
|
263
|
+
style.left = `${framePr.xTwips / 20}pt`;
|
|
264
|
+
}
|
|
265
|
+
if (typeof framePr.yTwips === "number") {
|
|
266
|
+
style.top = `${framePr.yTwips / 20}pt`;
|
|
267
|
+
}
|
|
268
|
+
if (typeof framePr.widthTwips === "number") {
|
|
269
|
+
style.width = `${framePr.widthTwips / 20}pt`;
|
|
270
|
+
}
|
|
271
|
+
if (typeof framePr.heightTwips === "number") {
|
|
272
|
+
if (framePr.hRule === "exact") {
|
|
273
|
+
style.height = `${framePr.heightTwips / 20}pt`;
|
|
274
|
+
} else if (framePr.hRule === "atLeast") {
|
|
275
|
+
style.minHeight = `${framePr.heightTwips / 20}pt`;
|
|
276
|
+
}
|
|
277
|
+
// hRule === "auto" (or missing) leaves the frame's vertical size
|
|
278
|
+
// content-driven. The height field is ignored — OOXML treats the
|
|
279
|
+
// value as a hint that layout engines may override.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
169
283
|
return style;
|
|
170
284
|
}
|
|
171
285
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
buildMarkerStyle,
|
|
9
9
|
buildParagraphStyle,
|
|
10
10
|
buildSegmentStyle,
|
|
11
|
+
computeTabWidthsInPoints,
|
|
11
12
|
hasStyleEntries,
|
|
12
13
|
headingClassList,
|
|
13
14
|
resolveHeadingLevel,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
|
|
20
21
|
/** Render a single inline segment. */
|
|
21
|
-
function renderSegment(seg: SurfaceInlineSegment): React.ReactNode {
|
|
22
|
+
function renderSegment(seg: SurfaceInlineSegment, tabWidthsPt: Map<string, number>): React.ReactNode {
|
|
22
23
|
switch (seg.kind) {
|
|
23
24
|
case "text": {
|
|
24
25
|
const style = buildSegmentStyle(seg.marks, seg.markAttrs);
|
|
@@ -32,16 +33,22 @@ function renderSegment(seg: SurfaceInlineSegment): React.ReactNode {
|
|
|
32
33
|
</span>
|
|
33
34
|
);
|
|
34
35
|
}
|
|
35
|
-
case "tab":
|
|
36
|
+
case "tab": {
|
|
37
|
+
const widthPt = tabWidthsPt.get(seg.segmentId);
|
|
38
|
+
const tabStyle: React.CSSProperties =
|
|
39
|
+
typeof widthPt === "number"
|
|
40
|
+
? { display: "inline-block", width: `${widthPt}pt`, minWidth: "8px" }
|
|
41
|
+
: { display: "inline-block", width: "32px", minWidth: "8px" };
|
|
36
42
|
return (
|
|
37
43
|
<span
|
|
38
44
|
key={seg.segmentId}
|
|
39
45
|
data-node-type="tab"
|
|
40
|
-
style={
|
|
46
|
+
style={tabStyle}
|
|
41
47
|
>
|
|
42
48
|
{"\u00A0"}
|
|
43
49
|
</span>
|
|
44
50
|
);
|
|
51
|
+
}
|
|
45
52
|
case "hard_break":
|
|
46
53
|
return <br key={seg.segmentId} />;
|
|
47
54
|
case "image":
|
|
@@ -114,6 +121,7 @@ function ParagraphBlock({
|
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
const pStyle = buildParagraphStyle(block);
|
|
124
|
+
const tabWidthsPt = computeTabWidthsInPoints(block);
|
|
117
125
|
const attrs: React.HTMLAttributes<HTMLParagraphElement> & {
|
|
118
126
|
"data-heading-level"?: string;
|
|
119
127
|
"data-numbered"?: string;
|
|
@@ -177,7 +185,7 @@ function ParagraphBlock({
|
|
|
177
185
|
<p {...attrs}>
|
|
178
186
|
{prefixSpan}
|
|
179
187
|
<span className="pm-paragraph-content">
|
|
180
|
-
{block.segments.map((seg) => renderSegment(seg))}
|
|
188
|
+
{block.segments.map((seg) => renderSegment(seg, tabWidthsPt))}
|
|
181
189
|
</span>
|
|
182
190
|
</p>
|
|
183
191
|
);
|
|
@@ -53,7 +53,7 @@ import { buildDecorations } from "./pm-decorations";
|
|
|
53
53
|
import { buildPageBreakDecorations } from "./pm-page-break-decorations";
|
|
54
54
|
import { DecorationSet } from "prosemirror-view";
|
|
55
55
|
import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
|
|
56
|
-
import { buildPagePreviewMaps } from "../../
|
|
56
|
+
import { buildPagePreviewMaps } from "../../api/public-types";
|
|
57
57
|
import { createContextualInteractionPlugin } from "./pm-contextual-ui";
|
|
58
58
|
import {
|
|
59
59
|
finishPerfProbe,
|
|
@@ -65,7 +65,10 @@ import { buildPositionMap, type PositionMap } from "./pm-position-map";
|
|
|
65
65
|
import { createLocalEditSessionState } from "./local-edit-session-state";
|
|
66
66
|
import { createFastTextEditLane } from "./fast-text-edit-lane";
|
|
67
67
|
import { createPredictedTxGate } from "./predicted-tx-gate";
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
createScopeTagRegistry,
|
|
70
|
+
type ScopeTagRegistry,
|
|
71
|
+
} from "../../api/public-types";
|
|
69
72
|
import { hasBailIfCrossedTagInRange } from "./predicted-tag-preflight";
|
|
70
73
|
import {
|
|
71
74
|
clearSearch as clearSearchPlugin,
|
|
@@ -265,6 +268,20 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
265
268
|
runtimeGetTableSelectionDescriptor?: (
|
|
266
269
|
state: import("prosemirror-state").EditorState,
|
|
267
270
|
) => TableSelectionDescriptor | null;
|
|
271
|
+
/**
|
|
272
|
+
* Coord-10 L11-4 — host-supplied scope-tag registry factory.
|
|
273
|
+
* Threaded from `api.runtime.workflow.createScopeTagRegistry` at the
|
|
274
|
+
* WordReviewEditor mount. Mints a fresh default-seeded
|
|
275
|
+
* `ScopeTagRegistry` that the PM surface uses to drive annotation-
|
|
276
|
+
* family boundary behaviors (bail-on-cross, split-on-cross, etc.).
|
|
277
|
+
*
|
|
278
|
+
* When omitted, the surface falls back to the public-types re-export
|
|
279
|
+
* (`createScopeTagRegistry` from `src/api/public-types.ts`, shipped
|
|
280
|
+
* 2026-04-24 per refactor/11 §4.17 as a pragmatic pass-through).
|
|
281
|
+
* The fallback is retained for back-compat with any consumer that
|
|
282
|
+
* mounts the surface without threading an api handle (legacy tests).
|
|
283
|
+
*/
|
|
284
|
+
scopeTagRegistryFactory?: () => ScopeTagRegistry;
|
|
268
285
|
onPasteApplied?: (meta: {
|
|
269
286
|
segmentCount: number;
|
|
270
287
|
charCount: number;
|
|
@@ -435,9 +452,17 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
435
452
|
const snapshotRef = useRef(snapshot);
|
|
436
453
|
snapshotRef.current = snapshot;
|
|
437
454
|
|
|
455
|
+
// Coord-10 L11-4 — prefer the host-supplied `scopeTagRegistryFactory`
|
|
456
|
+
// (threaded from `api.runtime.workflow.createScopeTagRegistry` at the
|
|
457
|
+
// WordReviewEditor mount); fall back to the public-types re-export
|
|
458
|
+
// for legacy mounts that don't thread an api handle.
|
|
459
|
+
const scopeTagRegistryFactoryProp = props.scopeTagRegistryFactory;
|
|
438
460
|
const scopeTagRegistry = useMemo(
|
|
439
|
-
() =>
|
|
440
|
-
|
|
461
|
+
() =>
|
|
462
|
+
scopeTagRegistryFactoryProp
|
|
463
|
+
? scopeTagRegistryFactoryProp()
|
|
464
|
+
: createScopeTagRegistry(),
|
|
465
|
+
[scopeTagRegistryFactoryProp],
|
|
441
466
|
);
|
|
442
467
|
|
|
443
468
|
// Keep callbacks ref up to date (avoids stale closures in PM plugins)
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -112,11 +112,13 @@ export {
|
|
|
112
112
|
type TwScopeRailLayerProps,
|
|
113
113
|
} from "./chrome-overlay";
|
|
114
114
|
|
|
115
|
-
// Session capabilities
|
|
115
|
+
// Session capabilities — sourced via `src/api/public-types` re-export
|
|
116
|
+
// so the public facade under `src/ui-tailwind/**` stays P3-clean.
|
|
117
|
+
// Canonical impl lives on `src/runtime/session-capabilities.ts`.
|
|
116
118
|
export {
|
|
117
119
|
deriveCapabilities,
|
|
118
120
|
type SessionCapabilities,
|
|
119
|
-
} from "../
|
|
121
|
+
} from "../api/public-types";
|
|
120
122
|
|
|
121
123
|
// Headless (re-export for convenience)
|
|
122
124
|
export {
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
DocumentNavigationSnapshot,
|
|
3
|
-
PageLayoutSnapshot,
|
|
4
|
-
SurfaceBlockSnapshot,
|
|
5
|
-
} from "../api/public-types.ts";
|
|
6
|
-
import { findPageForOffset } from "../runtime/document-navigation.ts";
|
|
7
1
|
import {
|
|
2
|
+
type DocumentNavigationSnapshot,
|
|
3
|
+
type PageLayoutSnapshot,
|
|
4
|
+
type SurfaceBlockSnapshot,
|
|
5
|
+
findPageForOffset,
|
|
8
6
|
DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
9
7
|
estimateBlockHeight,
|
|
10
8
|
estimateParagraphLineCount,
|
|
11
9
|
estimateParagraphLineHeight,
|
|
12
10
|
getUsableColumnWidth,
|
|
13
|
-
} from "../
|
|
11
|
+
} from "../api/public-types.ts";
|
|
14
12
|
|
|
15
13
|
const DOCUMENT_CONTENT_TOP_PADDING_PX = 40;
|
|
16
14
|
|
|
@@ -7,8 +7,11 @@ import type {
|
|
|
7
7
|
SurfaceInlineSegment,
|
|
8
8
|
} from "../../api/public-types.ts";
|
|
9
9
|
import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
storyTargetKey,
|
|
12
|
+
EMU_PER_PX,
|
|
13
|
+
TWIPS_PER_PX,
|
|
14
|
+
} from "../../api/public-types.ts";
|
|
12
15
|
import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -98,40 +101,57 @@ export function collectFloatingImageOverlayItems(input: {
|
|
|
98
101
|
);
|
|
99
102
|
const items: FloatingImageOverlayItem[] = [];
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
const collectFromStory = (
|
|
105
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
106
|
+
storyTarget: EditorStoryTarget,
|
|
107
|
+
) => {
|
|
108
|
+
walkSurfaceBlocks(blocks, (segment) => {
|
|
109
|
+
if (
|
|
110
|
+
segment.kind !== "image" ||
|
|
111
|
+
!shouldRenderAbsoluteFloatingImageInPageOverlay(segment.anchor)
|
|
112
|
+
) {
|
|
113
|
+
return;
|
|
111
114
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
|
|
116
|
+
const pages = resolveTargetPages(facet, segment.from, storyTarget);
|
|
117
|
+
for (const page of pages) {
|
|
118
|
+
const pageRect = rectByPageIndex.get(page.pageIndex);
|
|
119
|
+
if (!pageRect) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const localRect = resolveFloatingImageLocalRect(page, storyTarget, segment, pxPerTwip);
|
|
123
|
+
if (!localRect) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const preview = input.mediaPreviews?.[segment.mediaId];
|
|
127
|
+
items.push({
|
|
128
|
+
key: `${segment.segmentId}:${page.pageId}`,
|
|
129
|
+
mediaId: segment.mediaId,
|
|
130
|
+
from: segment.from,
|
|
131
|
+
to: segment.to,
|
|
132
|
+
pageId: page.pageId,
|
|
133
|
+
pageIndex: page.pageIndex,
|
|
134
|
+
topPx: pageRect.topPx + localRect.topPx,
|
|
135
|
+
leftPx: localRect.leftPx,
|
|
136
|
+
widthPx: localRect.widthPx,
|
|
137
|
+
heightPx: localRect.heightPx,
|
|
138
|
+
behindDoc: Boolean(segment.anchor?.behindDoc),
|
|
139
|
+
src: preview?.src ?? null,
|
|
140
|
+
altText: segment.altText ?? null,
|
|
141
|
+
detail: segment.detail ?? null,
|
|
142
|
+
});
|
|
115
143
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
heightPx: localRect.heightPx,
|
|
128
|
-
behindDoc: Boolean(segment.anchor?.behindDoc),
|
|
129
|
-
src: preview?.src ?? null,
|
|
130
|
-
altText: segment.altText ?? null,
|
|
131
|
-
detail: segment.detail ?? null,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
});
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// coord-01 §9 / §5.1 — CCEP logos live in header stories; collect from
|
|
148
|
+
// the main story for the active-story case AND from every secondary
|
|
149
|
+
// story so header/footer images reach the overlay regardless of which
|
|
150
|
+
// story is active in the editor.
|
|
151
|
+
collectFromStory(surface.blocks, activeStory);
|
|
152
|
+
for (const secondary of surface.secondaryStories ?? []) {
|
|
153
|
+
collectFromStory(secondary.blocks, secondary.target);
|
|
154
|
+
}
|
|
135
155
|
|
|
136
156
|
return items;
|
|
137
157
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
|
|
4
5
|
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -27,11 +28,13 @@ import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
|
27
28
|
export interface TwEndnoteAreaProps {
|
|
28
29
|
blocks: readonly SurfaceBlockSnapshot[];
|
|
29
30
|
"data-testid"?: string;
|
|
31
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export const TwEndnoteArea: React.FC<TwEndnoteAreaProps> = ({
|
|
33
35
|
blocks,
|
|
34
36
|
"data-testid": testId,
|
|
37
|
+
mediaPreviews,
|
|
35
38
|
}) => {
|
|
36
39
|
if (blocks.length === 0) return null;
|
|
37
40
|
return (
|
|
@@ -49,7 +52,7 @@ export const TwEndnoteArea: React.FC<TwEndnoteAreaProps> = ({
|
|
|
49
52
|
marginBottom: "8pt",
|
|
50
53
|
}}
|
|
51
54
|
/>
|
|
52
|
-
<TwRegionBlockRenderer blocks={blocks} />
|
|
55
|
+
<TwRegionBlockRenderer blocks={blocks} mediaPreviews={mediaPreviews} />
|
|
53
56
|
</div>
|
|
54
57
|
);
|
|
55
58
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
|
|
4
5
|
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -30,6 +31,7 @@ export interface TwFootnoteAreaProps {
|
|
|
30
31
|
widthPx: number;
|
|
31
32
|
heightPx: number;
|
|
32
33
|
"data-testid"?: string;
|
|
34
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export const TwFootnoteArea: React.FC<TwFootnoteAreaProps> = React.memo(({
|
|
@@ -40,6 +42,7 @@ export const TwFootnoteArea: React.FC<TwFootnoteAreaProps> = React.memo(({
|
|
|
40
42
|
widthPx,
|
|
41
43
|
heightPx,
|
|
42
44
|
"data-testid": testId,
|
|
45
|
+
mediaPreviews,
|
|
43
46
|
}) => {
|
|
44
47
|
return (
|
|
45
48
|
<div
|
|
@@ -63,7 +66,7 @@ export const TwFootnoteArea: React.FC<TwFootnoteAreaProps> = React.memo(({
|
|
|
63
66
|
marginBottom: "4pt",
|
|
64
67
|
}}
|
|
65
68
|
/>
|
|
66
|
-
<TwRegionBlockRenderer blocks={blocks} />
|
|
69
|
+
<TwRegionBlockRenderer blocks={blocks} mediaPreviews={mediaPreviews} />
|
|
67
70
|
</div>
|
|
68
71
|
);
|
|
69
72
|
});
|
|
@@ -26,6 +26,7 @@ import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
|
|
|
26
26
|
import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
|
|
27
27
|
import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
|
|
28
28
|
import { TwFootnoteArea } from "./tw-footnote-area.tsx";
|
|
29
|
+
import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
|
|
29
30
|
|
|
30
31
|
export interface TwPageChromeEntryProps {
|
|
31
32
|
rect: PageOverlayRect;
|
|
@@ -36,6 +37,9 @@ export interface TwPageChromeEntryProps {
|
|
|
36
37
|
onOpenStory?: (target: EditorStoryTarget) => void;
|
|
37
38
|
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
38
39
|
renderFrameRevision: number;
|
|
40
|
+
/** Preview catalog threaded into header/footer/footnote region renderers
|
|
41
|
+
* so images (CCEP logos on 7-of-8 CCEP docs) render as real <img>s. */
|
|
42
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
function TwPageChromeEntryInner({
|
|
@@ -47,6 +51,7 @@ function TwPageChromeEntryInner({
|
|
|
47
51
|
onOpenStory,
|
|
48
52
|
visiblePageIndexRange,
|
|
49
53
|
renderFrameRevision,
|
|
54
|
+
mediaPreviews,
|
|
50
55
|
}: TwPageChromeEntryProps): React.ReactElement {
|
|
51
56
|
const layout = page.layout;
|
|
52
57
|
const headerStory = page.stories.header;
|
|
@@ -186,6 +191,7 @@ function TwPageChromeEntryInner({
|
|
|
186
191
|
isActiveSlot={Boolean(headerActive)}
|
|
187
192
|
sectionLabel={headerActive ? headerSectionLabel : undefined}
|
|
188
193
|
onClick={handleHeaderClick}
|
|
194
|
+
mediaPreviews={mediaPreviews}
|
|
189
195
|
/>
|
|
190
196
|
) : null}
|
|
191
197
|
{footerRegion && footerStory ? (
|
|
@@ -199,6 +205,7 @@ function TwPageChromeEntryInner({
|
|
|
199
205
|
isActiveSlot={Boolean(footerActive)}
|
|
200
206
|
sectionLabel={footerActive ? footerSectionLabel : undefined}
|
|
201
207
|
onClick={handleFooterClick}
|
|
208
|
+
mediaPreviews={mediaPreviews}
|
|
202
209
|
/>
|
|
203
210
|
) : null}
|
|
204
211
|
{footnoteRegion ? (
|
|
@@ -209,6 +216,7 @@ function TwPageChromeEntryInner({
|
|
|
209
216
|
leftPx={bandLeftPx}
|
|
210
217
|
widthPx={px(footnoteRegion.widthTwips)}
|
|
211
218
|
heightPx={px(footnoteRegion.heightTwips)}
|
|
219
|
+
mediaPreviews={mediaPreviews}
|
|
212
220
|
/>
|
|
213
221
|
) : null}
|
|
214
222
|
{continuationTableEntries.map(({ blockId, headerRows }) => (
|
|
@@ -240,7 +248,8 @@ function propsAreEqual(
|
|
|
240
248
|
prev.renderFrameRevision === next.renderFrameRevision &&
|
|
241
249
|
prev.rect.topPx === next.rect.topPx &&
|
|
242
250
|
prev.rect.bottomPx === next.rect.bottomPx &&
|
|
243
|
-
prev.rect.pageId === next.rect.pageId
|
|
251
|
+
prev.rect.pageId === next.rect.pageId &&
|
|
252
|
+
prev.mediaPreviews === next.mediaPreviews
|
|
244
253
|
);
|
|
245
254
|
}
|
|
246
255
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
|
|
4
5
|
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -34,6 +35,7 @@ export interface TwPageFooterBandProps {
|
|
|
34
35
|
sectionLabel?: string;
|
|
35
36
|
onClick: () => void;
|
|
36
37
|
"data-testid"?: string;
|
|
38
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
@@ -47,6 +49,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
47
49
|
sectionLabel,
|
|
48
50
|
onClick,
|
|
49
51
|
"data-testid": testId,
|
|
52
|
+
mediaPreviews,
|
|
50
53
|
}) => {
|
|
51
54
|
return (
|
|
52
55
|
<div
|
|
@@ -77,7 +80,11 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
77
80
|
style={{ width: "100%", height: "100%" }}
|
|
78
81
|
/>
|
|
79
82
|
) : (
|
|
80
|
-
<TwRegionBlockRenderer
|
|
83
|
+
<TwRegionBlockRenderer
|
|
84
|
+
blocks={blocks}
|
|
85
|
+
mediaPreviews={mediaPreviews}
|
|
86
|
+
fallbackDisplay="hidden"
|
|
87
|
+
/>
|
|
81
88
|
)}
|
|
82
89
|
</div>
|
|
83
90
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
|
|
4
5
|
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -35,6 +36,10 @@ export interface TwPageHeaderBandProps {
|
|
|
35
36
|
sectionLabel?: string;
|
|
36
37
|
onClick: () => void;
|
|
37
38
|
"data-testid"?: string;
|
|
39
|
+
/** Preview catalog threaded to the region renderer so header images
|
|
40
|
+
* (CCEP logos on 7-of-8 CCEP docs) render as real <img>s instead of
|
|
41
|
+
* the 48×32 placeholder chip. */
|
|
42
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
@@ -48,6 +53,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
48
53
|
sectionLabel,
|
|
49
54
|
onClick,
|
|
50
55
|
"data-testid": testId,
|
|
56
|
+
mediaPreviews,
|
|
51
57
|
}) => {
|
|
52
58
|
return (
|
|
53
59
|
<div
|
|
@@ -78,7 +84,11 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
78
84
|
style={{ width: "100%", height: "100%" }}
|
|
79
85
|
/>
|
|
80
86
|
) : (
|
|
81
|
-
<TwRegionBlockRenderer
|
|
87
|
+
<TwRegionBlockRenderer
|
|
88
|
+
blocks={blocks}
|
|
89
|
+
mediaPreviews={mediaPreviews}
|
|
90
|
+
fallbackDisplay="hidden"
|
|
91
|
+
/>
|
|
82
92
|
)}
|
|
83
93
|
</div>
|
|
84
94
|
);
|
|
@@ -145,6 +145,10 @@ export interface TwPageStackChromeLayerProps {
|
|
|
145
145
|
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
146
146
|
/** Optional test id applied to the layer root. */
|
|
147
147
|
"data-testid"?: string;
|
|
148
|
+
/** Preview catalog threaded through to per-page chrome bands so images
|
|
149
|
+
* in headers/footers/footnote bodies render as real <img>s. Without
|
|
150
|
+
* this, image segments fall back to the 48×32 gray placeholder chip. */
|
|
151
|
+
mediaPreviews?: Record<string, import("../editor-surface/pm-state-from-snapshot.ts").MediaPreviewDescriptor>;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
@@ -157,6 +161,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
157
161
|
pmView,
|
|
158
162
|
visiblePageIndexRange,
|
|
159
163
|
"data-testid": testId,
|
|
164
|
+
mediaPreviews,
|
|
160
165
|
}) => {
|
|
161
166
|
const [rects, setRects] = React.useState<readonly PageOverlayRect[]>([]);
|
|
162
167
|
const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
|
|
@@ -401,10 +406,11 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
401
406
|
onOpenStory={onOpenStory}
|
|
402
407
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
403
408
|
renderFrameRevision={renderFrameRevision}
|
|
409
|
+
mediaPreviews={mediaPreviews}
|
|
404
410
|
/>
|
|
405
411
|
);
|
|
406
412
|
})}
|
|
407
|
-
<TwEndnoteArea blocks={endnoteBlocks} />
|
|
413
|
+
<TwEndnoteArea blocks={endnoteBlocks} mediaPreviews={mediaPreviews} />
|
|
408
414
|
</div>
|
|
409
415
|
);
|
|
410
416
|
};
|