ultisuite-client/components/settings/settings-kit.tsx
R3D347HR4Y 8f81d7aba1
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(admin-settings): refactor admin settings components for improved usability and consistency
- Replaced legacy components with new `SettingsCard`, `SettingsField`, and `SettingsToggleRow` for a unified design.
- Enhanced `AdminListControls` to support compact mode and improved pagination controls.
- Updated various sections including `AiAssistantSection`, `AuthenticationSection`, and `DriveMountOAuthSection` to utilize new components, streamlining the settings interface.
- Improved accessibility and user experience across admin settings with clearer labels and hints.
- Deprecated old components while maintaining backward compatibility for existing admin sections.
2026-06-15 11:10:17 +02:00

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 /mail/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>
)
}