import { useEffect, useRef, useState } from "react"; import type { Note } from "../../lib/types"; export const UNTITLED = "Untitled note"; interface Props { notes: Note[]; selectedId: string; title: string; onTitleChange: (value: string) => void; onCommit: () => void; onSelect: (id: string) => void; } /** * One row that both names the current note and switches to another. * * The dock has no room for a permanent list of titles, so the title field * doubles as the label of what is open and the chevron beside it holds the * rest. Renaming therefore needs no separate affordance. * * Two honest controls rather than one `role="combobox"`: a text field and a * button that opens a listbox. A real combobox owes its listbox keyboard * navigation, active-descendant tracking and an input that filters — none of * which this needs, and half of which is worse than not claiming the role. * * `OverflowMenu` is deliberately not reused here despite the shape being * close. It keys its items by label, and notes are addressed by id: two * untitled notes are the ordinary case and would collapse into one row. */ export default function NoteSwitcher({ notes, selectedId, title, onTitleChange, onCommit, onSelect, }: Props) { const [open, setOpen] = useState(false); const rootRef = useRef(null); // Same dismissal contract as `OverflowMenu`, so the two feel identical. useEffect(() => { if (!open) return; const onDocClick = (e: MouseEvent) => { if (!rootRef.current?.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onDocClick); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDocClick); document.removeEventListener("keydown", onKey); }; }, [open]); return (
onTitleChange(e.target.value)} onBlur={onCommit} placeholder="Note title" aria-label="Note title" className="flex-1 min-w-0 px-2 h-7 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] text-[13px] text-[var(--text-primary)] focus:border-[var(--accent)] transition-colors" /> {open && (
{/* Buttons directly inside the listbox: wrapping each in an `
  • ` would put an implicit `listitem` between the listbox and its options, which is not a child role a listbox owns. */} {notes.map((n) => ( ))}
  • )}
    ); }