@beyondwork/docx-react-component 1.0.109 → 1.0.110

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.
@@ -31,6 +31,16 @@ export interface PagePreviewMaps {
31
31
  footerPreviewByPageId: Map<string, string>;
32
32
  }
33
33
 
34
+ type PageLocalFieldLedger = readonly {
35
+ readonly family: string;
36
+ readonly displayText: string;
37
+ }[];
38
+
39
+ interface PagePreviewFieldState {
40
+ readonly ledger: PageLocalFieldLedger | undefined;
41
+ ordinal: number;
42
+ }
43
+
34
44
  export function buildPagePreviewMaps(
35
45
  graph: RuntimePageGraph,
36
46
  subParts: {
@@ -49,7 +59,10 @@ export function buildPagePreviewMaps(
49
59
  if (source) {
50
60
  headerPreviewByPageId.set(
51
61
  page.pageId,
52
- flattenBlocksToPreview(source.blocks, page, graph),
62
+ flattenBlocksToPreview(source.blocks, page, graph, {
63
+ ledger: findPageLocalStoryFieldLedger(page, headerStory),
64
+ ordinal: 0,
65
+ }),
53
66
  );
54
67
  }
55
68
  }
@@ -59,7 +72,10 @@ export function buildPagePreviewMaps(
59
72
  if (source) {
60
73
  footerPreviewByPageId.set(
61
74
  page.pageId,
62
- flattenBlocksToPreview(source.blocks, page, graph),
75
+ flattenBlocksToPreview(source.blocks, page, graph, {
76
+ ledger: findPageLocalStoryFieldLedger(page, footerStory),
77
+ ordinal: 0,
78
+ }),
63
79
  );
64
80
  }
65
81
  }
@@ -76,14 +92,30 @@ function findSubPart<T extends HeaderDocument | FooterDocument>(
76
92
  return parts.find((p) => p.relationshipId === relationshipId);
77
93
  }
78
94
 
95
+ function findPageLocalStoryFieldLedger(
96
+ page: RuntimePageNode,
97
+ story: NonNullable<RuntimePageNode["stories"]["header" | "footer"]>,
98
+ ): PageLocalFieldLedger | undefined {
99
+ const pageLocalStory = page.frame?.pageLocalStories.find(
100
+ (candidate) =>
101
+ candidate.kind === story.kind &&
102
+ candidate.relationshipId === story.relationshipId &&
103
+ candidate.variant === story.variant &&
104
+ (story.sectionIndex === undefined ||
105
+ candidate.sectionIndex === story.sectionIndex),
106
+ );
107
+ return pageLocalStory?.resolvedFields;
108
+ }
109
+
79
110
  function flattenBlocksToPreview(
80
111
  blocks: ReadonlyArray<BlockNode>,
81
112
  page: RuntimePageNode,
82
113
  graph: RuntimePageGraph,
114
+ fieldState: PagePreviewFieldState,
83
115
  ): string {
84
116
  const parts: string[] = [];
85
117
  for (const block of blocks) {
86
- collectBlockText(block, page, graph, parts);
118
+ collectBlockText(block, page, graph, fieldState, parts);
87
119
  if (joinPreview(parts).length >= MAX_PREVIEW_CHARS) break;
88
120
  }
89
121
  return truncate(joinPreview(parts));
@@ -93,26 +125,27 @@ function collectBlockText(
93
125
  block: BlockNode,
94
126
  page: RuntimePageNode,
95
127
  graph: RuntimePageGraph,
128
+ fieldState: PagePreviewFieldState,
96
129
  out: string[],
97
130
  ): void {
98
131
  switch (block.type) {
99
132
  case "paragraph":
100
133
  for (const child of block.children) {
101
- collectInlineText(child, page, graph, out);
134
+ collectInlineText(child, page, graph, fieldState, out);
102
135
  }
103
136
  break;
104
137
  case "table":
105
138
  for (const row of block.rows) {
106
139
  for (const cell of row.cells) {
107
140
  for (const childBlock of cell.children) {
108
- collectBlockText(childBlock, page, graph, out);
141
+ collectBlockText(childBlock, page, graph, fieldState, out);
109
142
  }
110
143
  }
111
144
  }
112
145
  break;
113
146
  case "sdt":
114
147
  for (const child of block.children) {
115
- collectBlockText(child, page, graph, out);
148
+ collectBlockText(child, page, graph, fieldState, out);
116
149
  }
117
150
  break;
118
151
  // opaque_block / section_break / custom_xml / alt_chunk — no preview text.
@@ -125,6 +158,7 @@ function collectInlineText(
125
158
  inline: InlineNode,
126
159
  page: RuntimePageNode,
127
160
  graph: RuntimePageGraph,
161
+ fieldState: PagePreviewFieldState,
128
162
  out: string[],
129
163
  ): void {
130
164
  switch (inline.type) {
@@ -141,8 +175,12 @@ function collectInlineText(
141
175
  const family = inline.fieldFamily ?? classifyFieldInstructionLocal(inline.instruction);
142
176
  const cached = flattenInline(inline.children);
143
177
  if (family === "PAGE" || family === "NUMPAGES" || family === "SECTIONPAGES") {
178
+ const ledgerField = fieldState.ledger?.[fieldState.ordinal];
179
+ fieldState.ordinal += 1;
144
180
  out.push(
145
- resolvePageFieldDisplayText(family, cached, { page, graph }),
181
+ ledgerField?.family === family
182
+ ? ledgerField.displayText
183
+ : resolvePageFieldDisplayText(family, cached, { page, graph }),
146
184
  );
147
185
  } else {
148
186
  out.push(cached);
@@ -151,7 +189,7 @@ function collectInlineText(
151
189
  }
152
190
  case "hyperlink":
153
191
  for (const child of inline.children) {
154
- collectInlineText(child, page, graph, out);
192
+ collectInlineText(child, page, graph, fieldState, out);
155
193
  }
156
194
  break;
157
195
  // bookmark_start/end, opaque_inline, note_ref etc — skip.
@@ -0,0 +1,62 @@
1
+ import React from "react";
2
+
3
+ export interface TableSplitRowCarryOverlayEntry {
4
+ blockId: string;
5
+ rowIndex: number;
6
+ topPx: number;
7
+ leftPx: number;
8
+ widthPx: number;
9
+ heightPx: number;
10
+ continuesFromPreviousPage?: boolean;
11
+ continuesToNextPage?: boolean;
12
+ }
13
+
14
+ export interface TwTableSplitRowCarryOverlayProps {
15
+ entries: readonly TableSplitRowCarryOverlayEntry[];
16
+ }
17
+
18
+ function TwTableSplitRowCarryOverlayInner({
19
+ entries,
20
+ }: TwTableSplitRowCarryOverlayProps): React.ReactElement | null {
21
+ if (entries.length === 0) return null;
22
+
23
+ return (
24
+ <>
25
+ {entries.map((entry) => (
26
+ <div
27
+ key={`${entry.blockId}:${entry.rowIndex}:${entry.topPx}:${entry.heightPx}`}
28
+ aria-hidden
29
+ data-table-split-row-carry=""
30
+ data-block-id={entry.blockId}
31
+ data-row-index={entry.rowIndex}
32
+ data-continues-from-previous-page={
33
+ entry.continuesFromPreviousPage ? "true" : undefined
34
+ }
35
+ data-continues-to-next-page={
36
+ entry.continuesToNextPage ? "true" : undefined
37
+ }
38
+ style={{
39
+ position: "absolute",
40
+ top: `${entry.topPx}px`,
41
+ left: `${entry.leftPx}px`,
42
+ width: `${entry.widthPx}px`,
43
+ height: `${entry.heightPx}px`,
44
+ pointerEvents: "none",
45
+ boxSizing: "border-box",
46
+ borderTop: entry.continuesFromPreviousPage
47
+ ? "1px dashed var(--color-border-accent)"
48
+ : undefined,
49
+ borderBottom: entry.continuesToNextPage
50
+ ? "1px dashed var(--color-border-accent)"
51
+ : undefined,
52
+ backgroundColor: "color-mix(in srgb, var(--color-accent) 8%, transparent)",
53
+ }}
54
+ />
55
+ ))}
56
+ </>
57
+ );
58
+ }
59
+
60
+ export const TwTableSplitRowCarryOverlay = React.memo(
61
+ TwTableSplitRowCarryOverlayInner,
62
+ );
@@ -151,6 +151,7 @@ function buildPageBreakDecorationsFromProps(
151
151
  pageIndex: p.page.pageIndex,
152
152
  startOffset: p.page.startOffset,
153
153
  isBlankFiller: p.page.isBlankFiller,
154
+ frame: p.page.frame,
154
155
  stories: {
155
156
  displayPageNumber: p.page.stories.displayPageNumber,
156
157
  header: p.page.stories.header,
@@ -16,6 +16,7 @@
16
16
  import React from "react";
17
17
  import type {
18
18
  EditorStoryTarget,
19
+ GeometryFacet,
19
20
  PublicPageNode,
20
21
  SurfaceTableRowSnapshot,
21
22
  WordReviewEditorLayoutFacet,
@@ -23,6 +24,10 @@ import type {
23
24
  import { buildPageAnchorAttributes } from "../../api/v3/_page-anchor-id.ts";
24
25
  import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
25
26
  import { TwTableContinuationHeader } from "../chrome-overlay/tw-table-continuation-header.tsx";
27
+ import {
28
+ TwTableSplitRowCarryOverlay,
29
+ type TableSplitRowCarryOverlayEntry,
30
+ } from "../chrome-overlay/tw-table-split-row-carry-overlay.tsx";
26
31
  import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
27
32
  import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
28
33
  import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
@@ -35,6 +40,7 @@ export interface TwPageChromeEntryProps {
35
40
  pageIndex: number;
36
41
  page: PublicPageNode;
37
42
  facet: WordReviewEditorLayoutFacet;
43
+ geometryFacet?: GeometryFacet;
38
44
  activeStory: EditorStoryTarget;
39
45
  activeStoryPageIndex?: number | null;
40
46
  onOpenStory?: (target: EditorStoryTarget, pageIndex: number) => void;
@@ -60,6 +66,7 @@ function TwPageChromeEntryInner({
60
66
  pageIndex,
61
67
  page,
62
68
  facet,
69
+ geometryFacet,
63
70
  activeStory,
64
71
  activeStoryPageIndex,
65
72
  onOpenStory,
@@ -134,6 +141,16 @@ function TwPageChromeEntryInner({
134
141
  // eslint-disable-next-line react-hooks/exhaustive-deps
135
142
  }, [facet, pageIndex, page, renderFrameRevision]);
136
143
 
144
+ const splitRowCarryEntries = React.useMemo(
145
+ () =>
146
+ collectSplitRowCarryOverlayEntries({
147
+ geometryFacet,
148
+ pageIndex,
149
+ pageTopPx: rect.topPx,
150
+ }),
151
+ [geometryFacet, pageIndex, rect.topPx, renderFrameRevision],
152
+ );
153
+
137
154
  const handleHeaderDoubleClick = React.useCallback(
138
155
  () => headerStory && onOpenStory?.(headerStory, pageIndex),
139
156
  [onOpenStory, headerStory, pageIndex],
@@ -257,6 +274,7 @@ function TwPageChromeEntryInner({
257
274
  bodyOriginTopPx={px(layout.marginTop)}
258
275
  />
259
276
  ))}
277
+ <TwTableSplitRowCarryOverlay entries={splitRowCarryEntries} />
260
278
  </div>
261
279
  );
262
280
  }
@@ -269,6 +287,7 @@ function propsAreEqual(
269
287
  prev.pageIndex === next.pageIndex &&
270
288
  prev.page === next.page &&
271
289
  prev.facet === next.facet &&
290
+ prev.geometryFacet === next.geometryFacet &&
272
291
  prev.activeStory === next.activeStory &&
273
292
  prev.activeStoryPageIndex === next.activeStoryPageIndex &&
274
293
  prev.onOpenStory === next.onOpenStory &&
@@ -285,6 +304,49 @@ function propsAreEqual(
285
304
 
286
305
  export const TwPageChromeEntry = React.memo(TwPageChromeEntryInner, propsAreEqual);
287
306
 
307
+ function collectSplitRowCarryOverlayEntries(input: {
308
+ geometryFacet?: GeometryFacet;
309
+ pageIndex: number;
310
+ pageTopPx: number;
311
+ }): readonly TableSplitRowCarryOverlayEntry[] {
312
+ const index = input.geometryFacet?.getGeometryIndex();
313
+ if (!index) return [];
314
+
315
+ const entries: TableSplitRowCarryOverlayEntry[] = [];
316
+ const seen = new Set<string>();
317
+ for (const entry of index.semanticEntries) {
318
+ if (entry.kind !== "table-row") continue;
319
+ if (entry.pageIndex !== input.pageIndex) continue;
320
+ if (entry.rect.space !== "frame") continue;
321
+ const carries = entry.tableContinuation?.splitRowCarry ?? [];
322
+ if (carries.length === 0) continue;
323
+ const blockId = entry.blockId;
324
+ if (!blockId) continue;
325
+ for (const carry of carries) {
326
+ const rowIndex = entry.rowIndex ?? carry.rowIndex;
327
+ if (rowIndex === undefined) continue;
328
+ const topPx = Math.max(0, entry.rect.topPx - input.pageTopPx);
329
+ const leftPx = Math.max(0, entry.rect.leftPx);
330
+ const widthPx = Math.max(1, entry.rect.widthPx);
331
+ const heightPx = Math.max(1, entry.rect.heightPx);
332
+ const key = `${blockId}:${rowIndex}:${topPx}:${leftPx}:${widthPx}:${heightPx}`;
333
+ if (seen.has(key)) continue;
334
+ seen.add(key);
335
+ entries.push({
336
+ blockId,
337
+ rowIndex,
338
+ topPx,
339
+ leftPx,
340
+ widthPx,
341
+ heightPx,
342
+ continuesFromPreviousPage: carry.continuesFromPreviousPage,
343
+ continuesToNextPage: carry.continuesToNextPage,
344
+ });
345
+ }
346
+ }
347
+ return entries;
348
+ }
349
+
288
350
  function isActiveStoryMatch(
289
351
  active: EditorStoryTarget,
290
352
  candidate: EditorStoryTarget,
@@ -503,6 +503,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
503
503
  pageIndex={rect.pageIndex}
504
504
  page={page}
505
505
  facet={facet}
506
+ geometryFacet={geometryFacet}
506
507
  activeStory={activeStory}
507
508
  activeStoryPageIndex={activeStoryPageIndex}
508
509
  onOpenStory={handleOpenStoryForPage}