diff --git a/src/app/(app)/o/[orgSlug]/p/[projectSlug]/change-requests/change-requests-client.tsx b/src/app/(app)/o/[orgSlug]/p/[projectSlug]/change-requests/change-requests-client.tsx
index 0d01f4fd7..63c51dd59 100644
--- a/src/app/(app)/o/[orgSlug]/p/[projectSlug]/change-requests/change-requests-client.tsx
+++ b/src/app/(app)/o/[orgSlug]/p/[projectSlug]/change-requests/change-requests-client.tsx
@@ -22,7 +22,6 @@ import { toast } from "sonner";
import { EmptyState } from "@/components/ui/empty-state";
import { ModalShell, ModalFooter } from "@/components/ui/modal";
import { FilterTabs } from "@/components/ui/filter-tabs";
-import { PageHeader } from "@/components/ui/page-header";
import { MarkdownEditor } from "@/components/markdown/markdown-editor";
function copyItemLink(id: string): void {
@@ -671,25 +670,24 @@ export default function ChangeRequestsClient({
return (
- setShowModal(true)}
- className="flex items-center gap-1.5 rounded-full bg-[#4CD964] px-4 py-2 text-sm font-semibold text-white hover:bg-[#3DBF55] transition-colors"
- >
-
- New Request
-
- }
- />
-
-
+ {/* Title + subtitle now live in the global PageBar (see page-meta.ts);
+ this toolbar keeps the primary action alongside the filter tabs. */}
+
+
+
+
{/* CR list */}
diff --git a/src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx b/src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx
index 2a0fe94e7..64a468605 100644
--- a/src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx
+++ b/src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx
@@ -29,11 +29,9 @@ import { toast } from "sonner";
import { pushUndoEntry } from "@/lib/hooks/use-undo-stack";
import { EmptyState } from "@/components/ui/empty-state";
import { FilterTabs } from "@/components/ui/filter-tabs";
-import { PageHeader } from "@/components/ui/page-header";
import { ModalShell, ModalFooter } from "@/components/ui/modal";
import { MarkdownEditor } from "@/components/markdown/markdown-editor";
import { MarkdownRenderer } from "@/components/markdown/markdown-renderer";
-import { usePageTitle } from "@/components/layout/page-bar-context";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -588,8 +586,8 @@ export function EpicsClient({
currentUserId: _currentUserId,
initialSelectedId,
}: Props) {
- usePageTitle("Epics");
-
+ // Title + subtitle resolve from the page-meta registry (the global PageBar is
+ // the single source) — no in-page override needed.
const router = useRouter();
const pathname = usePathname();
@@ -632,24 +630,25 @@ export function EpicsClient({
return (
- {/* Header */}
- setShowCreate(true)}
- className="flex items-center gap-1.5 rounded-full bg-[#4CD964] px-4 py-2 text-sm font-medium text-white hover:bg-[#3DBF55] transition-colors"
- >
-
- New Epic
-
- }
- />
-
- {/* Filter tabs */}
-
+ {/* Title + subtitle now live in the global PageBar (see page-meta.ts);
+ this toolbar keeps the primary action alongside the filter tabs. */}
+
+
+
+
{/* Content */}
diff --git a/src/app/(app)/o/[orgSlug]/p/[projectSlug]/flags/flags-client.tsx b/src/app/(app)/o/[orgSlug]/p/[projectSlug]/flags/flags-client.tsx
index 510fe3183..e5e458341 100644
--- a/src/app/(app)/o/[orgSlug]/p/[projectSlug]/flags/flags-client.tsx
+++ b/src/app/(app)/o/[orgSlug]/p/[projectSlug]/flags/flags-client.tsx
@@ -20,7 +20,7 @@ import { pushUndoEntry } from "@/lib/hooks/use-undo-stack";
import { EmptyState } from "@/components/ui/empty-state";
import { ModalShell, ModalFooter } from "@/components/ui/modal";
import { FilterTabs } from "@/components/ui/filter-tabs";
-import { PageHeader } from "@/components/ui/page-header";
+import { usePageSubtitle } from "@/components/layout/page-bar-context";
import { MarkdownEditor } from "@/components/markdown/markdown-editor";
import { MarkdownRenderer } from "@/components/markdown/markdown-renderer";
@@ -530,6 +530,13 @@ export default function FlagsClient({
}: Props) {
usePageSection("Flags");
const [flags, setFlags] = useState(initialFlags);
+ // Surface the live flag count in the global PageBar's subtitle line. This is a
+ // runtime-dynamic override of the static registry subtitle for this route.
+ usePageSubtitle(
+ flags.length === 0
+ ? "No flags raised yet"
+ : `${flags.length} flag${flags.length !== 1 ? "s" : ""} total`,
+ );
const [filter, setFilter] = useState("all");
const [showModal, setShowModal] = useState(false);
const [editingFlag, setEditingFlag] = useState(null);
@@ -652,31 +659,25 @@ export default function FlagsClient({
return (
-
-
- Raise Flag
-
- }
- />
-
-
+ {/* Title + subtitle now live in the global PageBar (registry + the
+ usePageSubtitle override above); this toolbar keeps the primary action
+ alongside the filter tabs. */}
+
+
+
+
{/* ── Flag list ──────────────────────────────────────────────────────── */}
diff --git a/src/components/layout/page-bar-context.tsx b/src/components/layout/page-bar-context.tsx
index f9626b8ba..e4119445c 100644
--- a/src/components/layout/page-bar-context.tsx
+++ b/src/components/layout/page-bar-context.tsx
@@ -4,22 +4,31 @@
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from "react";
-// undefined = not declared by page (PageBar infers from pathname)
-// null = page suppresses the bar title (page owns its own heading)
-// string = page declares an explicit title for the bar
-type PageTitleState = string | null | undefined;
+// undefined = not declared by page (PageBar resolves from the page-meta registry)
+// null = page suppresses the value (title null suppresses the whole bar;
+// subtitle null suppresses just the subtitle line)
+// string = page declares an explicit value for the bar
+type PageMetaState = string | null | undefined;
interface PageBarCtxValue {
- title: PageTitleState;
- setTitle: (t: PageTitleState) => void;
+ title: PageMetaState;
+ setTitle: (t: PageMetaState) => void;
+ subtitle: PageMetaState;
+ setSubtitle: (s: PageMetaState) => void;
}
const PageBarCtx = createContext(null);
export function PageBarProvider({ children }: { children: ReactNode }) {
- const [title, setTitleState] = useState(undefined);
- const setTitle = useCallback((t: PageTitleState) => setTitleState(t), []);
- return {children};
+ const [title, setTitleState] = useState(undefined);
+ const [subtitle, setSubtitleState] = useState(undefined);
+ const setTitle = useCallback((t: PageMetaState) => setTitleState(t), []);
+ const setSubtitle = useCallback((s: PageMetaState) => setSubtitleState(s), []);
+ return (
+
+ {children}
+
+ );
}
export function usePageBarCtx() {
@@ -30,7 +39,10 @@ export function usePageBarCtx() {
* Declare or suppress the page bar title from inside a page component.
*
* - usePageTitle("Tasks") → bar shows "Tasks"
- * - usePageTitle(null) → bar shows no title (page manages its own heading)
+ * - usePageTitle(null) → bar shows no title (the whole bar is suppressed)
+ *
+ * Leaves the subtitle untouched — the bar still resolves the subtitle from the
+ * page-meta registry unless the page also calls `usePageSubtitle`.
*/
export function usePageTitle(title: string | null) {
const ctx = useContext(PageBarCtx);
@@ -40,3 +52,22 @@ export function usePageTitle(title: string | null) {
return () => setTitle?.(undefined);
}, [title, setTitle]);
}
+
+/**
+ * Override or suppress the page bar subtitle from inside a page component.
+ *
+ * - usePageSubtitle("3 flags total") → bar's second line shows that text
+ * - usePageSubtitle(null) → bar shows the title only (no subtitle line)
+ *
+ * When a page does not call this hook the bar resolves the subtitle from the
+ * page-meta registry. Useful for runtime-dynamic subtitles (e.g. live counts)
+ * that the static registry cannot express.
+ */
+export function usePageSubtitle(subtitle: string | null) {
+ const ctx = useContext(PageBarCtx);
+ const setSubtitle = ctx?.setSubtitle;
+ useEffect(() => {
+ setSubtitle?.(subtitle);
+ return () => setSubtitle?.(undefined);
+ }, [subtitle, setSubtitle]);
+}
diff --git a/src/components/layout/page-bar.tsx b/src/components/layout/page-bar.tsx
index 560712a89..bef912a6d 100644
--- a/src/components/layout/page-bar.tsx
+++ b/src/components/layout/page-bar.tsx
@@ -5,7 +5,7 @@
import { usePathname } from "next/navigation";
import { usePageBarCtx } from "./page-bar-context";
-import { resolvePageTitle } from "./page-meta";
+import { resolvePageMeta, resolvePageTitle } from "./page-meta";
// SEGMENT_LABELS now lives in ./page-meta as the canonical title fallback.
// Re-exported here for backward compatibility with existing importers.
@@ -23,18 +23,26 @@ export function inferTitle(pathname: string): string {
export function PageBar() {
const pathname = usePathname();
+ const meta = resolvePageMeta(pathname);
const ctx = usePageBarCtx();
- // ctx.title === undefined → infer from pathname
- // ctx.title === null → page suppresses bar title (shows nothing)
- // ctx.title === string → page declares explicit title
- const displayTitle = ctx?.title === undefined ? inferTitle(pathname) : ctx.title;
+ // For both title and subtitle:
+ // ctx value === undefined → resolve from the page-meta registry
+ // ctx value === null → page suppresses it
+ // ctx value === string → page declares an explicit value
+ // A null/empty title suppresses the whole bar; a null/empty subtitle just
+ // drops the second line.
+ const displayTitle = ctx?.title === undefined ? meta.title : ctx.title;
+ const displaySubtitle = ctx?.subtitle === undefined ? meta.subtitle : ctx.subtitle;
if (!displayTitle) return null;
return (
-
);
}
diff --git a/src/components/layout/page-meta.ts b/src/components/layout/page-meta.ts
index 6fcf6c853..2f683a0ff 100644
--- a/src/components/layout/page-meta.ts
+++ b/src/components/layout/page-meta.ts
@@ -65,6 +65,7 @@ export const SEGMENT_LABELS: Record = {
technologies: "Technologies",
feedback: "Feedback",
flags: "Flags",
+ epics: "Epics",
"change-requests": "Change Requests",
approvals: "Approvals",
"my-approvals": "My Approvals",
@@ -161,6 +162,33 @@ export const PAGE_META: Record = {
actions: ["Draft and edit specs", "Score scope clarity"],
},
},
+ "/o/[org]/p/[project]/change-requests": {
+ title: "Change Requests",
+ subtitle: "Track and approve formal changes to this project's scope",
+ guide: {
+ summary:
+ "Change requests capture formal scope changes through a draft → submitted → approved/rejected workflow, keeping scope decisions auditable.",
+ actions: ["Raise a new change request", "Review, approve, or reject pending requests"],
+ },
+ },
+ "/o/[org]/p/[project]/flags": {
+ title: "Flags",
+ subtitle: "Risks, blockers, and concerns raised on this project",
+ guide: {
+ summary:
+ "Flags surface risks, blockers, and concerns so the team can track and resolve them before they impact delivery.",
+ actions: ["Raise a flag against the project", "Filter flags by status and severity"],
+ },
+ },
+ "/o/[org]/p/[project]/epics": {
+ title: "Epics",
+ subtitle: "Thematic groups of tasks synced with GitHub issues",
+ guide: {
+ summary:
+ "Epics group related tasks into themes and stay in sync with their GitHub issues, giving a higher-level view of project work.",
+ actions: ["Create an epic and link tasks to it", "Filter epics by status"],
+ },
+ },
};
/** The three route levels the resolver understands. */