Paramètres : choix du canal d'Excalidraw (stable ou préversion)

- Dialogue « Paramètres » (icône engrenage) : sections Apparence (titre, logo)
  et Excalidraw (canal latest/next, versions publiées, commande de
  reconstruction ; rappel qu'Excalidraw est compilé dans l'image).
- L'icône de mise à jour suit le canal choisi et compare par égalité (les
  numéros next sont inférieurs à la dernière stable) ; elle ouvre les
  paramètres sur la section Excalidraw.
- Compatibilité avec le canal next : la prop excalidrawAPI y devient
  onExcalidrawAPI, les deux sont passées (image next construite et testée).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EuvvuTv7AUc8ksDHv8VKx
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-07 16:02:09 +02:00
1 parent 7da52b6755
commit 88f7750577
9 files changed
+361 -255

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
import { useEffect, useRef, useState } from "react";
import { api, type Channel, type Settings, type UpdateStatus } from "./api";
import { CopyIcon } from "./icons";
import { INSTALLED, pendingVersion, rebuildCommand } from "./update";
export const DEFAULT_TITLE = "Excalidraw";
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. */
focusExcalidraw?: boolean;
onChange: (s: Settings) => void;
onClose: () => void;
}
const CHANNELS: { id: Channel; label: string; hint: string }[] = [
{ id: "latest", label: "Stable", hint: "latest : versions publiées officiellement (recommandé)." },
{
id: "next",
label: "Préversion",
hint: "next : construite à chaque modification de la branche principale d'Excalidraw. Nouveautés en avance, stabilité non garantie. Son numéro (0.18.0-<commit>) est inférieur à la dernière stable alors que le code est plus récent.",
},
];
/** 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);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const fileInput = useRef<HTMLInputElement>(null);
const exSection = useRef<HTMLElement>(null);
useEffect(() => {
if (focusExcalidraw) exSection.current?.scrollIntoView({ block: "start" });
}, [focusExcalidraw]);
useEffect(() => {
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
const run = async (op: () => Promise<Settings>, close = false) => {
setBusy(true);
setError(null);
try {
onChange(await op());
if (close) onClose();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setBusy(false);
}
};
const onFile = (f: File | undefined) => {
if (!f) return;
if (f.type !== "image/png") {
setError("Seuls les fichiers PNG sont acceptés.");
return;
}
run(() => api.uploadLogo(f));
};
const dirty = title.trim() !== settings.title;
const pending = pendingVersion(update, settings.channel);
const cmd = pending ? rebuildCommand(pending) : null;
const copy = () =>
cmd &&
navigator.clipboard?.writeText(cmd).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
return (
<div className="modal-backdrop" onClick={onClose}>
<form
className="modal customize settings"
role="dialog"
aria-modal="true"
aria-label="Paramètres"
onClick={(e) => e.stopPropagation()}
onSubmit={(e) => {
e.preventDefault();
run(() => api.setTitle(title), true);
}}
>
<h3>Paramètres</h3>
<h4>Apparence</h4>
<label className="field">
<span>Titre affiché</span>
<input
className="text-input"
value={title}
maxLength={MAX_TITLE}
placeholder={DEFAULT_TITLE}
autoFocus
onChange={(e) => setTitle(e.target.value)}
/>
</label>
<div className="field">
<span>Logo</span>
<div className="logo-edit">
{settings.logo ? (
<img className="logo big" src={api.logoUrl(settings.logo)} alt="Logo actuel" />
) : (
<span className="logo big placeholder-logo muted" aria-hidden>
?
</span>
)}
<div className="logo-actions">
<button type="button" disabled={busy} onClick={() => fileInput.current?.click()}>
{settings.logo ? "Remplacer…" : "Choisir un PNG…"}
</button>
{settings.logo && (
<button type="button" className="danger" disabled={busy} onClick={() => run(api.removeLogo)}>
Retirer
</button>
)}
<small className="muted">PNG, recadré au centre et réduit à 64×64 px, affiché en rond.</small>
</div>
<input
ref={fileInput}
type="file"
accept="image/png"
hidden
onChange={(e) => {
onFile(e.target.files?.[0]);
e.target.value = "";
}}
/>
</div>
</div>
<section className="field" ref={exSection}>
<h4>Excalidraw</h4>
<span>Version à suivre</span>
<div className="radio-group" role="radiogroup" aria-label="Canal d'Excalidraw">
{CHANNELS.map((c) => (
<button
key={c.id}
type="button"
role="radio"
aria-checked={settings.channel === c.id}
className={settings.channel === c.id ? "on" : ""}
disabled={busy}
title={c.hint}
onClick={() => settings.channel !== c.id && run(() => api.setChannel(c.id))}
>
{c.label}
<small>{update?.tags?.[c.id] ?? "?"}</small>
</button>
))}
</div>
<small className="muted">{CHANNELS.find((c) => c.id === settings.channel)?.hint}</small>
<p className="versions">
Version installée : <b>{INSTALLED}</b>
<br />
{!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 : <b>{pending}</b></>
: "À jour pour ce canal."}
</p>
{cmd && (
<>
<small className="muted">
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 :
</small>
<div className="cmd">
<code>{cmd}</code>
<button type="button" className="icon-btn" onClick={copy} title="Copier" aria-label="Copier la commande">
<CopyIcon />
</button>
</div>
<small className="muted">
{copied
? "Commande copiée."
: <>Pour conserver ce choix aux reconstructions suivantes, mettre <code>EXCALIDRAW_VERSION={pending}</code> dans <code>.env</code>. Tester l'image avant la mise en service : les versions 0.x peuvent changer d'API.</>}
</small>
</>
)}
</section>
{error && <div className="error">{error}</div>}
<div className="modal-actions">
<a className="link" href="https://github.com/excalidraw/excalidraw/releases" target="_blank" rel="noreferrer">
Notes de version
</a>
<span className="spacer" />
<button type="button" onClick={onClose}>
Fermer
</button>
<button type="submit" className="primary" disabled={busy || !dirty}>
Enregistrer le titre
</button>
</div>
</form>
</div>
);
}