Skip to content

Commit 1438b13

Browse files
refactor(ui): move HealthPageChrome into @repowise-dev/ui/health (#214)
The page-chrome that wraps the four /health/* dashboard pages used to live in packages/web and pulled in next/link, which kept downstream consumers of @repowise-dev/ui from reusing it. - New framework-agnostic packages/ui/src/health/page-chrome.tsx accepts a renderLink render-prop (already the pattern used by HealthTabs) and forwards it down — no next dep in ui/. - HealthTabs gains an optional basePath prop so callers with a different URL shape (snapshot-scoped, owner/name-scoped, etc.) can override the /repos/${repoId}/health prefix without forking the component. - packages/web/src/components/health/health-page-chrome.tsx becomes a thin Next-Link binding around the shared base — the four /health/* pages keep their existing import path with no behavioral change.
1 parent b09126b commit 1438b13

4 files changed

Lines changed: 118 additions & 84 deletions

File tree

packages/ui/src/health/health-tabs.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export type HealthTabKey = "overview" | "trend" | "coverage" | "refactoring";
77
export interface HealthTabsProps {
88
repoId: string;
99
active: HealthTabKey;
10+
/** Override the URL prefix the tabs build hrefs against. Defaults to
11+
* `/repos/${repoId}/health`. Consumers with a different URL shape
12+
* (snapshot-scoped, owner/name-scoped, …) pass their own prefix. */
13+
basePath?: string;
1014
/** Optional render-prop that produces a `<a href>` / `<Link>`. Lets the
1115
* caller plug Next's `<Link>` without dragging the next dep into ui/. */
1216
renderLink?: (props: {
@@ -25,12 +29,13 @@ const TABS: { key: HealthTabKey; label: string; suffix: string; Icon: React.Comp
2529
{ key: "refactoring", label: "Refactoring", suffix: "/refactoring-targets", Icon: Wrench },
2630
];
2731

28-
export function HealthTabs({ repoId, active, renderLink }: HealthTabsProps) {
32+
export function HealthTabs({ repoId, active, basePath, renderLink }: HealthTabsProps) {
33+
const prefix = basePath ?? `/repos/${repoId}/health`;
2934
return (
3035
<div className="border-b border-[var(--color-border-default)]">
3136
<nav className="-mb-px flex flex-wrap gap-1" aria-label="Code health views">
3237
{TABS.map((t) => {
33-
const href = `/repos/${repoId}/health${t.suffix}`;
38+
const href = `${prefix}${t.suffix}`;
3439
const isActive = active === t.key;
3540
const baseCls =
3641
"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors";

packages/ui/src/health/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from "./severity-distribution";
1515
export * from "./score-breakdown";
1616
export * from "./sparkline";
1717
export * from "./health-tabs";
18+
export * from "./page-chrome";
1819
export * from "./trend-chart";
1920
export * from "./risk-coverage-scatter";
2021
export * from "./impact-effort-quadrant";
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"use client";
2+
3+
import { HealthTabs, type HealthTabKey, type HealthTabsProps } from "./health-tabs";
4+
5+
export interface HealthPageChromeProps {
6+
repoId: string;
7+
active: HealthTabKey;
8+
title: string;
9+
subtitle?: React.ReactNode;
10+
icon?: React.ReactNode;
11+
meta?: {
12+
last_indexed_at: string | null;
13+
head_commit: string | null;
14+
snapshot_count: number;
15+
} | null;
16+
actions?: React.ReactNode;
17+
/** Forwarded to `HealthTabs`. Overrides the default `/repos/${repoId}/health`
18+
* prefix the tabs build hrefs against. */
19+
basePath?: string;
20+
/** Forwarded to `HealthTabs`. Lets the caller plug a framework-specific
21+
* link (e.g. Next.js `<Link>`) without dragging that dep into ui/. */
22+
renderLink?: HealthTabsProps["renderLink"];
23+
}
24+
25+
function formatIndexedAt(iso: string | null): string {
26+
if (!iso) return "never";
27+
const d = new Date(iso);
28+
const diff = Date.now() - d.getTime();
29+
const minutes = Math.round(diff / 60_000);
30+
if (minutes < 1) return "just now";
31+
if (minutes < 60) return `${minutes}m ago`;
32+
const hours = Math.round(minutes / 60);
33+
if (hours < 24) return `${hours}h ago`;
34+
const days = Math.round(hours / 24);
35+
if (days < 30) return `${days}d ago`;
36+
return d.toLocaleDateString();
37+
}
38+
39+
export function HealthPageChrome({
40+
repoId,
41+
active,
42+
title,
43+
subtitle,
44+
icon,
45+
meta,
46+
actions,
47+
basePath,
48+
renderLink,
49+
}: HealthPageChromeProps) {
50+
return (
51+
<div className="space-y-3">
52+
<div className="flex flex-wrap items-start justify-between gap-4">
53+
<div className="min-w-0">
54+
<h1 className="text-xl font-semibold text-[var(--color-text-primary)] mb-1 flex items-center gap-2">
55+
{icon}
56+
{title}
57+
</h1>
58+
{subtitle ? (
59+
<p className="text-sm text-[var(--color-text-secondary)]">{subtitle}</p>
60+
) : null}
61+
{meta ? (
62+
<p className="mt-1 text-[11px] text-[var(--color-text-tertiary)]">
63+
Indexed {formatIndexedAt(meta.last_indexed_at)}
64+
{meta.head_commit ? (
65+
<>
66+
{" "}
67+
· <span className="font-mono">{meta.head_commit.slice(0, 7)}</span>
68+
</>
69+
) : null}
70+
{meta.snapshot_count > 0 ? <> · {meta.snapshot_count} snapshots</> : null}
71+
</p>
72+
) : null}
73+
</div>
74+
{actions ? <div className="flex items-center gap-2 shrink-0">{actions}</div> : null}
75+
</div>
76+
<HealthTabs
77+
repoId={repoId}
78+
active={active}
79+
{...(basePath !== undefined ? { basePath } : {})}
80+
{...(renderLink !== undefined ? { renderLink } : {})}
81+
/>
82+
</div>
83+
);
84+
}
Lines changed: 26 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,34 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { HealthTabs, type HealthTabKey } from "@repowise-dev/ui/health";
4+
import {
5+
HealthPageChrome as BaseHealthPageChrome,
6+
type HealthPageChromeProps as BaseHealthPageChromeProps,
7+
} from "@repowise-dev/ui/health";
58

6-
interface HealthPageChromeProps {
7-
repoId: string;
8-
active: HealthTabKey;
9-
title: string;
10-
subtitle?: React.ReactNode;
11-
icon?: React.ReactNode;
12-
meta?: {
13-
last_indexed_at: string | null;
14-
head_commit: string | null;
15-
snapshot_count: number;
16-
} | null;
17-
actions?: React.ReactNode;
18-
}
19-
20-
function formatIndexedAt(iso: string | null): string {
21-
if (!iso) return "never";
22-
const d = new Date(iso);
23-
const diff = Date.now() - d.getTime();
24-
const minutes = Math.round(diff / 60_000);
25-
if (minutes < 1) return "just now";
26-
if (minutes < 60) return `${minutes}m ago`;
27-
const hours = Math.round(minutes / 60);
28-
if (hours < 24) return `${hours}h ago`;
29-
const days = Math.round(hours / 24);
30-
if (days < 30) return `${days}d ago`;
31-
return d.toLocaleDateString();
32-
}
9+
export type HealthPageChromeProps = Omit<BaseHealthPageChromeProps, "renderLink">;
3310

34-
export function HealthPageChrome({
35-
repoId,
36-
active,
37-
title,
38-
subtitle,
39-
icon,
40-
meta,
41-
actions,
42-
}: HealthPageChromeProps) {
11+
/** Web binding for the shared `HealthPageChrome` — injects Next's `<Link>`
12+
* so health-tab navigation is client-routed instead of hard-reloading. */
13+
export function HealthPageChrome(props: HealthPageChromeProps) {
4314
return (
44-
<div className="space-y-3">
45-
<div className="flex flex-wrap items-start justify-between gap-4">
46-
<div className="min-w-0">
47-
<h1 className="text-xl font-semibold text-[var(--color-text-primary)] mb-1 flex items-center gap-2">
48-
{icon}
49-
{title}
50-
</h1>
51-
{subtitle ? (
52-
<p className="text-sm text-[var(--color-text-secondary)]">{subtitle}</p>
53-
) : null}
54-
{meta ? (
55-
<p className="mt-1 text-[11px] text-[var(--color-text-tertiary)]">
56-
Indexed {formatIndexedAt(meta.last_indexed_at)}
57-
{meta.head_commit ? (
58-
<>
59-
{" "}
60-
· <span className="font-mono">{meta.head_commit.slice(0, 7)}</span>
61-
</>
62-
) : null}
63-
{meta.snapshot_count > 0 ? <> · {meta.snapshot_count} snapshots</> : null}
64-
</p>
65-
) : null}
66-
</div>
67-
{actions ? <div className="flex items-center gap-2 shrink-0">{actions}</div> : null}
68-
</div>
69-
<HealthTabs
70-
repoId={repoId}
71-
active={active}
72-
renderLink={({ href, label, active: isActive, icon, key }) => (
73-
<Link
74-
key={key}
75-
href={href}
76-
className={
77-
"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors " +
78-
(isActive
79-
? "border-emerald-500 text-[var(--color-text-primary)]"
80-
: "border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border-default)]")
81-
}
82-
>
83-
{icon}
84-
{label}
85-
</Link>
86-
)}
87-
/>
88-
</div>
15+
<BaseHealthPageChrome
16+
{...props}
17+
renderLink={({ href, label, active, icon, key }) => (
18+
<Link
19+
key={key}
20+
href={href}
21+
className={
22+
"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium border-b-2 transition-colors " +
23+
(active
24+
? "border-emerald-500 text-[var(--color-text-primary)]"
25+
: "border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border-default)]")
26+
}
27+
>
28+
{icon}
29+
{label}
30+
</Link>
31+
)}
32+
/>
8933
);
9034
}

0 commit comments

Comments
 (0)