@beyondwork/docx-react-component 1.0.42 → 1.0.45
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 +17 -0
- package/package.json +5 -4
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +333 -4
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +60 -10
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/search/search-text.ts +15 -2
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +29 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +692 -2
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +116 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +661 -42
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +63 -2
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +430 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +45 -7
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +142 -237
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +115 -12
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
|
@@ -98,6 +98,7 @@ function buildPageBreakDecorationsFromProps(
|
|
|
98
98
|
footerBandPx?: number;
|
|
99
99
|
interGapPx?: number;
|
|
100
100
|
} = {},
|
|
101
|
+
surfaceBlocks?: readonly import("../../api/public-types.ts").SurfaceBlockSnapshot[],
|
|
101
102
|
): ReturnType<typeof buildPageBreakDecorations> {
|
|
102
103
|
if (!facet || !isMainStory) return [];
|
|
103
104
|
if (typeof facet.getRenderFrame !== "function") return [];
|
|
@@ -133,6 +134,41 @@ function buildPageBreakDecorationsFromProps(
|
|
|
133
134
|
})
|
|
134
135
|
: undefined;
|
|
135
136
|
|
|
137
|
+
// L7 Phase 2 Task 2.2.4a — compute per-page block-index ranges from the
|
|
138
|
+
// render frame's page offsets + the surface blocks list. Each block has a
|
|
139
|
+
// `from`/`to` offset; we find the first and last block whose offset range
|
|
140
|
+
// falls within each page's [startOffset, nextPage.startOffset) window.
|
|
141
|
+
// This map is passed into `buildPageBreakDecorations` so the chrome widgets
|
|
142
|
+
// carry `data-page-first-block-index` / `data-page-last-block-index`
|
|
143
|
+
// attributes needed by `useVisibleBlockRange`.
|
|
144
|
+
let blockIndexRangeByPageIndex: Map<number, { first: number; last: number }> | undefined;
|
|
145
|
+
if (surfaceBlocks && surfaceBlocks.length > 0 && frame.pages.length > 0) {
|
|
146
|
+
blockIndexRangeByPageIndex = new Map();
|
|
147
|
+
for (let pi = 0; pi < frame.pages.length; pi++) {
|
|
148
|
+
const page = frame.pages[pi]!;
|
|
149
|
+
if (page.page.isBlankFiller) continue;
|
|
150
|
+
const pageStart = page.page.startOffset;
|
|
151
|
+
const pageEnd =
|
|
152
|
+
pi + 1 < frame.pages.length
|
|
153
|
+
? frame.pages[pi + 1]!.page.startOffset
|
|
154
|
+
: Infinity;
|
|
155
|
+
let first = -1;
|
|
156
|
+
let last = -1;
|
|
157
|
+
for (let bi = 0; bi < surfaceBlocks.length; bi++) {
|
|
158
|
+
const block = surfaceBlocks[bi]!;
|
|
159
|
+
const blockFrom = block.from; // from is required on all SurfaceBlockSnapshot variants
|
|
160
|
+
// Block belongs to this page if its start falls within the page's offset window.
|
|
161
|
+
if (blockFrom >= pageStart && blockFrom < pageEnd) {
|
|
162
|
+
if (first === -1) first = bi;
|
|
163
|
+
last = bi;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (first !== -1) {
|
|
167
|
+
blockIndexRangeByPageIndex.set(page.page.pageIndex, { first, last });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
return buildPageBreakDecorations({
|
|
137
173
|
graph: fakeGraph as never,
|
|
138
174
|
posture,
|
|
@@ -142,6 +178,7 @@ function buildPageBreakDecorationsFromProps(
|
|
|
142
178
|
runtimeToPmOffset: (offset) => positionMap.runtimeToPm(offset),
|
|
143
179
|
headerPreviewByPageId: previews?.headerPreviewByPageId,
|
|
144
180
|
footerPreviewByPageId: previews?.footerPreviewByPageId,
|
|
181
|
+
blockIndexRangeByPageIndex,
|
|
145
182
|
});
|
|
146
183
|
}
|
|
147
184
|
|
|
@@ -188,6 +225,11 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
188
225
|
onUndo?: () => void;
|
|
189
226
|
onRedo?: () => void;
|
|
190
227
|
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
228
|
+
onPasteApplied?: (meta: {
|
|
229
|
+
segmentCount: number;
|
|
230
|
+
charCount: number;
|
|
231
|
+
source: "paste" | "drop";
|
|
232
|
+
}) => void;
|
|
191
233
|
onCommentActivated?: (commentId: string) => void;
|
|
192
234
|
onRevisionActivated?: (revisionId: string) => void;
|
|
193
235
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
@@ -321,6 +363,9 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
321
363
|
onBlockedInput: (command, message) => {
|
|
322
364
|
props.onBlockedInput?.(command, message);
|
|
323
365
|
},
|
|
366
|
+
onPasteApplied: (meta) => {
|
|
367
|
+
props.onPasteApplied?.(meta);
|
|
368
|
+
},
|
|
324
369
|
onSelectionChange: (sel) => {
|
|
325
370
|
pendingSelectionProbeRef.current = startPerfProbe("selection");
|
|
326
371
|
props.onSelectionChange?.(
|
|
@@ -448,6 +493,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
448
493
|
onUndo: () => callbacksRef.current?.onUndo(),
|
|
449
494
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
450
495
|
onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
|
|
496
|
+
onPasteApplied: (meta) => callbacksRef.current?.onPasteApplied?.(meta),
|
|
451
497
|
onCompositionChange: (composing) => {
|
|
452
498
|
sessionRef.current?.setComposing(composing);
|
|
453
499
|
},
|
|
@@ -503,6 +549,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
503
549
|
footerBandPx: props.pageChromeFooterBandPx,
|
|
504
550
|
interGapPx: props.pageChromeInterGapPx,
|
|
505
551
|
},
|
|
552
|
+
snapshot.surface?.blocks,
|
|
506
553
|
);
|
|
507
554
|
const decorations = pageBreakDecos.length > 0
|
|
508
555
|
? DecorationSet.create(view.state.doc, [
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -11,7 +11,11 @@ export { renderTwCaret } from "./editor-surface/tw-caret";
|
|
|
11
11
|
|
|
12
12
|
// Review rail
|
|
13
13
|
export { TwReviewRail, type TwReviewRailProps, type ReviewRailTab } from "./review/tw-review-rail";
|
|
14
|
-
export { TwCommentSidebar } from "./review/tw-comment-sidebar";
|
|
14
|
+
export { TwCommentSidebar, type TwCommentSidebarProps } from "./review/tw-comment-sidebar";
|
|
15
|
+
export {
|
|
16
|
+
CommentMarkdownRenderer,
|
|
17
|
+
type CommentMarkdownRendererProps,
|
|
18
|
+
} from "./review/comment-markdown-renderer";
|
|
15
19
|
export { TwRevisionSidebar } from "./review/tw-revision-sidebar";
|
|
16
20
|
export { TwHealthPanel } from "./review/tw-health-panel";
|
|
17
21
|
export { TwWorkflowTab, type TwWorkflowTabProps } from "./review/tw-workflow-tab";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// TwEndnoteArea (P8.7)
|
|
8
|
+
//
|
|
9
|
+
// Read-only area for document-end endnote placement. Unlike footnotes, which
|
|
10
|
+
// the chrome layer positions per-page via an absolute rectangle, endnotes in
|
|
11
|
+
// the default mode sit at the end of the document (Word's `w:pos="docEnd"`
|
|
12
|
+
// behavior). This component is mounted as a sibling of the chrome layer
|
|
13
|
+
// after the last page rect — not as an absolutely positioned chrome overlay.
|
|
14
|
+
//
|
|
15
|
+
// It renders:
|
|
16
|
+
// 1. A default 1px separator (1/3 of the parent width) along the top edge,
|
|
17
|
+
// and
|
|
18
|
+
// 2. The ordered endnote bodies via `TwRegionBlockRenderer` (P8.4).
|
|
19
|
+
//
|
|
20
|
+
// When `blocks` is empty the component returns `null` so the document-end
|
|
21
|
+
// slot stays visually absent.
|
|
22
|
+
//
|
|
23
|
+
// Per-section endnote placement (`w:endnotePr/w:pos` other than `docEnd`)
|
|
24
|
+
// is a follow-up — tracked with the P8.b polish pass.
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface TwEndnoteAreaProps {
|
|
28
|
+
blocks: readonly SurfaceBlockSnapshot[];
|
|
29
|
+
"data-testid"?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const TwEndnoteArea: React.FC<TwEndnoteAreaProps> = ({
|
|
33
|
+
blocks,
|
|
34
|
+
"data-testid": testId,
|
|
35
|
+
}) => {
|
|
36
|
+
if (blocks.length === 0) return null;
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-endnote-area
|
|
40
|
+
data-testid={testId}
|
|
41
|
+
style={{ marginTop: "24pt" }}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
data-endnote-separator
|
|
45
|
+
style={{
|
|
46
|
+
width: "33%",
|
|
47
|
+
height: "1px",
|
|
48
|
+
backgroundColor: "currentColor",
|
|
49
|
+
marginBottom: "8pt",
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
<TwRegionBlockRenderer blocks={blocks} />
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default TwEndnoteArea;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// TwFootnoteArea (P8.6)
|
|
8
|
+
//
|
|
9
|
+
// Read-only area mounted at an absolute pixel rectangle inside a per-page
|
|
10
|
+
// chrome overlay. Renders:
|
|
11
|
+
// 1. A default 1px separator (1/3 of the parent width) along the top edge,
|
|
12
|
+
// and
|
|
13
|
+
// 2. The ordered footnote bodies via `TwRegionBlockRenderer` (P8.4).
|
|
14
|
+
//
|
|
15
|
+
// The chrome layer (P8.8) computes the `topPx / leftPx / widthPx / heightPx`
|
|
16
|
+
// rectangle from the page graph's `regions.footnotes` geometry and mounts
|
|
17
|
+
// this component above the footer band.
|
|
18
|
+
//
|
|
19
|
+
// The default separator matches Word's implicit footnote separator; reading
|
|
20
|
+
// the `w:separator` / `w:continuationSeparator` parts from the footnotes
|
|
21
|
+
// package is deferred to P8.b polish. No active-slot / portal plumbing in
|
|
22
|
+
// this pass — the P8 plan only reserves portal slots for header / footer.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface TwFootnoteAreaProps {
|
|
26
|
+
pageIndex: number;
|
|
27
|
+
blocks: readonly SurfaceBlockSnapshot[];
|
|
28
|
+
topPx: number;
|
|
29
|
+
leftPx: number;
|
|
30
|
+
widthPx: number;
|
|
31
|
+
heightPx: number;
|
|
32
|
+
"data-testid"?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TwFootnoteArea: React.FC<TwFootnoteAreaProps> = ({
|
|
36
|
+
pageIndex,
|
|
37
|
+
blocks,
|
|
38
|
+
topPx,
|
|
39
|
+
leftPx,
|
|
40
|
+
widthPx,
|
|
41
|
+
heightPx,
|
|
42
|
+
"data-testid": testId,
|
|
43
|
+
}) => {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
data-footnote-area
|
|
47
|
+
data-page-index={pageIndex}
|
|
48
|
+
data-testid={testId}
|
|
49
|
+
style={{
|
|
50
|
+
position: "absolute",
|
|
51
|
+
top: `${topPx}px`,
|
|
52
|
+
left: `${leftPx}px`,
|
|
53
|
+
width: `${widthPx}px`,
|
|
54
|
+
height: `${heightPx}px`,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<div
|
|
58
|
+
data-footnote-separator
|
|
59
|
+
style={{
|
|
60
|
+
width: `${Math.round(widthPx / 3)}px`,
|
|
61
|
+
height: "1px",
|
|
62
|
+
backgroundColor: "currentColor",
|
|
63
|
+
marginBottom: "4pt",
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
<TwRegionBlockRenderer blocks={blocks} />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default TwFootnoteArea;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// TwPageFooterBand (P8.5)
|
|
8
|
+
//
|
|
9
|
+
// Symmetric counterpart to `TwPageHeaderBand` (see its header for design
|
|
10
|
+
// context). The footer band positions itself via `bottom` rather than
|
|
11
|
+
// `top` so the chrome layer can measure the footer margin up from the
|
|
12
|
+
// page rect's bottom edge and keep footer content pinned correctly when
|
|
13
|
+
// page size / margin changes.
|
|
14
|
+
//
|
|
15
|
+
// When `isActiveSlot` is true, the band emits a single `data-pm-portal-slot`
|
|
16
|
+
// div tagged `data-page-band-slot="footer"` — the chrome layer portals the
|
|
17
|
+
// PM surface into this target in P8.10. Otherwise it renders the footer
|
|
18
|
+
// story's `SurfaceBlockSnapshot[]` through `TwRegionBlockRenderer` (P8.4).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface TwPageFooterBandProps {
|
|
22
|
+
pageIndex: number;
|
|
23
|
+
blocks: readonly SurfaceBlockSnapshot[];
|
|
24
|
+
bandHeightPx: number;
|
|
25
|
+
bottomPx: number;
|
|
26
|
+
leftPx: number;
|
|
27
|
+
widthPx: number;
|
|
28
|
+
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
29
|
+
isActiveSlot: boolean;
|
|
30
|
+
onClick: () => void;
|
|
31
|
+
"data-testid"?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = ({
|
|
35
|
+
pageIndex,
|
|
36
|
+
blocks,
|
|
37
|
+
bandHeightPx,
|
|
38
|
+
bottomPx,
|
|
39
|
+
leftPx,
|
|
40
|
+
widthPx,
|
|
41
|
+
isActiveSlot,
|
|
42
|
+
onClick,
|
|
43
|
+
"data-testid": testId,
|
|
44
|
+
}) => {
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
data-page-band="footer"
|
|
48
|
+
data-page-index={pageIndex}
|
|
49
|
+
data-testid={testId}
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
style={{
|
|
52
|
+
position: "absolute",
|
|
53
|
+
bottom: `${bottomPx}px`,
|
|
54
|
+
left: `${leftPx}px`,
|
|
55
|
+
width: `${widthPx}px`,
|
|
56
|
+
height: `${bandHeightPx}px`,
|
|
57
|
+
cursor: "pointer",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{isActiveSlot ? (
|
|
61
|
+
<div
|
|
62
|
+
data-pm-portal-slot
|
|
63
|
+
data-page-band-slot="footer"
|
|
64
|
+
style={{ width: "100%", height: "100%" }}
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<TwRegionBlockRenderer blocks={blocks} />
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default TwPageFooterBand;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
4
|
+
import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// TwPageHeaderBand (P8.5)
|
|
8
|
+
//
|
|
9
|
+
// Read-only band mounted at an absolute pixel position inside a per-page
|
|
10
|
+
// chrome overlay. The band either:
|
|
11
|
+
// - Renders the header story's `SurfaceBlockSnapshot[]` through
|
|
12
|
+
// `TwRegionBlockRenderer` (P8.4) as pure presentational DOM, or
|
|
13
|
+
// - Emits a single `data-pm-portal-slot` div when the chrome layer has
|
|
14
|
+
// promoted this band to the active story slot (P8.10 wires the PM
|
|
15
|
+
// surface into this target via React portal).
|
|
16
|
+
//
|
|
17
|
+
// Clicks on the band bubble to the chrome layer's `openStory` dispatch
|
|
18
|
+
// (wired in P8.8 / P8.10) so legal reviewers can promote a header into
|
|
19
|
+
// the active editing surface.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface TwPageHeaderBandProps {
|
|
23
|
+
pageIndex: number;
|
|
24
|
+
blocks: readonly SurfaceBlockSnapshot[];
|
|
25
|
+
bandHeightPx: number;
|
|
26
|
+
topPx: number;
|
|
27
|
+
leftPx: number;
|
|
28
|
+
widthPx: number;
|
|
29
|
+
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
30
|
+
isActiveSlot: boolean;
|
|
31
|
+
onClick: () => void;
|
|
32
|
+
"data-testid"?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = ({
|
|
36
|
+
pageIndex,
|
|
37
|
+
blocks,
|
|
38
|
+
bandHeightPx,
|
|
39
|
+
topPx,
|
|
40
|
+
leftPx,
|
|
41
|
+
widthPx,
|
|
42
|
+
isActiveSlot,
|
|
43
|
+
onClick,
|
|
44
|
+
"data-testid": testId,
|
|
45
|
+
}) => {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
data-page-band="header"
|
|
49
|
+
data-page-index={pageIndex}
|
|
50
|
+
data-testid={testId}
|
|
51
|
+
onClick={onClick}
|
|
52
|
+
style={{
|
|
53
|
+
position: "absolute",
|
|
54
|
+
top: `${topPx}px`,
|
|
55
|
+
left: `${leftPx}px`,
|
|
56
|
+
width: `${widthPx}px`,
|
|
57
|
+
height: `${bandHeightPx}px`,
|
|
58
|
+
cursor: "pointer",
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{isActiveSlot ? (
|
|
62
|
+
<div
|
|
63
|
+
data-pm-portal-slot
|
|
64
|
+
data-page-band-slot="header"
|
|
65
|
+
style={{ width: "100%", height: "100%" }}
|
|
66
|
+
/>
|
|
67
|
+
) : (
|
|
68
|
+
<TwRegionBlockRenderer blocks={blocks} />
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default TwPageHeaderBand;
|