From 8e0b5ea0d9542fb7f430c6c95c8e8e374dadc970 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 14 Oct 2026 13:40:06 +0200 Subject: [PATCH] =?UTF-8?q?Explorateur=20:=20actions=20d'un=20=C3=A9l?= =?UTF-8?q?=C3=A9ment=20dans=20un=20menu=20d=C3=A9roulant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le bouton « … » en bout de ligne (ou sur la vignette) ouvre un menu flottant aligné sur lui, au lieu d'insérer la barre d'actions sous l'élément. Le menu se referme au choix d'une action, par Échap, clic ailleurs, défilement ou redimensionnement ; flèches haut/bas pour naviguer. Icônes sur chaque action. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017AW78w4N95h6PXiCResh2v --- frontend/src/ActionMenu.tsx | 1 + frontend/src/Explorer.tsx | 121 +++++++++++++++++++++++++----------- frontend/src/icons.tsx | 2 + frontend/src/styles.css | 65 +++++++++++++++++-- 4 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 frontend/src/ActionMenu.tsx diff --git a/frontend/src/ActionMenu.tsx b/frontend/src/ActionMenu.tsx new file mode 100644 index 0000000..311c8dd --- /dev/null +++ b/frontend/src/ActionMenu.tsx @@ -0,0 +1 @@ +PLACEHOLDER \ No newline at end of file diff --git a/frontend/src/Explorer.tsx b/frontend/src/Explorer.tsx index 0250634..66e1577 100644 --- a/frontend/src/Explorer.tsx +++ b/frontend/src/Explorer.tsx @@ -19,17 +19,22 @@ import { import { CloseIcon, DeselectIcon, + CopyIcon, DotsIcon, + DownloadIcon, + EditIcon, FilePlusIcon, FolderIcon, FolderPlusIcon, FolderUploadIcon, GridIcon, + HistoryIcon, ListIcon, MoveIcon, RefreshIcon, SearchIcon, SelectAllIcon, + ShareIcon, StarFilledIcon, StarIcon, TagIcon, @@ -38,6 +43,7 @@ import { UploadIcon, UserIcon, } from "./icons"; +import { ActionMenu } from "./ActionMenu"; import { collectDrop, collectFiles, hasFiles, type Collected } from "./importer"; import { MoveDialog } from "./MoveDialog"; import { TagsDialog } from "./TagsDialog"; @@ -102,7 +108,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi const [view, setView] = useState(initialView); const [selected, setSelected] = useState>(new Set()); const [anchor, setAnchor] = useState(null); // origine d'une sélection Maj+clic - const [menuFor, setMenuFor] = useState(null); + const [menu, setMenu] = useState<{ entry: Hit; anchor: HTMLElement } | null>(null); // menu « … » ouvert const [moving, setMoving] = useState(null); // éléments du dialogue « Déplacer vers » const [dropTarget, setDropTarget] = useState(null); const [error, setError] = useState(null); @@ -122,6 +128,10 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi // Dossier Favoris : des liens vers les originaux, ni création ni déplacement. const favView = !results && inFavoris(dir); + const closeMenu = useCallback(() => setMenu(null), []); + // La liste change de forme : le bouton « … » d'origine disparaît. + useEffect(closeMenu, [view, dir, results, closeMenu]); + const guard = useCallback(async (fn: () => Promise) => { setError(null); try { @@ -139,7 +149,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi setTags(tagList); // Tag filtré qui n'existe plus : filtre retiré. setTagFilter((t) => (t && tagList.some((x) => x.name === t) ? t : null)); - setMenuFor(null); + setMenu(null); // Oublie les éléments sélectionnés qui ont disparu. setSelected((s) => { if (s.size === 0) return s; @@ -309,7 +319,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi files.map((e) => e.path), fav, ); - setMenuFor(null); + setMenu(null); if (favView) clearSelection(); await reload(); }); @@ -497,55 +507,90 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi // Dans Favoris : actions sur l'original, retrait du lien à la place de la suppression. const favActions = (e: Entry) => ( -
+ <> {!e.broken && ( <> - - - - + + + - + + Télécharger +
)} - -
+ ); const actions = (e: Entry) => ( -
- - - {!e.isDir && } - {!e.isDir && } - {!e.isDir && } + <> + + {!e.isDir && ( - + )} + {!e.isDir && ( + + )} + {!e.isDir && ( + + )} + {!e.isDir && ( + )} - {!e.isDir && ( - + + Télécharger )} - - {!e.isDir && {fmtSize(e.size)}} -
+ {!e.isDir && {fmtSize(e.size)}} + ); const item = (e: Hit) => { @@ -559,7 +604,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi e.broken ? "broken" : "", dropTarget === e.path ? "drop" : "", ].join(" "); - return [ + return (
  • {fmtDate(e.modTime)}} {!e.special && ( )}
    - {view === "list" && menuFor === e.path && (favView ? favActions(e) : actions(e))} -
  • , - // En grille, le menu occupe toute la largeur sous la vignette. - view === "grid" && menuFor === e.path && ( -
  • - {favView ? favActions(e) : actions(e)} -
  • - ), - ]; + + ); }; return ( @@ -832,7 +875,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi )} - {list.flatMap(item)} + {list.map(item)} {list.length === 0 && (
  • {results ? "Aucun résultat" : favView ? "Aucun favori : étoile ou glisser-déposer un schéma ici" : "Dossier vide"} @@ -857,6 +900,12 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHi /> )} + {menu && ( + + {favView ? favActions(menu.entry) : actions(menu.entry)} + + )} + {moving && ( , ); +export const EditIcon = icon(); + export const HistoryIcon = icon( <> diff --git a/frontend/src/styles.css b/frontend/src/styles.css index dc0c4fa..06e87cf 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -550,6 +550,64 @@ li.active > .row { height: var(--ex-btn); } +/* Menu « … » d'un élément : liste déroulante, comme les menus d'Excalidraw. */ +.icon-btn.more.open { + background: var(--ex-surface-high); +} +.menu-popup { + position: fixed; + z-index: 40; /* au-dessus du tiroir et du panneau flottant, sous les dialogues */ + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: stretch; + gap: 2px; + min-width: 190px; + max-height: calc(100vh - 16px); + overflow-y: auto; + padding: 6px; + background: var(--panel); + border-radius: var(--ex-radius-lg); + box-shadow: var(--ex-shadow-island), 0 0 0 1px var(--border); +} +.menu-popup button, +.menu-popup a { + justify-content: flex-start; + width: 100%; + padding: 0 0.625rem; + font-size: 0.8125rem; + background: transparent; + box-shadow: none; +} +.menu-popup button:hover:not(:disabled), +.menu-popup a:hover, +.menu-popup button:focus-visible, +.menu-popup a:focus-visible { + background: var(--ex-surface-high); + outline: none; +} +.menu-popup .danger:hover:not(:disabled), +.menu-popup .danger:focus-visible { + background: var(--ex-danger-bg); +} +.menu-popup hr { + width: 100%; + margin: 4px 0; + border: none; + border-top: 1px solid var(--border); +} +.menu-popup .size { + padding: 4px 0.625rem 0; + font-size: 11px; +} +@media (hover: none) { + /* Tactile : cibles plus hautes. */ + .menu-popup button, + .menu-popup a { + height: 2.5rem; + } +} + /* --- Explorateur non épinglé (bureau) : panneau flottant sur le canevas ------ */ .peek-edge { @@ -759,8 +817,7 @@ li.active > .row { gap: 6px; align-content: start; } -.entries.grid > .empty, -.entries.grid > .menu-row { +.entries.grid > .empty { grid-column: 1 / -1; } .entries.grid .row { @@ -819,6 +876,7 @@ li.active > .row { opacity: 0; } .entries.grid .item:hover .more, +.entries.grid .more.open, .entries.grid .more:focus-visible { opacity: 1; } @@ -827,9 +885,6 @@ li.active > .row { opacity: 1; } } -.entries.grid .actions { - padding: 4px 2px 8px; -} .drag-ghost { position: fixed;