@beyondwork/docx-react-component 1.0.52 → 1.0.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -40
- package/src/api/public-types.ts +67 -7
- package/src/io/chart-preview-resolver.ts +41 -0
- package/src/io/docx-session.ts +217 -23
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +88 -8
- package/src/runtime/document-runtime.ts +182 -9
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +97 -2
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +70 -1
- package/src/runtime/prerender/cache-envelope.ts +30 -0
- package/src/runtime/prerender/customxml-cache.ts +17 -3
- package/src/runtime/prerender/prerender-document.ts +17 -1
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/render/render-kernel.ts +67 -19
- package/src/runtime/surface-projection.ts +28 -0
- package/src/runtime/table-schema.ts +27 -0
- package/src/runtime/table-style-resolver.ts +51 -0
- package/src/ui/WordReviewEditor.tsx +6 -3
- package/src/ui/editor-runtime-boundary.ts +39 -2
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
- package/src/ui-tailwind/theme/editor-theme.css +275 -46
- package/src/ui-tailwind/theme/tokens.css +345 -0
- package/src/ui-tailwind/theme/tokens.ts +313 -0
- package/src/ui-tailwind/theme/use-density.ts +60 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwPageChromeEntry — memo-wrapped per-page chrome subtree.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `TwPageStackChromeLayer`'s per-page `.map()` so that
|
|
5
|
+
* `React.memo` can skip re-renders for pages whose `page` reference is
|
|
6
|
+
* stable (guaranteed post-D1+B2 for pages past the splice-convergence
|
|
7
|
+
* point). Rect values are compared structurally (topPx / bottomPx /
|
|
8
|
+
* pageId) because `resolvePageOverlayRects` allocates new objects each
|
|
9
|
+
* measurement pass even when positions haven't changed.
|
|
10
|
+
*
|
|
11
|
+
* Block arrays and click-handler callbacks are memoized on
|
|
12
|
+
* `(page, renderFrameRevision)` so they don't create new references
|
|
13
|
+
* when the page is unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React from "react";
|
|
17
|
+
import type {
|
|
18
|
+
EditorStoryTarget,
|
|
19
|
+
PublicPageNode,
|
|
20
|
+
SurfaceTableRowSnapshot,
|
|
21
|
+
WordReviewEditorLayoutFacet,
|
|
22
|
+
} from "../../api/public-types.ts";
|
|
23
|
+
import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
24
|
+
import { TwTableContinuationHeader } from "../chrome-overlay/tw-table-continuation-header.tsx";
|
|
25
|
+
import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
|
|
26
|
+
import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
|
|
27
|
+
import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
|
|
28
|
+
import { TwFootnoteArea } from "./tw-footnote-area.tsx";
|
|
29
|
+
|
|
30
|
+
export interface TwPageChromeEntryProps {
|
|
31
|
+
rect: PageOverlayRect;
|
|
32
|
+
pageIndex: number;
|
|
33
|
+
page: PublicPageNode;
|
|
34
|
+
facet: WordReviewEditorLayoutFacet;
|
|
35
|
+
activeStory: EditorStoryTarget;
|
|
36
|
+
onOpenStory?: (target: EditorStoryTarget) => void;
|
|
37
|
+
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
38
|
+
renderFrameRevision: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function TwPageChromeEntryInner({
|
|
42
|
+
rect,
|
|
43
|
+
pageIndex,
|
|
44
|
+
page,
|
|
45
|
+
facet,
|
|
46
|
+
activeStory,
|
|
47
|
+
onOpenStory,
|
|
48
|
+
visiblePageIndexRange,
|
|
49
|
+
renderFrameRevision,
|
|
50
|
+
}: TwPageChromeEntryProps): React.ReactElement {
|
|
51
|
+
const layout = page.layout;
|
|
52
|
+
const headerStory = page.stories.header;
|
|
53
|
+
const footerStory = page.stories.footer;
|
|
54
|
+
const headerRegion = page.regions.header;
|
|
55
|
+
const footerRegion = page.regions.footer;
|
|
56
|
+
const footnoteRegion = page.regions.footnotes?.[0];
|
|
57
|
+
|
|
58
|
+
// All hooks must be called unconditionally before any early return.
|
|
59
|
+
const headerBlocks = React.useMemo(
|
|
60
|
+
() =>
|
|
61
|
+
headerStory
|
|
62
|
+
? facet.getStoryBlocksForRegion(pageIndex, "header").map((b) => b.blockSnapshot)
|
|
63
|
+
: [],
|
|
64
|
+
// `page` as dependency captures per-page reference stability (D1+B2):
|
|
65
|
+
// when page ref is stable, the memo doesn't re-run even if revision ticks.
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
[facet, pageIndex, page, renderFrameRevision],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const footerBlocks = React.useMemo(
|
|
71
|
+
() =>
|
|
72
|
+
footerStory
|
|
73
|
+
? facet.getStoryBlocksForRegion(pageIndex, "footer").map((b) => b.blockSnapshot)
|
|
74
|
+
: [],
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
[facet, pageIndex, page, renderFrameRevision],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const footnoteBlocks = React.useMemo(
|
|
80
|
+
() =>
|
|
81
|
+
footnoteRegion
|
|
82
|
+
? facet
|
|
83
|
+
.getStoryBlocksForRegion(pageIndex, "footnote-area")
|
|
84
|
+
.map((b) => b.blockSnapshot)
|
|
85
|
+
: [],
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
[facet, pageIndex, page, renderFrameRevision],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Continuation table header entries: only non-empty for continuation pages
|
|
91
|
+
// of multi-page tables (isContinuationPage && repeatedHeaderRows.length > 0).
|
|
92
|
+
const continuationTableEntries = React.useMemo((): Array<{
|
|
93
|
+
blockId: string;
|
|
94
|
+
headerRows: readonly SurfaceTableRowSnapshot[];
|
|
95
|
+
}> => {
|
|
96
|
+
const bodyBlocks = facet.getStoryBlocksForRegion(pageIndex, "body");
|
|
97
|
+
const entries: Array<{
|
|
98
|
+
blockId: string;
|
|
99
|
+
headerRows: readonly SurfaceTableRowSnapshot[];
|
|
100
|
+
}> = [];
|
|
101
|
+
for (const { blockSnapshot } of bodyBlocks) {
|
|
102
|
+
if (blockSnapshot.kind !== "table") continue;
|
|
103
|
+
const plan = facet.getTableRenderPlan(blockSnapshot.blockId, pageIndex);
|
|
104
|
+
if (!plan || plan.repeatedHeaderRows.length === 0) continue;
|
|
105
|
+
const headerRows = plan.repeatedHeaderRows
|
|
106
|
+
.map((ref) => blockSnapshot.rows[ref.sourceRowIndex])
|
|
107
|
+
.filter((r): r is SurfaceTableRowSnapshot => r !== undefined);
|
|
108
|
+
if (headerRows.length > 0) {
|
|
109
|
+
entries.push({ blockId: blockSnapshot.blockId, headerRows });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
+
}, [facet, pageIndex, page, renderFrameRevision]);
|
|
115
|
+
|
|
116
|
+
const handleHeaderClick = React.useCallback(
|
|
117
|
+
() => headerStory && onOpenStory?.(headerStory),
|
|
118
|
+
[onOpenStory, headerStory],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const handleFooterClick = React.useCallback(
|
|
122
|
+
() => footerStory && onOpenStory?.(footerStory),
|
|
123
|
+
[onOpenStory, footerStory],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const frameHeightPx = rect.bottomPx - rect.topPx;
|
|
127
|
+
|
|
128
|
+
// Viewport cull — lightweight placeholder outside the visible range.
|
|
129
|
+
const isCulled =
|
|
130
|
+
visiblePageIndexRange !== null &&
|
|
131
|
+
visiblePageIndexRange !== undefined &&
|
|
132
|
+
(pageIndex < visiblePageIndexRange.start || pageIndex >= visiblePageIndexRange.end);
|
|
133
|
+
|
|
134
|
+
if (isCulled) {
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
data-page-chrome-frame=""
|
|
138
|
+
data-page-index={pageIndex}
|
|
139
|
+
data-page-chrome-culled=""
|
|
140
|
+
style={{
|
|
141
|
+
position: "absolute",
|
|
142
|
+
top: `${rect.topPx}px`,
|
|
143
|
+
left: 0,
|
|
144
|
+
width: "100%",
|
|
145
|
+
height: `${frameHeightPx}px`,
|
|
146
|
+
pointerEvents: "none",
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const px = (twips: number): number => twips * FRAME_PX_PER_TWIP_AT_96DPI;
|
|
153
|
+
const bandWidthPx = px(layout.pageWidth - layout.marginLeft - layout.marginRight);
|
|
154
|
+
const bandLeftPx = px(layout.marginLeft);
|
|
155
|
+
|
|
156
|
+
// L6d.U1 — 1-based section label for the active-band ribbon. The
|
|
157
|
+
// `sectionIndex` on the page is 0-based; reviewers expect "Section 1"
|
|
158
|
+
// not "Section 0", matching Word's section-break dialog numbering.
|
|
159
|
+
const sectionNumber = (page.sectionIndex ?? 0) + 1;
|
|
160
|
+
const headerSectionLabel = `Header — Section ${sectionNumber}`;
|
|
161
|
+
const footerSectionLabel = `Footer — Section ${sectionNumber}`;
|
|
162
|
+
const headerActive = headerStory && isActiveStoryMatch(activeStory, headerStory);
|
|
163
|
+
const footerActive = footerStory && isActiveStoryMatch(activeStory, footerStory);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
data-page-chrome-frame=""
|
|
168
|
+
data-page-index={pageIndex}
|
|
169
|
+
style={{
|
|
170
|
+
position: "absolute",
|
|
171
|
+
top: `${rect.topPx}px`,
|
|
172
|
+
left: 0,
|
|
173
|
+
width: "100%",
|
|
174
|
+
height: `${frameHeightPx}px`,
|
|
175
|
+
pointerEvents: "none",
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{headerRegion && headerStory ? (
|
|
179
|
+
<TwPageHeaderBand
|
|
180
|
+
pageIndex={pageIndex}
|
|
181
|
+
blocks={headerBlocks}
|
|
182
|
+
topPx={px(layout.headerMargin ?? 720)}
|
|
183
|
+
leftPx={bandLeftPx}
|
|
184
|
+
widthPx={bandWidthPx}
|
|
185
|
+
bandHeightPx={px(headerRegion.heightTwips)}
|
|
186
|
+
isActiveSlot={Boolean(headerActive)}
|
|
187
|
+
sectionLabel={headerActive ? headerSectionLabel : undefined}
|
|
188
|
+
onClick={handleHeaderClick}
|
|
189
|
+
/>
|
|
190
|
+
) : null}
|
|
191
|
+
{footerRegion && footerStory ? (
|
|
192
|
+
<TwPageFooterBand
|
|
193
|
+
pageIndex={pageIndex}
|
|
194
|
+
blocks={footerBlocks}
|
|
195
|
+
bottomPx={px(layout.footerMargin ?? 720)}
|
|
196
|
+
leftPx={bandLeftPx}
|
|
197
|
+
widthPx={bandWidthPx}
|
|
198
|
+
bandHeightPx={px(footerRegion.heightTwips)}
|
|
199
|
+
isActiveSlot={Boolean(footerActive)}
|
|
200
|
+
sectionLabel={footerActive ? footerSectionLabel : undefined}
|
|
201
|
+
onClick={handleFooterClick}
|
|
202
|
+
/>
|
|
203
|
+
) : null}
|
|
204
|
+
{footnoteRegion ? (
|
|
205
|
+
<TwFootnoteArea
|
|
206
|
+
pageIndex={pageIndex}
|
|
207
|
+
blocks={footnoteBlocks}
|
|
208
|
+
topPx={px(footnoteRegion.originTwips - layout.marginTop)}
|
|
209
|
+
leftPx={bandLeftPx}
|
|
210
|
+
widthPx={px(footnoteRegion.widthTwips)}
|
|
211
|
+
heightPx={px(footnoteRegion.heightTwips)}
|
|
212
|
+
/>
|
|
213
|
+
) : null}
|
|
214
|
+
{continuationTableEntries.map(({ blockId, headerRows }) => (
|
|
215
|
+
<TwTableContinuationHeader
|
|
216
|
+
key={`table-cont-${blockId}`}
|
|
217
|
+
blockId={blockId}
|
|
218
|
+
pageIndex={pageIndex}
|
|
219
|
+
facet={facet}
|
|
220
|
+
headerRows={headerRows}
|
|
221
|
+
pxPerTwip={FRAME_PX_PER_TWIP_AT_96DPI}
|
|
222
|
+
bodyOriginTopPx={px(layout.marginTop)}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function propsAreEqual(
|
|
230
|
+
prev: TwPageChromeEntryProps,
|
|
231
|
+
next: TwPageChromeEntryProps,
|
|
232
|
+
): boolean {
|
|
233
|
+
return (
|
|
234
|
+
prev.pageIndex === next.pageIndex &&
|
|
235
|
+
prev.page === next.page &&
|
|
236
|
+
prev.facet === next.facet &&
|
|
237
|
+
prev.activeStory === next.activeStory &&
|
|
238
|
+
prev.onOpenStory === next.onOpenStory &&
|
|
239
|
+
prev.visiblePageIndexRange === next.visiblePageIndexRange &&
|
|
240
|
+
prev.renderFrameRevision === next.renderFrameRevision &&
|
|
241
|
+
prev.rect.topPx === next.rect.topPx &&
|
|
242
|
+
prev.rect.bottomPx === next.rect.bottomPx &&
|
|
243
|
+
prev.rect.pageId === next.rect.pageId
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const TwPageChromeEntry = React.memo(TwPageChromeEntryInner, propsAreEqual);
|
|
248
|
+
|
|
249
|
+
function isActiveStoryMatch(
|
|
250
|
+
active: EditorStoryTarget,
|
|
251
|
+
candidate: EditorStoryTarget,
|
|
252
|
+
): boolean {
|
|
253
|
+
if (active.kind !== candidate.kind) return false;
|
|
254
|
+
if (active.kind === "main" || candidate.kind === "main") {
|
|
255
|
+
return active.kind === candidate.kind;
|
|
256
|
+
}
|
|
257
|
+
if (active.kind === "footnote" && candidate.kind === "footnote") {
|
|
258
|
+
return active.noteId === candidate.noteId;
|
|
259
|
+
}
|
|
260
|
+
if (active.kind === "endnote" && candidate.kind === "endnote") {
|
|
261
|
+
return active.noteId === candidate.noteId;
|
|
262
|
+
}
|
|
263
|
+
if (
|
|
264
|
+
(active.kind === "header" && candidate.kind === "header") ||
|
|
265
|
+
(active.kind === "footer" && candidate.kind === "footer")
|
|
266
|
+
) {
|
|
267
|
+
return (
|
|
268
|
+
active.relationshipId === candidate.relationshipId &&
|
|
269
|
+
active.variant === candidate.variant &&
|
|
270
|
+
active.sectionIndex === candidate.sectionIndex
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
@@ -27,11 +27,16 @@ export interface TwPageFooterBandProps {
|
|
|
27
27
|
widthPx: number;
|
|
28
28
|
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
29
29
|
isActiveSlot: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Lane 6d.U1 — section label for the active-band ribbon (e.g. "Footer — Section 1").
|
|
32
|
+
* Only rendered when `isActiveSlot` is true.
|
|
33
|
+
*/
|
|
34
|
+
sectionLabel?: string;
|
|
30
35
|
onClick: () => void;
|
|
31
36
|
"data-testid"?: string;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = ({
|
|
39
|
+
export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
35
40
|
pageIndex,
|
|
36
41
|
blocks,
|
|
37
42
|
bandHeightPx,
|
|
@@ -39,13 +44,16 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = ({
|
|
|
39
44
|
leftPx,
|
|
40
45
|
widthPx,
|
|
41
46
|
isActiveSlot,
|
|
47
|
+
sectionLabel,
|
|
42
48
|
onClick,
|
|
43
49
|
"data-testid": testId,
|
|
44
50
|
}) => {
|
|
45
51
|
return (
|
|
46
52
|
<div
|
|
53
|
+
className="wre-page-band"
|
|
47
54
|
data-page-band="footer"
|
|
48
55
|
data-page-index={pageIndex}
|
|
56
|
+
data-active={isActiveSlot ? "true" : undefined}
|
|
49
57
|
data-testid={testId}
|
|
50
58
|
onClick={onClick}
|
|
51
59
|
style={{
|
|
@@ -57,6 +65,11 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = ({
|
|
|
57
65
|
cursor: "pointer",
|
|
58
66
|
}}
|
|
59
67
|
>
|
|
68
|
+
{isActiveSlot && sectionLabel ? (
|
|
69
|
+
<span className="wre-page-band__label" data-kind="page-band-label">
|
|
70
|
+
{sectionLabel}
|
|
71
|
+
</span>
|
|
72
|
+
) : null}
|
|
60
73
|
{isActiveSlot ? (
|
|
61
74
|
<div
|
|
62
75
|
data-pm-portal-slot
|
|
@@ -68,6 +81,6 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = ({
|
|
|
68
81
|
)}
|
|
69
82
|
</div>
|
|
70
83
|
);
|
|
71
|
-
};
|
|
84
|
+
});
|
|
72
85
|
|
|
73
86
|
export default TwPageFooterBand;
|
|
@@ -28,11 +28,16 @@ export interface TwPageHeaderBandProps {
|
|
|
28
28
|
widthPx: number;
|
|
29
29
|
/** True when this band is the active-story slot. Renders portal target instead of read-only DOM. */
|
|
30
30
|
isActiveSlot: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Lane 6d.U1 — section label for the active-band ribbon (e.g. "Header — Section 1").
|
|
33
|
+
* Only rendered when `isActiveSlot` is true.
|
|
34
|
+
*/
|
|
35
|
+
sectionLabel?: string;
|
|
31
36
|
onClick: () => void;
|
|
32
37
|
"data-testid"?: string;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = ({
|
|
40
|
+
export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
36
41
|
pageIndex,
|
|
37
42
|
blocks,
|
|
38
43
|
bandHeightPx,
|
|
@@ -40,13 +45,16 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = ({
|
|
|
40
45
|
leftPx,
|
|
41
46
|
widthPx,
|
|
42
47
|
isActiveSlot,
|
|
48
|
+
sectionLabel,
|
|
43
49
|
onClick,
|
|
44
50
|
"data-testid": testId,
|
|
45
51
|
}) => {
|
|
46
52
|
return (
|
|
47
53
|
<div
|
|
54
|
+
className="wre-page-band"
|
|
48
55
|
data-page-band="header"
|
|
49
56
|
data-page-index={pageIndex}
|
|
57
|
+
data-active={isActiveSlot ? "true" : undefined}
|
|
50
58
|
data-testid={testId}
|
|
51
59
|
onClick={onClick}
|
|
52
60
|
style={{
|
|
@@ -58,6 +66,11 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = ({
|
|
|
58
66
|
cursor: "pointer",
|
|
59
67
|
}}
|
|
60
68
|
>
|
|
69
|
+
{isActiveSlot && sectionLabel ? (
|
|
70
|
+
<span className="wre-page-band__label" data-kind="page-band-label">
|
|
71
|
+
{sectionLabel}
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
61
74
|
{isActiveSlot ? (
|
|
62
75
|
<div
|
|
63
76
|
data-pm-portal-slot
|
|
@@ -69,6 +82,6 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = ({
|
|
|
69
82
|
)}
|
|
70
83
|
</div>
|
|
71
84
|
);
|
|
72
|
-
};
|
|
85
|
+
});
|
|
73
86
|
|
|
74
87
|
export default TwPageHeaderBand;
|
|
@@ -77,11 +77,8 @@ import {
|
|
|
77
77
|
resolvePageOverlayRects,
|
|
78
78
|
type PageOverlayRect,
|
|
79
79
|
} from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
80
|
-
import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
|
|
81
|
-
import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
|
|
82
|
-
import { TwFootnoteArea } from "./tw-footnote-area.tsx";
|
|
83
80
|
import { TwEndnoteArea } from "./tw-endnote-area.tsx";
|
|
84
|
-
import {
|
|
81
|
+
import { TwPageChromeEntry } from "./tw-page-chrome-entry.tsx";
|
|
85
82
|
|
|
86
83
|
/**
|
|
87
84
|
* Minimal structural type for the PM `EditorView` handle consumed by
|
|
@@ -364,120 +361,31 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
364
361
|
ref={overlayRootRef}
|
|
365
362
|
data-page-stack-chrome-layer=""
|
|
366
363
|
data-testid={testId ?? "page-stack-chrome-layer"}
|
|
364
|
+
// L6d.U1 — expose the active-story kind so the CSS can dim the
|
|
365
|
+
// body when an H/F story is being edited and un-dim again when
|
|
366
|
+
// the active story is the main body.
|
|
367
|
+
data-story-active={
|
|
368
|
+
activeStory.kind === "header" || activeStory.kind === "footer"
|
|
369
|
+
? activeStory.kind
|
|
370
|
+
: undefined
|
|
371
|
+
}
|
|
367
372
|
style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
|
|
368
373
|
>
|
|
369
374
|
{rects.map((rect, pageIndex) => {
|
|
370
375
|
const page = facet.getPage(pageIndex);
|
|
371
376
|
if (!page) return null;
|
|
372
|
-
|
|
373
|
-
// L7 Phase 2.8 — viewport cull. Pages outside the visible range
|
|
374
|
-
// (plus overscan) render an empty frame wrapper: the
|
|
375
|
-
// `data-page-chrome-frame` + `data-page-index` attributes stay
|
|
376
|
-
// put so downstream DOM queries (portal-slot, test hooks) keep
|
|
377
|
-
// working, but the four heavy React subtrees below do not mount.
|
|
378
|
-
const frameHeightPxForCull = rect.bottomPx - rect.topPx;
|
|
379
|
-
if (
|
|
380
|
-
visiblePageIndexRange &&
|
|
381
|
-
(pageIndex < visiblePageIndexRange.start ||
|
|
382
|
-
pageIndex >= visiblePageIndexRange.end)
|
|
383
|
-
) {
|
|
384
|
-
return (
|
|
385
|
-
<div
|
|
386
|
-
key={`page-chrome-${rect.pageId}`}
|
|
387
|
-
data-page-chrome-frame=""
|
|
388
|
-
data-page-index={pageIndex}
|
|
389
|
-
data-page-chrome-culled=""
|
|
390
|
-
style={{
|
|
391
|
-
position: "absolute",
|
|
392
|
-
top: `${rect.topPx}px`,
|
|
393
|
-
left: 0,
|
|
394
|
-
width: "100%",
|
|
395
|
-
height: `${frameHeightPxForCull}px`,
|
|
396
|
-
pointerEvents: "none",
|
|
397
|
-
}}
|
|
398
|
-
/>
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const layout = page.layout;
|
|
403
|
-
const headerStory = page.stories.header;
|
|
404
|
-
const footerStory = page.stories.footer;
|
|
405
|
-
const headerRegion = page.regions.header;
|
|
406
|
-
const footerRegion = page.regions.footer;
|
|
407
|
-
const footnoteRegion = page.regions.footnotes?.[0];
|
|
408
|
-
|
|
409
|
-
const headerBlocks = headerStory
|
|
410
|
-
? facet
|
|
411
|
-
.getStoryBlocksForRegion(pageIndex, "header")
|
|
412
|
-
.map((b) => b.blockSnapshot)
|
|
413
|
-
: [];
|
|
414
|
-
const footerBlocks = footerStory
|
|
415
|
-
? facet
|
|
416
|
-
.getStoryBlocksForRegion(pageIndex, "footer")
|
|
417
|
-
.map((b) => b.blockSnapshot)
|
|
418
|
-
: [];
|
|
419
|
-
const footnoteBlocks = footnoteRegion
|
|
420
|
-
? facet
|
|
421
|
-
.getStoryBlocksForRegion(pageIndex, "footnote-area")
|
|
422
|
-
.map((b) => b.blockSnapshot)
|
|
423
|
-
: [];
|
|
424
|
-
|
|
425
|
-
const px = (twips: number): number => twips * FRAME_PX_PER_TWIP_AT_96DPI;
|
|
426
|
-
const bandWidthPx = px(
|
|
427
|
-
layout.pageWidth - layout.marginLeft - layout.marginRight,
|
|
428
|
-
);
|
|
429
|
-
const bandLeftPx = px(layout.marginLeft);
|
|
430
|
-
const frameHeightPx = rect.bottomPx - rect.topPx;
|
|
431
|
-
|
|
432
377
|
return (
|
|
433
|
-
<
|
|
378
|
+
<TwPageChromeEntry
|
|
434
379
|
key={`page-chrome-${rect.pageId}`}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}}
|
|
445
|
-
>
|
|
446
|
-
{headerRegion && headerStory ? (
|
|
447
|
-
<TwPageHeaderBand
|
|
448
|
-
pageIndex={pageIndex}
|
|
449
|
-
blocks={headerBlocks}
|
|
450
|
-
topPx={px(layout.headerMargin ?? 720)}
|
|
451
|
-
leftPx={bandLeftPx}
|
|
452
|
-
widthPx={bandWidthPx}
|
|
453
|
-
bandHeightPx={px(headerRegion.heightTwips)}
|
|
454
|
-
isActiveSlot={isActiveStoryMatch(activeStory, headerStory)}
|
|
455
|
-
onClick={() => onOpenStory?.(headerStory)}
|
|
456
|
-
/>
|
|
457
|
-
) : null}
|
|
458
|
-
{footerRegion && footerStory ? (
|
|
459
|
-
<TwPageFooterBand
|
|
460
|
-
pageIndex={pageIndex}
|
|
461
|
-
blocks={footerBlocks}
|
|
462
|
-
bottomPx={px(layout.footerMargin ?? 720)}
|
|
463
|
-
leftPx={bandLeftPx}
|
|
464
|
-
widthPx={bandWidthPx}
|
|
465
|
-
bandHeightPx={px(footerRegion.heightTwips)}
|
|
466
|
-
isActiveSlot={isActiveStoryMatch(activeStory, footerStory)}
|
|
467
|
-
onClick={() => onOpenStory?.(footerStory)}
|
|
468
|
-
/>
|
|
469
|
-
) : null}
|
|
470
|
-
{footnoteRegion ? (
|
|
471
|
-
<TwFootnoteArea
|
|
472
|
-
pageIndex={pageIndex}
|
|
473
|
-
blocks={footnoteBlocks}
|
|
474
|
-
topPx={px(footnoteRegion.originTwips - layout.marginTop)}
|
|
475
|
-
leftPx={bandLeftPx}
|
|
476
|
-
widthPx={px(footnoteRegion.widthTwips)}
|
|
477
|
-
heightPx={px(footnoteRegion.heightTwips)}
|
|
478
|
-
/>
|
|
479
|
-
) : null}
|
|
480
|
-
</div>
|
|
380
|
+
rect={rect}
|
|
381
|
+
pageIndex={pageIndex}
|
|
382
|
+
page={page}
|
|
383
|
+
facet={facet}
|
|
384
|
+
activeStory={activeStory}
|
|
385
|
+
onOpenStory={onOpenStory}
|
|
386
|
+
visiblePageIndexRange={visiblePageIndexRange}
|
|
387
|
+
renderFrameRevision={renderFrameRevision}
|
|
388
|
+
/>
|
|
481
389
|
);
|
|
482
390
|
})}
|
|
483
391
|
<TwEndnoteArea blocks={endnoteBlocks} />
|
|
@@ -485,40 +393,4 @@ export const TwPageStackChromeLayer: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
485
393
|
);
|
|
486
394
|
};
|
|
487
395
|
|
|
488
|
-
/**
|
|
489
|
-
* Strict equality for two `EditorStoryTarget` values — used to decide
|
|
490
|
-
* whether a given band should render in active-slot mode.
|
|
491
|
-
*
|
|
492
|
-
* For `"main"` the kinds must match; for header/footer the triple
|
|
493
|
-
* `(relationshipId, variant, sectionIndex)` must match; for footnote /
|
|
494
|
-
* endnote the `noteId` must match. Any mismatch returns false.
|
|
495
|
-
*/
|
|
496
|
-
function isActiveStoryMatch(
|
|
497
|
-
active: EditorStoryTarget,
|
|
498
|
-
candidate: EditorStoryTarget,
|
|
499
|
-
): boolean {
|
|
500
|
-
if (active.kind !== candidate.kind) return false;
|
|
501
|
-
if (active.kind === "main" || candidate.kind === "main") {
|
|
502
|
-
return active.kind === candidate.kind;
|
|
503
|
-
}
|
|
504
|
-
if (active.kind === "footnote" && candidate.kind === "footnote") {
|
|
505
|
-
return active.noteId === candidate.noteId;
|
|
506
|
-
}
|
|
507
|
-
if (active.kind === "endnote" && candidate.kind === "endnote") {
|
|
508
|
-
return active.noteId === candidate.noteId;
|
|
509
|
-
}
|
|
510
|
-
// header / footer — triple match.
|
|
511
|
-
if (
|
|
512
|
-
(active.kind === "header" && candidate.kind === "header") ||
|
|
513
|
-
(active.kind === "footer" && candidate.kind === "footer")
|
|
514
|
-
) {
|
|
515
|
-
return (
|
|
516
|
-
active.relationshipId === candidate.relationshipId &&
|
|
517
|
-
active.variant === candidate.variant &&
|
|
518
|
-
active.sectionIndex === candidate.sectionIndex
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
396
|
export default TwPageStackChromeLayer;
|