From 7ff493a89a801ce2592434372040e1dcbf40306e Mon Sep 17 00:00:00 2001 From: Vectry Date: Tue, 10 Feb 2026 18:06:47 +0000 Subject: [PATCH] feat: add command palette, accessibility, scroll animations, and keyboard navigation Implements COMP-139 (command palette), COMP-140 (accessibility), COMP-141 (scroll animations), COMP-145 (keyboard navigation) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus --- apps/web/package.json | 3 +- apps/web/src/app/globals.css | 40 ++ apps/web/src/app/history/page.tsx | 33 +- apps/web/src/app/layout.tsx | 12 +- apps/web/src/app/page.tsx | 150 +++-- apps/web/src/components/command-palette.tsx | 224 +++++++ .../components/keyboard-shortcuts-help.tsx | 121 ++++ apps/web/src/components/navbar.tsx | 5 +- apps/web/src/components/repo-input.tsx | 6 +- apps/web/src/components/scroll-section.tsx | 28 + apps/web/src/hooks/use-keyboard-nav.ts | 112 ++++ apps/web/src/hooks/use-scroll-animate.ts | 52 ++ package-lock.json | 601 +++++++++++++++++- 13 files changed, 1308 insertions(+), 79 deletions(-) create mode 100644 apps/web/src/components/command-palette.tsx create mode 100644 apps/web/src/components/keyboard-shortcuts-help.tsx create mode 100644 apps/web/src/components/scroll-section.tsx create mode 100644 apps/web/src/hooks/use-keyboard-nav.ts create mode 100644 apps/web/src/hooks/use-scroll-animate.ts diff --git a/apps/web/package.json b/apps/web/package.json index 45832ed..56b71ba 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,10 +11,11 @@ "db:push": "prisma db push --schema=../../packages/database/prisma/schema.prisma" }, "dependencies": { - "@codeboard/shared": "*", "@codeboard/database": "*", + "@codeboard/shared": "*", "@tailwindcss/typography": "^0.5.19", "bullmq": "^5.34.0", + "cmdk": "^1.1.1", "ioredis": "^5.4.0", "lucide-react": "^0.563.0", "mermaid": "^11.4.0", diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index c5d05d4..8593cfc 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -36,6 +36,22 @@ box-sizing: border-box; } +:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--accent-blue); + border-radius: 0.25rem; +} + +a:focus-visible, +button:focus-visible, +[role="button"]:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--accent-blue); +} + html { scroll-behavior: smooth; } @@ -395,3 +411,27 @@ body { height: 1px; background: linear-gradient(90deg, transparent, var(--border), transparent); } + +[data-animate] { + opacity: 0; + transform: translateY(32px); + transition: opacity 0.7s ease-out, transform 0.7s ease-out; +} + +[data-animate="visible"] { + opacity: 1; + transform: translateY(0); +} + +[data-animate][data-animate-delay="1"] { transition-delay: 0.1s; } +[data-animate][data-animate-delay="2"] { transition-delay: 0.2s; } +[data-animate][data-animate-delay="3"] { transition-delay: 0.3s; } +[data-animate][data-animate-delay="4"] { transition-delay: 0.4s; } + +@media (prefers-reduced-motion: reduce) { + [data-animate] { + opacity: 1; + transform: none; + transition: none; + } +} diff --git a/apps/web/src/app/history/page.tsx b/apps/web/src/app/history/page.tsx index 34626fc..c71ce41 100644 --- a/apps/web/src/app/history/page.tsx +++ b/apps/web/src/app/history/page.tsx @@ -1,10 +1,13 @@ "use client"; -import { Suspense, useEffect, useState } from "react"; +import { Suspense, useEffect, useState, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import type { GeneratedDocs } from "@codeboard/shared"; import { MermaidDiagram } from "@/components/mermaid-diagram"; +import { useKeyboardNav } from "@/hooks/use-keyboard-nav"; +import { KeyboardShortcutsHelp } from "@/components/keyboard-shortcuts-help"; import { ArrowLeft, Clock, @@ -131,6 +134,7 @@ function ComparisonView({ @@ -410,6 +414,7 @@ function ComparisonView({ function HistoryContent() { const searchParams = useSearchParams(); const repo = searchParams.get("repo"); + const router = useRouter(); const [generations, setGenerations] = useState([]); const [loading, setLoading] = useState(true); @@ -421,6 +426,21 @@ function HistoryContent() { const [leftGen, setLeftGen] = useState(null); const [rightGen, setRightGen] = useState(null); + const handleKeyboardSelect = useCallback( + (index: number) => { + if (index >= 0 && index < generations.length) { + router.push(`/docs/${generations[index].id}`); + } + }, + [generations, router] + ); + + const { activeIndex, showHelp, setShowHelp } = useKeyboardNav({ + itemCount: generations.length, + onSelect: handleKeyboardSelect, + enabled: !loading && !comparing && generations.length > 1, + }); + useEffect(() => { if (!repo) { setLoading(false); @@ -678,17 +698,21 @@ function HistoryContent() {
- {generations.map((gen) => { + {generations.map((gen, index) => { const isSelected = selectedIds.has(gen.id); const canSelect = selectedIds.size < 2 || isSelected; + const isKeyboardActive = activeIndex === index; return (
@@ -700,6 +724,7 @@ function HistoryContent() { : "text-zinc-500 hover:text-zinc-300 hover:bg-white/5" } ${!canSelect ? "cursor-not-allowed" : ""}`} disabled={!canSelect} + aria-label={isSelected ? `Deselect version ${gen.commitHash.slice(0, 7)}` : `Select version ${gen.commitHash.slice(0, 7)} for comparison`} > {isSelected ? ( @@ -760,6 +785,8 @@ function HistoryContent() { )}
+ setShowHelp(false)} /> + {leftDoc && rightDoc && leftGen && rightGen && ( + + Skip to content +
-
-
+