diff --git a/frontend/src/SettingsDialog.tsx b/frontend/src/SettingsDialog.tsx index 41576b6..1ef1947 100644 --- a/frontend/src/SettingsDialog.tsx +++ b/frontend/src/SettingsDialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { ApiAccess } from "./ApiAccess"; import { api, type Channel, type Settings, type Share, type UpdateStatus } from "./api"; import { CopyIcon, TrashIcon } from "./icons"; @@ -11,7 +11,7 @@ const MAX_TITLE = 80; // voir storage.MaxTitleLen interface Props { settings: Settings; update: UpdateStatus | null; - /** Ouvert depuis l'icône de mise à jour : affiche directement la section Excalidraw. */ + /** Ouvert depuis l'icône de mise à jour : affiche directement l'onglet Excalidraw. */ focusExcalidraw?: boolean; onChange: (s: Settings) => void; onClose: () => void; @@ -34,6 +34,16 @@ const TRASH_DAYS = [ { days: 30, label: "30 jours" }, ]; +/** Onglets du dialogue : une section par onglet, pour que tout tienne sans défilement. */ +const TABS = [ + { id: "apparence", label: "Apparence" }, + { id: "excalidraw", label: "Excalidraw" }, + { id: "poubelle", label: "Poubelle" }, + { id: "partage", label: "Liens de partage" }, + { id: "api", label: "API et MCP" }, +] as const; +type Tab = (typeof TABS)[number]["id"]; + /** Paramètres partagés par tous les utilisateurs : titre, logo, version d'Excalidraw. */ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, onClose }: Props) { const [title, setTitle] = useState(settings.title); @@ -41,11 +51,12 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on const [error, setError] = useState(null); const [copied, setCopied] = useState(false); const [shares, setShares] = useState(null); + const [tab, setTab] = useState(focusExcalidraw ? "excalidraw" : "apparence"); const fileInput = useRef(null); - const exSection = useRef(null); + const tabList = useRef(null); useEffect(() => { - if (focusExcalidraw) exSection.current?.scrollIntoView({ block: "start" }); + if (focusExcalidraw) setTab("excalidraw"); }, [focusExcalidraw]); const loadShares = () => @@ -97,6 +108,16 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on run(() => api.uploadLogo(f)); }; + // Flèches du clavier dans la liste d'onglets (verticale sur bureau, horizontale sur mobile). + const onTabKey = (e: ReactKeyboardEvent) => { + const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[e.key]; + if (!step) return; + e.preventDefault(); + const i = (TABS.findIndex((t) => t.id === tab) + step + TABS.length) % TABS.length; + setTab(TABS[i].id); + tabList.current?.querySelectorAll("[role=tab]")[i]?.focus(); + }; + const dirty = title.trim() !== settings.title; const pending = pendingVersion(update, settings.channel); const cmd = pending ? rebuildCommand(pending) : null; @@ -107,6 +128,16 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on setTimeout(() => setCopied(false), 1500); }); + // Les onglets masqués restent montés : un jeton juste créé ou un titre en cours de saisie + // ne sont pas perdus en changeant d'onglet. + const pane = (id: Tab) => ({ + id: `settings-${id}`, + role: "tabpanel", + "aria-labelledby": `settings-tab-${id}`, + className: "settings-pane", + hidden: tab !== id, + }); + return (

Paramètres

-

Apparence

- - -
- Logo -
- {settings.logo ? ( - Logo actuel - ) : ( - - ? - - )} -
- - {settings.logo && ( - - )} - PNG, recadré au centre et réduit à 64×64 px, affiché en rond. + ))} +
+ +
+
+ + +
+ Logo +
+ {settings.logo ? ( + Logo actuel + ) : ( + + ? + + )} +
+ + {settings.logo && ( + + )} + PNG, recadré au centre et réduit à 64×64 px, affiché en rond. +
+ { + onFile(e.target.files?.[0]); + e.target.value = ""; + }} + /> +
+
+
+ +
+
+ Version à suivre +
+ {CHANNELS.map((c) => ( + + ))} +
+ {CHANNELS.find((c) => c.id === settings.channel)?.hint} +

