From 1cbce35d611f3ccccb7971557919e451d51d77fe Mon Sep 17 00:00:00 2001 From: D4kooo Date: Sun, 31 May 2026 11:45:21 +0200 Subject: [PATCH] fix(a11y): accessibility, theming & perf pass across all modules Systematic audit-driven fixes across 7 module groups (UI surface), verified with tsc + lint + production build (all green). Criticals: - Chat: scope the streaming live-region (was re-announcing every token) - Board: gate infinite edge animation + node pulse behind reduced-motion; add text/icon status badges (was color-only) - Settings: drop invalid CutoutCard role="button" wrapping nested interactives; add explicit "Configurer" trigger + hoist delete dialog Systemic: - aria-current on active nav (sidebar / settings / admin) - Spinner primitive: role=status on wrapper, sr-only label, motion-safe - form helper text wired via aria-describedby - citation highlight -> --highlight token (was hardcoded yellow) - list/table semantics: ul/li, scope, caption, role=progressbar, dl metrics - reduced-motion gating on spring/infinite animations - dead "Bientot" controls removed; emoji -> @tabler icons - print footer contrast; admin stats no longer hidden on mobile Bugs: - broken oklch(oklch()) vignette gradient (rendered nothing) -> color-mix - hardcoded black MiniMap maskColor -> token (dark-mode correct) Perf: - React.memo on assistant markdown rows via stable onOpenDoc, so historical messages stop re-rendering/re-linkifying on every streamed token --- src/app/(app)/admin/admin-nav.tsx | 4 +- src/app/(app)/admin/cabinet/cabinet-form.tsx | 1 + src/app/(app)/admin/users/[id]/page.tsx | 9 ++- src/app/(app)/admin/users/user-row.tsx | 53 +++++++++++- src/app/(app)/admin/users/users-table.tsx | 2 + src/app/(app)/board/[id]/add-agent-dialog.tsx | 2 +- src/app/(app)/board/[id]/agent-flow-node.tsx | 29 ++++++- src/app/(app)/board/[id]/animated-edge.tsx | 4 +- src/app/(app)/board/[id]/inline-rename.tsx | 5 +- src/app/(app)/board/[id]/page.tsx | 9 ++- .../(app)/board/[id]/pipeline-mode-bar.tsx | 40 ++++++++-- .../(app)/board/[id]/pipeline-workflow.tsx | 4 +- src/app/(app)/board/agent-edit-sheet.tsx | 29 ++++--- src/app/(app)/chat/agent-theatre.tsx | 9 ++- .../(app)/chat/assistant-message-actions.tsx | 4 +- src/app/(app)/chat/chat-shell.tsx | 48 ++++++++--- src/app/(app)/chat/composer-menu.tsx | 11 --- src/app/(app)/chat/docx-view.tsx | 6 +- src/app/(app)/chat/edit-card.tsx | 5 +- src/app/(app)/chat/live-workflow-panel.tsx | 9 ++- src/app/(app)/chat/pdf-view.tsx | 7 +- src/app/(app)/chat/thinking-indicator.tsx | 8 +- src/app/(app)/command-palette.tsx | 2 +- src/app/(app)/dashboard/page.tsx | 14 ++-- src/app/(app)/documents/document-row.tsx | 1 + .../(app)/documents/documents-dropzone.tsx | 6 +- src/app/(app)/documents/folder-row.tsx | 3 + src/app/(app)/documents/page.tsx | 33 +++++--- src/app/(app)/mobile-nav.tsx | 1 + .../settings/connectors/connector-card.tsx | 80 +++++++++---------- src/app/(app)/settings/connectors/page.tsx | 12 +-- .../(app)/settings/general/theme-picker.tsx | 3 +- src/app/(app)/settings/mcp/add-mcp-dialog.tsx | 3 +- .../(app)/settings/profile/password-form.tsx | 3 +- src/app/(app)/settings/providers/page.tsx | 13 +-- .../settings/providers/provider-card.tsx | 37 ++++----- src/app/(app)/settings/settings-nav.tsx | 4 +- .../settings/skills/skill-form-dialog.tsx | 6 +- src/app/(app)/settings/usage/page.tsx | 14 ++-- src/app/(app)/sidebar-content.tsx | 14 +++- .../tabular-reviews/[id]/auto-refresh.tsx | 35 ++++++-- .../[id]/column-edit-popover.tsx | 18 +---- .../tabular-reviews/[id]/review-grid.tsx | 36 ++++++--- .../tabular-reviews/new/new-review-form.tsx | 13 ++- src/app/login/login-form.tsx | 6 +- src/app/print/chat/[id]/page.tsx | 8 +- src/components/ui/checkbox.tsx | 1 + src/components/ui/select.tsx | 2 + src/components/ui/spinner.tsx | 5 +- 49 files changed, 431 insertions(+), 240 deletions(-) diff --git a/src/app/(app)/admin/admin-nav.tsx b/src/app/(app)/admin/admin-nav.tsx index c9c3369..e8b53b0 100644 --- a/src/app/(app)/admin/admin-nav.tsx +++ b/src/app/(app)/admin/admin-nav.tsx @@ -44,6 +44,7 @@ export function AdminNav({ horizontal }: { horizontal?: boolean }) { {sections.map((group) => (
-

+

{group.group}

-
+
= 100 diff --git a/src/app/(app)/admin/users/user-row.tsx b/src/app/(app)/admin/users/user-row.tsx index be7d434..b9930d1 100644 --- a/src/app/(app)/admin/users/user-row.tsx +++ b/src/app/(app)/admin/users/user-row.tsx @@ -220,6 +220,50 @@ export function UserRow({ {feedback && (
{feedback}
)} + + {/* Résumé compact des stats sur mobile : la rangée détaillée + (Conv./Docs/Projets/Ce mois) est masquée < md, on reflow donc + les chiffres ici pour ne pas les perdre sur petit écran. */} +
+ + Conv. + + {entry.stats.convCount} + + + + Docs + + {entry.stats.docCount} + + + + Projets + + {entry.stats.projectCount} + + + + Ce mois + 0 && + monthSpentCents >= quotaCents + ? "text-destructive" + : "text-foreground" + }`} + > + {formatEurFromCents(monthSpentCents)} + {quotaCents != null && ( + + {" / "} + {formatEurFromCents(quotaCents)} + + )} + + +
{/* Stats compactes : 4 chiffres en tabular-nums. Cabinet-friendly : @@ -250,7 +294,14 @@ export function UserRow({ )}
{quotaPercent != null && ( -
+
= 100 diff --git a/src/app/(app)/admin/users/users-table.tsx b/src/app/(app)/admin/users/users-table.tsx index e01f347..bb382d9 100644 --- a/src/app/(app)/admin/users/users-table.tsx +++ b/src/app/(app)/admin/users/users-table.tsx @@ -69,6 +69,7 @@ export function UsersTable({ rows, currentUserId, nowMs }: Props) { value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Rechercher par email ou nom…" + aria-label="Rechercher un utilisateur" className="w-full rounded-md border border-input bg-background pl-8 pr-3 py-1.5 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" />
@@ -78,6 +79,7 @@ export function UsersTable({ rows, currentUserId, nowMs }: Props) { key={f} type="button" onClick={() => setFilter(f)} + aria-pressed={filter === f} className={`px-2.5 py-1.5 rounded-md text-xs transition-colors ${ filter === f ? "bg-foreground text-background" diff --git a/src/app/(app)/board/[id]/add-agent-dialog.tsx b/src/app/(app)/board/[id]/add-agent-dialog.tsx index dac94f1..d7136c5 100644 --- a/src/app/(app)/board/[id]/add-agent-dialog.tsx +++ b/src/app/(app)/board/[id]/add-agent-dialog.tsx @@ -163,7 +163,7 @@ export function AddAgentDialog({
{mode === "council" && ( -

+

{(() => { const debaters = Math.max(0, agentCount - 1); const calls = rounds * debaters + 1; return ( <> - ⚠️ Coût estimé : {calls} appel{calls > 1 ? "s" : ""} LLM par - question — {debaters} débatteur{debaters > 1 ? "s" : ""} sur{" "} - {rounds} tour{rounds > 1 ? "s" : ""}, plus 1 synthèse finale. + + + Coût estimé : {calls} appel{calls > 1 ? "s" : ""} LLM par + question — {debaters} débatteur{debaters > 1 ? "s" : ""} sur{" "} + {rounds} tour{rounds > 1 ? "s" : ""}, plus 1 synthèse finale. + ); })()} diff --git a/src/app/(app)/board/[id]/pipeline-workflow.tsx b/src/app/(app)/board/[id]/pipeline-workflow.tsx index 3833441..4fd497a 100644 --- a/src/app/(app)/board/[id]/pipeline-workflow.tsx +++ b/src/app/(app)/board/[id]/pipeline-workflow.tsx @@ -349,7 +349,7 @@ function PipelineWorkflowInner({ {/* Vignette radiale subtile pour donner du caractère au canvas */}

{/* Bouton reset : visible dès qu'au moins un agent a des @@ -409,7 +409,7 @@ function PipelineWorkflowInner({ pannable zoomable className="!bg-card !border !border-border" - maskColor="rgb(0 0 0 / 0.05)" + maskColor="color-mix(in oklab, var(--foreground) 6%, transparent)" nodeColor="var(--color-foreground)" /> )} diff --git a/src/app/(app)/board/agent-edit-sheet.tsx b/src/app/(app)/board/agent-edit-sheet.tsx index 11f0069..90d8edc 100644 --- a/src/app/(app)/board/agent-edit-sheet.tsx +++ b/src/app/(app)/board/agent-edit-sheet.tsx @@ -251,15 +251,26 @@ export function AgentEditSheet({ maxLength={8000} aria-describedby={`prompt-help-${agent.id}`} /> - {systemPrompt.length > 2000 && ( -

- ⚠️ Ce prompt sera répété à chaque appel de cet agent — un - prompt long multiplie les coûts en mode council/parallel. -

- )} +

2000 + ? "flex items-start gap-1 text-xs text-warning" + : "text-xs text-muted-foreground" + } + > + {systemPrompt.length > 2000 ? ( + <> + + + Ce prompt sera répété à chaque appel de cet agent — un + prompt long multiplie les coûts en mode council/parallel. + + + ) : ( + "Vide = prompt « factory » du rôle. Plus le prompt est long, plus chaque appel coûte cher." + )} +

diff --git a/src/app/(app)/chat/agent-theatre.tsx b/src/app/(app)/chat/agent-theatre.tsx index 6047773..a34cdff 100644 --- a/src/app/(app)/chat/agent-theatre.tsx +++ b/src/app/(app)/chat/agent-theatre.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef } from "react"; -import { AnimatePresence, motion } from "motion/react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { @@ -259,6 +259,7 @@ function TurnCard({ turn }: { turn: AgentTurn }) { const meta = roleMeta(turn.role); const Icon = meta.icon; const isFinal = turn.key.endsWith("-final"); + const reduceMotion = useReducedMotion(); return ( 12 && ( <> - + {availableModels.length - 12} modèles supplémentaires — changez de modèle dans le composer - + )} diff --git a/src/app/(app)/chat/chat-shell.tsx b/src/app/(app)/chat/chat-shell.tsx index e3955ed..d9bd1d6 100644 --- a/src/app/(app)/chat/chat-shell.tsx +++ b/src/app/(app)/chat/chat-shell.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useChat } from "@ai-sdk/react"; @@ -675,7 +675,7 @@ function hasRenderableText( * un rendu char-par-char à ~60fps indépendant du débit SSE. Sur les * messages historiques, render direct sans buffer. */ -function AssistantMarkdownPart({ +const AssistantMarkdownPart = memo(function AssistantMarkdownPart({ text, isLive, mergedDocuments, @@ -729,7 +729,7 @@ function AssistantMarkdownPart({ {linkifyDocMentions(display, mergedDocuments)} ); -} +}); function DocPickerContent({ documents, @@ -915,6 +915,14 @@ export function ChatShell({ }, [initialConversationId] ); + // Adaptateur stable (signature ReactMarkdown/ToolPart → setOpenDoc) pour que + // `React.memo(AssistantMarkdownPart)` ne se ré-invalide pas à chaque token : + // les messages historiques ne re-rendent plus pendant le streaming. + const handleOpenDoc = useCallback( + (documentId: string, targetText: string) => + setOpenDoc({ documentId, targetText }), + [setOpenDoc] + ); // Tracking des document_id déjà auto-ouverts dans le DocPanel pour ne // pas réouvrir à chaque re-render. Survit au remount qui se produit // quand l'URL passe de /chat à /chat?id=xxx via sessionStorage. @@ -1045,6 +1053,10 @@ export function ChatShell({ [] ); + // Annonce de complétion pour lecteurs d'écran (région live dédiée, à part + // du thread streamé). Vide pendant le stream, renseigné dans onFinish. + const [statusText, setStatusText] = useState(""); + const { messages, setMessages, @@ -1064,6 +1076,7 @@ export function ChatShell({ // d'affichage stable indépendant du rythme du provider. experimental_throttle: 50, onFinish: ({ message }) => { + setStatusText("Réponse de Louis terminée."); const meta = message?.metadata as | { conversationId?: string; usage?: Usage } | undefined; @@ -1235,6 +1248,8 @@ export function ChatShell({ setEditingMessageId(null); setEditingDraft(""); setEditError(null); + // Le textarea inline d'édition se démonte → rendre le focus au composer. + composerRef.current?.focus(); } async function saveEditing(messageId: string) { @@ -1272,6 +1287,8 @@ export function ChatShell({ }); setEditingMessageId(null); setEditingDraft(""); + // Le textarea inline d'édition se démonte → rendre le focus au composer. + composerRef.current?.focus(); regenerate({ body: { providerKeyId, @@ -1326,6 +1343,8 @@ export function ChatShell({ function handleSubmit() { const trimmed = input.trim(); if (!trimmed || isBusy) return; + // Reset l'annonce live pour qu'elle se re-déclenche à la fin du tour. + setStatusText(""); // Mémorise les attachements pour les associer au message user qui // va apparaître dans `messages` au prochain tick (cf useEffect plus // bas qui consume cette ref). @@ -1349,6 +1368,10 @@ export function ChatShell({ // Le doc reste accessible via le picker / la sidebar /documents s'il // est nécessaire pour un prochain message. setAttachedDocIds([]); + // Rend le focus au composer après envoi (le textarea reste monté ; + // simplement disabled pendant le stream → focus revient au prochain + // tour de saisie). + composerRef.current?.focus(); } // Associe les attachements pending au dernier message user dès qu'il @@ -1585,11 +1608,16 @@ export function ChatShell({ {/* Messages or empty state */} + {/* Région live dédiée : annonce uniquement une string de complétion + courte (cf. statusText) — n'enveloppe PAS le thread streamé pour + éviter de ré-annoncer chaque token. */} +
+ {statusText} +
@@ -1615,6 +1643,7 @@ export function ChatShell({ return (
{/* Wrapper d'étapes : utile UNIQUEMENT pour pipelines @@ -1770,9 +1799,7 @@ export function ChatShell({ text={part.text} isLive={isLiveMessage} mergedDocuments={mergedDocuments} - onOpenDoc={(documentId, targetText) => - setOpenDoc({ documentId, targetText }) - } + onOpenDoc={handleOpenDoc} /> ) : ( @@ -1797,9 +1824,7 @@ export function ChatShell({ input={p.input} output={p.output} state={p.state} - onOpenDoc={(documentId, targetText) => - setOpenDoc({ documentId, targetText }) - } + onOpenDoc={handleOpenDoc} /> ); } @@ -1982,6 +2007,7 @@ export function ChatShell({ } }} placeholder="Posez votre question…" + aria-label="Votre message à Louis" rows={1} disabled={isBusy} className="w-full resize-none rounded-t-2xl bg-transparent px-4 pt-3 pb-1 text-[15px] leading-[1.55] placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 max-h-[240px] overflow-y-auto" diff --git a/src/app/(app)/chat/composer-menu.tsx b/src/app/(app)/chat/composer-menu.tsx index 4c22a2c..21b0410 100644 --- a/src/app/(app)/chat/composer-menu.tsx +++ b/src/app/(app)/chat/composer-menu.tsx @@ -7,7 +7,6 @@ import { IconSparkles, IconBriefcase, IconSettings, - IconWorld, IconFileText, IconKey, IconCpu, @@ -190,16 +189,6 @@ export function ComposerMenu({ Tous les réglages - - {/* Slot futur : web search toggle, style picker… */} - - - - Recherche web - - bientôt - - ); diff --git a/src/app/(app)/chat/docx-view.tsx b/src/app/(app)/chat/docx-view.tsx index ad3a826..89e7374 100644 --- a/src/app/(app)/chat/docx-view.tsx +++ b/src/app/(app)/chat/docx-view.tsx @@ -135,7 +135,7 @@ export function DocxView({ } .docx-view-container .docx-wrapper > section.docx { margin: 1rem auto !important; - box-shadow: 0 0 0 1px oklch(0.92 0.008 265); + box-shadow: 0 0 0 1px var(--border); } `} {loading && ( @@ -180,8 +180,8 @@ function highlightAndScroll( range.setEnd(node, start + needle.trim().length); const mark = document.createElement("mark"); mark.className = "louis-quote-mark"; - mark.style.backgroundColor = "oklch(0.94 0.12 90)"; - mark.style.color = "inherit"; + mark.style.backgroundColor = "var(--highlight)"; + mark.style.color = "var(--highlight-foreground)"; range.surroundContents(mark); const rect = mark.getBoundingClientRect(); const scrollRect = scrollEl.getBoundingClientRect(); diff --git a/src/app/(app)/chat/edit-card.tsx b/src/app/(app)/chat/edit-card.tsx index fd64adb..e6c28ff 100644 --- a/src/app/(app)/chat/edit-card.tsx +++ b/src/app/(app)/chat/edit-card.tsx @@ -61,8 +61,9 @@ export function EditCard({ raw }: { raw: string }) { Suggestion d'édition
{decision === "kept" && ( - - ✓ Conservée + + + Conservée )} diff --git a/src/app/(app)/chat/live-workflow-panel.tsx b/src/app/(app)/chat/live-workflow-panel.tsx index 4714b26..449ed73 100644 --- a/src/app/(app)/chat/live-workflow-panel.tsx +++ b/src/app/(app)/chat/live-workflow-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { AnimatePresence, motion } from "motion/react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { IconBriefcase, IconLoader2, IconCheck, IconAlertTriangle, IconX } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; import { roleMeta } from "../board/agent-role-meta"; @@ -47,6 +47,7 @@ export function LiveWorkflowPanel({ onClose, onOpenTheatre, }: LiveWorkflowPanelProps) { + const reduceMotion = useReducedMotion(); return ( {open && ( @@ -54,7 +55,11 @@ export function LiveWorkflowPanel({ initial={{ opacity: 0, y: 20, scale: 0.96 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 12, scale: 0.98 }} - transition={{ type: "spring", damping: 24, stiffness: 280 }} + transition={ + reduceMotion + ? { duration: 0.2, ease: [0.23, 1, 0.32, 1] } + : { type: "spring", damping: 24, stiffness: 280 } + } role="status" aria-live="polite" aria-atomic="false" diff --git a/src/app/(app)/chat/pdf-view.tsx b/src/app/(app)/chat/pdf-view.tsx index 7eaf6ec..f8203fc 100644 --- a/src/app/(app)/chat/pdf-view.tsx +++ b/src/app/(app)/chat/pdf-view.tsx @@ -115,14 +115,11 @@ export function PdfView({ fileUrl, targetText }: Props) {
); diff --git a/src/app/(app)/chat/thinking-indicator.tsx b/src/app/(app)/chat/thinking-indicator.tsx index 621b2a2..d3fb6ae 100644 --- a/src/app/(app)/chat/thinking-indicator.tsx +++ b/src/app/(app)/chat/thinking-indicator.tsx @@ -33,9 +33,13 @@ export function ThinkingIndicator() { }, []); return ( -
+
- + -
+
↑↓ pour naviguer · ↵ pour ouvrir · ESC pour fermer ⌘K
diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index b9fac15..1215459 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -264,17 +264,17 @@ function Stat({ href?: string; }) { const inner = ( - <> -

+

+
{label} -

-

+

+
{value} -

+
{hint && ( -

{hint}

+
{hint}
)} - +
); if (href) { return ( diff --git a/src/app/(app)/documents/document-row.tsx b/src/app/(app)/documents/document-row.tsx index 534e081..ac7c41e 100644 --- a/src/app/(app)/documents/document-row.tsx +++ b/src/app/(app)/documents/document-row.tsx @@ -280,6 +280,7 @@ export function DocumentRow({ type="file" accept=".pdf,.docx,.txt,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain" className="hidden" + aria-label="Téléverser une nouvelle version" onChange={onReplaceChange} /> {replaceError && ( diff --git a/src/app/(app)/documents/documents-dropzone.tsx b/src/app/(app)/documents/documents-dropzone.tsx index 4f7410a..615515a 100644 --- a/src/app/(app)/documents/documents-dropzone.tsx +++ b/src/app/(app)/documents/documents-dropzone.tsx @@ -50,7 +50,11 @@ export function DocumentsDropzone({ overlayHint="PDF, DOCX ou texte — 25 Mo max par fichier" > {(uploadingCount > 0 || error) && ( -
+
{uploadingCount > 0 && ( diff --git a/src/app/(app)/documents/folder-row.tsx b/src/app/(app)/documents/folder-row.tsx index b9fbedf..ca9d6b1 100644 --- a/src/app/(app)/documents/folder-row.tsx +++ b/src/app/(app)/documents/folder-row.tsx @@ -62,6 +62,7 @@ export function FolderRow({ folder }: { folder: DocumentFolder }) {
setDraft(e.target.value)} onKeyDown={(e) => { @@ -78,6 +79,7 @@ export function FolderRow({ folder }: { folder: DocumentFolder }) { onClick={commitRename} className="size-8 inline-flex items-center justify-center rounded-md text-primary hover:bg-accent" title="Valider" + aria-label="Valider" > @@ -89,6 +91,7 @@ export function FolderRow({ folder }: { folder: DocumentFolder }) { }} className="size-8 inline-flex items-center justify-center rounded-md text-muted-foreground hover:bg-accent" title="Annuler" + aria-label="Annuler" > diff --git a/src/app/(app)/documents/page.tsx b/src/app/(app)/documents/page.tsx index 4d57b27..d16d848 100644 --- a/src/app/(app)/documents/page.tsx +++ b/src/app/(app)/documents/page.tsx @@ -119,8 +119,11 @@ export default async function DocumentsPage({
-
-

Documents

+

+ Fichiers · dossiers · versions +

+
+

Documents

Importez vos PDF / DOCX (≤ 25 Mo), organisez-les en dossiers et versionnez-les. Interrogez-les ensuite depuis le chat (pièce @@ -184,20 +187,26 @@ export default async function DocumentsPage({ {isEmpty ? ( ) : ( -
+
    {subFolders.map((f) => ( - +
  • + +
  • ))} {familyViews.map((fv) => ( - +
  • + +
  • ))} -
+ )} diff --git a/src/app/(app)/mobile-nav.tsx b/src/app/(app)/mobile-nav.tsx index 7d97a74..76df9f0 100644 --- a/src/app/(app)/mobile-nav.tsx +++ b/src/app/(app)/mobile-nav.tsx @@ -43,6 +43,7 @@ export function MobileNav({ user, conversations, projects }: Props) { diff --git a/src/app/(app)/settings/connectors/connector-card.tsx b/src/app/(app)/settings/connectors/connector-card.tsx index 5271687..0e9ee2e 100644 --- a/src/app/(app)/settings/connectors/connector-card.tsx +++ b/src/app/(app)/settings/connectors/connector-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useTransition, type MouseEvent } from "react"; +import { useState, useTransition } from "react"; import { IconCheck, IconCircleDashed, @@ -60,10 +60,6 @@ type Props = { keys: ConnectorKey[]; }; -function stopCardClick(e: MouseEvent) { - e.stopPropagation(); -} - export function ConnectorCard({ type, keys }: Props) { const meta = CONNECTOR_CATALOG[type]; const Icon = meta.icon; @@ -100,16 +96,6 @@ export function ConnectorCard({ type, keys }: Props) { <> { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - openDialog(); - } - }} > {primary.isActive ? "Activé" : "Inactif"} @@ -197,7 +182,6 @@ export function ConnectorCard({ type, keys }: Props) { href={meta.docsUrl} target="_blank" rel="noopener noreferrer" - onClick={stopCardClick} className="text-muted-foreground hover:text-foreground transition-colors" aria-label="Documentation" > @@ -210,14 +194,10 @@ export function ConnectorCard({ type, keys }: Props) { className="-mt-1 -mr-1 size-7 inline-flex shrink-0 items-center justify-center rounded-md border border-border hover:bg-accent transition-colors disabled:opacity-50" aria-label="Actions" disabled={pending} - onClick={stopCardClick} > - + - {isConfigured && ( - - « {primary.label} » sera supprimé. La clé chiffrée est - retirée — l'intégration ne fonctionnera plus tant - qu'une nouvelle clé n'est pas saisie. - - } - pending={pending} - onConfirm={() => { - startTransition(async () => { - await deleteConnectorKey(primary.id); - setDeleteOpen(false); - }); - }} - /> - )} -

{meta.description}

@@ -276,6 +234,18 @@ export function ConnectorCard({ type, keys }: Props) {

)} + +
+ +
@@ -286,6 +256,28 @@ export function ConnectorCard({ type, keys }: Props) {
+ {isConfigured && ( + + « {primary.label} » sera supprimé. La clé chiffrée est + retirée — l'intégration ne fonctionnera plus tant + qu'une nouvelle clé n'est pas saisie. + + } + pending={pending} + onConfirm={() => { + startTransition(async () => { + await deleteConnectorKey(primary.id); + setDeleteOpen(false); + }); + }} + /> + )} + diff --git a/src/app/(app)/settings/connectors/page.tsx b/src/app/(app)/settings/connectors/page.tsx index ff34415..1536d19 100644 --- a/src/app/(app)/settings/connectors/page.tsx +++ b/src/app/(app)/settings/connectors/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { desc, eq } from "drizzle-orm"; -import { IconShieldLock, IconInfoCircle, IconClock } from "@tabler/icons-react"; +import { IconShieldLock, IconClock } from "@tabler/icons-react"; import { auth } from "@/auth"; import { db } from "@/db"; import { connectorKeys, type ConnectorKey } from "@/db/schema"; @@ -116,16 +116,6 @@ export default async function ConnectorsPage() { ))}
- -
); } diff --git a/src/app/(app)/settings/general/theme-picker.tsx b/src/app/(app)/settings/general/theme-picker.tsx index 25a628c..0180f44 100644 --- a/src/app/(app)/settings/general/theme-picker.tsx +++ b/src/app/(app)/settings/general/theme-picker.tsx @@ -2,7 +2,7 @@ import { useSyncExternalStore } from "react"; import { useTheme } from "next-themes"; -import { IconSun, IconMoon, IconDeviceLaptop } from "@tabler/icons-react"; +import { IconSun, IconMoon, IconDeviceLaptop, IconCheck } from "@tabler/icons-react"; function useMounted(): boolean { return useSyncExternalStore( @@ -47,6 +47,7 @@ export function ThemePicker() { > {o.label} + {active && } ); })} diff --git a/src/app/(app)/settings/mcp/add-mcp-dialog.tsx b/src/app/(app)/settings/mcp/add-mcp-dialog.tsx index 5b54ca5..8f73ac0 100644 --- a/src/app/(app)/settings/mcp/add-mcp-dialog.tsx +++ b/src/app/(app)/settings/mcp/add-mcp-dialog.tsx @@ -112,8 +112,9 @@ export function AddMcpDialog() { name="headers" placeholder='{"Authorization": "Bearer …"}' autoComplete="off" + aria-describedby="headers-help" /> -

+

Format JSON. Laisser vide si aucune authentification.

diff --git a/src/app/(app)/settings/profile/password-form.tsx b/src/app/(app)/settings/profile/password-form.tsx index 80e6e96..450a6b8 100644 --- a/src/app/(app)/settings/profile/password-form.tsx +++ b/src/app/(app)/settings/profile/password-form.tsx @@ -48,8 +48,9 @@ export function PasswordForm() { autoComplete="new-password" minLength={10} required + aria-describedby="newPassword-help" /> -

+

Minimum 10 caractères. Un gestionnaire de mots de passe est recommandé.

diff --git a/src/app/(app)/settings/providers/page.tsx b/src/app/(app)/settings/providers/page.tsx index eda305f..9623802 100644 --- a/src/app/(app)/settings/providers/page.tsx +++ b/src/app/(app)/settings/providers/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { desc, eq } from "drizzle-orm"; -import { IconShieldLock, IconInfoCircle } from "@tabler/icons-react"; +import { IconShieldLock } from "@tabler/icons-react"; import { auth } from "@/auth"; import { db } from "@/db"; import { providerKeys, type ProviderKey } from "@/db/schema"; @@ -104,17 +104,6 @@ export default async function ProvidersPage() {
))} - - ); } diff --git a/src/app/(app)/settings/providers/provider-card.tsx b/src/app/(app)/settings/providers/provider-card.tsx index d6e4902..2b7a54b 100644 --- a/src/app/(app)/settings/providers/provider-card.tsx +++ b/src/app/(app)/settings/providers/provider-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useTransition, type MouseEvent } from "react"; +import { useState, useTransition } from "react"; import { IconCheck, IconAlertTriangle, @@ -66,11 +66,6 @@ type Props = { keys: ProviderKey[]; }; -/** Stop a click from bubbling up to the CutoutCard root (which opens the dialog). */ -function stopCardClick(e: MouseEvent) { - e.stopPropagation(); -} - export function ProviderCard({ type, keys }: Props) { const meta = PROVIDER_CATALOG[type]; const primary = keys.find((k) => k.isDefault) ?? keys[0] ?? null; @@ -107,16 +102,6 @@ export function ProviderCard({ type, keys }: Props) { <> { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - openDialog(); - } - }} > {primary.isActive ? "Activé" : "Inactif"} @@ -206,7 +190,6 @@ export function ProviderCard({ type, keys }: Props) { href={meta.docsUrl} target="_blank" rel="noopener noreferrer" - onClick={stopCardClick} className="text-muted-foreground hover:text-foreground transition-colors" aria-label="Documentation" > @@ -219,14 +202,10 @@ export function ProviderCard({ type, keys }: Props) { className="-mt-1 -mr-1 size-7 inline-flex shrink-0 items-center justify-center rounded-md border border-border hover:bg-accent transition-colors disabled:opacity-50" aria-label="Actions" disabled={pending} - onClick={stopCardClick} > - + { @@ -271,6 +250,18 @@ export function ProviderCard({ type, keys }: Props) {

)} + +
+ +
{/* Reveal-on-hover CTA */} diff --git a/src/app/(app)/settings/settings-nav.tsx b/src/app/(app)/settings/settings-nav.tsx index 8ef8d98..8566b02 100644 --- a/src/app/(app)/settings/settings-nav.tsx +++ b/src/app/(app)/settings/settings-nav.tsx @@ -67,6 +67,7 @@ export function SettingsNav({ {all.map((group) => (
-

+

{group.group}

    @@ -97,6 +98,7 @@ export function SettingsNav({
  • void }) { value={triggerHint} onChange={(e) => setTriggerHint(e.target.value)} placeholder="ex. Quand l'utilisateur cite une jurisprudence ou un article de loi" + aria-describedby="skill-hint-help" /> -

    +

    Phrase descriptive lue par un petit modèle qui décide d'activer ou non la compétence selon la demande.

    @@ -186,8 +187,9 @@ function SkillForm({ mode, onClose }: { mode: Mode; onClose: () => void }) { onChange={(e) => setSystemPrompt(e.target.value)} placeholder="Le texte qui sera injecté dans le prompt système quand la compétence est activée…" className="w-full resize-y rounded-md border border-input bg-card px-3 py-2 text-sm font-mono leading-relaxed shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" + aria-describedby="skill-prompt-help" /> -

    +

    Soyez explicite, donnez des exemples si nécessaire. Cette instruction prend le pas sur le comportement par défaut.

    diff --git a/src/app/(app)/settings/usage/page.tsx b/src/app/(app)/settings/usage/page.tsx index b1d57c7..9c5ce35 100644 --- a/src/app/(app)/settings/usage/page.tsx +++ b/src/app/(app)/settings/usage/page.tsx @@ -112,14 +112,14 @@ export default async function UsagePage() { {/* Coût du mois — typographie large, asymétrique, pas une carte */}
    -
    -

    +

    +
    Coût estimé ce mois -

    -

    +

    +
    {formatTotals(totalsMonth)} -

    -
    + +
    -
    +
    {label}
    diff --git a/src/app/(app)/sidebar-content.tsx b/src/app/(app)/sidebar-content.tsx index ddb4e66..c8c1d9a 100644 --- a/src/app/(app)/sidebar-content.tsx +++ b/src/app/(app)/sidebar-content.tsx @@ -147,7 +147,7 @@ export function SidebarContent({ {/* Nav + conversations */}
    -