ultisuite-client/components/gmail/compose/compose-toolbar.tsx
2026-05-20 16:01:08 +02:00

995 lines
31 KiB
TypeScript

"use client"
import {
useState,
useCallback,
lazy,
Suspense,
} from "react"
import { useEditor, type Editor } from "@tiptap/react"
import {
ChevronDown,
Paperclip,
Link as LinkIcon,
Smile,
HardDrive,
Image as ImageIcon,
Lock,
PenTool,
MoreVertical,
Trash2,
Bold,
Italic,
Underline as UnderlineIcon,
AlignLeft,
AlignCenter,
AlignRight,
AlignJustify,
List,
ListOrdered,
Undo,
Redo,
Type,
Clock,
Indent,
Outdent,
RemoveFormatting,
Palette,
ALargeSmall,
CaseSensitive,
Send,
} from "lucide-react"
import {
type ComposeState,
SIGNATURES,
useComposeActions,
} from "@/lib/compose-context"
import { cn, getNextLocalWallClockDate } from "@/lib/utils"
import {
MAIL_COMPOSE_BOTTOM_ICON_BTN,
MAIL_COMPOSE_BOTTOM_ICON_BTN_ACTIVE,
MAIL_COMPOSE_MENU_SELECTED_CLASS,
MAIL_COMPOSE_POPOVER_CLASS,
MAIL_COMPOSE_PRIMARY_SEND_BTN,
MAIL_COMPOSE_TOOLBAR_BTN,
MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE,
MAIL_COMPOSE_TOOLBAR_SEP,
MAIL_MENU_SURFACE_CLASS,
} from "@/lib/mail-chrome-classes"
import { useTheme } from "next-themes"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import data from "@emoji-mart/data"
import { COMPOSE_PORTAL_Z, insertSignatureHtml } from "./compose-shared"
const LazyPicker = lazy(() => import("@emoji-mart/react"))
function ComposeEmojiPicker({ onSelect }: { onSelect: (emoji: { native: string }) => void }) {
const { resolvedTheme } = useTheme()
return (
<Suspense fallback={<div className="flex h-[435px] w-[352px] items-center justify-center text-sm text-muted-foreground">Chargement</div>}>
<LazyPicker
data={data}
onEmojiSelect={onSelect}
locale="fr"
theme={resolvedTheme === "dark" ? "dark" : "light"}
previewPosition="none"
skinTonePosition="search"
set="native"
/>
</Suspense>
)
}
function AlignmentDropdown({
editor,
btnClass,
activeClass,
}: {
editor: NonNullable<ReturnType<typeof useEditor>>
btnClass: string
activeClass: string
}) {
const currentIcon = editor.isActive({ textAlign: "center" })
? AlignCenter
: editor.isActive({ textAlign: "right" })
? AlignRight
: editor.isActive({ textAlign: "justify" })
? AlignJustify
: AlignLeft
const CurrentIcon = currentIcon
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(btnClass, "w-auto gap-0.5 px-1")}
title="Alignement"
>
<CurrentIcon className="h-4 w-4" />
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("min-w-[160px]", COMPOSE_PORTAL_Z)}
>
<DropdownMenuItem
onSelect={() => editor.chain().focus().setTextAlign("left").run()}
className={cn(editor.isActive({ textAlign: "left" }) && MAIL_COMPOSE_MENU_SELECTED_CLASS)}
>
<AlignLeft className="h-4 w-4" /> Aligner à gauche
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => editor.chain().focus().setTextAlign("center").run()}
className={cn(editor.isActive({ textAlign: "center" }) && MAIL_COMPOSE_MENU_SELECTED_CLASS)}
>
<AlignCenter className="h-4 w-4" /> Centrer
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => editor.chain().focus().setTextAlign("right").run()}
className={cn(editor.isActive({ textAlign: "right" }) && MAIL_COMPOSE_MENU_SELECTED_CLASS)}
>
<AlignRight className="h-4 w-4" /> Aligner à droite
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => editor.chain().focus().setTextAlign("justify").run()}
className={cn(editor.isActive({ textAlign: "justify" }) && MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE)}
>
<AlignJustify className="h-4 w-4" /> Justifier
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
const FONT_FAMILIES = [
{ label: "Sans Serif", value: "sans-serif" },
{ label: "Serif", value: "serif" },
{ label: "Monospace", value: "monospace" },
{ label: "Cursive", value: "cursive" },
{ label: "Comic Sans MS", value: "Comic Sans MS, cursive" },
{ label: "Garamond", value: "Garamond, serif" },
{ label: "Georgia", value: "Georgia, serif" },
{ label: "Impact", value: "Impact, sans-serif" },
{ label: "Tahoma", value: "Tahoma, sans-serif" },
{ label: "Trebuchet MS", value: "Trebuchet MS, sans-serif" },
{ label: "Verdana", value: "Verdana, sans-serif" },
]
const FONT_SIZES = [
{ label: "Très petit", value: "10px" },
{ label: "Petit", value: "13px" },
{ label: "Normal", value: "" },
{ label: "Grand", value: "18px" },
{ label: "Très grand", value: "24px" },
{ label: "Énorme", value: "32px" },
]
const TEXT_COLORS = [
"#000000", "#434343", "#666666", "#999999", "#cccccc", "#efefef", "#f3f3f3", "#ffffff",
"#fb4934", "#fe8019", "#fabd2f", "#b8bb26", "#8ec07c", "#83a598", "#d3869b", "#ebdbb2",
"#cc241d", "#d65d0e", "#d79921", "#98971a", "#689d6a", "#458588", "#b16286", "#a89984",
"#9d0006", "#af3a03", "#b57614", "#79740e", "#427b58", "#076678", "#8f3f71", "#7c6f64",
]
function FontDropdown({
editor,
btnClass,
}: {
editor: NonNullable<ReturnType<typeof useEditor>>
btnClass: string
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button type="button" className={cn(btnClass, "w-auto gap-0.5 px-1")} title="Police">
<CaseSensitive className="h-4 w-4" />
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("max-h-[280px] min-w-[180px] overflow-y-auto", COMPOSE_PORTAL_Z)}
>
{FONT_FAMILIES.map((f) => (
<DropdownMenuItem
key={f.value}
onSelect={() => editor.chain().focus().setMark("textStyle", { fontFamily: f.value }).run()}
style={{ fontFamily: f.value }}
className={cn(
editor.isActive("textStyle", { fontFamily: f.value }) && MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE
)}
>
{f.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
function FontSizeDropdown({
editor,
btnClass,
}: {
editor: NonNullable<ReturnType<typeof useEditor>>
btnClass: string
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button type="button" className={cn(btnClass, "w-auto gap-0.5 px-1")} title="Taille du texte">
<ALargeSmall className="h-4 w-4" />
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("min-w-[140px]", COMPOSE_PORTAL_Z)}
>
{FONT_SIZES.map((s) => (
<DropdownMenuItem
key={s.label}
onSelect={() => {
if (s.value) {
editor.chain().focus().setMark("textStyle", { fontSize: s.value }).run()
} else {
editor.chain().focus().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run()
}
}}
style={s.value ? { fontSize: s.value } : undefined}
className={cn(
s.value && editor.isActive("textStyle", { fontSize: s.value }) && MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE
)}
>
{s.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
function ColorDropdown({
editor,
btnClass,
}: {
editor: NonNullable<ReturnType<typeof useEditor>>
btnClass: string
}) {
const [tab, setTab] = useState<"text" | "bg">("text")
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button type="button" className={cn(btnClass, "w-auto gap-0.5 px-1")} title="Couleur du texte">
<Palette className="h-4 w-4" />
<ChevronDown className="h-3 w-3" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={cn("w-[268px] p-2", COMPOSE_PORTAL_Z)}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="mb-2 flex gap-1 border-b border-border pb-2">
<button
type="button"
className={cn(
"flex-1 rounded px-2 py-1 text-xs font-medium transition-colors",
tab === "text" ? MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE : "text-[#5f6368] hover:bg-[#f1f3f4]"
)}
onClick={() => setTab("text")}
>
Couleur du texte
</button>
<button
type="button"
className={cn(
"flex-1 rounded px-2 py-1 text-xs font-medium transition-colors",
tab === "bg" ? MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE : "text-[#5f6368] hover:bg-[#f1f3f4]"
)}
onClick={() => setTab("bg")}
>
Couleur de fond
</button>
</div>
<div className="grid grid-cols-8 gap-1">
{TEXT_COLORS.map((color) => (
<button
key={`${tab}-${color}`}
type="button"
className="h-6 w-6 rounded border border-border hover:scale-110 transition-transform"
style={{ backgroundColor: color }}
title={color}
onClick={() => {
if (tab === "text") {
editor.chain().focus().setColor(color).run()
} else {
editor.chain().focus().setMark("textStyle", { backgroundColor: color }).run()
}
}}
/>
))}
</div>
<button
type="button"
className="mt-2 w-full rounded px-2 py-1 text-xs text-muted-foreground hover:bg-accent transition-colors"
onClick={() => {
if (tab === "text") {
editor.chain().focus().unsetColor().run()
} else {
editor.chain().focus().setMark("textStyle", { backgroundColor: null }).removeEmptyTextStyle().run()
}
}}
>
Réinitialiser
</button>
</DropdownMenuContent>
</DropdownMenu>
)
}
export function FormattingToolbar({
editor,
}: {
editor: Editor | null
}) {
if (!editor) return null
const btnClass = MAIL_COMPOSE_TOOLBAR_BTN
const activeClass = MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE
const sep = <span className={MAIL_COMPOSE_TOOLBAR_SEP} aria-hidden />
return (
<div className="compose-toolbar flex flex-wrap items-center border-t border-border bg-muted px-1 py-1">
{/* Undo / Redo */}
<button
type="button"
className={btnClass}
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
title="Annuler"
>
<Undo className="h-4 w-4" />
</button>
<button
type="button"
className={btnClass}
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
title="Rétablir"
>
<Redo className="h-4 w-4" />
</button>
{sep}
{/* Font */}
<FontDropdown editor={editor} btnClass={btnClass} />
{sep}
{/* Font size */}
<FontSizeDropdown editor={editor} btnClass={btnClass} />
{sep}
{/* Bold, Italic, Underline, Colors */}
<button
type="button"
className={cn(btnClass, editor.isActive("bold") && activeClass)}
onClick={() => editor.chain().focus().toggleMark("bold").run()}
title="Gras"
>
<Bold className="h-4 w-4" />
</button>
<button
type="button"
className={cn(btnClass, editor.isActive("italic") && activeClass)}
onClick={() => editor.chain().focus().toggleMark("italic").run()}
title="Italique"
>
<Italic className="h-4 w-4" />
</button>
<button
type="button"
className={cn(btnClass, editor.isActive("underline") && activeClass)}
onClick={() => editor.chain().focus().toggleUnderline().run()}
title="Souligné"
>
<UnderlineIcon className="h-4 w-4" />
</button>
<ColorDropdown editor={editor} btnClass={btnClass} />
{sep}
{/* Alignment dropdown, lists, indent/outdent, remove formatting */}
<AlignmentDropdown editor={editor} btnClass={btnClass} activeClass={activeClass} />
<button
type="button"
className={cn(btnClass, editor.isActive("orderedList") && activeClass)}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
title="Liste numérotée"
>
<ListOrdered className="h-4 w-4" />
</button>
<button
type="button"
className={cn(btnClass, editor.isActive("bulletList") && activeClass)}
onClick={() => editor.chain().focus().toggleBulletList().run()}
title="Liste à puces"
>
<List className="h-4 w-4" />
</button>
<button
type="button"
className={btnClass}
onClick={() => {
try { editor.chain().focus().liftListItem("listItem").run() } catch { /* not in list */ }
}}
title="Désindenter"
>
<Outdent className="h-4 w-4" />
</button>
<button
type="button"
className={btnClass}
onClick={() => {
try { editor.chain().focus().sinkListItem("listItem").run() } catch { /* not in list */ }
}}
title="Indenter"
>
<Indent className="h-4 w-4" />
</button>
<button
type="button"
className={btnClass}
onClick={() => editor.chain().focus().clearNodes().unsetAllMarks().run()}
title="Supprimer la mise en forme"
>
<RemoveFormatting className="h-4 w-4" />
</button>
</div>
)
}
function ComposeEmojiButton({
editor,
}: {
editor: Editor | null
}) {
const [open, setOpen] = useState(false)
const handleSelect = useCallback(
(emoji: { native: string }) => {
editor?.chain().focus().insertContent(emoji.native).run()
setOpen(false)
},
[editor]
)
if (!editor) return null
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Insérer un emoji"
>
<Smile className="h-[18px] w-[18px]" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
side="top"
className={cn("w-auto border-0 bg-popover p-0 shadow-xl", COMPOSE_PORTAL_Z)}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<ComposeEmojiPicker onSelect={handleSelect} />
</PopoverContent>
</Popover>
)
}
function ComposeLinkButton({
editor,
}: {
editor: Editor | null
}) {
const [open, setOpen] = useState(false)
const [url, setUrl] = useState("")
const [text, setText] = useState("")
if (!editor) return null
const isLinkActive = editor.isActive("link")
const handleToggle = () => {
if (isLinkActive) {
editor.chain().focus().extendMarkRange("link").unsetLink().run()
return
}
setOpen(true)
}
const handleOpen = (isOpen: boolean) => {
if (isOpen) {
const { from, to, empty } = editor.state.selection
if (isLinkActive) {
const attrs = editor.getAttributes("link")
setUrl(attrs.href || "")
const selectedText = editor.state.doc.textBetween(from, to, " ")
setText(selectedText)
} else if (!empty) {
const selectedText = editor.state.doc.textBetween(from, to, " ")
setText(selectedText)
setUrl("")
} else {
setText("")
setUrl("")
}
}
setOpen(isOpen)
}
const handleInsert = () => {
if (!url.trim()) return
const href = url.match(/^https?:\/\//) ? url : `https://${url}`
const { empty } = editor.state.selection
if (empty && !isLinkActive) {
const displayText = text.trim() || href
editor
.chain()
.focus()
.insertContent(`<a href="${href}">${displayText}</a>`)
.run()
} else {
if (text.trim() && text.trim() !== editor.state.doc.textBetween(
editor.state.selection.from,
editor.state.selection.to,
" "
)) {
editor
.chain()
.focus()
.deleteSelection()
.insertContent(`<a href="${href}">${text.trim()}</a>`)
.run()
} else {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href })
.run()
}
}
setOpen(false)
setUrl("")
setText("")
}
const handleRemoveLink = () => {
editor.chain().focus().extendMarkRange("link").unsetLink().run()
setOpen(false)
setUrl("")
setText("")
}
return (
<Popover open={open} onOpenChange={handleOpen}>
<PopoverTrigger asChild>
<button
type="button"
onClick={(e) => {
if (isLinkActive) {
e.preventDefault()
handleToggle()
}
}}
className={cn(
MAIL_COMPOSE_BOTTOM_ICON_BTN,
isLinkActive && MAIL_COMPOSE_BOTTOM_ICON_BTN_ACTIVE
)}
title={isLinkActive ? "Supprimer le lien" : "Insérer un lien"}
>
<LinkIcon className="h-[18px] w-[18px]" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
side="top"
className={cn("w-[340px]", MAIL_COMPOSE_POPOVER_CLASS, COMPOSE_PORTAL_Z)}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<div className="flex flex-col gap-2.5">
<div className="text-sm font-medium text-foreground">
{isLinkActive ? "Modifier le lien" : "Insérer un lien"}
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Texte à afficher</label>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Texte du lien"
className="h-8 rounded border border-border bg-mail-surface px-2 text-sm text-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">URL</label>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
handleInsert()
}
}}
className="h-8 rounded border border-border bg-mail-surface px-2 text-sm text-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring"
autoFocus
/>
</div>
<div className="flex items-center justify-between pt-1">
{isLinkActive ? (
<button
type="button"
onClick={handleRemoveLink}
className="text-sm text-destructive hover:text-destructive/90 transition-colors"
>
Supprimer le lien
</button>
) : (
<span />
)}
<div className="flex gap-2">
<button
type="button"
onClick={() => setOpen(false)}
className="rounded px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent transition-colors"
>
Annuler
</button>
<button
type="button"
onClick={handleInsert}
disabled={!url.trim()}
className={cn("rounded px-3 py-1.5 text-sm font-medium disabled:opacity-50", MAIL_COMPOSE_PRIMARY_SEND_BTN)}
>
{isLinkActive ? "Modifier" : "Insérer"}
</button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
}
export function ComposeSignatureButton({
editor,
compose,
}: {
editor: Editor | null
compose: ComposeState
}) {
const { updateCompose } = useComposeActions()
const replaceSignature = useCallback(
(sigId: string | null) => {
if (!editor) return
const newHtml = insertSignatureHtml(editor.getHTML(), sigId)
editor.commands.setContent(newHtml)
updateCompose(compose.id, { bodyHtml: newHtml, signatureId: sigId })
},
[editor, compose.id, updateCompose]
)
const toggleAutoInsert = useCallback(() => {
const newVal = !compose.autoInsertSignature
updateCompose(compose.id, { autoInsertSignature: newVal })
if (!newVal) {
replaceSignature(null)
} else {
const sigId = compose.from.defaultSignatureId
if (sigId) replaceSignature(sigId)
}
}, [compose.autoInsertSignature, compose.from.defaultSignatureId, compose.id, updateCompose, replaceSignature])
if (!editor) return null
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Insérer une signature"
>
<PenTool className="h-[18px] w-[18px]" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="top"
className={cn(MAIL_MENU_SURFACE_CLASS, "min-w-[220px]", COMPOSE_PORTAL_Z)}
>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault()
toggleAutoInsert()
}}
className="gap-2"
>
<span className="flex h-4 w-4 items-center justify-center">
{compose.autoInsertSignature && <span className="text-xs"></span>}
</span>
Insérer automatiquement
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => replaceSignature(null)}
className={cn("gap-2", !compose.signatureId && MAIL_COMPOSE_MENU_SELECTED_CLASS)}
>
<span className="flex h-4 w-4 items-center justify-center">
{!compose.signatureId && <span className="text-xs"></span>}
</span>
Aucune signature
</DropdownMenuItem>
{SIGNATURES.map((sig) => (
<DropdownMenuItem
key={sig.id}
onSelect={() => replaceSignature(sig.id)}
className={cn("gap-2", compose.signatureId === sig.id && MAIL_COMPOSE_MENU_SELECTED_CLASS)}
>
<span className="flex h-4 w-4 items-center justify-center">
{compose.signatureId === sig.id && <span className="text-xs"></span>}
</span>
{sig.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
export interface ComposeBottomToolbarProps {
compose: ComposeState
editor: Editor | null
isEditingScheduled: boolean
showFormatting: boolean
sendMenuOpen: boolean
setShowFormatting: (v: boolean | ((prev: boolean) => boolean)) => void
setSendMenuOpen: (v: boolean) => void
handleSend: () => void
saveScheduledEdit: () => void | Promise<void>
sendScheduledFromEditNow: () => void | Promise<void>
applyScheduledPlanAt: (sendAt: Date) => void | Promise<void>
submitScheduledSendAt: (sendAt: Date) => void | Promise<void>
handleClose: () => void
fileInputRef: React.RefObject<HTMLInputElement | null>
imageInputRef: React.RefObject<HTMLInputElement | null>
}
export function ComposeBottomToolbar(props: ComposeBottomToolbarProps) {
const {
compose,
editor,
isEditingScheduled,
showFormatting,
sendMenuOpen,
setShowFormatting,
setSendMenuOpen,
handleSend,
saveScheduledEdit,
sendScheduledFromEditNow,
applyScheduledPlanAt,
submitScheduledSendAt,
handleClose,
fileInputRef,
imageInputRef,
} = props
return (
<div className="flex shrink-0 items-center gap-1 border-t border-border px-2 py-1.5">
{/* Send / save + dropdown */}
<div className="flex items-center">
{isEditingScheduled ? (
<>
<button
type="button"
onClick={() => void saveScheduledEdit()}
className={cn("rounded-l-full px-5 text-sm font-medium", MAIL_COMPOSE_PRIMARY_SEND_BTN)}
>
Enregistrer
</button>
<DropdownMenu modal={false} open={sendMenuOpen} onOpenChange={setSendMenuOpen}>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn("rounded-r-full border-l border-primary-foreground/30 px-1.5", MAIL_COMPOSE_PRIMARY_SEND_BTN)}
>
<ChevronDown className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className={cn("min-w-[220px]", COMPOSE_PORTAL_Z)}>
<DropdownMenuItem
onSelect={() => {
void sendScheduledFromEditNow()
}}
>
<Send className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
Envoyer maintenant
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="[&>svg:last-child]:text-muted-foreground">
<Clock className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
Planifier
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className={cn("min-w-[220px]", COMPOSE_PORTAL_Z)}>
<DropdownMenuItem
onSelect={() => {
void applyScheduledPlanAt(
new Date(Date.now() + 60 * 60 * 1000)
)
}}
>
<Clock className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
Envoyer dans une heure
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
void applyScheduledPlanAt(
getNextLocalWallClockDate(9, 0)
)
}}
>
<Clock className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
Envoyer à 9h
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
</>
) : (
<>
<button
type="button"
onClick={handleSend}
className={cn("rounded-l-full px-5 text-sm font-medium", MAIL_COMPOSE_PRIMARY_SEND_BTN)}
>
Envoyer
</button>
<DropdownMenu modal={false} open={sendMenuOpen} onOpenChange={setSendMenuOpen}>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn("rounded-r-full border-l border-primary-foreground/30 px-1.5", MAIL_COMPOSE_PRIMARY_SEND_BTN)}
>
<ChevronDown className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className={cn("min-w-[220px]", COMPOSE_PORTAL_Z)}>
<DropdownMenuItem
onSelect={() => {
void submitScheduledSendAt(
new Date(Date.now() + 60 * 60 * 1000)
)
}}
>
<Clock className="h-4 w-4 text-muted-foreground" />
Envoyer dans une heure
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
void submitScheduledSendAt(
getNextLocalWallClockDate(9, 0)
)
}}
>
<Clock className="h-4 w-4 text-muted-foreground" />
Envoyer à 9h
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setSendMenuOpen(false)}>
<Clock className="h-4 w-4 text-muted-foreground" />
Programmer l&apos;envoi
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
{/* Toolbar icons */}
<div className="flex items-center gap-0.5 ml-1">
<button
type="button"
onClick={() => setShowFormatting(!showFormatting)}
className={cn(
MAIL_COMPOSE_BOTTOM_ICON_BTN,
showFormatting && MAIL_COMPOSE_BOTTOM_ICON_BTN_ACTIVE
)}
title="Options de mise en forme"
>
<Type className="h-[18px] w-[18px]" />
</button>
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Joindre des fichiers"
onClick={() => fileInputRef.current?.click()}
>
<Paperclip className="h-[18px] w-[18px]" />
</button>
<ComposeLinkButton editor={editor} />
<ComposeEmojiButton editor={editor} />
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Insérer des fichiers avec Google Drive"
>
<HardDrive className="h-[18px] w-[18px]" />
</button>
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Insérer une photo"
onClick={() => imageInputRef.current?.click()}
>
<ImageIcon className="h-[18px] w-[18px]" />
</button>
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Activer le mode confidentiel"
>
<Lock className="h-[18px] w-[18px]" />
</button>
<ComposeSignatureButton editor={editor} compose={compose} />
<button
type="button"
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Plus d'options"
>
<MoreVertical className="h-[18px] w-[18px]" />
</button>
</div>
<div className="flex-1" />
<button
type="button"
onClick={handleClose}
className={MAIL_COMPOSE_BOTTOM_ICON_BTN}
title="Supprimer le brouillon"
>
<Trash2 className="h-[18px] w-[18px]" />
</button>
</div>
)
}