import { useMemo } from 'react'; import type { LucideIcon } from 'lucide-react'; import { Bot, CalendarDays, Cpu, Globe, KeyRound, LayoutGrid, Lock, Mail, Mic, ScanText, ShieldCheck, Sparkles, User, Users, Volume2, Wrench, } from 'lucide-react'; import { usePermissions } from 'hooks/usePermissions'; import { CORE_DOCK_ITEMS, dockItemsFromPlugins } from '../Dock'; // What Spotlight can take you to. // // ── It indexes the DOCK, rather than a list of its own ── // // The dock is already the composed, permission-filtered answer to "what can this account open here": // the shell's own screens, plus whatever installed plugins contribute, minus anything `canVisit` refuses. // Building a second list would mean two answers to one question, and the second would be wrong the day // somebody installs a plugin — which is the whole failure the dock's own comment warns about, arrived at // from a third direction. // // So a new plugin appears in Spotlight for the same reason it appears in the dock: because it is // installed. Nothing here is edited. // // Settings sections are added because they are the things people hunt for and the dock deliberately does // not carry. They are static: a settings screen is shell, not a plugin. export type SpotlightItem = { /** Stable across renders — used as the React key and for the recency store. */ id: string; label: string; /** Where it goes. Matched against `canVisit` for anything not already filtered. */ to: string; /** Shown small beside the label, so two "Profile"s are distinguishable. */ hint?: string; icon?: LucideIcon; /** Plugin artwork, when the tile has some. */ image?: string; color?: string; /** * Copied from the dock tile, and it has to be. * * `bare` means the artwork brings its own frame — calendar, contacts, and every plugin shipping a logo. * Dropping it here painted a coloured swatch behind those logos, which the dock's own comment calls out * as reading like a mistake. It was dropped, so Spotlight looked like a different app's list of the same * things. */ tile?: 'badge' | 'bare'; }; /** * The settings sections, which the dock does not carry. * * Hand-listed rather than derived, and that is a real cost: a section added to `IntegrationsSettings` * will not appear here until someone adds it. The alternative is exporting every settings screen's * section array up into the shell, which couples five screens to this one to save five lines. * * IT DRIFTED, and within two days. The first version listed nine rows against a settings area that has * twenty-one, missed Languages and Voice entirely, pointed `/settings/users` at a route that does not * exist (it is `/settings/user-management/users`), and gave four different rows the same User glyph — * which is most of why the whole panel read as a generic list rather than as the app's own. * * So the cost is now measured rather than predicted, and deriving these from the screens is the fix if it * happens a third time. Each group below names the file it is copied from, so checking is one grep. * * `hint` is what makes them findable — typing "keys" should reach the API keys screen, and the label alone * does not say where it lives. */ const SETTINGS_ITEMS: SpotlightItem[] = [ // Profile — ProfileSettings/index.tsx { id: 'set:profile', label: 'Profile', to: '/settings/profile', hint: 'avatar, name, email', icon: User }, { id: 'set:password', label: 'Change password', to: '/settings/profile/change-password', icon: Lock }, { id: 'set:api-keys', label: 'API keys', to: '/settings/profile/api-keys', hint: 'keys that sign in as you', icon: KeyRound, }, { id: 'set:languages', label: 'Languages', to: '/settings/profile/languages', hint: 'spoken and translation', icon: Globe, }, { id: 'set:voice', label: 'Voice', to: '/settings/profile/voice', hint: 'text-to-speech', icon: Volume2 }, { id: 'set:dock', label: 'Dock', to: '/settings/profile/dock', hint: 'choose and reorder', icon: LayoutGrid }, // AI — AISettings.tsx { id: 'set:ai-my-models', label: 'My Models', to: '/settings/ai/my-models', icon: Sparkles }, { id: 'set:ai-channel-models', label: 'Channel Models', to: '/settings/ai/member-models', icon: Sparkles }, { id: 'set:ai-default-models', label: 'Default Models', to: '/settings/ai/default-models', icon: Sparkles }, { id: 'set:ai-harnesses', label: 'Providers', to: '/settings/ai/ai-harnesses', hint: 'harnesses', icon: Bot }, // System — SystemSettings.tsx { id: 'set:tts', label: 'Text to Speech', to: '/settings/system/tts', icon: Volume2 }, { id: 'set:stt', label: 'Speech to Text', to: '/settings/system/stt', icon: Mic }, { id: 'set:ocr', label: 'OCR', to: '/settings/system/ocr', icon: ScanText }, { id: 'set:maintenance', label: 'Rebuild & restart', to: '/settings/system/maintenance', hint: 'maintenance', icon: Cpu, }, { id: 'set:smtp', label: 'SMTP', to: '/settings/system/smtp', hint: 'outgoing mail', icon: Mail }, // Integrations — IntegrationsSettings { id: 'set:google-oauth', label: 'Google OAuth', to: '/settings/integrations/google-oauth', icon: KeyRound }, { id: 'set:apify', label: 'Apify', to: '/settings/integrations/apify', icon: Wrench }, { id: 'set:dav', label: 'Calendar & Contacts sync', to: '/settings/integrations/dav-app-passwords', hint: 'app passwords', icon: CalendarDays, }, // Users — UserManagement { id: 'set:users', label: 'Accounts', to: '/settings/user-management/users', hint: 'users', icon: Users }, { id: 'set:permissions', label: 'Permissions', to: '/settings/user-management/permissions', icon: ShieldCheck }, ]; /** Everything Spotlight can offer this account, in a stable order. */ export function useSpotlightItems(): SpotlightItem[] { const { canVisit, plugins } = usePermissions(); return useMemo(() => { const fromDock: SpotlightItem[] = [...CORE_DOCK_ITEMS, ...dockItemsFromPlugins(plugins)] .filter((item) => canVisit(item.to)) .map((item) => ({ id: `nav:${item.to}`, label: item.label, to: item.to, icon: item.icon, image: item.image, color: item.color, tile: item.tile, })); // Settings go through `canVisit` too. `usePermissions` fails OPEN by design — hiding a door is a // courtesy and the 403 is the lock — so this cannot lock anyone out of their own settings. const settings = SETTINGS_ITEMS.filter((item) => canVisit(item.to)); return [...fromDock, ...settings]; }, [canVisit, plugins]); }