Poubelle par utilisateur (interface) : icône pleine/vide, restauration, vidage, réglage de la purge dans les paramètres

This commit is contained in:
claude Bot committed 2026-10-15 10:00:33 +02:00
1 parent 8a1417055b
commit d052ac383b
7 files changed
+261 -26

No files matched your search

+1 -1
View File
@@ -104,7 +104,7 @@ export default function App() {
const [loadError, setLoadError] = useState<string | null>(null);
const [drawerWidth, setDrawerWidth] = useState(initialDrawerWidth);
const [resizing, setResizing] = useState(false);
const [settings, setSettings] = useState<Settings>({ title: "", logo: "", channel: "latest" });
const [settings, setSettings] = useState<Settings>({ title: "", logo: "", channel: "latest", trashDays: 0 });
const updateStatus = useUpdateStatus();
const pending = pendingVersion(updateStatus, settings.channel);
const [customizing, setCustomizing] = useState<false | "all" | "excalidraw">(false);
+154 -20
View File
@@ -8,8 +8,10 @@ import {
displayPath,
EXT,
inFavoris,
inPoubelle,
join,
label,
POUBELLE,
stripExt,
withExt,
type Entry,
@@ -23,6 +25,7 @@ import {
DotsIcon,
DownloadIcon,
EditIcon,
FileIcon,
FilePlusIcon,
FolderIcon,
FolderPlusIcon,
@@ -32,12 +35,14 @@ import {
ListIcon,
MoveIcon,
RefreshIcon,
RestoreIcon,
SearchIcon,
SelectAllIcon,
ShareIcon,
StarFilledIcon,
StarIcon,
TagIcon,
TrashFullIcon,
TrashIcon,
UpIcon,
UploadIcon,
@@ -121,6 +126,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
const [tagFilter, setTagFilter] = useState<string | null>(null);
const [tagging, setTagging] = useState<Entry[] | null>(null); // éléments du dialogue « Tags »
const [iconing, setIconing] = useState<Entry[] | null>(null); // dossiers du dialogue « Icône »
const [trashDays, setTrashDays] = useState<number | null>(null); // purge automatique (0 : jamais)
const fileInput = useRef<HTMLInputElement>(null);
const dirInput = useRef<HTMLInputElement>(null);
const stay = useRef<string | null>(null); // schéma ouvert depuis Favoris : on reste dans Favoris
@@ -130,6 +136,9 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
const filtering = query.trim() !== "" || tagFilter !== null;
// Dossier Favoris : des liens vers les originaux, ni création ni déplacement.
const favView = !results && inFavoris(dir);
// Poubelle : éléments à restaurer ou à supprimer définitivement, rien d'autre.
const trashView = !results && inPoubelle(dir);
const locked = favView || trashView; // ni création, ni import, ni déplacement
const closeMenu = useCallback(() => setMenu(null), []);
// La liste change de forme : le bouton « … » d'origine disparaît.
@@ -196,6 +205,15 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
if (filtering) setResults(await api.search(query, filter));
};
// Délai de purge, affiché dans la poubelle (réglé dans les paramètres).
useEffect(() => {
if (!trashView) return;
api
.settings()
.then((s) => setTrashDays(s.trashDays))
.catch(() => setTrashDays(null));
}, [trashView, refreshKey]);
useEffect(() => {
try {
localStorage.setItem("explorerView", view);
@@ -235,6 +253,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
const onItemClick = (ev: MouseEvent, e: Entry) => {
if (!e.special) {
if (ev.shiftKey) return selectRange(e.path);
if (trashView) return toggle(e.path); // un élément de la poubelle ne s'ouvre pas
// Ctrl/Cmd+clic, ou clic quand une sélection est en cours (mode sélection tactile).
if (ev.ctrlKey || ev.metaKey || selected.size > 0) return toggle(e.path);
}
@@ -362,8 +381,14 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
? `${plural(paths.length, "élément")} (dossiers compris avec leur contenu)`
: one?.isDir
? `le dossier « ${one.name} » et tout son contenu`
: `« ${basename(paths[0])} »`;
if (!confirm(`Mettre ${what} à la corbeille ?`)) return;
: `« ${stripExt(one?.name ?? basename(paths[0]))} »`;
if (trashView) {
if (!confirm(`Supprimer définitivement ${what} ? Cette action est irréversible.`)) return;
for (const p of paths) await api.remove(p, true);
clearSelection();
return reload();
}
if (!confirm(`Mettre ${what} à la poubelle ?`)) return;
for (const p of paths) {
await api.remove(p, true);
onDeleted(p);
@@ -372,6 +397,36 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
await reload();
});
/** Remet des éléments de la poubelle à leur emplacement d'origine. */
const restoreMany = (paths: string[]) =>
guard(async () => {
const res = await api.restoreTrash(paths);
const failed = res.filter((r) => r.error);
const done = res.filter((r) => r.to);
clearSelection();
await reload();
if (done.length) {
setNotice({
text: `${plural(done.length, "élément")} restauré${done.length > 1 ? "s" : ""}`,
detail: done.map((r) => displayPath(r.to!)).join("\n"),
});
}
if (failed.length) {
const name = (p: string) => stripExt(list.find((e) => e.path === p)?.name ?? basename(p));
setError(
`${plural(failed.length, "élément")} non restauré${failed.length > 1 ? "s" : ""} — ${failed.map((r) => `${name(r.from)} : ${r.error}`).join(" ; ")}`,
);
}
});
const emptyTrash = () =>
guard(async () => {
if (!confirm(`Vider la poubelle (${plural(list.length, "élément")}) ? Cette action est irréversible.`)) return;
await api.remove(POUBELLE, true);
clearSelection();
await reload();
});
/**
* Importe fichiers et arborescences dans targetDir : les dossiers sont recréés,
* seuls les .excalidraw sont importés, les schémas déjà présents sont conservés.
@@ -442,7 +497,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
onDragOver: (ev: DragEvent) => {
const external = hasFiles(ev.dataTransfer);
if (!external && !ev.dataTransfer.types.includes(DND_TYPE)) return;
if (external && inFavoris(targetDir)) return; // pas d'import dans Favoris
if (external && (inFavoris(targetDir) || inPoubelle(targetDir))) return; // pas d'import dans Favoris ni la poubelle
ev.preventDefault();
ev.dataTransfer.dropEffect = external ? "copy" : inFavoris(targetDir) ? "link" : "move";
setDropTarget(targetDir);
@@ -461,6 +516,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
return;
}
if (inFavoris(targetDir)) addFavorites(paths);
else if (inPoubelle(targetDir)) removeMany(paths);
else moveMany(paths, targetDir);
},
});
@@ -468,13 +524,15 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
// --- Rendu ---------------------------------------------------------------
const crumbs = dir ? dir.split("/") : [];
const importDir = results || favView ? "" : dir; // pendant une recherche, le dossier courant n'est pas affiché
const importDir = results || locked ? "" : dir; // pendant une recherche, le dossier courant n'est pas affiché
const where = (p: string) => displayPath(dirname(p)) || "Racine";
const deletedInfo = (e: Entry) =>
`Supprimé le ${fmtDate(e.modTime)}${e.by ? ` par ${e.by}` : ""}\nEmplacement d'origine : ${e.origin ? where(e.origin) : "inconnu"}`;
// Dépôt n'importe où dans l'explorateur : import dans le dossier courant.
const zoneProps = {
onDragOver: (ev: DragEvent) => {
if (favView || !hasFiles(ev.dataTransfer)) return;
if (locked || !hasFiles(ev.dataTransfer)) return;
ev.preventDefault();
ev.dataTransfer.dropEffect = "copy";
setFileDrag(true);
@@ -483,7 +541,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
if (!ev.currentTarget.contains(ev.relatedTarget as Node | null)) setFileDrag(false);
},
onDrop: (ev: DragEvent) => {
if (favView || !hasFiles(ev.dataTransfer)) return;
if (locked || !hasFiles(ev.dataTransfer)) return;
ev.preventDefault();
setFileDrag(false);
importTree(collectDrop(ev.dataTransfer), importDir);
@@ -493,8 +551,22 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
const big = view === "grid" ? 40 : 20;
const icon = (e: Entry) =>
e.special ? (
<span className={`icon special ${e.special}`} aria-hidden>
{e.special === "favoris" ? <StarFilledIcon size={big} /> : <UserIcon size={big} />}
<span className={`icon special ${e.special} ${e.count ? "full" : ""}`} aria-hidden>
{e.special === "favoris" ? (
<StarFilledIcon size={big} />
) : e.special === "poubelle" ? (
e.count ? (
<TrashFullIcon size={big} />
) : (
<TrashIcon size={big} />
)
) : (
<UserIcon size={big} />
)}
</span>
) : trashView ? (
<span className="icon" aria-hidden>
{e.isDir ? <FolderIcon size={big} /> : <FileIcon size={big} />}
</span>
) : e.broken ? (
<span className="icon folder" aria-hidden>
@@ -543,6 +615,22 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
</>
);
// Dans la poubelle : restauration ou suppression définitive.
const trashActions = (e: Entry) => (
<>
<button role="menuitem" onClick={() => restoreMany([e.path])}>
<RestoreIcon />
Restaurer
</button>
<hr />
<button role="menuitem" className="danger" onClick={() => removeMany([e.path])}>
<TrashIcon />
Supprimer définitivement
</button>
<span className="muted size deleted">{deletedInfo(e)}</span>
</>
);
const actions = (e: Entry) => (
<>
<button role="menuitem" onClick={() => rename(e)}>
@@ -617,7 +705,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
<li key={e.path} className={cls}>
<div
className="row"
draggable={!e.special && !favView}
draggable={!e.special && !locked}
onDragStart={(ev) => onDragStart(ev, e)}
{...(e.isDir ? dropProps(e.path) : {})}
>
@@ -640,7 +728,17 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
<button
className="name"
onClick={(ev) => onItemClick(ev, e)}
title={e.broken ? "Original introuvable : retirer ce favori" : displayPath(e.path)}
title={
e.broken
? "Original introuvable : retirer ce favori"
: trashView
? deletedInfo(e)
: e.special === "poubelle"
? e.count
? `Poubelle : ${plural(e.count, "élément")}`
: "Poubelle vide"
: displayPath(e.path)
}
>
{icon(e)}
<span className="label">
@@ -650,7 +748,9 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
</span>
)}
{e.isDir ? e.name : stripExt(e.name)}
{e.special === "poubelle" && !!e.count && <small className="badge">{e.count}</small>}
{(results || (favView && !e.broken)) && <small className="muted where"> {where(e.path)}</small>}
{trashView && <small className="muted where"> {e.origin ? where(e.origin) : ""}</small>}
{e.snippet && <small className="snippet">{e.snippet}</small>}
</span>
</button>
@@ -668,7 +768,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
))}
</span>
)}
{!e.isDir && view === "list" && <span className="meta muted">{fmtDate(e.modTime)}</span>}
{(!e.isDir || trashView) && view === "list" && <span className="meta muted">{fmtDate(e.modTime)}</span>}
{!e.special && (
<button
className={`icon-btn more ${menu?.entry.path === e.path ? "open" : ""}`}
@@ -692,18 +792,18 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
return (
<div className={`explorer ${fileDrag ? "file-drag" : ""}`} onKeyDown={onKeyDown} {...zoneProps}>
<div className="toolbar">
<button className="ex-btn" onClick={newDrawing} title="Nouveau schéma" disabled={favView}>
<button className="ex-btn" onClick={newDrawing} title="Nouveau schéma" disabled={locked}>
<FilePlusIcon />
<span>Schéma</span>
</button>
<button className="ex-btn" onClick={newFolder} title="Nouveau dossier" disabled={favView}>
<button className="ex-btn" onClick={newFolder} title="Nouveau dossier" disabled={locked}>
<FolderPlusIcon />
<span>Dossier</span>
</button>
<button
className="ex-btn"
onClick={() => fileInput.current?.click()}
disabled={favView}
disabled={locked}
title="Importer des fichiers .excalidraw (ou glisser-déposer fichiers et dossiers ici)"
>
<UploadIcon />
@@ -712,7 +812,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
<button
className="ex-btn square"
onClick={() => dirInput.current?.click()}
disabled={favView}
disabled={locked}
title="Importer un dossier complet (arborescence recréée, seuls les .excalidraw sont importés)"
aria-label="Importer un dossier"
>
@@ -812,6 +912,22 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
</nav>
)}
{trashView && (
<div className="trashbar">
<small className="muted">
{trashDays === null
? ""
: trashDays === 0
? "Conservés jusqu'à suppression définitive"
: `Supprimés définitivement après ${plural(trashDays, "jour")}`}
</small>
<button className="ex-btn danger" onClick={emptyTrash} disabled={list.length === 0}>
<TrashIcon />
<span>Vider la poubelle</span>
</button>
</div>
)}
{selected.size > 0 && (
<div className="selbar">
<button className="icon-btn" onClick={clearSelection} aria-label="Annuler la sélection" title="Annuler la sélection (Échap)">
@@ -826,6 +942,13 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
>
{selected.size === selectable.length ? <DeselectIcon /> : <SelectAllIcon />}
</button>
{trashView && (
<button className="ex-btn" onClick={() => restoreMany([...selected])}>
<RestoreIcon />
<span>Restaurer</span>
</button>
)}
{!trashView && (
<button
className="icon-btn"
onClick={() => toggleFavorite(list.filter((e) => selected.has(e.path)))}
@@ -834,6 +957,8 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
>
<StarIcon />
</button>
)}
{!trashView && (
<button
className="icon-btn"
onClick={() => setTagging(list.filter((e) => selected.has(e.path)))}
@@ -842,7 +967,8 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
>
<TagIcon />
</button>
{!favView && (
)}
{!locked && (
<button className="ex-btn" onClick={() => setMoving([...selected])}>
<MoveIcon />
<span>Déplacer…</span>
@@ -851,8 +977,10 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
<button
className="icon-btn danger"
onClick={() => removeMany([...selected])}
title={favView ? "Retirer des favoris (Suppr)" : "Mettre à la corbeille (Suppr)"}
aria-label={favView ? "Retirer des favoris" : "Supprimer"}
title={
favView ? "Retirer des favoris (Suppr)" : trashView ? "Supprimer définitivement (Suppr)" : "Mettre à la poubelle (Suppr)"
}
aria-label={favView ? "Retirer des favoris" : trashView ? "Supprimer définitivement" : "Supprimer"}
>
<TrashIcon />
</button>
@@ -887,7 +1015,13 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
{list.map(item)}
{list.length === 0 && (
<li className="muted empty">
{results ? "Aucun résultat" : favView ? "Aucun favori : étoile ou glisser-déposer un schéma ici" : "Dossier vide"}
{results
? "Aucun résultat"
: favView
? "Aucun favori : étoile ou glisser-déposer un schéma ici"
: trashView
? "La poubelle est vide"
: "Dossier vide"}
</li>
)}
</ul>
@@ -913,7 +1047,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi
{menu && (
<ActionMenu anchor={menu.anchor} onClose={closeMenu}>
{favView ? favActions(menu.entry) : actions(menu.entry)}
{favView ? favActions(menu.entry) : trashView ? trashActions(menu.entry) : actions(menu.entry)}
</ActionMenu>
)}
+1 -1
View File
@@ -21,7 +21,7 @@ export function MoveDialog({ paths, startDir, onCancel, onConfirm }: Props) {
setError(null);
api
.list(dir)
.then((es) => live && setFolders(es.filter((e) => e.isDir && e.special !== "favoris")))
.then((es) => live && setFolders(es.filter((e) => e.isDir && e.special !== "favoris" && e.special !== "poubelle")))
.catch((e) => live && setError(e.message));
return () => {
live = false;
+33
View File
@@ -26,6 +26,14 @@ const CHANNELS: { id: Channel; label: string; hint: string }[] = [
},
];
/** Délais de purge de la poubelle (0 : jamais). */
const TRASH_DAYS = [
{ days: 0, label: "Jamais" },
{ days: 1, label: "1 jour" },
{ days: 7, label: "7 jours" },
{ days: 30, label: "30 jours" },
];
/** 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);
@@ -214,6 +222,31 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
)}
</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 ? (
+22 -4
View File
@@ -10,8 +10,14 @@ export interface Entry {
favorite?: boolean;
/** Dossier : icône choisie (voir folderIcons.tsx). */
icon?: string;
/** Entrée virtuelle de la racine : "favoris" ou "perso" (espace personnel). */
special?: "favoris" | "perso";
/** Entrée virtuelle de la racine : "favoris", "perso" (espace personnel) ou "poubelle". */
special?: "favoris" | "perso" | "poubelle";
/** Entrée « poubelle » de la racine : nombre d'éléments qu'elle contient. */
count?: number;
/** Élément de la poubelle : chemin d'origine (modTime = date de suppression). */
origin?: string;
/** Élément de la poubelle : supprimé par (vide si inconnu). */
by?: string;
/** Dans le dossier Favoris : nom du lien (le chemin est celui de l'original). */
link?: string;
/** Favori dont l'original a disparu (chemin .favoris/<lien>, à retirer). */
@@ -43,11 +49,12 @@ export interface MoveResult {
/** Canal npm d'Excalidraw : latest (stable) ou next (préversion). */
export type Channel = "latest" | "next";
/** Paramètres partagés ; logo = "" sans logo, sinon sa version. */
/** Paramètres partagés ; logo = "" sans logo, sinon sa version ; trashDays = 0 : poubelle jamais purgée. */
export interface Settings {
title: string;
logo: string;
channel: Channel;
trashDays: number;
}
/** Versions d'Excalidraw publiées sur npm par canal (vide si inconnu ou désactivé). */
@@ -184,10 +191,15 @@ export const api = {
copy: async (from: string, to: string): Promise<Entry> =>
(await request("/api/copy", json({ from, to }))).json(),
/** Met à la poubelle ; dans .poubelle, supprime définitivement (path = .poubelle : la vide). */
remove: async (path: string, recursive: boolean): Promise<void> => {
await request(`/api/entry?path=${q(path)}&recursive=${recursive}`, { method: "DELETE" });
},
/** Remet des éléments de la poubelle à leur emplacement d'origine (to : chemin restauré). */
restoreTrash: async (paths: string[]): Promise<MoveResult[]> =>
(await request("/api/trash/restore", json({ paths }))).json(),
history: async (path: string): Promise<Version[]> =>
(await request(`/api/history?path=${q(path)}`)).json(),
@@ -253,6 +265,10 @@ export const api = {
setChannel: async (channel: Channel): Promise<Settings> =>
(await request("/api/settings", { ...json({ channel }), method: "PUT" })).json(),
/** days = 0 : la poubelle n'est jamais purgée. */
setTrashDays: async (trashDays: number): Promise<Settings> =>
(await request("/api/settings", { ...json({ trashDays }), method: "PUT" })).json(),
/** Le serveur recadre au centre et ramène le PNG à 64x64. */
uploadLogo: async (png: Blob): Promise<Settings> =>
(
@@ -285,12 +301,14 @@ export const join = (dir: string, name: string) => (dir ? `${dir}/${name}` : nam
// Dossiers propres à l'utilisateur connecté (voir internal/storage/spaces.go).
export const PERSO = ".perso";
export const FAVORIS = ".favoris";
const SPECIAL_LABELS: Record<string, string> = { [PERSO]: "Personnel", [FAVORIS]: "Favoris" };
export const POUBELLE = ".poubelle";
const SPECIAL_LABELS: Record<string, string> = { [PERSO]: "Personnel", [FAVORIS]: "Favoris", [POUBELLE]: "Poubelle" };
/** Libellé d'un composant de chemin (".perso" → "Personnel"). */
export const label = (part: string) => SPECIAL_LABELS[part] ?? part;
/** Chemin affiché : "Personnel/projets" pour ".perso/projets". */
export const displayPath = (p: string) => p.split("/").map(label).join("/");
export const inFavoris = (p: string) => p === FAVORIS || p.startsWith(FAVORIS + "/");
export const inPoubelle = (p: string) => p === POUBELLE || p.startsWith(POUBELLE + "/");
export const EXT = ".excalidraw";
export const withExt = (name: string) => (name.toLowerCase().endsWith(EXT) ? name : name + EXT);
export const stripExt = (name: string) =>
+18
View File
@@ -112,6 +112,24 @@ export const TrashIcon = icon(
<path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3" />,
);
/** Poubelle qui contient des éléments : une feuille et une boule de papier dépassent du bord. */
export const TrashFullIcon = icon(
<>
<path d="M4 10h16M5 10l1 9a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-9M10 14v3M14 14v3" />
<path d="M7.5 10 9 3.5l4.5 1-1.2 5.5M10.2 6.5l2 .5" />
<circle cx="16" cy="7.8" r="2.2" />
</>,
);
export const RestoreIcon = icon(<path d="M9 14l-4-4 4-4M5 10h11a4 4 0 1 1 0 8h-1" />);
export const FileIcon = icon(
<>
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z" />
</>,
);
export const SelectAllIcon = icon(
<>
<rect x="4" y="4" width="16" height="16" rx="2" />
+32
View File
@@ -1526,3 +1526,35 @@ button.chip.on,
text-align: center;
line-height: 1.2;
}
/* Poubelle : icône pleine ou vide, nombre d'éléments, barre « Vider ». */
.icon.special.poubelle {
color: var(--muted);
}
.icon.special.poubelle.full {
color: var(--accent);
}
.row .label .badge {
display: inline-block;
min-width: 18px;
margin-left: 6px;
padding: 0 5px;
border-radius: 9px;
background: var(--accent-soft);
color: var(--accent);
font-size: 11px;
line-height: 18px;
text-align: center;
vertical-align: 1px;
}
.trashbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0 6px;
}
.menu-popup .size.deleted {
max-width: 260px;
white-space: pre-line;
}