frontmatter in skills files
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import { Folder, Pin, PinOff, Search, Clock, FolderOpen, Loader2, X, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Folder, Pin, PinOff, Search, Clock, FolderOpen, Loader2, X } from 'lucide-react';
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import { useFiles, type DirEntry, Breadcrumb } from 'widgets/FileBrowser';
|
||||
import { useRecentFiles } from './state/useRecentFiles';
|
||||
import { usePinnedFiles } from './state/usePinnedFiles';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
import { Card } from '@/components/Card';
|
||||
import { Widget } from '@/components/Widget';
|
||||
|
||||
type Tab = 'browse' | 'recent' | 'pinned';
|
||||
|
||||
@@ -22,7 +21,6 @@ export const FileBrowser = () => {
|
||||
const { recents, addRecent } = useRecentFiles();
|
||||
const { pinned, togglePin, isPinned } = usePinnedFiles();
|
||||
|
||||
const [collapsed, setCollapsed] = useUserState('widget:fileBrowser:collapsed', true);
|
||||
const [tab, setTab] = useState<Tab>('browse');
|
||||
const [browsePath, setBrowsePath] = useState('/');
|
||||
const [entries, setEntries] = useState<DirEntry[]>([]);
|
||||
@@ -72,171 +70,154 @@ export const FileBrowser = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="overflow-hidden">
|
||||
<div className={`flex items-center justify-between px-4 pt-3 ${collapsed ? 'pb-3' : 'pb-1'}`}>
|
||||
<Link to="/files" className="text-xs font-semibold text-duck-dark/60 uppercase tracking-wide hover:underline">
|
||||
File Browser
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
className="text-duck-dark/40 hover:text-duck-dark/70 cursor-pointer transition-colors"
|
||||
>
|
||||
{collapsed ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
||||
</button>
|
||||
<Widget title="File Browser">
|
||||
{/* Tab bar + search */}
|
||||
<div className="flex items-center gap-2 px-4 pb-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{TABS.map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setTab(key)}
|
||||
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer ${
|
||||
!isSearching && tab === key
|
||||
? 'bg-duck-teal/10 text-duck-teal'
|
||||
: 'text-duck-dark/50 hover:text-duck-dark/70 hover:bg-duck-dark/5'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<>
|
||||
{/* Tab bar + search */}
|
||||
<div className="flex items-center gap-2 px-4 pb-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{TABS.map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setTab(key)}
|
||||
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer ${
|
||||
!isSearching && tab === key
|
||||
? 'bg-duck-teal/10 text-duck-teal'
|
||||
: 'text-duck-dark/50 hover:text-duck-dark/70 hover:bg-duck-dark/5'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative ml-auto w-28 md:w-44">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/30" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(ev) => setSearchQuery(ev.target.value)}
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-lg border border-duck-dark/20 bg-white pl-8 pr-8 py-1 text-xs text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-duck-dark/30 hover:text-duck-dark/60 cursor-pointer"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative ml-auto w-28 md:w-44">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/30" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(ev) => setSearchQuery(ev.target.value)}
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-lg border border-duck-dark/20 bg-white pl-8 pr-8 py-1 text-xs text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-duck-dark/30 hover:text-duck-dark/60 cursor-pointer"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
||||
{isSearching ? (
|
||||
searching ? (
|
||||
{/* Content */}
|
||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
||||
{isSearching ? (
|
||||
searching ? (
|
||||
<div className="flex items-center justify-center py-6 text-duck-dark/40">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No results</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{searchResults.map((entry) => (
|
||||
<EntryRow
|
||||
key={entry.path}
|
||||
name={entry.name}
|
||||
subtitle={entry.path}
|
||||
type={entry.type}
|
||||
pinned={entry.type === 'file' && isPinned(entry.path!)}
|
||||
onPin={entry.type === 'file' ? () => togglePin(entry.path!, entry.name) : undefined}
|
||||
onClick={() => {
|
||||
if (entry.type === 'file') openFile(entry.path!, entry.name);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{tab === 'browse' && (
|
||||
<div>
|
||||
<div className="py-2">
|
||||
<Breadcrumb path={browsePath} onNavigate={setBrowsePath} />
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-6 text-duck-dark/40">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No results</p>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">Empty directory</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{searchResults.map((entry) => (
|
||||
{sorted.map((entry) => {
|
||||
const fullPath = browsePath === '/' ? `/${entry.name}` : `${browsePath}/${entry.name}`;
|
||||
return (
|
||||
<EntryRow
|
||||
key={entry.name}
|
||||
name={entry.name}
|
||||
type={entry.type}
|
||||
pinned={entry.type === 'file' && isPinned(fullPath)}
|
||||
onPin={entry.type === 'file' ? () => togglePin(fullPath, entry.name) : undefined}
|
||||
onClick={() => {
|
||||
if (entry.type === 'directory') setBrowsePath(fullPath);
|
||||
else openFile(fullPath, entry.name);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'recent' && (
|
||||
<div className="pt-2">
|
||||
{recents.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No recent files</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{recents.map((f) => (
|
||||
<EntryRow
|
||||
key={entry.path}
|
||||
name={entry.name}
|
||||
subtitle={entry.path}
|
||||
type={entry.type}
|
||||
pinned={entry.type === 'file' && isPinned(entry.path!)}
|
||||
onPin={entry.type === 'file' ? () => togglePin(entry.path!, entry.name) : undefined}
|
||||
onClick={() => {
|
||||
if (entry.type === 'file') openFile(entry.path!, entry.name);
|
||||
}}
|
||||
key={f.path}
|
||||
name={f.name}
|
||||
subtitle={f.path}
|
||||
type="file"
|
||||
pinned={isPinned(f.path)}
|
||||
onPin={() => togglePin(f.path, f.name)}
|
||||
onClick={() => openFile(f.path, f.name)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{tab === 'browse' && (
|
||||
<div>
|
||||
<div className="py-2">
|
||||
<Breadcrumb path={browsePath} onNavigate={setBrowsePath} />
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-6 text-duck-dark/40">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">Empty directory</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{sorted.map((entry) => {
|
||||
const fullPath = browsePath === '/' ? `/${entry.name}` : `${browsePath}/${entry.name}`;
|
||||
return (
|
||||
<EntryRow
|
||||
key={entry.name}
|
||||
name={entry.name}
|
||||
type={entry.type}
|
||||
pinned={entry.type === 'file' && isPinned(fullPath)}
|
||||
onPin={entry.type === 'file' ? () => togglePin(fullPath, entry.name) : undefined}
|
||||
onClick={() => {
|
||||
if (entry.type === 'directory') setBrowsePath(fullPath);
|
||||
else openFile(fullPath, entry.name);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'recent' && (
|
||||
<div className="pt-2">
|
||||
{recents.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No recent files</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{recents.map((f) => (
|
||||
<EntryRow
|
||||
key={f.path}
|
||||
name={f.name}
|
||||
subtitle={f.path}
|
||||
type="file"
|
||||
pinned={isPinned(f.path)}
|
||||
onPin={() => togglePin(f.path, f.name)}
|
||||
onClick={() => openFile(f.path, f.name)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'pinned' && (
|
||||
<div className="pt-2">
|
||||
{pinned.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No pinned files</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{pinned.map((f) => (
|
||||
<EntryRow
|
||||
key={f.path}
|
||||
name={f.name}
|
||||
subtitle={f.path}
|
||||
type="file"
|
||||
pinned
|
||||
onPin={() => togglePin(f.path, f.name)}
|
||||
onClick={() => openFile(f.path, f.name)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{tab === 'pinned' && (
|
||||
<div className="pt-2">
|
||||
{pinned.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No pinned files</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{pinned.map((f) => (
|
||||
<EntryRow
|
||||
key={f.path}
|
||||
name={f.name}
|
||||
subtitle={f.path}
|
||||
type="file"
|
||||
pinned
|
||||
onPin={() => togglePin(f.path, f.name)}
|
||||
onClick={() => openFile(f.path, f.name)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user