YT-AI-Automation / frontend /src /components /CommandPalette.tsx
github-actions
Sync Docker Space
9b79a79
Raw
History Blame Contribute Delete
5.85 kB
import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { ArrowRight, FileText, Home, Image, Library, Search, Settings, UploadCloud, X } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { preloadRoute } from '../lib/routePreload'
import { useFocusTrap } from '../hooks/useFocusTrap'
interface Command {
label: string
path: string
group: string
keywords: string
icon: typeof Home
}
const COMMANDS: Command[] = [
{ label: 'Home', path: '/', group: 'Navigate', keywords: 'dashboard status recent', icon: Home },
{ label: 'New run', path: '/workspace', group: 'Create', keywords: 'workspace tools start', icon: FileText },
{ label: 'Text to Video', path: '/workspace/text', group: 'Create', keywords: 'text ai lesson video', icon: FileText },
{ label: 'HTML to Video', path: '/workspace/html', group: 'Create', keywords: 'html render playwright', icon: FileText },
{ label: 'Image/PDF to Screenshots', path: '/workspace/image', group: 'Create', keywords: 'image pdf ocr screenshot', icon: Image },
{ label: 'Screenshots to Video', path: '/workspace/screenshots', group: 'Create', keywords: 'screenshots export pptx mp4', icon: Image },
{ label: 'YouTube to Video', path: '/workspace/youtube', group: 'Create', keywords: 'youtube timestamps video', icon: UploadCloud },
{ label: 'YouTube to Screenshots', path: '/workspace/youtube-screenshots', group: 'Utility', keywords: 'youtube screenshots capture folder', icon: Image },
{ label: 'Library', path: '/library', group: 'Manage', keywords: 'files outputs assets bundles', icon: Library },
{ label: 'Publish', path: '/publish', group: 'Manage', keywords: 'youtube metadata upload checklist', icon: UploadCloud },
{ label: 'Processes', path: '/processes', group: 'Monitor', keywords: 'runs logs queue progress', icon: FileText },
{ label: 'Settings', path: '/settings', group: 'System', keywords: 'backend theme defaults diagnostics', icon: Settings },
]
export default function CommandPalette({
open,
onClose,
}: {
open: boolean
onClose: () => void
}) {
const nav = useNavigate()
const dialogRef = useFocusTrap<HTMLDivElement>(open)
const [query, setQuery] = useState('')
useEffect(() => {
if (!open) return
const resetId = window.setTimeout(() => setQuery(''), 0)
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose()
}
window.addEventListener('keydown', onKey)
return () => {
window.clearTimeout(resetId)
window.removeEventListener('keydown', onKey)
}
}, [onClose, open])
const matches = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return COMMANDS
return COMMANDS.filter((command) =>
`${command.label} ${command.group} ${command.keywords}`.toLowerCase().includes(q),
)
}, [query])
const run = (command: Command) => {
preloadRoute(command.path)
nav(command.path)
onClose()
}
if (!open) return null
return createPortal(
<div className="fixed inset-0 z-[80] flex items-start justify-center bg-slate-950/45 px-4 pt-[12vh] backdrop-blur-sm">
<div className="absolute inset-0" onClick={onClose} aria-hidden="true" />
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label="Command palette"
tabIndex={-1}
className="relative w-full max-w-2xl overflow-hidden rounded-lg border border-slate-200 bg-[rgb(var(--bg-surface))] shadow-2xl dark:border-white/10"
>
<div className="flex items-center gap-3 border-b border-slate-200 px-4 py-3 dark:border-white/10">
<Search size={17} className="shrink-0 text-faint" />
<input
autoFocus
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && matches[0]) run(matches[0])
}}
placeholder="Search tools, pages, runs, settings..."
className="min-w-0 flex-1 bg-transparent text-sm text-[rgb(var(--text-strong))] outline-none placeholder:text-faint"
/>
<button type="button" className="btn-ghost btn-sm !px-2" onClick={onClose} aria-label="Close command palette">
<X size={15} />
</button>
</div>
<div className="max-h-[56vh] overflow-auto p-2">
{matches.length === 0 ? (
<div className="px-3 py-8 text-center text-sm text-muted">No commands found.</div>
) : (
matches.map((command) => {
const Icon = command.icon
return (
<button
key={command.path}
type="button"
className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition hover:bg-[rgb(var(--bg-muted))] focus:bg-[rgb(var(--bg-muted))] focus:outline-none"
onMouseEnter={() => preloadRoute(command.path)}
onFocus={() => preloadRoute(command.path)}
onClick={() => run(command)}
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500 dark:bg-white/[0.06] dark:text-slate-300">
<Icon size={15} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-[rgb(var(--text-strong))]">{command.label}</span>
<span className="block text-xs text-muted">{command.group}</span>
</span>
<ArrowRight size={14} className="text-faint" />
</button>
)
})
)}
</div>
</div>
</div>,
document.body,
)
}