diff --git a/periscope/src/frontend/content/changelog.md b/periscope/src/frontend/content/changelog.md index 5541cfe..4cfec26 100644 --- a/periscope/src/frontend/content/changelog.md +++ b/periscope/src/frontend/content/changelog.md @@ -2,6 +2,12 @@ What's new in Periscope. +## 2.60.8 — 2026-09-21 — Folder picker, not Carica file dialog + +Apri progetto opened the Mac file list (**Carica**, no row selected) because the control was a React `` without a working `webkitdirectory`. Empty Carica then looked like “no .kicad_pro”. It now uses `showDirectoryPicker` (Chrome) or a hidden input created with `webkitdirectory=true` and **no accept**. Cancel/empty pick is ignored. + +- [Fixed] One folder gesture; `.kicad_pro` from name or webkitRelativePath. + ## 2.60.7 — 2026-09-21 — Detect HubAudio.kicad_pro after folder pick Apri progetto was rejecting the folder that actually contains `HubAudio.kicad_pro`. Safari/macOS folder pick often puts the real name in `webkitRelativePath` while `File.name` is the stem without `.kicad_pro`, and React was not setting `webkitdirectory` on the input. We match name **or** relative path, sniff JSON if the suffix is missing, and set the folder attribute on the DOM. diff --git a/periscope/src/frontend/src/components/dashboard/create-project-dialog.tsx b/periscope/src/frontend/src/components/dashboard/create-project-dialog.tsx index ecae69c..a6212fb 100644 --- a/periscope/src/frontend/src/components/dashboard/create-project-dialog.tsx +++ b/periscope/src/frontend/src/components/dashboard/create-project-dialog.tsx @@ -884,6 +884,7 @@ export function CreateProjectDialog({ }, []); const onKicadProjectFolder = useCallback((incoming: File[]) => { + if (incoming.length === 0) return; setBomFromBundle(true); setPcbFromBundle(true); setPcbFile(null); @@ -2122,9 +2123,7 @@ export function CreateProjectDialog({
diff --git a/periscope/src/frontend/src/components/upload/file-upload-zone.tsx b/periscope/src/frontend/src/components/upload/file-upload-zone.tsx index 0908f22..90e1da6 100644 --- a/periscope/src/frontend/src/components/upload/file-upload-zone.tsx +++ b/periscope/src/frontend/src/components/upload/file-upload-zone.tsx @@ -1,12 +1,12 @@ "use client"; -import { useCallback, useState, type ChangeEvent, type DragEvent } from "react"; +import { useCallback, useState, type ChangeEvent, type DragEvent, type MouseEvent } from "react"; import { FileCheck, Upload } from "lucide-react"; import { cn } from "@/lib/utils"; export type FileUploadZoneProps = { label: string; - accept: string; + accept?: string; multiple?: boolean; directory?: boolean; files: File[]; @@ -94,6 +94,64 @@ function mergeSelection(current: File[], incoming: File[], multiple: boolean, di return [...current, ...incoming]; } +async function filesFromDirectoryHandle( + handle: FileSystemDirectoryHandle, + prefix = "", +): Promise { + const out: File[] = []; + for await (const [name, child] of handle.entries()) { + if (SKIP_DIR.test(name)) continue; + if (child.kind === "file") { + const file = await child.getFile(); + out.push(stampRelativePath(file, prefix + name)); + } else if (child.kind === "directory") { + out.push( + ...(await filesFromDirectoryHandle( + child as FileSystemDirectoryHandle, + prefix + name + "/", + )), + ); + } + } + return out; +} + +function pickFolderViaHiddenInput(): Promise { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.setAttribute("webkitdirectory", "true"); + input.setAttribute("directory", "true"); + input.style.display = "none"; + const finish = (files: File[]) => { + input.remove(); + resolve(files); + }; + input.addEventListener("change", () => finish(Array.from(input.files ?? []))); + input.addEventListener("cancel", () => finish([])); + document.body.appendChild(input); + input.click(); + }); +} + +async function pickProjectFolder(): Promise { + const picker = ( + window as Window & { + showDirectoryPicker?: () => Promise; + } + ).showDirectoryPicker; + if (typeof picker === "function") { + try { + const handle = await picker.call(window); + return await filesFromDirectoryHandle(handle, `${handle.name}/`); + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") return []; + } + } + return pickFolderViaHiddenInput(); +} + export function FileUploadZone({ label, accept, @@ -111,6 +169,7 @@ export function FileUploadZone({ event.preventDefault(); setHover(false); void filesFromDropEvent(event).then((dropped) => { + if (dropped.length === 0) return; onFilesChange(mergeSelection(files, dropped, multiple, directory)); }); }, @@ -120,25 +179,32 @@ export function FileUploadZone({ const onPick = useCallback( (event: ChangeEvent) => { const picked = Array.from(event.target.files ?? []); + if (picked.length === 0) return; onFilesChange(mergeSelection(files, picked, multiple, directory)); }, [files, multiple, directory, onFilesChange], ); - return ( -
) : null} + + ); + + if (directory) { + return ( + + ); + } + + return ( + ); diff --git a/periscope/src/frontend/src/lib/kicad-project-files.ts b/periscope/src/frontend/src/lib/kicad-project-files.ts index 4a7ce64..001e0e4 100644 --- a/periscope/src/frontend/src/lib/kicad-project-files.ts +++ b/periscope/src/frontend/src/lib/kicad-project-files.ts @@ -1,9 +1,8 @@ -"""Pick KiCad project files from a folder picker FileList (Chrome + Safari). +/** Pick KiCad project files from a folder picker FileList (Chrome + Safari). -Safari/macOS often puts the real name in ``webkitRelativePath`` while -``File.name`` is the stem without ``.kicad_pro``. Matching only ``file.name`` -misses ``HubAudio.kicad_pro`` after Apri progetto. -""" +Safari/macOS often puts the real name in webkitRelativePath while File.name +is the stem without .kicad_pro. Matching only file.name misses HubAudio.kicad_pro. +*/ export type NamedUpload = { name: string; @@ -62,8 +61,8 @@ function isKicadKeep(file: NamedUpload): boolean { base.endsWith(".kicad_sch") || base.endsWith(".kicad_pcb") || looksLikeKicadSch(file) || - (file.name.toLowerCase().includes(".kicad_pcb") || - uploadRel(file).toLowerCase().includes(".kicad_pcb")) + file.name.toLowerCase().includes(".kicad_pcb") || + uploadRel(file).toLowerCase().includes(".kicad_pcb") ); }