ultisuite-client/components/drive/richtext/docs-format-menu.tsx
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

1082 lines
40 KiB
TypeScript

"use client"
import { useMemo, useState, type ReactNode } from "react"
import { Icon } from "@iconify/react"
import {
AlignCenter,
AlignJustify,
AlignLeft,
AlignRight,
Bold,
Check,
Italic,
List,
ListOrdered,
Strikethrough,
Underline,
} from "lucide-react"
import {
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar"
import {
DocsExclusiveMenuSub,
DocsExclusiveMenuSubRoot,
} from "@/components/drive/richtext/docs-exclusive-menu-sub"
import {
DocsFormatBulletPresets,
DocsFormatChecklistPresets,
DocsFormatColumnPresets,
DocsFormatNumberedPresets,
} from "@/components/drive/richtext/docs-format-menu-presets"
import { DocsCreateStyleDialog } from "@/components/drive/richtext/docs-paragraph-style-ui"
import { DocsLineSpacingDialog } from "@/components/drive/richtext/docs-line-spacing-dialog"
import { DocsListStartDialog } from "@/components/drive/richtext/docs-list-start-dialog"
import { DOCS_MENUBAR_CONTENT_PROPS } from "@/components/drive/richtext/docs-menubar-props"
import { DOCS_LINE_HEIGHT_PRESETS } from "@/lib/drive/docs-line-spacing"
import { buildParagraphStyleMenuEntries } from "@/lib/drive/docs-paragraph-styles"
import { useDocsParagraphStylesContext } from "@/lib/drive/docs-paragraph-styles-context"
import { docsModKeyLabel, isMacPlatform } from "@/lib/drive/docs-keyboard-shortcut"
import { DOCS_TABLE_BORDER_COLOR_PRESETS } from "@/lib/drive/docs-table-types"
import { cn } from "@/lib/utils"
export type DocsFormatMenuState = {
isBold: boolean
isItalic: boolean
isUnderline: boolean
isStrike: boolean
alignLeft: boolean
alignCenter: boolean
alignRight: boolean
alignJustify: boolean
isBulletList: boolean
isOrderedList: boolean
styleId: string
tableSelected: boolean
tableCanMerge: boolean
tableCanSplit: boolean
graphicSelected: boolean
imageSelected: boolean
lineHeightPresetId: string | "mixed" | null
hasSpaceBefore: boolean | "mixed"
hasSpaceAfter: boolean | "mixed"
keepWithNext: boolean | "mixed"
keepLinesTogether: boolean | "mixed"
preventWidowOrphan: boolean | "mixed"
pageBreakBefore: boolean | "mixed"
customSpacingDraft: import("@/lib/drive/docs-line-spacing").DocsParagraphSpacingAttrs
isTaskList: boolean
bulletStyleId: import("@/lib/drive/docs-list-styles").DocsBulletStyleId | null | "mixed"
orderedStyleId: import("@/lib/drive/docs-list-styles").DocsOrderedStyleId | null | "mixed"
checklistStyleId: import("@/lib/drive/docs-list-styles").DocsChecklistStyleId | null | "mixed"
orderedListStart: number | "mixed"
}
export type DocsFormatMenuActions = {
onBold: () => void
onItalic: () => void
onUnderline: () => void
onStrike: () => void
onAlignLeft: () => void
onAlignCenter: () => void
onAlignRight: () => void
onAlignJustify: () => void
onIncreaseIndent: () => void
onDecreaseIndent: () => void
onToggleBulletList: () => void
onToggleOrderedList: () => void
onApplyStyle: (styleId: string) => void
onClearFormatting: () => void
onPageSetup?: () => void
onTableAddRowBefore: () => void
onTableAddRowAfter: () => void
onTableAddColumnBefore: () => void
onTableAddColumnAfter: () => void
onTableDeleteRow: () => void
onTableDeleteColumn: () => void
onTableDelete: () => void
onTableMergeCells: () => void
onTableSplitCell: () => void
onTableToggleHeaderRow: () => void
onTableToggleHeaderColumn: () => void
onTableSetCellBackground: (color: string | null) => void
onTableAlignLeft: () => void
onTableAlignCenter: () => void
onTableAlignRight: () => void
onTableFixStructure: () => void
onTableSetCellBorders: (color: string) => void
onTableClearCellBorders: () => void
onTableSetRowHeight: (height: string | null) => void
onSetLineHeight: (lineHeight: number) => void
onToggleSpaceBefore: () => void
onToggleSpaceAfter: () => void
onApplyCustomSpacing: (input: {
lineHeight: number | null
spaceBeforePt: number
spaceAfterPt: number
}) => void
onToggleKeepWithNext: () => void
onToggleKeepLinesTogether: () => void
onTogglePreventWidowOrphan: () => void
onTogglePageBreakBefore: () => void
onApplyBulletStyle: (styleId: import("@/lib/drive/docs-list-styles").DocsBulletStyleId) => void
onApplyOrderedStyle: (styleId: import("@/lib/drive/docs-list-styles").DocsOrderedStyleId) => void
onApplyChecklistStyle: (styleId: import("@/lib/drive/docs-list-styles").DocsChecklistStyleId) => void
onRestartOrderedList: () => void
onContinueOrderedList: () => void
onSetOrderedListStart: (start: number) => void
}
const LINE_SPACING_OPTIONS = DOCS_LINE_HEIGHT_PRESETS
function MenuIcon({ children }: { children: ReactNode }) {
return <span className="docs-menu-item-icon">{children}</span>
}
function FormatShortcut({ children }: { children: ReactNode }) {
return <MenubarShortcut>{children}</MenubarShortcut>
}
function modShortcut(key: string, options?: { shift?: boolean; alt?: boolean }) {
const mod = docsModKeyLabel()
if (isMacPlatform()) {
const parts: string[] = []
if (options?.shift) parts.push("Maj")
if (options?.alt) parts.push("⌥")
if (parts.length > 0) return `${mod}+${parts.join("+")}+${key}`
return `${mod}${key}`
}
const parts: string[] = []
if (options?.shift) parts.push("Shift")
if (options?.alt) parts.push("Alt")
parts.push(mod, key)
return parts.join("+")
}
function CheckMenuOption({
checked,
onSelect,
children,
disabled,
className,
}: {
checked: boolean
onSelect: () => void
children: ReactNode
disabled?: boolean
className?: string
}) {
return (
<MenubarItem
className={cn("docs-menu-item relative pl-8", className)}
disabled={disabled}
onClick={onSelect}
>
{checked ? <Check className="absolute left-2 size-4" aria-hidden /> : null}
{children}
</MenubarItem>
)
}
function ParagraphStyleSubmenu({
styleId,
label,
disabled,
onApply,
onUpdate,
menuDisabled,
}: {
styleId: string
label: string
disabled?: boolean
onApply: (styleId: string) => void
onUpdate: (styleId: string) => void
menuDisabled?: boolean
}) {
return (
<DocsExclusiveMenuSub id={`style-${styleId}`}>
<MenubarSubTrigger className="docs-menu-item" disabled={disabled || menuDisabled}>
{label}
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[320px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem
className="docs-menu-item"
disabled={disabled || menuDisabled}
onClick={() => onApply(styleId)}
>
Appliquer le style {label}
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled || menuDisabled}
onClick={() => onUpdate(styleId)}
>
Mettre à jour &quot;{label}&quot; en fonction de la sélection/l&apos;emplacement du
signe d&apos;insertion
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
)
}
export function DocsFormatMenu({
actions,
state,
disabled,
}: {
actions: DocsFormatMenuActions
state: DocsFormatMenuState
disabled?: boolean
}) {
const paragraphStylesCtx = useDocsParagraphStylesContext()
const [createStyleOpen, setCreateStyleOpen] = useState(false)
const [customSpacingOpen, setCustomSpacingOpen] = useState(false)
const [listStartOpen, setListStartOpen] = useState(false)
const styleEntries = useMemo(() => {
if (!paragraphStylesCtx) return []
return buildParagraphStyleMenuEntries(
paragraphStylesCtx.state.documentStyles,
paragraphStylesCtx.state.userStyles
)
}, [paragraphStylesCtx])
const objectFormattingDisabled =
disabled || (!state.tableSelected && !state.graphicSelected && !state.imageSelected)
return (
<MenubarMenu>
<MenubarTrigger className="docs-menu-trigger">Format</MenubarTrigger>
<MenubarContent
{...DOCS_MENUBAR_CONTENT_PROPS}
className="docs-menu-content min-w-[320px] overflow-visible"
data-docs-menu-surface
>
<DocsExclusiveMenuSubRoot>
<DocsExclusiveMenuSub id="text">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<Bold className="size-4" />
</MenuIcon>
Texte
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[280px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem className="docs-menu-item" disabled={disabled} onClick={actions.onBold}>
<MenuIcon>
<Bold className="size-4" />
</MenuIcon>
Gras
<FormatShortcut>{modShortcut("B")}</FormatShortcut>
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={actions.onItalic}
>
<MenuIcon>
<Italic className="size-4" />
</MenuIcon>
Italique
<FormatShortcut>{modShortcut("I")}</FormatShortcut>
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={actions.onUnderline}
>
<MenuIcon>
<Underline className="size-4" />
</MenuIcon>
Souligner
<FormatShortcut>{modShortcut("U")}</FormatShortcut>
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={actions.onStrike}
>
<MenuIcon>
<Strikethrough className="size-4" />
</MenuIcon>
Barrer
<FormatShortcut>{modShortcut("X", { shift: true })}</FormatShortcut>
</MenubarItem>
<MenubarItem className="docs-menu-item" disabled>
<MenuIcon>
<span className="text-[11px] font-semibold tracking-tight">Sc</span>
</MenuIcon>
Petites majuscules
<FormatShortcut>{isMacPlatform() ? "⌥+Maj+K" : "Alt+Shift+K"}</FormatShortcut>
</MenubarItem>
<MenubarItem className="docs-menu-item" disabled>
<MenuIcon>
<span className="text-xs leading-none">
X<sup className="text-[9px]">2</sup>
</span>
</MenuIcon>
Exposant
<FormatShortcut>{modShortcut(".")}</FormatShortcut>
</MenubarItem>
<MenubarItem className="docs-menu-item" disabled>
<MenuIcon>
<span className="text-xs leading-none">
X<sub className="text-[9px]">2</sub>
</span>
</MenuIcon>
Indice
<FormatShortcut>{modShortcut(",")}</FormatShortcut>
</MenubarItem>
<MenubarSeparator />
<DocsExclusiveMenuSub id="text-size">
<MenubarSubTrigger className="docs-menu-item" disabled>
Taille
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[200px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem disabled className="docs-menu-item text-muted-foreground">
Bientôt disponible
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="text-case">
<MenubarSubTrigger className="docs-menu-item" disabled>
Minuscules/majuscules
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[240px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem disabled className="docs-menu-item text-muted-foreground">
Bientôt disponible
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="paragraph-styles">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<Icon icon="material-symbols:format-paragraph" className="size-4" />
</MenuIcon>
Styles de paragraphe
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[240px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem className="docs-menu-item" disabled>
Bordures et ombrage
</MenubarItem>
<MenubarSeparator />
{styleEntries
.filter((entry) => entry.section === "document")
.map(({ definition }) => (
<ParagraphStyleSubmenu
key={definition.id}
styleId={definition.id}
label={definition.name}
menuDisabled={disabled}
onApply={actions.onApplyStyle}
onUpdate={
paragraphStylesCtx?.updateStyleFromSelection ??
(() => undefined)
}
/>
))}
{styleEntries.some((entry) => entry.section === "user") ? (
<>
<MenubarSeparator />
<div className="px-3 py-1.5 text-xs font-medium text-[#5f6368] dark:text-muted-foreground">
Mes styles
</div>
{styleEntries
.filter((entry) => entry.section === "user")
.map(({ definition }) => (
<ParagraphStyleSubmenu
key={`user-${definition.id}`}
styleId={definition.id}
label={definition.name}
menuDisabled={disabled}
onApply={actions.onApplyStyle}
onUpdate={
paragraphStylesCtx?.updateStyleFromSelection ??
(() => undefined)
}
/>
))}
</>
) : null}
<MenubarSeparator />
<DocsExclusiveMenuSub id="paragraph-style-options">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
Options
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[220px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={() => setCreateStyleOpen(true)}
>
Créer un style
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="align-indent">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<AlignLeft className="size-4" />
</MenuIcon>
Aligner et mettre en retrait
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[300px] overflow-visible"
data-docs-menu-surface
>
<CheckMenuOption
checked={state.alignLeft}
disabled={disabled}
onSelect={actions.onAlignLeft}
>
<MenuIcon>
<AlignLeft className="size-4" />
</MenuIcon>
À gauche
<FormatShortcut>{modShortcut("L", { shift: true })}</FormatShortcut>
</CheckMenuOption>
<CheckMenuOption
checked={state.alignCenter}
disabled={disabled}
onSelect={actions.onAlignCenter}
>
<MenuIcon>
<AlignCenter className="size-4" />
</MenuIcon>
Centrer
<FormatShortcut>{modShortcut("E", { shift: true })}</FormatShortcut>
</CheckMenuOption>
<CheckMenuOption
checked={state.alignRight}
disabled={disabled}
onSelect={actions.onAlignRight}
>
<MenuIcon>
<AlignRight className="size-4" />
</MenuIcon>
À droite
<FormatShortcut>{modShortcut("R", { shift: true })}</FormatShortcut>
</CheckMenuOption>
<CheckMenuOption
checked={state.alignJustify}
disabled={disabled}
onSelect={actions.onAlignJustify}
>
<MenuIcon>
<AlignJustify className="size-4" />
</MenuIcon>
Justifié
<FormatShortcut>{modShortcut("J", { shift: true })}</FormatShortcut>
</CheckMenuOption>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={actions.onIncreaseIndent}
>
<MenuIcon>
<Icon icon="material-symbols:format-indent-increase" className="size-4" />
</MenuIcon>
Augmenter le retrait
<FormatShortcut>{modShortcut("]")}</FormatShortcut>
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={actions.onDecreaseIndent}
>
<MenuIcon>
<Icon icon="material-symbols:format-indent-decrease" className="size-4" />
</MenuIcon>
Diminuer le retrait
<FormatShortcut>{modShortcut("[")}</FormatShortcut>
</MenubarItem>
<MenubarItem className="docs-menu-item" disabled>
Options de mise en retrait
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="line-spacing">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<Icon icon="material-symbols:format-line-spacing" className="size-4" />
</MenuIcon>
Interligne et espace entre paragraphes
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[320px] overflow-visible"
data-docs-menu-surface
>
{LINE_SPACING_OPTIONS.map((option) => (
<CheckMenuOption
key={option.id}
checked={
state.lineHeightPresetId !== "mixed" && state.lineHeightPresetId === option.id
}
disabled={disabled}
onSelect={() => actions.onSetLineHeight(option.value)}
>
{option.label}
</CheckMenuOption>
))}
<MenubarSeparator />
<CheckMenuOption
checked={state.hasSpaceBefore === true}
disabled={disabled}
onSelect={actions.onToggleSpaceBefore}
>
Insérer un espacement avant le paragraphe
</CheckMenuOption>
<CheckMenuOption
checked={state.hasSpaceAfter === true}
disabled={disabled}
onSelect={actions.onToggleSpaceAfter}
>
Insérer un espacement après le paragraphe
</CheckMenuOption>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={() => setCustomSpacingOpen(true)}
>
Espacement personnalisé
</MenubarItem>
<MenubarSeparator />
<CheckMenuOption
checked={state.keepWithNext === true}
disabled={disabled}
onSelect={actions.onToggleKeepWithNext}
>
Rattacher au paragraphe suivant
</CheckMenuOption>
<CheckMenuOption
checked={state.keepLinesTogether === true}
disabled={disabled}
onSelect={actions.onToggleKeepLinesTogether}
>
Ne pas laisser de ligne isolée
</CheckMenuOption>
<CheckMenuOption
checked={state.preventWidowOrphan === true}
disabled={disabled}
onSelect={actions.onTogglePreventWidowOrphan}
>
Empêcher les lignes isolées
</CheckMenuOption>
<CheckMenuOption
checked={state.pageBreakBefore === true}
disabled={disabled}
onSelect={actions.onTogglePageBreakBefore}
>
Ajouter un saut de page avant
</CheckMenuOption>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="columns">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<Icon icon="material-symbols:view-column" className="size-4" />
</MenuIcon>
Colonnes
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[220px] overflow-visible p-2"
data-docs-menu-surface
>
<DocsFormatColumnPresets disabled />
<MenubarSeparator className="my-2" />
<MenubarItem className="docs-menu-item" disabled>
Autres options
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="lists">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<List className="size-4" />
</MenuIcon>
Puces et numéros
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[240px] overflow-visible"
data-docs-menu-surface
>
<DocsExclusiveMenuSub id="list-options">
<MenubarSubTrigger
className="docs-menu-item"
disabled={disabled || (!state.isOrderedList && !state.isBulletList && !state.isTaskList)}
>
Options de liste
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[280px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem
className="docs-menu-item"
disabled={disabled || !state.isOrderedList}
onClick={actions.onRestartOrderedList}
>
Recommencer la numérotation
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled || !state.isOrderedList}
onClick={actions.onContinueOrderedList}
>
Continuer la numérotation précédente
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled || !state.isOrderedList}
onClick={() => setListStartOpen(true)}
>
Modifier le numéro de départ
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="ordered-list">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<ListOrdered className="size-4" />
</MenuIcon>
Liste numérotée
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content overflow-visible p-2"
data-docs-menu-surface
>
<DocsFormatNumberedPresets
disabled={disabled}
activeStyleId={state.isOrderedList ? state.orderedStyleId : null}
onSelect={actions.onApplyOrderedStyle}
/>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="bullet-list">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<List className="size-4" />
</MenuIcon>
Liste à puces
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content overflow-visible p-2"
data-docs-menu-surface
>
<DocsFormatBulletPresets
disabled={disabled}
activeStyleId={state.isBulletList ? state.bulletStyleId : null}
onSelect={actions.onApplyBulletStyle}
/>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="checklist">
<MenubarSubTrigger className="docs-menu-item" disabled={disabled}>
<MenuIcon>
<Icon icon="material-symbols:checklist" className="size-4" />
</MenuIcon>
Menu de type checklist
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content overflow-visible p-2"
data-docs-menu-surface
>
<DocsFormatChecklistPresets
disabled={disabled}
activeStyleId={state.isTaskList ? state.checklistStyleId : null}
onSelect={actions.onApplyChecklistStyle}
/>
</MenubarSubContent>
</DocsExclusiveMenuSub>
</MenubarSubContent>
</DocsExclusiveMenuSub>
</DocsExclusiveMenuSubRoot>
<MenubarSeparator />
<MenubarItem className="docs-menu-item" disabled>
<MenuIcon>
<Icon icon="material-symbols:page-header" className="size-4" />
</MenuIcon>
En-têtes et pieds de page
</MenubarItem>
<MenubarItem className="docs-menu-item" disabled>
<MenuIcon>
<Icon icon="material-symbols:counter-1" className="size-4" />
</MenuIcon>
Numéros de page
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled || !actions.onPageSetup}
onClick={actions.onPageSetup}
>
<MenuIcon>
<Icon icon="material-symbols:screen-rotation" className="size-4" />
</MenuIcon>
Orientation de la page
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={disabled || !actions.onPageSetup}
onClick={actions.onPageSetup}
>
<MenuIcon>
<Icon icon="material-symbols:select" className="size-4" />
</MenuIcon>
Passer au format Sans pages
</MenubarItem>
<MenubarSeparator />
<DocsExclusiveMenuSub id="table-format">
<MenubarSubTrigger
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
>
<MenuIcon>
<Icon icon="material-symbols:table" className="size-4" />
</MenuIcon>
Tableau
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[260px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAddRowBefore}
>
Insérer une ligne au-dessus
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAddRowAfter}
>
Insérer une ligne en dessous
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAddColumnBefore}
>
Insérer une colonne à gauche
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAddColumnAfter}
>
Insérer une colonne à droite
</MenubarItem>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected || !state.tableCanMerge}
onClick={actions.onTableMergeCells}
>
Fusionner les cellules
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected || !state.tableCanSplit}
onClick={actions.onTableSplitCell}
>
Scinder la cellule
</MenubarItem>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableToggleHeaderRow}
>
Ligne d&apos;en-tête
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableToggleHeaderColumn}
>
Colonne d&apos;en-tête
</MenubarItem>
<MenubarSeparator />
<DocsExclusiveMenuSub id="table-cell-color">
<MenubarSubTrigger
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
>
Couleur de cellule
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[200px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetCellBackground(null)}
>
Aucune
</MenubarItem>
{[
{ label: "Gris clair", color: "#f3f3f3" },
{ label: "Bleu clair", color: "#c9daf8" },
{ label: "Vert clair", color: "#d9ead3" },
{ label: "Jaune clair", color: "#fff2cc" },
].map((preset) => (
<MenubarItem
key={preset.color}
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetCellBackground(preset.color)}
>
{preset.label}
</MenubarItem>
))}
</MenubarSubContent>
</DocsExclusiveMenuSub>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAlignLeft}
>
Aligner le tableau à gauche
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAlignCenter}
>
Centrer le tableau
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableAlignRight}
>
Aligner le tableau à droite
</MenubarItem>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableDeleteRow}
>
Supprimer la ligne
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableDeleteColumn}
>
Supprimer la colonne
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableDelete}
>
Supprimer le tableau
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableFixStructure}
>
Réparer la structure
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="image-format">
<MenubarSubTrigger
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.imageSelected}
>
<MenuIcon>
<Icon icon="material-symbols:image-outline" className="size-4" />
</MenuIcon>
Image
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[220px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem disabled className="docs-menu-item text-muted-foreground">
Bientôt disponible
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<DocsExclusiveMenuSub id="borders-lines">
<MenubarSubTrigger
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
>
<MenuIcon>
<Icon icon="material-symbols:horizontal-rule" className="size-4" />
</MenuIcon>
Bordures et lignes
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[220px] overflow-visible"
data-docs-menu-surface
>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetCellBorders("#000000")}
>
Bordures sur toutes les cellules
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={actions.onTableClearCellBorders}
>
Supprimer les bordures
</MenubarItem>
<MenubarSeparator />
<DocsExclusiveMenuSub id="table-border-color">
<MenubarSubTrigger
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
>
Couleur des bordures
</MenubarSubTrigger>
<MenubarSubContent
className="docs-menu-content docs-menu-sub-content min-w-[180px] overflow-visible"
data-docs-menu-surface
>
{DOCS_TABLE_BORDER_COLOR_PRESETS.map((color) => (
<MenubarItem
key={color}
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetCellBorders(color)}
>
<span
className="mr-2 inline-block size-4 rounded-sm border border-border"
style={{ backgroundColor: color }}
aria-hidden
/>
{color}
</MenubarItem>
))}
</MenubarSubContent>
</DocsExclusiveMenuSub>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetRowHeight("32px")}
>
Hauteur de ligne compacte
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetRowHeight("48px")}
>
Hauteur de ligne normale
</MenubarItem>
<MenubarItem
className="docs-menu-item"
disabled={objectFormattingDisabled || !state.tableSelected}
onClick={() => actions.onTableSetRowHeight(null)}
>
Hauteur de ligne automatique
</MenubarItem>
</MenubarSubContent>
</DocsExclusiveMenuSub>
<MenubarSeparator />
<MenubarItem
className="docs-menu-item"
disabled={disabled}
onClick={actions.onClearFormatting}
>
<MenuIcon>
<Icon icon="material-symbols:format-clear" className="size-4" />
</MenuIcon>
Supprimer la mise en forme
<FormatShortcut>{modShortcut("\\")}</FormatShortcut>
</MenubarItem>
</MenubarContent>
<DocsCreateStyleDialog
open={createStyleOpen}
onOpenChange={setCreateStyleOpen}
onCreate={(input) => paragraphStylesCtx?.createUserStyle(input)}
/>
<DocsLineSpacingDialog
open={customSpacingOpen}
onOpenChange={setCustomSpacingOpen}
initial={state.customSpacingDraft}
onApply={actions.onApplyCustomSpacing}
/>
<DocsListStartDialog
open={listStartOpen}
onOpenChange={setListStartOpen}
initialStart={state.orderedListStart === "mixed" ? 1 : state.orderedListStart}
onApply={actions.onSetOrderedListStart}
/>
</MenubarMenu>
)
}