@beyondwork/docx-react-component 1.0.41 → 1.0.43
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 +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- 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 +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- 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-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- 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 +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -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/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -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,
|
|
@@ -88,6 +89,10 @@ import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
|
88
89
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
89
90
|
import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
|
|
90
91
|
import { TwChromeOverlay } from "./chrome-overlay";
|
|
92
|
+
import {
|
|
93
|
+
cycleScopeIndex,
|
|
94
|
+
shouldHandleScopeNavKey,
|
|
95
|
+
} from "./chrome-overlay/scope-keyboard-cycle";
|
|
91
96
|
|
|
92
97
|
export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
|
|
93
98
|
|
|
@@ -153,6 +158,16 @@ export interface TwReviewWorkspaceProps {
|
|
|
153
158
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
154
159
|
chromePreset?: WordReviewEditorChromePreset;
|
|
155
160
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
161
|
+
/** P9g — live collab session for the `"collab"` chrome preset's top nav. */
|
|
162
|
+
collabSession?: import("../runtime/collab-session.ts").CollabSession;
|
|
163
|
+
collabTransportStatus?: import("../api/awareness-identity-types.ts").TransportStatus;
|
|
164
|
+
collabActorId?: string;
|
|
165
|
+
collabSendBaseline?: {
|
|
166
|
+
originDocumentId: string;
|
|
167
|
+
originPayloadId: string;
|
|
168
|
+
originContentHash: string;
|
|
169
|
+
payloadXml: string;
|
|
170
|
+
};
|
|
156
171
|
reviewQueue?: ReviewQueueSnapshot;
|
|
157
172
|
documentContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
158
173
|
selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
@@ -274,7 +289,18 @@ export interface TwReviewWorkspaceProps {
|
|
|
274
289
|
onAcceptAllChanges?: () => void;
|
|
275
290
|
onRejectAllChanges?: () => void;
|
|
276
291
|
onCloseStory?: () => void;
|
|
292
|
+
/**
|
|
293
|
+
* @deprecated P8.11 — the workspace no longer renders a workspace-level
|
|
294
|
+
* header band with an "Edit header" button; per-page header bands route
|
|
295
|
+
* clicks via `onOpenStory` / `runtime.openStory` directly. The prop
|
|
296
|
+
* remains optional for one release so existing hosts continue to
|
|
297
|
+
* compile; supplying it emits a `console.warn` on mount.
|
|
298
|
+
*/
|
|
277
299
|
onOpenHeaderStory?: () => void;
|
|
300
|
+
/**
|
|
301
|
+
* @deprecated P8.11 — see `onOpenHeaderStory`. Footer variant of the
|
|
302
|
+
* same deprecation.
|
|
303
|
+
*/
|
|
278
304
|
onOpenFooterStory?: () => void;
|
|
279
305
|
/**
|
|
280
306
|
* Open a header/footer story for a specific page. Called when the user
|
|
@@ -284,6 +310,13 @@ export interface TwReviewWorkspaceProps {
|
|
|
284
310
|
*/
|
|
285
311
|
onOpenHeaderStoryForPage?: (pageIndex: number) => void;
|
|
286
312
|
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
313
|
+
/**
|
|
314
|
+
* P8.11 — fired when a per-page chrome band (header / footer) is
|
|
315
|
+
* clicked to promote it into the active editing surface. Wire to
|
|
316
|
+
* `runtime.openStory(target)`; the chrome layer's portal mechanism
|
|
317
|
+
* then reparents the PM surface into the matching band's active slot.
|
|
318
|
+
*/
|
|
319
|
+
onOpenStory?: (target: EditorStoryTarget) => void;
|
|
287
320
|
onSetParagraphIndentation?: (indentation: {
|
|
288
321
|
left?: number;
|
|
289
322
|
right?: number;
|
|
@@ -343,6 +376,34 @@ export interface TwReviewWorkspaceProps {
|
|
|
343
376
|
issueId: string;
|
|
344
377
|
action: import("../api/public-types.ts").ScopeIssueAction;
|
|
345
378
|
}) => void;
|
|
379
|
+
/**
|
|
380
|
+
* R3 — scope card suggestion-group accept button fired. WordReview-
|
|
381
|
+
* Editor relays to `ref.acceptSuggestionGroup(groupId)` which fans
|
|
382
|
+
* out to individual `acceptChange` calls across the group members.
|
|
383
|
+
*/
|
|
384
|
+
onScopeAcceptSuggestionGroup?: (payload: {
|
|
385
|
+
scopeId: string;
|
|
386
|
+
groupId: string;
|
|
387
|
+
}) => void;
|
|
388
|
+
/** R3 — scope card suggestion-group reject. */
|
|
389
|
+
onScopeRejectSuggestionGroup?: (payload: {
|
|
390
|
+
scopeId: string;
|
|
391
|
+
groupId: string;
|
|
392
|
+
}) => void;
|
|
393
|
+
/**
|
|
394
|
+
* K2 — scope card "Ask review agent" fired. WordReviewEditor emits
|
|
395
|
+
* `agent-on-selection-requested` via WordReviewEditorEvent.
|
|
396
|
+
*/
|
|
397
|
+
onScopeAskAgent?: (payload: {
|
|
398
|
+
scopeId: string;
|
|
399
|
+
}) => void;
|
|
400
|
+
/**
|
|
401
|
+
* P3 — optional scope-tag editor slot rendered inside the scope
|
|
402
|
+
* card when `editorRole === "workflow"`. Hosts pass a chip picker,
|
|
403
|
+
* free-text input, or whatever authoring surface they want. Unset
|
|
404
|
+
* in editor/review roles.
|
|
405
|
+
*/
|
|
406
|
+
scopeCardScopeTagEditor?: ReactNode;
|
|
346
407
|
}
|
|
347
408
|
|
|
348
409
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
@@ -352,6 +413,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
352
413
|
} as TwReviewWorkspaceProps & EditorCommandBag;
|
|
353
414
|
const { snapshot, viewState } = props;
|
|
354
415
|
const selectionToolbarRootRef = useRef<HTMLDivElement>(null);
|
|
416
|
+
// P8.11 — body slot wrapping `{props.document}` (the PM surface) + scroll
|
|
417
|
+
// root ref. The chrome layer's `TwPageStackChromeLayer` needs both to
|
|
418
|
+
// measure per-page rects and to reparent PM's DOM node across band
|
|
419
|
+
// portals when `activeStory` changes. See comment near the body slot
|
|
420
|
+
// in the render tree below.
|
|
421
|
+
const bodySlotRef = useRef<HTMLDivElement | null>(null);
|
|
422
|
+
const scrollRootRef = useRef<HTMLDivElement | null>(null);
|
|
423
|
+
const [pmSurfaceElement, setPmSurfaceElement] =
|
|
424
|
+
useState<HTMLElement | null>(null);
|
|
425
|
+
const [pageStackScrollRoot, setPageStackScrollRoot] =
|
|
426
|
+
useState<HTMLElement | null>(null);
|
|
355
427
|
const caps = props.capabilities;
|
|
356
428
|
const isPageWorkspace = props.workspaceMode === "page";
|
|
357
429
|
const markupDisplay = props.markupDisplay;
|
|
@@ -373,6 +445,37 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
373
445
|
const handleScopeCardClose = useCallback(() => {
|
|
374
446
|
setActiveScopeId(null);
|
|
375
447
|
}, []);
|
|
448
|
+
|
|
449
|
+
// P3d: keyboard scope navigation. J / K cycle the active scope in
|
|
450
|
+
// document order; Enter opens the first scope when none is active.
|
|
451
|
+
// `shouldHandleScopeNavKey` + `cycleScopeIndex` are extracted pure
|
|
452
|
+
// helpers so the logic is unit-testable without a workspace mount.
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
const layoutFacet = props.layoutFacet;
|
|
455
|
+
if (!layoutFacet || typeof layoutFacet.getAllScopeCardModels !== "function") {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
const onKey = (event: KeyboardEvent) => {
|
|
459
|
+
if (!shouldHandleScopeNavKey(event)) return;
|
|
460
|
+
const models = layoutFacet.getAllScopeCardModels();
|
|
461
|
+
if (models.length === 0) return;
|
|
462
|
+
const ids = models.map((model) => model.scopeId);
|
|
463
|
+
const key = event.key.toLowerCase();
|
|
464
|
+
if (key === "enter") {
|
|
465
|
+
if (!activeScopeId) {
|
|
466
|
+
setActiveScopeId(ids[0] ?? null);
|
|
467
|
+
event.preventDefault();
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const direction: 1 | -1 = key === "j" ? 1 : -1;
|
|
472
|
+
const next = cycleScopeIndex(activeScopeId, ids, direction);
|
|
473
|
+
setActiveScopeId(next);
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
};
|
|
476
|
+
window.addEventListener("keydown", onKey);
|
|
477
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
478
|
+
}, [props.layoutFacet, activeScopeId]);
|
|
376
479
|
const onScopeModeChangeRequested = props.onScopeModeChangeRequested;
|
|
377
480
|
const handleScopeCardModeChange = useCallback(
|
|
378
481
|
(scopeId: string, mode: import("../api/public-types.ts").WorkflowScopeMode) => {
|
|
@@ -391,10 +494,32 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
391
494
|
},
|
|
392
495
|
[onScopeIssueActionRequested],
|
|
393
496
|
);
|
|
497
|
+
const onScopeAcceptSuggestionGroup = props.onScopeAcceptSuggestionGroup;
|
|
498
|
+
const handleScopeCardAcceptSuggestionGroup = useCallback(
|
|
499
|
+
(scopeId: string, groupId: string) => {
|
|
500
|
+
onScopeAcceptSuggestionGroup?.({ scopeId, groupId });
|
|
501
|
+
},
|
|
502
|
+
[onScopeAcceptSuggestionGroup],
|
|
503
|
+
);
|
|
504
|
+
const onScopeRejectSuggestionGroup = props.onScopeRejectSuggestionGroup;
|
|
505
|
+
const handleScopeCardRejectSuggestionGroup = useCallback(
|
|
506
|
+
(scopeId: string, groupId: string) => {
|
|
507
|
+
onScopeRejectSuggestionGroup?.({ scopeId, groupId });
|
|
508
|
+
},
|
|
509
|
+
[onScopeRejectSuggestionGroup],
|
|
510
|
+
);
|
|
511
|
+
const onScopeAskAgent = props.onScopeAskAgent;
|
|
512
|
+
const handleScopeCardAskAgent = useCallback(
|
|
513
|
+
(scopeId: string) => {
|
|
514
|
+
onScopeAskAgent?.({ scopeId });
|
|
515
|
+
},
|
|
516
|
+
[onScopeAskAgent],
|
|
517
|
+
);
|
|
394
518
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
519
|
+
// Numeric zooms resolve immediately; "pageWidth" / "onePage" need the
|
|
520
|
+
// page-frame dimensions to fit against — they're resolved below once
|
|
521
|
+
// `pageShellMetrics` has been computed (P2.c).
|
|
522
|
+
const numericZoomScale = typeof zoomLevel === "number" ? zoomLevel / 100 : 1;
|
|
398
523
|
const chromePreset = resolveChromePreset(props.chromePreset, props.reviewMode);
|
|
399
524
|
const chromeOptions = resolveChromePresetOptions(chromePreset, props.chromeOptions);
|
|
400
525
|
const preserveOnlyCount = caps?.preserveOnlyCount ??
|
|
@@ -412,6 +537,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
412
537
|
});
|
|
413
538
|
const reviewRailAvailable = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
|
|
414
539
|
const [viewportWidth, setViewportWidth] = useState<number | undefined>(() => readViewportWidth());
|
|
540
|
+
const [viewportHeight, setViewportHeight] = useState<number | undefined>(() => readViewportHeight());
|
|
415
541
|
const [reviewRailOpen, setReviewRailOpen] = useState(() =>
|
|
416
542
|
getInitialReviewRailOpen({
|
|
417
543
|
viewportWidth: readViewportWidth(),
|
|
@@ -461,6 +587,33 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
461
587
|
}
|
|
462
588
|
return props.activeSelectionTool;
|
|
463
589
|
}, [props.activeSelectionTool, chromeVisibility.contextToolbars]);
|
|
590
|
+
const pageShellMetrics = useMemo(
|
|
591
|
+
() => buildPageShellMetrics(snapshot.pageLayout),
|
|
592
|
+
[snapshot.pageLayout],
|
|
593
|
+
);
|
|
594
|
+
// P2.c — resolve "pageWidth" / "onePage" against the active section's
|
|
595
|
+
// real paper dimensions. Numeric zooms pass through. Falls back to
|
|
596
|
+
// `numericZoomScale` (1.0 for symbolic zooms when paper dims are
|
|
597
|
+
// unavailable, e.g., during initial load).
|
|
598
|
+
const zoomScale = useMemo(() => {
|
|
599
|
+
if (typeof zoomLevel === "number") return numericZoomScale;
|
|
600
|
+
return resolveZoomMultiplier(
|
|
601
|
+
zoomLevel,
|
|
602
|
+
pageShellMetrics.frameWidthPx ?? 0,
|
|
603
|
+
pageShellMetrics.frameHeightPx ?? 0,
|
|
604
|
+
viewportWidth,
|
|
605
|
+
viewportHeight,
|
|
606
|
+
);
|
|
607
|
+
}, [
|
|
608
|
+
zoomLevel,
|
|
609
|
+
numericZoomScale,
|
|
610
|
+
pageShellMetrics.frameWidthPx,
|
|
611
|
+
pageShellMetrics.frameHeightPx,
|
|
612
|
+
viewportWidth,
|
|
613
|
+
viewportHeight,
|
|
614
|
+
]);
|
|
615
|
+
const pageZoomBucket =
|
|
616
|
+
!isPageWorkspace ? undefined : zoomScale < 1 ? "low" : zoomScale > 1 ? "high" : "base";
|
|
464
617
|
const selectionToolbarPlacement = useMemo(() => {
|
|
465
618
|
// Prefer render-frame anchors when the layout facet is available — this
|
|
466
619
|
// keeps the tool glued to kernel coordinates across zoom, scroll, and
|
|
@@ -496,12 +649,41 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
496
649
|
renderFrameRevision,
|
|
497
650
|
]);
|
|
498
651
|
const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
652
|
+
// P5b — status-bar facts derived from the layout facet so the
|
|
653
|
+
// Page-N-of-M display + measurement-fidelity badge ("E" / "C" / "C+F")
|
|
654
|
+
// refresh on every layout-affecting facet event. The subscription
|
|
655
|
+
// above bumps `renderFrameRevision` on the same kinds; including it
|
|
656
|
+
// in the dependency list re-runs this memo without a separate
|
|
657
|
+
// subscription.
|
|
658
|
+
const statusBarPageFacts = useMemo(() => {
|
|
659
|
+
const facet = props.layoutFacet;
|
|
660
|
+
if (!facet) {
|
|
661
|
+
return {
|
|
662
|
+
displayPageNumber: null as number | null,
|
|
663
|
+
pageCount: null as number | null,
|
|
664
|
+
measurementFidelity: undefined as
|
|
665
|
+
| import("../api/public-types.ts").PublicMeasurementFidelity
|
|
666
|
+
| undefined,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const head = selectionPosition;
|
|
670
|
+
const pageRef = facet.getPageForOffset(head, snapshot.activeStory);
|
|
671
|
+
const displayPageNumber =
|
|
672
|
+
pageRef !== null && typeof pageRef.pageIndex === "number"
|
|
673
|
+
? facet.getDisplayPageNumber(pageRef.pageIndex) ?? pageRef.pageIndex + 1
|
|
674
|
+
: null;
|
|
675
|
+
const pageCount = facet.getPageCount();
|
|
676
|
+
return {
|
|
677
|
+
displayPageNumber,
|
|
678
|
+
pageCount,
|
|
679
|
+
measurementFidelity: facet.getMeasurementFidelity(),
|
|
680
|
+
};
|
|
681
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
682
|
+
}, [props.layoutFacet, selectionPosition, snapshot.activeStory, renderFrameRevision]);
|
|
683
|
+
// P8.11 — `headerBandLabel` / `footerBandLabel` retired along with the
|
|
684
|
+
// workspace-level bands. Per-page bands in `TwPageStackChromeLayer`
|
|
685
|
+
// render the actual header / footer story blocks via
|
|
686
|
+
// `TwRegionBlockRenderer`, so a label row is no longer meaningful here.
|
|
505
687
|
const hidePageBorderForActiveEditing =
|
|
506
688
|
isPageWorkspace &&
|
|
507
689
|
snapshot.activeStory.kind === "main" &&
|
|
@@ -568,29 +750,131 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
568
750
|
}
|
|
569
751
|
}, [isPageWorkspace, snapshot.activeStory.kind]);
|
|
570
752
|
|
|
753
|
+
// P8.11 — capture the scroll-root DOM element on mount so the chrome
|
|
754
|
+
// overlay's `TwPageStackChromeLayer` can measure per-page rects and
|
|
755
|
+
// observe DOM mutations. `scrollRootRef` is attached to the existing
|
|
756
|
+
// `[data-wre-scroll-root]` container; rely on a mount effect rather
|
|
757
|
+
// than a ref callback so render-time state stays cheap.
|
|
758
|
+
useEffect(() => {
|
|
759
|
+
if (scrollRootRef.current !== pageStackScrollRoot) {
|
|
760
|
+
setPageStackScrollRoot(scrollRootRef.current);
|
|
761
|
+
}
|
|
762
|
+
// A `useEffect` re-runs after every render; the comparison guard
|
|
763
|
+
// keeps `setPageStackScrollRoot` from firing every commit. The
|
|
764
|
+
// scroll-root identity only changes when the component re-mounts.
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// P8.11 — capture the PM surface DOM element. The ProseMirror surface
|
|
768
|
+
// mounts inside `bodySlotRef` on its own schedule (the PM constructor
|
|
769
|
+
// runs inside the `TwProseMirrorSurface` child component). A
|
|
770
|
+
// `MutationObserver` scoped to the body slot's `childList` picks up
|
|
771
|
+
// the PM root on first commit; once captured, the chrome layer owns
|
|
772
|
+
// reparent state (including portal-slot promotion), so we skip
|
|
773
|
+
// further updates unless PM is actually disconnected from the
|
|
774
|
+
// document (e.g. session/document swap tearing PM down).
|
|
775
|
+
useEffect(() => {
|
|
776
|
+
const slot = bodySlotRef.current;
|
|
777
|
+
if (!slot) return undefined;
|
|
778
|
+
|
|
779
|
+
// If we already hold a live reference, the chrome layer may have
|
|
780
|
+
// portaled PM into a per-page band — PM has left `bodySlotRef` but
|
|
781
|
+
// is still connected to the document. We keep the reference until
|
|
782
|
+
// the node is fully disconnected.
|
|
783
|
+
if (pmSurfaceElement && pmSurfaceElement.isConnected) {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const readPm = (): HTMLElement | null =>
|
|
788
|
+
slot.querySelector<HTMLElement>(".ProseMirror");
|
|
789
|
+
|
|
790
|
+
const current = readPm();
|
|
791
|
+
if (current !== pmSurfaceElement) {
|
|
792
|
+
setPmSurfaceElement(current);
|
|
793
|
+
}
|
|
794
|
+
const runtime = slot.ownerDocument?.defaultView as
|
|
795
|
+
| (Window & { MutationObserver?: typeof MutationObserver })
|
|
796
|
+
| null;
|
|
797
|
+
if (!runtime?.MutationObserver) return undefined;
|
|
798
|
+
const observer = new runtime.MutationObserver(() => {
|
|
799
|
+
const next = readPm();
|
|
800
|
+
if (next !== null && next !== pmSurfaceElement) {
|
|
801
|
+
setPmSurfaceElement(next);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
// `childList: true, subtree: false` — we only care when children of
|
|
805
|
+
// the body slot change (e.g. PM is added for the first time).
|
|
806
|
+
// Subtree mutations (PM's own edits) are not our concern and would
|
|
807
|
+
// fire on every keystroke.
|
|
808
|
+
observer.observe(slot, { childList: true, subtree: false });
|
|
809
|
+
return () => observer.disconnect();
|
|
810
|
+
}, [pmSurfaceElement]);
|
|
811
|
+
|
|
812
|
+
// P8.11 — deprecation shim for the legacy `onOpenHeaderStory` /
|
|
813
|
+
// `onOpenFooterStory` props. Per-page chrome bands route clicks via
|
|
814
|
+
// `onOpenStory` + `runtime.openStory` directly; the workspace-level
|
|
815
|
+
// bands that consumed these callbacks are gone. Kept optional for one
|
|
816
|
+
// release so existing hosts compile; a mount-time `console.warn` nudges
|
|
817
|
+
// them toward `onOpenStory`.
|
|
818
|
+
useEffect(() => {
|
|
819
|
+
if (props.onOpenHeaderStory) {
|
|
820
|
+
// eslint-disable-next-line no-console
|
|
821
|
+
console.warn(
|
|
822
|
+
"[docx-react-component] `onOpenHeaderStory` is deprecated. Per-page header bands route clicks via runtime.openStory directly. (P8)",
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (props.onOpenFooterStory) {
|
|
826
|
+
// eslint-disable-next-line no-console
|
|
827
|
+
console.warn(
|
|
828
|
+
"[docx-react-component] `onOpenFooterStory` is deprecated. Per-page footer bands route clicks via runtime.openStory directly. (P8)",
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
// Mount-once: we only want to nudge hosts at startup, not per render.
|
|
832
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
833
|
+
}, []);
|
|
834
|
+
|
|
571
835
|
useEffect(() => {
|
|
572
836
|
if (typeof window === "undefined") {
|
|
573
837
|
return;
|
|
574
838
|
}
|
|
575
839
|
|
|
576
|
-
const
|
|
840
|
+
const updateViewport = () => {
|
|
577
841
|
setViewportWidth(readViewportWidth());
|
|
842
|
+
setViewportHeight(readViewportHeight());
|
|
578
843
|
};
|
|
579
844
|
|
|
580
|
-
|
|
581
|
-
window.addEventListener("resize",
|
|
845
|
+
updateViewport();
|
|
846
|
+
window.addEventListener("resize", updateViewport);
|
|
582
847
|
return () => {
|
|
583
|
-
window.removeEventListener("resize",
|
|
848
|
+
window.removeEventListener("resize", updateViewport);
|
|
584
849
|
};
|
|
585
850
|
}, []);
|
|
586
851
|
|
|
587
|
-
// Subscribe to layout facet events so chrome re-projects
|
|
588
|
-
//
|
|
852
|
+
// Subscribe to layout facet events so chrome re-projects whenever the
|
|
853
|
+
// engine produces new pagination state, fields dirty, or measurement
|
|
854
|
+
// fidelity changes. P5b broadened this beyond the original P3.b set
|
|
855
|
+
// ("zoom_changed" / "render_frame_ready") so the status-bar Page-N-of-M
|
|
856
|
+
// and fidelity badge transition in real time; the hardening commit
|
|
857
|
+
// added "measurement_backend_ready" so the canvas backend swap also
|
|
858
|
+
// refreshes the badge. P14.b adds "layout_committed" — a single
|
|
859
|
+
// coalesced event per applyPatch — so consumers that only care
|
|
860
|
+
// about "the engine just finished a build" can react once instead
|
|
861
|
+
// of N times.
|
|
589
862
|
useEffect(() => {
|
|
590
863
|
if (!props.layoutFacet) return;
|
|
591
864
|
const unsub = props.layoutFacet.subscribe((event) => {
|
|
592
|
-
|
|
593
|
-
|
|
865
|
+
switch (event.kind) {
|
|
866
|
+
case "zoom_changed":
|
|
867
|
+
case "render_frame_ready":
|
|
868
|
+
case "layout_recomputed":
|
|
869
|
+
case "incremental_relayout":
|
|
870
|
+
case "page_count_changed":
|
|
871
|
+
case "page_field_dirtied":
|
|
872
|
+
case "measurement_backend_ready":
|
|
873
|
+
case "layout_committed":
|
|
874
|
+
setRenderFrameRevision((n) => n + 1);
|
|
875
|
+
break;
|
|
876
|
+
default:
|
|
877
|
+
break;
|
|
594
878
|
}
|
|
595
879
|
});
|
|
596
880
|
return unsub;
|
|
@@ -658,6 +942,20 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
658
942
|
<div className="px-3 pt-3">
|
|
659
943
|
<ChromePresetToolbar
|
|
660
944
|
chromePreset={chromePreset}
|
|
945
|
+
{...(props.collabSession ? { collabSession: props.collabSession } : {})}
|
|
946
|
+
{...(props.collabTransportStatus
|
|
947
|
+
? { collabTransportStatus: props.collabTransportStatus }
|
|
948
|
+
: {})}
|
|
949
|
+
{...(props.activeCommentId !== undefined
|
|
950
|
+
? { activeCommentId: props.activeCommentId }
|
|
951
|
+
: {})}
|
|
952
|
+
{...(props.collabActorId !== undefined
|
|
953
|
+
? { collabActorId: props.collabActorId }
|
|
954
|
+
: {})}
|
|
955
|
+
{...(props.collabSendBaseline
|
|
956
|
+
? { collabSendBaseline: props.collabSendBaseline }
|
|
957
|
+
: {})}
|
|
958
|
+
chromeOptionsResolved={chromeOptions}
|
|
661
959
|
capabilities={caps}
|
|
662
960
|
compatibility={snapshot.compatibility}
|
|
663
961
|
warnings={snapshot.warnings}
|
|
@@ -916,6 +1214,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
916
1214
|
{/* Document column */}
|
|
917
1215
|
<div className="flex flex-1 flex-col min-w-0">
|
|
918
1216
|
<div
|
|
1217
|
+
ref={scrollRootRef}
|
|
919
1218
|
className="flex-1 overflow-y-auto bg-surface"
|
|
920
1219
|
data-wre-scroll-root="true"
|
|
921
1220
|
>
|
|
@@ -923,11 +1222,32 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
923
1222
|
ref={selectionToolbarRootRef}
|
|
924
1223
|
className={`mx-auto min-h-full w-full ${
|
|
925
1224
|
isPageWorkspace
|
|
926
|
-
? "wre-page-chrome wre-page-surface relative
|
|
1225
|
+
? "wre-page-chrome wre-page-surface relative my-8 overflow-hidden"
|
|
927
1226
|
: "wre-canvas-surface relative my-8 overflow-hidden"
|
|
928
1227
|
}`}
|
|
929
1228
|
data-zoom-bucket={pageZoomBucket}
|
|
930
|
-
|
|
1229
|
+
data-zoom-scale={isPageWorkspace ? zoomScale : undefined}
|
|
1230
|
+
style={
|
|
1231
|
+
isPageWorkspace
|
|
1232
|
+
? {
|
|
1233
|
+
// P2.a — real-dim page frame: width/height from
|
|
1234
|
+
// `pageLayout.pageWidth/pageHeight × FRAME_PX_PER_TWIP_AT_96DPI`
|
|
1235
|
+
// so every paper size renders at its
|
|
1236
|
+
// Word-matching CSS px. `max-w-[840px]` retired.
|
|
1237
|
+
...(pageShellMetrics.frameWidthPx
|
|
1238
|
+
? { width: `${pageShellMetrics.frameWidthPx}px` }
|
|
1239
|
+
: {}),
|
|
1240
|
+
...(pageShellMetrics.frameHeightPx
|
|
1241
|
+
? { minHeight: `${pageShellMetrics.frameHeightPx}px` }
|
|
1242
|
+
: {}),
|
|
1243
|
+
// P2.b — browser-native CSS `zoom` rescales layout
|
|
1244
|
+
// so `getBoundingClientRect()` and hit-test offsets
|
|
1245
|
+
// stay truthful at any zoom — no inverse-projection
|
|
1246
|
+
// math needed downstream.
|
|
1247
|
+
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1248
|
+
}
|
|
1249
|
+
: undefined
|
|
1250
|
+
}
|
|
931
1251
|
>
|
|
932
1252
|
{isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
|
|
933
1253
|
<div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
|
|
@@ -1164,25 +1484,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1164
1484
|
}
|
|
1165
1485
|
: pageChromeModel.documentGridStyle}
|
|
1166
1486
|
>
|
|
1167
|
-
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
1168
|
-
<div
|
|
1169
|
-
data-testid="page-header-band"
|
|
1170
|
-
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]"
|
|
1171
|
-
style={pageShellMetrics.headerBandStyle}
|
|
1172
|
-
>
|
|
1173
|
-
<span className="uppercase tracking-[0.12em] text-tertiary">{headerBandLabel}</span>
|
|
1174
|
-
{snapshot.pageLayout?.headerVariants[0] ? (
|
|
1175
|
-
<button
|
|
1176
|
-
type="button"
|
|
1177
|
-
aria-label="Open header story"
|
|
1178
|
-
onClick={props.onOpenHeaderStory}
|
|
1179
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
1180
|
-
>
|
|
1181
|
-
Edit header
|
|
1182
|
-
</button>
|
|
1183
|
-
) : null}
|
|
1184
|
-
</div>
|
|
1185
|
-
) : null}
|
|
1186
1487
|
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
|
|
1187
1488
|
<div
|
|
1188
1489
|
aria-hidden="true"
|
|
@@ -1192,15 +1493,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1192
1493
|
/>
|
|
1193
1494
|
) : null}
|
|
1194
1495
|
<div className={isPageWorkspace ? "relative z-10" : "relative"}>
|
|
1195
|
-
{/*
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
`
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1496
|
+
{/* P8.11 — workspace-level header / footer bands
|
|
1497
|
+
retired. The PM surface now mounts inside
|
|
1498
|
+
`data-pm-body-slot`; per-page header, footer,
|
|
1499
|
+
footnote, and endnote chrome is owned by
|
|
1500
|
+
`TwPageStackChromeLayer` inside `TwChromeOverlay`
|
|
1501
|
+
(see below). When the user clicks a per-page
|
|
1502
|
+
band, the chrome layer reparents PM's DOM node
|
|
1503
|
+
into the active band's `data-pm-portal-slot`;
|
|
1504
|
+
when the user returns to the body, PM slides
|
|
1505
|
+
back into this wrapper. */}
|
|
1506
|
+
<div
|
|
1507
|
+
data-pm-body-slot=""
|
|
1508
|
+
ref={bodySlotRef}
|
|
1509
|
+
style={{ width: "100%" }}
|
|
1510
|
+
>
|
|
1511
|
+
{props.document}
|
|
1512
|
+
</div>
|
|
1204
1513
|
{props.layoutFacet ? (
|
|
1205
1514
|
<TwChromeOverlay
|
|
1206
1515
|
facet={props.layoutFacet}
|
|
@@ -1208,32 +1517,39 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1208
1517
|
onSetColumnWidth={props.onSetColumnWidth}
|
|
1209
1518
|
onSetRowHeight={props.onSetRowHeight}
|
|
1210
1519
|
activeScopeId={activeScopeId}
|
|
1520
|
+
editorRole={viewState.editorRole}
|
|
1521
|
+
scopeCardScopeTagEditor={props.scopeCardScopeTagEditor}
|
|
1211
1522
|
onScopeStripeClick={handleScopeStripeClick}
|
|
1212
1523
|
onScopeCardClose={handleScopeCardClose}
|
|
1213
1524
|
onScopeCardModeChange={handleScopeCardModeChange}
|
|
1214
1525
|
onScopeCardIssueAction={handleScopeCardIssueAction}
|
|
1526
|
+
onScopeCardAcceptSuggestionGroup={
|
|
1527
|
+
onScopeAcceptSuggestionGroup
|
|
1528
|
+
? handleScopeCardAcceptSuggestionGroup
|
|
1529
|
+
: undefined
|
|
1530
|
+
}
|
|
1531
|
+
onScopeCardRejectSuggestionGroup={
|
|
1532
|
+
onScopeRejectSuggestionGroup
|
|
1533
|
+
? handleScopeCardRejectSuggestionGroup
|
|
1534
|
+
: undefined
|
|
1535
|
+
}
|
|
1536
|
+
onScopeCardAskAgent={
|
|
1537
|
+
onScopeAskAgent
|
|
1538
|
+
? handleScopeCardAskAgent
|
|
1539
|
+
: undefined
|
|
1540
|
+
}
|
|
1541
|
+
pageStackScrollRoot={
|
|
1542
|
+
isPageWorkspace && chromeVisibility.pageChrome
|
|
1543
|
+
? pageStackScrollRoot
|
|
1544
|
+
: undefined
|
|
1545
|
+
}
|
|
1546
|
+
renderFrameRevision={renderFrameRevision}
|
|
1547
|
+
activeStory={snapshot.activeStory}
|
|
1548
|
+
onOpenStory={props.onOpenStory}
|
|
1549
|
+
pmSurfaceElement={pmSurfaceElement}
|
|
1215
1550
|
/>
|
|
1216
1551
|
) : null}
|
|
1217
1552
|
</div>
|
|
1218
|
-
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
1219
|
-
<div
|
|
1220
|
-
data-testid="page-footer-band"
|
|
1221
|
-
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]"
|
|
1222
|
-
style={pageShellMetrics.footerBandStyle}
|
|
1223
|
-
>
|
|
1224
|
-
<span className="uppercase tracking-[0.12em] text-tertiary">{footerBandLabel}</span>
|
|
1225
|
-
{snapshot.pageLayout?.footerVariants[0] ? (
|
|
1226
|
-
<button
|
|
1227
|
-
type="button"
|
|
1228
|
-
aria-label="Open footer story"
|
|
1229
|
-
onClick={props.onOpenFooterStory}
|
|
1230
|
-
className="rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
1231
|
-
>
|
|
1232
|
-
Edit footer
|
|
1233
|
-
</button>
|
|
1234
|
-
) : null}
|
|
1235
|
-
</div>
|
|
1236
|
-
) : null}
|
|
1237
1553
|
</div>
|
|
1238
1554
|
</div>
|
|
1239
1555
|
</div>
|
|
@@ -1257,6 +1573,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1257
1573
|
? props.documentContextAnalytics
|
|
1258
1574
|
: null
|
|
1259
1575
|
}
|
|
1576
|
+
displayPageNumber={statusBarPageFacts.displayPageNumber}
|
|
1577
|
+
pageCount={statusBarPageFacts.pageCount}
|
|
1578
|
+
measurementFidelity={statusBarPageFacts.measurementFidelity}
|
|
1260
1579
|
/>
|
|
1261
1580
|
) : null}
|
|
1262
1581
|
</div>
|
|
@@ -1364,6 +1683,10 @@ function readViewportWidth(): number | undefined {
|
|
|
1364
1683
|
return typeof window === "undefined" ? undefined : window.innerWidth;
|
|
1365
1684
|
}
|
|
1366
1685
|
|
|
1686
|
+
function readViewportHeight(): number | undefined {
|
|
1687
|
+
return typeof window === "undefined" ? undefined : window.innerHeight;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1367
1690
|
function shouldHidePageBorderForSelection(
|
|
1368
1691
|
selection: EditorViewStateSnapshot["selection"],
|
|
1369
1692
|
): boolean {
|
|
@@ -1496,11 +1819,22 @@ const EMPTY_PAGE_CHROME_MODEL: PageChromeModel = {
|
|
|
1496
1819
|
|
|
1497
1820
|
const DOCUMENT_CONTENT_TOP_PADDING_PX = 40;
|
|
1498
1821
|
|
|
1499
|
-
|
|
1822
|
+
// P2.a — real-dimension page frame. Page frame width/height are
|
|
1823
|
+
// `pageLayout.pageWidth/pageHeight × FRAME_PX_PER_TWIP_AT_96DPI` so
|
|
1824
|
+
// every paper size renders at its Word-matching CSS px (Letter
|
|
1825
|
+
// 816×1056, A4 794×1123, Legal 816×1344, …). Constants are exported
|
|
1826
|
+
// so tests + harness panels can derive the same values.
|
|
1827
|
+
export const FRAME_PX_PER_TWIP_AT_96DPI = 96 / 1440;
|
|
1828
|
+
/** Floor on header/footer band heights so empty bands stay clickable. */
|
|
1829
|
+
export const MIN_BAND_HEIGHT_PX = 20;
|
|
1830
|
+
|
|
1831
|
+
export interface PageShellMetrics {
|
|
1832
|
+
/** P2.a — page frame CSS px width = `pageWidth × FRAME_PX_PER_TWIP_AT_96DPI`. */
|
|
1833
|
+
frameWidthPx?: number;
|
|
1834
|
+
/** P2.a — page frame CSS px height = `pageHeight × FRAME_PX_PER_TWIP_AT_96DPI`. */
|
|
1835
|
+
frameHeightPx?: number;
|
|
1500
1836
|
contentInsetStyle: CSSProperties;
|
|
1501
1837
|
pageFrameStyle: CSSProperties;
|
|
1502
|
-
headerBandStyle: CSSProperties;
|
|
1503
|
-
footerBandStyle: CSSProperties;
|
|
1504
1838
|
}
|
|
1505
1839
|
|
|
1506
1840
|
function buildPageChromeModel(
|
|
@@ -1539,41 +1873,36 @@ function buildPageChromeModel(
|
|
|
1539
1873
|
};
|
|
1540
1874
|
}
|
|
1541
1875
|
|
|
1542
|
-
function buildPageShellMetrics(
|
|
1876
|
+
export function buildPageShellMetrics(
|
|
1543
1877
|
pageLayout: RuntimeRenderSnapshot["pageLayout"] | undefined,
|
|
1544
1878
|
): PageShellMetrics {
|
|
1545
1879
|
if (!pageLayout) {
|
|
1546
1880
|
return {
|
|
1547
1881
|
contentInsetStyle: {},
|
|
1548
1882
|
pageFrameStyle: {},
|
|
1549
|
-
|
|
1550
|
-
|
|
1883
|
+
frameWidthPx: 0,
|
|
1884
|
+
frameHeightPx: 0,
|
|
1551
1885
|
};
|
|
1552
1886
|
}
|
|
1553
1887
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
);
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
)
|
|
1566
|
-
|
|
1567
|
-
40,
|
|
1568
|
-
Math.min(96, Math.round(pageLayout.footerMargin * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP + 16)),
|
|
1569
|
-
);
|
|
1888
|
+
// P2.a — frame dimensions follow the section's real twip width/height
|
|
1889
|
+
// so every paper size in the catalog renders at its Word-matching CSS
|
|
1890
|
+
// px (Letter 816×1056, A4 794×1123, Legal 816×1344, …).
|
|
1891
|
+
const pxPerTwip = FRAME_PX_PER_TWIP_AT_96DPI;
|
|
1892
|
+
const frameWidthPx = Math.round(pageLayout.pageWidth * pxPerTwip);
|
|
1893
|
+
const frameHeightPx = Math.round(pageLayout.pageHeight * pxPerTwip);
|
|
1894
|
+
const horizontalInsetPx = Math.round(pageLayout.marginLeft * pxPerTwip);
|
|
1895
|
+
const horizontalInsetRightPx = Math.round(pageLayout.marginRight * pxPerTwip);
|
|
1896
|
+
|
|
1897
|
+
// P8.11 — `headerBandStyle` / `footerBandStyle` removed. The
|
|
1898
|
+
// workspace-level band divs that consumed them are gone; per-page
|
|
1899
|
+
// bands (rendered by `TwPageStackChromeLayer`) compute their own
|
|
1900
|
+
// heights from the runtime's `PageRegionsSnapshot`.
|
|
1570
1901
|
|
|
1571
1902
|
return {
|
|
1572
1903
|
contentInsetStyle: {
|
|
1573
1904
|
paddingLeft: `${horizontalInsetPx}px`,
|
|
1574
|
-
paddingRight: `${
|
|
1575
|
-
paddingTop: `${Math.max(20, verticalInsetPx - 12)}px`,
|
|
1576
|
-
paddingBottom: `${Math.max(20, Math.round(pageLayout.marginBottom * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP) - 12)}px`,
|
|
1905
|
+
paddingRight: `${horizontalInsetRightPx}px`,
|
|
1577
1906
|
},
|
|
1578
1907
|
pageFrameStyle: {
|
|
1579
1908
|
backgroundColor: "var(--color-page-bg)",
|
|
@@ -1581,37 +1910,45 @@ function buildPageShellMetrics(
|
|
|
1581
1910
|
boxShadow: "0 24px 48px -32px rgba(15, 23, 42, 0.38), 0 8px 20px -18px rgba(15, 23, 42, 0.22)",
|
|
1582
1911
|
border: "1px solid rgba(148, 163, 184, 0.2)",
|
|
1583
1912
|
},
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
},
|
|
1587
|
-
footerBandStyle: {
|
|
1588
|
-
minHeight: `${footerBandHeightPx}px`,
|
|
1589
|
-
},
|
|
1913
|
+
frameWidthPx,
|
|
1914
|
+
frameHeightPx,
|
|
1590
1915
|
};
|
|
1591
1916
|
}
|
|
1592
1917
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1918
|
+
// P2.c — fit-to-width / fit-to-page resolves against the active section's
|
|
1919
|
+
// real paper size (not a global constant), so Letter and A4 produce the
|
|
1920
|
+
// expected 1.029:1 fit-width ratio at the same viewport. Clamped so
|
|
1921
|
+
// extreme viewports don't pin the editor at unreadable zooms.
|
|
1922
|
+
const FIT_WIDTH_CHROME_RESERVATION_PX = 96;
|
|
1923
|
+
const FIT_HEIGHT_CHROME_RESERVATION_PX = 180;
|
|
1924
|
+
const MIN_FIT_ZOOM = 0.5;
|
|
1925
|
+
const MAX_FIT_ZOOM = 2.0;
|
|
1926
|
+
|
|
1927
|
+
export function resolveZoomMultiplier(
|
|
1928
|
+
zoomLevel: number | "pageWidth" | "onePage",
|
|
1929
|
+
frameWidthPx: number,
|
|
1930
|
+
frameHeightPx: number,
|
|
1931
|
+
viewportWidth: number | undefined,
|
|
1932
|
+
viewportHeight: number | undefined,
|
|
1933
|
+
): number {
|
|
1934
|
+
if (typeof zoomLevel === "number") {
|
|
1935
|
+
return zoomLevel / 100;
|
|
1613
1936
|
}
|
|
1614
|
-
return
|
|
1937
|
+
if (!viewportWidth || frameWidthPx <= 0) return 1;
|
|
1938
|
+
const widthFit =
|
|
1939
|
+
(viewportWidth - FIT_WIDTH_CHROME_RESERVATION_PX) / frameWidthPx;
|
|
1940
|
+
if (zoomLevel === "pageWidth") {
|
|
1941
|
+
return Math.max(MIN_FIT_ZOOM, Math.min(MAX_FIT_ZOOM, widthFit));
|
|
1942
|
+
}
|
|
1943
|
+
if (!viewportHeight || frameHeightPx <= 0) {
|
|
1944
|
+
return Math.max(MIN_FIT_ZOOM, Math.min(MAX_FIT_ZOOM, widthFit));
|
|
1945
|
+
}
|
|
1946
|
+
const heightFit =
|
|
1947
|
+
(viewportHeight - FIT_HEIGHT_CHROME_RESERVATION_PX) / frameHeightPx;
|
|
1948
|
+
return Math.max(
|
|
1949
|
+
MIN_FIT_ZOOM,
|
|
1950
|
+
Math.min(MAX_FIT_ZOOM, Math.min(widthFit, heightFit)),
|
|
1951
|
+
);
|
|
1615
1952
|
}
|
|
1616
1953
|
|
|
1617
1954
|
function buildLineNumberMarkers(
|