+ Version installée : {INSTALLED} +
+ {!update?.enabled + ? "Vérification des mises à jour désactivée (UPDATE_CHECK=false)." + : !update.tags && !update.latest + ? "Registre npm injoignable pour l'instant : versions publiées inconnues." + : pending + ? <>À installer : {pending} + : "À jour pour ce canal."} +

+ {cmd && ( + <> + + Excalidraw est compilé dans l'image : changer de canal ou de version ne prend effet + qu'après reconstruction, depuis le dossier du docker-compose : + +
+ {cmd} + +
+ + {copied + ? "Commande copiée." + : <>Pour conserver ce choix aux reconstructions suivantes, mettre EXCALIDRAW_VERSION={pending} dans .env. Tester l'image avant la mise en service : les versions 0.x peuvent changer d'API.} + + + )} + + Notes de version + +
+
+ +
+
+ Suppression définitive automatique +
+ {TRASH_DAYS.map((d) => ( + + ))} +
+ + {settings.trashDays + ? `Les éléments mis à la poubelle sont supprimés définitivement ${settings.trashDays} jour${settings.trashDays > 1 ? "s" : ""} après leur suppression.` + : "La poubelle garde tout jusqu'à ce qu'on la vide ; son icône indique si elle contient des éléments."} + +
+
+ +
+
+ {shares?.length ? ( +
    + {shares.map((s) => ( +
  • +
    + {s.path} + +
    + + Créé le {new Date(s.created).toLocaleDateString(undefined, { dateStyle: "short" })} · {fmtExpiry(s)} + +
  • + ))} +
+ ) : ( + + Aucun lien actif. Pour partager un schéma en lecture seule : icône de partage de la barre du haut, ou menu + « … » de l'explorateur. + + )} + {!!shares?.length && ( + + )} +
+
+ +
+
- { - onFile(e.target.files?.[0]); - e.target.value = ""; - }} - />
-
-

Excalidraw

- Version à suivre -
- {CHANNELS.map((c) => ( - - ))} -
- {CHANNELS.find((c) => c.id === settings.channel)?.hint} -

- Version installée : {INSTALLED} -
- {!update?.enabled - ? "Vérification des mises à jour désactivée (UPDATE_CHECK=false)." - : !update.tags && !update.latest - ? "Registre npm injoignable pour l'instant : versions publiées inconnues." - : pending - ? <>À installer : {pending} - : "À jour pour ce canal."} -

- {cmd && ( - <> - - Excalidraw est compilé dans l'image : changer de canal ou de version ne prend effet - qu'après reconstruction, depuis le dossier du docker-compose : - -
- {cmd} - -
- - {copied - ? "Commande copiée." - : <>Pour conserver ce choix aux reconstructions suivantes, mettre EXCALIDRAW_VERSION={pending} dans .env. Tester l'image avant la mise en service : les versions 0.x peuvent changer d'API.} - - - )} -
- -
-

Poubelle

- Suppression définitive automatique -
- {TRASH_DAYS.map((d) => ( - - ))} -
- - {settings.trashDays - ? `Les éléments mis à la poubelle sont supprimés définitivement ${settings.trashDays} jour${settings.trashDays > 1 ? "s" : ""} après leur suppression.` - : "La poubelle garde tout jusqu'à ce qu'on la vide ; son icône indique si elle contient des éléments."} - -
- -
-

Liens de partage

- {shares?.length ? ( -
    - {shares.map((s) => ( -
  • -
    - {s.path} - -
    - - Créé le {new Date(s.created).toLocaleDateString(undefined, { dateStyle: "short" })} · {fmtExpiry(s)} - -
  • - ))} -
- ) : ( - - Aucun lien actif. Pour partager un schéma en lecture seule : icône de partage de la barre du haut, ou menu - « … » de l'explorateur. - - )} - {!!shares?.length && ( - - )} -
- - - {error &&
{error}
}
- - Notes de version -