@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.
- package/package.json +1 -1
- package/src/api/public-types.ts +51 -1
- package/src/api/v3/runtime/workflow.ts +21 -2
- package/src/core/commands/add-scope.ts +163 -36
- package/src/io/ooxml/parse-shapes.ts +32 -6
- package/src/model/canonical-document.ts +45 -8
- package/src/runtime/document-runtime.ts +77 -2
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +27 -1
- package/src/runtime/layout/public-facet.ts +35 -0
- package/src/runtime/workflow/coordinator.ts +63 -10
- package/src/runtime/workflow/scope-writer.ts +90 -2
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +12 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +20 -1
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +17 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +13 -13
- package/src/ui-tailwind/page-stack/tw-active-band-ribbon.tsx +229 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +15 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +18 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +20 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +10 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +56 -6
|
@@ -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
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
const shouldResolveActiveParagraphLayout =
|
|
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>
|