diff --git a/web/src/app/_ui/page-head.tsx b/web/src/app/_ui/page-head.tsx
new file mode 100644
index 0000000..705576f
--- /dev/null
+++ b/web/src/app/_ui/page-head.tsx
@@ -0,0 +1,8 @@
+export function PageHead({ title, eyebrow }: { title: string; eyebrow?: string }) {
+ return (
+
+ {eyebrow ?
{eyebrow}
: null}
+
{title}
+
+ );
+}
diff --git a/web/src/app/artists/[id]/discography-client.tsx b/web/src/app/artists/[id]/discography-client.tsx
index ac16328..6b7c4b4 100644
--- a/web/src/app/artists/[id]/discography-client.tsx
+++ b/web/src/app/artists/[id]/discography-client.tsx
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
+import { PageHead } from "../../_ui/page-head";
type Release = {
id: string;
@@ -14,10 +15,10 @@ type Release = {
};
type Detail = { id: string; name: string; autoMonitorFuture: boolean; showAllTypes: boolean; releases: Release[] };
-function badge(r: Release): string {
- if (r.currentQualityClass !== null) return `Have (q${r.currentQualityClass})`;
- if (r.monitored) return r.state === "wanted" ? "Wanted" : r.state;
- return "Dormant";
+function badge(r: Release): { label: string; cls: string } {
+ if (r.currentQualityClass !== null) return { label: `Have q${r.currentQualityClass}`, cls: "chip done" };
+ if (r.monitored) return { label: r.state === "wanted" ? "Wanted" : r.state, cls: "chip working" };
+ return { label: "Dormant", cls: "chip" };
}
export function DiscographyClient({ id }: { id: string }) {
@@ -53,32 +54,56 @@ export function DiscographyClient({ id }: { id: string }) {
refresh();
}
- if (!detail) return Loading…
;
+ if (!detail) return Loading…
;
return (
-
{detail.name}
-
-
);
diff --git a/web/src/app/artists/[id]/page.tsx b/web/src/app/artists/[id]/page.tsx
index 33339b6..610dcb3 100644
--- a/web/src/app/artists/[id]/page.tsx
+++ b/web/src/app/artists/[id]/page.tsx
@@ -2,10 +2,5 @@ import { DiscographyClient } from "./discography-client";
export default async function ArtistDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
- return (
-
- ← Artists
-
-
- );
+ return ;
}
diff --git a/web/src/app/artists/artists-client.tsx b/web/src/app/artists/artists-client.tsx
index 729ab4d..5b78757 100644
--- a/web/src/app/artists/artists-client.tsx
+++ b/web/src/app/artists/artists-client.tsx
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useState } from "react";
+import { PageHead } from "../_ui/page-head";
+import { SectionHeader } from "../_ui/section-header";
type Artist = {
id: string;
@@ -68,38 +70,86 @@ export function ArtistsClient() {
return (
-
- {searching ?
Searching…
: null}
+
{hits.length > 0 ? (
-
);
}
diff --git a/web/src/app/artists/page.tsx b/web/src/app/artists/page.tsx
index 3c12d26..f1ec8b0 100644
--- a/web/src/app/artists/page.tsx
+++ b/web/src/app/artists/page.tsx
@@ -1,11 +1,5 @@
import { ArtistsClient } from "./artists-client";
export default function ArtistsPage() {
- return (
-
- Watched artists
- Home · Wanted
-
-
- );
+ return ;
}
diff --git a/web/src/app/design.css b/web/src/app/design.css
index a688c51..3d6b5ca 100644
--- a/web/src/app/design.css
+++ b/web/src/app/design.css
@@ -169,3 +169,56 @@ nav.contents .sep { flex: 1; }
.job { grid-template-columns: 4px 1fr; }
.job .chip { grid-column: 2 / 3; justify-self: start; margin-top: 12px; }
}
+
+/* ── Phase 2: page headers, list rows, tools ──────────── */
+.page-head { margin: 6px 0 26px; }
+.page-head .eyebrow {
+ font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.2em;
+ text-transform: uppercase; color: var(--graphite); margin: 0 0 9px;
+}
+.page-head h1 {
+ font-size: clamp(1.9rem, 4vw, 2.6rem); font-weight: 600; letter-spacing: -0.02em;
+ margin: 0; line-height: 1; text-wrap: balance;
+}
+
+.list { list-style: none; margin: 0 0 40px; padding: 0; }
+.list-row {
+ display: flex; gap: 14px 20px; align-items: baseline; justify-content: space-between;
+ flex-wrap: wrap; padding: 16px 0; border-bottom: 1px solid var(--rule);
+}
+.list-row .main { min-width: 0; }
+.list-row .rtitle { font-size: 1.2rem; line-height: 1.2; }
+.list-row .rtitle a { color: inherit; text-decoration: none; }
+.list-row .rtitle a:hover { text-decoration: underline; text-decoration-color: var(--accent); }
+.list-row .rtitle .artist, .list-row .rtitle .dim { color: var(--graphite); }
+.list-row .rmeta {
+ font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase;
+ color: var(--graphite); margin-top: 7px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
+}
+.list-row .rmeta .dot { opacity: 0.5; }
+.actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
+
+.btn.sm { padding: 6px 11px; font-size: 0.62rem; letter-spacing: 0.12em; }
+.btn.ghost { border-color: var(--rule-2); color: var(--graphite); }
+.btn.ghost:hover { background: transparent; border-color: var(--ink); color: var(--ink); }
+
+.toggle {
+ font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.1em; text-transform: uppercase;
+ color: var(--graphite); display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
+}
+.toggle input { accent-color: var(--accent); }
+.following { font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); }
+
+.tool-row { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; margin: 0 0 28px; }
+.notice { font-family: var(--mono); font-size: 0.7rem; letter-spacing: 0.04em; color: var(--alert); }
+.muted-note { color: var(--graphite); font-size: 0.95rem; }
+.score { font-variant-numeric: tabular-nums; }
+
+/* tracklist (preview page) */
+.tracks { list-style: none; margin: 10px 0 4px; padding: 0 0 0 2px; }
+.tracks li {
+ display: flex; gap: 12px; font-family: var(--mono); font-size: 0.74rem; color: var(--graphite);
+ padding: 3px 0; font-variant-numeric: tabular-nums;
+}
+.tracks li .tnum { color: var(--rule-2); min-width: 2em; }
+.tracks li .tname { color: var(--ink); }
diff --git a/web/src/app/discover/artist/[mbid]/page.tsx b/web/src/app/discover/artist/[mbid]/page.tsx
index 057c0ba..2782718 100644
--- a/web/src/app/discover/artist/[mbid]/page.tsx
+++ b/web/src/app/discover/artist/[mbid]/page.tsx
@@ -9,12 +9,5 @@ export default async function PreviewPage({
}) {
const { mbid } = await params;
const { name } = await searchParams;
- return (
-
-
- ← Discover · Artists
-
-
-
- );
+ return ;
}
diff --git a/web/src/app/discover/artist/[mbid]/preview-client.tsx b/web/src/app/discover/artist/[mbid]/preview-client.tsx
index 5261857..b730420 100644
--- a/web/src/app/discover/artist/[mbid]/preview-client.tsx
+++ b/web/src/app/discover/artist/[mbid]/preview-client.tsx
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
+import { PageHead } from "../../../_ui/page-head";
type Release = {
rgMbid: string;
@@ -105,52 +106,78 @@ export function PreviewClient({ mbid, initialName }: { mbid: string; initialName
if (error)
return (
-
- Couldn’t load this artist.
+
+ Couldn’t load this artist.{" "}
+
);
- if (!data) return Loading…
;
+ if (!data) return Loading…
;
return (
-
{data.artistName || "Artist"}
-
- {followed ? Following : }
- {" · "}
+
+
+
+ {followed ? (
+
Following
+ ) : (
+
+ )}
MusicBrainz ↗
-
-
-
+
+
);
diff --git a/web/src/app/discover/discover-client.tsx b/web/src/app/discover/discover-client.tsx
index 6211d96..dc1f1c4 100644
--- a/web/src/app/discover/discover-client.tsx
+++ b/web/src/app/discover/discover-client.tsx
@@ -1,6 +1,8 @@
"use client";
import { useCallback, useEffect, useState } from "react";
+import { PageHead } from "../_ui/page-head";
+import { SectionHeader } from "../_ui/section-header";
type Suggestion = {
id: string;
@@ -93,80 +95,114 @@ export function DiscoverClient() {
return (
-
-
);
}
diff --git a/web/src/app/discover/page.tsx b/web/src/app/discover/page.tsx
index 6990e86..ab5b50e 100644
--- a/web/src/app/discover/page.tsx
+++ b/web/src/app/discover/page.tsx
@@ -1,14 +1,5 @@
import { DiscoverClient } from "./discover-client";
export default function DiscoverPage() {
- return (
-
- Discover
-
- Home · Artists · Wanted ·{" "}
- Settings
-
-
-
- );
+ return ;
}
diff --git a/web/src/app/wanted/page.tsx b/web/src/app/wanted/page.tsx
index 34b9aa1..43aec7a 100644
--- a/web/src/app/wanted/page.tsx
+++ b/web/src/app/wanted/page.tsx
@@ -1,11 +1,5 @@
import { WantedClient } from "./wanted-client";
export default function WantedPage() {
- return (
-
- Wanted
- Home · Artists
-
-
- );
+ return ;
}
diff --git a/web/src/app/wanted/wanted-client.tsx b/web/src/app/wanted/wanted-client.tsx
index ef9e32b..5dbbe70 100644
--- a/web/src/app/wanted/wanted-client.tsx
+++ b/web/src/app/wanted/wanted-client.tsx
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useState } from "react";
+import { PageHead } from "../_ui/page-head";
+import { SectionHeader } from "../_ui/section-header";
type Wanted = {
id: string;
@@ -11,6 +13,11 @@ type Wanted = {
lastSearchedAt: string | null;
};
+function stateLabel(w: Wanted): string {
+ if (w.currentQualityClass !== null) return `Have q${w.currentQualityClass} · upgrading`;
+ return w.state === "wanted" ? "Wanted" : w.state;
+}
+
export function WantedClient() {
const [rows, setRows] = useState([]);
const [artist, setArtist] = useState("");
@@ -35,7 +42,7 @@ export function WantedClient() {
body: JSON.stringify({ artist, album }),
});
if (!res.ok) {
- setError((await res.json()).error ?? "failed to add");
+ setError((await res.json()).error ?? "Couldn’t add that — no MusicBrainz match?");
return;
}
setArtist("");
@@ -50,21 +57,48 @@ export function WantedClient() {
return (
-
-
- {rows.map((w) => (
- -
- {w.artistName} — {w.album} · {w.currentQualityClass !== null ? `have q${w.currentQualityClass}, upgrading` : w.state}
- {w.lastSearchedAt ? ` · last tried ${new Date(w.lastSearchedAt).toLocaleString()}` : " · never tried"}{" "}
- searchNow(w)}>Search now
-
- ))}
-
+
+
+ {rows.length === 0 ? (
+
Nothing wanted yet. Add a release above, or send one over from an artist’s discography.
+ ) : (
+
+ )}
);
}