(null);
const load = useCallback(async (path: string) => {
setLoading(true);
setError(null);
try {
setListing(await listDirectory(path));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load directory");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
// Reset cached listing when the popover closes so the next open reloads
// from `value`. Without this, a user who browsed deep into the tree,
// closed without committing, and reopened would still see the stale
// last-browsed directory instead of their picked folder (or the root).
if (!open) {
setListing(null);
return;
}
if (listing) return;
void load(value || "");
}, [open, listing, load, value]);
return { listing, loading, error, load };
}
/** Splits an absolute path into clickable breadcrumb segments. */
function pathSegments(path: string): Array<{ name: string; path: string }> {
if (!path) return [];
const parts = path.split("/").filter(Boolean);
const segs: Array<{ name: string; path: string }> = [{ name: "/", path: "/" }];
let acc = "";
for (const part of parts) {
acc += `/${part}`;
segs.push({ name: part, path: acc });
}
return segs;
}
function Breadcrumb({ path, onNavigate }: { path: string; onNavigate: (p: string) => void }) {
const segs = pathSegments(path);
return (
{segs.length === 0 && (
Loading…
)}
{segs.map((seg, i) => {
const last = i === segs.length - 1;
return (
{i > 0 && (
)}
);
})}
);
}
function Entries({
listing,
loading,
error,
onDescend,
}: {
listing: DirectoryListing | null;
loading: boolean;
error: string | null;
onDescend: (path: string) => void;
}) {
if (loading) return ;
if (error) return ;
if (!listing || listing.entries.length === 0) {
return ;
}
return (
e.stopPropagation()}
>
{listing.entries.map((entry) => (
))}
);
}
function EmptyRow({ text, variant, testId }: { text: string; variant?: "error"; testId?: string }) {
return (
{text}
);
}
function Footer({
choosable,
onUseScratch,
onChoose,
}: {
choosable: boolean;
onUseScratch: () => void;
onChoose: () => void;
}) {
return (
);
}