From 62b381b8444d1839fb8e00bb8fb934d392e1836f Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Mon, 20 Apr 2026 08:55:26 +0200 Subject: [PATCH] refactor(ui): unify shared primitives across pages --- docs/ui-ux-audit-2026-04-20.md | 87 +++++++++++++++++++ src/components/agents/AgentForm.tsx | 36 +++++--- src/components/agents/AgentList.tsx | 22 +++-- src/components/layout/NotificationCenter.tsx | 13 ++- src/components/layout/WindowControls.tsx | 7 +- src/components/projects/ProjectDashboard.tsx | 35 +++----- src/components/projects/ProjectForm.tsx | 31 ++++--- src/components/projects/ProjectGraylog.tsx | 52 +++++++----- src/components/projects/ProjectLiveAgent.tsx | 42 +++++---- src/components/projects/ProjectModules.tsx | 20 +++-- src/components/projects/ProjectTasks.tsx | 40 +++++---- src/components/settings/SettingsPage.tsx | 40 +++++---- src/components/tickets/TicketDetail.tsx | 67 ++++++--------- src/components/tickets/TicketList.tsx | 46 +++------- src/components/trackers/FilterBuilder.tsx | 15 ++-- src/components/trackers/TrackerConfig.tsx | 46 +++++----- src/components/trackers/TrackerList.tsx | 13 +-- src/components/ui/ConfirmModal.tsx | 7 +- src/components/ui/TicketStatusBadge.tsx | 33 ++++++++ src/components/ui/primitives.ts | 89 ++++++++++++++++++++ 20 files changed, 483 insertions(+), 258 deletions(-) create mode 100644 docs/ui-ux-audit-2026-04-20.md create mode 100644 src/components/ui/TicketStatusBadge.tsx create mode 100644 src/components/ui/primitives.ts diff --git a/docs/ui-ux-audit-2026-04-20.md b/docs/ui-ux-audit-2026-04-20.md new file mode 100644 index 0000000..1bb1562 --- /dev/null +++ b/docs/ui-ux-audit-2026-04-20.md @@ -0,0 +1,87 @@ +# Audit UI/UX transverse - 2026-04-20 + +## Périmètre audité + +Front React complet sous `src/components`: +- `agents/*` +- `projects/*` +- `tickets/*` +- `trackers/*` +- `settings/*` +- `layout/*` + +## Incohérences détectées (avant refactor) + +1. Boutons d'action dupliqués avec variations de couleurs et de tailles selon les pages (`bg-blue-600`, `bg-gray-900`, `bg-red-100`, etc.). +2. Champs de formulaire répétés avec plusieurs variantes de classes (`input`, `select`, `textarea`) non centralisées. +3. Cartes/blocs de contenu dupliqués (`rounded-lg border border-gray-200 bg-white p-4`) avec petites divergences de spacing. +4. Alertes (`error`, `warning`, `success`, `info`) construites page par page avec des styles proches mais non unifiés. +5. Badges de statut ticket réimplémentés dans plusieurs pages avec logique dupliquée et couverture de statuts non homogène. + +## Composants réutilisés sur plusieurs pages + +### 1) Boutons (multi-pages) + +Usage sur: +- Agents: `AgentList`, `AgentForm` +- Projets: `ProjectForm`, `ProjectDashboard`, `ProjectGraylog`, `ProjectLiveAgent`, `ProjectModules`, `ProjectTasks` +- Tickets: `TicketList`, `TicketDetail` +- Trackers: `TrackerConfig`, `TrackerList`, `FilterBuilder` +- Settings: `SettingsPage` +- Layout: `WindowControls`, `NotificationCenter` +- UI: `ConfirmModal` + +Unification appliquée: +- Ajout de `buttonClass()` dans [`src/components/ui/primitives.ts`](/home/leclere/Projets/IA/orchai/src/components/ui/primitives.ts) +- Variantes centralisées: `primary`, `secondary`, `danger`, `dangerSoft`, `neutralDark`, `success`, `ghost` +- Tailles centralisées: `xs`, `sm`, `md`, `icon` + +### 2) Formulaires (input/select/textarea + labels) + +Usage sur: +- `AgentForm`, `ProjectForm`, `SettingsPage`, `TrackerConfig`, `ProjectGraylog`, `ProjectLiveAgent`, `ProjectTasks`, `TicketDetail`, `FilterBuilder` + +Unification appliquée: +- `labelClass`, `inputClass`, `textAreaClass` dans [`src/components/ui/primitives.ts`](/home/leclere/Projets/IA/orchai/src/components/ui/primitives.ts) +- Remplacement des classes inline répétées par ces styles partagés. + +### 3) Cartes de contenu + +Usage sur: +- `ProjectDashboard`, `ProjectGraylog`, `ProjectLiveAgent`, `ProjectModules`, `ProjectTasks`, `TrackerConfig`, `TrackerList`, `TicketList`, `TicketDetail`, `AgentList` + +Unification appliquée: +- `cardClass` / `cardContentClass` dans [`src/components/ui/primitives.ts`](/home/leclere/Projets/IA/orchai/src/components/ui/primitives.ts) +- Réemploi des mêmes classes de surface sur les pages. + +### 4) Alertes (erreur/succès/warning/info) + +Usage sur: +- `AgentForm`, `AgentList`, `ProjectForm`, `ProjectGraylog`, `ProjectLiveAgent`, `ProjectModules`, `ProjectTasks`, `SettingsPage`, `TrackerConfig`, `TicketDetail` + +Unification appliquée: +- `noticeClass(tone, compact)` dans [`src/components/ui/primitives.ts`](/home/leclere/Projets/IA/orchai/src/components/ui/primitives.ts) +- Tons standards: `error`, `success`, `warning`, `info`. + +### 5) Badges de statut ticket + +Usage sur: +- `TicketList`, `TicketDetail`, `ProjectDashboard` + +Unification appliquée: +- Nouveau composant [`src/components/ui/TicketStatusBadge.tsx`](/home/leclere/Projets/IA/orchai/src/components/ui/TicketStatusBadge.tsx) +- Mapping de statuts mutualisé via `ticketStatusClass()` +- Suppression des mappings locaux dupliqués dans les pages tickets/dashboard. + +## Méthode de génération commune + +Oui, pour les composants réutilisés multi-pages identifiés, la génération du style passe désormais par une couche commune: +- `primitives.ts` pour les patterns UI transverses +- `TicketStatusBadge.tsx` pour les badges de statuts ticket + +Cela réduit la divergence visuelle et simplifie les évolutions de design futures. + +## Limites / reste à harmoniser + +1. Certaines zones très contextuelles (ex: bulles de conversation live, badges de session archivée, blocs de diff) conservent des styles spécifiques, volontairement. +2. Une seconde passe peut encore factoriser quelques micro-patterns (ex: tabs, badges non-ticket, liens d'action inline) si nécessaire. diff --git a/src/components/agents/AgentForm.tsx b/src/components/agents/AgentForm.tsx index 8af623e..9f437f4 100644 --- a/src/components/agents/AgentForm.tsx +++ b/src/components/agents/AgentForm.tsx @@ -3,6 +3,14 @@ import { useNavigate, useParams } from "react-router-dom"; import { createAgent, getAgent, improveAgentPrompt, updateAgent } from "../../lib/api"; import { getErrorMessage } from "../../lib/errors"; import type { AgentRole, AgentTool } from "../../lib/types"; +import { + buttonClass, + inputClass, + labelClass, + noticeClass, + pageTitleClass, + textAreaClass, +} from "../ui/primitives"; export default function AgentForm() { const navigate = useNavigate(); @@ -76,36 +84,36 @@ export default function AgentForm() { return (
-

