diff --git a/README.md b/README.md index 179a515..bef9b08 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Regenerate with `pnpm screenshots` (UI + API running; default password `workbenc **Intent:** Answer "who is blocked, and what should I do next?" without hunting through every page. -**How it works:** Pick a cycle to see pipeline status (self / peer / manager), deadline nudges for missing self reviews, and a compact cycle dashboard (writing time, evidence coverage, thin dossiers). Links out to writing desks, backfill, and team performance when the numbers look thin. **Needs director attention** surfaces flags only (stagnant ratings, thin evidence, promo-ready, open concern counts) with a jump into consistency / director packs — no private note bodies. +**How it works:** Pick a cycle to see pipeline status (self / peer / manager), deadline nudges for missing self reviews, and a compact cycle dashboard (writing time, evidence coverage, thin dossiers). Links out to writing desks, backfill, and team performance when the numbers look thin. **Needs director attention** surfaces flags only (stagnant ratings, thin evidence, promo-ready, open concern counts) with a jump into consistency / director packs — no private note bodies. Sticky **On this page** jumps between cycle, attention, dashboard, and queue. ![Command center](./docs/screenshots/02-home.png) @@ -111,7 +111,7 @@ Regenerate with `pnpm screenshots` (UI + API running; default password `workbenc **Intent:** Build a citation pool all year so the writing desk is not empty in week one of reviews. -**How it works:** Capture achievements, goals, feedback (with optional 1:1 theme tags), documents, concern / underperformance notes, and external links (for example Jira or Bitbucket sync). The **Timeline** panel stitches those events with manager reviews and rating decisions so you can prep a 1:1 without opening five pages. Visibility controls keep EM-only material out of subject exports. AI digests and drafts pull from this pool (and from concerns only when you opt them in). +**How it works:** Capture achievements, goals, feedback (with optional 1:1 theme tags), documents, concern / underperformance notes, and external links (for example Jira or Bitbucket sync). The **Timeline** panel stitches those events with manager reviews and rating decisions so you can prep a 1:1 without opening five pages. Visibility controls keep EM-only material out of subject exports. AI digests and drafts pull from this pool (and from concerns only when you opt them in). Sticky **On this page** jumps across timeline, integrations, and capture sections. ![Person dossier](./docs/screenshots/04-person.png) diff --git a/apps/ui/src/components/PageSectionNav.tsx b/apps/ui/src/components/PageSectionNav.tsx index b09c69a..0b46a24 100644 --- a/apps/ui/src/components/PageSectionNav.tsx +++ b/apps/ui/src/components/PageSectionNav.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; export type SectionNavItem = { id: string; @@ -14,6 +14,11 @@ export function PageSectionNav({ }) { const [activeId, setActiveId] = useState(items[0]?.id ?? ""); const sectionKey = items.map((item) => item.id).join("|"); + const clickLockUntil = useRef(0); + + useEffect(() => { + if (items[0]?.id) setActiveId(items[0].id); + }, [sectionKey, items]); useEffect(() => { const nodes = items @@ -24,6 +29,7 @@ export function PageSectionNav({ const ratios = new Map(); const observer = new IntersectionObserver( (entries) => { + if (Date.now() < clickLockUntil.current) return; for (const entry of entries) { ratios.set(entry.target.id, entry.isIntersecting ? entry.intersectionRatio : 0); } @@ -55,6 +61,7 @@ export function PageSectionNav({ function goTo(id: string) { const el = document.getElementById(id); if (!el) return; + clickLockUntil.current = Date.now() + 700; setActiveId(id); el.scrollIntoView({ behavior: "smooth", block: "start" }); } diff --git a/apps/ui/src/components/PageSectionsLayout.tsx b/apps/ui/src/components/PageSectionsLayout.tsx new file mode 100644 index 0000000..8f99830 --- /dev/null +++ b/apps/ui/src/components/PageSectionsLayout.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import { PageSectionNav, type SectionNavItem } from "./PageSectionNav"; + +/** Sticky right-rail (or top chips on phone) for long multi-section pages. */ +export function PageSectionsLayout({ + sections, + children, + ariaLabel = "On this page", +}: { + sections: SectionNavItem[]; + children: ReactNode; + ariaLabel?: string; +}) { + if (sections.length < 2) { + return
{children}
; + } + return ( +
+
{children}
+ +
+ ); +} + +export type { SectionNavItem }; diff --git a/apps/ui/src/pages/ConsistencyPage.tsx b/apps/ui/src/pages/ConsistencyPage.tsx index c67dfed..3fb640b 100644 --- a/apps/ui/src/pages/ConsistencyPage.tsx +++ b/apps/ui/src/pages/ConsistencyPage.tsx @@ -5,6 +5,7 @@ import { competencyLabel } from "@prm/shared"; import { api } from "../lib/api"; import { downloadBinaryExport, openHtmlExport } from "../lib/exports"; import { NextStep, PageHeader } from "../components/PageChrome"; +import { PageSectionsLayout } from "../components/PageSectionsLayout"; export function ConsistencyPage() { const [params, setParams] = useSearchParams(); @@ -94,8 +95,18 @@ export function ConsistencyPage() { - {data && ( -
+ + {!data ? ( +

Loading…

+ ) : ( + +

Calibration bundles

Director round-trip: export cards, capture decisions in the form (or paste JSON), then import @@ -309,12 +320,7 @@ export function ConsistencyPage() { {calibMsg &&

{calibMsg}

}
- )} - {!data ? ( -

Loading…

- ) : ( - <> {thin.length > 0 ? ( )} -
+

Overall distribution

{["1", "2", "3", "4", "5", "unset"].map((bucket) => { @@ -356,7 +362,7 @@ export function ConsistencyPage() {
-
+
{data.cards.map((card) => (
@@ -395,7 +401,7 @@ export function ConsistencyPage() {
))}
- + )}
); diff --git a/apps/ui/src/pages/CyclesPage.tsx b/apps/ui/src/pages/CyclesPage.tsx index 88a31b3..36a7f47 100644 --- a/apps/ui/src/pages/CyclesPage.tsx +++ b/apps/ui/src/pages/CyclesPage.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import type { CycleDTO, PersonDTO } from "@prm/shared"; import { api } from "../lib/api"; import { NextStep, PageHeader } from "../components/PageChrome"; +import { PageSectionsLayout } from "../components/PageSectionsLayout"; const PHASE_STATUSES = [ { value: "self_open", label: "Self open" }, @@ -81,7 +82,15 @@ export function CyclesPage() { /> )} -
+ +

Cycles

{cycles.length === 0 ? (

No cycles yet.

@@ -156,7 +165,7 @@ export function CyclesPage() {
{selected && ( -
+

Phase wizard — {selected.name}

Soft gates only: advance phases when ready. Waive missing self/peer from the writing desk with a reason. @@ -266,7 +275,7 @@ export function CyclesPage() { -

+

2. Import completed bundle

@@ -320,6 +329,7 @@ export function CyclesPage() { action={Open command center} /> )} +
); } diff --git a/apps/ui/src/pages/HomePage.tsx b/apps/ui/src/pages/HomePage.tsx index 27b8210..0556566 100644 --- a/apps/ui/src/pages/HomePage.tsx +++ b/apps/ui/src/pages/HomePage.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import type { CycleDTO, CycleStatsDTO, DeadlineNudge, DirectorAttentionDTO, DueThisWeekItem, WorkspaceStatus } from "@prm/shared"; import { api } from "../lib/api"; import { NextStep, PageHeader } from "../components/PageChrome"; +import { PageSectionsLayout } from "../components/PageSectionsLayout"; import { OnboardingWizard } from "../components/OnboardingWizard"; type CommandItem = { @@ -246,7 +247,19 @@ export function HomePage() { {next} -
+ 0 + ? [{ id: "home-attention", label: "Attention" }] + : []), + ...(dueWeek.length > 0 ? [{ id: "home-due", label: "Due" }] : []), + ...(cycleStats ? [{ id: "home-dashboard", label: "Dashboard" }] : []), + ...(cycleId && nudges.length > 0 ? [{ id: "home-nudges", label: "Nudges" }] : []), + { id: "home-queue", label: "Queue" }, + ]} + > +