'use client' import type { AnyNodeId, LevelNode } from '@pascal-app/core' import { useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Command, useCommandState } from 'cmdk' import { ChevronRight, Search } from 'lucide-react' import type { ReactNode } from 'react' import { useEffect, useState } from 'react' import { create } from 'zustand' import { useShallow } from 'zustand/shallow' import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog' import { getLevelDisplayName } from '@pascal-app/core' import { useCommandRegistry } from '../../../store/use-command-registry' import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry' // --------------------------------------------------------------------------- // Open + navigation state store // --------------------------------------------------------------------------- interface CommandPaletteStore { open: boolean setOpen: (open: boolean) => void /** Current rendering mode. 'command' = normal palette; anything else = registered mode view. */ mode: string setMode: (mode: string) => void pages: string[] inputValue: string setInputValue: (value: string) => void navigateTo: (page: string) => void goBack: () => void } export const useCommandPalette = create((set, get) => ({ open: false, setOpen: (open) => { set({ open }) if (!open) set({ pages: [], inputValue: '', mode: 'command' }) }, mode: 'command', setMode: (mode) => set({ mode }), pages: [], inputValue: '', setInputValue: (value) => set({ inputValue: value }), navigateTo: (page) => set((s) => ({ pages: [...s.pages, page], inputValue: '' })), goBack: () => { set((s) => ({ pages: s.pages.slice(0, -1), inputValue: '' })) }, })) // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function resolve(value: string | (() => string)): string { return typeof value === 'function' ? value() : value } function Shortcut({ keys }: { keys: string[] }) { return ( {keys.map((k) => ( {k} ))} ) } function Item({ icon, label, onSelect, shortcut, disabled = false, keywords = [], badge, navigate = false, }: { icon: React.ReactNode label: string | (() => string) onSelect: () => void shortcut?: string[] disabled?: boolean keywords?: string[] badge?: string | (() => string) navigate?: boolean }) { const resolvedLabel = resolve(label) const resolvedBadge = badge ? resolve(badge) : undefined return ( {icon} {resolvedLabel} {resolvedBadge && ( {resolvedBadge} )} {shortcut && } {(resolvedBadge || navigate) && ( )} ) } function OptionItem({ label, isActive = false, onSelect, icon, disabled = false, }: { label: string isActive?: boolean onSelect: () => void icon?: React.ReactNode disabled?: boolean }) { return ( {isActive ?
: icon} {label} ) } // --------------------------------------------------------------------------- // Sub-page label map // --------------------------------------------------------------------------- const PAGE_LABEL: Record = { 'wall-mode': 'Wall Mode', 'level-mode': 'Level Mode', 'rename-level': 'Rename Level', 'goto-level': 'Go to Level', } // --------------------------------------------------------------------------- // Empty state fallback (force-mounted, visible only when no results) // --------------------------------------------------------------------------- export interface CommandPaletteEmptyAction { icon: ReactNode label: (query: string) => string onSelect: (query: string) => void } function EmptyActionItem({ action }: { action: CommandPaletteEmptyAction }) { const count = useCommandState((s) => s.filtered.count) const search = useCommandState((s) => s.search) if (count > 0) return null // No Command.Group wrapper — groups hide themselves when not in filtered.groups (which is // empty when nothing matches), swallowing the force-mounted item even with forceMount on // the item itself. return ( action.onSelect(search)} value="__empty_action__" > {action.icon} {action.label(search)} ) } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEmptyAction }) { const { open, setOpen, mode, setMode, pages, inputValue, setInputValue, navigateTo, goBack } = useCommandPalette() const [meta, setMeta] = useState('⌘') const [isFullscreen, setIsFullscreen] = useState(false) const page = pages[pages.length - 1] const actions = useCommandRegistry((s) => s.actions) const views = usePaletteViewRegistry((s) => s.views) const activeLevelId = useViewer((s) => s.selection.levelId) const wallMode = useViewer((s) => s.wallMode) const setWallMode = useViewer((s) => s.setWallMode) const levelMode = useViewer((s) => s.levelMode) const setLevelMode = useViewer((s) => s.setLevelMode) const allLevels = useScene( useShallow((s) => (Object.values(s.nodes).filter((n) => n.type === 'level') as LevelNode[]).sort( (a, b) => a.level - b.level, ), ), ) // Platform detection useEffect(() => { setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? '⌘' : 'Ctrl') }, []) // Fullscreen tracking useEffect(() => { const handler = () => setIsFullscreen(!!document.fullscreenElement) document.addEventListener('fullscreenchange', handler) return () => document.removeEventListener('fullscreenchange', handler) }, []) // Cmd/Ctrl+K global shortcut useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault() setOpen(true) } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [setOpen]) const run = (fn: () => void) => { fn() setOpen(false) } const wallModeLabel: Record<'cutaway' | 'up' | 'down' | 'translucent', string> = { cutaway: 'Cutaway', up: 'Up', down: 'Down', translucent: 'Translucent', } const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = { manual: 'Manual', stacked: 'Stacked', exploded: 'Exploded', solo: 'Solo', } const confirmRename = () => { if (!(activeLevelId && inputValue.trim())) return run(() => { useScene.getState().updateNode(activeLevelId as AnyNodeId, { name: inputValue.trim() } as any) }) } // --------------------------------------------------------------------------- // Group registered actions by group (preserving insertion order) // --------------------------------------------------------------------------- const grouped = actions.reduce>((acc, action) => { const list = acc.get(action.group) ?? [] list.push(action) acc.set(action.group, list) return acc }, new Map()) const onClose = () => setOpen(false) const onBack = () => { if (mode !== 'command') { setMode('command') } else { goBack() } } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- // Mode view: replaces the entire cmdk shell const modeView = mode !== 'command' ? views.get(mode) : undefined return ( Command Palette {modeView && } {!modeView && ( { if (e.key === 'Backspace' && !inputValue && pages.length > 0) { e.preventDefault() goBack() } }} shouldFilter={page !== 'rename-level'} > {/* Search bar */}
{page && ( )}
{(!emptyAction || page) && ( No commands found. )} {emptyAction && !page && } {/* ── Registered page view (e.g. 'ai') ─────────────────────── */} {page && views.get(page)?.type === 'page' && (() => { const pageView = views.get(page) return pageView ? : null })()} {/* ── Root view: render from registry ───────────────────────── */} {!page && Array.from(grouped.entries()).map(([group, groupActions]) => ( {groupActions.map((action) => ( action.execute()} shortcut={action.shortcut} /> ))} ))} {/* ── Wall Mode sub-page ────────────────────────────────────── */} {page === 'wall-mode' && ( {(['cutaway', 'up', 'down', 'translucent'] as const).map((mode) => ( run(() => setWallMode(mode))} /> ))} )} {/* ── Level Mode sub-page ───────────────────────────────────── */} {page === 'level-mode' && ( {(['stacked', 'exploded', 'solo'] as const).map((mode) => ( run(() => setLevelMode(mode))} /> ))} )} {/* ── Go to Level sub-page ──────────────────────────────────── */} {page === 'goto-level' && ( {allLevels.map((level) => ( run(() => useViewer.getState().setSelection({ levelId: level.id })) } /> ))} )} {/* ── Rename Level sub-page ─────────────────────────────────── */} {page === 'rename-level' && ( {inputValue.trim() ? ( <> Rename to "{inputValue.trim()}" ) : ( Type a new name above… )} )} {/* Footer hint */}
navigate select {page ? ( back ) : ( close )}
)}
) }