From 65c0adfd769b9ef11b897c96a3634c61120055b8 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Mon, 8 Dec 2025 00:18:03 +0900
Subject: feat(client): redesign frontend with TailwindCSS v4
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Replace inline styles with TailwindCSS, implementing a cohesive Japanese-inspired
design system with custom colors (cream, teal primary), typography (Fraunces,
DM Sans), and animations. Update all pages and components with consistent styling,
improve accessibility by adding aria-hidden to decorative SVGs, and configure
Biome for Tailwind CSS syntax support.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 src/client/pages/DeckDetailPage.tsx | 452 +++++++++++++++++++++++-------------
 1 file changed, 291 insertions(+), 161 deletions(-)

(limited to 'src/client/pages/DeckDetailPage.tsx')

diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx
index 3d7ffb5..cb1e3fb 100644
--- a/src/client/pages/DeckDetailPage.tsx
+++ b/src/client/pages/DeckDetailPage.tsx
@@ -31,6 +31,13 @@ const CardStateLabels: Record<number, string> = {
 	3: "Relearning",
 };
 
+const CardStateColors: Record<number, string> = {
+	0: "bg-info/10 text-info",
+	1: "bg-warning/10 text-warning",
+	2: "bg-success/10 text-success",
+	3: "bg-error/10 text-error",
+};
+
 export function DeckDetailPage() {
 	const { deckId } = useParams<{ deckId: string }>();
 	const [deck, setDeck] = useState<Deck | null>(null);
@@ -114,195 +121,318 @@ export function DeckDetailPage() {
 
 	if (!deckId) {
 		return (
-			<div>
-				<p>Invalid deck ID</p>
-				<Link href="/">Back to decks</Link>
+			<div className="min-h-screen bg-cream flex items-center justify-center">
+				<div className="text-center">
+					<p className="text-muted mb-4">Invalid deck ID</p>
+					<Link
+						href="/"
+						className="text-primary hover:text-primary-dark font-medium"
+					>
+						Back to decks
+					</Link>
+				</div>
 			</div>
 		);
 	}
 
 	return (
-		<div>
-			<header style={{ marginBottom: "1rem" }}>
-				<Link href="/" style={{ textDecoration: "none" }}>
-					&larr; Back to Decks
-				</Link>
-			</header>
-
-			{isLoading && <p>Loading...</p>}
-
-			{error && (
-				<div role="alert" style={{ color: "red" }}>
-					{error}
-					<button
-						type="button"
-						onClick={fetchData}
-						style={{ marginLeft: "0.5rem" }}
+		<div className="min-h-screen bg-cream">
+			{/* Header */}
+			<header className="bg-white border-b border-border/50">
+				<div className="max-w-4xl mx-auto px-4 py-4">
+					<Link
+						href="/"
+						className="inline-flex items-center gap-2 text-muted hover:text-slate transition-colors text-sm"
 					>
-						Retry
-					</button>
+						<svg
+							className="w-4 h-4"
+							fill="none"
+							stroke="currentColor"
+							viewBox="0 0 24 24"
+							aria-hidden="true"
+						>
+							<path
+								strokeLinecap="round"
+								strokeLinejoin="round"
+								strokeWidth={2}
+								d="M15 19l-7-7 7-7"
+							/>
+						</svg>
+						Back to Decks
+					</Link>
 				</div>
-			)}
+			</header>
 
-			{!isLoading && !error && deck && (
-				<main>
-					<div style={{ marginBottom: "1.5rem" }}>
-						<h1 style={{ margin: 0 }}>{deck.name}</h1>
-						{deck.description && (
-							<p style={{ margin: "0.5rem 0 0 0", color: "#666" }}>
-								{deck.description}
-							</p>
-						)}
+			{/* Main Content */}
+			<main className="max-w-4xl mx-auto px-4 py-8">
+				{/* Loading State */}
+				{isLoading && (
+					<div className="flex items-center justify-center py-12">
+						<svg
+							className="animate-spin h-8 w-8 text-primary"
+							viewBox="0 0 24 24"
+							aria-hidden="true"
+						>
+							<circle
+								className="opacity-25"
+								cx="12"
+								cy="12"
+								r="10"
+								stroke="currentColor"
+								strokeWidth="4"
+								fill="none"
+							/>
+							<path
+								className="opacity-75"
+								fill="currentColor"
+								d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
+							/>
+						</svg>
 					</div>
+				)}
 
+				{/* Error State */}
+				{error && (
 					<div
-						style={{
-							display: "flex",
-							gap: "0.5rem",
-							marginBottom: "1rem",
-						}}
+						role="alert"
+						className="bg-error/5 border border-error/20 rounded-xl p-4 flex items-center justify-between"
 					>
-						<Link href={`/decks/${deckId}/study`}>
+						<span className="text-error">{error}</span>
+						<button
+							type="button"
+							onClick={fetchData}
+							className="text-error hover:text-error/80 font-medium text-sm"
+						>
+							Retry
+						</button>
+					</div>
+				)}
+
+				{/* Deck Content */}
+				{!isLoading && !error && deck && (
+					<div className="animate-fade-in">
+						{/* Deck Header */}
+						<div className="mb-8">
+							<h1 className="font-display text-3xl font-semibold text-ink mb-2">
+								{deck.name}
+							</h1>
+							{deck.description && (
+								<p className="text-muted">{deck.description}</p>
+							)}
+						</div>
+
+						{/* Study Button */}
+						<div className="mb-8">
+							<Link
+								href={`/decks/${deckId}/study`}
+								className="inline-flex items-center gap-2 bg-success hover:bg-success/90 text-white font-medium py-3 px-6 rounded-xl transition-all duration-200 active:scale-[0.98] shadow-sm hover:shadow-md"
+							>
+								<svg
+									className="w-5 h-5"
+									fill="none"
+									stroke="currentColor"
+									viewBox="0 0 24 24"
+									aria-hidden="true"
+								>
+									<path
+										strokeLinecap="round"
+										strokeLinejoin="round"
+										strokeWidth={2}
+										d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
+									/>
+									<path
+										strokeLinecap="round"
+										strokeLinejoin="round"
+										strokeWidth={2}
+										d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+									/>
+								</svg>
+								Study Now
+							</Link>
+						</div>
+
+						{/* Cards Section */}
+						<div className="flex items-center justify-between mb-6">
+							<h2 className="font-display text-xl font-medium text-slate">
+								Cards{" "}
+								<span className="text-muted font-normal">({cards.length})</span>
+							</h2>
 							<button
 								type="button"
-								style={{
-									backgroundColor: "#28a745",
-									color: "white",
-									border: "none",
-									padding: "0.5rem 1rem",
-									borderRadius: "4px",
-									cursor: "pointer",
-								}}
+								onClick={() => setIsCreateModalOpen(true)}
+								className="inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-all duration-200 active:scale-[0.98]"
 							>
-								Study Now
+								<svg
+									className="w-5 h-5"
+									fill="none"
+									stroke="currentColor"
+									viewBox="0 0 24 24"
+									aria-hidden="true"
+								>
+									<path
+										strokeLinecap="round"
+										strokeLinejoin="round"
+										strokeWidth={2}
+										d="M12 4v16m8-8H4"
+									/>
+								</svg>
+								Add Card
 							</button>
-						</Link>
-					</div>
-
-					<div
-						style={{
-							display: "flex",
-							justifyContent: "space-between",
-							alignItems: "center",
-							marginBottom: "1rem",
-						}}
-					>
-						<h2 style={{ margin: 0 }}>Cards ({cards.length})</h2>
-						<button type="button" onClick={() => setIsCreateModalOpen(true)}>
-							Add Card
-						</button>
-					</div>
-
-					{cards.length === 0 && (
-						<div>
-							<p>This deck has no cards yet.</p>
-							<p>Add cards to start studying!</p>
 						</div>
-					)}
-
-					{cards.length > 0 && (
-						<ul style={{ listStyle: "none", padding: 0 }}>
-							{cards.map((card) => (
-								<li
-									key={card.id}
-									style={{
-										border: "1px solid #ccc",
-										padding: "1rem",
-										marginBottom: "0.5rem",
-										borderRadius: "4px",
-									}}
+
+						{/* Empty State */}
+						{cards.length === 0 && (
+							<div className="text-center py-12 bg-white rounded-xl border border-border/50">
+								<div className="w-14 h-14 mx-auto mb-4 bg-ivory rounded-xl flex items-center justify-center">
+									<svg
+										className="w-7 h-7 text-muted"
+										fill="none"
+										stroke="currentColor"
+										viewBox="0 0 24 24"
+										aria-hidden="true"
+									>
+										<path
+											strokeLinecap="round"
+											strokeLinejoin="round"
+											strokeWidth={1.5}
+											d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
+										/>
+									</svg>
+								</div>
+								<h3 className="font-display text-lg font-medium text-slate mb-2">
+									No cards yet
+								</h3>
+								<p className="text-muted text-sm mb-4">
+									Add cards to start studying
+								</p>
+								<button
+									type="button"
+									onClick={() => setIsCreateModalOpen(true)}
+									className="inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-all duration-200"
 								>
+									<svg
+										className="w-5 h-5"
+										fill="none"
+										stroke="currentColor"
+										viewBox="0 0 24 24"
+										aria-hidden="true"
+									>
+										<path
+											strokeLinecap="round"
+											strokeLinejoin="round"
+											strokeWidth={2}
+											d="M12 4v16m8-8H4"
+										/>
+									</svg>
+									Add Your First Card
+								</button>
+							</div>
+						)}
+
+						{/* Card List */}
+						{cards.length > 0 && (
+							<div className="space-y-3">
+								{cards.map((card, index) => (
 									<div
-										style={{
-											display: "flex",
-											justifyContent: "space-between",
-											alignItems: "flex-start",
-										}}
+										key={card.id}
+										className="bg-white rounded-xl border border-border/50 p-5 shadow-card hover:shadow-md transition-all duration-200"
+										style={{ animationDelay: `${index * 30}ms` }}
 									>
-										<div style={{ flex: 1, minWidth: 0 }}>
-											<div
-												style={{
-													display: "flex",
-													gap: "1rem",
-													marginBottom: "0.5rem",
-												}}
-											>
-												<div style={{ flex: 1, minWidth: 0 }}>
-													<strong>Front:</strong>
-													<p
-														style={{
-															margin: "0.25rem 0 0 0",
-															whiteSpace: "pre-wrap",
-															wordBreak: "break-word",
-														}}
-													>
-														{card.front}
-													</p>
+										<div className="flex items-start justify-between gap-4">
+											<div className="flex-1 min-w-0">
+												{/* Front/Back Preview */}
+												<div className="grid grid-cols-2 gap-4 mb-3">
+													<div>
+														<span className="text-xs font-medium text-muted uppercase tracking-wide">
+															Front
+														</span>
+														<p className="mt-1 text-slate text-sm line-clamp-2 whitespace-pre-wrap break-words">
+															{card.front}
+														</p>
+													</div>
+													<div>
+														<span className="text-xs font-medium text-muted uppercase tracking-wide">
+															Back
+														</span>
+														<p className="mt-1 text-slate text-sm line-clamp-2 whitespace-pre-wrap break-words">
+															{card.back}
+														</p>
+													</div>
 												</div>
-												<div style={{ flex: 1, minWidth: 0 }}>
-													<strong>Back:</strong>
-													<p
-														style={{
-															margin: "0.25rem 0 0 0",
-															whiteSpace: "pre-wrap",
-															wordBreak: "break-word",
-														}}
+
+												{/* Card Stats */}
+												<div className="flex items-center gap-3 text-xs">
+													<span
+														className={`px-2 py-0.5 rounded-full font-medium ${CardStateColors[card.state] || "bg-muted/10 text-muted"}`}
 													>
-														{card.back}
-													</p>
+														{CardStateLabels[card.state] || "Unknown"}
+													</span>
+													<span className="text-muted">
+														{card.reps} reviews
+													</span>
+													{card.lapses > 0 && (
+														<span className="text-muted">
+															{card.lapses} lapses
+														</span>
+													)}
 												</div>
 											</div>
-											<div
-												style={{
-													display: "flex",
-													gap: "1rem",
-													fontSize: "0.875rem",
-													color: "#666",
-												}}
-											>
-												<span>
-													State: {CardStateLabels[card.state] || "Unknown"}
-												</span>
-												<span>Reviews: {card.reps}</span>
-												<span>Lapses: {card.lapses}</span>
+
+											{/* Actions */}
+											<div className="flex items-center gap-1 shrink-0">
+												<button
+													type="button"
+													onClick={() => setEditingCard(card)}
+													className="p-2 text-muted hover:text-slate hover:bg-ivory rounded-lg transition-colors"
+													title="Edit card"
+												>
+													<svg
+														className="w-4 h-4"
+														fill="none"
+														stroke="currentColor"
+														viewBox="0 0 24 24"
+														aria-hidden="true"
+													>
+														<path
+															strokeLinecap="round"
+															strokeLinejoin="round"
+															strokeWidth={2}
+															d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
+														/>
+													</svg>
+												</button>
+												<button
+													type="button"
+													onClick={() => setDeletingCard(card)}
+													className="p-2 text-muted hover:text-error hover:bg-error/5 rounded-lg transition-colors"
+													title="Delete card"
+												>
+													<svg
+														className="w-4 h-4"
+														fill="none"
+														stroke="currentColor"
+														viewBox="0 0 24 24"
+														aria-hidden="true"
+													>
+														<path
+															strokeLinecap="round"
+															strokeLinejoin="round"
+															strokeWidth={2}
+															d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
+														/>
+													</svg>
+												</button>
 											</div>
 										</div>
-										<div
-											style={{
-												display: "flex",
-												gap: "0.5rem",
-												marginLeft: "1rem",
-											}}
-										>
-											<button
-												type="button"
-												onClick={() => setEditingCard(card)}
-											>
-												Edit
-											</button>
-											<button
-												type="button"
-												onClick={() => setDeletingCard(card)}
-												style={{
-													backgroundColor: "#dc3545",
-													color: "white",
-													border: "none",
-													padding: "0.5rem 1rem",
-													borderRadius: "4px",
-													cursor: "pointer",
-												}}
-											>
-												Delete
-											</button>
-										</div>
 									</div>
-								</li>
-							))}
-						</ul>
-					)}
-				</main>
-			)}
+								))}
+							</div>
+						)}
+					</div>
+				)}
+			</main>
 
+			{/* Modals */}
 			{deckId && (
 				<CreateCardModal
 					isOpen={isCreateModalOpen}
-- 
cgit v1.2.3-70-g09d2

