@beyondwork/docx-react-component 1.0.71 → 1.0.72
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 +243 -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/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/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/io/export/serialize-headers-footers.ts +1 -0
- package/src/io/export/serialize-main-document.ts +13 -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 +11 -0
- package/src/io/ooxml/parse-main-document.ts +21 -5
- 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 +257 -1
- package/src/runtime/layout/paginated-layout-engine.ts +134 -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/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 +49 -3
- package/src/runtime/scopes/semantic-scope-types.ts +8 -0
- package/src/runtime/surface-projection.ts +22 -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/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 +18 -4
- 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 +37 -0
- 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 +5 -2
- 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 +4 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +7 -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 +73 -8
- 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/tw-review-workspace.tsx +1 -0
|
@@ -253,12 +253,26 @@ export const editorSchema = new Schema({
|
|
|
253
253
|
else if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}pt`);
|
|
254
254
|
const shadingColor = safeHexColor(node.attrs.shadingFill as string | null);
|
|
255
255
|
if (shadingColor) styles.push(`background-color: ${shadingColor}`);
|
|
256
|
+
// Paragraph borders. Reads the PublicBorderSpec shape
|
|
257
|
+
// (`{value, size, space, color}`) that `pm-state-from-snapshot.ts`
|
|
258
|
+
// forwards verbatim from `SurfaceBlockFragment.borders`. `size` is
|
|
259
|
+
// in eighths of a point (ECMA-376 §17.18.88 ST_Border); `value`
|
|
260
|
+
// follows ECMA-376 §17.18.2 ST_Border enumeration.
|
|
256
261
|
for (const [side, attrName] of [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]] as const) {
|
|
257
|
-
const border = node.attrs[attrName] as
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
const border = node.attrs[attrName] as
|
|
263
|
+
| { color?: string; size?: number; space?: number; value?: string }
|
|
264
|
+
| null;
|
|
265
|
+
if (border && border.value && border.value !== "none" && border.value !== "nil") {
|
|
266
|
+
const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
|
|
260
267
|
const color = safeHexColor(border.color ?? null) ?? "#000000";
|
|
261
|
-
const bStyle =
|
|
268
|
+
const bStyle =
|
|
269
|
+
border.value === "double"
|
|
270
|
+
? "double"
|
|
271
|
+
: border.value === "dashed" || border.value === "dashSmallGap"
|
|
272
|
+
? "dashed"
|
|
273
|
+
: border.value === "dotted"
|
|
274
|
+
? "dotted"
|
|
275
|
+
: "solid";
|
|
262
276
|
styles.push(`border-${side}: ${width} ${bStyle} ${color}`);
|
|
263
277
|
}
|
|
264
278
|
}
|
|
@@ -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
|
|
@@ -159,6 +159,43 @@ export function buildParagraphStyle(
|
|
|
159
159
|
const shadingColor = safeHexColor(shadingFill);
|
|
160
160
|
if (shadingColor) style.backgroundColor = shadingColor;
|
|
161
161
|
|
|
162
|
+
// Paragraph borders (`w:pBdr`). Mirror `pm-schema.ts::paragraph.toDOM`
|
|
163
|
+
// so the static render path (pages 2+, non-PM) paints the same
|
|
164
|
+
// borders as the PM-rendered page 1. Reads the PublicBorderSpec shape
|
|
165
|
+
// (`{value, size, space, color}`); `size` is eighths of a point.
|
|
166
|
+
const borders = block.borders;
|
|
167
|
+
if (borders) {
|
|
168
|
+
const sideToCss: Array<["top" | "bottom" | "left" | "right", keyof NonNullable<typeof borders>]> = [
|
|
169
|
+
["top", "top"],
|
|
170
|
+
["bottom", "bottom"],
|
|
171
|
+
["left", "left"],
|
|
172
|
+
["right", "right"],
|
|
173
|
+
];
|
|
174
|
+
for (const [cssSide, key] of sideToCss) {
|
|
175
|
+
const border = borders[key];
|
|
176
|
+
if (!border || !border.value || border.value === "none" || border.value === "nil") continue;
|
|
177
|
+
const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
|
|
178
|
+
const color = safeHexColor(border.color ?? null) ?? "#000000";
|
|
179
|
+
const bStyle =
|
|
180
|
+
border.value === "double"
|
|
181
|
+
? "double"
|
|
182
|
+
: border.value === "dashed" || border.value === "dashSmallGap"
|
|
183
|
+
? "dashed"
|
|
184
|
+
: border.value === "dotted"
|
|
185
|
+
? "dotted"
|
|
186
|
+
: "solid";
|
|
187
|
+
const cssKey =
|
|
188
|
+
cssSide === "top"
|
|
189
|
+
? "borderTop"
|
|
190
|
+
: cssSide === "bottom"
|
|
191
|
+
? "borderBottom"
|
|
192
|
+
: cssSide === "left"
|
|
193
|
+
? "borderLeft"
|
|
194
|
+
: "borderRight";
|
|
195
|
+
(style as Record<string, string>)[cssKey] = `${width} ${bStyle} ${color}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
162
199
|
// Page break visual indicator
|
|
163
200
|
if (block.pageBreakBefore) {
|
|
164
201
|
style.borderTop = "2px dashed rgba(0,0,0,0.1)";
|
|
@@ -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
|
/**
|
|
@@ -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,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
77
80
|
style={{ width: "100%", height: "100%" }}
|
|
78
81
|
/>
|
|
79
82
|
) : (
|
|
80
|
-
<TwRegionBlockRenderer blocks={blocks} />
|
|
83
|
+
<TwRegionBlockRenderer blocks={blocks} mediaPreviews={mediaPreviews} />
|
|
81
84
|
)}
|
|
82
85
|
</div>
|
|
83
86
|
);
|
|
@@ -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,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
78
84
|
style={{ width: "100%", height: "100%" }}
|
|
79
85
|
/>
|
|
80
86
|
) : (
|
|
81
|
-
<TwRegionBlockRenderer blocks={blocks} />
|
|
87
|
+
<TwRegionBlockRenderer blocks={blocks} mediaPreviews={mediaPreviews} />
|
|
82
88
|
)}
|
|
83
89
|
</div>
|
|
84
90
|
);
|
|
@@ -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
|
};
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
SurfaceBlockSnapshot,
|
|
5
5
|
SurfaceInlineSegment,
|
|
6
6
|
} from "../../api/public-types.ts";
|
|
7
|
+
import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
|
|
7
8
|
import {
|
|
8
9
|
buildMarkerStyle,
|
|
9
10
|
buildParagraphStyle,
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
resolveHeadingLevel,
|
|
14
15
|
} from "../editor-surface/tw-page-block-view.helpers.ts";
|
|
15
16
|
|
|
17
|
+
const EMU_PER_PX = 9525;
|
|
18
|
+
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
// TwRegionBlockRenderer (P8.4)
|
|
18
21
|
//
|
|
@@ -37,7 +40,10 @@ import {
|
|
|
37
40
|
// Inline segment renderer — mirrors `tw-page-block-view`'s `renderSegment`.
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
|
|
40
|
-
function renderSegment(
|
|
43
|
+
function renderSegment(
|
|
44
|
+
seg: SurfaceInlineSegment,
|
|
45
|
+
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
46
|
+
): React.ReactNode {
|
|
41
47
|
switch (seg.kind) {
|
|
42
48
|
case "text": {
|
|
43
49
|
const style = buildSegmentStyle(seg.marks, seg.markAttrs);
|
|
@@ -63,11 +69,46 @@ function renderSegment(seg: SurfaceInlineSegment): React.ReactNode {
|
|
|
63
69
|
);
|
|
64
70
|
case "hard_break":
|
|
65
71
|
return <br key={seg.segmentId} />;
|
|
66
|
-
case "image":
|
|
72
|
+
case "image": {
|
|
73
|
+
// Mirror body-renderer behavior (`pm-state-from-snapshot.ts` :500+):
|
|
74
|
+
// look up the preview via `seg.mediaId` and render a real <img> when
|
|
75
|
+
// available. Without a preview (or `state === "missing"`), fall back
|
|
76
|
+
// to the placeholder chip — a deliberate signal that the media part
|
|
77
|
+
// exists canonical-side but no bytes have been resolved yet.
|
|
78
|
+
const preview = mediaPreviews[seg.mediaId];
|
|
79
|
+
const widthEmu = preview?.widthEmu ?? seg.anchor?.extent.widthEmu;
|
|
80
|
+
const heightEmu = preview?.heightEmu ?? seg.anchor?.extent.heightEmu;
|
|
81
|
+
if (preview?.src && seg.state !== "missing") {
|
|
82
|
+
const widthPx = widthEmu
|
|
83
|
+
? Math.max(8, Math.round(widthEmu / EMU_PER_PX))
|
|
84
|
+
: undefined;
|
|
85
|
+
const heightPx = heightEmu
|
|
86
|
+
? Math.max(8, Math.round(heightEmu / EMU_PER_PX))
|
|
87
|
+
: undefined;
|
|
88
|
+
return (
|
|
89
|
+
<img
|
|
90
|
+
key={seg.segmentId}
|
|
91
|
+
src={preview.src}
|
|
92
|
+
alt={seg.altText ?? ""}
|
|
93
|
+
data-node-type="image"
|
|
94
|
+
data-media-id={seg.mediaId}
|
|
95
|
+
style={{
|
|
96
|
+
display: "inline-block",
|
|
97
|
+
verticalAlign: "middle",
|
|
98
|
+
...(widthPx ? { width: `${widthPx}px` } : {}),
|
|
99
|
+
...(heightPx ? { height: `${heightPx}px` } : {}),
|
|
100
|
+
maxWidth: "100%",
|
|
101
|
+
objectFit: "contain",
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
67
106
|
return (
|
|
68
107
|
<span
|
|
69
108
|
key={seg.segmentId}
|
|
70
109
|
data-node-type="image"
|
|
110
|
+
data-media-id={seg.mediaId}
|
|
111
|
+
data-state={seg.state}
|
|
71
112
|
style={{
|
|
72
113
|
display: "inline-block",
|
|
73
114
|
width: "48px",
|
|
@@ -80,6 +121,7 @@ function renderSegment(seg: SurfaceInlineSegment): React.ReactNode {
|
|
|
80
121
|
title={seg.altText ?? "Image"}
|
|
81
122
|
/>
|
|
82
123
|
);
|
|
124
|
+
}
|
|
83
125
|
case "field_ref":
|
|
84
126
|
return (
|
|
85
127
|
<span
|
|
@@ -121,8 +163,10 @@ function renderSegment(seg: SurfaceInlineSegment): React.ReactNode {
|
|
|
121
163
|
|
|
122
164
|
function RegionParagraph({
|
|
123
165
|
block,
|
|
166
|
+
mediaPreviews,
|
|
124
167
|
}: {
|
|
125
168
|
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
|
|
169
|
+
mediaPreviews: Record<string, MediaPreviewDescriptor>;
|
|
126
170
|
}): React.ReactElement {
|
|
127
171
|
const headingLevel = resolveHeadingLevel(block.styleId, block.outlineLevel);
|
|
128
172
|
const classes: string[] = ["leading-relaxed"];
|
|
@@ -195,7 +239,7 @@ function RegionParagraph({
|
|
|
195
239
|
<div {...attrs}>
|
|
196
240
|
{prefixSpan}
|
|
197
241
|
<span className="pm-paragraph-content">
|
|
198
|
-
{block.segments.map((seg) => renderSegment(seg))}
|
|
242
|
+
{block.segments.map((seg) => renderSegment(seg, mediaPreviews))}
|
|
199
243
|
</span>
|
|
200
244
|
</div>
|
|
201
245
|
);
|
|
@@ -203,8 +247,10 @@ function RegionParagraph({
|
|
|
203
247
|
|
|
204
248
|
function RegionTable({
|
|
205
249
|
block,
|
|
250
|
+
mediaPreviews,
|
|
206
251
|
}: {
|
|
207
252
|
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>;
|
|
253
|
+
mediaPreviews: Record<string, MediaPreviewDescriptor>;
|
|
208
254
|
}): React.ReactElement {
|
|
209
255
|
const tableStyle: React.CSSProperties = {
|
|
210
256
|
borderCollapse: "collapse",
|
|
@@ -273,7 +319,11 @@ function RegionTable({
|
|
|
273
319
|
style={Object.keys(cellStyle).length > 0 ? cellStyle : undefined}
|
|
274
320
|
>
|
|
275
321
|
{cell.content.map((childBlock) => (
|
|
276
|
-
<RegionBlockItem
|
|
322
|
+
<RegionBlockItem
|
|
323
|
+
key={childBlock.blockId}
|
|
324
|
+
block={childBlock}
|
|
325
|
+
mediaPreviews={mediaPreviews}
|
|
326
|
+
/>
|
|
277
327
|
))}
|
|
278
328
|
</td>
|
|
279
329
|
);
|
|
@@ -311,14 +361,16 @@ function RegionOpaque({
|
|
|
311
361
|
|
|
312
362
|
function RegionBlockItem({
|
|
313
363
|
block,
|
|
364
|
+
mediaPreviews,
|
|
314
365
|
}: {
|
|
315
366
|
block: SurfaceBlockSnapshot;
|
|
367
|
+
mediaPreviews: Record<string, MediaPreviewDescriptor>;
|
|
316
368
|
}): React.ReactElement | null {
|
|
317
369
|
switch (block.kind) {
|
|
318
370
|
case "paragraph":
|
|
319
|
-
return <RegionParagraph block={block} />;
|
|
371
|
+
return <RegionParagraph block={block} mediaPreviews={mediaPreviews} />;
|
|
320
372
|
case "table":
|
|
321
|
-
return <RegionTable block={block} />;
|
|
373
|
+
return <RegionTable block={block} mediaPreviews={mediaPreviews} />;
|
|
322
374
|
case "sdt_block":
|
|
323
375
|
return (
|
|
324
376
|
<section
|
|
@@ -328,7 +380,7 @@ function RegionBlockItem({
|
|
|
328
380
|
style={{ margin: "8px 0" }}
|
|
329
381
|
>
|
|
330
382
|
{block.children.map((child) => (
|
|
331
|
-
<RegionBlockItem key={child.blockId} block={child} />
|
|
383
|
+
<RegionBlockItem key={child.blockId} block={child} mediaPreviews={mediaPreviews} />
|
|
332
384
|
))}
|
|
333
385
|
</section>
|
|
334
386
|
);
|
|
@@ -348,8 +400,19 @@ export interface TwRegionBlockRendererProps {
|
|
|
348
400
|
blocks: readonly SurfaceBlockSnapshot[];
|
|
349
401
|
/** Optional class name applied to the root wrapper. */
|
|
350
402
|
className?: string;
|
|
403
|
+
/**
|
|
404
|
+
* Media preview catalog. Without it, any `kind: "image"` segment falls
|
|
405
|
+
* back to the 48×32 gray placeholder chip — the pre-existing behavior
|
|
406
|
+
* for the 7-of-8 CCEP docs whose headers carry a logo. Pass the same
|
|
407
|
+
* catalog the body renderer uses (`props.mediaPreviews` on the
|
|
408
|
+
* workspace root) to light up headers + footers + footnote bodies with
|
|
409
|
+
* real image bytes.
|
|
410
|
+
*/
|
|
411
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
351
412
|
}
|
|
352
413
|
|
|
414
|
+
const EMPTY_MEDIA_PREVIEWS: Record<string, MediaPreviewDescriptor> = {};
|
|
415
|
+
|
|
353
416
|
/**
|
|
354
417
|
* TwRegionBlockRenderer — read-only React renderer for a region's
|
|
355
418
|
* `SurfaceBlockSnapshot[]`. Used by the header / footer / footnote /
|
|
@@ -363,9 +426,11 @@ export interface TwRegionBlockRendererProps {
|
|
|
363
426
|
export function TwRegionBlockRenderer({
|
|
364
427
|
blocks,
|
|
365
428
|
className,
|
|
429
|
+
mediaPreviews,
|
|
366
430
|
}: TwRegionBlockRendererProps): React.ReactElement {
|
|
367
431
|
const rootClasses = ["ProseMirror"];
|
|
368
432
|
if (className) rootClasses.push(className);
|
|
433
|
+
const previews = mediaPreviews ?? EMPTY_MEDIA_PREVIEWS;
|
|
369
434
|
return (
|
|
370
435
|
<div
|
|
371
436
|
className={rootClasses.join(" ")}
|
|
@@ -373,7 +438,7 @@ export function TwRegionBlockRenderer({
|
|
|
373
438
|
data-region-block-renderer=""
|
|
374
439
|
>
|
|
375
440
|
{blocks.map((block) => (
|
|
376
|
-
<RegionBlockItem key={block.blockId} block={block} />
|
|
441
|
+
<RegionBlockItem key={block.blockId} block={block} mediaPreviews={previews} />
|
|
377
442
|
))}
|
|
378
443
|
</div>
|
|
379
444
|
);
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
CommentBody,
|
|
6
6
|
CommentMention,
|
|
7
7
|
} from "../../api/comment-presentation-types";
|
|
8
|
-
import { sanitizeMarkdown } from "../../
|
|
8
|
+
import { sanitizeMarkdown } from "../../api/public-types";
|
|
9
9
|
|
|
10
10
|
export interface CommentMarkdownRendererProps {
|
|
11
11
|
body: CommentBody;
|