From 6810472f097f568d9d2d16eb7558122436c546d0 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 7 Oct 2026 15:46:25 +0200 Subject: [PATCH] =?UTF-8?q?Interface=20align=C3=A9e=20sur=20Excalidraw,=20?= =?UTF-8?q?import=20de=20dossiers,=20ic=C3=B4ne=20de=20mise=20=C3=A0=20jou?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jetons de couleur, rayons, ombres, police (Assistant) et tailles de boutons repris d'Excalidraw 0.18, en clair et en sombre. - Boutons de l'explorateur et de la barre de titre au style du bouton « Bibliothèque », icônes au trait (jeu Tabler) à la place des glyphes. - Glisser-déposer de fichiers et dossiers du système dans l'explorateur ou sur un dossier : arborescence recréée, seuls les .excalidraw importés, schémas existants conservés, bilan affiché. Bouton « Importer un dossier ». - Icône dans la barre du haut quand une version d'Excalidraw plus récente que celle embarquée (__EXCALIDRAW_VERSION__) est publiée ; un clic donne la commande de reconstruction. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_019EuvvuTv7AUc8ksDHv8VKx --- frontend/src/App.tsx | 29 ++++-- frontend/src/CustomizeDialog.tsx | 2 +- frontend/src/MoveDialog.tsx | 11 +- frontend/src/UpdateBadge.tsx | 99 ++++++++++++++++++ frontend/src/api.ts | 9 ++ frontend/src/icons.tsx | 166 ++++++++++++++++++++++++++----- frontend/src/importer.ts | 91 +++++++++++++++++ frontend/src/vite-env.d.ts | 3 + frontend/vite.config.ts | 12 ++- 9 files changed, 387 insertions(+), 35 deletions(-) create mode 100644 frontend/src/UpdateBadge.tsx create mode 100644 frontend/src/importer.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e2559b3..4076005 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,8 +17,9 @@ import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/ty import { api, ApiError, basename, stripExt, type Settings } from "./api"; import { CustomizeDialog, DEFAULT_TITLE } from "./CustomizeDialog"; import { Explorer } from "./Explorer"; -import { MoonIcon, SettingsIcon, SunIcon } from "./icons"; +import { CloseIcon, MenuIcon, MoonIcon, SaveIcon, SettingsIcon, SunIcon } from "./icons"; import { Splitter } from "./Splitter"; +import { UpdateBadge } from "./UpdateBadge"; import { storeThumb } from "./thumbs"; type Theme = "light" | "dark"; @@ -338,11 +339,13 @@ export default function App() { >
- )}
+ ))} {folders.length === 0 &&
  • Aucun sous-dossier
  • }
    - + + {open && ( +
    setOpen(false)}> +
    e.stopPropagation()} + > +

    Mise à jour d'Excalidraw disponible

    +

    + Version installée : {CURRENT} +
    + Dernière version publiée : {latest} +

    +

    Reconstruire et relancer le conteneur depuis le dossier du docker-compose :

    +
    + {cmd} + +
    +

    + {copied + ? "Commande copiée." + : "Les versions 0.x d'Excalidraw peuvent changer d'API : tester l'image avant la mise en service. Pour figer la version dans le dépôt, voir « Mettre à jour Excalidraw » dans le README."} +

    +
    + + + Notes de version + + +
    +
    +
    + )} + + ); +} diff --git a/frontend/src/api.ts b/frontend/src/api.ts index f9aa4fb..4c3ef6b 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -20,6 +20,13 @@ export interface Settings { logo: string; } +/** Dernière version d'Excalidraw publiée sur npm (vide si inconnue ou désactivé). */ +export interface UpdateStatus { + enabled: boolean; + latest?: string; + checkedAt?: string; +} + export class ApiError extends Error { constructor(public status: number, message: string) { super(message); @@ -48,6 +55,8 @@ const json = (body: unknown): RequestInit => ({ }); export const api = { + update: async (): Promise => (await request("/api/update")).json(), + list: async (dir: string): Promise => (await request(`/api/list?path=${q(dir)}`)).json(), diff --git a/frontend/src/icons.tsx b/frontend/src/icons.tsx index 539587c..ad6a1ac 100644 --- a/frontend/src/icons.tsx +++ b/frontend/src/icons.tsx @@ -1,33 +1,151 @@ -// Icônes de la barre de titre (traits, couleur héritée). +// Icônes au trait, dans le style d'Excalidraw (jeu Tabler, trait fin, couleur héritée). +import type { ReactNode } from "react"; -const svg = { - width: 18, - height: 18, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - strokeWidth: 2, - strokeLinecap: "round" as const, - strokeLinejoin: "round" as const, - "aria-hidden": true, -}; +const icon = (paths: ReactNode) => + function Icon({ size = 16 }: { size?: number }) { + return ( + + {paths} + + ); + }; -export const SunIcon = () => ( - +export const SunIcon = icon( + <> - + , ); -export const MoonIcon = () => ( - - - +export const MoonIcon = icon(); + +export const SettingsIcon = icon( + <> + + + , ); -export const SettingsIcon = () => ( - - - - +export const MenuIcon = icon(); + +export const CloseIcon = icon(); + +export const FilePlusIcon = icon( + <> + + + + , +); + +export const FolderPlusIcon = icon( + <> + + + , +); + +export const UploadIcon = icon( + <> + + + , +); + +export const FolderUploadIcon = icon( + <> + + + , +); + +export const FolderIcon = icon( + , +); + +export const UpIcon = icon(); + +export const SearchIcon = icon( + <> + + + , +); + +export const ListIcon = icon(); + +export const GridIcon = icon( + <> + + + + + , +); + +export const RefreshIcon = icon( + <> + + + , +); + +export const DotsIcon = icon( + <> + + + + , +); + +export const TrashIcon = icon( + , +); + +export const SelectAllIcon = icon( + <> + + + , +); + +export const DeselectIcon = icon(); + +export const MoveIcon = icon( + <> + + + , +); + +export const SaveIcon = icon( + <> + + + + , +); + +export const UpdateIcon = icon( + <> + + + , +); + +export const CopyIcon = icon( + <> + + + , ); diff --git a/frontend/src/importer.ts b/frontend/src/importer.ts new file mode 100644 index 0000000..7ab5b76 --- /dev/null +++ b/frontend/src/importer.ts @@ -0,0 +1,91 @@ +// Import de fichiers et d'arborescences (glisser-déposer ou sélecteur de dossier). + +export interface PickedFile { + /** Chemin relatif au point d'import, dossiers compris (« Archi/réseau/x.excalidraw »). */ + rel: string; + file: File; +} + +export interface Collected { + files: PickedFile[]; + /** Dossiers à recréer (y compris vides), parents avant enfants. */ + dirs: string[]; + /** Fichiers ignorés : pas des schémas Excalidraw. */ + ignored: string[]; +} + +export const isScene = (name: string) => /\.excalidraw$/i.test(name); + +// Fichiers et dossiers cachés (.DS_Store, .git…) : ignorés sans être signalés. +const hidden = (name: string) => name.startsWith("."); + +/** Le glisser contient-il des fichiers venant du système ? */ +export const hasFiles = (dt: DataTransfer) => dt.types.includes("Files"); + +const readAll = (reader: FileSystemDirectoryReader) => + new Promise((resolve, reject) => { + const out: FileSystemEntry[] = []; + // readEntries rend les entrées par lots (100 sous Chrome) : boucler jusqu'au lot vide. + const next = () => + reader.readEntries((batch) => { + if (batch.length === 0) return resolve(out); + out.push(...batch); + next(); + }, reject); + next(); + }); + +const fileOf = (entry: FileSystemFileEntry) => new Promise((resolve, reject) => entry.file(resolve, reject)); + +async function walk(entry: FileSystemEntry, prefix: string, acc: Collected) { + if (hidden(entry.name)) return; + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory) { + acc.dirs.push(rel); + for (const child of await readAll((entry as FileSystemDirectoryEntry).createReader())) { + await walk(child, rel, acc); + } + } else if (isScene(entry.name)) { + acc.files.push({ rel, file: await fileOf(entry as FileSystemFileEntry) }); + } else { + acc.ignored.push(rel); + } +} + +/** + * Lit un dépôt (fichiers et dossiers). Les entrées doivent être capturées pendant + * l'événement drop : le DataTransfer est vidé dès que le gestionnaire rend la main. + */ +export function collectDrop(dt: DataTransfer): Promise { + const entries = Array.from(dt.items) + .filter((i) => i.kind === "file") + .map((i) => i.webkitGetAsEntry?.()) + .filter((e): e is FileSystemEntry => !!e); + const plain = entries.length ? [] : Array.from(dt.files); // navigateur sans webkitGetAsEntry + return (async () => { + const acc: Collected = { files: [], dirs: [], ignored: [] }; + for (const e of entries) await walk(e, "", acc); + collectInto(plain, acc); + return acc; + })(); +} + +/** Fichiers d'un (chemins dans webkitRelativePath). */ +export function collectFiles(files: FileList | File[]): Collected { + const acc: Collected = { files: [], dirs: [], ignored: [] }; + collectInto(Array.from(files), acc); + return acc; +} + +function collectInto(files: File[], acc: Collected) { + const dirs = new Set(acc.dirs); + for (const file of files) { + const rel = file.webkitRelativePath || file.name; + const parts = rel.split("/"); + if (parts.some(hidden)) continue; + for (let i = 1; i < parts.length; i++) dirs.add(parts.slice(0, i).join("/")); + if (isScene(file.name)) acc.files.push({ rel, file }); + else acc.ignored.push(rel); + } + acc.dirs = [...dirs].sort((a, b) => a.split("/").length - b.split("/").length); +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index fbfbd1f..00748cd 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -3,3 +3,6 @@ interface Window { EXCALIDRAW_ASSET_PATH?: string | string[]; } + +/** Version de @excalidraw/excalidraw embarquée au build (voir vite.config.ts). */ +declare const __EXCALIDRAW_VERSION__: string; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index dd18e44..de612e3 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,5 +1,12 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import { readFileSync } from "node:fs"; + +// Version d'Excalidraw réellement installée (et non la plage de package.json), +// comparée par l'interface à la dernière version publiée sur npm. +const excalidrawVersion: string = JSON.parse( + readFileSync(new URL("./node_modules/@excalidraw/excalidraw/package.json", import.meta.url), "utf8"), +).version; export default defineConfig({ plugins: [react()], @@ -7,5 +14,8 @@ export default defineConfig({ build: { outDir: "../web/dist", emptyOutDir: true, chunkSizeWarningLimit: 4096 }, // `npm run dev` : l'API Go tourne sur :8080. server: { proxy: { "/api": "http://localhost:8080" } }, - define: { "process.env.IS_PREACT": JSON.stringify("false") }, + define: { + "process.env.IS_PREACT": JSON.stringify("false"), + __EXCALIDRAW_VERSION__: JSON.stringify(excalidrawVersion), + }, });