diff --git a/AGENTS.md b/AGENTS.md
index c82ffae..d4ed9d3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,7 +15,7 @@ Performance Review Manager — a local, single-user ("solo EM") pnpm monorepo. S
### Data / running
- Local data lives in `/workspace/data` (gitignored): SQLite DB, encrypted vault, uploaded files. Delete it to reset to a clean state (stop the API first, since it holds the DB handle).
-- First app load shows "Create & seed demo team" (seeds an 8-person org). After init it shows "Unlock". Default seed password: `workbench`.
+- First app load offers **Empty workspace** or **Demo team** (seeds an 8-person org). After init it shows "Unlock". Default demo password: `workbench`.
- Locking the workspace ("Lock & encrypt") seals the SQLite DB + files to `.enc` at rest; unlocking decrypts them.
- `.session` stores the auth token only (not the AES key). After an API process restart you must unlock again even if the UI still has a token.
diff --git a/README.md b/README.md
index ec74a41..e156a18 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ pnpm dev
| API | http://127.0.0.1:8787 |
| Seed password | `workbench` |
-First load: **Create & seed demo team** (8-person org), then unlock. Between sessions use **Lock & encrypt**.
+First load: choose **Empty workspace** or **Demo team** (8-person org, password default `workbench`), then unlock later sessions with your password. Between sessions use **Lock & encrypt**.
Requires **pnpm 10.33.3** (see [`AGENTS.md`](./AGENTS.md) if Corepack hands you pnpm 11).
@@ -85,7 +85,7 @@ Regenerate with `pnpm screenshots` (UI + API running; default password `workbenc
**Intent:** Keep the workspace sealed when you walk away. The unlock gate is the trust boundary for local data.
-**How it works:** On first run you create a workspace password and can seed a demo org. Later sessions decrypt the vault with that password. **Lock & encrypt** re-seals SQLite and files so a stolen laptop disk is not a readable review archive.
+**How it works:** On first run you create a workspace password and choose an empty vault or a seeded demo org. Later sessions decrypt the vault with that password. **Lock & encrypt** re-seals SQLite and files so a stolen laptop disk is not a readable review archive.

@@ -199,7 +199,7 @@ Regenerate with `pnpm screenshots` (UI + API running; default password `workbenc
**Intent:** Configure providers, privacy gates, appearance, and vault maintenance in one place.
-**How it works:** Set AI provider and models (Anthropic, OpenAI, Cursor gateway, Ollama), toggle egress for private notes / meetings / document extracts, manage Jira and Bitbucket credentials, run encrypted backup / restore, verify the activity log, and switch dark / light theme.
+**How it works:** Set AI provider and models (Anthropic, OpenAI, Cursor gateway, Ollama), toggle egress for private notes / meetings / document extracts, manage Jira and Bitbucket credentials, run encrypted backup / restore, verify the activity log, and choose appearance (system / dark / light plus Teal, Ocean, Forest, or Slate accent).

diff --git a/apps/ui/index.html b/apps/ui/index.html
index 2e61709..740b6e3 100644
--- a/apps/ui/index.html
+++ b/apps/ui/index.html
@@ -1,26 +1,44 @@
-
+
-
+
Performance Review Manager
-
+
diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx
index e84d86a..02bb262 100644
--- a/apps/ui/src/App.tsx
+++ b/apps/ui/src/App.tsx
@@ -1,4 +1,4 @@
-import { NavLink, Navigate, Route, Routes } from "react-router-dom";
+import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom";
import { useEffect, useState } from "react";
import type { WorkspaceStatus } from "@prm/shared";
import { api, getToken, setToken, waitForApi } from "./lib/api";
@@ -20,10 +20,60 @@ import { SprintPulsePage } from "./pages/SprintPulsePage";
import { SearchPage } from "./pages/SearchPage";
import { BackfillPage } from "./pages/BackfillPage";
+const NAV = [
+ {
+ label: "Work",
+ links: [
+ { to: "/", end: true, label: "Home" },
+ { to: "/team", label: "Team" },
+ { to: "/roles", label: "Roles" },
+ { to: "/cycles", label: "Cycles" },
+ { to: "/promotions", label: "Promotions" },
+ ],
+ },
+ {
+ label: "Review",
+ links: [
+ { to: "/consistency", label: "Consistency" },
+ { to: "/team-performance", label: "Team performance" },
+ { to: "/sprint-pulse", label: "Sprint pulse" },
+ { to: "/search", label: "Search" },
+ { to: "/backfill", label: "Backfill" },
+ ],
+ },
+ {
+ label: "Tools",
+ links: [
+ { to: "/chat", label: "AI Chat" },
+ { to: "/settings", label: "Settings" },
+ ],
+ },
+] as const;
+
export function App() {
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
- const { theme, toggle } = useTheme();
+ const [navOpen, setNavOpen] = useState(false);
+ const { resolved, toggle } = useTheme();
+ const location = useLocation();
+
+ useEffect(() => {
+ setNavOpen(false);
+ }, [location.pathname]);
+
+ useEffect(() => {
+ if (!navOpen) return;
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") setNavOpen(false);
+ }
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ window.addEventListener("keydown", onKey);
+ return () => {
+ document.body.style.overflow = prev;
+ window.removeEventListener("keydown", onKey);
+ };
+ }, [navOpen]);
async function refresh() {
try {
@@ -45,10 +95,10 @@ export function App() {
type="button"
className="btn ghost theme-toggle"
onClick={toggle}
- aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
- title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
+ aria-label={resolved === "dark" ? "Switch to light mode" : "Switch to dark mode"}
+ title={resolved === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
- {theme === "dark" ? "Light mode" : "Dark mode"}
+ {resolved === "dark" ? "Light mode" : "Dark mode"}
);
@@ -57,7 +107,6 @@ export function App() {
const logPath = typeof window !== "undefined" ? window.__PRM_DESKTOP_LOG__ : undefined;
const apiBase = typeof window !== "undefined" ? window.__PRM_API_BASE_URL__ : undefined;
async function retryDesktop() {
- // Prefer same-origin UI served by the sidecar once it is healthy.
const base = (apiBase || "http://127.0.0.1:8787").replace(/\/$/, "");
try {
const res = await fetch(`${base}/api/health`);
@@ -90,7 +139,7 @@ export function App() {
)}