Paramètres : un onglet par section, fenêtre dimensionnée sur l'écran
La boîte de dialogue empilait cinq sections dans 480 px de large et défilait verticalement (et horizontalement à cause des boutons de la poubelle). Elle passe à 760 × 480 px, bornée par la hauteur de la fenêtre, avec les sections en onglets à gauche (Apparence, Excalidraw, Poubelle, Liens de partage, API et MCP) ; boutons Fermer/Enregistrer toujours visibles. Sur mobile, les onglets passent en ligne au-dessus. L'onglet Excalidraw porte une pastille quand une version est à installer, l'onglet Partage le nombre de liens actifs ; l'icône de mise à jour ouvre directement l'onglet Excalidraw. Navigation au clavier par les flèches. Les onglets masqués restent montés (jeton créé, titre en cours de saisie conservés). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5Kam45ChmR5syg6igrpHZ
This commit is contained in:
1 parent
d052ac383b
commit
de81d99375
2 files changed
+342
-179
No files matched your search
+237
-176
@@ -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 { ApiAccess } from "./ApiAccess";
|
||||||
import { api, type Channel, type Settings, type Share, type UpdateStatus } from "./api";
|
import { api, type Channel, type Settings, type Share, type UpdateStatus } from "./api";
|
||||||
import { CopyIcon, TrashIcon } from "./icons";
|
import { CopyIcon, TrashIcon } from "./icons";
|
||||||
@@ -11,7 +11,7 @@ const MAX_TITLE = 80; // voir storage.MaxTitleLen
|
|||||||
interface Props {
|
interface Props {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
update: UpdateStatus | null;
|
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;
|
focusExcalidraw?: boolean;
|
||||||
onChange: (s: Settings) => void;
|
onChange: (s: Settings) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -34,6 +34,16 @@ const TRASH_DAYS = [
|
|||||||
{ days: 30, label: "30 jours" },
|
{ 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. */
|
/** Paramètres partagés par tous les utilisateurs : titre, logo, version d'Excalidraw. */
|
||||||
export function SettingsDialog({ settings, update, focusExcalidraw, onChange, onClose }: Props) {
|
export function SettingsDialog({ settings, update, focusExcalidraw, onChange, onClose }: Props) {
|
||||||
const [title, setTitle] = useState(settings.title);
|
const [title, setTitle] = useState(settings.title);
|
||||||
@@ -41,11 +51,12 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [shares, setShares] = useState<Share[] | null>(null);
|
const [shares, setShares] = useState<Share[] | null>(null);
|
||||||
|
const [tab, setTab] = useState<Tab>(focusExcalidraw ? "excalidraw" : "apparence");
|
||||||
const fileInput = useRef<HTMLInputElement>(null);
|
const fileInput = useRef<HTMLInputElement>(null);
|
||||||
const exSection = useRef<HTMLElement>(null);
|
const tabList = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusExcalidraw) exSection.current?.scrollIntoView({ block: "start" });
|
if (focusExcalidraw) setTab("excalidraw");
|
||||||
}, [focusExcalidraw]);
|
}, [focusExcalidraw]);
|
||||||
|
|
||||||
const loadShares = () =>
|
const loadShares = () =>
|
||||||
@@ -97,6 +108,16 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
|
|||||||
run(() => api.uploadLogo(f));
|
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<HTMLButtonElement>("[role=tab]")[i]?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
const dirty = title.trim() !== settings.title;
|
const dirty = title.trim() !== settings.title;
|
||||||
const pending = pendingVersion(update, settings.channel);
|
const pending = pendingVersion(update, settings.channel);
|
||||||
const cmd = pending ? rebuildCommand(pending) : null;
|
const cmd = pending ? rebuildCommand(pending) : null;
|
||||||
@@ -107,6 +128,16 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
|
|||||||
setTimeout(() => setCopied(false), 1500);
|
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 (
|
return (
|
||||||
<div className="modal-backdrop" onClick={onClose}>
|
<div className="modal-backdrop" onClick={onClose}>
|
||||||
<form
|
<form
|
||||||
@@ -122,186 +153,216 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
|
|||||||
>
|
>
|
||||||
<h3>Paramètres</h3>
|
<h3>Paramètres</h3>
|
||||||
|
|
||||||
<h4>Apparence</h4>
|
<div className="settings-body">
|
||||||
<label className="field">
|
<div className="settings-tabs" role="tablist" aria-label="Sections" ref={tabList} onKeyDown={onTabKey}>
|
||||||
<span>Titre affiché</span>
|
{TABS.map((t) => (
|
||||||
<input
|
<button
|
||||||
className="text-input"
|
key={t.id}
|
||||||
value={title}
|
id={`settings-tab-${t.id}`}
|
||||||
maxLength={MAX_TITLE}
|
type="button"
|
||||||
placeholder={DEFAULT_TITLE}
|
role="tab"
|
||||||
autoFocus
|
aria-selected={tab === t.id}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
aria-controls={`settings-${t.id}`}
|
||||||
/>
|
tabIndex={tab === t.id ? 0 : -1}
|
||||||
</label>
|
className={tab === t.id ? "on" : ""}
|
||||||
|
onClick={() => setTab(t.id)}
|
||||||
<div className="field">
|
>
|
||||||
<span>Logo</span>
|
{t.label}
|
||||||
<div className="logo-edit">
|
{t.id === "excalidraw" && pending && <i className="dot" title={`À installer : ${pending}`} />}
|
||||||
{settings.logo ? (
|
{t.id === "partage" && !!shares?.length && <small>{shares.length}</small>}
|
||||||
<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>
|
</button>
|
||||||
{settings.logo && (
|
))}
|
||||||
<button type="button" className="danger" disabled={busy} onClick={() => run(api.removeLogo)}>
|
</div>
|
||||||
Retirer
|
|
||||||
</button>
|
<div className="settings-panel">
|
||||||
)}
|
<section {...pane("apparence")}>
|
||||||
<small className="muted">PNG, recadré au centre et réduit à 64×64 px, affiché en rond.</small>
|
<label className="field">
|
||||||
|
<span>Titre affiché</span>
|
||||||
|
<input
|
||||||
|
className="text-input"
|
||||||
|
value={title}
|
||||||
|
maxLength={MAX_TITLE}
|
||||||
|
placeholder={DEFAULT_TITLE}
|
||||||
|
autoFocus={!focusExcalidraw}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<section {...pane("excalidraw")}>
|
||||||
|
<div className="field">
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<a className="link" href="https://github.com/excalidraw/excalidraw/releases" target="_blank" rel="noreferrer">
|
||||||
|
Notes de version
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section {...pane("poubelle")}>
|
||||||
|
<div className="field">
|
||||||
|
<span>Suppression définitive automatique</span>
|
||||||
|
<div className="radio-group" role="radiogroup" aria-label="Purge de la poubelle">
|
||||||
|
{TRASH_DAYS.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.days}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={settings.trashDays === d.days}
|
||||||
|
className={settings.trashDays === d.days ? "on" : ""}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => settings.trashDays !== d.days && run(() => api.setTrashDays(d.days))}
|
||||||
|
>
|
||||||
|
{d.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<small className="muted">
|
||||||
|
{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."}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section {...pane("partage")}>
|
||||||
|
<div className="field">
|
||||||
|
{shares?.length ? (
|
||||||
|
<ul className="share-list all">
|
||||||
|
{shares.map((s) => (
|
||||||
|
<li key={s.id}>
|
||||||
|
<div className="cmd">
|
||||||
|
<code title={api.shareUrl(s.token)}>{s.path}</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-btn danger"
|
||||||
|
onClick={() => revoke(() => api.revokeShare(s.id), `Révoquer le lien vers « ${s.path} » ?`)}
|
||||||
|
title="Révoquer"
|
||||||
|
aria-label={`Révoquer le lien vers ${s.path}`}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small className="muted">
|
||||||
|
Créé le {new Date(s.created).toLocaleDateString(undefined, { dateStyle: "short" })} · {fmtExpiry(s)}
|
||||||
|
</small>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<small className="muted">
|
||||||
|
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.
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{!!shares?.length && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="danger"
|
||||||
|
style={{ alignSelf: "flex-start" }}
|
||||||
|
onClick={() =>
|
||||||
|
revoke(
|
||||||
|
api.revokeAllShares,
|
||||||
|
"Révoquer tous les liens de partage ? La clé de signature est renouvelée : aucun lien existant ne fonctionnera plus.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tout révoquer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div {...pane("api")}>
|
||||||
|
<ApiAccess onError={setError} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
ref={fileInput}
|
|
||||||
type="file"
|
|
||||||
accept="image/png"
|
|
||||||
hidden
|
|
||||||
onChange={(e) => {
|
|
||||||
onFile(e.target.files?.[0]);
|
|
||||||
e.target.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
|
|
||||||
<section className="field">
|
|
||||||
<h4>Poubelle</h4>
|
|
||||||
<span>Suppression définitive automatique</span>
|
|
||||||
<div className="radio-group" role="radiogroup" aria-label="Purge de la poubelle">
|
|
||||||
{TRASH_DAYS.map((d) => (
|
|
||||||
<button
|
|
||||||
key={d.days}
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
aria-checked={settings.trashDays === d.days}
|
|
||||||
className={settings.trashDays === d.days ? "on" : ""}
|
|
||||||
disabled={busy}
|
|
||||||
onClick={() => settings.trashDays !== d.days && run(() => api.setTrashDays(d.days))}
|
|
||||||
>
|
|
||||||
{d.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<small className="muted">
|
|
||||||
{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."}
|
|
||||||
</small>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="field">
|
|
||||||
<h4>Liens de partage</h4>
|
|
||||||
{shares?.length ? (
|
|
||||||
<ul className="share-list all">
|
|
||||||
{shares.map((s) => (
|
|
||||||
<li key={s.id}>
|
|
||||||
<div className="cmd">
|
|
||||||
<code title={api.shareUrl(s.token)}>{s.path}</code>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="icon-btn danger"
|
|
||||||
onClick={() => revoke(() => api.revokeShare(s.id), `Révoquer le lien vers « ${s.path} » ?`)}
|
|
||||||
title="Révoquer"
|
|
||||||
aria-label={`Révoquer le lien vers ${s.path}`}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small className="muted">
|
|
||||||
Créé le {new Date(s.created).toLocaleDateString(undefined, { dateStyle: "short" })} · {fmtExpiry(s)}
|
|
||||||
</small>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<small className="muted">
|
|
||||||
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.
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{!!shares?.length && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="danger"
|
|
||||||
style={{ alignSelf: "flex-start" }}
|
|
||||||
onClick={() =>
|
|
||||||
revoke(
|
|
||||||
api.revokeAllShares,
|
|
||||||
"Révoquer tous les liens de partage ? La clé de signature est renouvelée : aucun lien existant ne fonctionnera plus.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Tout révoquer
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ApiAccess onError={setError} />
|
|
||||||
|
|
||||||
{error && <div className="error">{error}</div>}
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
<div className="modal-actions">
|
<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" />
|
<span className="spacer" />
|
||||||
<button type="button" onClick={onClose}>
|
<button type="button" onClick={onClose}>
|
||||||
Fermer
|
Fermer
|
||||||
|
|||||||
+105
-3
@@ -1059,11 +1059,113 @@ li.active > .row {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dialogue « Paramètres » */
|
/* Dialogue « Paramètres » : un onglet par section, une seule visible à la fois. Hauteur
|
||||||
|
fixe, bornée par la fenêtre, pour que la boîte ne change pas de taille d'un onglet à
|
||||||
|
l'autre ; seul le contenu de l'onglet défile en dernier recours, les boutons restent
|
||||||
|
visibles. Sur mobile, les onglets passent en ligne au-dessus du contenu et la boîte
|
||||||
|
prend la hauteur de l'onglet affiché. */
|
||||||
.modal.settings {
|
.modal.settings {
|
||||||
width: min(480px, 100%);
|
width: min(760px, 100%);
|
||||||
max-height: min(720px, 92dvh);
|
height: min(480px, calc(100dvh - 32px));
|
||||||
|
max-height: none;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.settings-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 170px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.settings-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding-right: 12px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.settings-tabs button {
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
.settings-tabs button.on {
|
||||||
|
background: var(--ex-primary-container);
|
||||||
|
color: var(--ex-on-primary-container);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.settings-tabs small {
|
||||||
|
margin-left: auto;
|
||||||
|
min-width: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--ex-surface-low);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.settings-tabs .dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
margin-left: auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--warn);
|
||||||
|
}
|
||||||
|
.settings-panel {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.settings-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.settings-pane[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.settings-pane h4 {
|
||||||
|
display: none; /* redondant avec l'onglet */
|
||||||
|
}
|
||||||
|
.settings .radio-group {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.settings .radio-group button {
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
.settings .field .link {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
.settings .modal-actions {
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.modal.settings {
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(100dvh - 32px);
|
||||||
|
}
|
||||||
|
.settings-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.settings-tabs {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 0 8px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.settings-tabs button {
|
||||||
|
width: auto;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.modal h4 {
|
.modal h4 {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
|
|||||||
Reference in new issue
Block a user