"use client"; import { SearchIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react"; import { Kbd } from "@/components/ui/kbd"; import { cn } from "@/lib/utils"; export interface Shortcut { label: string; keys: string[][]; keywords?: string[]; } export interface ShortcutGroup { title: string; shortcuts: Shortcut[]; } export type Platform = "mac" | "windows"; export interface ShortcutSheetProps { groups: ShortcutGroup[]; children: ReactNode; defaultPlatform?: Platform; title?: string; className?: string; } const MAC: Record = { mod: "⌘", shift: "⇧", alt: "⌥", ctrl: "⌃", enter: "↵", esc: "esc", backspace: "⌫", up: "↑", down: "↓", left: "←", right: "→" }; const WIN: Record = { mod: "Ctrl", shift: "Shift", alt: "Alt", ctrl: "Ctrl", enter: "Enter", esc: "Esc", backspace: "Backspace", up: "↑", down: "↓", left: "←", right: "→" }; const SPOKEN: Record = { mod: "Command", shift: "Shift", alt: "Option", ctrl: "Control", enter: "Return", esc: "Escape", backspace: "Delete", up: "Up", down: "Down", left: "Left", right: "Right" }; const keyLabel = (token: string, platform: Platform) => (platform === "mac" ? MAC : WIN)[token.toLowerCase()] ?? token.toUpperCase(); function isTyping(el: EventTarget | null) { if (!(el instanceof HTMLElement)) return false; return el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"; } function Highlight({ text, query }: { text: string; query: string }) { const i = query ? text.toLowerCase().indexOf(query) : -1; if (i < 0) return <>{text}; return ( <> {text.slice(0, i)} {text.slice(i, i + query.length)} {text.slice(i + query.length)} ); } export function ShortcutSheet({ groups, children, defaultPlatform, title = "Keyboard shortcuts", className }: ShortcutSheetProps) { const reduce = useReducedMotion(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [platform, setPlatform] = useState(defaultPlatform ?? "mac"); const rootRef = useRef(null); const inputRef = useRef(null); const titleId = useId(); useEffect(() => { if (defaultPlatform) return; const nav = navigator as Navigator & { userAgentData?: { platform?: string } }; const name = nav.userAgentData?.platform ?? nav.platform ?? ""; if (!/mac|iphone|ipad/i.test(name)) setPlatform("windows"); }, [defaultPlatform]); useEffect(() => { if (open) inputRef.current?.focus(); }, [open]); const close = () => { setOpen(false); setQuery(""); rootRef.current?.focus(); }; const q = query.trim().toLowerCase(); const filtered = useMemo(() => { if (!q) return groups; const words = q.split(/[\s+]+/).filter(Boolean); return groups .map((g) => ({ ...g, shortcuts: g.shortcuts.filter((s) => { const keyText = s.keys.flat().flatMap((k) => [k.toLowerCase(), keyLabel(k, "windows").toLowerCase(), SPOKEN[k.toLowerCase()]?.toLowerCase() ?? ""]); const hay = [s.label.toLowerCase(), g.title.toLowerCase(), ...(s.keywords ?? []).map((k) => k.toLowerCase())].join(" "); return hay.includes(q) || words.every((w) => keyText.includes(w) || hay.includes(w)); }), })) .filter((g) => g.shortcuts.length); }, [groups, q]); const total = filtered.reduce((n, g) => n + g.shortcuts.length, 0); const onRootKey = (event: KeyboardEvent) => { if (open) { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } return; } if (event.key === "?" && !event.metaKey && !event.ctrlKey && !isTyping(event.target)) { event.preventDefault(); setOpen(true); } }; const ease = [0.23, 1, 0.32, 1] as const; return (
{ if (!isTyping(event.target) && !(event.target as HTMLElement).closest("button,a,[tabindex]:not([data-sheet-root])")) { event.preventDefault(); rootRef.current?.focus(); } }} data-sheet-root="" ref={rootRef} role="region" tabIndex={0} >
{children}
Click anywhere in here, then press
{open && ( { if (event.target === event.currentTarget) { event.preventDefault(); close(); } }} transition={{ duration: 0.18 }} > { if (event.key !== "Tab") return; const items = [...event.currentTarget.querySelectorAll("input,button")]; const first = items[0]; const last = items[items.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } }} role="dialog" transition={{ duration: 0.24, ease }} >

{title}

setQuery(e.target.value)} placeholder="Search shortcuts…" ref={inputRef} value={query} />
{filtered.length === 0 ? (

No shortcuts for “{query.trim()}”

Try an action like “archive”, or a key like “shift”.

) : ( filtered.map((g) => (

{g.title}

    {g.shortcuts.map((s) => (
  • ))}
)) )}

{q ? `${total} shortcut${total === 1 ? "" : "s"} found` : ""}

)}
); } function Keys({ keys, platform, reduce }: { keys: string[][]; platform: Platform; reduce: boolean }) { const spoken = keys.map((combo) => combo.map((k) => SPOKEN[k.toLowerCase()] ?? k).join(" ")).join(", then "); return ( {keys.map((combo, i) => ( {i > 0 && then} {combo.map((k) => ( {keyLabel(k, platform)} ))} ))} ); } function PlatformSwitch({ value, onChange }: { value: Platform; onChange: (p: Platform) => void }) { const id = useId(); return (
{(["mac", "windows"] as const).map((p) => ( ))}
); }