mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
Thème - Clair par défaut ; les classes dark: suivent le bouton de thème (et non plus le réglage du système) ; animations tw-animate-css enfin importées. - Couleurs shadcn branchées sur les variables (survols des menus visibles), libellés gris au contraste AA dans les deux thèmes, fond plein pour le menu et l'en-tête. Composants partagés (src/components/ui) - PageHeader, Card/Field, StatCard, Badge/DeltaBadge/StoreBadge/GammeBadge, Tabs/Segmented/useUrlTab, Select/SearchInput, EmptyState/ErrorState/ Skeleton, Tooltip/Terme (glossaire), toast et confirmer, Pagination, DataTable (recherche, filtres, tri, pagination, export, totaux). - Sources uniques : magasins, gammes (avec leur sens), glossaire, seuils de marge, export Excel. Navigation - Menu groupé (Au quotidien / Analyses / Suivi / Administration), déplié par défaut, libellés en français. - En-tête : nom de la page, thème, menu du compte avec le vrai nom, la hauteur des lignes de la Grille et la déconnexion. La recherche globale, qui ne filtrait que la Grille, est retirée. - Page Historique (sessions + exports) ; /snapshots et /exports y redirigent. - Squelettes de chargement au lieu de la fenêtre plein écran, pages d'erreur et « introuvable » en français, arrivée sur l'Accueil après connexion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
258 lines
8.3 KiB
TypeScript
258 lines
8.3 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function DropdownMenu({
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
}
|
|
|
|
function DropdownMenuPortal({
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
return (
|
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
)
|
|
}
|
|
|
|
function DropdownMenuTrigger({
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
return (
|
|
<DropdownMenuPrimitive.Trigger
|
|
data-slot="dropdown-menu-trigger"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuContent({
|
|
className,
|
|
sideOffset = 4,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
return (
|
|
<DropdownMenuPrimitive.Portal>
|
|
<DropdownMenuPrimitive.Content
|
|
data-slot="dropdown-menu-content"
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 max-h-[80vh] min-w-[12rem] overflow-x-hidden overflow-y-auto rounded-lg border border-[var(--border-strong)] bg-[var(--bg-surface)] p-1.5 text-[var(--text-primary)] shadow-[var(--shadow-md)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</DropdownMenuPrimitive.Portal>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuGroup({
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
return (
|
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
)
|
|
}
|
|
|
|
function DropdownMenuItem({
|
|
className,
|
|
inset,
|
|
variant = "default",
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
inset?: boolean
|
|
variant?: "default" | "destructive"
|
|
}) {
|
|
return (
|
|
<DropdownMenuPrimitive.Item
|
|
data-slot="dropdown-menu-item"
|
|
data-inset={inset}
|
|
data-variant={variant}
|
|
className={cn(
|
|
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='w-'])]:w-4 [&_svg:not([class*='h-'])]:h-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuCheckboxItem({
|
|
className,
|
|
children,
|
|
checked,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
return (
|
|
<DropdownMenuPrimitive.CheckboxItem
|
|
data-slot="dropdown-menu-checkbox-item"
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center rounded-sm py-2 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
className
|
|
)}
|
|
checked={checked}
|
|
{...props}
|
|
>
|
|
<span className="pointer-events-none absolute left-2 flex w-3.5 h-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<CheckIcon className="w-4 h-4" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</DropdownMenuPrimitive.CheckboxItem>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuRadioGroup({
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
return (
|
|
<DropdownMenuPrimitive.RadioGroup
|
|
data-slot="dropdown-menu-radio-group"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuRadioItem({
|
|
className,
|
|
children,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
return (
|
|
<DropdownMenuPrimitive.RadioItem
|
|
data-slot="dropdown-menu-radio-item"
|
|
className={cn(
|
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm focus:outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='w-'])]:w-4 [&_svg:not([class*='h-'])]:h-4",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<span className="pointer-events-none absolute left-2 flex w-3.5 h-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<CircleIcon className="w-2 h-2 fill-current" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</DropdownMenuPrimitive.RadioItem>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuLabel({
|
|
className,
|
|
inset,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
inset?: boolean
|
|
}) {
|
|
return (
|
|
<DropdownMenuPrimitive.Label
|
|
data-slot="dropdown-menu-label"
|
|
data-inset={inset}
|
|
className={cn(
|
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuSeparator({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
return (
|
|
<DropdownMenuPrimitive.Separator
|
|
data-slot="dropdown-menu-separator"
|
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuShortcut({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"span">) {
|
|
return (
|
|
<span
|
|
data-slot="dropdown-menu-shortcut"
|
|
className={cn(
|
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuSub({
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
|
}
|
|
|
|
function DropdownMenuSubTrigger({
|
|
className,
|
|
inset,
|
|
children,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
inset?: boolean
|
|
}) {
|
|
return (
|
|
<DropdownMenuPrimitive.SubTrigger
|
|
data-slot="dropdown-menu-sub-trigger"
|
|
data-inset={inset}
|
|
className={cn(
|
|
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm focus:outline-none select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='w-'])]:w-4 [&_svg:not([class*='h-'])]:h-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ChevronRightIcon className="ml-auto w-4 h-4" />
|
|
</DropdownMenuPrimitive.SubTrigger>
|
|
)
|
|
}
|
|
|
|
function DropdownMenuSubContent({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
return (
|
|
<DropdownMenuPrimitive.SubContent
|
|
data-slot="dropdown-menu-sub-content"
|
|
className={cn(
|
|
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg border border-[var(--border-strong)] bg-[var(--bg-surface)] p-1 text-[var(--text-primary)] shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export {
|
|
DropdownMenu,
|
|
DropdownMenuPortal,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuLabel,
|
|
DropdownMenuItem,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuSubContent,
|
|
}
|