document the navigation conventions in claude.md
the url-as-source-of-truth rules from docs/navigation-audit.md were only in the audit; anything new was as likely to reach for a selection channel as a link. also records the workspace-barrel requirement for shared route helpers, and adds officer-headscale to the pm2 list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ Long-running and privileged work lives in **sidecars**: separate processes that
|
|||||||
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
|
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
|
||||||
(`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`,
|
(`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`,
|
||||||
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
||||||
`officer-slskd`.
|
`officer-slskd`, `officer-headscale`.
|
||||||
|
|
||||||
**`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic
|
**`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic
|
||||||
credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry
|
credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry
|
||||||
@@ -205,14 +205,49 @@ Worked examples: `/soulseek`, `/music`, `/chat`.
|
|||||||
that pins `appType`s to an allow-list. Panels are windowed apps under
|
that pins `appType`s to an allow-list. Panels are windowed apps under
|
||||||
`src/workspaces/officerdev/src/apps/<Feature>/`, each exporting `appRegistryMetas`
|
`src/workspaces/officerdev/src/apps/<Feature>/`, each exporting `appRegistryMetas`
|
||||||
(`{ key, name, icon, component, availableOnPanel: false }`) and registered in `AppRegistry.tsx`.
|
(`{ key, name, icon, component, availableOnPanel: false }`) and registered in `AppRegistry.tsx`.
|
||||||
Panels coordinate via `usePanelChannel`.
|
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`
|
- **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);
|
(`{ match: (p) => p.startsWith('/<name>'), title: '<Name>' }`, most-specific first);
|
||||||
`usePageTitleSync` does the rest.
|
`usePageTitleSync` does the rest. `startsWith` means nested routes are covered.
|
||||||
|
|
||||||
|
### The URL is the source of truth for selection
|
||||||
|
|
||||||
|
`docs/navigation-audit.md` is the authority here — read it before building a screen that selects
|
||||||
|
things. It names the **"opaque click" anti-pattern**: an element that opens something addressable but
|
||||||
|
keeps the id in an onClick closure instead of the DOM, leaves the URL unchanged, holds the selection
|
||||||
|
in `usePanelChannel`/`useGlobal`, and has no anchor semantics (no cmd-click, no middle-click, not
|
||||||
|
link-focusable). Half the app still does this; none of the new code should.
|
||||||
|
|
||||||
|
- **Addressable state goes in the URL** (`useParams` / `useSearchParams`), never in a channel.
|
||||||
|
`usePanelChannel` is for genuine signals and refresh buses (`preview:refresh`,
|
||||||
|
`files:refresh-signal`, `SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`, `FILE_VIEWER_CHANNEL`).
|
||||||
|
"Which thing is open" is a URL. Panels each read the URL rather than passing it between themselves.
|
||||||
|
- **Rows and nav items are real links.** `<Link>` for rows (exemplar: the `/chat` session list,
|
||||||
|
`f35c145`); **react-router's `<NavLink>`** for nav chrome, so active state comes from the router.
|
||||||
|
The hand-rolled `isActive` in `Dock`/`Header` is scheduled for replacement (audit Phase 4) — don't
|
||||||
|
copy it. A disabled entry renders as a `<span>`; a disabled `<a>` is not a thing. A control that
|
||||||
|
*mutates* rather than navigates stays a `<button>`.
|
||||||
|
- **Route pairs.** A bare screen route plus a param route rendering the same component: `/chat` +
|
||||||
|
`/chat/:sessionId`, `/jobs` + `/jobs/:id`, `/email` + `/email/:emailId`, `/headscale` +
|
||||||
|
`/headscale/:section`. One `<Navigate … replace />` guard in the screen, placed after all hooks,
|
||||||
|
canonicalises both the bare route and a bogus param.
|
||||||
|
- **Master list + live preview uses `?selected=<id>`**, not the detail route — linking rows straight
|
||||||
|
to `/x/:id` destroys preview-on-list, because that route is the full page. Rows link to
|
||||||
|
`/x?selected=id` on-page and `/x/:id` off-page; list, mobile panel and preview all read the param.
|
||||||
|
Action buttons are **siblings** of the anchor, never nested inside it.
|
||||||
|
- **`src/workspaces/components/NavLink.tsx` is not react-router's `NavLink`** — it's a `<Link>`
|
||||||
|
wrapper that appends `useGlobalQueryString()` and provides no active state. Import react-router's
|
||||||
|
when you want `isActive`.
|
||||||
|
- Route helpers shared between an `officerdev` panel app and an `officer-web` screen must be
|
||||||
|
re-exported from `src/workspaces/officerdev/src/index.ts` (named exports only — the barrel
|
||||||
|
deliberately avoids `export *` for app modules to keep `appRegistryMetas` from colliding).
|
||||||
|
|
||||||
## Further Reading
|
## Further Reading
|
||||||
|
|
||||||
- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale
|
- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale
|
||||||
|
- `docs/navigation-audit.md` — **authoritative** on routing/navigation: the opaque-click anti-pattern,
|
||||||
|
a severity-ranked findings table, the channel-selection map and the four-phase plan
|
||||||
- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree**
|
- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree**
|
||||||
- `src/apps/CLAUDE.md` — shared frontend patterns
|
- `src/apps/CLAUDE.md` — shared frontend patterns
|
||||||
- `src/databases/CLAUDE.md` — database patterns
|
- `src/databases/CLAUDE.md` — database patterns
|
||||||
|
|||||||
Reference in New Issue
Block a user