frontmatter in skills files

This commit is contained in:
2026-02-18 01:23:22 +00:00
parent 030cf9fbd3
commit c2660cd125
16 changed files with 959 additions and 506 deletions
@@ -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>
);
};