page title: derive from the route path instead of a hand-maintained table
RULES was a hardcoded map from every route prefix to a title, which meant a new route silently fell through to 'Officer' until somebody remembered to add an entry — the gap that hit installed-plugin routes, which had no entry at all. titleForPath now takes the URL's first path segment and title-cases it, so every route gets a sane tab title for free, including ones a plugin adds at runtime. Trade-off: a few routes lose their hand-tuned casing (QR Transfer -> Qr Transfer, Command HQ -> Command Hq) and settings sub-routes collapse to the one 'Settings' title instead of a distinct one per section — both accepted as the point of removing the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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('/<name>'), title: '<Name>' }`, 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
|
||||
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
Reference in New Issue
Block a user