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;