"use client" import * as React from "react" import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" import { cn } from "../../lib/utils" import { DROPDOWN_MENU_CONTENT_SURFACE_CLASS } from "../../lib/dropdown-menu-surface" import { floatingWindowOverlayZClass, useInsideFloatingWindowOverlay, } from "./floating-window-overlay-context" import { useShortcutScope, wasOverlayOpenAtKeydown, watchOverlayState, } from "../../lib/shortcut-scope" function DropdownMenu({ ...props }: React.ComponentProps) { return } function DropdownMenuPortal({ ...props }: React.ComponentProps) { return ( ) } function DropdownMenuTrigger({ asChild, className, ...props }: React.ComponentProps) { return ( ) } function DropdownMenuContent({ className, align = "start", sideOffset = 4, ...props }: React.ComponentProps) { const insideFloatingWindow = useInsideFloatingWindowOverlay() return ( ) } function DropdownMenuGroup({ ...props }: React.ComponentProps) { return ( ) } /** * A highlighted row repaints its descendants' text, except where a descendant * owns its own ink. * * The sweep exists because a row's secondary text sets `text-muted-foreground` * on itself, and muted grey on the accent fill is the one combination in the * menu that fails to read. Forcing every descendant to `accent-foreground` fixes * that and breaks anything painted on a colour of its own: a product tile is a * saturated brand square whose mark is white by contract, and a filled * `StatusBadge` is a fill and an ink chosen as a pair. Both went to near-black * ink on hover, on backgrounds picked for white. * * `[data-fixed-ink]` is the opt-out, and it covers the element's subtree, since * what needs protecting is usually the glyph or label inside the coloured * surface rather than the surface itself. */ function DropdownMenuItem({ className, inset, variant = "default", shortcut, children, asChild, ...props }: React.ComponentProps & { inset?: boolean variant?: "default" | "destructive" /** Visual keyboard-shortcut hint shown on the right of the item (e.g. "⌘E", "F2"). * Purely cosmetic — to actually bind the key, render a sibling `` * in a parent that stays mounted (menu items unmount when the menu closes). */ shortcut?: string }) { return ( {asChild ? ( children ) : ( <> {children} {shortcut ? {shortcut} : null} )} ) } /** Invisible component that binds a keyboard shortcut globally while mounted. * Pair with `DropdownMenuItem shortcut="⌘E"` for the visual hint. */ function Shortcut({ keys, onInvoke, disabled, }: { keys: string onInvoke: (e: KeyboardEvent) => void disabled?: boolean }) { useShortcut(keys, onInvoke, disabled) return null } function useShortcut( keys: string, onInvoke: (e: KeyboardEvent) => void, disabled?: boolean, ) { const ref = React.useRef(onInvoke) const scope = useShortcutScope() React.useEffect(() => { ref.current = onInvoke }, [onInvoke]) React.useEffect(() => { if (disabled) return watchOverlayState() const parsed = parseShortcut(keys) if (!parsed) return function handler(e: KeyboardEvent) { if (!matchesShortcut(e, parsed!)) return const t = e.target as HTMLElement | null if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return // Skip when the user is activating an interactive control — Enter/Space on a // focused button, link, or role=button is already that control's native action. // Without this guard, a page-level `Shortcut keys="Enter"` (e.g. the wizard's // "submit on review") races the button's own click: the Edit / Back / Cancel // button runs its handler AND the window listener submits the form on the same // keystroke — the classic "review auto-closes when I click Edit" bug. if (t && (parsed!.key === "enter" || parsed!.key === " ")) { const role = t.getAttribute("role") if ( t.tagName === "BUTTON" || t.tagName === "A" || t.tagName === "SELECT" || role === "button" || role === "link" || role === "menuitem" || role === "tab" || role === "option" || role === "checkbox" || role === "radio" || role === "switch" || t.closest('[role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="option"]') ) return } // While a Radix dialog/sheet/alert-dialog is open, only bindings mounted // inside it may fire. Page-level chords stay parked until it closes, so // Escape reaches the overlay alone instead of also running a page // handler. In-overlay bindings (Export, Save) are unaffected. // // The test is the binding's scope, not the event target: a non-modal rail // lets focus sit inside the overlay while page bindings are still // mounted, so a target-based test fires exactly the handlers it should // suppress. if (scope !== "overlay" && wasOverlayOpenAtKeydown()) return e.preventDefault() e.stopPropagation() ref.current(e) } window.addEventListener("keydown", handler) return () => window.removeEventListener("keydown", handler) }, [keys, disabled, scope]) } /* -------------------------------------------------------------------------- * Shortcut parsing + global binding * ------------------------------------------------------------------------ */ type ParsedShortcut = { meta: boolean ctrl: boolean shift: boolean alt: boolean key: string // normalized lowercase, or special like "f2", "backspace", "delete", "enter", "escape", "arrowup"… } function mapKey(raw: string): string { const p = raw.toLowerCase() if (raw === "⌫") return "backspace" if (raw === "⌦") return "delete" if (raw === "⏎" || p === "enter" || p === "return") return "enter" if (raw === "␣" || p === "space") return " " if (p === "esc" || p === "escape") return "escape" if (p === "tab") return "tab" if (p === "up" || raw === "↑") return "arrowup" if (p === "down" || raw === "↓") return "arrowdown" if (p === "left" || raw === "←") return "arrowleft" if (p === "right" || raw === "→") return "arrowright" return p } function parseShortcut(input: string): ParsedShortcut | null { let s = input.trim() if (!s) return null const out: ParsedShortcut = { meta: false, ctrl: false, shift: false, alt: false, key: "" } // Strip leading symbolic modifiers (⌘ ⌃ ⇧ ⌥) which may be glued to the key char. while (s.length) { const c = s[0] if (c === "⌘") { out.meta = true; s = s.slice(1) } else if (c === "⌃") { out.ctrl = true; s = s.slice(1) } else if (c === "⇧") { out.shift = true; s = s.slice(1) } else if (c === "⌥") { out.alt = true; s = s.slice(1) } else break } if (!s) return null // Word-style modifiers (Cmd+Shift+D, Alt P) joined by + or whitespace. if (/[+\s]/.test(s)) { for (const raw of s.split(/[+\s]+/).filter(Boolean)) { const p = raw.toLowerCase() if (raw === "⌘" || p === "cmd" || p === "meta" || p === "command") out.meta = true else if (raw === "⌃" || p === "ctrl" || p === "control") out.ctrl = true else if (raw === "⇧" || p === "shift") out.shift = true else if (raw === "⌥" || p === "alt" || p === "opt" || p === "option") out.alt = true else out.key = mapKey(raw) } } else { out.key = mapKey(s) } return out.key ? out : null } function matchesShortcut(e: KeyboardEvent, s: ParsedShortcut): boolean { if (e.metaKey !== s.meta) return false if (e.ctrlKey !== s.ctrl) return false if (e.altKey !== s.alt) return false if (e.shiftKey !== s.shift) return false return e.key.toLowerCase() === s.key } function DropdownMenuCheckboxItem({ className, children, checked, inset, ...props }: React.ComponentProps & { inset?: boolean }) { return (