@beyondwork/docx-react-component 1.0.18 → 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 +24 -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
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
--font-legal-serif: var(--font-legal-serif, "Source Serif 4", "Georgia", "Times New Roman", serif);
|
|
69
69
|
--font-legal-sans: var(--font-legal-sans, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
|
|
70
70
|
--font-legal-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Consolas", monospace;
|
|
71
|
+
|
|
72
|
+
/* Page-mode document surface */
|
|
73
|
+
--color-page-shadow: rgba(16, 24, 40, 0.06);
|
|
74
|
+
--color-page-border: rgba(0, 0, 0, 0.04);
|
|
75
|
+
--color-page-bg: #ffffff;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/* ─── Dark mode overrides ─── */
|
|
@@ -108,6 +113,96 @@
|
|
|
108
113
|
|
|
109
114
|
--color-shadow: rgba(0, 0, 0, 0.32);
|
|
110
115
|
--color-shadow-strong: rgba(0, 0, 0, 0.48);
|
|
116
|
+
|
|
117
|
+
--color-page-shadow: rgba(0, 0, 0, 0.18);
|
|
118
|
+
--color-page-border: rgba(255, 255, 255, 0.06);
|
|
119
|
+
--color-page-bg: #1e1e1e;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* ─── Font substitution map ───
|
|
124
|
+
*
|
|
125
|
+
* Common Word font families mapped to redistributable metric-compatible or
|
|
126
|
+
* open substitutes. These CSS custom properties are consumed by the
|
|
127
|
+
* ProseMirror surface font_family mark and by the page-mode typography
|
|
128
|
+
* layer. Hosts can override individual properties to supply their own
|
|
129
|
+
* licensed fonts.
|
|
130
|
+
*
|
|
131
|
+
* Strategy:
|
|
132
|
+
* 1. Use metric-compatible open substitutes where available
|
|
133
|
+
* (e.g. Liberation Serif for Times New Roman, Carlito for Calibri).
|
|
134
|
+
* 2. Fall back through system-installed metric-compatible fonts
|
|
135
|
+
* (e.g. Cambria → system serif, Calibri → system sans).
|
|
136
|
+
* 3. Final fallback to the editor's legal-serif or legal-sans role font.
|
|
137
|
+
*
|
|
138
|
+
* This pipeline does NOT ship proprietary Microsoft fonts. It uses only
|
|
139
|
+
* redistributable or system-installed families.
|
|
140
|
+
*/
|
|
141
|
+
:root {
|
|
142
|
+
/* Serif substitutions */
|
|
143
|
+
--wre-font-times-new-roman: "Liberation Serif", "Times New Roman", "Source Serif 4", "Georgia", serif;
|
|
144
|
+
--wre-font-cambria: "Caladea", "Cambria", "Source Serif 4", "Georgia", serif;
|
|
145
|
+
--wre-font-garamond: "EB Garamond", "Garamond", "Source Serif 4", serif;
|
|
146
|
+
--wre-font-book-antiqua: "Palatino Linotype", "Book Antiqua", "Source Serif 4", serif;
|
|
147
|
+
--wre-font-georgia: "Georgia", "Source Serif 4", serif;
|
|
148
|
+
|
|
149
|
+
/* Sans-serif substitutions */
|
|
150
|
+
--wre-font-calibri: "Carlito", "Calibri", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
151
|
+
--wre-font-arial: "Liberation Sans", "Arial", "Inter", sans-serif;
|
|
152
|
+
--wre-font-helvetica: "Liberation Sans", "Helvetica Neue", "Helvetica", "Inter", sans-serif;
|
|
153
|
+
--wre-font-verdana: "Verdana", "Inter", sans-serif;
|
|
154
|
+
--wre-font-tahoma: "Tahoma", "Inter", sans-serif;
|
|
155
|
+
--wre-font-segoe-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
156
|
+
--wre-font-century-gothic: "URW Gothic", "Century Gothic", "Inter", sans-serif;
|
|
157
|
+
|
|
158
|
+
/* Monospace substitutions */
|
|
159
|
+
--wre-font-courier-new: "Liberation Mono", "Courier New", "JetBrains Mono", monospace;
|
|
160
|
+
--wre-font-consolas: "JetBrains Mono", "Consolas", "SF Mono", monospace;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ─── Page-mode typography ─── */
|
|
164
|
+
.wre-page-surface {
|
|
165
|
+
font-family: var(--font-legal-serif);
|
|
166
|
+
font-size: 15px;
|
|
167
|
+
line-height: 1.6;
|
|
168
|
+
color: var(--color-primary);
|
|
169
|
+
-webkit-font-smoothing: antialiased;
|
|
170
|
+
-moz-osx-font-smoothing: grayscale;
|
|
171
|
+
text-rendering: optimizeLegibility;
|
|
172
|
+
font-kerning: normal;
|
|
173
|
+
font-variant-ligatures: common-ligatures;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.wre-page-surface p {
|
|
177
|
+
margin: 0 0 0.5em 0;
|
|
178
|
+
orphans: 2;
|
|
179
|
+
widows: 2;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Page chrome — shadow, border, and background for the page-mode document panel */
|
|
183
|
+
.wre-page-chrome {
|
|
184
|
+
background: var(--color-page-bg);
|
|
185
|
+
border: 1px solid var(--color-page-border);
|
|
186
|
+
border-radius: 2px;
|
|
187
|
+
box-shadow: 0 1px 4px var(--color-page-shadow);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Canvas-mode typography — lighter, review-first baseline */
|
|
191
|
+
.wre-canvas-surface {
|
|
192
|
+
font-family: var(--font-legal-serif);
|
|
193
|
+
font-size: 15px;
|
|
194
|
+
line-height: 1.6;
|
|
195
|
+
color: var(--color-primary);
|
|
196
|
+
-webkit-font-smoothing: antialiased;
|
|
197
|
+
-moz-osx-font-smoothing: grayscale;
|
|
198
|
+
text-rendering: optimizeLegibility;
|
|
199
|
+
font-kerning: normal;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Wide-table readability in page mode */
|
|
203
|
+
.wre-page-surface table {
|
|
204
|
+
font-size: 14px;
|
|
205
|
+
line-height: 1.45;
|
|
111
206
|
}
|
|
112
207
|
|
|
113
208
|
/* ─── Base resets ─── */
|
|
@@ -188,3 +283,31 @@
|
|
|
188
283
|
.prosemirror-surface .ProseMirror ::selection {
|
|
189
284
|
background: var(--color-accent-soft);
|
|
190
285
|
}
|
|
286
|
+
|
|
287
|
+
.prosemirror-surface .ProseMirror .ProseMirror-selectednode {
|
|
288
|
+
outline: none;
|
|
289
|
+
background-color: var(--color-accent-soft);
|
|
290
|
+
border-radius: 4px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/*
|
|
294
|
+
* ─── Preservation noise suppression ───
|
|
295
|
+
*
|
|
296
|
+
* Quiet-marker opaque inlines (w:proofErr, w:lastRenderedPageBreak, w:permStart,
|
|
297
|
+
* w:permEnd) are preserved in the canonical document for export safety but
|
|
298
|
+
* contribute zero visual weight to the reading/editing surface. They remain in
|
|
299
|
+
* the DOM as zero-dimension spans so round-trip export is unaffected.
|
|
300
|
+
*/
|
|
301
|
+
.prosemirror-surface .ProseMirror [data-inline-presentation="quiet-marker"] {
|
|
302
|
+
display: inline-block;
|
|
303
|
+
width: 0;
|
|
304
|
+
height: 0;
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
vertical-align: baseline;
|
|
307
|
+
pointer-events: none;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ─── Page workspace zoom scaling ─── */
|
|
311
|
+
.wre-page-chrome[style*="scale"] {
|
|
312
|
+
will-change: transform;
|
|
313
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
3
3
|
|
|
4
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
5
|
+
|
|
4
6
|
export interface TwToolbarIconButtonProps {
|
|
5
7
|
icon: React.ComponentType<{ className?: string }>;
|
|
6
8
|
label: string;
|
|
@@ -19,7 +21,9 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
|
|
|
19
21
|
<Tooltip.Trigger asChild>
|
|
20
22
|
<button
|
|
21
23
|
type="button"
|
|
24
|
+
aria-label={props.label}
|
|
22
25
|
disabled={props.disabled}
|
|
26
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
23
27
|
onClick={props.onClick}
|
|
24
28
|
className={[
|
|
25
29
|
"inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors outline-none",
|
|
@@ -1,50 +1,94 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import * as Popover from "@radix-ui/react-popover";
|
|
4
|
+
import * as Select from "@radix-ui/react-select";
|
|
4
5
|
import * as Toggle from "@radix-ui/react-toggle";
|
|
5
6
|
import * as ToggleGroup from "@radix-ui/react-toggle-group";
|
|
6
7
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
7
8
|
import {
|
|
9
|
+
Bold,
|
|
10
|
+
ChevronDown,
|
|
8
11
|
Download,
|
|
9
12
|
Eye,
|
|
10
13
|
EyeOff,
|
|
11
14
|
FileText,
|
|
15
|
+
Indent,
|
|
16
|
+
Italic,
|
|
12
17
|
MessageSquare,
|
|
18
|
+
Minus,
|
|
13
19
|
Monitor,
|
|
20
|
+
Outdent,
|
|
21
|
+
Plus,
|
|
14
22
|
Redo2,
|
|
15
23
|
ShieldAlert,
|
|
16
24
|
ShieldCheck,
|
|
25
|
+
Underline,
|
|
17
26
|
Undo2,
|
|
18
27
|
} from "lucide-react";
|
|
19
28
|
|
|
20
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
CompatibilityPanelSnapshot,
|
|
31
|
+
EditorStoryTarget,
|
|
32
|
+
EditorWarning,
|
|
33
|
+
FormattingStateSnapshot,
|
|
34
|
+
StyleCatalogSnapshot,
|
|
35
|
+
WorkspaceMode,
|
|
36
|
+
ZoomLevel,
|
|
37
|
+
} from "../../api/public-types";
|
|
21
38
|
import type { SessionCapabilities } from "../../runtime/session-capabilities";
|
|
39
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
22
40
|
import { TwHealthPanel } from "../review/tw-health-panel";
|
|
23
41
|
import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
|
|
24
42
|
|
|
25
|
-
export type ViewMode = "canvas" | "document";
|
|
26
|
-
|
|
27
43
|
export interface TwToolbarProps {
|
|
28
44
|
sourceLabel?: string;
|
|
29
45
|
capabilities?: SessionCapabilities;
|
|
30
46
|
compatibility?: CompatibilityPanelSnapshot;
|
|
31
47
|
warnings?: EditorWarning[];
|
|
32
|
-
|
|
48
|
+
workspaceMode: WorkspaceMode;
|
|
49
|
+
zoomLevel?: ZoomLevel;
|
|
50
|
+
formattingState?: FormattingStateSnapshot;
|
|
51
|
+
styleCatalog?: StyleCatalogSnapshot;
|
|
33
52
|
/** Display toggle for tracked change decorations (not a runtime mutation toggle). */
|
|
34
53
|
showTrackedChanges: boolean;
|
|
54
|
+
/** Active story target — shows a breadcrumb when editing a secondary story. */
|
|
55
|
+
activeStory?: EditorStoryTarget;
|
|
56
|
+
/** Called when the user clicks the story breadcrumb to return to main body. */
|
|
57
|
+
onCloseStory?: () => void;
|
|
35
58
|
onUndo: () => void;
|
|
36
59
|
onRedo: () => void;
|
|
60
|
+
onSetParagraphStyle?: (styleId: string) => void;
|
|
61
|
+
onToggleBold?: () => void;
|
|
62
|
+
onToggleItalic?: () => void;
|
|
63
|
+
onToggleUnderline?: () => void;
|
|
64
|
+
onOutdent?: () => void;
|
|
65
|
+
onIndent?: () => void;
|
|
37
66
|
onAddComment: () => void;
|
|
38
67
|
onExport: () => void;
|
|
39
|
-
|
|
68
|
+
onWorkspaceModeChange: (value: WorkspaceMode) => void;
|
|
69
|
+
onZoomChange?: (level: ZoomLevel) => void;
|
|
40
70
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
41
71
|
}
|
|
42
72
|
|
|
73
|
+
export function getSupportedZoomPresets(): ReadonlyArray<number> {
|
|
74
|
+
return [75, 100, 125, 150];
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
const focusRingClass =
|
|
44
78
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
45
79
|
|
|
46
80
|
export function TwToolbar(props: TwToolbarProps) {
|
|
47
81
|
const caps = props.capabilities;
|
|
82
|
+
const workspaceMode = props.workspaceMode;
|
|
83
|
+
const isPageMode = workspaceMode === "page";
|
|
84
|
+
const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
|
|
85
|
+
const zoomLevel = props.zoomLevel ?? 100;
|
|
86
|
+
const zoomLabel =
|
|
87
|
+
typeof zoomLevel === "number"
|
|
88
|
+
? `${zoomLevel}%`
|
|
89
|
+
: zoomLevel === "pageWidth"
|
|
90
|
+
? "Fit width"
|
|
91
|
+
: "Fit page";
|
|
48
92
|
|
|
49
93
|
return (
|
|
50
94
|
<header className="flex h-10 shrink-0 items-center gap-1 border-b border-border px-2">
|
|
@@ -64,7 +108,68 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
64
108
|
/>
|
|
65
109
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
66
110
|
|
|
67
|
-
|
|
111
|
+
<ToolbarParagraphStyleSelect
|
|
112
|
+
disabled={!caps?.canEdit || paragraphStyles.length === 0 || !props.onSetParagraphStyle}
|
|
113
|
+
styles={paragraphStyles}
|
|
114
|
+
value={props.formattingState?.paragraphStyleId}
|
|
115
|
+
onValueChange={props.onSetParagraphStyle}
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
119
|
+
|
|
120
|
+
<TwToolbarIconButton
|
|
121
|
+
icon={Bold}
|
|
122
|
+
label="Bold"
|
|
123
|
+
active={props.formattingState?.bold ?? false}
|
|
124
|
+
disabled={caps ? !caps.canEdit : true}
|
|
125
|
+
onClick={props.onToggleBold}
|
|
126
|
+
/>
|
|
127
|
+
<TwToolbarIconButton
|
|
128
|
+
icon={Italic}
|
|
129
|
+
label="Italic"
|
|
130
|
+
active={props.formattingState?.italic ?? false}
|
|
131
|
+
disabled={caps ? !caps.canEdit : true}
|
|
132
|
+
onClick={props.onToggleItalic}
|
|
133
|
+
/>
|
|
134
|
+
<TwToolbarIconButton
|
|
135
|
+
icon={Underline}
|
|
136
|
+
label="Underline"
|
|
137
|
+
active={props.formattingState?.underline ?? false}
|
|
138
|
+
disabled={caps ? !caps.canEdit : true}
|
|
139
|
+
onClick={props.onToggleUnderline}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
143
|
+
|
|
144
|
+
<TwToolbarIconButton
|
|
145
|
+
icon={Outdent}
|
|
146
|
+
label="Outdent"
|
|
147
|
+
disabled={caps ? !caps.canEdit : true}
|
|
148
|
+
onClick={props.onOutdent}
|
|
149
|
+
/>
|
|
150
|
+
<TwToolbarIconButton
|
|
151
|
+
icon={Indent}
|
|
152
|
+
label="Indent"
|
|
153
|
+
disabled={caps ? !caps.canEdit : true}
|
|
154
|
+
onClick={props.onIndent}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
{/* Story focus breadcrumb — visible when editing a secondary story */}
|
|
158
|
+
{props.activeStory && props.activeStory.kind !== "main" ? (
|
|
159
|
+
<>
|
|
160
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={props.onCloseStory}
|
|
164
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
165
|
+
className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium text-accent hover:bg-accent-soft transition-colors outline-none ${focusRingClass}`}
|
|
166
|
+
aria-label={`Editing ${storyLabel(props.activeStory)} — click to return to main body`}
|
|
167
|
+
>
|
|
168
|
+
<span className="text-secondary">←</span>
|
|
169
|
+
{storyLabel(props.activeStory)}
|
|
170
|
+
</button>
|
|
171
|
+
</>
|
|
172
|
+
) : null}
|
|
68
173
|
</div>
|
|
69
174
|
|
|
70
175
|
{/* Center: document title */}
|
|
@@ -90,6 +195,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
90
195
|
pressed={props.showTrackedChanges}
|
|
91
196
|
onPressedChange={props.onShowTrackedChangesChange}
|
|
92
197
|
disabled={caps ? !caps.trackChangesSupported : false}
|
|
198
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
93
199
|
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
94
200
|
>
|
|
95
201
|
{props.showTrackedChanges ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
|
|
@@ -107,12 +213,12 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
107
213
|
|
|
108
214
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
109
215
|
|
|
110
|
-
{/* View mode toggle group: Canvas (clean, flowing) /
|
|
216
|
+
{/* View mode toggle group: Canvas (clean, flowing) / Page (layout-sensitive) */}
|
|
111
217
|
<ToggleGroup.Root
|
|
112
218
|
type="single"
|
|
113
|
-
value={
|
|
219
|
+
value={workspaceMode}
|
|
114
220
|
onValueChange={(v: string) => {
|
|
115
|
-
if (v) props.
|
|
221
|
+
if (v) props.onWorkspaceModeChange(v as WorkspaceMode);
|
|
116
222
|
}}
|
|
117
223
|
className="flex items-center gap-0.5"
|
|
118
224
|
>
|
|
@@ -120,6 +226,8 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
120
226
|
<Tooltip.Trigger asChild>
|
|
121
227
|
<ToggleGroup.Item
|
|
122
228
|
value="canvas"
|
|
229
|
+
aria-label="Canvas workspace"
|
|
230
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
123
231
|
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none ${focusRingClass}`}
|
|
124
232
|
>
|
|
125
233
|
<Monitor className="h-3.5 w-3.5" />
|
|
@@ -134,7 +242,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
134
242
|
<Tooltip.Root>
|
|
135
243
|
<Tooltip.Trigger asChild>
|
|
136
244
|
<ToggleGroup.Item
|
|
137
|
-
value="
|
|
245
|
+
value="page"
|
|
246
|
+
aria-label="Page workspace"
|
|
247
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
138
248
|
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none ${focusRingClass}`}
|
|
139
249
|
>
|
|
140
250
|
<FileText className="h-3.5 w-3.5" />
|
|
@@ -142,12 +252,113 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
142
252
|
</Tooltip.Trigger>
|
|
143
253
|
<Tooltip.Portal>
|
|
144
254
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
145
|
-
|
|
255
|
+
Page — layout-sensitive view
|
|
146
256
|
</Tooltip.Content>
|
|
147
257
|
</Tooltip.Portal>
|
|
148
258
|
</Tooltip.Root>
|
|
149
259
|
</ToggleGroup.Root>
|
|
150
260
|
|
|
261
|
+
{/* Zoom controls — visible in page mode */}
|
|
262
|
+
{isPageMode && props.onZoomChange ? (
|
|
263
|
+
<>
|
|
264
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
265
|
+
<div className="flex items-center gap-0.5">
|
|
266
|
+
<Tooltip.Root>
|
|
267
|
+
<Tooltip.Trigger asChild>
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
aria-label="Zoom out"
|
|
271
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none ${focusRingClass}`}
|
|
272
|
+
disabled={typeof zoomLevel === "number" && zoomLevel <= 50}
|
|
273
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
274
|
+
onClick={() => {
|
|
275
|
+
const current = typeof zoomLevel === "number" ? zoomLevel : 100;
|
|
276
|
+
props.onZoomChange!(Math.max(50, current - 10));
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<Minus className="h-3 w-3" />
|
|
280
|
+
</button>
|
|
281
|
+
</Tooltip.Trigger>
|
|
282
|
+
<Tooltip.Portal>
|
|
283
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
284
|
+
Zoom out
|
|
285
|
+
</Tooltip.Content>
|
|
286
|
+
</Tooltip.Portal>
|
|
287
|
+
</Tooltip.Root>
|
|
288
|
+
|
|
289
|
+
<Popover.Root>
|
|
290
|
+
<Tooltip.Root>
|
|
291
|
+
<Tooltip.Trigger asChild>
|
|
292
|
+
<Popover.Trigger asChild>
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
aria-label={`Zoom: ${zoomLabel}`}
|
|
296
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
297
|
+
className={`inline-flex h-7 items-center justify-center rounded-md px-1.5 text-[11px] font-medium text-secondary transition-colors hover:bg-surface outline-none ${focusRingClass}`}
|
|
298
|
+
>
|
|
299
|
+
{zoomLabel}
|
|
300
|
+
</button>
|
|
301
|
+
</Popover.Trigger>
|
|
302
|
+
</Tooltip.Trigger>
|
|
303
|
+
<Tooltip.Portal>
|
|
304
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
305
|
+
Zoom level
|
|
306
|
+
</Tooltip.Content>
|
|
307
|
+
</Tooltip.Portal>
|
|
308
|
+
</Tooltip.Root>
|
|
309
|
+
<Popover.Portal>
|
|
310
|
+
<Popover.Content
|
|
311
|
+
className="w-[140px] rounded-lg bg-canvas shadow-lg ring-1 ring-border p-1 z-50"
|
|
312
|
+
sideOffset={8}
|
|
313
|
+
align="center"
|
|
314
|
+
>
|
|
315
|
+
{getSupportedZoomPresets().map((preset) => {
|
|
316
|
+
const label = `${preset}%`;
|
|
317
|
+
return (
|
|
318
|
+
<Popover.Close key={preset} asChild>
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
322
|
+
className={`w-full rounded-md px-3 py-1.5 text-left text-xs transition-colors hover:bg-surface ${
|
|
323
|
+
zoomLevel === preset ? "font-semibold text-accent" : "text-primary"
|
|
324
|
+
}`}
|
|
325
|
+
onClick={() => props.onZoomChange!(preset)}
|
|
326
|
+
>
|
|
327
|
+
{label}
|
|
328
|
+
</button>
|
|
329
|
+
</Popover.Close>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</Popover.Content>
|
|
333
|
+
</Popover.Portal>
|
|
334
|
+
</Popover.Root>
|
|
335
|
+
|
|
336
|
+
<Tooltip.Root>
|
|
337
|
+
<Tooltip.Trigger asChild>
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
aria-label="Zoom in"
|
|
341
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none ${focusRingClass}`}
|
|
342
|
+
disabled={typeof zoomLevel === "number" && zoomLevel >= 200}
|
|
343
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
344
|
+
onClick={() => {
|
|
345
|
+
const current = typeof zoomLevel === "number" ? zoomLevel : 100;
|
|
346
|
+
props.onZoomChange!(Math.min(200, current + 10));
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
<Plus className="h-3 w-3" />
|
|
350
|
+
</button>
|
|
351
|
+
</Tooltip.Trigger>
|
|
352
|
+
<Tooltip.Portal>
|
|
353
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
354
|
+
Zoom in
|
|
355
|
+
</Tooltip.Content>
|
|
356
|
+
</Tooltip.Portal>
|
|
357
|
+
</Tooltip.Root>
|
|
358
|
+
</div>
|
|
359
|
+
</>
|
|
360
|
+
) : null}
|
|
361
|
+
|
|
151
362
|
{/* Health indicator */}
|
|
152
363
|
{props.compatibility && props.warnings ? (
|
|
153
364
|
<Popover.Root>
|
|
@@ -156,6 +367,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
156
367
|
<Popover.Trigger asChild>
|
|
157
368
|
<button
|
|
158
369
|
type="button"
|
|
370
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
159
371
|
className={`relative inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-surface hover:text-primary outline-none ${focusRingClass} ${
|
|
160
372
|
(caps?.healthIssueCount ?? 0) > 0 ? "text-secondary" : "text-secondary"
|
|
161
373
|
}`}
|
|
@@ -204,6 +416,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
204
416
|
<button
|
|
205
417
|
type="button"
|
|
206
418
|
disabled={caps ? !caps.canExport : true}
|
|
419
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
207
420
|
className={[
|
|
208
421
|
"inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs font-semibold transition-colors outline-none",
|
|
209
422
|
focusRingClass,
|
|
@@ -214,7 +427,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
214
427
|
onClick={props.onExport}
|
|
215
428
|
>
|
|
216
429
|
<Download className="h-3.5 w-3.5" />
|
|
217
|
-
Export
|
|
430
|
+
Export .docx
|
|
218
431
|
</button>
|
|
219
432
|
</Tooltip.Trigger>
|
|
220
433
|
<Tooltip.Portal>
|
|
@@ -229,3 +442,69 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
229
442
|
</header>
|
|
230
443
|
);
|
|
231
444
|
}
|
|
445
|
+
|
|
446
|
+
function ToolbarParagraphStyleSelect(props: {
|
|
447
|
+
styles: StyleCatalogSnapshot["paragraphs"];
|
|
448
|
+
value?: string;
|
|
449
|
+
disabled: boolean;
|
|
450
|
+
onValueChange?: (styleId: string) => void;
|
|
451
|
+
}) {
|
|
452
|
+
const resolvedValue =
|
|
453
|
+
props.value && props.styles.some((style) => style.styleId === props.value)
|
|
454
|
+
? props.value
|
|
455
|
+
: undefined;
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<Select.Root
|
|
459
|
+
disabled={props.disabled}
|
|
460
|
+
onValueChange={(value) => props.onValueChange?.(value)}
|
|
461
|
+
value={resolvedValue}
|
|
462
|
+
>
|
|
463
|
+
<Select.Trigger
|
|
464
|
+
aria-label="Paragraph style"
|
|
465
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
466
|
+
className={`inline-flex h-7 min-w-[8.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2.5 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
467
|
+
>
|
|
468
|
+
<Select.Value placeholder="Style" />
|
|
469
|
+
<Select.Icon>
|
|
470
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
471
|
+
</Select.Icon>
|
|
472
|
+
</Select.Trigger>
|
|
473
|
+
<Select.Portal>
|
|
474
|
+
<Select.Content
|
|
475
|
+
align="start"
|
|
476
|
+
className="z-50 overflow-hidden rounded-lg bg-canvas shadow-lg ring-1 ring-border"
|
|
477
|
+
position="popper"
|
|
478
|
+
sideOffset={8}
|
|
479
|
+
>
|
|
480
|
+
<Select.Viewport className="p-1">
|
|
481
|
+
{props.styles.map((style) => (
|
|
482
|
+
<Select.Item
|
|
483
|
+
className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-accent-soft data-[state=checked]:text-accent ${focusRingClass}`}
|
|
484
|
+
key={style.styleId}
|
|
485
|
+
value={style.styleId}
|
|
486
|
+
>
|
|
487
|
+
<Select.ItemText>{style.displayName}</Select.ItemText>
|
|
488
|
+
</Select.Item>
|
|
489
|
+
))}
|
|
490
|
+
</Select.Viewport>
|
|
491
|
+
</Select.Content>
|
|
492
|
+
</Select.Portal>
|
|
493
|
+
</Select.Root>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function storyLabel(target: EditorStoryTarget): string {
|
|
498
|
+
switch (target.kind) {
|
|
499
|
+
case "header":
|
|
500
|
+
return `Header (${target.variant})`;
|
|
501
|
+
case "footer":
|
|
502
|
+
return `Footer (${target.variant})`;
|
|
503
|
+
case "footnote":
|
|
504
|
+
return "Footnote";
|
|
505
|
+
case "endnote":
|
|
506
|
+
return "Endnote";
|
|
507
|
+
default:
|
|
508
|
+
return "Document";
|
|
509
|
+
}
|
|
510
|
+
}
|