@beyondwork/docx-react-component 1.0.19 → 1.0.21
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 +44 -25
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +915 -1314
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1448 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +55 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui/workflow-surface-blocked-rails.ts +94 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +130 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
|
@@ -6,20 +6,32 @@ import * as Toggle from "@radix-ui/react-toggle";
|
|
|
6
6
|
import * as ToggleGroup from "@radix-ui/react-toggle-group";
|
|
7
7
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
8
8
|
import {
|
|
9
|
+
AlignCenter,
|
|
10
|
+
AlignJustify,
|
|
11
|
+
AlignLeft,
|
|
12
|
+
AlignRight,
|
|
13
|
+
Baseline,
|
|
9
14
|
Bold,
|
|
10
15
|
ChevronDown,
|
|
11
16
|
Download,
|
|
12
17
|
Eye,
|
|
13
18
|
EyeOff,
|
|
14
19
|
FileText,
|
|
20
|
+
Highlighter,
|
|
21
|
+
ImagePlus,
|
|
15
22
|
Indent,
|
|
16
23
|
Italic,
|
|
17
24
|
MessageSquare,
|
|
18
25
|
Minus,
|
|
19
26
|
Monitor,
|
|
27
|
+
MoreHorizontal,
|
|
20
28
|
Outdent,
|
|
21
29
|
Plus,
|
|
22
30
|
Redo2,
|
|
31
|
+
Rows3,
|
|
32
|
+
Strikethrough,
|
|
33
|
+
Subscript,
|
|
34
|
+
Superscript,
|
|
23
35
|
ShieldAlert,
|
|
24
36
|
ShieldCheck,
|
|
25
37
|
Underline,
|
|
@@ -31,7 +43,11 @@ import type {
|
|
|
31
43
|
EditorStoryTarget,
|
|
32
44
|
EditorWarning,
|
|
33
45
|
FormattingStateSnapshot,
|
|
46
|
+
FormattingAlignment,
|
|
47
|
+
InsertImageOptions,
|
|
48
|
+
SectionBreakType,
|
|
34
49
|
StyleCatalogSnapshot,
|
|
50
|
+
WorkflowBlockedCommandReason,
|
|
35
51
|
WorkspaceMode,
|
|
36
52
|
ZoomLevel,
|
|
37
53
|
} from "../../api/public-types";
|
|
@@ -45,6 +61,7 @@ export interface TwToolbarProps {
|
|
|
45
61
|
capabilities?: SessionCapabilities;
|
|
46
62
|
compatibility?: CompatibilityPanelSnapshot;
|
|
47
63
|
warnings?: EditorWarning[];
|
|
64
|
+
blockedReasons?: WorkflowBlockedCommandReason[];
|
|
48
65
|
workspaceMode: WorkspaceMode;
|
|
49
66
|
zoomLevel?: ZoomLevel;
|
|
50
67
|
formattingState?: FormattingStateSnapshot;
|
|
@@ -61,9 +78,21 @@ export interface TwToolbarProps {
|
|
|
61
78
|
onToggleBold?: () => void;
|
|
62
79
|
onToggleItalic?: () => void;
|
|
63
80
|
onToggleUnderline?: () => void;
|
|
81
|
+
onToggleStrikethrough?: () => void;
|
|
82
|
+
onToggleSuperscript?: () => void;
|
|
83
|
+
onToggleSubscript?: () => void;
|
|
84
|
+
onSetFontFamily?: (fontFamily: string) => void;
|
|
85
|
+
onSetFontSize?: (fontSize: number) => void;
|
|
86
|
+
onSetTextColor?: (color: string) => void;
|
|
87
|
+
onSetHighlightColor?: (color: string | null) => void;
|
|
88
|
+
onSetAlignment?: (alignment: FormattingAlignment) => void;
|
|
64
89
|
onOutdent?: () => void;
|
|
65
90
|
onIndent?: () => void;
|
|
66
91
|
onAddComment: () => void;
|
|
92
|
+
onInsertPageBreak?: () => void;
|
|
93
|
+
onInsertTable?: () => void;
|
|
94
|
+
onInsertSectionBreak?: (type: SectionBreakType) => void;
|
|
95
|
+
onInsertImage?: (options: InsertImageOptions) => void;
|
|
67
96
|
onExport: () => void;
|
|
68
97
|
onWorkspaceModeChange: (value: WorkspaceMode) => void;
|
|
69
98
|
onZoomChange?: (level: ZoomLevel) => void;
|
|
@@ -77,12 +106,24 @@ export function getSupportedZoomPresets(): ReadonlyArray<number> {
|
|
|
77
106
|
const focusRingClass =
|
|
78
107
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
79
108
|
|
|
109
|
+
const FONT_FAMILIES = ["Arial", "Times New Roman", "Calibri", "Cambria", "Georgia", "Verdana"];
|
|
110
|
+
const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 36];
|
|
111
|
+
const TEXT_COLORS = ["#000000", "#434343", "#1660a8", "#1a7f37", "#cf222e", "#7a4f00"];
|
|
112
|
+
const HIGHLIGHT_COLORS = [
|
|
113
|
+
{ value: "#ffff00", label: "Yellow" },
|
|
114
|
+
{ value: "#00ff00", label: "Green" },
|
|
115
|
+
{ value: "#00ffff", label: "Cyan" },
|
|
116
|
+
{ value: "#ff69b4", label: "Pink" },
|
|
117
|
+
{ value: null, label: "None" },
|
|
118
|
+
] as const;
|
|
119
|
+
|
|
80
120
|
export function TwToolbar(props: TwToolbarProps) {
|
|
81
121
|
const caps = props.capabilities;
|
|
82
122
|
const workspaceMode = props.workspaceMode;
|
|
83
123
|
const isPageMode = workspaceMode === "page";
|
|
84
124
|
const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
|
|
85
125
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
126
|
+
const canEdit = caps ? caps.canEdit : false;
|
|
86
127
|
const zoomLabel =
|
|
87
128
|
typeof zoomLevel === "number"
|
|
88
129
|
? `${zoomLevel}%`
|
|
@@ -109,12 +150,23 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
109
150
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
110
151
|
|
|
111
152
|
<ToolbarParagraphStyleSelect
|
|
112
|
-
disabled={!
|
|
153
|
+
disabled={!canEdit || paragraphStyles.length === 0 || !props.onSetParagraphStyle}
|
|
113
154
|
styles={paragraphStyles}
|
|
114
155
|
value={props.formattingState?.paragraphStyleId}
|
|
115
156
|
onValueChange={props.onSetParagraphStyle}
|
|
116
157
|
/>
|
|
117
158
|
|
|
159
|
+
<ToolbarFontFamilySelect
|
|
160
|
+
disabled={!canEdit || !props.onSetFontFamily}
|
|
161
|
+
value={props.formattingState?.fontFamily}
|
|
162
|
+
onValueChange={props.onSetFontFamily}
|
|
163
|
+
/>
|
|
164
|
+
<ToolbarFontSizeSelect
|
|
165
|
+
disabled={!canEdit || !props.onSetFontSize}
|
|
166
|
+
value={props.formattingState?.fontSize}
|
|
167
|
+
onValueChange={props.onSetFontSize}
|
|
168
|
+
/>
|
|
169
|
+
|
|
118
170
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
119
171
|
|
|
120
172
|
<TwToolbarIconButton
|
|
@@ -135,24 +187,63 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
135
187
|
icon={Underline}
|
|
136
188
|
label="Underline"
|
|
137
189
|
active={props.formattingState?.underline ?? false}
|
|
138
|
-
disabled={
|
|
190
|
+
disabled={!canEdit}
|
|
139
191
|
onClick={props.onToggleUnderline}
|
|
140
192
|
/>
|
|
193
|
+
<ToolbarFormattingOverflow
|
|
194
|
+
disabled={!canEdit}
|
|
195
|
+
formattingState={props.formattingState}
|
|
196
|
+
onToggleStrikethrough={props.onToggleStrikethrough}
|
|
197
|
+
onToggleSuperscript={props.onToggleSuperscript}
|
|
198
|
+
onToggleSubscript={props.onToggleSubscript}
|
|
199
|
+
/>
|
|
200
|
+
<ToolbarColorPopover
|
|
201
|
+
ariaLabel="Text color"
|
|
202
|
+
colors={TEXT_COLORS.map((value) => ({ value, label: value }))}
|
|
203
|
+
disabled={!canEdit || !props.onSetTextColor}
|
|
204
|
+
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
205
|
+
onSelect={(value) => {
|
|
206
|
+
if (value) {
|
|
207
|
+
props.onSetTextColor?.(value);
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
title="Text color"
|
|
211
|
+
/>
|
|
212
|
+
<ToolbarColorPopover
|
|
213
|
+
ariaLabel="Highlight color"
|
|
214
|
+
colors={HIGHLIGHT_COLORS.map((entry) => ({ value: entry.value, label: entry.label }))}
|
|
215
|
+
disabled={!canEdit || !props.onSetHighlightColor}
|
|
216
|
+
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
217
|
+
onSelect={(value) => props.onSetHighlightColor?.(value)}
|
|
218
|
+
title="Highlight color"
|
|
219
|
+
/>
|
|
220
|
+
<ToolbarAlignmentPopover
|
|
221
|
+
activeAlignment={props.formattingState?.alignment}
|
|
222
|
+
disabled={!canEdit || !props.onSetAlignment}
|
|
223
|
+
onSelect={(alignment) => props.onSetAlignment?.(alignment)}
|
|
224
|
+
/>
|
|
141
225
|
|
|
142
226
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
143
227
|
|
|
144
228
|
<TwToolbarIconButton
|
|
145
229
|
icon={Outdent}
|
|
146
230
|
label="Outdent"
|
|
147
|
-
disabled={
|
|
231
|
+
disabled={!canEdit}
|
|
148
232
|
onClick={props.onOutdent}
|
|
149
233
|
/>
|
|
150
234
|
<TwToolbarIconButton
|
|
151
235
|
icon={Indent}
|
|
152
236
|
label="Indent"
|
|
153
|
-
disabled={
|
|
237
|
+
disabled={!canEdit}
|
|
154
238
|
onClick={props.onIndent}
|
|
155
239
|
/>
|
|
240
|
+
<ToolbarInsertMenu
|
|
241
|
+
disabled={!canEdit}
|
|
242
|
+
onInsertImage={props.onInsertImage}
|
|
243
|
+
onInsertPageBreak={props.onInsertPageBreak}
|
|
244
|
+
onInsertSectionBreak={props.onInsertSectionBreak}
|
|
245
|
+
onInsertTable={props.onInsertTable}
|
|
246
|
+
/>
|
|
156
247
|
|
|
157
248
|
{/* Story focus breadcrumb — visible when editing a secondary story */}
|
|
158
249
|
{props.activeStory && props.activeStory.kind !== "main" ? (
|
|
@@ -400,6 +491,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
400
491
|
align="end"
|
|
401
492
|
>
|
|
402
493
|
<TwHealthPanel
|
|
494
|
+
blockedReasons={props.blockedReasons}
|
|
403
495
|
compatibility={props.compatibility}
|
|
404
496
|
warnings={props.warnings}
|
|
405
497
|
/>
|
|
@@ -452,7 +544,7 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
452
544
|
const resolvedValue =
|
|
453
545
|
props.value && props.styles.some((style) => style.styleId === props.value)
|
|
454
546
|
? props.value
|
|
455
|
-
:
|
|
547
|
+
: "";
|
|
456
548
|
|
|
457
549
|
return (
|
|
458
550
|
<Select.Root
|
|
@@ -494,6 +586,452 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
494
586
|
);
|
|
495
587
|
}
|
|
496
588
|
|
|
589
|
+
function ToolbarFontFamilySelect(props: {
|
|
590
|
+
value?: string;
|
|
591
|
+
disabled: boolean;
|
|
592
|
+
onValueChange?: (fontFamily: string) => void;
|
|
593
|
+
}) {
|
|
594
|
+
const resolvedValue = props.value && FONT_FAMILIES.includes(props.value) ? props.value : "";
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
<Select.Root
|
|
598
|
+
disabled={props.disabled}
|
|
599
|
+
onValueChange={(value) => props.onValueChange?.(value)}
|
|
600
|
+
value={resolvedValue}
|
|
601
|
+
>
|
|
602
|
+
<Select.Trigger
|
|
603
|
+
aria-label="Font family"
|
|
604
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
605
|
+
className={`inline-flex h-7 min-w-[7rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
606
|
+
>
|
|
607
|
+
<Select.Value placeholder="Font" />
|
|
608
|
+
<Select.Icon>
|
|
609
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
610
|
+
</Select.Icon>
|
|
611
|
+
</Select.Trigger>
|
|
612
|
+
<Select.Portal>
|
|
613
|
+
<Select.Content
|
|
614
|
+
align="start"
|
|
615
|
+
className="z-50 overflow-hidden rounded-lg bg-canvas shadow-lg ring-1 ring-border"
|
|
616
|
+
position="popper"
|
|
617
|
+
sideOffset={8}
|
|
618
|
+
>
|
|
619
|
+
<Select.Viewport className="p-1">
|
|
620
|
+
{FONT_FAMILIES.map((font) => (
|
|
621
|
+
<Select.Item
|
|
622
|
+
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}`}
|
|
623
|
+
key={font}
|
|
624
|
+
value={font}
|
|
625
|
+
>
|
|
626
|
+
<Select.ItemText>{font}</Select.ItemText>
|
|
627
|
+
</Select.Item>
|
|
628
|
+
))}
|
|
629
|
+
</Select.Viewport>
|
|
630
|
+
</Select.Content>
|
|
631
|
+
</Select.Portal>
|
|
632
|
+
</Select.Root>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function ToolbarFontSizeSelect(props: {
|
|
637
|
+
value?: number;
|
|
638
|
+
disabled: boolean;
|
|
639
|
+
onValueChange?: (fontSize: number) => void;
|
|
640
|
+
}) {
|
|
641
|
+
const resolvedValue =
|
|
642
|
+
typeof props.value === "number" && FONT_SIZES.includes(props.value) ? String(props.value) : "";
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<Select.Root
|
|
646
|
+
disabled={props.disabled}
|
|
647
|
+
onValueChange={(value) => props.onValueChange?.(Number(value))}
|
|
648
|
+
value={resolvedValue}
|
|
649
|
+
>
|
|
650
|
+
<Select.Trigger
|
|
651
|
+
aria-label="Font size"
|
|
652
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
653
|
+
className={`inline-flex h-7 min-w-[4rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
654
|
+
>
|
|
655
|
+
<Select.Value placeholder="Size" />
|
|
656
|
+
<Select.Icon>
|
|
657
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
658
|
+
</Select.Icon>
|
|
659
|
+
</Select.Trigger>
|
|
660
|
+
<Select.Portal>
|
|
661
|
+
<Select.Content
|
|
662
|
+
align="start"
|
|
663
|
+
className="z-50 overflow-hidden rounded-lg bg-canvas shadow-lg ring-1 ring-border"
|
|
664
|
+
position="popper"
|
|
665
|
+
sideOffset={8}
|
|
666
|
+
>
|
|
667
|
+
<Select.Viewport className="p-1">
|
|
668
|
+
{FONT_SIZES.map((size) => (
|
|
669
|
+
<Select.Item
|
|
670
|
+
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}`}
|
|
671
|
+
key={size}
|
|
672
|
+
value={String(size)}
|
|
673
|
+
>
|
|
674
|
+
<Select.ItemText>{size}</Select.ItemText>
|
|
675
|
+
</Select.Item>
|
|
676
|
+
))}
|
|
677
|
+
</Select.Viewport>
|
|
678
|
+
</Select.Content>
|
|
679
|
+
</Select.Portal>
|
|
680
|
+
</Select.Root>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function ToolbarFormattingOverflow(props: {
|
|
685
|
+
disabled: boolean;
|
|
686
|
+
formattingState?: FormattingStateSnapshot;
|
|
687
|
+
onToggleStrikethrough?: () => void;
|
|
688
|
+
onToggleSuperscript?: () => void;
|
|
689
|
+
onToggleSubscript?: () => void;
|
|
690
|
+
}) {
|
|
691
|
+
const [open, setOpen] = React.useState(false);
|
|
692
|
+
|
|
693
|
+
return (
|
|
694
|
+
<div className="relative">
|
|
695
|
+
<Tooltip.Root>
|
|
696
|
+
<Tooltip.Trigger asChild>
|
|
697
|
+
<button
|
|
698
|
+
type="button"
|
|
699
|
+
aria-label="More text formatting"
|
|
700
|
+
aria-expanded={open}
|
|
701
|
+
disabled={props.disabled}
|
|
702
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
703
|
+
onClick={() => setOpen((value) => !value)}
|
|
704
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
705
|
+
>
|
|
706
|
+
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
707
|
+
</button>
|
|
708
|
+
</Tooltip.Trigger>
|
|
709
|
+
<Tooltip.Portal>
|
|
710
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
711
|
+
More text formatting
|
|
712
|
+
</Tooltip.Content>
|
|
713
|
+
</Tooltip.Portal>
|
|
714
|
+
</Tooltip.Root>
|
|
715
|
+
{open ? (
|
|
716
|
+
<div className="absolute left-0 top-9 z-50 w-[220px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border">
|
|
717
|
+
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
718
|
+
Text styling
|
|
719
|
+
</div>
|
|
720
|
+
<div className="grid grid-cols-3 gap-1">
|
|
721
|
+
<ToolbarPopoverActionButton
|
|
722
|
+
active={props.formattingState?.strikethrough ?? false}
|
|
723
|
+
ariaLabel="Strikethrough"
|
|
724
|
+
disabled={props.disabled}
|
|
725
|
+
icon={<Strikethrough className="h-3.5 w-3.5" />}
|
|
726
|
+
onClick={() => {
|
|
727
|
+
props.onToggleStrikethrough?.();
|
|
728
|
+
setOpen(false);
|
|
729
|
+
}}
|
|
730
|
+
/>
|
|
731
|
+
<ToolbarPopoverActionButton
|
|
732
|
+
active={props.formattingState?.superscript ?? false}
|
|
733
|
+
ariaLabel="Superscript"
|
|
734
|
+
disabled={props.disabled}
|
|
735
|
+
icon={<Superscript className="h-3.5 w-3.5" />}
|
|
736
|
+
onClick={() => {
|
|
737
|
+
props.onToggleSuperscript?.();
|
|
738
|
+
setOpen(false);
|
|
739
|
+
}}
|
|
740
|
+
/>
|
|
741
|
+
<ToolbarPopoverActionButton
|
|
742
|
+
active={props.formattingState?.subscript ?? false}
|
|
743
|
+
ariaLabel="Subscript"
|
|
744
|
+
disabled={props.disabled}
|
|
745
|
+
icon={<Subscript className="h-3.5 w-3.5" />}
|
|
746
|
+
onClick={() => {
|
|
747
|
+
props.onToggleSubscript?.();
|
|
748
|
+
setOpen(false);
|
|
749
|
+
}}
|
|
750
|
+
/>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
) : null}
|
|
754
|
+
</div>
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function ToolbarColorPopover(props: {
|
|
759
|
+
ariaLabel: string;
|
|
760
|
+
colors: ReadonlyArray<{ value: string | null; label: string }>;
|
|
761
|
+
disabled: boolean;
|
|
762
|
+
icon: React.ReactNode;
|
|
763
|
+
title: string;
|
|
764
|
+
onSelect: (value: string | null) => void;
|
|
765
|
+
}) {
|
|
766
|
+
const [open, setOpen] = React.useState(false);
|
|
767
|
+
|
|
768
|
+
return (
|
|
769
|
+
<div className="relative">
|
|
770
|
+
<Tooltip.Root>
|
|
771
|
+
<Tooltip.Trigger asChild>
|
|
772
|
+
<button
|
|
773
|
+
type="button"
|
|
774
|
+
aria-label={props.ariaLabel}
|
|
775
|
+
aria-expanded={open}
|
|
776
|
+
disabled={props.disabled}
|
|
777
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
778
|
+
onClick={() => setOpen((value) => !value)}
|
|
779
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
780
|
+
>
|
|
781
|
+
{props.icon}
|
|
782
|
+
</button>
|
|
783
|
+
</Tooltip.Trigger>
|
|
784
|
+
<Tooltip.Portal>
|
|
785
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
786
|
+
{props.title}
|
|
787
|
+
</Tooltip.Content>
|
|
788
|
+
</Tooltip.Portal>
|
|
789
|
+
</Tooltip.Root>
|
|
790
|
+
{open ? (
|
|
791
|
+
<div className="absolute left-0 top-9 z-50 w-[180px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border">
|
|
792
|
+
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
793
|
+
{props.title}
|
|
794
|
+
</div>
|
|
795
|
+
<div className="grid grid-cols-3 gap-1">
|
|
796
|
+
{props.colors.map((color) => (
|
|
797
|
+
<button
|
|
798
|
+
key={`${props.ariaLabel}-${color.label}`}
|
|
799
|
+
type="button"
|
|
800
|
+
aria-label={`${props.title} ${color.label}`}
|
|
801
|
+
disabled={props.disabled}
|
|
802
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
803
|
+
onClick={() => {
|
|
804
|
+
props.onSelect(color.value);
|
|
805
|
+
setOpen(false);
|
|
806
|
+
}}
|
|
807
|
+
className={`inline-flex h-8 items-center justify-center rounded-md border border-border text-[10px] font-medium text-primary transition-transform hover:scale-[1.04] disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
808
|
+
color.value ? "" : "bg-surface"
|
|
809
|
+
} ${focusRingClass}`}
|
|
810
|
+
style={color.value ? { backgroundColor: color.value } : undefined}
|
|
811
|
+
>
|
|
812
|
+
{color.value ? <span className="sr-only">{color.label}</span> : "None"}
|
|
813
|
+
</button>
|
|
814
|
+
))}
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
) : null}
|
|
818
|
+
</div>
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function ToolbarAlignmentPopover(props: {
|
|
823
|
+
activeAlignment?: FormattingAlignment;
|
|
824
|
+
disabled: boolean;
|
|
825
|
+
onSelect: (alignment: FormattingAlignment) => void;
|
|
826
|
+
}) {
|
|
827
|
+
const [open, setOpen] = React.useState(false);
|
|
828
|
+
const alignments = [
|
|
829
|
+
{ value: "left" as const, label: "Align left", icon: <AlignLeft className="h-3.5 w-3.5" /> },
|
|
830
|
+
{ value: "center" as const, label: "Align center", icon: <AlignCenter className="h-3.5 w-3.5" /> },
|
|
831
|
+
{ value: "right" as const, label: "Align right", icon: <AlignRight className="h-3.5 w-3.5" /> },
|
|
832
|
+
{ value: "justify" as const, label: "Align justify", icon: <AlignJustify className="h-3.5 w-3.5" /> },
|
|
833
|
+
];
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<div className="relative">
|
|
837
|
+
<Tooltip.Root>
|
|
838
|
+
<Tooltip.Trigger asChild>
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
aria-label="Paragraph alignment"
|
|
842
|
+
aria-expanded={open}
|
|
843
|
+
disabled={props.disabled}
|
|
844
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
845
|
+
onClick={() => setOpen((value) => !value)}
|
|
846
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
847
|
+
>
|
|
848
|
+
{(alignments.find((entry) => entry.value === props.activeAlignment) ?? alignments[0])?.icon}
|
|
849
|
+
</button>
|
|
850
|
+
</Tooltip.Trigger>
|
|
851
|
+
<Tooltip.Portal>
|
|
852
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
853
|
+
Paragraph alignment
|
|
854
|
+
</Tooltip.Content>
|
|
855
|
+
</Tooltip.Portal>
|
|
856
|
+
</Tooltip.Root>
|
|
857
|
+
{open ? (
|
|
858
|
+
<div className="absolute left-0 top-9 z-50 w-[220px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border">
|
|
859
|
+
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
860
|
+
Paragraph alignment
|
|
861
|
+
</div>
|
|
862
|
+
<div className="grid grid-cols-2 gap-1">
|
|
863
|
+
{alignments.map((entry) => (
|
|
864
|
+
<ToolbarPopoverActionButton
|
|
865
|
+
key={entry.value}
|
|
866
|
+
active={props.activeAlignment === entry.value}
|
|
867
|
+
ariaLabel={entry.label}
|
|
868
|
+
disabled={props.disabled}
|
|
869
|
+
icon={entry.icon}
|
|
870
|
+
onClick={() => {
|
|
871
|
+
props.onSelect(entry.value);
|
|
872
|
+
setOpen(false);
|
|
873
|
+
}}
|
|
874
|
+
/>
|
|
875
|
+
))}
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
) : null}
|
|
879
|
+
</div>
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function ToolbarInsertMenu(props: {
|
|
884
|
+
disabled: boolean;
|
|
885
|
+
onInsertPageBreak?: () => void;
|
|
886
|
+
onInsertTable?: () => void;
|
|
887
|
+
onInsertSectionBreak?: (type: SectionBreakType) => void;
|
|
888
|
+
onInsertImage?: (options: InsertImageOptions) => void;
|
|
889
|
+
}) {
|
|
890
|
+
const [open, setOpen] = React.useState(false);
|
|
891
|
+
|
|
892
|
+
async function handleImageChange(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
|
893
|
+
const file = event.target.files?.[0];
|
|
894
|
+
if (!file || props.disabled || !props.onInsertImage) {
|
|
895
|
+
event.target.value = "";
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
899
|
+
props.onInsertImage({
|
|
900
|
+
data,
|
|
901
|
+
mimeType: file.type || "image/png",
|
|
902
|
+
altText: file.name,
|
|
903
|
+
});
|
|
904
|
+
setOpen(false);
|
|
905
|
+
event.target.value = "";
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return (
|
|
909
|
+
<div className="relative">
|
|
910
|
+
<Tooltip.Root>
|
|
911
|
+
<Tooltip.Trigger asChild>
|
|
912
|
+
<button
|
|
913
|
+
type="button"
|
|
914
|
+
aria-label="Insert"
|
|
915
|
+
aria-expanded={open}
|
|
916
|
+
disabled={props.disabled}
|
|
917
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
918
|
+
onClick={() => setOpen((value) => !value)}
|
|
919
|
+
className={`inline-flex h-7 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
920
|
+
>
|
|
921
|
+
Insert
|
|
922
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
923
|
+
</button>
|
|
924
|
+
</Tooltip.Trigger>
|
|
925
|
+
<Tooltip.Portal>
|
|
926
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
927
|
+
Insert
|
|
928
|
+
</Tooltip.Content>
|
|
929
|
+
</Tooltip.Portal>
|
|
930
|
+
</Tooltip.Root>
|
|
931
|
+
{open ? (
|
|
932
|
+
<div className="absolute left-0 top-9 z-50 w-[220px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border">
|
|
933
|
+
<div className="space-y-1">
|
|
934
|
+
<ToolbarMenuButton
|
|
935
|
+
ariaLabel="Insert page break"
|
|
936
|
+
disabled={props.disabled || !props.onInsertPageBreak}
|
|
937
|
+
icon={<Minus className="h-3.5 w-3.5" />}
|
|
938
|
+
label="Page break"
|
|
939
|
+
onClick={() => {
|
|
940
|
+
props.onInsertPageBreak?.();
|
|
941
|
+
setOpen(false);
|
|
942
|
+
}}
|
|
943
|
+
/>
|
|
944
|
+
<ToolbarMenuButton
|
|
945
|
+
ariaLabel="Insert table"
|
|
946
|
+
disabled={props.disabled || !props.onInsertTable}
|
|
947
|
+
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
948
|
+
label="Table"
|
|
949
|
+
onClick={() => {
|
|
950
|
+
props.onInsertTable?.();
|
|
951
|
+
setOpen(false);
|
|
952
|
+
}}
|
|
953
|
+
/>
|
|
954
|
+
<label
|
|
955
|
+
className={`flex h-8 cursor-pointer items-center gap-2 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:bg-surface ${
|
|
956
|
+
props.disabled || !props.onInsertImage ? "pointer-events-none opacity-40" : ""
|
|
957
|
+
}`}
|
|
958
|
+
>
|
|
959
|
+
<ImagePlus className="h-3.5 w-3.5 text-secondary" />
|
|
960
|
+
<span>Image</span>
|
|
961
|
+
<input
|
|
962
|
+
accept="image/png,image/jpeg,image/gif"
|
|
963
|
+
aria-label="Insert image"
|
|
964
|
+
className="sr-only"
|
|
965
|
+
disabled={props.disabled || !props.onInsertImage}
|
|
966
|
+
type="file"
|
|
967
|
+
onChange={(event) => {
|
|
968
|
+
void handleImageChange(event);
|
|
969
|
+
}}
|
|
970
|
+
/>
|
|
971
|
+
</label>
|
|
972
|
+
<ToolbarMenuButton
|
|
973
|
+
ariaLabel="Insert next-page section break"
|
|
974
|
+
disabled={props.disabled || !props.onInsertSectionBreak}
|
|
975
|
+
icon={<FileText className="h-3.5 w-3.5" />}
|
|
976
|
+
label="Next-page section break"
|
|
977
|
+
onClick={() => {
|
|
978
|
+
props.onInsertSectionBreak?.("nextPage");
|
|
979
|
+
setOpen(false);
|
|
980
|
+
}}
|
|
981
|
+
/>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
) : null}
|
|
985
|
+
</div>
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function ToolbarPopoverActionButton(props: {
|
|
990
|
+
active: boolean;
|
|
991
|
+
ariaLabel: string;
|
|
992
|
+
disabled: boolean;
|
|
993
|
+
icon: React.ReactNode;
|
|
994
|
+
onClick?: () => void;
|
|
995
|
+
}) {
|
|
996
|
+
return (
|
|
997
|
+
<button
|
|
998
|
+
type="button"
|
|
999
|
+
aria-label={props.ariaLabel}
|
|
1000
|
+
aria-pressed={props.active}
|
|
1001
|
+
disabled={props.disabled}
|
|
1002
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1003
|
+
onClick={props.onClick}
|
|
1004
|
+
className={`inline-flex h-8 items-center justify-center rounded-md border border-border transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
1005
|
+
props.active ? "bg-accent-soft text-accent" : "bg-canvas text-secondary hover:bg-surface"
|
|
1006
|
+
} ${focusRingClass}`}
|
|
1007
|
+
>
|
|
1008
|
+
{props.icon}
|
|
1009
|
+
</button>
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function ToolbarMenuButton(props: {
|
|
1014
|
+
ariaLabel: string;
|
|
1015
|
+
disabled: boolean;
|
|
1016
|
+
icon: React.ReactNode;
|
|
1017
|
+
label: string;
|
|
1018
|
+
onClick?: () => void;
|
|
1019
|
+
}) {
|
|
1020
|
+
return (
|
|
1021
|
+
<button
|
|
1022
|
+
type="button"
|
|
1023
|
+
aria-label={props.ariaLabel}
|
|
1024
|
+
disabled={props.disabled}
|
|
1025
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1026
|
+
onClick={props.onClick}
|
|
1027
|
+
className={`flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
1028
|
+
>
|
|
1029
|
+
<span className="text-secondary">{props.icon}</span>
|
|
1030
|
+
<span>{props.label}</span>
|
|
1031
|
+
</button>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
497
1035
|
function storyLabel(target: EditorStoryTarget): string {
|
|
498
1036
|
switch (target.kind) {
|
|
499
1037
|
case "header":
|