"use client" import { memo, useCallback, useMemo, useState } from "react" import { Icon } from "@iconify/react" import type { Editor } from "@tiptap/react" import { AlignCenter, AlignJustify, AlignLeft, AlignRight, ChevronDown, ChevronUp, Bold, Italic, List, ListOrdered, Minus, MoreHorizontal, Plus, Printer, Redo, Underline as UnderlineIcon, Undo, X, } from "lucide-react" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { useToolbarOverflow } from "@/lib/drive/use-toolbar-overflow" import { applyFontSizePx, DOCS_FONT_SIZES, stepFontSizePx, } from "@/lib/drive/docs-font-size" import { DOCS_FONT_FAMILIES, } from "@/lib/drive/docs-font-family" import { DocsGraphicInsertMenu, } from "@/components/drive/richtext/docs-graphic-toolbar-menu" import { DocsLinkPopover } from "@/components/drive/richtext/docs-link-popover" import { DocsParagraphStyleSelect } from "@/components/drive/richtext/docs-paragraph-style-ui" import { useDocsParagraphStylesContext } from "@/lib/drive/docs-paragraph-styles-context" import { docsFontStackByName, useDocsFonts } from "@/lib/drive/use-docs-fonts" import { useDocsToolbarState } from "@/lib/drive/use-docs-toolbar-state" import { DOCS_LINE_HEIGHT_PRESETS } from "@/lib/drive/docs-line-spacing" import { readDocsCustomSpacingDraft } from "@/lib/drive/docs-line-spacing-actions" import { DocsLineSpacingDialog } from "@/components/drive/richtext/docs-line-spacing-dialog" import { cn } from "@/lib/utils" const ZOOM_OPTIONS = [50, 75, 90, 100, 125, 150, 200] as const const TEXT_COLORS = [ "#000000", "#434343", "#666666", "#999999", "#b7b7b7", "#cccccc", "#d9d9d9", "#efefef", "#f3f3f3", "#ffffff", "#980000", "#ff0000", "#ff9900", "#ffff00", "#00ff00", "#00ffff", "#4a86e8", "#0000ff", "#9900ff", "#ff00ff", ] as const /** Classic highlighter + pastel palette (Google Docs / marker style). */ const HIGHLIGHT_COLORS = [ "#ffff00", "#fff475", "#fce8b2", "#f4cccc", "#ffc8dd", "#d9ead3", "#b6d7a8", "#cfe2f3", "#a4c2f4", "#d9d2e9", "#e6e6e6", ] as const function DocsToolbarInner({ editor, disabled, zoom, onZoomChange, spellcheck, onToggleSpellcheck, showChromeToggle, chromeCollapsed, onToggleChromeCollapsed, onPrint, embedded, }: { editor: Editor | null disabled?: boolean zoom: number onZoomChange: (zoom: number) => void spellcheck: boolean onToggleSpellcheck: () => void showChromeToggle?: boolean chromeCollapsed?: boolean onToggleChromeCollapsed?: () => void onPrint?: () => void /** Rendered inside DocsEditorWorkspace shell (no outer docs-toolbar-shell). */ embedded?: boolean }) { const toolbarState = useDocsToolbarState(editor) const paragraphStylesCtx = useDocsParagraphStylesContext() const fontsQuery = useDocsFonts() const fonts = fontsQuery.data ?? DOCS_FONT_FAMILIES.map((f) => ({ name: f.name, stack: f.stack })) const [customSpacingOpen, setCustomSpacingOpen] = useState(false) const segments = useMemo(() => { if (!editor || !toolbarState) return [] const { canUndo, canRedo, styleId, fontFamilyState, fontSizeState, canStepFontSizeDown, canStepFontSizeUp, textColor, highlightColor, isBold, isItalic, isUnderline, isLink, alignLeft, alignCenter, alignRight, alignJustify, isBulletList, isOrderedList, isTaskList, canIncreaseIndent, canDecreaseIndent, lineHeightPresetId, } = toolbarState return [ { id: "history", sepAfter: false, node: ( <> editor.chain().focus().undo().run()} label="Annuler" > editor.chain().focus().redo().run()} label="Rétablir" > > ), }, { id: "print", sepAfter: false, node: ( <> onPrint?.() ?? window.print()}> > ), }, { id: "zoom", sepAfter: true, node: ( onZoomChange(Number(v))} disabled={disabled} > {ZOOM_OPTIONS.map((z) => ( {z}% ))} ), }, { id: "style", sepAfter: true, node: paragraphStylesCtx ? ( paragraphStylesCtx.applyStyle(value)} /> ) : ( ), }, { id: "font-family", sepAfter: true, node: ( { editor.chain().focus().setFontFamily(docsFontStackByName(fonts, value)).run() }} > {fonts.map((f) => ( {f.name} ))} ), }, { id: "font-size", sepAfter: true, node: ( stepFontSizePx(editor, -1)} > applyFontSizePx(editor, Number(value))} > {DOCS_FONT_SIZES.map((size) => ( {size} ))} stepFontSizePx(editor, 1)} > ), }, { id: "marks-basic", sepAfter: false, node: ( <> editor.chain().focus().toggleMark("bold").run()} label="Gras" > editor.chain().focus().toggleMark("italic").run()} label="Italique" > editor.chain().focus().toggleUnderline().run()} label="Souligné" > editor.chain().focus().setColor(color).run()} icon="material-symbols:format-color-text" /> > ), }, { id: "highlight", sepAfter: true, node: ( { editor.chain().focus().setHighlight({ color }).run() }} onClear={() => editor.chain().focus().unsetHighlight().run()} /> ), }, { id: "insert-link", sepAfter: false, node: ( <> > ), }, { id: "insert-graphic", sepAfter: true, node: , }, { id: "align", sepAfter: false, node: ( <> editor.chain().focus().setTextAlign("left").run()} label="Aligner à gauche" > editor.chain().focus().setTextAlign("center").run()} label="Centrer" > editor.chain().focus().setTextAlign("right").run()} label="Aligner à droite" > editor.chain().focus().setTextAlign("justify").run()} label="Justifier" > {DOCS_LINE_HEIGHT_PRESETS.map((option) => ( editor.chain().focus().setDocsLineHeight(option.value).run()} > {option.label} {lineHeightPresetId === option.id ? ( ) : null} ))} setCustomSpacingOpen(true)}> Espacement personnalisé… > ), }, { id: "lists", sepAfter: false, node: ( <> { if (isBulletList) editor.chain().focus().toggleBulletList().run() else editor.chain().focus().applyDocsBulletStyle("disc").run() }} label="Liste à puces" > { if (isOrderedList) editor.chain().focus().toggleOrderedList().run() else editor.chain().focus().applyDocsOrderedStyle("decimal").run() }} label="Liste numérotée" > { if (isTaskList) editor.chain().focus().toggleTaskList().run() else editor.chain().focus().applyDocsChecklistStyle("simple").run() }} label="Liste de contrôle" > editor.chain().focus().decreaseDocsIndent().run()} label="Diminuer le retrait" > editor.chain().focus().increaseDocsIndent().run()} label="Augmenter le retrait" > > ), }, { id: "clear", sepAfter: false, node: ( editor.chain().focus().unsetAllMarks().clearNodes().run()} > ), }, ] }, [ editor, toolbarState, disabled, zoom, onZoomChange, spellcheck, onToggleSpellcheck, paragraphStylesCtx, fonts, ]) const reservedTrailingPx = showChromeToggle ? 44 : 0 const { containerRef, measureRef, visibleCount, hasOverflow } = useToolbarOverflow( segments.length, reservedTrailingPx ) if (!editor) return null const visibleSegments = segments.slice(0, visibleCount) const overflowSegments = segments.slice(visibleCount) const toolbarRow = ( {segments.map((segment) => ( {segment.node} {segment.sepAfter ? : null} ))} {visibleSegments.map((segment) => ( {segment.node} {segment.sepAfter ? : null} ))} {hasOverflow ? ( {overflowSegments.map((segment, index) => ( {index > 0 ? null : null} {segment.node} ))} ) : null} {showChromeToggle ? ( <> {chromeCollapsed ? ( ) : ( )} > ) : null} ) if (embedded) { return ( <> {toolbarRow} {editor ? ( editor.chain().focus().setDocsCustomSpacing(input).run()} /> ) : null} > ) } return ( {toolbarRow} {editor ? ( editor.chain().focus().setDocsCustomSpacing(input).run()} /> ) : null} ) } export const DocsToolbar = memo(DocsToolbarInner) function ToolbarSep() { return } function ToolbarIconBtn({ ref, children, onClick, active, disabled, label, className, ...rest }: { children: React.ReactNode onClick?: React.MouseEventHandler active?: boolean disabled?: boolean label: string className?: string } & React.ComponentPropsWithoutRef<"button"> & { ref?: React.Ref }) { return ( {children} ) } function colorSwatchOutlineClass(hex: string): string { const luminance = colorRelativeLuminance(hex) if (luminance > 0.72) { return "border border-black/50 ring-1 ring-black/30" } if (luminance > 0.45) { return "border border-black/40 ring-1 ring-black/20" } return "border border-white/60 ring-1 ring-black/35" } function colorRelativeLuminance(hex: string): number { const raw = hex.replace("#", "") if (raw.length !== 6) return 0 const channel = (index: number) => { const value = parseInt(raw.slice(index, index + 2), 16) / 255 return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4 } return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4) } function ColorToolbarGlyph({ icon, color, highlight = false, }: { icon: string color: string highlight?: boolean }) { return ( ) } function ColorPicker({ disabled, label, colors, currentColor, onPick, icon, }: { disabled?: boolean label: string colors: readonly string[] currentColor: string onPick: (color: string) => void icon: string }) { return ( {colors.map((color) => ( onPick(color)} /> ))} ) } function HighlightColorPicker({ disabled, colors, currentColor, isActive, onPick, onClear, }: { disabled?: boolean colors: readonly string[] currentColor: string isActive: boolean onPick: (color: string) => void onClear: () => void }) { return ( {colors.map((color) => ( onPick(color)} /> ))} ) }