/** * SettingsPopover — the per-pane settings disclosure (spec §07; arch 10). * * Astrolabe distributes preferences to where they apply: a small gear button in a * pane's toolbar discloses a popover of **live** controls for that cluster * (editor / preview / library). This is the shared primitive behind all of them. * * It is a **disclosure + non-modal popover** (WAI-ARIA APG disclosure; Carbon * popover), deliberately NOT an ARIA menu: a menu lists actions/commands * (menuitem/checkbox/radio), whereas these panels hold sliders, number/text * inputs, and radio groups — so the container is a labelled `group`, not a * `menu`. The gear carries `aria-expanded` + `aria-controls`; Enter/Space toggle; * Esc closes and returns focus to the gear; an outside click closes. Non-modal, * so there is no focus trap (unlike the modal shell). At most one popover is open * at a time, and any can be opened imperatively (the Cmd/Ctrl+, shortcut). * * The panel is **portaled to `document.body`** and positioned `fixed` from the * gear's rect, because the panes clip their content (`overflow: auto/hidden`); an * in-flow absolute popover would be cut off. Position re-measures on scroll/resize. */ import { type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { usePopover } from '../hooks/usePopover'; import { Icon } from './Icon'; import { IconButton } from './IconButton'; import styles from './SettingsPopover.module.css'; /** Focus the first interactive control on open (any kind — these are forms). */ const INITIAL_FOCUS = ['input, button, select, textarea, [tabindex]'] as const; export interface SettingsPopoverProps { /** Stable id, also the popover's element id for `aria-controls` (e.g. 'editor-settings'). */ id: string; /** Accessible name for the gear button. */ label: string; /** Heading shown at the top of the popover, and its group label. */ title: string; /** Which edge of the gear the popover aligns to (default right). */ align?: 'left' | 'right'; children: ReactNode; } export function SettingsPopover({ id, label, title, align = 'right', children, }: SettingsPopoverProps) { const { open, toggle, triggerRef, setPopNode } = usePopover({ id, align, initialFocus: INITIAL_FOCUS, }); return (