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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Vectry
2026-02-10 18:06:47 +00:00
parent 38d5b4806c
commit 7ff493a89a
13 changed files with 1308 additions and 79 deletions

View File

@@ -0,0 +1,52 @@
"use client";
import { useEffect, useRef } from "react";
interface UseScrollAnimateOptions {
threshold?: number;
rootMargin?: string;
once?: boolean;
}
export function useScrollAnimate<T extends HTMLElement = HTMLDivElement>({
threshold = 0.1,
rootMargin = "0px 0px -60px 0px",
once = true,
}: UseScrollAnimateOptions = {}) {
const ref = useRef<T>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (prefersReducedMotion) {
el.setAttribute("data-animate", "visible");
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.setAttribute("data-animate", "visible");
if (once) {
observer.unobserve(entry.target);
}
} else if (!once) {
entry.target.setAttribute("data-animate", "hidden");
}
});
},
{ threshold, rootMargin }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold, rootMargin, once]);
return ref;
}