@beyondwork/docx-react-component 1.0.77 → 1.0.79

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.
@@ -27,6 +27,7 @@ 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
29
  import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
30
+ import type { TwActiveBandRibbonProps } from "./tw-active-band-ribbon.tsx";
30
31
 
31
32
  export interface TwPageChromeEntryProps {
32
33
  rect: PageOverlayRect;
@@ -40,6 +41,15 @@ export interface TwPageChromeEntryProps {
40
41
  /** Preview catalog threaded into header/footer/footnote region renderers
41
42
  * so images (CCEP logos on 7-of-8 CCEP docs) render as real <img>s. */
42
43
  mediaPreviews?: Record<string, MediaPreviewDescriptor>;
44
+ /**
45
+ * Slice B (§6.20 reshape) — section-properties ribbon bundle forwarded
46
+ * to whichever band is active for this page. The bundle's `pageLayout`
47
+ * + `viewState` + `paragraphLayout` already reflect the active story;
48
+ * `kind` is set by the band itself. Pass `null` (or omit) when no
49
+ * header/footer is active anywhere — keeps the inactive-page memo
50
+ * stable across activate/deactivate cycles on a different page.
51
+ */
52
+ activeBandRibbonProps?: Omit<TwActiveBandRibbonProps, "kind" | "data-testid"> | null;
43
53
  }
44
54
 
45
55
  function TwPageChromeEntryInner({
@@ -52,6 +62,7 @@ function TwPageChromeEntryInner({
52
62
  visiblePageIndexRange,
53
63
  renderFrameRevision,
54
64
  mediaPreviews,
65
+ activeBandRibbonProps,
55
66
  }: TwPageChromeEntryProps): React.ReactElement {
56
67
  const layout = page.layout;
57
68
  const headerStory = page.stories.header;
@@ -192,6 +203,7 @@ function TwPageChromeEntryInner({
192
203
  sectionLabel={headerActive ? headerSectionLabel : undefined}
193
204
  onClick={handleHeaderClick}
194
205
  mediaPreviews={mediaPreviews}
206
+ ribbonProps={headerActive ? activeBandRibbonProps ?? null : null}
195
207
  />
196
208
  ) : null}
197
209
  {footerRegion && footerStory ? (
@@ -206,6 +218,7 @@ function TwPageChromeEntryInner({
206
218
  sectionLabel={footerActive ? footerSectionLabel : undefined}
207
219
  onClick={handleFooterClick}
208
220
  mediaPreviews={mediaPreviews}
221
+ ribbonProps={footerActive ? activeBandRibbonProps ?? null : null}
209
222
  />
210
223
  ) : null}
211
224
  {footnoteRegion ? (
@@ -249,7 +262,8 @@ function propsAreEqual(
249
262
  prev.rect.topPx === next.rect.topPx &&
250
263
  prev.rect.bottomPx === next.rect.bottomPx &&
251
264
  prev.rect.pageId === next.rect.pageId &&
252
- prev.mediaPreviews === next.mediaPreviews
265
+ prev.mediaPreviews === next.mediaPreviews &&
266
+ prev.activeBandRibbonProps === next.activeBandRibbonProps
253
267
  );
254
268
  }
255
269
 
@@ -3,6 +3,10 @@ import React from "react";
3
3
  import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
4
4
  import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
5
5
  import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
6
+ import {
7
+ TwActiveBandRibbon,
8
+ type TwActiveBandRibbonProps,
9
+ } from "./tw-active-band-ribbon.tsx";
6
10
 
7
11
  // ---------------------------------------------------------------------------
8
12
  // TwPageFooterBand (P8.5)
@@ -36,6 +40,12 @@ export interface TwPageFooterBandProps {
36
40
  onClick: () => void;
37
41
  "data-testid"?: string;
38
42
  mediaPreviews?: Record<string, MediaPreviewDescriptor>;
43
+ /**
44
+ * Slice B (§6.20 reshape) — section-properties ribbon that floats
45
+ * below the footer band when it is the active story slot. See
46
+ * `TwPageHeaderBandProps.ribbonProps` for shape rationale.
47
+ */
48
+ ribbonProps?: Omit<TwActiveBandRibbonProps, "kind" | "data-testid"> | null;
39
49
  }
40
50
 
41
51
  export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
@@ -50,6 +60,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
50
60
  onClick,
51
61
  "data-testid": testId,
52
62
  mediaPreviews,
63
+ ribbonProps,
53
64
  }) => {
54
65
  return (
55
66
  <div
@@ -73,6 +84,13 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
73
84
  {sectionLabel}
74
85
  </span>
75
86
  ) : null}
87
+ {isActiveSlot && ribbonProps ? (
88
+ <TwActiveBandRibbon
89
+ kind="footer"
90
+ {...ribbonProps}
91
+ data-testid={testId ? `${testId}-ribbon` : undefined}
92
+ />
93
+ ) : null}
76
94
  {isActiveSlot ? (
77
95
  <div
78
96
  data-pm-portal-slot
@@ -3,6 +3,10 @@ import React from "react";
3
3
  import type { SurfaceBlockSnapshot } from "../../api/public-types.ts";
4
4
  import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
5
5
  import { TwRegionBlockRenderer } from "./tw-region-block-renderer.tsx";
6
+ import {
7
+ TwActiveBandRibbon,
8
+ type TwActiveBandRibbonProps,
9
+ } from "./tw-active-band-ribbon.tsx";
6
10
 
7
11
  // ---------------------------------------------------------------------------
8
12
  // TwPageHeaderBand (P8.5)
@@ -40,6 +44,14 @@ export interface TwPageHeaderBandProps {
40
44
  * (CCEP logos on 7-of-8 CCEP docs) render as real <img>s instead of
41
45
  * the 48×32 placeholder chip. */
42
46
  mediaPreviews?: Record<string, MediaPreviewDescriptor>;
47
+ /**
48
+ * Slice B (§6.20 reshape) — section-properties ribbon that floats
49
+ * above the band when it is the active story slot. Omit (or pass
50
+ * `null`) to suppress the ribbon — `isActiveSlot` alone is not
51
+ * sufficient because some hosts mount the band in active mode without
52
+ * surfacing layout controls (e.g. headless / read-only previews).
53
+ */
54
+ ribbonProps?: Omit<TwActiveBandRibbonProps, "kind" | "data-testid"> | null;
43
55
  }
44
56
 
45
57
  export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
@@ -54,6 +66,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
54
66
  onClick,
55
67
  "data-testid": testId,
56
68
  mediaPreviews,
69
+ ribbonProps,
57
70
  }) => {
58
71
  return (
59
72
  <div
@@ -77,6 +90,13 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
77
90
  {sectionLabel}
78
91
  </span>
79
92
  ) : null}
93
+ {isActiveSlot && ribbonProps ? (
94
+ <TwActiveBandRibbon
95
+ kind="header"
96
+ {...ribbonProps}
97
+ data-testid={testId ? `${testId}-ribbon` : undefined}
98
+ />
99
+ ) : null}
80
100
  {isActiveSlot ? (
81
101
  <div
82
102
  data-pm-portal-slot
@@ -80,6 +80,7 @@ import {
80
80
  } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
81
81
  import { TwEndnoteArea } from "./tw-endnote-area.tsx";
82
82
  import { TwPageChromeEntry } from "./tw-page-chrome-entry.tsx";
83
+ import type { TwActiveBandRibbonProps } from "./tw-active-band-ribbon.tsx";
83
84
 
84
85
  /**
85
86
  * Minimal structural type for the PM `EditorView` handle consumed by
@@ -149,6 +150,13 @@ export interface TwPageStackChromeLayerProps {
149
150
  * in headers/footers/footnote bodies render as real <img>s. Without
150
151
  * this, image segments fall back to the 48×32 gray placeholder chip. */
151
152
  mediaPreviews?: Record<string, import("../editor-surface/pm-state-from-snapshot.ts").MediaPreviewDescriptor>;
153
+ /**
154
+ * Slice B (§6.20 reshape) — section-properties ribbon bundle forwarded
155
+ * to whichever page's active band renders it. Pass `null` (or omit)
156
+ * when no header/footer is active to keep memo equality stable for
157
+ * inactive sessions.
158
+ */
159
+ activeBandRibbonProps?: Omit<TwActiveBandRibbonProps, "kind" | "data-testid"> | null;
152
160
  }
153
161
 
154
162
  const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
@@ -162,6 +170,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
162
170
  visiblePageIndexRange,
163
171
  "data-testid": testId,
164
172
  mediaPreviews,
173
+ activeBandRibbonProps,
165
174
  }) => {
166
175
  const [rects, setRects] = React.useState<readonly PageOverlayRect[]>([]);
167
176
  const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
@@ -407,6 +416,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
407
416
  visiblePageIndexRange={visiblePageIndexRange}
408
417
  renderFrameRevision={renderFrameRevision}
409
418
  mediaPreviews={mediaPreviews}
419
+ activeBandRibbonProps={activeBandRibbonProps}
410
420
  />
411
421
  );
412
422
  })}
@@ -220,12 +220,14 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
220
220
  viewState.selection.activeRange.kind === "node"
221
221
  ? viewState.selection.activeRange.at
222
222
  : viewState.selection.head;
223
- // Slice A (designsystem §6.20 reshape): the legacy "Layout tools"
224
- // disclosure that gated this resolver is gone. Slice B (active-band
225
- // ribbon) re-enables resolution when a header/footer band is the
226
- // active story; until then, no consumer reads activeParagraphLayout
227
- // so the resolver stays inert.
228
- const shouldResolveActiveParagraphLayout = false;
223
+ // Slice B (designsystem §6.20 reshape): the active-band ribbon's
224
+ // `TwPageRuler` reads `activeParagraphLayout` when a header or footer
225
+ // story is the active slot, so the resolver runs only while that ribbon
226
+ // is mounted. Body-mode editing skips the resolver — no consumer reads
227
+ // it on the body path.
228
+ const shouldResolveActiveParagraphLayout =
229
+ snapshot.activeStory.kind === "header" ||
230
+ snapshot.activeStory.kind === "footer";
229
231
  const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
230
232
  const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
231
233
  const {
@@ -333,6 +335,53 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
333
335
  onDismissSelectionToolbar: props.onDismissSelectionToolbar,
334
336
  });
