- Updated routing for mail settings to redirect to the new settings layout. - Introduced new account layout and section components for better organization. - Replaced hardcoded paths with constants for account and mail settings to enhance maintainability. - Removed deprecated mail settings layout and integrated it into the new settings structure. - Enhanced user experience by streamlining navigation between account and mail settings.
287 lines
7.4 KiB
TypeScript
287 lines
7.4 KiB
TypeScript
"use client"
|
|
|
|
import type { ReactNode } from "react"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Switch } from "@/components/ui/switch"
|
|
import { Checkbox } from "@/components/ui/checkbox"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/**
|
|
* Settings UI kit — composants unifiés pour les interfaces de réglages
|
|
* (/admin/* et /settings). Centralise cards, champs, lignes à bascule,
|
|
* grilles et hints afin d'homogénéiser typographies, paddings et alignements.
|
|
*/
|
|
|
|
/** Surface card commune (réglages admin + mail) — alignée sur la card mail. */
|
|
export const SETTINGS_CARD_SURFACE_CLASS = cn(
|
|
"rounded-xl border border-mail-border bg-mail-surface shadow-sm",
|
|
"dark:bg-mail-surface-elevated dark:shadow-[0_1px_4px_rgba(0,0,0,0.35)]",
|
|
)
|
|
|
|
export const SETTINGS_CARD_TITLE_CLASS = "text-sm font-semibold text-foreground"
|
|
export const SETTINGS_CARD_DESCRIPTION_CLASS =
|
|
"text-[13px] leading-relaxed text-muted-foreground"
|
|
export const SETTINGS_FIELD_LABEL_CLASS = "text-xs font-medium text-muted-foreground"
|
|
|
|
/** Carte de réglages : en-tête (titre + description + action) puis corps. */
|
|
export function SettingsCard({
|
|
title,
|
|
description,
|
|
hint,
|
|
action,
|
|
badges,
|
|
footer,
|
|
children,
|
|
className,
|
|
bodyClassName,
|
|
contentClassName,
|
|
divider = true,
|
|
}: {
|
|
title?: ReactNode
|
|
description?: ReactNode
|
|
/** Élément aligné à droite de l'en-tête (Switch, bouton, badge…). */
|
|
action?: ReactNode
|
|
/** Ligne d'indications/badges sous la description. */
|
|
badges?: ReactNode
|
|
hint?: ReactNode
|
|
footer?: ReactNode
|
|
children?: ReactNode
|
|
className?: string
|
|
/** Classe sur le wrapper interne (padding). */
|
|
bodyClassName?: string
|
|
/** Classe sur le conteneur du corps (children). */
|
|
contentClassName?: string
|
|
/** Séparateur entre en-tête et corps. */
|
|
divider?: boolean
|
|
}) {
|
|
const hasHeader = Boolean(title || description || action || badges || hint)
|
|
|
|
return (
|
|
<section className={cn(SETTINGS_CARD_SURFACE_CLASS, className)}>
|
|
<div className={cn("p-5", bodyClassName)}>
|
|
{hasHeader ? (
|
|
<div className="flex items-start gap-4">
|
|
<div className="min-w-0 flex-1">
|
|
{title ? <p className={SETTINGS_CARD_TITLE_CLASS}>{title}</p> : null}
|
|
{description ? (
|
|
<p className={cn(title && "mt-1", SETTINGS_CARD_DESCRIPTION_CLASS)}>
|
|
{description}
|
|
</p>
|
|
) : null}
|
|
{badges ? <div className="mt-2 flex flex-wrap gap-2">{badges}</div> : null}
|
|
{hint ? <div className="mt-2">{hint}</div> : null}
|
|
</div>
|
|
{action ? <div className="shrink-0">{action}</div> : null}
|
|
</div>
|
|
) : null}
|
|
|
|
{children ? (
|
|
<div
|
|
className={cn(
|
|
hasHeader && "mt-4 pt-4",
|
|
hasHeader && divider && "border-t border-mail-border",
|
|
"space-y-4",
|
|
contentClassName,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
) : null}
|
|
|
|
{footer ? <div className="mt-4">{footer}</div> : null}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
/** Champ de formulaire : label + contrôle + hint/erreur. */
|
|
export function SettingsField({
|
|
label,
|
|
htmlFor,
|
|
hint,
|
|
error,
|
|
required,
|
|
children,
|
|
className,
|
|
labelClassName,
|
|
labelAction,
|
|
}: {
|
|
label?: ReactNode
|
|
htmlFor?: string
|
|
hint?: ReactNode
|
|
error?: ReactNode
|
|
required?: boolean
|
|
children: ReactNode
|
|
className?: string
|
|
labelClassName?: string
|
|
/** Élément aligné à droite du label (lien, bouton…). */
|
|
labelAction?: ReactNode
|
|
}) {
|
|
return (
|
|
<div className={cn("space-y-1.5", className)}>
|
|
{label ? (
|
|
<div className="flex items-center justify-between gap-2">
|
|
<Label htmlFor={htmlFor} className={cn(SETTINGS_FIELD_LABEL_CLASS, labelClassName)}>
|
|
{label}
|
|
{required ? <span className="ml-0.5 text-destructive">*</span> : null}
|
|
</Label>
|
|
{labelAction}
|
|
</div>
|
|
) : null}
|
|
{children}
|
|
{hint ? (
|
|
<div className="text-xs leading-relaxed text-muted-foreground">{hint}</div>
|
|
) : null}
|
|
{error ? (
|
|
<div className="text-xs leading-relaxed text-destructive">{error}</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Ligne à bascule : titre + description + Switch. */
|
|
export function SettingsToggleRow({
|
|
title,
|
|
description,
|
|
checked,
|
|
onCheckedChange,
|
|
disabled,
|
|
hint,
|
|
variant = "bordered",
|
|
className,
|
|
}: {
|
|
title: ReactNode
|
|
description?: ReactNode
|
|
checked: boolean
|
|
onCheckedChange: (checked: boolean) => void
|
|
disabled?: boolean
|
|
/** Indication sous la ligne (ex. verrou déploiement). */
|
|
hint?: ReactNode
|
|
variant?: "bordered" | "plain"
|
|
className?: string
|
|
}) {
|
|
const row = (
|
|
<label
|
|
className={cn(
|
|
"flex items-center justify-between gap-4",
|
|
variant === "bordered" &&
|
|
"rounded-lg border border-mail-border bg-mail-surface-muted/40 px-3.5 py-3",
|
|
disabled && "opacity-70",
|
|
className,
|
|
)}
|
|
>
|
|
<span className="min-w-0">
|
|
<span className="block text-sm font-medium text-foreground">{title}</span>
|
|
{description ? (
|
|
<span className="mt-0.5 block text-xs leading-relaxed text-muted-foreground">
|
|
{description}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
<Switch checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} />
|
|
</label>
|
|
)
|
|
|
|
if (!hint) return row
|
|
return (
|
|
<div className="space-y-1.5">
|
|
{row}
|
|
{hint}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Ligne à cocher : titre + description + Checkbox. */
|
|
export function SettingsCheckboxRow({
|
|
title,
|
|
description,
|
|
checked,
|
|
onCheckedChange,
|
|
disabled,
|
|
variant = "plain",
|
|
className,
|
|
}: {
|
|
title: ReactNode
|
|
description?: ReactNode
|
|
checked: boolean
|
|
onCheckedChange: (checked: boolean) => void
|
|
disabled?: boolean
|
|
variant?: "bordered" | "plain"
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<label
|
|
className={cn(
|
|
"flex items-start gap-2.5",
|
|
variant === "bordered" &&
|
|
"rounded-lg border border-mail-border bg-mail-surface-muted/40 px-3.5 py-3",
|
|
disabled && "opacity-70",
|
|
className,
|
|
)}
|
|
>
|
|
<Checkbox
|
|
checked={checked}
|
|
disabled={disabled}
|
|
onCheckedChange={(v) => onCheckedChange(v === true)}
|
|
className="mt-0.5"
|
|
/>
|
|
<span className="min-w-0">
|
|
<span className="block text-sm text-foreground">{title}</span>
|
|
{description ? (
|
|
<span className="mt-0.5 block text-xs leading-relaxed text-muted-foreground">
|
|
{description}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
</label>
|
|
)
|
|
}
|
|
|
|
/** Grille de champs responsive (1 ou 2 colonnes). */
|
|
export function SettingsGrid({
|
|
columns = 2,
|
|
children,
|
|
className,
|
|
}: {
|
|
columns?: 1 | 2
|
|
children: ReactNode
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"grid min-w-0 gap-4",
|
|
columns === 2 && "sm:grid-cols-2",
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Indication brève (texte muted) sous un champ ou une carte. */
|
|
export function SettingsHint({
|
|
children,
|
|
tone = "muted",
|
|
className,
|
|
}: {
|
|
children: ReactNode
|
|
tone?: "muted" | "warning" | "danger"
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<p
|
|
className={cn(
|
|
"text-xs leading-relaxed",
|
|
tone === "muted" && "text-muted-foreground",
|
|
tone === "warning" && "text-amber-600 dark:text-amber-500",
|
|
tone === "danger" && "text-destructive",
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</p>
|
|
)
|
|
}
|