@beyondwork/docx-react-component 1.0.93 → 1.0.95
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 +1 -1
- package/src/api/v3/ai/replacement.ts +3 -3
- package/src/api/v3/runtime/formatting.ts +3 -3
- package/src/core/commands/formatting-commands.ts +146 -3
- package/src/core/state/text-transaction.ts +6 -3
- package/src/runtime/document-runtime.ts +3 -0
- package/src/runtime/scopes/semantic-scope-types.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +9 -14
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +109 -42
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -5
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +46 -15
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -7
- package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +34 -12
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +78 -16
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +389 -109
- package/src/ui-tailwind/tw-review-workspace.tsx +20 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
2
3
|
|
|
3
4
|
import * as Popover from "@radix-ui/react-popover";
|
|
4
5
|
import * as Select from "@radix-ui/react-select";
|
|
@@ -83,6 +84,13 @@ export interface TwToolbarProps {
|
|
|
83
84
|
scopedChromePolicy?: ScopedChromePolicy;
|
|
84
85
|
preset?: WordReviewEditorChromePreset;
|
|
85
86
|
compactMode?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* True when the runtime has an active editable text/paragraph target
|
|
89
|
+
* (focused caret or range selection). Formatting, paragraph, list, and
|
|
90
|
+
* structural insert controls use this in addition to document-level edit
|
|
91
|
+
* capability so top chrome does not advertise commands against no target.
|
|
92
|
+
*/
|
|
93
|
+
hasEditableSelectionTarget?: boolean;
|
|
86
94
|
workspaceMode: WorkspaceMode;
|
|
87
95
|
zoomLevel?: ZoomLevel;
|
|
88
96
|
formattingState?: FormattingStateSnapshot;
|
|
@@ -231,6 +239,80 @@ const HIGHLIGHT_COLORS = [
|
|
|
231
239
|
{ value: null, label: "None" },
|
|
232
240
|
] as const;
|
|
233
241
|
|
|
242
|
+
function ToolbarPortalMenu(props: {
|
|
243
|
+
anchorRef: React.RefObject<HTMLButtonElement | null>;
|
|
244
|
+
align?: "start" | "end";
|
|
245
|
+
children: React.ReactNode;
|
|
246
|
+
className: string;
|
|
247
|
+
menuWidthPx: number;
|
|
248
|
+
open: boolean;
|
|
249
|
+
}): React.ReactPortal | null {
|
|
250
|
+
const style = useToolbarPortalPosition({
|
|
251
|
+
align: props.align ?? "start",
|
|
252
|
+
anchorRef: props.anchorRef,
|
|
253
|
+
menuWidthPx: props.menuWidthPx,
|
|
254
|
+
open: props.open,
|
|
255
|
+
});
|
|
256
|
+
const body = props.anchorRef.current?.ownerDocument?.body;
|
|
257
|
+
if (!props.open || !body) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return createPortal(
|
|
261
|
+
<div className={props.className} style={style}>
|
|
262
|
+
{props.children}
|
|
263
|
+
</div>,
|
|
264
|
+
body,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function useToolbarPortalPosition(input: {
|
|
269
|
+
align: "start" | "end";
|
|
270
|
+
anchorRef: React.RefObject<HTMLButtonElement | null>;
|
|
271
|
+
menuWidthPx: number;
|
|
272
|
+
open: boolean;
|
|
273
|
+
}): React.CSSProperties {
|
|
274
|
+
const [style, setStyle] = React.useState<React.CSSProperties>({
|
|
275
|
+
left: 8,
|
|
276
|
+
position: "fixed",
|
|
277
|
+
top: 8,
|
|
278
|
+
zIndex: 50,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
React.useLayoutEffect(() => {
|
|
282
|
+
if (!input.open) return;
|
|
283
|
+
const anchor = input.anchorRef.current;
|
|
284
|
+
const ownerWindow = anchor?.ownerDocument?.defaultView;
|
|
285
|
+
if (!anchor || !ownerWindow) return;
|
|
286
|
+
|
|
287
|
+
const update = () => {
|
|
288
|
+
const rect = anchor.getBoundingClientRect();
|
|
289
|
+
const viewportWidth = ownerWindow.innerWidth || input.menuWidthPx + 16;
|
|
290
|
+
const rawLeft =
|
|
291
|
+
input.align === "end" ? rect.right - input.menuWidthPx : rect.left;
|
|
292
|
+
const left = Math.min(
|
|
293
|
+
Math.max(8, rawLeft),
|
|
294
|
+
Math.max(8, viewportWidth - input.menuWidthPx - 8),
|
|
295
|
+
);
|
|
296
|
+
setStyle({
|
|
297
|
+
left,
|
|
298
|
+
position: "fixed",
|
|
299
|
+
top: Math.max(8, rect.bottom + 8),
|
|
300
|
+
zIndex: 50,
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
update();
|
|
305
|
+
ownerWindow.addEventListener("resize", update);
|
|
306
|
+
ownerWindow.addEventListener("scroll", update, true);
|
|
307
|
+
return () => {
|
|
308
|
+
ownerWindow.removeEventListener("resize", update);
|
|
309
|
+
ownerWindow.removeEventListener("scroll", update, true);
|
|
310
|
+
};
|
|
311
|
+
}, [input.align, input.anchorRef, input.menuWidthPx, input.open]);
|
|
312
|
+
|
|
313
|
+
return style;
|
|
314
|
+
}
|
|
315
|
+
|
|
234
316
|
export function TwToolbar(props: TwToolbarProps) {
|
|
235
317
|
const caps = props.capabilities;
|
|
236
318
|
const preset = props.preset ?? "advanced";
|
|
@@ -238,8 +320,21 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
238
320
|
const workspaceMode = props.workspaceMode;
|
|
239
321
|
const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
|
|
240
322
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
241
|
-
const
|
|
242
|
-
const
|
|
323
|
+
const hasEditableSelectionTarget = props.hasEditableSelectionTarget ?? true;
|
|
324
|
+
const baseCanFormatText = props.interactionPolicy?.canFormatText ?? (caps ? caps.canEdit : false);
|
|
325
|
+
const baseCanInsertStructural = props.interactionPolicy?.canInsertStructural ?? baseCanFormatText;
|
|
326
|
+
const selectionTargetDisabledReason = !hasEditableSelectionTarget
|
|
327
|
+
? "Place the cursor in the document or select text first."
|
|
328
|
+
: undefined;
|
|
329
|
+
const editModeDisabledReason =
|
|
330
|
+
props.interactionPolicy && props.interactionPolicy.mode !== "edit"
|
|
331
|
+
? `Not available in ${props.interactionPolicy.mode} mode.`
|
|
332
|
+
: "Editing is not available in this document.";
|
|
333
|
+
const editDisabledReason = selectionTargetDisabledReason ?? (!baseCanFormatText ? editModeDisabledReason : undefined);
|
|
334
|
+
const insertDisabledReason =
|
|
335
|
+
selectionTargetDisabledReason ?? (!baseCanInsertStructural ? editModeDisabledReason : undefined);
|
|
336
|
+
const canEdit = baseCanFormatText && hasEditableSelectionTarget;
|
|
337
|
+
const canInsertStructural = baseCanInsertStructural && hasEditableSelectionTarget;
|
|
243
338
|
const canAddComment = props.interactionPolicy?.canAddComment ?? (caps ? caps.canAddComment : false);
|
|
244
339
|
const showDiagnosticsChrome = props.showDiagnosticsChrome ?? true;
|
|
245
340
|
const scopedChromePolicy = props.scopedChromePolicy ?? resolveScopedChromePolicy({
|
|
@@ -354,6 +449,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
354
449
|
<>
|
|
355
450
|
<ToolbarParagraphStyleSelect
|
|
356
451
|
disabled={!canEdit || paragraphStyles.length === 0 || !props.onSetParagraphStyle}
|
|
452
|
+
disabledReason={editDisabledReason}
|
|
357
453
|
styles={paragraphStyles}
|
|
358
454
|
value={props.formattingState?.paragraphStyleId}
|
|
359
455
|
hasMixedValue={props.hasMixedParagraphStyle ?? false}
|
|
@@ -362,12 +458,14 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
362
458
|
|
|
363
459
|
<ToolbarFontFamilySelect
|
|
364
460
|
disabled={!canEdit || !props.onSetFontFamily}
|
|
461
|
+
disabledReason={editDisabledReason}
|
|
365
462
|
value={props.formattingState?.fontFamily}
|
|
366
463
|
hasMixedValue={props.hasMixedFontFamily ?? false}
|
|
367
464
|
onValueChange={props.onSetFontFamily}
|
|
368
465
|
/>
|
|
369
466
|
<ToolbarFontSizeSelect
|
|
370
467
|
disabled={!canEdit || !props.onSetFontSize}
|
|
468
|
+
disabledReason={editDisabledReason}
|
|
371
469
|
value={props.formattingState?.fontSize}
|
|
372
470
|
hasMixedValue={props.hasMixedFontSize ?? false}
|
|
373
471
|
onValueChange={props.onSetFontSize}
|
|
@@ -385,6 +483,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
385
483
|
shortcut="⌘B"
|
|
386
484
|
active={props.formattingState?.bold ?? false}
|
|
387
485
|
disabled={!canEdit || !props.onToggleBold}
|
|
486
|
+
disabledReason={editDisabledReason}
|
|
388
487
|
onClick={props.onToggleBold}
|
|
389
488
|
/>
|
|
390
489
|
<TwToolbarIconButton
|
|
@@ -393,6 +492,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
393
492
|
shortcut="⌘I"
|
|
394
493
|
active={props.formattingState?.italic ?? false}
|
|
395
494
|
disabled={!canEdit || !props.onToggleItalic}
|
|
495
|
+
disabledReason={editDisabledReason}
|
|
396
496
|
onClick={props.onToggleItalic}
|
|
397
497
|
/>
|
|
398
498
|
<TwToolbarIconButton
|
|
@@ -401,6 +501,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
401
501
|
shortcut="⌘U"
|
|
402
502
|
active={props.formattingState?.underline ?? false}
|
|
403
503
|
disabled={!canEdit || !props.onToggleUnderline}
|
|
504
|
+
disabledReason={editDisabledReason}
|
|
404
505
|
onClick={props.onToggleUnderline}
|
|
405
506
|
/>
|
|
406
507
|
{showAdvancedFormatting ? (
|
|
@@ -411,6 +512,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
411
512
|
!props.onToggleSuperscript &&
|
|
412
513
|
!props.onToggleSubscript)
|
|
413
514
|
}
|
|
515
|
+
disabledReason={editDisabledReason}
|
|
414
516
|
formattingState={props.formattingState}
|
|
415
517
|
onToggleStrikethrough={props.onToggleStrikethrough}
|
|
416
518
|
onToggleSuperscript={props.onToggleSuperscript}
|
|
@@ -427,7 +529,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
427
529
|
ariaLabel="Text color"
|
|
428
530
|
colors={TEXT_COLORS.map((value) => ({ value, label: value }))}
|
|
429
531
|
disabled={!canEdit || !props.onSetTextColor}
|
|
532
|
+
disabledReason={editDisabledReason}
|
|
430
533
|
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
534
|
+
activeValue={props.formattingState?.textColor?.toLowerCase()}
|
|
431
535
|
onSelect={(value) => {
|
|
432
536
|
if (value) {
|
|
433
537
|
props.onSetTextColor?.(value);
|
|
@@ -439,7 +543,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
439
543
|
ariaLabel="Highlight color"
|
|
440
544
|
colors={HIGHLIGHT_COLORS.map((entry) => ({ value: entry.value, label: entry.label }))}
|
|
441
545
|
disabled={!canEdit || !props.onSetHighlightColor}
|
|
546
|
+
disabledReason={editDisabledReason}
|
|
442
547
|
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
548
|
+
activeValue={props.formattingState?.highlightColor?.toLowerCase() ?? null}
|
|
443
549
|
onSelect={(value) => props.onSetHighlightColor?.(value)}
|
|
444
550
|
title="Highlight color"
|
|
445
551
|
/>
|
|
@@ -449,6 +555,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
449
555
|
<ToolbarAlignmentPopover
|
|
450
556
|
activeAlignment={props.formattingState?.alignment}
|
|
451
557
|
disabled={!canEdit || !props.onSetAlignment}
|
|
558
|
+
disabledReason={editDisabledReason}
|
|
452
559
|
onSelect={(alignment) => props.onSetAlignment?.(alignment)}
|
|
453
560
|
/>
|
|
454
561
|
) : null}
|
|
@@ -464,6 +571,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
464
571
|
label="Bulleted list"
|
|
465
572
|
active={Boolean(props.activeListContext && !props.activeListContext.isOrdered)}
|
|
466
573
|
disabled={!canEdit || !props.onToggleBulletedList}
|
|
574
|
+
disabledReason={editDisabledReason}
|
|
467
575
|
onClick={props.onToggleBulletedList}
|
|
468
576
|
/>
|
|
469
577
|
<TwToolbarIconButton
|
|
@@ -471,6 +579,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
471
579
|
label="Numbered list"
|
|
472
580
|
active={Boolean(props.activeListContext?.isOrdered)}
|
|
473
581
|
disabled={!canEdit || !props.onToggleNumberedList}
|
|
582
|
+
disabledReason={editDisabledReason}
|
|
474
583
|
onClick={props.onToggleNumberedList}
|
|
475
584
|
/>
|
|
476
585
|
</>
|
|
@@ -481,12 +590,14 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
481
590
|
icon={Outdent}
|
|
482
591
|
label="Outdent"
|
|
483
592
|
disabled={!canEdit || !props.onOutdent}
|
|
593
|
+
disabledReason={editDisabledReason}
|
|
484
594
|
onClick={props.onOutdent}
|
|
485
595
|
/>
|
|
486
596
|
<TwToolbarIconButton
|
|
487
597
|
icon={Indent}
|
|
488
598
|
label="Indent"
|
|
489
599
|
disabled={!canEdit || !props.onIndent}
|
|
600
|
+
disabledReason={editDisabledReason}
|
|
490
601
|
onClick={props.onIndent}
|
|
491
602
|
/>
|
|
492
603
|
</>
|
|
@@ -497,6 +608,8 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
497
608
|
type="button"
|
|
498
609
|
aria-label="Restart numbering"
|
|
499
610
|
disabled={!canEdit || !props.onRestartNumbering}
|
|
611
|
+
title={!canEdit && editDisabledReason ? `Not available: ${editDisabledReason}` : undefined}
|
|
612
|
+
data-disabled-reason={!canEdit && editDisabledReason ? editDisabledReason : undefined}
|
|
500
613
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
501
614
|
onClick={props.onRestartNumbering}
|
|
502
615
|
className={`inline-flex h-6 items-center rounded-md px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
@@ -507,6 +620,8 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
507
620
|
type="button"
|
|
508
621
|
aria-label="Continue numbering"
|
|
509
622
|
disabled={!canEdit || !props.onContinueNumbering}
|
|
623
|
+
title={!canEdit && editDisabledReason ? `Not available: ${editDisabledReason}` : undefined}
|
|
624
|
+
data-disabled-reason={!canEdit && editDisabledReason ? editDisabledReason : undefined}
|
|
510
625
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
511
626
|
onClick={props.onContinueNumbering}
|
|
512
627
|
className={`inline-flex h-6 items-center rounded-md px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
@@ -518,6 +633,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
518
633
|
{showInsertMenu && showInsertActionsInRow ? (
|
|
519
634
|
<ToolbarInsertMenu
|
|
520
635
|
disabled={!canInsertStructural}
|
|
636
|
+
disabledReason={insertDisabledReason}
|
|
521
637
|
disabledReasons={props.insertDisabledReasons}
|
|
522
638
|
onInsertImage={props.onInsertImage}
|
|
523
639
|
onInsertPageBreak={props.onInsertPageBreak}
|
|
@@ -531,6 +647,8 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
531
647
|
type="button"
|
|
532
648
|
aria-label="Refresh fields"
|
|
533
649
|
disabled={!canEdit || !props.onUpdateFields}
|
|
650
|
+
title={!canEdit && editDisabledReason ? `Not available: ${editDisabledReason}` : undefined}
|
|
651
|
+
data-disabled-reason={!canEdit && editDisabledReason ? editDisabledReason : undefined}
|
|
534
652
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
535
653
|
onClick={props.onUpdateFields}
|
|
536
654
|
className={`inline-flex h-6 items-center rounded-md px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
@@ -541,6 +659,8 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
541
659
|
type="button"
|
|
542
660
|
aria-label="Refresh table of contents"
|
|
543
661
|
disabled={!canEdit || !props.onUpdateTableOfContents}
|
|
662
|
+
title={!canEdit && editDisabledReason ? `Not available: ${editDisabledReason}` : undefined}
|
|
663
|
+
data-disabled-reason={!canEdit && editDisabledReason ? editDisabledReason : undefined}
|
|
544
664
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
545
665
|
onClick={props.onUpdateTableOfContents}
|
|
546
666
|
className={`inline-flex h-6 items-center rounded-md px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
@@ -554,7 +674,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
554
674
|
activeListContext={props.activeListContext}
|
|
555
675
|
canEdit={canEdit}
|
|
556
676
|
canInsertStructural={canInsertStructural}
|
|
677
|
+
editDisabledReason={editDisabledReason}
|
|
557
678
|
formattingState={props.formattingState}
|
|
679
|
+
insertDisabledReason={insertDisabledReason}
|
|
558
680
|
paragraphStyles={paragraphStyles}
|
|
559
681
|
showInsertMenu={getToolbarChromePlacement(scopedChromePolicy, "insert-actions") === "overflow"}
|
|
560
682
|
showListActions={getToolbarChromePlacement(scopedChromePolicy, "list-actions") === "overflow"}
|
|
@@ -918,6 +1040,7 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
918
1040
|
styles: StyleCatalogSnapshot["paragraphs"];
|
|
919
1041
|
value?: string;
|
|
920
1042
|
disabled: boolean;
|
|
1043
|
+
disabledReason?: string;
|
|
921
1044
|
hasMixedValue?: boolean;
|
|
922
1045
|
onValueChange?: (styleId: string) => void;
|
|
923
1046
|
}) {
|
|
@@ -939,7 +1062,9 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
939
1062
|
aria-label="Paragraph style"
|
|
940
1063
|
aria-disabled={props.disabled || undefined}
|
|
941
1064
|
data-disabled={props.disabled ? "" : undefined}
|
|
1065
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
942
1066
|
data-mixed={isMixed ? "true" : undefined}
|
|
1067
|
+
title={props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined}
|
|
943
1068
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
944
1069
|
className={`inline-flex h-6 min-w-[7.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
945
1070
|
>
|
|
@@ -984,6 +1109,7 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
984
1109
|
function ToolbarFontFamilySelect(props: {
|
|
985
1110
|
value?: string;
|
|
986
1111
|
disabled: boolean;
|
|
1112
|
+
disabledReason?: string;
|
|
987
1113
|
hasMixedValue?: boolean;
|
|
988
1114
|
onValueChange?: (fontFamily: string) => void;
|
|
989
1115
|
}) {
|
|
@@ -1003,7 +1129,9 @@ function ToolbarFontFamilySelect(props: {
|
|
|
1003
1129
|
aria-label="Font family"
|
|
1004
1130
|
aria-disabled={props.disabled || undefined}
|
|
1005
1131
|
data-disabled={props.disabled ? "" : undefined}
|
|
1132
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
1006
1133
|
data-mixed={isMixed ? "true" : undefined}
|
|
1134
|
+
title={props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined}
|
|
1007
1135
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1008
1136
|
className={`inline-flex h-6 min-w-[6.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
1009
1137
|
>
|
|
@@ -1048,6 +1176,7 @@ function ToolbarFontFamilySelect(props: {
|
|
|
1048
1176
|
function ToolbarFontSizeSelect(props: {
|
|
1049
1177
|
value?: number;
|
|
1050
1178
|
disabled: boolean;
|
|
1179
|
+
disabledReason?: string;
|
|
1051
1180
|
hasMixedValue?: boolean;
|
|
1052
1181
|
onValueChange?: (fontSize: number) => void;
|
|
1053
1182
|
}) {
|
|
@@ -1069,7 +1198,9 @@ function ToolbarFontSizeSelect(props: {
|
|
|
1069
1198
|
aria-label="Font size"
|
|
1070
1199
|
aria-disabled={props.disabled || undefined}
|
|
1071
1200
|
data-disabled={props.disabled ? "" : undefined}
|
|
1201
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
1072
1202
|
data-mixed={isMixed ? "true" : undefined}
|
|
1203
|
+
title={props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined}
|
|
1073
1204
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1074
1205
|
className={`inline-flex h-6 min-w-[3.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
1075
1206
|
>
|
|
@@ -1115,7 +1246,9 @@ function ToolbarCompactOverflow(props: {
|
|
|
1115
1246
|
activeListContext?: ActiveListContext | null;
|
|
1116
1247
|
canEdit: boolean;
|
|
1117
1248
|
canInsertStructural: boolean;
|
|
1249
|
+
editDisabledReason?: string;
|
|
1118
1250
|
formattingState?: FormattingStateSnapshot;
|
|
1251
|
+
insertDisabledReason?: string;
|
|
1119
1252
|
paragraphStyles: StyleCatalogSnapshot["paragraphs"];
|
|
1120
1253
|
showInsertMenu: boolean;
|
|
1121
1254
|
showListActions: boolean;
|
|
@@ -1140,6 +1273,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1140
1273
|
onUpdateTableOfContents?: () => void;
|
|
1141
1274
|
}) {
|
|
1142
1275
|
const [open, setOpen] = React.useState(false);
|
|
1276
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
1143
1277
|
|
|
1144
1278
|
async function handleImageChange(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
|
1145
1279
|
const file = event.target.files?.[0];
|
|
@@ -1159,19 +1293,28 @@ function ToolbarCompactOverflow(props: {
|
|
|
1159
1293
|
}
|
|
1160
1294
|
|
|
1161
1295
|
return (
|
|
1162
|
-
<
|
|
1296
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
1163
1297
|
<Tooltip.Root>
|
|
1164
1298
|
<Tooltip.Trigger asChild>
|
|
1165
|
-
<
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1299
|
+
<Popover.Anchor asChild>
|
|
1300
|
+
<button
|
|
1301
|
+
ref={triggerRef}
|
|
1302
|
+
type="button"
|
|
1303
|
+
aria-label="More document tools"
|
|
1304
|
+
aria-expanded={open}
|
|
1305
|
+
aria-haspopup="menu"
|
|
1306
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1307
|
+
onClick={(event) => {
|
|
1308
|
+
event.preventDefault();
|
|
1309
|
+
setOpen((value) => !value);
|
|
1310
|
+
}}
|
|
1311
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-canvas text-primary transition-colors hover:bg-surface outline-none ${
|
|
1312
|
+
open ? "text-accent ring-1 ring-accent/30 shadow-sm" : ""
|
|
1313
|
+
} ${focusRingClass}`}
|
|
1314
|
+
>
|
|
1315
|
+
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
1316
|
+
</button>
|
|
1317
|
+
</Popover.Anchor>
|
|
1175
1318
|
</Tooltip.Trigger>
|
|
1176
1319
|
<Tooltip.Portal>
|
|
1177
1320
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
@@ -1179,8 +1322,12 @@ function ToolbarCompactOverflow(props: {
|
|
|
1179
1322
|
</Tooltip.Content>
|
|
1180
1323
|
</Tooltip.Portal>
|
|
1181
1324
|
</Tooltip.Root>
|
|
1182
|
-
|
|
1183
|
-
|
|
1325
|
+
<ToolbarPortalMenu
|
|
1326
|
+
anchorRef={triggerRef}
|
|
1327
|
+
className="w-[min(20rem,calc(100vw-2rem))] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border"
|
|
1328
|
+
menuWidthPx={320}
|
|
1329
|
+
open={open}
|
|
1330
|
+
>
|
|
1184
1331
|
<div className="space-y-3">
|
|
1185
1332
|
{props.showStyleSelectors ? (
|
|
1186
1333
|
<div className="space-y-2">
|
|
@@ -1190,6 +1337,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1190
1337
|
<div className="grid gap-2">
|
|
1191
1338
|
<ToolbarParagraphStyleSelect
|
|
1192
1339
|
disabled={!props.canEdit || props.paragraphStyles.length === 0 || !props.onSetParagraphStyle}
|
|
1340
|
+
disabledReason={props.editDisabledReason}
|
|
1193
1341
|
styles={props.paragraphStyles}
|
|
1194
1342
|
value={props.formattingState?.paragraphStyleId}
|
|
1195
1343
|
onValueChange={(styleId) => {
|
|
@@ -1200,6 +1348,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1200
1348
|
<div className="grid grid-cols-2 gap-2">
|
|
1201
1349
|
<ToolbarFontFamilySelect
|
|
1202
1350
|
disabled={!props.canEdit || !props.onSetFontFamily}
|
|
1351
|
+
disabledReason={props.editDisabledReason}
|
|
1203
1352
|
value={props.formattingState?.fontFamily}
|
|
1204
1353
|
onValueChange={(fontFamily) => {
|
|
1205
1354
|
props.onSetFontFamily?.(fontFamily);
|
|
@@ -1208,6 +1357,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1208
1357
|
/>
|
|
1209
1358
|
<ToolbarFontSizeSelect
|
|
1210
1359
|
disabled={!props.canEdit || !props.onSetFontSize}
|
|
1360
|
+
disabledReason={props.editDisabledReason}
|
|
1211
1361
|
value={props.formattingState?.fontSize}
|
|
1212
1362
|
onValueChange={(fontSize) => {
|
|
1213
1363
|
props.onSetFontSize?.(fontSize);
|
|
@@ -1227,6 +1377,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1227
1377
|
<ToolbarMenuButton
|
|
1228
1378
|
ariaLabel="Bulleted list"
|
|
1229
1379
|
disabled={!props.canEdit || !props.onToggleBulletedList}
|
|
1380
|
+
disabledReason={props.editDisabledReason}
|
|
1230
1381
|
icon={<List className="h-3.5 w-3.5" />}
|
|
1231
1382
|
label="Bulleted list"
|
|
1232
1383
|
onClick={() => {
|
|
@@ -1237,6 +1388,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1237
1388
|
<ToolbarMenuButton
|
|
1238
1389
|
ariaLabel="Numbered list"
|
|
1239
1390
|
disabled={!props.canEdit || !props.onToggleNumberedList}
|
|
1391
|
+
disabledReason={props.editDisabledReason}
|
|
1240
1392
|
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1241
1393
|
label="Numbered list"
|
|
1242
1394
|
onClick={() => {
|
|
@@ -1257,6 +1409,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1257
1409
|
<ToolbarMenuButton
|
|
1258
1410
|
ariaLabel="Outdent"
|
|
1259
1411
|
disabled={!props.canEdit || !props.onOutdent}
|
|
1412
|
+
disabledReason={props.editDisabledReason}
|
|
1260
1413
|
icon={<Outdent className="h-3.5 w-3.5" />}
|
|
1261
1414
|
label="Outdent"
|
|
1262
1415
|
onClick={() => {
|
|
@@ -1267,6 +1420,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1267
1420
|
<ToolbarMenuButton
|
|
1268
1421
|
ariaLabel="Indent"
|
|
1269
1422
|
disabled={!props.canEdit || !props.onIndent}
|
|
1423
|
+
disabledReason={props.editDisabledReason}
|
|
1270
1424
|
icon={<Indent className="h-3.5 w-3.5" />}
|
|
1271
1425
|
label="Indent"
|
|
1272
1426
|
onClick={() => {
|
|
@@ -1281,6 +1435,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1281
1435
|
<ToolbarMenuButton
|
|
1282
1436
|
ariaLabel="Restart numbering"
|
|
1283
1437
|
disabled={!props.canEdit || !props.onRestartNumbering}
|
|
1438
|
+
disabledReason={props.editDisabledReason}
|
|
1284
1439
|
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1285
1440
|
label="Restart numbering"
|
|
1286
1441
|
onClick={() => {
|
|
@@ -1291,6 +1446,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1291
1446
|
<ToolbarMenuButton
|
|
1292
1447
|
ariaLabel="Continue numbering"
|
|
1293
1448
|
disabled={!props.canEdit || !props.onContinueNumbering}
|
|
1449
|
+
disabledReason={props.editDisabledReason}
|
|
1294
1450
|
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1295
1451
|
label="Continue numbering"
|
|
1296
1452
|
onClick={() => {
|
|
@@ -1311,6 +1467,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1311
1467
|
<ToolbarMenuButton
|
|
1312
1468
|
ariaLabel="Insert page break"
|
|
1313
1469
|
disabled={!props.canInsertStructural || !props.onInsertPageBreak}
|
|
1470
|
+
disabledReason={props.insertDisabledReason}
|
|
1314
1471
|
icon={<Minus className="h-3.5 w-3.5" />}
|
|
1315
1472
|
label="Page break"
|
|
1316
1473
|
onClick={() => {
|
|
@@ -1321,6 +1478,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1321
1478
|
<ToolbarMenuButton
|
|
1322
1479
|
ariaLabel="Insert table"
|
|
1323
1480
|
disabled={!props.canInsertStructural || !props.onInsertTable}
|
|
1481
|
+
disabledReason={props.insertDisabledReason}
|
|
1324
1482
|
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1325
1483
|
label="Table"
|
|
1326
1484
|
onClick={() => {
|
|
@@ -1329,6 +1487,16 @@ function ToolbarCompactOverflow(props: {
|
|
|
1329
1487
|
}}
|
|
1330
1488
|
/>
|
|
1331
1489
|
<label
|
|
1490
|
+
title={
|
|
1491
|
+
!props.canInsertStructural && props.insertDisabledReason
|
|
1492
|
+
? `Not available: ${props.insertDisabledReason}`
|
|
1493
|
+
: undefined
|
|
1494
|
+
}
|
|
1495
|
+
data-disabled-reason={
|
|
1496
|
+
!props.canInsertStructural && props.insertDisabledReason
|
|
1497
|
+
? props.insertDisabledReason
|
|
1498
|
+
: undefined
|
|
1499
|
+
}
|
|
1332
1500
|
className={`flex h-7 cursor-pointer items-center gap-2 rounded-md px-2 text-left text-[11px] font-medium text-primary transition-colors hover:bg-surface ${
|
|
1333
1501
|
!props.canInsertStructural || !props.onInsertImage ? "pointer-events-none opacity-40" : ""
|
|
1334
1502
|
}`}
|
|
@@ -1349,6 +1517,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1349
1517
|
<ToolbarMenuButton
|
|
1350
1518
|
ariaLabel="Insert next-page section break"
|
|
1351
1519
|
disabled={!props.canInsertStructural || !props.onInsertSectionBreak}
|
|
1520
|
+
disabledReason={props.insertDisabledReason}
|
|
1352
1521
|
icon={<FileText className="h-3.5 w-3.5" />}
|
|
1353
1522
|
label="Next-page section break"
|
|
1354
1523
|
onClick={() => {
|
|
@@ -1367,6 +1536,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1367
1536
|
<ToolbarMenuButton
|
|
1368
1537
|
ariaLabel="Refresh fields"
|
|
1369
1538
|
disabled={!props.canEdit || !props.onUpdateFields}
|
|
1539
|
+
disabledReason={props.editDisabledReason}
|
|
1370
1540
|
icon={<RotateCcw className="h-3.5 w-3.5" />}
|
|
1371
1541
|
label="Fields"
|
|
1372
1542
|
onClick={() => {
|
|
@@ -1377,6 +1547,7 @@ function ToolbarCompactOverflow(props: {
|
|
|
1377
1547
|
<ToolbarMenuButton
|
|
1378
1548
|
ariaLabel="Refresh table of contents"
|
|
1379
1549
|
disabled={!props.canEdit || !props.onUpdateTableOfContents}
|
|
1550
|
+
disabledReason={props.editDisabledReason}
|
|
1380
1551
|
icon={<RotateCcw className="h-3.5 w-3.5" />}
|
|
1381
1552
|
label="Table of contents"
|
|
1382
1553
|
onClick={() => {
|
|
@@ -1387,36 +1558,50 @@ function ToolbarCompactOverflow(props: {
|
|
|
1387
1558
|
</div>
|
|
1388
1559
|
) : null}
|
|
1389
1560
|
</div>
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
</div>
|
|
1561
|
+
</ToolbarPortalMenu>
|
|
1562
|
+
</Popover.Root>
|
|
1393
1563
|
);
|
|
1394
1564
|
}
|
|
1395
1565
|
|
|
1396
1566
|
function ToolbarFormattingOverflow(props: {
|
|
1397
1567
|
disabled: boolean;
|
|
1568
|
+
disabledReason?: string;
|
|
1398
1569
|
formattingState?: FormattingStateSnapshot;
|
|
1399
1570
|
onToggleStrikethrough?: () => void;
|
|
1400
1571
|
onToggleSuperscript?: () => void;
|
|
1401
1572
|
onToggleSubscript?: () => void;
|
|
1402
1573
|
}) {
|
|
1403
1574
|
const [open, setOpen] = React.useState(false);
|
|
1575
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
1576
|
+
const disabledTitle =
|
|
1577
|
+
props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined;
|
|
1404
1578
|
|
|
1405
1579
|
return (
|
|
1406
|
-
<
|
|
1580
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
1407
1581
|
<Tooltip.Root>
|
|
1408
1582
|
<Tooltip.Trigger asChild>
|
|
1409
|
-
<
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1583
|
+
<Popover.Anchor asChild>
|
|
1584
|
+
<button
|
|
1585
|
+
ref={triggerRef}
|
|
1586
|
+
type="button"
|
|
1587
|
+
aria-label="More text formatting"
|
|
1588
|
+
aria-expanded={open}
|
|
1589
|
+
aria-haspopup="menu"
|
|
1590
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
1591
|
+
disabled={props.disabled}
|
|
1592
|
+
title={disabledTitle}
|
|
1593
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1594
|
+
onClick={(event) => {
|
|
1595
|
+
event.preventDefault();
|
|
1596
|
+
setOpen((value) => !value);
|
|
1597
|
+
}}
|
|
1598
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
1599
|
+
open ? "bg-canvas text-accent ring-1 ring-accent/30 shadow-sm" : ""
|
|
1600
|
+
} ${focusRingClass}`}
|
|
1601
|
+
>
|
|
1602
|
+
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
1603
|
+
</button>
|
|
1604
|
+
</Popover.Anchor>
|
|
1420
1605
|
</Tooltip.Trigger>
|
|
1421
1606
|
<Tooltip.Portal>
|
|
1422
1607
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
@@ -1424,8 +1609,12 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1424
1609
|
</Tooltip.Content>
|
|
1425
1610
|
</Tooltip.Portal>
|
|
1426
1611
|
</Tooltip.Root>
|
|
1427
|
-
|
|
1428
|
-
|
|
1612
|
+
<ToolbarPortalMenu
|
|
1613
|
+
anchorRef={triggerRef}
|
|
1614
|
+
className="w-[220px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border"
|
|
1615
|
+
menuWidthPx={220}
|
|
1616
|
+
open={open}
|
|
1617
|
+
>
|
|
1429
1618
|
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
1430
1619
|
Text styling
|
|
1431
1620
|
</div>
|
|
@@ -1434,6 +1623,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1434
1623
|
active={props.formattingState?.strikethrough ?? false}
|
|
1435
1624
|
ariaLabel="Strikethrough"
|
|
1436
1625
|
disabled={props.disabled || !props.onToggleStrikethrough}
|
|
1626
|
+
disabledReason={props.disabledReason}
|
|
1437
1627
|
icon={<Strikethrough className="h-3.5 w-3.5" />}
|
|
1438
1628
|
onClick={() => {
|
|
1439
1629
|
props.onToggleStrikethrough?.();
|
|
@@ -1444,6 +1634,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1444
1634
|
active={props.formattingState?.superscript ?? false}
|
|
1445
1635
|
ariaLabel="Superscript"
|
|
1446
1636
|
disabled={props.disabled || !props.onToggleSuperscript}
|
|
1637
|
+
disabledReason={props.disabledReason}
|
|
1447
1638
|
icon={<Superscript className="h-3.5 w-3.5" />}
|
|
1448
1639
|
onClick={() => {
|
|
1449
1640
|
props.onToggleSuperscript?.();
|
|
@@ -1454,6 +1645,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1454
1645
|
active={props.formattingState?.subscript ?? false}
|
|
1455
1646
|
ariaLabel="Subscript"
|
|
1456
1647
|
disabled={props.disabled || !props.onToggleSubscript}
|
|
1648
|
+
disabledReason={props.disabledReason}
|
|
1457
1649
|
icon={<Subscript className="h-3.5 w-3.5" />}
|
|
1458
1650
|
onClick={() => {
|
|
1459
1651
|
props.onToggleSubscript?.();
|
|
@@ -1461,9 +1653,8 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1461
1653
|
}}
|
|
1462
1654
|
/>
|
|
1463
1655
|
</div>
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
</div>
|
|
1656
|
+
</ToolbarPortalMenu>
|
|
1657
|
+
</Popover.Root>
|
|
1467
1658
|
);
|
|
1468
1659
|
}
|
|
1469
1660
|
|
|
@@ -1471,27 +1662,44 @@ function ToolbarColorPopover(props: {
|
|
|
1471
1662
|
ariaLabel: string;
|
|
1472
1663
|
colors: ReadonlyArray<{ value: string | null; label: string }>;
|
|
1473
1664
|
disabled: boolean;
|
|
1665
|
+
disabledReason?: string;
|
|
1474
1666
|
icon: React.ReactNode;
|
|
1667
|
+
activeValue?: string | null;
|
|
1475
1668
|
title: string;
|
|
1476
1669
|
onSelect: (value: string | null) => void;
|
|
1477
1670
|
}) {
|
|
1478
1671
|
const [open, setOpen] = React.useState(false);
|
|
1672
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
1673
|
+
const activeValue = props.activeValue?.toLowerCase() ?? null;
|
|
1674
|
+
const disabledTitle =
|
|
1675
|
+
props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined;
|
|
1479
1676
|
|
|
1480
1677
|
return (
|
|
1481
|
-
<
|
|
1678
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
1482
1679
|
<Tooltip.Root>
|
|
1483
1680
|
<Tooltip.Trigger asChild>
|
|
1484
|
-
<
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1681
|
+
<Popover.Anchor asChild>
|
|
1682
|
+
<button
|
|
1683
|
+
ref={triggerRef}
|
|
1684
|
+
type="button"
|
|
1685
|
+
aria-label={props.ariaLabel}
|
|
1686
|
+
aria-expanded={open}
|
|
1687
|
+
aria-haspopup="menu"
|
|
1688
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
1689
|
+
disabled={props.disabled}
|
|
1690
|
+
title={disabledTitle}
|
|
1691
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1692
|
+
onClick={(event) => {
|
|
1693
|
+
event.preventDefault();
|
|
1694
|
+
setOpen((value) => !value);
|
|
1695
|
+
}}
|
|
1696
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
1697
|
+
open ? "bg-canvas text-accent ring-1 ring-accent/30 shadow-sm" : ""
|
|
1698
|
+
} ${focusRingClass}`}
|
|
1699
|
+
>
|
|
1700
|
+
{props.icon}
|
|
1701
|
+
</button>
|
|
1702
|
+
</Popover.Anchor>
|
|
1495
1703
|
</Tooltip.Trigger>
|
|
1496
1704
|
<Tooltip.Portal>
|
|
1497
1705
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
@@ -1499,44 +1707,59 @@ function ToolbarColorPopover(props: {
|
|
|
1499
1707
|
</Tooltip.Content>
|
|
1500
1708
|
</Tooltip.Portal>
|
|
1501
1709
|
</Tooltip.Root>
|
|
1502
|
-
|
|
1503
|
-
|
|
1710
|
+
<ToolbarPortalMenu
|
|
1711
|
+
anchorRef={triggerRef}
|
|
1712
|
+
className="w-[180px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border"
|
|
1713
|
+
menuWidthPx={180}
|
|
1714
|
+
open={open}
|
|
1715
|
+
>
|
|
1504
1716
|
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
1505
1717
|
{props.title}
|
|
1506
1718
|
</div>
|
|
1507
1719
|
<div className="grid grid-cols-3 gap-1">
|
|
1508
|
-
{props.colors.map((color) =>
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1720
|
+
{props.colors.map((color) => {
|
|
1721
|
+
const normalizedValue = color.value?.toLowerCase() ?? null;
|
|
1722
|
+
const isActive = normalizedValue === activeValue;
|
|
1723
|
+
return (
|
|
1724
|
+
<button
|
|
1725
|
+
key={`${props.ariaLabel}-${color.label}`}
|
|
1726
|
+
type="button"
|
|
1727
|
+
aria-label={`${props.title} ${color.label}`}
|
|
1728
|
+
aria-pressed={isActive}
|
|
1729
|
+
data-active={isActive ? "true" : undefined}
|
|
1730
|
+
disabled={props.disabled}
|
|
1731
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1732
|
+
onClick={() => {
|
|
1733
|
+
props.onSelect(color.value);
|
|
1734
|
+
setOpen(false);
|
|
1735
|
+
}}
|
|
1736
|
+
className={`inline-flex h-7 items-center justify-center rounded-md border text-[10px] font-medium text-primary transition-transform hover:scale-[1.04] disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
1737
|
+
isActive
|
|
1738
|
+
? "border-accent ring-2 ring-accent/35 shadow-sm"
|
|
1739
|
+
: "border-border"
|
|
1740
|
+
} ${color.value ? "" : "bg-surface"} ${focusRingClass}`}
|
|
1741
|
+
style={color.value ? { backgroundColor: color.value } : undefined}
|
|
1742
|
+
>
|
|
1743
|
+
{color.value ? <span className="sr-only">{color.label}</span> : "None"}
|
|
1744
|
+
</button>
|
|
1745
|
+
);
|
|
1746
|
+
})}
|
|
1527
1747
|
</div>
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
</div>
|
|
1748
|
+
</ToolbarPortalMenu>
|
|
1749
|
+
</Popover.Root>
|
|
1531
1750
|
);
|
|
1532
1751
|
}
|
|
1533
1752
|
|
|
1534
1753
|
function ToolbarAlignmentPopover(props: {
|
|
1535
1754
|
activeAlignment?: FormattingAlignment;
|
|
1536
1755
|
disabled: boolean;
|
|
1756
|
+
disabledReason?: string;
|
|
1537
1757
|
onSelect: (alignment: FormattingAlignment) => void;
|
|
1538
1758
|
}) {
|
|
1539
1759
|
const [open, setOpen] = React.useState(false);
|
|
1760
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
1761
|
+
const disabledTitle =
|
|
1762
|
+
props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined;
|
|
1540
1763
|
const alignments = [
|
|
1541
1764
|
{ value: "left" as const, label: "Align left", icon: <AlignLeft className="h-3.5 w-3.5" /> },
|
|
1542
1765
|
{ value: "center" as const, label: "Align center", icon: <AlignCenter className="h-3.5 w-3.5" /> },
|
|
@@ -1545,20 +1768,31 @@ function ToolbarAlignmentPopover(props: {
|
|
|
1545
1768
|
];
|
|
1546
1769
|
|
|
1547
1770
|
return (
|
|
1548
|
-
<
|
|
1771
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
1549
1772
|
<Tooltip.Root>
|
|
1550
1773
|
<Tooltip.Trigger asChild>
|
|
1551
|
-
<
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1774
|
+
<Popover.Anchor asChild>
|
|
1775
|
+
<button
|
|
1776
|
+
ref={triggerRef}
|
|
1777
|
+
type="button"
|
|
1778
|
+
aria-label="Paragraph alignment"
|
|
1779
|
+
aria-expanded={open}
|
|
1780
|
+
aria-haspopup="menu"
|
|
1781
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
1782
|
+
disabled={props.disabled}
|
|
1783
|
+
title={disabledTitle}
|
|
1784
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1785
|
+
onClick={(event) => {
|
|
1786
|
+
event.preventDefault();
|
|
1787
|
+
setOpen((value) => !value);
|
|
1788
|
+
}}
|
|
1789
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
1790
|
+
open ? "bg-canvas text-accent ring-1 ring-accent/30 shadow-sm" : ""
|
|
1791
|
+
} ${focusRingClass}`}
|
|
1792
|
+
>
|
|
1793
|
+
{(alignments.find((entry) => entry.value === props.activeAlignment) ?? alignments[0])?.icon}
|
|
1794
|
+
</button>
|
|
1795
|
+
</Popover.Anchor>
|
|
1562
1796
|
</Tooltip.Trigger>
|
|
1563
1797
|
<Tooltip.Portal>
|
|
1564
1798
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
@@ -1566,8 +1800,12 @@ function ToolbarAlignmentPopover(props: {
|
|
|
1566
1800
|
</Tooltip.Content>
|
|
1567
1801
|
</Tooltip.Portal>
|
|
1568
1802
|
</Tooltip.Root>
|
|
1569
|
-
|
|
1570
|
-
|
|
1803
|
+
<ToolbarPortalMenu
|
|
1804
|
+
anchorRef={triggerRef}
|
|
1805
|
+
className="w-[220px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border"
|
|
1806
|
+
menuWidthPx={220}
|
|
1807
|
+
open={open}
|
|
1808
|
+
>
|
|
1571
1809
|
<div className="mb-1 px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
1572
1810
|
Paragraph alignment
|
|
1573
1811
|
</div>
|
|
@@ -1578,6 +1816,7 @@ function ToolbarAlignmentPopover(props: {
|
|
|
1578
1816
|
active={props.activeAlignment === entry.value}
|
|
1579
1817
|
ariaLabel={entry.label}
|
|
1580
1818
|
disabled={props.disabled}
|
|
1819
|
+
disabledReason={props.disabledReason}
|
|
1581
1820
|
icon={entry.icon}
|
|
1582
1821
|
onClick={() => {
|
|
1583
1822
|
props.onSelect(entry.value);
|
|
@@ -1586,14 +1825,14 @@ function ToolbarAlignmentPopover(props: {
|
|
|
1586
1825
|
/>
|
|
1587
1826
|
))}
|
|
1588
1827
|
</div>
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
</div>
|
|
1828
|
+
</ToolbarPortalMenu>
|
|
1829
|
+
</Popover.Root>
|
|
1592
1830
|
);
|
|
1593
1831
|
}
|
|
1594
1832
|
|
|
1595
1833
|
function ToolbarInsertMenu(props: {
|
|
1596
1834
|
disabled: boolean;
|
|
1835
|
+
disabledReason?: string;
|
|
1597
1836
|
/**
|
|
1598
1837
|
* Lane 6b §6b.U3 — optional per-item disabled-explanation reasons.
|
|
1599
1838
|
* When the item is disabled (no handler or policy-gated), hovering it
|
|
@@ -1611,6 +1850,9 @@ function ToolbarInsertMenu(props: {
|
|
|
1611
1850
|
onInsertImage?: (options: InsertImageOptions) => void;
|
|
1612
1851
|
}) {
|
|
1613
1852
|
const [open, setOpen] = React.useState(false);
|
|
1853
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
1854
|
+
const disabledTitle =
|
|
1855
|
+
props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined;
|
|
1614
1856
|
|
|
1615
1857
|
async function handleImageChange(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
|
1616
1858
|
const file = event.target.files?.[0];
|
|
@@ -1629,21 +1871,32 @@ function ToolbarInsertMenu(props: {
|
|
|
1629
1871
|
}
|
|
1630
1872
|
|
|
1631
1873
|
return (
|
|
1632
|
-
<
|
|
1874
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
1633
1875
|
<Tooltip.Root>
|
|
1634
1876
|
<Tooltip.Trigger asChild>
|
|
1635
|
-
<
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1877
|
+
<Popover.Anchor asChild>
|
|
1878
|
+
<button
|
|
1879
|
+
ref={triggerRef}
|
|
1880
|
+
type="button"
|
|
1881
|
+
aria-label="Insert"
|
|
1882
|
+
aria-expanded={open}
|
|
1883
|
+
aria-haspopup="menu"
|
|
1884
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
1885
|
+
disabled={props.disabled}
|
|
1886
|
+
title={disabledTitle}
|
|
1887
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1888
|
+
onClick={(event) => {
|
|
1889
|
+
event.preventDefault();
|
|
1890
|
+
setOpen((value) => !value);
|
|
1891
|
+
}}
|
|
1892
|
+
className={`inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
1893
|
+
open ? "text-accent ring-1 ring-accent/30 shadow-sm" : ""
|
|
1894
|
+
} ${focusRingClass}`}
|
|
1895
|
+
>
|
|
1896
|
+
Insert
|
|
1897
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
1898
|
+
</button>
|
|
1899
|
+
</Popover.Anchor>
|
|
1647
1900
|
</Tooltip.Trigger>
|
|
1648
1901
|
<Tooltip.Portal>
|
|
1649
1902
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
@@ -1651,13 +1904,17 @@ function ToolbarInsertMenu(props: {
|
|
|
1651
1904
|
</Tooltip.Content>
|
|
1652
1905
|
</Tooltip.Portal>
|
|
1653
1906
|
</Tooltip.Root>
|
|
1654
|
-
|
|
1655
|
-
|
|
1907
|
+
<ToolbarPortalMenu
|
|
1908
|
+
anchorRef={triggerRef}
|
|
1909
|
+
className="w-[220px] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border"
|
|
1910
|
+
menuWidthPx={220}
|
|
1911
|
+
open={open}
|
|
1912
|
+
>
|
|
1656
1913
|
<div className="space-y-1">
|
|
1657
1914
|
<ToolbarMenuButton
|
|
1658
1915
|
ariaLabel="Insert page break"
|
|
1659
1916
|
disabled={props.disabled || !props.onInsertPageBreak}
|
|
1660
|
-
disabledReason={props.disabledReasons?.pageBreak}
|
|
1917
|
+
disabledReason={props.disabled ? props.disabledReason : props.disabledReasons?.pageBreak}
|
|
1661
1918
|
icon={<Minus className="h-3.5 w-3.5" />}
|
|
1662
1919
|
label="Page break"
|
|
1663
1920
|
onClick={() => {
|
|
@@ -1668,7 +1925,7 @@ function ToolbarInsertMenu(props: {
|
|
|
1668
1925
|
<ToolbarMenuButton
|
|
1669
1926
|
ariaLabel="Insert table"
|
|
1670
1927
|
disabled={props.disabled || !props.onInsertTable}
|
|
1671
|
-
disabledReason={props.disabledReasons?.table}
|
|
1928
|
+
disabledReason={props.disabled ? props.disabledReason : props.disabledReasons?.table}
|
|
1672
1929
|
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1673
1930
|
label="Table"
|
|
1674
1931
|
onClick={() => {
|
|
@@ -1677,6 +1934,21 @@ function ToolbarInsertMenu(props: {
|
|
|
1677
1934
|
}}
|
|
1678
1935
|
/>
|
|
1679
1936
|
<label
|
|
1937
|
+
aria-disabled={props.disabled || !props.onInsertImage ? "true" : undefined}
|
|
1938
|
+
title={
|
|
1939
|
+
props.disabled
|
|
1940
|
+
? disabledTitle
|
|
1941
|
+
: !props.onInsertImage && props.disabledReasons?.image
|
|
1942
|
+
? `Not available: ${props.disabledReasons.image}`
|
|
1943
|
+
: undefined
|
|
1944
|
+
}
|
|
1945
|
+
data-disabled-reason={
|
|
1946
|
+
props.disabled
|
|
1947
|
+
? props.disabledReason
|
|
1948
|
+
: !props.onInsertImage
|
|
1949
|
+
? props.disabledReasons?.image
|
|
1950
|
+
: undefined
|
|
1951
|
+
}
|
|
1680
1952
|
className={`flex h-7 cursor-pointer items-center gap-2 rounded-md px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface ${
|
|
1681
1953
|
props.disabled || !props.onInsertImage ? "pointer-events-none opacity-40" : ""
|
|
1682
1954
|
}`}
|
|
@@ -1697,7 +1969,7 @@ function ToolbarInsertMenu(props: {
|
|
|
1697
1969
|
<ToolbarMenuButton
|
|
1698
1970
|
ariaLabel="Insert next-page section break"
|
|
1699
1971
|
disabled={props.disabled || !props.onInsertSectionBreak}
|
|
1700
|
-
disabledReason={props.disabledReasons?.sectionBreak}
|
|
1972
|
+
disabledReason={props.disabled ? props.disabledReason : props.disabledReasons?.sectionBreak}
|
|
1701
1973
|
icon={<FileText className="h-3.5 w-3.5" />}
|
|
1702
1974
|
label="Next-page section break"
|
|
1703
1975
|
onClick={() => {
|
|
@@ -1706,9 +1978,8 @@ function ToolbarInsertMenu(props: {
|
|
|
1706
1978
|
}}
|
|
1707
1979
|
/>
|
|
1708
1980
|
</div>
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
</div>
|
|
1981
|
+
</ToolbarPortalMenu>
|
|
1982
|
+
</Popover.Root>
|
|
1712
1983
|
);
|
|
1713
1984
|
}
|
|
1714
1985
|
|
|
@@ -1716,15 +1987,24 @@ function ToolbarPopoverActionButton(props: {
|
|
|
1716
1987
|
active: boolean;
|
|
1717
1988
|
ariaLabel: string;
|
|
1718
1989
|
disabled: boolean;
|
|
1990
|
+
disabledReason?: string;
|
|
1719
1991
|
icon: React.ReactNode;
|
|
1720
1992
|
onClick?: () => void;
|
|
1721
1993
|
}) {
|
|
1994
|
+
const titleAttr =
|
|
1995
|
+
props.disabled && props.disabledReason
|
|
1996
|
+
? `Not available: ${props.disabledReason}`
|
|
1997
|
+
: undefined;
|
|
1722
1998
|
return (
|
|
1723
1999
|
<button
|
|
1724
2000
|
type="button"
|
|
1725
2001
|
aria-label={props.ariaLabel}
|
|
1726
2002
|
aria-pressed={props.active}
|
|
1727
2003
|
disabled={props.disabled}
|
|
2004
|
+
title={titleAttr}
|
|
2005
|
+
data-disabled-reason={
|
|
2006
|
+
props.disabled && props.disabledReason ? props.disabledReason : undefined
|
|
2007
|
+
}
|
|
1728
2008
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
1729
2009
|
onClick={props.onClick}
|
|
1730
2010
|
className={`inline-flex h-7 items-center justify-center rounded-md border border-border transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
|