import { useMemo } from 'react'; import type { LucideIcon } from 'lucide-react'; import { Bot, CalendarDays, Cpu, Globe, KeyRound, LayoutGrid, Lock, Mail, ScanText, ShieldCheck, Sparkles, User, Users, Volume2, Wrench, } from 'lucide-react'; import { usePermissions } from 'hooks/usePermissions'; import { useTranslation, type AppTranslationKey } from '@/lib/i18n'; import { useDockItems } 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; /** Resolved for the active locale, because `rank()` matches what the reader can actually see. */ 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. */ type SettingsItem = Omit & { labelKey: AppTranslationKey; hintKey?: AppTranslationKey; }; const SETTINGS_ITEMS: SettingsItem[] = [ // Profile — ProfileSettings/index.tsx { id: 'set:profile', labelKey: 'settings.sections.profile.label', to: '/settings/profile', hintKey: 'settings.sections.profile.hint', icon: User, }, { id: 'set:password', labelKey: 'settings.sections.changePassword.label', to: '/settings/profile/change-password', icon: Lock, }, { id: 'set:api-keys', labelKey: 'settings.sections.apiKeys.label', to: '/settings/profile/api-keys', hintKey: 'settings.sections.apiKeys.hint', icon: KeyRound, }, { id: 'set:languages', labelKey: 'settings.sections.languages.label', to: '/settings/profile/languages', hintKey: 'settings.sections.languages.hint', icon: Globe, }, { id: 'set:voice', labelKey: 'settings.sections.voice.label', to: '/settings/profile/voice', hintKey: 'settings.sections.voice.hint', icon: Volume2, }, { id: 'set:dock', labelKey: 'settings.sections.dock.label', to: '/settings/profile/dock', hintKey: 'settings.sections.dock.hint', icon: LayoutGrid, }, // AI — AISettings.tsx { id: 'set:ai-my-models', labelKey: 'settings.sections.myModels.label', to: '/settings/ai/my-models', icon: Sparkles, }, { id: 'set:ai-channel-models', labelKey: 'settings.sections.channelModels.label', to: '/settings/ai/member-models', icon: Sparkles, }, { id: 'set:ai-default-models', labelKey: 'settings.sections.defaultModels.label', to: '/settings/ai/default-models', icon: Sparkles, }, { id: 'set:ai-harnesses', labelKey: 'settings.sections.providers.label', to: '/settings/ai/ai-harnesses', hintKey: 'settings.sections.providers.hint', icon: Bot, }, // System — SystemSettings.tsx { id: 'set:tts', labelKey: 'settings.sections.tts.label', to: '/settings/system/tts', icon: Volume2 }, { id: 'set:ocr', labelKey: 'settings.sections.ocr.label', to: '/settings/system/ocr', icon: ScanText }, { id: 'set:maintenance', labelKey: 'settings.sections.maintenance.label', to: '/settings/system/maintenance', hintKey: 'settings.sections.maintenance.hint', icon: Cpu, }, { id: 'set:smtp', labelKey: 'settings.sections.smtp.label', to: '/settings/system/smtp', hintKey: 'settings.sections.smtp.hint', icon: Mail, }, // Integrations — IntegrationsSettings { id: 'set:google-oauth', labelKey: 'settings.sections.googleOauth.label', to: '/settings/integrations/google-oauth', icon: KeyRound, }, { id: 'set:apify', labelKey: 'settings.sections.apify.label', to: '/settings/integrations/apify', icon: Wrench }, // DAV sync setup left with the caldav plugin on 2026-08-27 — it is a modal on the Calendar screen now, // and Spotlight has no way to open a plugin's modal. `bun run docs:check` caught this one: it compares // every settings route Spotlight offers against the sections that actually exist. // Users — UserManagement { id: 'set:users', labelKey: 'settings.sections.accounts.label', to: '/settings/user-management/users', hintKey: 'settings.sections.accounts.hint', icon: Users, }, { id: 'set:permissions', labelKey: 'settings.sections.permissions.label', 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(); const { t } = useTranslation(); const dockItems = useDockItems(plugins); return useMemo(() => { const fromDock: SpotlightItem[] = dockItems .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: SpotlightItem[] = SETTINGS_ITEMS.filter((item) => canVisit(item.to)).map( ({ labelKey, hintKey, ...item }) => ({ ...item, label: t(labelKey), ...(hintKey ? { hint: t(hintKey) } : {}), }), ); return [...fromDock, ...settings]; }, [canVisit, dockItems, t]); }