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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user