feat: add command palette, accessibility, scroll animations, demo workspace, and keyboard navigation

- COMP-139: Command palette for quick navigation
- COMP-140: Accessibility improvements
- COMP-141: Scroll animations with animate-on-scroll component
- COMP-143: Demo workspace with seed data and demo banner
- COMP-145: Keyboard navigation and shortcuts help

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Vectry
2026-02-10 18:06:36 +00:00
parent f9e7956e6f
commit 64c827ee84
18 changed files with 2047 additions and 40 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import {
Search,
Filter,
@@ -23,6 +23,7 @@ import {
WifiOff,
} from "lucide-react";
import { cn, formatDuration, formatRelativeTime } from "@/lib/utils";
import { useKeyboardNav } from "@/hooks/use-keyboard-nav";
type TraceStatus = "RUNNING" | "COMPLETED" | "ERROR";
@@ -87,6 +88,7 @@ export function TraceList({
initialTotalPages,
initialPage,
}: TraceListProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [traces, setTraces] = useState<Trace[]>(initialTraces);
const [total, setTotal] = useState(initialTotal);
@@ -283,6 +285,19 @@ export function TraceList({
return matchesSearch && matchesStatus;
});
const { selectedIndex } = useKeyboardNav({
itemCount: filteredTraces.length,
onSelect: useCallback(
(index: number) => {
const trace = filteredTraces[index];
if (trace) {
router.push(`/dashboard/traces/${trace.id}`);
}
},
[filteredTraces, router]
),
});
const filterChips: { value: FilterStatus; label: string }[] = [
{ value: "ALL", label: "All" },
{ value: "RUNNING", label: "Running" },
@@ -376,7 +391,9 @@ export function TraceList({
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-neutral-500" />
<label htmlFor="trace-search" className="sr-only">Search traces</label>
<input
id="trace-search"
type="text"
placeholder="Search traces..."
value={searchQuery}
@@ -422,8 +439,9 @@ export function TraceList({
{showAdvancedFilters && (
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-xs text-neutral-500 font-medium">Sort by</label>
<label htmlFor="sort-filter" className="text-xs text-neutral-500 font-medium">Sort by</label>
<select
id="sort-filter"
value={sortFilter}
onChange={(e) => setSortFilter(e.target.value as SortOption)}
className="w-full bg-neutral-900 border border-neutral-700 rounded-lg px-3 py-2 text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 transition-all"
@@ -437,8 +455,9 @@ export function TraceList({
</div>
<div className="space-y-2">
<label className="text-xs text-neutral-500 font-medium">Date from</label>
<label htmlFor="date-from" className="text-xs text-neutral-500 font-medium">Date from</label>
<input
id="date-from"
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
@@ -447,8 +466,9 @@ export function TraceList({
</div>
<div className="space-y-2">
<label className="text-xs text-neutral-500 font-medium">Date to</label>
<label htmlFor="date-to" className="text-xs text-neutral-500 font-medium">Date to</label>
<input
id="date-to"
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
@@ -457,8 +477,9 @@ export function TraceList({
</div>
<div className="sm:col-span-3 space-y-2">
<label className="text-xs text-neutral-500 font-medium">Tags (comma-separated)</label>
<label htmlFor="tags-filter" className="text-xs text-neutral-500 font-medium">Tags (comma-separated)</label>
<input
id="tags-filter"
type="text"
placeholder="e.g., production, critical, api"
value={tagsFilter}
@@ -473,8 +494,13 @@ export function TraceList({
{/* Trace List */}
<div className="space-y-3">
{filteredTraces.map((trace) => (
<TraceCard key={trace.id} trace={trace} />
{filteredTraces.map((trace, index) => (
<TraceCard
key={trace.id}
trace={trace}
index={index}
isSelected={index === selectedIndex}
/>
))}
</div>
@@ -497,6 +523,7 @@ export function TraceList({
<button
disabled={currentPage <= 1}
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
aria-label="Previous page"
className="p-2 rounded-lg border border-neutral-800 text-neutral-400 hover:text-neutral-100 hover:border-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft className="w-5 h-5" />
@@ -504,6 +531,7 @@ export function TraceList({
<button
disabled={currentPage >= totalPages}
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}
aria-label="Next page"
className="p-2 rounded-lg border border-neutral-800 text-neutral-400 hover:text-neutral-100 hover:border-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-5 h-5" />
@@ -515,13 +543,29 @@ export function TraceList({
);
}
function TraceCard({ trace }: { trace: Trace }) {
function TraceCard({
trace,
index,
isSelected,
}: {
trace: Trace;
index: number;
isSelected: boolean;
}) {
const status = statusConfig[trace.status];
const StatusIcon = status.icon;
return (
<Link href={`/dashboard/traces/${trace.id}`}>
<div className="group p-5 bg-neutral-900 border border-neutral-800 rounded-xl hover:border-emerald-500/30 hover:bg-neutral-800/50 transition-all duration-200 cursor-pointer">
<div
data-keyboard-index={index}
className={cn(
"group p-5 bg-neutral-900 border rounded-xl hover:border-emerald-500/30 hover:bg-neutral-800/50 transition-all duration-200 cursor-pointer",
isSelected
? "border-emerald-500/40 bg-emerald-500/5 ring-1 ring-emerald-500/20"
: "border-neutral-800"
)}
>
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* Left: Name and Status */}
<div className="flex-1 min-w-0">