@beyondwork/docx-react-component 1.0.17 → 1.0.19
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 +8 -2
- package/package.json +32 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -0
|
@@ -1,40 +1,89 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type FocusEventHandler,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
type Ref,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
2
12
|
|
|
3
13
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
14
|
+
import { ChevronLeft, ChevronRight, List } from "lucide-react";
|
|
4
15
|
|
|
5
16
|
import type {
|
|
6
17
|
CommentSidebarThreadSnapshot,
|
|
18
|
+
DocumentNavigationSnapshot,
|
|
19
|
+
EditorViewStateSnapshot,
|
|
20
|
+
FormattingStateSnapshot,
|
|
7
21
|
RuntimeRenderSnapshot,
|
|
22
|
+
StyleCatalogSnapshot,
|
|
23
|
+
SurfaceBlockSnapshot,
|
|
8
24
|
TrackedChangeEntrySnapshot,
|
|
25
|
+
WorkspaceMode,
|
|
26
|
+
ZoomLevel,
|
|
9
27
|
} from "../api/public-types";
|
|
28
|
+
import { findPageForOffset } from "../runtime/document-navigation.ts";
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
31
|
+
estimateBlockHeight,
|
|
32
|
+
estimateParagraphLineCount,
|
|
33
|
+
estimateParagraphLineHeight,
|
|
34
|
+
getUsableColumnWidth,
|
|
35
|
+
} from "../runtime/page-layout-estimation.ts";
|
|
10
36
|
import type { SessionCapabilities } from "../runtime/session-capabilities";
|
|
37
|
+
import type {
|
|
38
|
+
SelectionToolbarAnchor,
|
|
39
|
+
SelectionToolbarModel,
|
|
40
|
+
} from "../ui/headless/selection-toolbar-model";
|
|
11
41
|
import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
|
|
42
|
+
import { preserveEditorSelectionMouseDown } from "../ui/headless/preserve-editor-selection";
|
|
12
43
|
import { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
44
|
+
import { TwPageRuler } from "./chrome/tw-page-ruler";
|
|
13
45
|
import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
|
|
14
46
|
import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
15
47
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
16
|
-
import { TwToolbar
|
|
48
|
+
import { TwToolbar } from "./toolbar/tw-toolbar";
|
|
17
49
|
|
|
18
50
|
export interface TwReviewWorkspaceProps {
|
|
19
51
|
snapshot: RuntimeRenderSnapshot;
|
|
52
|
+
viewState: EditorViewStateSnapshot;
|
|
20
53
|
currentUserId?: string;
|
|
21
54
|
capabilities?: SessionCapabilities;
|
|
22
55
|
reviewMode?: "editing" | "review";
|
|
23
56
|
document: ReactNode;
|
|
24
|
-
|
|
57
|
+
workspaceMode: WorkspaceMode;
|
|
58
|
+
zoomLevel?: ZoomLevel;
|
|
59
|
+
formattingState?: FormattingStateSnapshot;
|
|
60
|
+
styleCatalog?: StyleCatalogSnapshot;
|
|
25
61
|
activeRailTab: ReviewRailTab;
|
|
26
62
|
activeCommentId?: string;
|
|
27
63
|
activeRevisionId?: string;
|
|
28
64
|
showTrackedChanges: boolean;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
65
|
+
selectionToolbar?: SelectionToolbarModel | null;
|
|
66
|
+
selectionToolbarAnchor?: SelectionToolbarAnchor | null;
|
|
67
|
+
documentNavigation?: DocumentNavigationSnapshot;
|
|
68
|
+
onWorkspaceModeChange: (value: WorkspaceMode) => void;
|
|
69
|
+
onZoomChange?: (level: ZoomLevel) => void;
|
|
32
70
|
onActiveRailTabChange: (value: ReviewRailTab) => void;
|
|
33
71
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
34
72
|
onUndo: () => void;
|
|
35
73
|
onRedo: () => void;
|
|
74
|
+
onSetParagraphStyle?: (styleId: string) => void;
|
|
75
|
+
onToggleBold?: () => void;
|
|
76
|
+
onToggleItalic?: () => void;
|
|
77
|
+
onToggleUnderline?: () => void;
|
|
78
|
+
onOutdent?: () => void;
|
|
79
|
+
onIndent?: () => void;
|
|
36
80
|
onAddComment: () => void;
|
|
81
|
+
onAddCommentFromSelection?: () => void;
|
|
37
82
|
onExport: () => void;
|
|
83
|
+
onDismissSelectionToolbar?: () => void;
|
|
84
|
+
onSelectionToolbarFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
85
|
+
onSelectionToolbarBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
86
|
+
selectionToolbarRef?: Ref<HTMLDivElement>;
|
|
38
87
|
onOpenComment: (thread: CommentSidebarThreadSnapshot) => void;
|
|
39
88
|
onResolveComment: (commentId: string) => void;
|
|
40
89
|
onReopenComment?: (commentId: string) => void;
|
|
@@ -45,17 +94,84 @@ export interface TwReviewWorkspaceProps {
|
|
|
45
94
|
onRejectRevision: (revisionId: string) => void;
|
|
46
95
|
onAcceptAllChanges: () => void;
|
|
47
96
|
onRejectAllChanges: () => void;
|
|
97
|
+
onCloseStory?: () => void;
|
|
98
|
+
onOpenHeaderStory?: () => void;
|
|
99
|
+
onOpenFooterStory?: () => void;
|
|
100
|
+
onSetParagraphIndentation?: (indentation: {
|
|
101
|
+
left?: number;
|
|
102
|
+
right?: number;
|
|
103
|
+
firstLine?: number;
|
|
104
|
+
hanging?: number;
|
|
105
|
+
}) => void;
|
|
106
|
+
onSetParagraphTabStops?: (tabStops: Array<{ pos: number; val?: string; leader?: string }>) => void;
|
|
107
|
+
onRestartNumbering?: () => void;
|
|
108
|
+
onContinueNumbering?: () => void;
|
|
109
|
+
onNavigateHeading?: (headingId: string) => void;
|
|
48
110
|
}
|
|
49
111
|
|
|
50
112
|
export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
|
|
51
|
-
const { snapshot } = props;
|
|
113
|
+
const { snapshot, viewState } = props;
|
|
114
|
+
const selectionToolbarRootRef = useRef<HTMLDivElement>(null);
|
|
52
115
|
const caps = props.capabilities;
|
|
53
|
-
const
|
|
116
|
+
const isPageWorkspace = props.workspaceMode === "page";
|
|
117
|
+
const markupDisplay: MarkupDisplay = isPageWorkspace ? "all" : "clean";
|
|
118
|
+
const [navOpen, setNavOpen] = useState(false);
|
|
119
|
+
const [layoutToolsOpen, setLayoutToolsOpen] = useState(false);
|
|
120
|
+
const zoomLevel = props.zoomLevel ?? 100;
|
|
121
|
+
const zoomScale = typeof zoomLevel === "number" ? zoomLevel / 100 : 1;
|
|
54
122
|
const preserveOnlyCount = caps?.preserveOnlyCount ??
|
|
55
123
|
snapshot.compatibility.featureEntries.filter(
|
|
56
124
|
(entry) => entry.featureClass === "preserve-only",
|
|
57
125
|
).length;
|
|
58
126
|
const showReviewRail = caps?.reviewRailVisible ?? true;
|
|
127
|
+
const headings = props.documentNavigation?.headings ?? [];
|
|
128
|
+
const selectionPosition =
|
|
129
|
+
viewState.selection.activeRange.kind === "node"
|
|
130
|
+
? viewState.selection.activeRange.at
|
|
131
|
+
: viewState.selection.head;
|
|
132
|
+
const activeParagraphLayout = resolveActiveParagraphLayout(snapshot.surface, selectionPosition);
|
|
133
|
+
const pageChromeModel = useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
buildPageChromeModel(
|
|
136
|
+
snapshot.surface,
|
|
137
|
+
snapshot.pageLayout,
|
|
138
|
+
props.documentNavigation,
|
|
139
|
+
snapshot.activeStory,
|
|
140
|
+
),
|
|
141
|
+
[props.documentNavigation, snapshot.activeStory, snapshot.pageLayout, snapshot.surface],
|
|
142
|
+
);
|
|
143
|
+
const selectionToolbarPlacement = resolveSelectionToolbarPlacement(
|
|
144
|
+
props.selectionToolbarAnchor,
|
|
145
|
+
selectionToolbarRootRef.current,
|
|
146
|
+
zoomScale,
|
|
147
|
+
);
|
|
148
|
+
const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
|
|
149
|
+
const pageShellMetrics = useMemo(
|
|
150
|
+
() => buildPageShellMetrics(snapshot.pageLayout),
|
|
151
|
+
[snapshot.pageLayout],
|
|
152
|
+
);
|
|
153
|
+
const hidePageBorderForActiveEditing =
|
|
154
|
+
isPageWorkspace &&
|
|
155
|
+
snapshot.activeStory.kind === "main" &&
|
|
156
|
+
shouldHidePageBorderForSelection(viewState.selection);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (isPageWorkspace && snapshot.activeStory.kind !== "main") {
|
|
160
|
+
setLayoutToolsOpen(true);
|
|
161
|
+
}
|
|
162
|
+
}, [isPageWorkspace, snapshot.activeStory.kind]);
|
|
163
|
+
|
|
164
|
+
const dismissSelectionToolbar = useCallback(() => {
|
|
165
|
+
props.onDismissSelectionToolbar?.();
|
|
166
|
+
}, [props.onDismissSelectionToolbar]);
|
|
167
|
+
|
|
168
|
+
const runWithSelectionToolbarDismiss = useCallback(
|
|
169
|
+
(action?: () => void) => () => {
|
|
170
|
+
dismissSelectionToolbar();
|
|
171
|
+
action?.();
|
|
172
|
+
},
|
|
173
|
+
[dismissSelectionToolbar],
|
|
174
|
+
);
|
|
59
175
|
|
|
60
176
|
return (
|
|
61
177
|
<Tooltip.Provider delayDuration={400}>
|
|
@@ -65,41 +181,366 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
|
|
|
65
181
|
capabilities={caps}
|
|
66
182
|
compatibility={snapshot.compatibility}
|
|
67
183
|
warnings={snapshot.warnings}
|
|
68
|
-
|
|
184
|
+
workspaceMode={props.workspaceMode}
|
|
185
|
+
zoomLevel={props.zoomLevel}
|
|
186
|
+
formattingState={props.formattingState}
|
|
187
|
+
styleCatalog={props.styleCatalog}
|
|
69
188
|
showTrackedChanges={props.showTrackedChanges}
|
|
70
|
-
onUndo={props.onUndo}
|
|
71
|
-
onRedo={props.onRedo}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
189
|
+
onUndo={runWithSelectionToolbarDismiss(props.onUndo)}
|
|
190
|
+
onRedo={runWithSelectionToolbarDismiss(props.onRedo)}
|
|
191
|
+
onSetParagraphStyle={props.onSetParagraphStyle
|
|
192
|
+
? (styleId) => {
|
|
193
|
+
dismissSelectionToolbar();
|
|
194
|
+
props.onSetParagraphStyle?.(styleId);
|
|
195
|
+
}
|
|
196
|
+
: undefined}
|
|
197
|
+
onToggleBold={runWithSelectionToolbarDismiss(props.onToggleBold)}
|
|
198
|
+
onToggleItalic={runWithSelectionToolbarDismiss(props.onToggleItalic)}
|
|
199
|
+
onToggleUnderline={runWithSelectionToolbarDismiss(props.onToggleUnderline)}
|
|
200
|
+
onOutdent={runWithSelectionToolbarDismiss(props.onOutdent)}
|
|
201
|
+
onIndent={runWithSelectionToolbarDismiss(props.onIndent)}
|
|
202
|
+
onAddComment={runWithSelectionToolbarDismiss(props.onAddComment)}
|
|
203
|
+
onExport={runWithSelectionToolbarDismiss(props.onExport)}
|
|
204
|
+
activeStory={snapshot.activeStory}
|
|
205
|
+
onCloseStory={props.onCloseStory
|
|
206
|
+
? runWithSelectionToolbarDismiss(props.onCloseStory)
|
|
207
|
+
: undefined}
|
|
208
|
+
onWorkspaceModeChange={(value) => {
|
|
209
|
+
dismissSelectionToolbar();
|
|
210
|
+
props.onWorkspaceModeChange(value);
|
|
211
|
+
}}
|
|
212
|
+
onZoomChange={props.onZoomChange
|
|
213
|
+
? (level) => {
|
|
214
|
+
dismissSelectionToolbar();
|
|
215
|
+
props.onZoomChange?.(level);
|
|
216
|
+
}
|
|
217
|
+
: undefined}
|
|
218
|
+
onShowTrackedChangesChange={(show) => {
|
|
219
|
+
dismissSelectionToolbar();
|
|
220
|
+
props.onShowTrackedChangesChange(show);
|
|
221
|
+
}}
|
|
76
222
|
/>
|
|
77
223
|
|
|
78
224
|
<TwAlertBanner snapshot={snapshot} preserveOnlyCount={preserveOnlyCount} />
|
|
79
225
|
|
|
80
226
|
<div className="flex flex-1 min-h-0">
|
|
227
|
+
{/* Collapsible document navigator — page mode only */}
|
|
228
|
+
{isPageWorkspace ? (
|
|
229
|
+
<aside
|
|
230
|
+
aria-label="Document navigator"
|
|
231
|
+
className={`shrink-0 border-r border-border bg-surface transition-[width] duration-200 ${
|
|
232
|
+
navOpen ? "w-48" : "w-0"
|
|
233
|
+
} overflow-hidden`}
|
|
234
|
+
>
|
|
235
|
+
{navOpen ? (
|
|
236
|
+
<div className="flex h-full flex-col">
|
|
237
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
238
|
+
<span className="text-xs font-medium text-secondary uppercase tracking-wider">Navigator</span>
|
|
239
|
+
<Tooltip.Root>
|
|
240
|
+
<Tooltip.Trigger asChild>
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
aria-label="Collapse navigator"
|
|
244
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
245
|
+
onClick={() => {
|
|
246
|
+
dismissSelectionToolbar();
|
|
247
|
+
setNavOpen(false);
|
|
248
|
+
}}
|
|
249
|
+
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary hover:bg-surface-hover transition-colors"
|
|
250
|
+
>
|
|
251
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
252
|
+
</button>
|
|
253
|
+
</Tooltip.Trigger>
|
|
254
|
+
<Tooltip.Portal>
|
|
255
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
256
|
+
Collapse navigator
|
|
257
|
+
</Tooltip.Content>
|
|
258
|
+
</Tooltip.Portal>
|
|
259
|
+
</Tooltip.Root>
|
|
260
|
+
</div>
|
|
261
|
+
<nav className="flex-1 overflow-y-auto px-2 py-2" aria-label="Document headings">
|
|
262
|
+
{headings.length > 0 ? (
|
|
263
|
+
<ul className="space-y-0.5">
|
|
264
|
+
{headings.map((entry) => (
|
|
265
|
+
<li key={entry.headingId}>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
className="block w-full truncate rounded-md px-2 py-1 text-left text-xs text-primary hover:bg-surface-hover"
|
|
269
|
+
style={{ paddingLeft: `${8 + (entry.level - 1) * 12}px` }}
|
|
270
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
271
|
+
onClick={() => {
|
|
272
|
+
dismissSelectionToolbar();
|
|
273
|
+
props.onNavigateHeading?.(entry.headingId);
|
|
274
|
+
setNavOpen(false);
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
{entry.text}
|
|
278
|
+
</button>
|
|
279
|
+
</li>
|
|
280
|
+
))}
|
|
281
|
+
</ul>
|
|
282
|
+
) : (
|
|
283
|
+
<p className="px-2 py-4 text-xs text-tertiary">No headings found.</p>
|
|
284
|
+
)}
|
|
285
|
+
</nav>
|
|
286
|
+
</div>
|
|
287
|
+
) : null}
|
|
288
|
+
</aside>
|
|
289
|
+
) : null}
|
|
290
|
+
|
|
291
|
+
{/* Navigator expand toggle — page mode only when collapsed */}
|
|
292
|
+
{isPageWorkspace && !navOpen ? (
|
|
293
|
+
<div className="shrink-0 flex items-start pt-2 pl-1">
|
|
294
|
+
<Tooltip.Root>
|
|
295
|
+
<Tooltip.Trigger asChild>
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
aria-label="Open document navigator"
|
|
299
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
300
|
+
onClick={() => {
|
|
301
|
+
dismissSelectionToolbar();
|
|
302
|
+
setNavOpen(true);
|
|
303
|
+
}}
|
|
304
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary hover:bg-surface-hover transition-colors"
|
|
305
|
+
>
|
|
306
|
+
<List className="h-3.5 w-3.5" />
|
|
307
|
+
</button>
|
|
308
|
+
</Tooltip.Trigger>
|
|
309
|
+
<Tooltip.Portal>
|
|
310
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
311
|
+
Open document navigator
|
|
312
|
+
</Tooltip.Content>
|
|
313
|
+
</Tooltip.Portal>
|
|
314
|
+
</Tooltip.Root>
|
|
315
|
+
</div>
|
|
316
|
+
) : null}
|
|
317
|
+
|
|
81
318
|
{/* Document column */}
|
|
82
319
|
<div className="flex flex-1 flex-col min-w-0">
|
|
83
|
-
<div
|
|
320
|
+
<div
|
|
321
|
+
className={`flex-1 overflow-y-auto ${isPageWorkspace ? "bg-surface" : "bg-canvas"}`}
|
|
322
|
+
data-wre-scroll-root="true"
|
|
323
|
+
>
|
|
84
324
|
<div
|
|
325
|
+
ref={selectionToolbarRootRef}
|
|
85
326
|
className={`mx-auto min-h-full ${
|
|
86
|
-
|
|
87
|
-
? "max-w-[
|
|
88
|
-
: "bg-canvas"
|
|
327
|
+
isPageWorkspace
|
|
328
|
+
? "wre-page-chrome wre-page-surface relative max-w-[840px] my-8 overflow-hidden"
|
|
329
|
+
: "wre-canvas-surface relative bg-canvas"
|
|
89
330
|
}`}
|
|
331
|
+
style={isPageWorkspace && zoomScale !== 1 ? { transform: `scale(${zoomScale})`, transformOrigin: "top center" } : undefined}
|
|
90
332
|
>
|
|
91
|
-
{
|
|
92
|
-
<div className="
|
|
93
|
-
<
|
|
94
|
-
|
|
333
|
+
{isPageWorkspace && snapshot.pageLayout ? (
|
|
334
|
+
<div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
|
|
335
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
336
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-secondary">
|
|
337
|
+
<span className="rounded-full bg-canvas px-2 py-1 font-medium text-primary">
|
|
338
|
+
{activePage
|
|
339
|
+
? `Page ${activePage.pageIndex + 1} of ${props.documentNavigation?.pageCount ?? 1}`
|
|
340
|
+
: "Page workspace"}
|
|
341
|
+
</span>
|
|
342
|
+
<span>{`Section ${snapshot.pageLayout.sectionIndex + 1}`}</span>
|
|
343
|
+
<span className="uppercase tracking-[0.12em] text-tertiary">
|
|
344
|
+
{snapshot.pageLayout.orientation}
|
|
345
|
+
</span>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="flex items-center gap-2">
|
|
348
|
+
{snapshot.activeStory.kind !== "main" ? (
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
aria-label="Return to document body"
|
|
352
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
353
|
+
onClick={runWithSelectionToolbarDismiss(props.onCloseStory)}
|
|
354
|
+
className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
355
|
+
>
|
|
356
|
+
Body
|
|
357
|
+
</button>
|
|
358
|
+
) : null}
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
aria-label="Toggle layout tools"
|
|
362
|
+
aria-expanded={layoutToolsOpen}
|
|
363
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
364
|
+
onClick={() => {
|
|
365
|
+
dismissSelectionToolbar();
|
|
366
|
+
setLayoutToolsOpen((open) => !open);
|
|
367
|
+
}}
|
|
368
|
+
className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
369
|
+
>
|
|
370
|
+
<ChevronRight className={`h-3.5 w-3.5 transition-transform ${layoutToolsOpen ? "rotate-90" : ""}`} />
|
|
371
|
+
Layout tools
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
) : null}
|
|
377
|
+
{isPageWorkspace && snapshot.pageLayout && layoutToolsOpen ? (
|
|
378
|
+
<div className="px-5 pt-3">
|
|
379
|
+
<TwPageRuler
|
|
380
|
+
pageLayout={snapshot.pageLayout}
|
|
381
|
+
viewState={viewState}
|
|
382
|
+
paragraphLayout={activeParagraphLayout}
|
|
95
383
|
readOnly={snapshot.readOnly}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
384
|
+
onReturnToBody={props.onCloseStory
|
|
385
|
+
? runWithSelectionToolbarDismiss(props.onCloseStory)
|
|
386
|
+
: () => undefined}
|
|
387
|
+
onOpenHeader={props.onOpenHeaderStory
|
|
388
|
+
? runWithSelectionToolbarDismiss(props.onOpenHeaderStory)
|
|
389
|
+
: undefined}
|
|
390
|
+
onOpenFooter={props.onOpenFooterStory
|
|
391
|
+
? runWithSelectionToolbarDismiss(props.onOpenFooterStory)
|
|
392
|
+
: undefined}
|
|
393
|
+
onSetIndentation={props.onSetParagraphIndentation
|
|
394
|
+
? (indentation) => {
|
|
395
|
+
dismissSelectionToolbar();
|
|
396
|
+
props.onSetParagraphIndentation?.(indentation);
|
|
397
|
+
}
|
|
398
|
+
: undefined}
|
|
399
|
+
onSetTabStops={props.onSetParagraphTabStops
|
|
400
|
+
? (tabStops) => {
|
|
401
|
+
dismissSelectionToolbar();
|
|
402
|
+
props.onSetParagraphTabStops?.(tabStops);
|
|
403
|
+
}
|
|
404
|
+
: undefined}
|
|
405
|
+
onRestartNumbering={props.onRestartNumbering
|
|
406
|
+
? runWithSelectionToolbarDismiss(props.onRestartNumbering)
|
|
407
|
+
: undefined}
|
|
408
|
+
onContinueNumbering={props.onContinueNumbering
|
|
409
|
+
? runWithSelectionToolbarDismiss(props.onContinueNumbering)
|
|
410
|
+
: undefined}
|
|
99
411
|
/>
|
|
100
412
|
</div>
|
|
101
413
|
) : null}
|
|
102
|
-
{props.
|
|
414
|
+
{props.selectionToolbar && selectionToolbarPlacement ? (
|
|
415
|
+
<div className="pointer-events-none absolute inset-0 z-20" data-testid="selection-toolbar-overlay">
|
|
416
|
+
<div
|
|
417
|
+
className="pointer-events-auto absolute"
|
|
418
|
+
data-placement={selectionToolbarPlacement.placement}
|
|
419
|
+
style={selectionToolbarPlacement.style}
|
|
420
|
+
>
|
|
421
|
+
<TwSelectionToolbar
|
|
422
|
+
ref={props.selectionToolbarRef}
|
|
423
|
+
model={props.selectionToolbar}
|
|
424
|
+
disabledReason={props.selectionToolbar.disabledReason}
|
|
425
|
+
onFocusCapture={props.onSelectionToolbarFocusCapture}
|
|
426
|
+
onBlurCapture={props.onSelectionToolbarBlurCapture}
|
|
427
|
+
onToggleBold={props.onToggleBold}
|
|
428
|
+
onToggleItalic={props.onToggleItalic}
|
|
429
|
+
onToggleUnderline={props.onToggleUnderline}
|
|
430
|
+
onAddComment={props.onAddCommentFromSelection ?? props.onAddComment}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
) : null}
|
|
435
|
+
{props.selectionToolbar && !selectionToolbarPlacement ? (
|
|
436
|
+
<div
|
|
437
|
+
className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
|
|
438
|
+
data-testid="selection-toolbar-fallback"
|
|
439
|
+
>
|
|
440
|
+
<div className="pointer-events-auto" data-placement="fallback">
|
|
441
|
+
<TwSelectionToolbar
|
|
442
|
+
ref={props.selectionToolbarRef}
|
|
443
|
+
model={props.selectionToolbar}
|
|
444
|
+
disabledReason={props.selectionToolbar.disabledReason}
|
|
445
|
+
onFocusCapture={props.onSelectionToolbarFocusCapture}
|
|
446
|
+
onBlurCapture={props.onSelectionToolbarBlurCapture}
|
|
447
|
+
onToggleBold={props.onToggleBold}
|
|
448
|
+
onToggleItalic={props.onToggleItalic}
|
|
449
|
+
onToggleUnderline={props.onToggleUnderline}
|
|
450
|
+
onAddComment={props.onAddCommentFromSelection ?? props.onAddComment}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
) : null}
|
|
455
|
+
<div
|
|
456
|
+
className={isPageWorkspace ? "relative" : undefined}
|
|
457
|
+
data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
|
|
458
|
+
>
|
|
459
|
+
{isPageWorkspace && pageChromeModel.lineNumberingEnabled ? (
|
|
460
|
+
<div
|
|
461
|
+
aria-hidden="true"
|
|
462
|
+
className="pointer-events-none absolute inset-y-0 left-0 z-10"
|
|
463
|
+
data-testid="page-line-number-gutter"
|
|
464
|
+
style={{ width: `${pageChromeModel.gutterWidthPx}px` }}
|
|
465
|
+
>
|
|
466
|
+
{pageChromeModel.lineMarkers.map((marker) => (
|
|
467
|
+
<span
|
|
468
|
+
key={marker.id}
|
|
469
|
+
className="absolute right-2 font-[family-name:var(--font-legal-sans)] text-[10px] font-medium tabular-nums tracking-[0.12em] text-tertiary/80"
|
|
470
|
+
style={{ top: `${marker.topPx}px` }}
|
|
471
|
+
>
|
|
472
|
+
{marker.label}
|
|
473
|
+
</span>
|
|
474
|
+
))}
|
|
475
|
+
</div>
|
|
476
|
+
) : null}
|
|
477
|
+
<div
|
|
478
|
+
className={isPageWorkspace && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
|
|
479
|
+
style={isPageWorkspace ? pageShellMetrics.contentInsetStyle : undefined}
|
|
480
|
+
>
|
|
481
|
+
<div
|
|
482
|
+
className={isPageWorkspace ? "relative" : undefined}
|
|
483
|
+
data-document-grid={pageChromeModel.documentGridType}
|
|
484
|
+
data-page-border-display={pageChromeModel.pageBorderDisplay}
|
|
485
|
+
style={isPageWorkspace
|
|
486
|
+
? {
|
|
487
|
+
...pageChromeModel.documentGridStyle,
|
|
488
|
+
...pageShellMetrics.pageFrameStyle,
|
|
489
|
+
}
|
|
490
|
+
: pageChromeModel.documentGridStyle}
|
|
491
|
+
>
|
|
492
|
+
{isPageWorkspace ? (
|
|
493
|
+
<div
|
|
494
|
+
data-testid="page-header-band"
|
|
495
|
+
className="relative z-10 flex items-center justify-between border-b border-dashed border-border/60 px-4 text-[11px] text-secondary"
|
|
496
|
+
style={pageShellMetrics.headerBandStyle}
|
|
497
|
+
>
|
|
498
|
+
<span className="uppercase tracking-[0.12em] text-tertiary">Header</span>
|
|
499
|
+
{snapshot.pageLayout?.headerVariants[0] ? (
|
|
500
|
+
<button
|
|
501
|
+
type="button"
|
|
502
|
+
aria-label="Open header story"
|
|
503
|
+
onClick={props.onOpenHeaderStory}
|
|
504
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
505
|
+
>
|
|
506
|
+
Edit header
|
|
507
|
+
</button>
|
|
508
|
+
) : null}
|
|
509
|
+
</div>
|
|
510
|
+
) : null}
|
|
511
|
+
{isPageWorkspace && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
|
|
512
|
+
<div
|
|
513
|
+
aria-hidden="true"
|
|
514
|
+
className="pointer-events-none absolute inset-0 z-0 rounded-[2px]"
|
|
515
|
+
data-testid="page-border-overlay"
|
|
516
|
+
style={pageChromeModel.pageBorderStyle}
|
|
517
|
+
/>
|
|
518
|
+
) : null}
|
|
519
|
+
<div className={isPageWorkspace ? "relative z-10" : undefined}>
|
|
520
|
+
{props.document}
|
|
521
|
+
</div>
|
|
522
|
+
{isPageWorkspace ? (
|
|
523
|
+
<div
|
|
524
|
+
data-testid="page-footer-band"
|
|
525
|
+
className="relative z-10 flex items-center justify-between border-t border-dashed border-border/60 px-4 text-[11px] text-secondary"
|
|
526
|
+
style={pageShellMetrics.footerBandStyle}
|
|
527
|
+
>
|
|
528
|
+
<span className="uppercase tracking-[0.12em] text-tertiary">Footer</span>
|
|
529
|
+
{snapshot.pageLayout?.footerVariants[0] ? (
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
aria-label="Open footer story"
|
|
533
|
+
onClick={props.onOpenFooterStory}
|
|
534
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface"
|
|
535
|
+
>
|
|
536
|
+
Edit footer
|
|
537
|
+
</button>
|
|
538
|
+
) : null}
|
|
539
|
+
</div>
|
|
540
|
+
) : null}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
103
544
|
</div>
|
|
104
545
|
</div>
|
|
105
546
|
|
|
@@ -141,3 +582,461 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
|
|
|
141
582
|
</Tooltip.Provider>
|
|
142
583
|
);
|
|
143
584
|
}
|
|
585
|
+
|
|
586
|
+
function shouldHidePageBorderForSelection(
|
|
587
|
+
selection: EditorViewStateSnapshot["selection"],
|
|
588
|
+
): boolean {
|
|
589
|
+
if (selection.isCollapsed) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return selection.activeRange.kind === "range";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function resolveActiveParagraphLayout(
|
|
597
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
598
|
+
position: number,
|
|
599
|
+
): {
|
|
600
|
+
leftIndent: number;
|
|
601
|
+
rightIndent: number;
|
|
602
|
+
firstLineOffset: number;
|
|
603
|
+
tabStops: Array<{ pos: number; val?: string; leader?: string }>;
|
|
604
|
+
} | null {
|
|
605
|
+
const paragraph = surface ? findActiveParagraph(surface.blocks, position) : null;
|
|
606
|
+
if (!paragraph) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
leftIndent: paragraph.indentation?.left ?? 0,
|
|
612
|
+
rightIndent: paragraph.indentation?.right ?? 0,
|
|
613
|
+
firstLineOffset:
|
|
614
|
+
paragraph.indentation?.firstLine ??
|
|
615
|
+
(paragraph.indentation?.hanging ? -paragraph.indentation.hanging : 0),
|
|
616
|
+
tabStops: paragraph.tabStops ? [...paragraph.tabStops] : [],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function findActiveParagraph(
|
|
621
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
622
|
+
position: number,
|
|
623
|
+
): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
|
|
624
|
+
for (const block of blocks) {
|
|
625
|
+
if (block.kind === "paragraph" && position >= block.from && position <= block.to) {
|
|
626
|
+
return block;
|
|
627
|
+
}
|
|
628
|
+
if (block.kind === "table") {
|
|
629
|
+
for (const row of block.rows) {
|
|
630
|
+
for (const cell of row.cells) {
|
|
631
|
+
const paragraph = findActiveParagraph(cell.content, position);
|
|
632
|
+
if (paragraph) {
|
|
633
|
+
return paragraph;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (block.kind === "sdt_block") {
|
|
639
|
+
const paragraph = findActiveParagraph(block.children, position);
|
|
640
|
+
if (paragraph) {
|
|
641
|
+
return paragraph;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
interface PageChromeModel {
|
|
649
|
+
lineNumberingEnabled: boolean;
|
|
650
|
+
gutterWidthPx: number;
|
|
651
|
+
lineMarkers: Array<{ id: string; label: string; topPx: number }>;
|
|
652
|
+
showPageBorder: boolean;
|
|
653
|
+
pageBorderDisplay: string;
|
|
654
|
+
pageBorderStyle: CSSProperties | undefined;
|
|
655
|
+
documentGridType: string;
|
|
656
|
+
documentGridStyle: CSSProperties | undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const EMPTY_PAGE_CHROME_MODEL: PageChromeModel = {
|
|
660
|
+
lineNumberingEnabled: false,
|
|
661
|
+
gutterWidthPx: 0,
|
|
662
|
+
lineMarkers: [],
|
|
663
|
+
showPageBorder: false,
|
|
664
|
+
pageBorderDisplay: "none",
|
|
665
|
+
pageBorderStyle: undefined,
|
|
666
|
+
documentGridType: "none",
|
|
667
|
+
documentGridStyle: undefined,
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const DOCUMENT_CONTENT_TOP_PADDING_PX = 40;
|
|
671
|
+
|
|
672
|
+
interface PageShellMetrics {
|
|
673
|
+
contentInsetStyle: CSSProperties;
|
|
674
|
+
pageFrameStyle: CSSProperties;
|
|
675
|
+
headerBandStyle: CSSProperties;
|
|
676
|
+
footerBandStyle: CSSProperties;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function buildPageChromeModel(
|
|
680
|
+
surface: RuntimeRenderSnapshot["surface"] | undefined,
|
|
681
|
+
pageLayout: RuntimeRenderSnapshot["pageLayout"] | undefined,
|
|
682
|
+
navigation: DocumentNavigationSnapshot | undefined,
|
|
683
|
+
activeStory: RuntimeRenderSnapshot["activeStory"],
|
|
684
|
+
): PageChromeModel {
|
|
685
|
+
if (!surface || !pageLayout || !navigation || activeStory.kind !== "main") {
|
|
686
|
+
return EMPTY_PAGE_CHROME_MODEL;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const lineMarkers = buildLineNumberMarkers(surface.blocks, navigation.pages);
|
|
690
|
+
const lineNumberingEnabled =
|
|
691
|
+
Boolean(pageLayout.lineNumbering) && lineMarkers.length > 0;
|
|
692
|
+
const distance = pageLayout.lineNumbering?.distance ?? 0;
|
|
693
|
+
const gutterWidthPx = lineNumberingEnabled
|
|
694
|
+
? Math.max(40, Math.min(88, 24 + Math.round(distance * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP)))
|
|
695
|
+
: 0;
|
|
696
|
+
const showPageBorder = shouldRenderPageBorder(pageLayout, navigation.pages, navigation.activePageIndex);
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
lineNumberingEnabled,
|
|
700
|
+
gutterWidthPx,
|
|
701
|
+
lineMarkers,
|
|
702
|
+
showPageBorder,
|
|
703
|
+
pageBorderDisplay: pageLayout.pageBorders?.display ?? "none",
|
|
704
|
+
pageBorderStyle: showPageBorder ? buildPageBorderStyle(pageLayout) : undefined,
|
|
705
|
+
documentGridType: pageLayout.documentGrid?.type ?? "none",
|
|
706
|
+
documentGridStyle: buildDocumentGridStyle(pageLayout.documentGrid),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function buildPageShellMetrics(
|
|
711
|
+
pageLayout: RuntimeRenderSnapshot["pageLayout"] | undefined,
|
|
712
|
+
): PageShellMetrics {
|
|
713
|
+
if (!pageLayout) {
|
|
714
|
+
return {
|
|
715
|
+
contentInsetStyle: {},
|
|
716
|
+
pageFrameStyle: {},
|
|
717
|
+
headerBandStyle: {},
|
|
718
|
+
footerBandStyle: {},
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const horizontalInsetPx = Math.max(
|
|
723
|
+
24,
|
|
724
|
+
Math.min(120, Math.round(pageLayout.marginLeft * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP)),
|
|
725
|
+
);
|
|
726
|
+
const verticalInsetPx = Math.max(
|
|
727
|
+
24,
|
|
728
|
+
Math.min(140, Math.round(pageLayout.marginTop * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP)),
|
|
729
|
+
);
|
|
730
|
+
const headerBandHeightPx = Math.max(
|
|
731
|
+
40,
|
|
732
|
+
Math.min(96, Math.round(pageLayout.headerMargin * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP + 16)),
|
|
733
|
+
);
|
|
734
|
+
const footerBandHeightPx = Math.max(
|
|
735
|
+
40,
|
|
736
|
+
Math.min(96, Math.round(pageLayout.footerMargin * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP + 16)),
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
contentInsetStyle: {
|
|
741
|
+
paddingLeft: `${horizontalInsetPx}px`,
|
|
742
|
+
paddingRight: `${horizontalInsetPx}px`,
|
|
743
|
+
paddingTop: `${Math.max(20, verticalInsetPx - 12)}px`,
|
|
744
|
+
paddingBottom: `${Math.max(20, Math.round(pageLayout.marginBottom * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP) - 12)}px`,
|
|
745
|
+
},
|
|
746
|
+
pageFrameStyle: {
|
|
747
|
+
backgroundColor: "var(--color-page-bg)",
|
|
748
|
+
},
|
|
749
|
+
headerBandStyle: {
|
|
750
|
+
minHeight: `${headerBandHeightPx}px`,
|
|
751
|
+
},
|
|
752
|
+
footerBandStyle: {
|
|
753
|
+
minHeight: `${footerBandHeightPx}px`,
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function buildLineNumberMarkers(
|
|
759
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
760
|
+
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|
|
761
|
+
): Array<{ id: string; label: string; topPx: number }> {
|
|
762
|
+
const markers: Array<{ id: string; label: string; topPx: number }> = [];
|
|
763
|
+
if (pages.length === 0) {
|
|
764
|
+
return markers;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let currentTopTwips = 0;
|
|
768
|
+
let lineNumber = 1;
|
|
769
|
+
let lastPageIndex = -1;
|
|
770
|
+
let lastSectionIndex = -1;
|
|
771
|
+
|
|
772
|
+
for (const block of blocks) {
|
|
773
|
+
const pageIndex = findPageForOffset(pages, block.from);
|
|
774
|
+
const page = pages[pageIndex];
|
|
775
|
+
if (!page) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const lineNumbering = page.layout.lineNumbering;
|
|
780
|
+
const restartMode = lineNumbering?.restart ?? "newPage";
|
|
781
|
+
const restartStart = lineNumbering?.start ?? 1;
|
|
782
|
+
const countBy = Math.max(1, lineNumbering?.countBy ?? 1);
|
|
783
|
+
const columnWidth = getUsableColumnWidth(page.layout);
|
|
784
|
+
|
|
785
|
+
if (pageIndex !== lastPageIndex) {
|
|
786
|
+
if (restartMode === "newPage" || lastPageIndex === -1) {
|
|
787
|
+
lineNumber = restartStart;
|
|
788
|
+
}
|
|
789
|
+
lastPageIndex = pageIndex;
|
|
790
|
+
}
|
|
791
|
+
if (page.sectionIndex !== lastSectionIndex) {
|
|
792
|
+
if (restartMode === "newSection" || lastSectionIndex === -1) {
|
|
793
|
+
lineNumber = restartStart;
|
|
794
|
+
}
|
|
795
|
+
lastSectionIndex = page.sectionIndex;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (block.kind === "paragraph" && lineNumbering) {
|
|
799
|
+
const lineCount = estimateParagraphLineCount(block, columnWidth);
|
|
800
|
+
const lineHeight = estimateParagraphLineHeight(block);
|
|
801
|
+
const suppress = block.suppressLineNumbers === true;
|
|
802
|
+
for (let lineIndex = 0; lineIndex < lineCount; lineIndex += 1) {
|
|
803
|
+
if (!suppress && (lineNumber - restartStart) % countBy === 0) {
|
|
804
|
+
markers.push({
|
|
805
|
+
id: `${block.blockId}-${lineIndex}`,
|
|
806
|
+
label: String(lineNumber),
|
|
807
|
+
topPx:
|
|
808
|
+
DOCUMENT_CONTENT_TOP_PADDING_PX +
|
|
809
|
+
(currentTopTwips + lineIndex * lineHeight) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
if (!suppress) {
|
|
813
|
+
lineNumber += 1;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
currentTopTwips += estimateBlockHeight(block, columnWidth);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return markers;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function shouldRenderPageBorder(
|
|
825
|
+
pageLayout: RuntimeRenderSnapshot["pageLayout"],
|
|
826
|
+
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|
|
827
|
+
activePageIndex: number,
|
|
828
|
+
): boolean {
|
|
829
|
+
const display = pageLayout?.pageBorders?.display ?? "allPages";
|
|
830
|
+
const activePage = pages[activePageIndex];
|
|
831
|
+
if (!pageLayout?.pageBorders || !activePage) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
switch (display) {
|
|
836
|
+
case "firstPage":
|
|
837
|
+
return activePage.pageInSection === 0;
|
|
838
|
+
case "notFirstPage":
|
|
839
|
+
return activePage.pageInSection > 0;
|
|
840
|
+
default:
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function buildPageBorderStyle(
|
|
846
|
+
pageLayout: NonNullable<RuntimeRenderSnapshot["pageLayout"]>,
|
|
847
|
+
): CSSProperties | undefined {
|
|
848
|
+
const pageBorders = pageLayout.pageBorders;
|
|
849
|
+
if (!pageBorders) {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const leftInset = createInsetValue(
|
|
854
|
+
pageBorders.left?.space,
|
|
855
|
+
pageBorders.offsetFrom === "text"
|
|
856
|
+
? (pageLayout.marginLeft / Math.max(1, pageLayout.pageWidth)) * 100
|
|
857
|
+
: 1.25,
|
|
858
|
+
);
|
|
859
|
+
const rightInset = createInsetValue(
|
|
860
|
+
pageBorders.right?.space,
|
|
861
|
+
pageBorders.offsetFrom === "text"
|
|
862
|
+
? (pageLayout.marginRight / Math.max(1, pageLayout.pageWidth)) * 100
|
|
863
|
+
: 1.25,
|
|
864
|
+
);
|
|
865
|
+
const topInset = createInsetValue(
|
|
866
|
+
pageBorders.top?.space,
|
|
867
|
+
pageBorders.offsetFrom === "text"
|
|
868
|
+
? (pageLayout.marginTop / Math.max(1, pageLayout.pageHeight)) * 100
|
|
869
|
+
: 1.5,
|
|
870
|
+
);
|
|
871
|
+
const bottomInset = createInsetValue(
|
|
872
|
+
pageBorders.bottom?.space,
|
|
873
|
+
pageBorders.offsetFrom === "text"
|
|
874
|
+
? (pageLayout.marginBottom / Math.max(1, pageLayout.pageHeight)) * 100
|
|
875
|
+
: 1.5,
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
top: topInset,
|
|
880
|
+
right: rightInset,
|
|
881
|
+
bottom: bottomInset,
|
|
882
|
+
left: leftInset,
|
|
883
|
+
borderTop: toBorderCss(pageBorders.top),
|
|
884
|
+
borderRight: toBorderCss(pageBorders.right),
|
|
885
|
+
borderBottom: toBorderCss(pageBorders.bottom),
|
|
886
|
+
borderLeft: toBorderCss(pageBorders.left),
|
|
887
|
+
boxSizing: "border-box",
|
|
888
|
+
mixBlendMode: pageBorders.zOrder === "back" ? "multiply" : undefined,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function buildDocumentGridStyle(
|
|
893
|
+
documentGrid: NonNullable<RuntimeRenderSnapshot["pageLayout"]>["documentGrid"] | undefined,
|
|
894
|
+
): CSSProperties | undefined {
|
|
895
|
+
if (!documentGrid || !documentGrid.type || documentGrid.type === "default") {
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const linePitchPx = Math.max(
|
|
900
|
+
18,
|
|
901
|
+
Math.round((documentGrid.linePitch ?? 360) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP),
|
|
902
|
+
);
|
|
903
|
+
const charSpacePx = Math.max(
|
|
904
|
+
12,
|
|
905
|
+
Math.round((documentGrid.charSpace ?? 204) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP),
|
|
906
|
+
);
|
|
907
|
+
const gridColor = "rgba(15, 23, 42, 0.06)";
|
|
908
|
+
const backgrounds: string[] = [];
|
|
909
|
+
|
|
910
|
+
if (
|
|
911
|
+
documentGrid.type === "lines" ||
|
|
912
|
+
documentGrid.type === "linesAndChars" ||
|
|
913
|
+
documentGrid.type === "snapToChars"
|
|
914
|
+
) {
|
|
915
|
+
backgrounds.push(
|
|
916
|
+
`repeating-linear-gradient(to bottom, ${gridColor} 0, ${gridColor} 1px, transparent 1px, transparent ${linePitchPx}px)`,
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (
|
|
920
|
+
documentGrid.type === "linesAndChars" ||
|
|
921
|
+
documentGrid.type === "snapToChars"
|
|
922
|
+
) {
|
|
923
|
+
backgrounds.push(
|
|
924
|
+
`repeating-linear-gradient(to right, rgba(15, 23, 42, 0.04) 0, rgba(15, 23, 42, 0.04) 1px, transparent 1px, transparent ${charSpacePx}px)`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (backgrounds.length === 0) {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return {
|
|
933
|
+
backgroundImage: backgrounds.join(", "),
|
|
934
|
+
backgroundOrigin: "content-box",
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function createInsetValue(spaceTwips: number | undefined, percent: number): string {
|
|
939
|
+
const spacingPx = Math.max(0, Math.round((spaceTwips ?? 0) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP));
|
|
940
|
+
return `calc(${percent.toFixed(2)}% + ${spacingPx}px)`;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function resolveSelectionToolbarPlacement(
|
|
944
|
+
anchor: SelectionToolbarAnchor | null | undefined,
|
|
945
|
+
root: HTMLDivElement | null,
|
|
946
|
+
zoomScale: number,
|
|
947
|
+
): { placement: "right" | "left" | "above" | "below"; style: CSSProperties } | null {
|
|
948
|
+
if (!anchor || !root) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const rootRect = root.getBoundingClientRect();
|
|
953
|
+
if (rootRect.width <= 0 || rootRect.height <= 0 || zoomScale <= 0) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const centerX = (anchor.left + anchor.right) / 2;
|
|
958
|
+
const centerY = (anchor.top + anchor.bottom) / 2;
|
|
959
|
+
const localLeftEdge = (anchor.left - rootRect.left) / zoomScale;
|
|
960
|
+
const localRightEdge = (anchor.right - rootRect.left) / zoomScale;
|
|
961
|
+
const localLeft = (centerX - rootRect.left) / zoomScale;
|
|
962
|
+
const localCenterY = (centerY - rootRect.top) / zoomScale;
|
|
963
|
+
const localTop = (anchor.top - rootRect.top) / zoomScale;
|
|
964
|
+
const localBottom = (anchor.bottom - rootRect.top) / zoomScale;
|
|
965
|
+
const edgePadding = 16 / zoomScale;
|
|
966
|
+
const containerWidth = rootRect.width / zoomScale;
|
|
967
|
+
const containerHeight = rootRect.height / zoomScale;
|
|
968
|
+
const gapPx = 12 / zoomScale;
|
|
969
|
+
const estimatedToolbarWidth = Math.min(260 / zoomScale, Math.max(168 / zoomScale, containerWidth * 0.32));
|
|
970
|
+
const estimatedToolbarHeight = 44 / zoomScale;
|
|
971
|
+
const clampedCenterLeft = Math.max(
|
|
972
|
+
edgePadding,
|
|
973
|
+
Math.min(localLeft, Math.max(edgePadding, containerWidth - edgePadding)),
|
|
974
|
+
);
|
|
975
|
+
const clampedCenterY = Math.max(
|
|
976
|
+
edgePadding + estimatedToolbarHeight / 2,
|
|
977
|
+
Math.min(localCenterY, Math.max(edgePadding + estimatedToolbarHeight / 2, containerHeight - edgePadding - estimatedToolbarHeight / 2)),
|
|
978
|
+
);
|
|
979
|
+
const rightClearance = containerWidth - localRightEdge - gapPx - edgePadding;
|
|
980
|
+
const leftClearance = localLeftEdge - gapPx - edgePadding;
|
|
981
|
+
|
|
982
|
+
if (rightClearance >= estimatedToolbarWidth) {
|
|
983
|
+
return {
|
|
984
|
+
placement: "right",
|
|
985
|
+
style: {
|
|
986
|
+
left: `${localRightEdge}px`,
|
|
987
|
+
top: `${clampedCenterY}px`,
|
|
988
|
+
maxWidth: `${Math.max(220, containerWidth - edgePadding * 2)}px`,
|
|
989
|
+
transform: `translate(${gapPx}px, -50%)`,
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (leftClearance >= estimatedToolbarWidth) {
|
|
995
|
+
return {
|
|
996
|
+
placement: "left",
|
|
997
|
+
style: {
|
|
998
|
+
left: `${localLeftEdge}px`,
|
|
999
|
+
top: `${clampedCenterY}px`,
|
|
1000
|
+
maxWidth: `${Math.max(220, containerWidth - edgePadding * 2)}px`,
|
|
1001
|
+
transform: `translate(calc(-100% - ${gapPx}px), -50%)`,
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const placement = localTop < estimatedToolbarHeight + gapPx + edgePadding ? "below" : "above";
|
|
1007
|
+
|
|
1008
|
+
return {
|
|
1009
|
+
placement,
|
|
1010
|
+
style: {
|
|
1011
|
+
left: `${clampedCenterLeft}px`,
|
|
1012
|
+
top: `${placement === "above" ? localTop : localBottom}px`,
|
|
1013
|
+
maxWidth: `${Math.max(220, containerWidth - edgePadding * 2)}px`,
|
|
1014
|
+
transform:
|
|
1015
|
+
placement === "above"
|
|
1016
|
+
? `translate(-50%, calc(-100% - ${gapPx}px))`
|
|
1017
|
+
: `translate(-50%, ${gapPx}px)`,
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function toBorderCss(
|
|
1023
|
+
border:
|
|
1024
|
+
| NonNullable<NonNullable<RuntimeRenderSnapshot["pageLayout"]>["pageBorders"]>["top"]
|
|
1025
|
+
| undefined,
|
|
1026
|
+
): string | undefined {
|
|
1027
|
+
if (!border || border.value === "none" || border.value === "nil") {
|
|
1028
|
+
return undefined;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
|
|
1032
|
+
const style =
|
|
1033
|
+
border.value === "double"
|
|
1034
|
+
? "double"
|
|
1035
|
+
: border.value === "dotted"
|
|
1036
|
+
? "dotted"
|
|
1037
|
+
: border.value === "dashed" || border.value === "dashSmallGap"
|
|
1038
|
+
? "dashed"
|
|
1039
|
+
: "solid";
|
|
1040
|
+
const color = border.color && border.color !== "auto" ? `#${border.color}` : "rgba(31, 31, 31, 0.28)";
|
|
1041
|
+
return `${width} ${style} ${color}`;
|
|
1042
|
+
}
|