diff --git a/web/ProjectsPanel.tsx b/web/ProjectsPanel.tsx index 55c8724..326634a 100644 --- a/web/ProjectsPanel.tsx +++ b/web/ProjectsPanel.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { FolderPlus, FolderTree, Loader2, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; +import { DirectoryPicker } from 'officerdev'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useProjects, useSelectedProject } from './useKanban'; @@ -27,6 +28,7 @@ export const ProjectsPanel = () => { const [name, setName] = useState(''); const [path, setPath] = useState(''); const [confirming, setConfirming] = useState(null); + const [picking, setPicking] = useState(false); /* Land on something rather than on nothing. @@ -72,14 +74,43 @@ export const ProjectsPanel = () => { {adding && (
- setName(e.target.value)} placeholder="Name" className="text-sm" /> setPath(e.target.value)} - placeholder="~/projects/thing/todo" - className="font-mono text-xs" - spellCheck={false} + autoFocus + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Name" + className="text-sm" /> + {/* + Browse OR type. The picker fills the field and the field is what saves, so the path stays + visible and editable before it is stored — and a folder outside home, which the picker + cannot reach, can still be typed and will be refused by the server with a reason. + */} +
+ setPath(e.target.value)} + placeholder="~/projects/thing/todo" + className="min-w-0 flex-1 font-mono text-xs" + spellCheck={false} + /> + +
+ + {picking && ( + setPicking(false)} + onPick={(absolutePath) => { + setPath(absolutePath); + setPicking(false); + // A folder is usually named after itself — offer that as the name rather than an empty + // field, and leave it editable. + if (!name.trim()) setName(absolutePath.split('/').filter(Boolean).slice(-2).join('/')); + }} + /> + )} {/* Said before they press it, not after it fails: the folder holding the theme directories is not the repository root, and the two are one level apart. */}