feat(plans/wizard): éditeur avec icônes de tonalité + toggle de sélection
- Champ Décalage retiré : on change le timing en cliquant une autre case du calendrier (delete + click ailleurs), c'est plus aligné avec la métaphore calendrier - Tonalité passée d'un select à un groupe de 4 boutons icônes : · Doux → Smile (sourire chaleureux) · Standard → MessageCircle (bulle de conversation polie) · Ferme → AlertTriangle (alerte mesurée) · Strict → Gavel (marteau de juge) Chaque bouton actif prend la couleur de fond de sa tonalité, plus visuel et compact qu'un dropdown - Header de l'éditeur : la pastille colorée devient une pastille avec l'icône de tonalité dedans → on lit la tonalité d'un coup d'œil - Toggle : re-cliquer la case déjà sélectionnée la désélectionne (retour à l'état "vue d'ensemble" avec le hint), au lieu d'avoir une sélection permanente Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
07712da774
commit
0a3b8523ef
@ -1,11 +1,32 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChevronLeft, ChevronRight, Plus, Trash2, AlertTriangle } from "lucide-react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
Smile,
|
||||
MessageCircle,
|
||||
Gavel,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { RelanceTone } from "@rubis/shared";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TONE_LABELS } from "@/lib/plans";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
|
||||
/**
|
||||
* Icône qui illustre chaque tonalité. Cohérent avec l'escalade émotionnelle :
|
||||
* sourire pour amical, bulle de conversation pour standard, alerte pour
|
||||
* ferme, marteau de juge pour mise en demeure.
|
||||
*/
|
||||
const TONE_ICON: Record<RelanceTone, LucideIcon> = {
|
||||
amical: Smile,
|
||||
courtois: MessageCircle,
|
||||
ferme: AlertTriangle,
|
||||
mise_en_demeure: Gavel,
|
||||
};
|
||||
|
||||
/**
|
||||
* Étape minimale pour la visu calendrier (subject/body arrivent à
|
||||
@ -404,8 +425,16 @@ function DayCell({
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Éditeur compact — header explicite + 1 ligne de champs
|
||||
// Éditeur compact — header avec icône de tonalité + sélecteur 4 icônes
|
||||
// ============================================================================
|
||||
|
||||
const TONE_ORDER: RelanceTone[] = [
|
||||
"amical",
|
||||
"courtois",
|
||||
"ferme",
|
||||
"mise_en_demeure",
|
||||
];
|
||||
|
||||
function CompactEditor({
|
||||
step,
|
||||
stepDate,
|
||||
@ -419,18 +448,22 @@ function CompactEditor({
|
||||
onUpdate: (patch: Partial<DraftStepLite>) => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
const HeaderIcon = TONE_ICON[step.tone];
|
||||
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
{/* Header explicite : on sait quelle relance on édite */}
|
||||
<div className="space-y-3">
|
||||
{/* Header : pastille tonalité (icône + couleur) + date + supprimer */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"shrink-0 size-2.5 rounded-full border",
|
||||
"shrink-0 flex items-center justify-center size-7 rounded-full border-2",
|
||||
TONE_CELL_CLASS[step.tone],
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<HeaderIcon size={14} aria-hidden="true" />
|
||||
</span>
|
||||
<p className="text-[13px] text-ink-2 truncate leading-tight">
|
||||
Relance du{" "}
|
||||
<strong className="font-display font-semibold text-ink">
|
||||
@ -455,41 +488,43 @@ function CompactEditor({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[110px,1fr] gap-2 items-end">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-[11px] font-semibold text-ink-3">
|
||||
Décalage
|
||||
</span>
|
||||
<OffsetInput
|
||||
id={`step-offset`}
|
||||
value={step.offsetDays}
|
||||
onCommit={(n) => onUpdate({ offsetDays: n })}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-[11px] font-semibold text-ink-3">Tonalité</span>
|
||||
<select
|
||||
className={cn(
|
||||
"h-10 w-full rounded-default border border-line bg-white px-2.5",
|
||||
"text-[13.5px] text-ink",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rubis-glow focus-visible:border-rubis",
|
||||
)}
|
||||
value={step.tone}
|
||||
onChange={(e) => {
|
||||
const tone = e.target.value as RelanceTone;
|
||||
{/* Sélecteur de tonalité : 4 boutons icône, plus visuel qu'un select */}
|
||||
<div>
|
||||
<p className="text-[11px] font-semibold text-ink-3 mb-1.5">Tonalité</p>
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
{TONE_ORDER.map((t) => {
|
||||
const Icon = TONE_ICON[t];
|
||||
const isActive = step.tone === t;
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onUpdate({
|
||||
tone,
|
||||
requiresManualValidation: tone === "mise_en_demeure",
|
||||
});
|
||||
}}
|
||||
tone: t,
|
||||
requiresManualValidation: t === "mise_en_demeure",
|
||||
})
|
||||
}
|
||||
aria-pressed={isActive}
|
||||
aria-label={TONE_LABELS[t]}
|
||||
title={TONE_LABELS[t]}
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center gap-0.5 py-2 px-1",
|
||||
"rounded-default border-2 transition-all",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rubis-glow",
|
||||
isActive
|
||||
? cn(TONE_CELL_CLASS[t], "ring-2 ring-rubis-glow scale-[1.04]")
|
||||
: "border-line bg-white text-ink-2 hover:border-ink-3 hover:bg-cream-2/50",
|
||||
)}
|
||||
>
|
||||
{(Object.keys(TONE_LABELS) as RelanceTone[]).map((t) => (
|
||||
<option key={t} value={t}>
|
||||
<Icon size={16} aria-hidden="true" />
|
||||
<span className="text-[10.5px] font-semibold leading-tight">
|
||||
{TONE_LABELS[t]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{step.tone === "mise_en_demeure" && (
|
||||
@ -559,50 +594,3 @@ function formatShortDate(d: Date): string {
|
||||
return FR_SHORT.format(d);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OffsetInput
|
||||
// ============================================================================
|
||||
function OffsetInput({
|
||||
id,
|
||||
value,
|
||||
onCommit,
|
||||
}: {
|
||||
id: string;
|
||||
value: number;
|
||||
onCommit: (n: number) => void;
|
||||
}) {
|
||||
const [local, setLocal] = useState<string>(String(value));
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(String(value));
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="-?[0-9]*"
|
||||
className="h-10 text-center tabular-nums"
|
||||
value={local}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
if (next === "" || next === "-" || /^-?\d{0,3}$/.test(next)) {
|
||||
setLocal(next);
|
||||
if (next !== "" && next !== "-") {
|
||||
const parsed = parseInt(next, 10);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
onCommit(Math.max(-30, Math.min(180, parsed)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (local === "" || local === "-") {
|
||||
setLocal("0");
|
||||
onCommit(0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -476,7 +476,11 @@ function StepCadence({ draft, onChange }: { draft: Draft; onChange: (d: Draft) =
|
||||
steps={draft.steps}
|
||||
selectedIndex={selectedIdx}
|
||||
globalTone={draft.globalTone}
|
||||
onSelectStep={setSelectedIdx}
|
||||
// Toggle : re-cliquer une étape déjà sélectionnée la désélectionne
|
||||
// pour revenir à la vue d'ensemble.
|
||||
onSelectStep={(idx) =>
|
||||
setSelectedIdx((cur) => (cur === idx ? -1 : idx))
|
||||
}
|
||||
onUpdateStep={updateStep}
|
||||
onAddStep={addStep}
|
||||
onAddStepAtOffset={addStepAtOffset}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user