{isEditing ? "Edit agent" : "New agent"}

+

{isEditing ? "Edit agent" : "New agent"}

{initializing &&
Loading agent...
} {isEditing && isDefaultAgent && ( -
+
This is a default agent. Its tool and script/prompt can be modified, but its name and role are fixed.
)}
- + setName(e.target.value)} disabled={isEditing && isDefaultAgent} required - className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + className={inputClass} />
- + setTool(e.target.value as AgentTool)} - className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + className={inputClass} > @@ -126,14 +134,14 @@ export default function AgentForm() {
-
{error && ( -
+
{error}
)} @@ -161,14 +169,14 @@ export default function AgentForm() { diff --git a/src/components/agents/AgentList.tsx b/src/components/agents/AgentList.tsx index 7fd3522..4a6efa4 100644 --- a/src/components/agents/AgentList.tsx +++ b/src/components/agents/AgentList.tsx @@ -4,6 +4,13 @@ import { deleteAgent, listAgents } from "../../lib/api"; import { getErrorMessage } from "../../lib/errors"; import type { Agent } from "../../lib/types"; import ConfirmModal from "../ui/ConfirmModal"; +import { + buttonClass, + cardContentClass, + noticeClass, + pageClass, + pageTitleClass, +} from "../ui/primitives"; export default function AgentList() { const [agents, setAgents] = useState([]); @@ -52,13 +59,10 @@ export default function AgentList() { } return ( -
+
-

Agents

- +

Agents

+ New agent
@@ -66,7 +70,7 @@ export default function AgentList() { {loading &&
Loading...
} {error && ( -
+
{error}
)} @@ -79,7 +83,7 @@ export default function AgentList() { {agents.map((agent) => (
setAgentToDelete(agent)} - className="rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200" + className={buttonClass({ variant: "dangerSoft", size: "xs" })} > Delete diff --git a/src/components/layout/NotificationCenter.tsx b/src/components/layout/NotificationCenter.tsx index 4252b39..ed580de 100644 --- a/src/components/layout/NotificationCenter.tsx +++ b/src/components/layout/NotificationCenter.tsx @@ -12,6 +12,7 @@ import { markNotificationRead, } from "../../lib/api"; import type { OrchaiNotification } from "../../lib/types"; +import { buttonClass, cardClass, pillClass } from "../ui/primitives"; type NewNotificationEvent = { notification: OrchaiNotification; @@ -180,7 +181,7 @@ export default function NotificationCenter() { {open && ( -
+

Notifications

diff --git a/src/components/layout/WindowControls.tsx b/src/components/layout/WindowControls.tsx index 09b3404..93336bb 100644 --- a/src/components/layout/WindowControls.tsx +++ b/src/components/layout/WindowControls.tsx @@ -1,4 +1,5 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; +import { buttonClass } from "../ui/primitives"; async function minimizeWindow() { try { @@ -31,7 +32,7 @@ export default function WindowControls() { type="button" onClick={() => void minimizeWindow()} title="Minimize" - className="h-7 w-7 rounded border border-gray-300 bg-white text-xs text-gray-700 hover:bg-gray-100" + className={`${buttonClass({ variant: "secondary", size: "icon" })} border border-gray-300`} > _ @@ -39,7 +40,7 @@ export default function WindowControls() { type="button" onClick={() => void toggleMaximizeWindow()} title="Maximize / Restore" - className="h-7 w-7 rounded border border-gray-300 bg-white text-xs text-gray-700 hover:bg-gray-100" + className={`${buttonClass({ variant: "secondary", size: "icon" })} border border-gray-300`} > [] @@ -47,7 +48,7 @@ export default function WindowControls() { type="button" onClick={() => void closeWindow()} title="Close" - className="h-7 w-7 rounded border border-red-300 bg-red-50 text-xs text-red-700 hover:bg-red-100" + className={`${buttonClass({ variant: "dangerSoft", size: "icon" })} border border-red-300`} > X diff --git a/src/components/projects/ProjectDashboard.tsx b/src/components/projects/ProjectDashboard.tsx index f936b03..404a035 100644 --- a/src/components/projects/ProjectDashboard.tsx +++ b/src/components/projects/ProjectDashboard.tsx @@ -16,6 +16,8 @@ import type { } from "../../lib/types"; import TrackerList from "../trackers/TrackerList"; import ConfirmModal from "../ui/ConfirmModal"; +import TicketStatusBadge from "../ui/TicketStatusBadge"; +import { buttonClass, cardContentClass, pageClass, pageTitleClass } from "../ui/primitives"; type ActivityLevel = "info" | "success" | "error"; @@ -331,19 +333,6 @@ export default function ProjectDashboard() { navigate("/"); } - function statusBadgeClass(status: string): string { - switch (status) { - case "Pending": - return "bg-yellow-100 text-yellow-700"; - case "Done": - return "bg-green-100 text-green-700"; - case "Error": - return "bg-red-100 text-red-700"; - default: - return "bg-blue-100 text-blue-700"; - } - } - function formatLeadTime(seconds: number | null): string { if (seconds === null || Number.isNaN(seconds)) { return "—"; @@ -374,26 +363,26 @@ export default function ProjectDashboard() { const errorRate24h = resolved24h > 0 ? `${Math.round((error24h / resolved24h) * 100)}%` : "—"; return ( -
+
-

{project.name}

+

{project.name}

Edit
-
+
Path: {project.path} @@ -499,7 +488,7 @@ export default function ProjectDashboard() {

Live Activity

-
+
Polling en cours: {activePollList.length} @@ -566,7 +555,7 @@ export default function ProjectDashboard() {
@@ -574,11 +563,7 @@ export default function ProjectDashboard() { {ticket.artifact_title}
- - {ticket.status} - + ))}
diff --git a/src/components/projects/ProjectForm.tsx b/src/components/projects/ProjectForm.tsx index f5455c8..b551a10 100644 --- a/src/components/projects/ProjectForm.tsx +++ b/src/components/projects/ProjectForm.tsx @@ -3,6 +3,13 @@ import { useNavigate, useParams } from "react-router-dom"; import { open } from "@tauri-apps/plugin-dialog"; import { createProject, getProject, updateProject } from "../../lib/api"; import { getErrorMessage } from "../../lib/errors"; +import { + buttonClass, + inputClass, + labelClass, + noticeClass, + pageTitleClass, +} from "../ui/primitives"; export default function ProjectForm() { const navigate = useNavigate(); @@ -60,13 +67,13 @@ export default function ProjectForm() { return (
-

+

{isEditing ? "Edit project" : "New project"}

-
{!isEditing && ( <>
-