@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
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ActiveListContext,
|
|
18
18
|
CommentSidebarThreadSnapshot,
|
|
19
19
|
DocumentNavigationSnapshot,
|
|
20
|
+
EditorStoryTarget,
|
|
20
21
|
EditorViewStateSnapshot,
|
|
21
22
|
FormattingStateSnapshot,
|
|
22
23
|
FormattingAlignment,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
incrementInvalidationCounter,
|
|
52
53
|
recordPerfSample,
|
|
53
54
|
} from "./editor-surface/perf-probe.ts";
|
|
55
|
+
import { useVisibleBlockRange } from "./page-stack/use-visible-block-range.ts";
|
|
54
56
|
import { sliceBlocksForPage } from "./editor-surface/page-slice-util.ts";
|
|
55
57
|
import { TwPageBlockView } from "./editor-surface/tw-page-block-view.tsx";
|
|
56
58
|
import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
|
|
@@ -157,6 +159,16 @@ export interface TwReviewWorkspaceProps {
|
|
|
157
159
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
158
160
|
chromePreset?: WordReviewEditorChromePreset;
|
|
159
161
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
162
|
+
/** P9g — live collab session for the `"collab"` chrome preset's top nav. */
|
|
163
|
+
collabSession?: import("../runtime/collab-session.ts").CollabSession;
|
|
164
|
+
collabTransportStatus?: import("../api/awareness-identity-types.ts").TransportStatus;
|
|
165
|
+
collabActorId?: string;
|
|
166
|
+
collabSendBaseline?: {
|
|
167
|
+
originDocumentId: string;
|
|
168
|
+
originPayloadId: string;
|
|
169
|
+
originContentHash: string;
|
|
170
|
+
payloadXml: string;
|
|
171
|
+
};
|
|
160
172
|
reviewQueue?: ReviewQueueSnapshot;
|
|
161
173
|
documentContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
162
174
|
selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
@@ -278,7 +290,18 @@ export interface TwReviewWorkspaceProps {
|
|
|
278
290
|
onAcceptAllChanges?: () => void;
|
|
279
291
|
onRejectAllChanges?: () => void;
|
|
280
292
|
onCloseStory?: () => void;
|
|
293
|
+
/**
|
|
294
|
+
* @deprecated P8.11 — the workspace no longer renders a workspace-level
|
|
295
|
+
* header band with an "Edit header" button; per-page header bands route
|
|
296
|
+
* clicks via `onOpenStory` / `runtime.openStory` directly. The prop
|
|
297
|
+
* remains optional for one release so existing hosts continue to
|
|
298
|
+
* compile; supplying it emits a `console.warn` on mount.
|
|
299
|
+
*/
|
|
281
300
|
onOpenHeaderStory?: () => void;
|
|
301
|
+
/**
|
|
302
|
+
* @deprecated P8.11 — see `onOpenHeaderStory`. Footer variant of the
|
|
303
|
+
* same deprecation.
|
|
304
|
+
*/
|
|
282
305
|
onOpenFooterStory?: () => void;
|
|
283
306
|
/**
|
|
284
307
|
* Open a header/footer story for a specific page. Called when the user
|
|
@@ -288,6 +311,13 @@ export interface TwReviewWorkspaceProps {
|
|
|
288
311
|
*/
|
|
289
312
|
onOpenHeaderStoryForPage?: (pageIndex: number) => void;
|
|
290
313
|
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
314
|
+
/**
|
|
315
|
+
* P8.11 — fired when a per-page chrome band (header / footer) is
|
|
316
|
+
* clicked to promote it into the active editing surface. Wire to
|
|
317
|
+
* `runtime.openStory(target)`; the chrome layer's portal mechanism
|
|
318
|
+
* then reparents the PM surface into the matching band's active slot.
|
|
319
|
+
*/
|
|
320
|
+
onOpenStory?: (target: EditorStoryTarget) => void;
|
|
291
321
|
onSetParagraphIndentation?: (indentation: {
|
|
292
322
|
left?: number;
|
|
293
323
|
right?: number;
|
|
@@ -384,6 +414,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
384
414
|
} as TwReviewWorkspaceProps & EditorCommandBag;
|
|
385
415
|
const { snapshot, viewState } = props;
|
|
386
416
|
const selectionToolbarRootRef = useRef<HTMLDivElement>(null);
|
|
417
|
+
// P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
|
|
418
|
+
// root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
|
|
419
|
+
// measure per-page rects and to reparent PM's DOM node across band
|
|
420
|
+
// portals when `activeStory` changes. See comment near the body slot
|
|
421
|
+
// in the render tree below.
|
|
422
|
+
const bodySlotRef = useRef<HTMLDivElement | null>(null);
|
|
423
|
+
const scrollRootRef = useRef<HTMLDivElement | null>(null);
|
|
424
|
+
const [pmSurfaceElement, setPmSurfaceElement] =
|
|
425
|
+
useState<HTMLElement | null>(null);
|
|
426
|
+
const [pageStackScrollRoot, setPageStackScrollRoot] =
|
|
427
|
+
useState<HTMLElement | null>(null);
|
|
387
428
|
const caps = props.capabilities;
|
|
388
429
|
const isPageWorkspace = props.workspaceMode === "page";
|
|
389
430
|
const markupDisplay = props.markupDisplay;
|
|
@@ -640,8 +681,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
640
681
|
};
|
|
641
682
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
642
683
|
}, [props.layoutFacet, selectionPosition, snapshot.activeStory, renderFrameRevision]);
|
|
643
|
-
|
|
644
|
-
|
|
684
|
+
// P8.11 — `headerBandLabel` / `footerBandLabel` retired along with the
|
|
685
|
+
// workspace-level bands. Per-page bands in `TwPageStackChromeLayer`
|
|
686
|
+
// render the actual header / footer story blocks via
|
|
687
|
+
// `TwRegionBlockRenderer`, so a label row is no longer meaningful here.
|
|
645
688
|
const hidePageBorderForActiveEditing =
|
|
646
689
|
isPageWorkspace &&
|
|
647
690
|
snapshot.activeStory.kind === "main" &&
|
|
@@ -708,6 +751,88 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
708
751
|
}
|
|
709
752
|
}, [isPageWorkspace, snapshot.activeStory.kind]);
|
|
710
753
|
|
|
754
|
+
// P8.11 — capture the scroll-root DOM element on mount so the chrome
|
|
755
|
+
// overlay's `TwPageStackChromeLayer` can measure per-page rects and
|
|
756
|
+
// observe DOM mutations. `scrollRootRef` is attached to the existing
|
|
757
|
+
// `[data-wre-scroll-root]` container; rely on a mount effect rather
|
|
758
|
+
// than a ref callback so render-time state stays cheap.
|
|
759
|
+
useEffect(() => {
|
|
760
|
+
if (scrollRootRef.current !== pageStackScrollRoot) {
|
|
761
|
+
setPageStackScrollRoot(scrollRootRef.current);
|
|
762
|
+
}
|
|
763
|
+
// A `useEffect` re-runs after every render; the comparison guard
|
|
764
|
+
// keeps `setPageStackScrollRoot` from firing every commit. The
|
|
765
|
+
// scroll-root identity only changes when the component re-mounts.
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// P8.11 — capture the PM surface DOM element. The ProseMirror surface
|
|
769
|
+
// mounts inside `bodySlotRef` on its own schedule (the PM constructor
|
|
770
|
+
// runs inside the `TwProseMirrorSurface` child component). A
|
|
771
|
+
// `MutationObserver` scoped to the body slot's `childList` picks up
|
|
772
|
+
// the PM root on first commit; once captured, the chrome layer owns
|
|
773
|
+
// reparent state (including portal-slot promotion), so we skip
|
|
774
|
+
// further updates unless PM is actually disconnected from the
|
|
775
|
+
// document (e.g. session/document swap tearing PM down).
|
|
776
|
+
useEffect(() => {
|
|
777
|
+
const slot = bodySlotRef.current;
|
|
778
|
+
if (!slot) return undefined;
|
|
779
|
+
|
|
780
|
+
// If we already hold a live reference, the chrome layer may have
|
|
781
|
+
// portaled PM into a per-page band — PM has left `bodySlotRef` but
|
|
782
|
+
// is still connected to the document. We keep the reference until
|
|
783
|
+
// the node is fully disconnected.
|
|
784
|
+
if (pmSurfaceElement && pmSurfaceElement.isConnected) {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const readPm = (): HTMLElement | null =>
|
|
789
|
+
slot.querySelector<HTMLElement>(".ProseMirror");
|
|
790
|
+
|
|
791
|
+
const current = readPm();
|
|
792
|
+
if (current !== pmSurfaceElement) {
|
|
793
|
+
setPmSurfaceElement(current);
|
|
794
|
+
}
|
|
795
|
+
const runtime = slot.ownerDocument?.defaultView as
|
|
796
|
+
| (Window & { MutationObserver?: typeof MutationObserver })
|
|
797
|
+
| null;
|
|
798
|
+
if (!runtime?.MutationObserver) return undefined;
|
|
799
|
+
const observer = new runtime.MutationObserver(() => {
|
|
800
|
+
const next = readPm();
|
|
801
|
+
if (next !== null && next !== pmSurfaceElement) {
|
|
802
|
+
setPmSurfaceElement(next);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
// `childList: true, subtree: false` — we only care when children of
|
|
806
|
+
// the body slot change (e.g. PM is added for the first time).
|
|
807
|
+
// Subtree mutations (PM's own edits) are not our concern and would
|
|
808
|
+
// fire on every keystroke.
|
|
809
|
+
observer.observe(slot, { childList: true, subtree: false });
|
|
810
|
+
return () => observer.disconnect();
|
|
811
|
+
}, [pmSurfaceElement]);
|
|
812
|
+
|
|
813
|
+
// P8.11 — deprecation shim for the legacy `onOpenHeaderStory` /
|
|
814
|
+
// `onOpenFooterStory` props. Per-page chrome bands route clicks via
|
|
815
|
+
// `onOpenStory` + `runtime.openStory` directly; the workspace-level
|
|
816
|
+
// bands that consumed these callbacks are gone. Kept optional for one
|
|
817
|
+
// release so existing hosts compile; a mount-time `console.warn` nudges
|
|
818
|
+
// them toward `onOpenStory`.
|
|
819
|
+
useEffect(() => {
|
|
820
|
+
if (props.onOpenHeaderStory) {
|
|
821
|
+
// eslint-disable-next-line no-console
|
|
822
|
+
console.warn(
|
|
823
|
+
"[docx-react-component] `onOpenHeaderStory` is deprecated. Per-page header bands route clicks via runtime.openStory directly. (P8)",
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (props.onOpenFooterStory) {
|
|
827
|
+
// eslint-disable-next-line no-console
|
|
828
|
+
console.warn(
|
|
829
|
+
"[docx-react-component] `onOpenFooterStory` is deprecated. Per-page footer bands route clicks via runtime.openStory directly. (P8)",
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
// Mount-once: we only want to nudge hosts at startup, not per render.
|
|
833
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
834
|
+
}, []);
|
|
835
|
+
|
|
711
836
|
useEffect(() => {
|
|
712
837
|
if (typeof window === "undefined") {
|
|
713
838
|
return;
|
|
@@ -771,6 +896,101 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
771
896
|
);
|
|
772
897
|
}, [reviewRailAvailable, viewportWidth]);
|
|
773
898
|
|
|
899
|
+
// L7 Phase 2 Task 2.2.4a — viewport-scroll wiring.
|
|
900
|
+
// Collect DOM elements with `[data-page-frame]` from the PM surface so the
|
|
901
|
+
// IntersectionObserver inside `useVisibleBlockRange` can determine which
|
|
902
|
+
// pages are currently visible. The MutationObserver refreshes the set when
|
|
903
|
+
// the PM surface re-renders (e.g. a document load changes the page count).
|
|
904
|
+
const [pageMarkers, setPageMarkers] = useState<readonly HTMLElement[]>([]);
|
|
905
|
+
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
const root = pageStackScrollRoot;
|
|
908
|
+
if (!root) {
|
|
909
|
+
setPageMarkers([]);
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
const refresh = () => {
|
|
913
|
+
const found = Array.from(root.querySelectorAll<HTMLElement>("[data-page-frame]"));
|
|
914
|
+
// The boundary widgets between pages N and N+1 carry `data-page-frame`
|
|
915
|
+
// for the *next* page (pages 1 … N). Page 0 has no widget before it,
|
|
916
|
+
// so we synthesize an in-memory element that carries page-0's attributes.
|
|
917
|
+
// This element is NOT in the DOM — the IntersectionObserver won't fire
|
|
918
|
+
// on it — but the hook's useMemo uses it to look up block indices when
|
|
919
|
+
// the overscan expansion includes page 0 (which happens whenever any of
|
|
920
|
+
// pages 1-2 are visible with overscan ≥ 1).
|
|
921
|
+
const hasPage0 = found.some(
|
|
922
|
+
(el) => el.getAttribute("data-page-frame") === "0",
|
|
923
|
+
);
|
|
924
|
+
if (!hasPage0 && found.length > 0) {
|
|
925
|
+
// Derive page-0 block range from the snapshot surface: page 0 starts
|
|
926
|
+
// at block 0 and ends just before the first block that belongs to page 1.
|
|
927
|
+
// The boundary widget's `data-page-first-block-index` for page 1 tells
|
|
928
|
+
// us where page 0 ends.
|
|
929
|
+
const page1Marker = found.find(
|
|
930
|
+
(el) => el.getAttribute("data-page-frame") === "1",
|
|
931
|
+
);
|
|
932
|
+
const page1First = page1Marker
|
|
933
|
+
? Number(page1Marker.getAttribute("data-page-first-block-index") ?? "")
|
|
934
|
+
: NaN;
|
|
935
|
+
const page0Last = Number.isFinite(page1First) && page1First > 0
|
|
936
|
+
? page1First - 1
|
|
937
|
+
: -1; // page 0 empty or unknown; synthetic marker contributes nothing.
|
|
938
|
+
const ownerDoc = found[0]!.ownerDocument;
|
|
939
|
+
const synth = ownerDoc.createElement("span");
|
|
940
|
+
synth.setAttribute("data-page-frame", "0");
|
|
941
|
+
synth.setAttribute("data-page-first-block-index", "0");
|
|
942
|
+
synth.setAttribute("data-page-last-block-index", String(Math.max(0, page0Last)));
|
|
943
|
+
setPageMarkers([synth, ...found]);
|
|
944
|
+
} else {
|
|
945
|
+
setPageMarkers(found);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
refresh();
|
|
949
|
+
// Observe PM surface mutations for page-count changes (new doc load, etc).
|
|
950
|
+
const view = root.ownerDocument?.defaultView;
|
|
951
|
+
if (!view?.MutationObserver) return undefined;
|
|
952
|
+
const mo = new view.MutationObserver(refresh);
|
|
953
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
954
|
+
return () => mo.disconnect();
|
|
955
|
+
// Re-run when the scroll root changes OR when a new snapshot lands
|
|
956
|
+
// (which may have a different page count and new widgets in the PM DOM).
|
|
957
|
+
// NOTE: snapshot.surface is intentionally excluded — its reference changes on
|
|
958
|
+
// every requestViewportRefresh(), which would add two extra render passes per
|
|
959
|
+
// scroll event. The page-0 fallback uses -1 when page1First is unknown,
|
|
960
|
+
// which is correct (no page-0 blocks → synthetic marker contributes nothing).
|
|
961
|
+
}, [pageStackScrollRoot, snapshot.revisionToken]);
|
|
962
|
+
|
|
963
|
+
// Derive the surface block index for the current selection head so the
|
|
964
|
+
// hook can extend the visible range to always include the selection.
|
|
965
|
+
const selectionBlockIndex = useMemo(() => {
|
|
966
|
+
const sel = snapshot.selection;
|
|
967
|
+
const blocks = snapshot.surface?.blocks;
|
|
968
|
+
if (!sel || !blocks) return null;
|
|
969
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
970
|
+
const block = blocks[i]!; // from/to are required on all SurfaceBlockSnapshot variants
|
|
971
|
+
const blockFrom = block.from;
|
|
972
|
+
const blockTo = block.to;
|
|
973
|
+
if (sel.head >= blockFrom && sel.head <= blockTo) return i;
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}, [snapshot.selection, snapshot.surface]);
|
|
977
|
+
|
|
978
|
+
const visibleBlockRange = useVisibleBlockRange({
|
|
979
|
+
pageMarkers,
|
|
980
|
+
overscanPages: 2,
|
|
981
|
+
selectionBlockIndex,
|
|
982
|
+
totalBlockCount: snapshot.surface?.blocks.length ?? 0,
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Push the visible range into the layout facet (which delegates to the
|
|
986
|
+
// runtime's viewport-culling machinery). Depend on `[start, end]` values
|
|
987
|
+
// (not the range object) so identity-preserving updates are a no-op.
|
|
988
|
+
useEffect(() => {
|
|
989
|
+
if (!props.layoutFacet) return;
|
|
990
|
+
props.layoutFacet.setVisibleBlockRange(visibleBlockRange);
|
|
991
|
+
props.layoutFacet.requestViewportRefresh();
|
|
992
|
+
}, [props.layoutFacet, visibleBlockRange.start, visibleBlockRange.end]);
|
|
993
|
+
|
|
774
994
|
const dismissSelectionToolbar = useCallback(() => {
|
|
775
995
|
props.onDismissSelectionToolbar?.();
|
|
776
996
|
}, [props.onDismissSelectionToolbar]);
|
|
@@ -818,6 +1038,20 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
818
1038
|
<div className="px-3 pt-3">
|
|
819
1039
|
<ChromePresetToolbar
|
|
820
1040
|
chromePreset={chromePreset}
|
|
1041
|
+
{...(props.collabSession ? { collabSession: props.collabSession } : {})}
|
|
1042
|
+
{...(props.collabTransportStatus
|
|
1043
|
+
? { collabTransportStatus: props.collabTransportStatus }
|
|
1044
|
+
: {})}
|
|
1045
|
+
{...(props.activeCommentId !== undefined
|
|
1046
|
+
? { activeCommentId: props.activeCommentId }
|
|
1047
|
+
: {})}
|
|
1048
|
+
{...(props.collabActorId !== undefined
|
|
1049
|
+
? { collabActorId: props.collabActorId }
|
|
1050
|
+
: {})}
|
|
1051
|
+
{...(props.collabSendBaseline
|
|
1052
|
+
? { collabSendBaseline: props.collabSendBaseline }
|
|
1053
|
+
: {})}
|
|
1054
|
+
chromeOptionsResolved={chromeOptions}
|
|
821
1055
|
capabilities={caps}
|
|
822
1056
|
compatibility={snapshot.compatibility}
|
|
823
1057
|
warnings={snapshot.warnings}
|
|
@@ -1076,6 +1310,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1076
1310
|
{/* Document column */}
|
|
1077
1311
|
<div className="flex flex-1 flex-col min-w-0">
|
|
1078
1312
|
<div
|
|
1313
|
+
ref={scrollRootRef}
|
|
1079
1314
|
className="flex-1 overflow-y-auto bg-surface"
|
|
1080
1315
|
data-wre-scroll-root="true"
|
|
1081
1316
|
>
|
|
@@ -1083,32 +1318,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1083
1318
|
ref={selectionToolbarRootRef}
|
|
1084
1319
|
className={`mx-auto min-h-full w-full ${
|
|
1085
1320
|
isPageWorkspace
|
|
1086
|
-
? "wre-page-chrome
|
|
1321
|
+
? "wre-page-chrome relative overflow-hidden"
|
|
1087
1322
|
: "wre-canvas-surface relative my-8 overflow-hidden"
|
|
1088
1323
|
}`}
|
|
1089
1324
|
data-zoom-bucket={pageZoomBucket}
|
|
1090
1325
|
data-zoom-scale={isPageWorkspace ? zoomScale : undefined}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
? {
|
|
1094
|
-
// P2.a — real-dim page frame: width/height from
|
|
1095
|
-
// `pageLayout.pageWidth/pageHeight × FRAME_PX_PER_TWIP_AT_96DPI`
|
|
1096
|
-
// so every paper size renders at its
|
|
1097
|
-
// Word-matching CSS px. `max-w-[840px]` retired.
|
|
1098
|
-
...(pageShellMetrics.frameWidthPx
|
|
1099
|
-
? { width: `${pageShellMetrics.frameWidthPx}px` }
|
|
1100
|
-
: {}),
|
|
1101
|
-
...(pageShellMetrics.frameHeightPx
|
|
1102
|
-
? { minHeight: `${pageShellMetrics.frameHeightPx}px` }
|
|
1103
|
-
: {}),
|
|
1104
|
-
// P2.b — browser-native CSS `zoom` rescales layout
|
|
1105
|
-
// so `getBoundingClientRect()` and hit-test offsets
|
|
1106
|
-
// stay truthful at any zoom — no inverse-projection
|
|
1107
|
-
// math needed downstream.
|
|
1108
|
-
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1109
|
-
}
|
|
1110
|
-
: undefined
|
|
1111
|
-
}
|
|
1326
|
+
data-workspace-canvas={isPageWorkspace ? "true" : undefined}
|
|
1327
|
+
data-workspace-mode={isPageWorkspace ? "page" : "canvas"}
|
|
1112
1328
|
>
|
|
1113
1329
|
{isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
|
|
1114
1330
|
<div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
|
|
@@ -1309,8 +1525,38 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1309
1525
|
/>
|
|
1310
1526
|
) : null}
|
|
1311
1527
|
<div
|
|
1312
|
-
className={
|
|
1528
|
+
className={
|
|
1529
|
+
isPageWorkspace
|
|
1530
|
+
? "wre-page-surface relative mx-auto my-8"
|
|
1531
|
+
: "relative"
|
|
1532
|
+
}
|
|
1313
1533
|
data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
|
|
1534
|
+
data-paper-frame={isPageWorkspace ? "true" : undefined}
|
|
1535
|
+
data-debug-page-layout={
|
|
1536
|
+
isPageWorkspace && snapshot.pageLayout
|
|
1537
|
+
? `${snapshot.pageLayout.pageWidth}:${snapshot.pageLayout.pageHeight}:${snapshot.pageLayout.orientation}:${snapshot.pageLayout.sectionIndex}`
|
|
1538
|
+
: undefined
|
|
1539
|
+
}
|
|
1540
|
+
style={
|
|
1541
|
+
isPageWorkspace
|
|
1542
|
+
? {
|
|
1543
|
+
// Phase A (L8 page-native layout): the paper frame
|
|
1544
|
+
// owns paper width/height + browser-native CSS
|
|
1545
|
+
// `zoom` so layout measurement stays truthful
|
|
1546
|
+
// inside the card. `pageShellMetrics.pageFrameStyle`
|
|
1547
|
+
// carries the paper chrome (bg + shadow + rounded +
|
|
1548
|
+
// border) — painted exactly once.
|
|
1549
|
+
...(pageShellMetrics.frameWidthPx
|
|
1550
|
+
? { width: `${pageShellMetrics.frameWidthPx}px` }
|
|
1551
|
+
: {}),
|
|
1552
|
+
...(pageShellMetrics.frameHeightPx
|
|
1553
|
+
? { minHeight: `${pageShellMetrics.frameHeightPx}px` }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1556
|
+
...pageShellMetrics.pageFrameStyle,
|
|
1557
|
+
}
|
|
1558
|
+
: undefined
|
|
1559
|
+
}
|
|
1314
1560
|
>
|
|
1315
1561
|
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
|
|
1316
1562
|
<div
|
|
@@ -1338,32 +1584,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1338
1584
|
className={isPageWorkspace ? "relative" : undefined}
|
|
1339
1585
|
data-document-grid={pageChromeModel.documentGridType}
|
|
1340
1586
|
data-page-border-display={pageChromeModel.pageBorderDisplay}
|
|
1341
|
-
style={
|
|
1342
|
-
? {
|
|
1343
|
-
...pageChromeModel.documentGridStyle,
|
|
1344
|
-
...pageShellMetrics.pageFrameStyle,
|
|
1345
|
-
}
|
|
1346
|
-
: pageChromeModel.documentGridStyle}
|
|
1587
|
+
style={pageChromeModel.documentGridStyle}
|
|
1347
1588
|
>
|
|
1348
|
-
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
1349
|
-
<div
|
|
1350
|
-
data-testid="page-header-band"
|
|
1351
|
-
className="relative z-10 flex items-center justify-between border-b border-border/50 bg-surface/45 px-4 text-[11px] text-secondary backdrop-blur-[1px]"
|
|
1352
|
-
style={pageShellMetrics.headerBandStyle}
|
|
1353
|
-
>
|
|
1354
|
-
<span className="uppercase tracking-[0.12em] text-tertiary">{headerBandLabel}</span>
|
|
1355
|
-
{snapshot.pageLayout?.headerVariants[0] ? (
|
|
1356
|
-
<button
|
|
1357
|
-
type="button"
|
|
1358
|
-
aria-label="Open header story"
|
|
1359
|
-
onClick={props.onOpenHeaderStory}
|
|
1360
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
1361
|
-
>
|
|
1362
|
-
Edit header
|
|
1363
|
-
</button>
|
|
1364
|
-
) : null}
|
|
1365
|
-
</div>
|
|
1366
|
-
) : null}
|
|
1367
1589
|
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
|
|
1368
1590
|
<div
|
|
1369
1591
|
aria-hidden="true"
|
|
@@ -1373,15 +1595,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1373
1595
|
/>
|
|
1374
1596
|
) : null}
|
|
1375
1597
|
<div className={isPageWorkspace ? "relative z-10" : "relative"}>
|
|
1376
|
-
{/*
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
`
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1598
|
+
{/* P8.11 — workspace-level header / footer bands
|
|
1599
|
+
retired. The PM surface now mounts inside
|
|
1600
|
+
`data-pm-body-slot`; per-page header, footer,
|
|
1601
|
+
footnote, and endnote chrome is owned by
|
|
1602
|
+
`TwPageStackChromeLayer` inside `TwChromeOverlay`
|
|
1603
|
+
(see below). When the user clicks a per-page
|
|
1604
|
+
band, the chrome layer reparents PM's DOM node
|
|
1605
|
+
into the active band's `data-pm-portal-slot`;
|
|
1606
|
+
when the user returns to the body, PM slides
|
|
1607
|
+
back into this wrapper. */}
|
|
1608
|
+
<div
|
|
1609
|
+
data-pm-body-slot=""
|
|
1610
|
+
ref={bodySlotRef}
|
|
1611
|
+
style={{ width: "100%" }}
|
|
1612
|
+
>
|
|
1613
|
+
{props.document}
|
|
1614
|
+
</div>
|
|
1385
1615
|
{props.layoutFacet ? (
|
|
1386
1616
|
<TwChromeOverlay
|
|
1387
1617
|
facet={props.layoutFacet}
|
|
@@ -1410,28 +1640,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1410
1640
|
? handleScopeCardAskAgent
|
|
1411
1641
|
: undefined
|
|
1412
1642
|
}
|
|
1643
|
+
pageStackScrollRoot={
|
|
1644
|
+
isPageWorkspace && chromeVisibility.pageChrome
|
|
1645
|
+
? pageStackScrollRoot
|
|
1646
|
+
: undefined
|
|
1647
|
+
}
|
|
1648
|
+
renderFrameRevision={renderFrameRevision}
|
|
1649
|
+
activeStory={snapshot.activeStory}
|
|
1650
|
+
onOpenStory={props.onOpenStory}
|
|
1651
|
+
pmSurfaceElement={pmSurfaceElement}
|
|
1413
1652
|
/>
|
|
1414
1653
|
) : null}
|
|
1415
1654
|
</div>
|
|
1416
|
-
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
1417
|
-
<div
|
|
1418
|
-
data-testid="page-footer-band"
|
|
1419
|
-
className="relative z-10 flex items-center justify-between border-t border-border/50 bg-surface/45 px-4 text-[11px] text-secondary backdrop-blur-[1px]"
|
|
1420
|
-
style={pageShellMetrics.footerBandStyle}
|
|
1421
|
-
>
|
|
1422
|
-
<span className="uppercase tracking-[0.12em] text-tertiary">{footerBandLabel}</span>
|
|
1423
|
-
{snapshot.pageLayout?.footerVariants[0] ? (
|
|
1424
|
-
<button
|
|
1425
|
-
type="button"
|
|
1426
|
-
aria-label="Open footer story"
|
|
1427
|
-
onClick={props.onOpenFooterStory}
|
|
1428
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
1429
|
-
>
|
|
1430
|
-
Edit footer
|
|
1431
|
-
</button>
|
|
1432
|
-
) : null}
|
|
1433
|
-
</div>
|
|
1434
|
-
) : null}
|
|
1435
1655
|
</div>
|
|
1436
1656
|
</div>
|
|
1437
1657
|
</div>
|
|
@@ -1717,8 +1937,6 @@ export interface PageShellMetrics {
|
|
|
1717
1937
|
frameHeightPx?: number;
|
|
1718
1938
|
contentInsetStyle: CSSProperties;
|
|
1719
1939
|
pageFrameStyle: CSSProperties;
|
|
1720
|
-
headerBandStyle: CSSProperties;
|
|
1721
|
-
footerBandStyle: CSSProperties;
|
|
1722
1940
|
}
|
|
1723
1941
|
|
|
1724
1942
|
function buildPageChromeModel(
|
|
@@ -1764,8 +1982,6 @@ export function buildPageShellMetrics(
|
|
|
1764
1982
|
return {
|
|
1765
1983
|
contentInsetStyle: {},
|
|
1766
1984
|
pageFrameStyle: {},
|
|
1767
|
-
headerBandStyle: {},
|
|
1768
|
-
footerBandStyle: {},
|
|
1769
1985
|
frameWidthPx: 0,
|
|
1770
1986
|
frameHeightPx: 0,
|
|
1771
1987
|
};
|
|
@@ -1779,17 +1995,11 @@ export function buildPageShellMetrics(
|
|
|
1779
1995
|
const frameHeightPx = Math.round(pageLayout.pageHeight * pxPerTwip);
|
|
1780
1996
|
const horizontalInsetPx = Math.round(pageLayout.marginLeft * pxPerTwip);
|
|
1781
1997
|
const horizontalInsetRightPx = Math.round(pageLayout.marginRight * pxPerTwip);
|
|
1782
|
-
|
|
1783
|
-
//
|
|
1784
|
-
//
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
Math.round(Math.max(0, pageLayout.marginTop - pageLayout.headerMargin) * pxPerTwip),
|
|
1788
|
-
);
|
|
1789
|
-
const footerBandHeightPx = Math.max(
|
|
1790
|
-
MIN_BAND_HEIGHT_PX,
|
|
1791
|
-
Math.round(Math.max(0, pageLayout.marginBottom - pageLayout.footerMargin) * pxPerTwip),
|
|
1792
|
-
);
|
|
1998
|
+
|
|
1999
|
+
// P8.11 — `headerBandStyle` / `footerBandStyle` removed. The
|
|
2000
|
+
// workspace-level band divs that consumed them are gone; per-page
|
|
2001
|
+
// bands (rendered by `TwPageStackChromeLayer`) compute their own
|
|
2002
|
+
// heights from the runtime's `PageRegionsSnapshot`.
|
|
1793
2003
|
|
|
1794
2004
|
return {
|
|
1795
2005
|
contentInsetStyle: {
|
|
@@ -1802,12 +2012,6 @@ export function buildPageShellMetrics(
|
|
|
1802
2012
|
boxShadow: "0 24px 48px -32px rgba(15, 23, 42, 0.38), 0 8px 20px -18px rgba(15, 23, 42, 0.22)",
|
|
1803
2013
|
border: "1px solid rgba(148, 163, 184, 0.2)",
|
|
1804
2014
|
},
|
|
1805
|
-
headerBandStyle: {
|
|
1806
|
-
minHeight: `${headerBandHeightPx}px`,
|
|
1807
|
-
},
|
|
1808
|
-
footerBandStyle: {
|
|
1809
|
-
minHeight: `${footerBandHeightPx}px`,
|
|
1810
|
-
},
|
|
1811
2015
|
frameWidthPx,
|
|
1812
2016
|
frameHeightPx,
|
|
1813
2017
|
};
|
|
@@ -1849,30 +2053,6 @@ export function resolveZoomMultiplier(
|
|
|
1849
2053
|
);
|
|
1850
2054
|
}
|
|
1851
2055
|
|
|
1852
|
-
function resolvePageBandLabel(
|
|
1853
|
-
region: "header" | "footer",
|
|
1854
|
-
activeStory: RuntimeRenderSnapshot["activeStory"],
|
|
1855
|
-
): string {
|
|
1856
|
-
const regionLabel = region === "header" ? "header" : "footer";
|
|
1857
|
-
let label: string;
|
|
1858
|
-
if (activeStory.kind !== region) {
|
|
1859
|
-
label = region === "header" ? "Header" : "Footer";
|
|
1860
|
-
} else {
|
|
1861
|
-
switch (activeStory.variant) {
|
|
1862
|
-
case "first":
|
|
1863
|
-
label = `First page ${regionLabel}`;
|
|
1864
|
-
break;
|
|
1865
|
-
case "even":
|
|
1866
|
-
label = `Even page ${regionLabel}`;
|
|
1867
|
-
break;
|
|
1868
|
-
default:
|
|
1869
|
-
label = `Default ${regionLabel}`;
|
|
1870
|
-
break;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
return label;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
2056
|
function buildLineNumberMarkers(
|
|
1877
2057
|
blocks: readonly SurfaceBlockSnapshot[],
|
|
1878
2058
|
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|