diff --git a/CLAUDE.md b/CLAUDE.md index 353df69c..f5f1f5f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -349,9 +349,11 @@ Worked examples: `/soulseek`, `/music`, `/chat`. (`{ key, name, icon, component, availableOnPanel: false }`) and registered in `AppRegistry.tsx`. The Workspace/Panel framework is orthogonal to routing — it contains no route navigation, and panels live inside the Route element tree, so they can call `useParams`/`useSearchParams` directly. -- **Page title by route.** Add a rule to `RULES` in `src/apps/officer-web/state/usePageTitle.ts` - (`{ match: (p) => p.startsWith('/'), title: '' }`, most-specific first); - `usePageTitleSync` does the rest. `startsWith` means nested routes are covered. +- **Page title by route.** Automatic — no rule to add. `titleForPath` in + `src/apps/officer-web/state/usePageTitle.ts` takes the URL's first path segment, turns `kebab-case` + into `Title Case`, and that's the tab title; `usePageTitleSync` does the rest. A screen that wants to + say more than its route does (a chat's own name, not just "Chat") publishes an override instead — + `usePublishPageTitle` in `officerdev`. ### The URL is the source of truth for selection diff --git a/src/apps/officer-web/state/usePageTitle.ts b/src/apps/officer-web/state/usePageTitle.ts index 8abd43f4..bcdc415c 100644 --- a/src/apps/officer-web/state/usePageTitle.ts +++ b/src/apps/officer-web/state/usePageTitle.ts @@ -5,41 +5,21 @@ import { usePageTitleOverride } from 'officerdev'; import { useSessionState, writeSessionValue } from 'hooks/useSessionState'; import { randomId } from 'helpers/random-id'; -type TitleRule = { match: (p: string) => boolean; title: string }; - -// Default page title per route — most specific first. -const RULES: TitleRule[] = [ - { match: (p) => p === '/', title: 'Home' }, - { match: (p) => p.startsWith('/settings/ai'), title: 'AI Settings' }, - { match: (p) => p.startsWith('/settings/profile'), title: 'Profile' }, - { match: (p) => p.startsWith('/settings/integrations'), title: 'Integrations' }, - { match: (p) => p.startsWith('/settings/user-management'), title: 'User Management' }, - { match: (p) => p.startsWith('/settings/system'), title: 'System' }, - { match: (p) => p.startsWith('/settings'), title: 'Settings' }, - { match: (p) => p.startsWith('/chat'), title: 'Chat' }, - { match: (p) => p.startsWith('/files'), title: 'Files' }, - { match: (p) => p.startsWith('/calendar'), title: 'Calendar' }, - { match: (p) => p.startsWith('/contacts'), title: 'Contacts' }, - { match: (p) => p.startsWith('/app-store'), title: 'App store' }, - { match: (p) => p.startsWith('/marketplace'), title: 'Marketplace' }, - { match: (p) => p.startsWith('/plugins'), title: 'Marketplace' }, - { match: (p) => p.startsWith('/command-hq'), title: 'Command HQ' }, - { match: (p) => p.startsWith('/system-monitor'), title: 'System Monitor' }, - { match: (p) => p.startsWith('/qr-transfer'), title: 'QR Transfer' }, - { match: (p) => p.startsWith('/activity'), title: 'Activity' }, - { match: (p) => p.startsWith('/code-editor'), title: 'Code Editor' }, - { match: (p) => p.startsWith('/tasks'), title: 'Tasks' }, - { match: (p) => p.startsWith('/jobs'), title: 'Jobs' }, - { match: (p) => p.startsWith('/skills'), title: 'Skills' }, - { match: (p) => p.startsWith('/processes'), title: 'Processes' }, - { match: (p) => p.startsWith('/dashboards'), title: 'Dashboards' }, - { match: (p) => p.startsWith('/terminal'), title: 'Terminal' }, - { match: (p) => p.startsWith('/browser'), title: 'Browser' }, - { match: (p) => p.startsWith('/desktop'), title: 'Desktop' }, -]; - +// Default page title per route — derived from the path itself rather than a hand-maintained table, so +// a new route (including one an installed plugin brings in at runtime) gets a sane tab title for free +// instead of silently falling through to 'Officer' until somebody remembers to add a rule. +// +// Only the first segment counts — `/settings/ai` and `/settings/profile` both say 'Settings' — because +// that segment is the one every route conventionally owns (see "Frontend route conventions" in the root +// CLAUDE.md). A screen that wants to say more than its route does publishes an override instead; see +// `usePageTitleOverride` below. export function titleForPath(pathname: string): string { - return RULES.find((r) => r.match(pathname))?.title ?? 'Officer'; + const [first] = pathname.split('/').filter(Boolean); + if (!first) return 'Home'; + return first + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); } // ── The name you gave this tab ──