335
337
 
338
+ // Slice B (designsystem §6.20 reshape) — section-properties ribbon
339
+ // bundle. Built only while a header or footer band is the active
340
+ // story; the chrome layer forwards it to whichever band rendered the
341
+ // ribbon. Document-level read-only governs the controls (the
342
+ // pageChromeReadOnly flag also disables on header/footer-active, which
343
+ // is the wrong polarity here — the ribbon's whole point is editing
344
+ // section properties from inside the header/footer).
345
+ const ribbonReadOnly =
346
+ snapshot.readOnly || effectiveSelectionMode !== "edit";
347
+ const activeBandRibbonProps = useMemo(() => {
348
+ if (
349
+ snapshot.activeStory.kind !== "header" &&
350
+ snapshot.activeStory.kind !== "footer"
351
+ ) {
352
+ return null;
353
+ }
354
+ if (!snapshot.pageLayout) {
355
+ return null;
356
+ }
357
+ return {
358
+ pageLayout: snapshot.pageLayout,
359
+ viewState,
360
+ paragraphLayout: activeParagraphLayout,
361
+ readOnly: ribbonReadOnly,
362
+ onCloseStory: props.onCloseStory,
363
+ onInsertSectionBreak: props.onInsertSectionBreak,
364
+ onUpdateSectionLayout: props.onUpdateSectionLayout,
365
+ onSetSectionPageNumbering: props.onSetSectionPageNumbering,
366
+ onSetHeaderFooterLink: props.onSetHeaderFooterLink,
367
+ onSetParagraphIndentation: props.onSetParagraphIndentation,
368
+ onSetParagraphTabStops: props.onSetParagraphTabStops,
369
+ };
370
+ }, [
371
+ snapshot.activeStory.kind,
372
+ snapshot.pageLayout,
373
+ viewState,
374
+ activeParagraphLayout,
375
+ ribbonReadOnly,
376
+ props.onCloseStory,
377
+ props.onInsertSectionBreak,
378
+ props.onUpdateSectionLayout,
379
+ props.onSetSectionPageNumbering,
380
+ props.onSetHeaderFooterLink,
381
+ props.onSetParagraphIndentation,
382
+ props.onSetParagraphTabStops,
383
+ ]);
384
+
336
385
  // Audit §2.4 — the shell header is ALWAYS present in default composition.
337
386
  // When the host does not supply a pre-assembled shell node, fall back to
338
387
  // a default TwShellHeader wired to the workspace's editor-role state so
@@ -1010,6 +1059,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1010
1059
  pmSurfaceElement={pmSurfaceElement}
1011
1060
  visiblePageIndexRange={visiblePageIndexRange}
1012
1061
  mediaPreviews={props.mediaPreviews}
1062
+ activeBandRibbonProps={activeBandRibbonProps}
1013
1063
  />
1014
1064
  ) : null}
1015
1065
  </div>