Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -671,25 +670,24 @@ export default function ChangeRequestsClient({

return (
<div className="flex flex-col h-full bg-[#F5F5F5]">
<PageHeader
title="Change Requests"
action={
<button
onClick={() => 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"
>
<Plus className="h-4 w-4" strokeWidth={2} />
New Request
</button>
}
/>

<FilterTabs
tabs={FILTER_TABS}
value={filter}
onChange={setFilter}
counts={tabCounts}
/>
{/* Title + subtitle now live in the global PageBar (see page-meta.ts);
this toolbar keeps the primary action alongside the filter tabs. */}
<div className="flex items-center justify-between gap-3 border-b border-gray-200 bg-white px-4 py-2 sm:px-6">
<FilterTabs
tabs={FILTER_TABS}
value={filter}
onChange={setFilter}
counts={tabCounts}
className="border-b-0 bg-transparent px-0 py-0"
/>
<button
onClick={() => setShowModal(true)}
className="flex shrink-0 items-center gap-1.5 rounded-full bg-[#4CD964] px-4 py-2 text-sm font-semibold text-white hover:bg-[#3DBF55] transition-colors"
>
<Plus className="h-4 w-4" strokeWidth={2} />
New Request
</button>
</div>

{/* CR list */}
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
Expand Down
43 changes: 21 additions & 22 deletions src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@
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 ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -584,12 +582,12 @@
export function EpicsClient({
projectId,
initialEpics,
members: _members,

Check warning on line 585 in src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx

View workflow job for this annotation

GitHub Actions / typecheck + lint

'_members' is defined but never used
currentUserId: _currentUserId,

Check warning on line 586 in src/app/(app)/o/[orgSlug]/p/[projectSlug]/epics/epics-client.tsx

View workflow job for this annotation

GitHub Actions / typecheck + lint

'_currentUserId' is defined but never used
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();

Expand Down Expand Up @@ -632,24 +630,25 @@

return (
<div className="flex h-full flex-col bg-[#F5F5F5]">
{/* Header */}
<PageHeader
title="Epics"
subtitle="Thematic groups of tasks synced with GitHub issues"
action={
<button
type="button"
onClick={() => 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"
>
<Plus className="h-4 w-4" strokeWidth={2} />
New Epic
</button>
}
/>

{/* Filter tabs */}
<FilterTabs tabs={FILTER_TABS} value={filter} onChange={setFilter} counts={counts} />
{/* Title + subtitle now live in the global PageBar (see page-meta.ts);
this toolbar keeps the primary action alongside the filter tabs. */}
<div className="flex items-center justify-between gap-3 border-b border-gray-200 bg-white px-4 py-2 sm:px-6">
<FilterTabs
tabs={FILTER_TABS}
value={filter}
onChange={setFilter}
counts={counts}
className="border-b-0 bg-transparent px-0 py-0"
/>
<button
type="button"
onClick={() => setShowCreate(true)}
className="flex shrink-0 items-center gap-1.5 rounded-full bg-[#4CD964] px-4 py-2 text-sm font-medium text-white hover:bg-[#3DBF55] transition-colors"
>
<Plus className="h-4 w-4" strokeWidth={2} />
New Epic
</button>
</div>

{/* Content */}
<div className="flex flex-1 overflow-hidden">
Expand Down
53 changes: 27 additions & 26 deletions src/app/(app)/o/[orgSlug]/p/[projectSlug]/flags/flags-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -530,6 +530,13 @@ export default function FlagsClient({
}: Props) {
usePageSection("Flags");
const [flags, setFlags] = useState<FlagItem[]>(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<FilterTab>("all");
const [showModal, setShowModal] = useState(false);
const [editingFlag, setEditingFlag] = useState<FlagItem | null>(null);
Expand Down Expand Up @@ -652,31 +659,25 @@ export default function FlagsClient({

return (
<div className="flex h-full flex-col overflow-hidden bg-[#F5F5F5]">
<PageHeader
title="Flags"
subtitle={
flags.length === 0
? "No flags raised yet"
: `${flags.length} flag${flags.length !== 1 ? "s" : ""} total`
}
action={
<button
onClick={openModal}
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"
>
<Plus className="h-4 w-4" strokeWidth={2} />
Raise Flag
</button>
}
/>

<FilterTabs
tabs={FILTER_TABS}
value={filter}
onChange={setFilter}
counts={tabCounts}
className="pb-3 pt-2"
/>
{/* Title + subtitle now live in the global PageBar (registry + the
usePageSubtitle override above); this toolbar keeps the primary action
alongside the filter tabs. */}
<div className="flex items-center justify-between gap-3 border-b border-gray-200 bg-white px-4 py-2 sm:px-6">
<FilterTabs
tabs={FILTER_TABS}
value={filter}
onChange={setFilter}
counts={tabCounts}
className="border-b-0 bg-transparent px-0 py-0"
/>
<button
onClick={openModal}
className="flex shrink-0 items-center gap-1.5 rounded-full bg-[#4CD964] px-4 py-2 text-sm font-semibold text-white hover:bg-[#3DBF55] transition-colors"
>
<Plus className="h-4 w-4" strokeWidth={2} />
Raise Flag
</button>
</div>

{/* ── Flag list ──────────────────────────────────────────────────────── */}
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
Expand Down
51 changes: 41 additions & 10 deletions src/components/layout/page-bar-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PageBarCtxValue | null>(null);

export function PageBarProvider({ children }: { children: ReactNode }) {
const [title, setTitleState] = useState<PageTitleState>(undefined);
const setTitle = useCallback((t: PageTitleState) => setTitleState(t), []);
return <PageBarCtx.Provider value={{ title, setTitle }}>{children}</PageBarCtx.Provider>;
const [title, setTitleState] = useState<PageMetaState>(undefined);
const [subtitle, setSubtitleState] = useState<PageMetaState>(undefined);
const setTitle = useCallback((t: PageMetaState) => setTitleState(t), []);
const setSubtitle = useCallback((s: PageMetaState) => setSubtitleState(s), []);
return (
<PageBarCtx.Provider value={{ title, setTitle, subtitle, setSubtitle }}>
{children}
</PageBarCtx.Provider>
);
}

export function usePageBarCtx() {
Expand All @@ -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);
Expand All @@ -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]);
}
22 changes: 15 additions & 7 deletions src/components/layout/page-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 (
<div className="flex h-9 shrink-0 items-center border-b border-gray-100 bg-white pl-4 pr-3 sm:pl-6 sm:pr-4">
<span className="flex-1 truncate text-sm font-medium text-[#1C1C1E]">{displayTitle}</span>
<div className="flex shrink-0 flex-col justify-center border-b border-gray-100 bg-white py-2 pl-4 pr-3 sm:pl-6 sm:pr-4">
<span className="truncate text-sm font-semibold leading-5 text-[#1C1C1E]">{displayTitle}</span>
{displaySubtitle ? (
<span className="truncate text-xs leading-4 text-[#8E8E93]">{displaySubtitle}</span>
) : null}
</div>
);
}
28 changes: 28 additions & 0 deletions src/components/layout/page-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const SEGMENT_LABELS: Record<string, string> = {
technologies: "Technologies",
feedback: "Feedback",
flags: "Flags",
epics: "Epics",
"change-requests": "Change Requests",
approvals: "Approvals",
"my-approvals": "My Approvals",
Expand Down Expand Up @@ -161,6 +162,33 @@ export const PAGE_META: Record<string, PageMeta> = {
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. */
Expand Down
Loading