Add dark mode with system preference auto-detection

All user-facing components now support dark mode via prefers-color-scheme.
- Dark backgrounds: gray-950/900/800
- Dark text: gray-100/200/300/400
- Orange brand colors adapt with darker tints
- Glass effects work in both modes
- Skeletons, cards, filters, bottom sheet all themed
- Google Maps InfoWindow stays light (maps don't support dark)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-09 16:14:29 +09:00
parent 99660bf07b
commit 6223691b33
9 changed files with 102 additions and 111 deletions

View File

@@ -3,7 +3,7 @@
:root {
--background: #ffffff;
--foreground: #171717;
color-scheme: light only;
color-scheme: light dark;
}
@theme inline {
@@ -14,16 +14,11 @@
@media (prefers-color-scheme: dark) {
:root {
--background: #ffffff;
--foreground: #171717;
color-scheme: light only;
--background: #0a0a0a;
--foreground: #ededed;
}
}
* {
color-scheme: light only;
}
body {
background: var(--background);
color: var(--foreground);
@@ -34,11 +29,7 @@ html, body, #__next {
margin: 0;
}
input, select, textarea {
color-scheme: light;
}
/* Force Google Maps InfoWindow to light mode */
/* Force Google Maps InfoWindow to light mode (maps don't support dark) */
.gm-style .gm-style-iw,
.gm-style .gm-style-iw-c,
.gm-style .gm-style-iw-d,

View File

@@ -19,7 +19,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="ko" style={{ colorScheme: "light" }}>
<html lang="ko" className="dark:bg-gray-950" suppressHydrationWarning>
<body className={`${geist.variable} font-sans antialiased`}>
<Providers>{children}</Providers>
</body>

View File

@@ -418,9 +418,9 @@ export default function Home() {
);
return (
<div className="h-screen flex flex-col">
<div className="h-screen flex flex-col bg-white dark:bg-gray-950">
{/* ── Header row 1: Logo + User ── */}
<header className="bg-white/80 backdrop-blur-md border-b shrink-0">
<header className="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b dark:border-gray-800 shrink-0">
<div className="px-5 py-3 flex items-center justify-between">
<button onClick={handleReset} className="text-lg font-bold whitespace-nowrap">
Tasteby
@@ -440,7 +440,7 @@ export default function Home() {
setSelected(null);
setShowDetail(false);
}}
className="border rounded-lg px-3 py-1.5 text-sm text-gray-600"
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
>
<option value="">📺 </option>
{channels.map((ch) => (
@@ -452,7 +452,7 @@ export default function Home() {
<select
value={cuisineFilter}
onChange={(e) => setCuisineFilter(e.target.value)}
className="border rounded-lg px-3 py-1.5 text-sm text-gray-600"
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
>
<option value="">🍽 </option>
{CUISINE_GROUPS.map((g) => (
@@ -462,7 +462,7 @@ export default function Home() {
<select
value={priceFilter}
onChange={(e) => setPriceFilter(e.target.value)}
className="border rounded-lg px-3 py-1.5 text-sm text-gray-600"
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
>
<option value="">💰 </option>
{PRICE_GROUPS.map((g) => (
@@ -475,7 +475,7 @@ export default function Home() {
<select
value={countryFilter}
onChange={(e) => handleCountryChange(e.target.value)}
className="border rounded-lg px-3 py-1.5 text-sm text-gray-600"
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
>
<option value="">🌍 </option>
{countries.map((c) => (
@@ -486,7 +486,7 @@ export default function Home() {
<select
value={cityFilter}
onChange={(e) => handleCityChange(e.target.value)}
className="border rounded-lg px-3 py-1.5 text-sm text-gray-600"
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
>
<option value="">🏙 /</option>
{cities.map((c) => (
@@ -498,7 +498,7 @@ export default function Home() {
<select
value={districtFilter}
onChange={(e) => handleDistrictChange(e.target.value)}
className="border rounded-lg px-3 py-1.5 text-sm text-gray-600"
className="border dark:border-gray-700 rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
>
<option value="">🏘 /</option>
{districts.map((d) => (
@@ -506,13 +506,13 @@ export default function Home() {
))}
</select>
)}
<div className="w-px h-5 bg-gray-200" />
<div className="w-px h-5 bg-gray-200 dark:bg-gray-700" />
<button
onClick={() => setBoundsFilterOn(!boundsFilterOn)}
className={`px-3 py-1.5 text-sm border rounded-lg transition-colors ${
boundsFilterOn
? "bg-orange-50 border-orange-300 text-orange-600"
: "hover:bg-gray-100 text-gray-600"
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
: "hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
title="지도 영역 내 식당만 표시"
>
@@ -520,7 +520,7 @@ export default function Home() {
</button>
<button
onClick={() => setViewMode(viewMode === "map" ? "list" : "map")}
className="px-3 py-1.5 text-sm border rounded-lg transition-colors hover:bg-gray-100 text-gray-600"
className="px-3 py-1.5 text-sm border rounded-lg transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400"
title={viewMode === "map" ? "리스트 우선" : "지도 우선"}
>
{viewMode === "map" ? "🗺 지도" : "☰ 리스트"}
@@ -531,8 +531,8 @@ export default function Home() {
onClick={handleToggleFavorites}
className={`px-3.5 py-1.5 text-sm rounded-full border transition-colors ${
showFavorites
? "bg-rose-50 border-rose-300 text-rose-600"
: "border-gray-300 text-gray-600 hover:bg-gray-100"
? "bg-rose-50 dark:bg-rose-900/30 border-rose-300 dark:border-rose-700 text-rose-600 dark:text-rose-400"
: "border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-100"
}`}
>
{showFavorites ? "♥ 내 찜" : "♡ 찜"}
@@ -541,8 +541,8 @@ export default function Home() {
onClick={handleToggleMyReviews}
className={`px-3.5 py-1.5 text-sm rounded-full border transition-colors ${
showMyReviews
? "bg-orange-50 border-orange-300 text-orange-600"
: "border-gray-300 text-gray-600 hover:bg-gray-100"
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
: "border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-100"
}`}
>
{showMyReviews ? "✎ 내 리뷰" : "✎ 리뷰"}
@@ -570,12 +570,12 @@ export default function Home() {
{(user.nickname || user.email || "?").charAt(0).toUpperCase()}
</div>
)}
<span className="hidden sm:inline text-sm font-medium text-gray-700">
<span className="hidden sm:inline text-sm font-medium text-gray-700 dark:text-gray-300">
{user.nickname || user.email}
</span>
<button
onClick={logout}
className="ml-1 px-2.5 py-1 text-xs text-gray-500 border border-gray-300 rounded-full hover:bg-gray-100 hover:text-gray-700 transition-colors"
className="ml-1 px-2.5 py-1 text-xs text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-700 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
>
</button>
@@ -604,7 +604,7 @@ export default function Home() {
onClick={() => setViewMode(viewMode === "map" ? "list" : "map")}
className={`px-3 py-1.5 text-xs border rounded-lg transition-colors ${
viewMode === "map"
? "bg-orange-50 border-orange-300 text-orange-600"
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
: "text-gray-600"
}`}
>
@@ -614,7 +614,7 @@ export default function Home() {
onClick={() => setShowMobileFilters(!showMobileFilters)}
className={`px-3 py-1.5 text-xs border rounded-lg transition-colors relative ${
showMobileFilters || channelFilter || cuisineFilter || priceFilter || countryFilter || boundsFilterOn
? "bg-orange-50 border-orange-300 text-orange-600"
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
: "text-gray-600"
}`}
>
@@ -631,8 +631,8 @@ export default function Home() {
onClick={handleToggleFavorites}
className={`px-3 py-1.5 text-xs rounded-full border transition-colors ${
showFavorites
? "bg-rose-50 border-rose-300 text-rose-600"
: "border-gray-300 text-gray-600"
? "bg-rose-50 dark:bg-rose-900/30 border-rose-300 dark:border-rose-700 text-rose-600 dark:text-rose-400"
: "border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400"
}`}
>
{showFavorites ? "♥ 찜" : "♡ 찜"}
@@ -641,8 +641,8 @@ export default function Home() {
onClick={handleToggleMyReviews}
className={`px-3 py-1.5 text-xs rounded-full border transition-colors ${
showMyReviews
? "bg-orange-50 border-orange-300 text-orange-600"
: "border-gray-300 text-gray-600"
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
: "border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400"
}`}
>
{showMyReviews ? "✎ 리뷰" : "✎ 리뷰"}
@@ -656,7 +656,7 @@ export default function Home() {
{/* Collapsible filter panel */}
{showMobileFilters && (
<div className="bg-white/70 backdrop-blur-md rounded-xl p-3.5 space-y-3 border border-white/50 shadow-sm">
<div className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-md rounded-xl p-3.5 space-y-3 border border-white/50 dark:border-gray-700/50 shadow-sm">
{/* Dropdown filters */}
<div className="flex items-center gap-2 flex-wrap">
<select
@@ -666,7 +666,7 @@ export default function Home() {
setSelected(null);
setShowDetail(false);
}}
className="border rounded-lg px-2.5 py-1.5 text-xs text-gray-600 bg-white"
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
>
<option value="">📺 </option>
{channels.map((ch) => (
@@ -678,7 +678,7 @@ export default function Home() {
<select
value={cuisineFilter}
onChange={(e) => setCuisineFilter(e.target.value)}
className="border rounded-lg px-2.5 py-1.5 text-xs text-gray-600 bg-white"
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
>
<option value="">🍽 </option>
{CUISINE_GROUPS.map((g) => (
@@ -688,7 +688,7 @@ export default function Home() {
<select
value={priceFilter}
onChange={(e) => setPriceFilter(e.target.value)}
className="border rounded-lg px-2.5 py-1.5 text-xs text-gray-600 bg-white"
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
>
<option value="">💰 </option>
{PRICE_GROUPS.map((g) => (
@@ -701,7 +701,7 @@ export default function Home() {
<select
value={countryFilter}
onChange={(e) => handleCountryChange(e.target.value)}
className="border rounded-lg px-2.5 py-1.5 text-xs text-gray-600 bg-white"
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
>
<option value="">🌍 </option>
{countries.map((c) => (
@@ -712,7 +712,7 @@ export default function Home() {
<select
value={cityFilter}
onChange={(e) => handleCityChange(e.target.value)}
className="border rounded-lg px-2.5 py-1.5 text-xs text-gray-600 bg-white"
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
>
<option value="">🏙 /</option>
{cities.map((c) => (
@@ -724,7 +724,7 @@ export default function Home() {
<select
value={districtFilter}
onChange={(e) => handleDistrictChange(e.target.value)}
className="border rounded-lg px-2.5 py-1.5 text-xs text-gray-600 bg-white"
className="border dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800"
>
<option value="">🏘 /</option>
{districts.map((d) => (
@@ -739,8 +739,8 @@ export default function Home() {
onClick={() => setBoundsFilterOn(!boundsFilterOn)}
className={`px-2.5 py-1.5 text-xs border rounded-lg transition-colors ${
boundsFilterOn
? "bg-orange-50 border-orange-300 text-orange-600"
: "text-gray-600 bg-white"
? "bg-orange-50 dark:bg-orange-900/30 border-orange-300 dark:border-orange-700 text-orange-600 dark:text-orange-400"
: "text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800"
}`}
>
{boundsFilterOn ? "📍 영역 ON" : "📍 영역"}
@@ -757,7 +757,7 @@ export default function Home() {
<div className="hidden md:flex flex-1 overflow-hidden">
{viewMode === "map" ? (
<>
<aside className="w-80 bg-white border-r overflow-y-auto shrink-0">
<aside className="w-80 bg-white dark:bg-gray-950 border-r dark:border-gray-800 overflow-y-auto shrink-0">
{sidebarContent}
</aside>
<main className="flex-1 relative">
@@ -777,10 +777,10 @@ export default function Home() {
</>
) : (
<>
<aside className="flex-1 bg-white overflow-y-auto">
<aside className="flex-1 bg-white dark:bg-gray-950 overflow-y-auto">
{sidebarContent}
</aside>
<main className="w-[40%] shrink-0 relative border-l">
<main className="w-[40%] shrink-0 relative border-l dark:border-gray-800">
<MapView
restaurants={filteredRestaurants}
selected={selected}
@@ -819,7 +819,7 @@ export default function Home() {
</>
) : (
<>
<div className="flex-1 bg-white overflow-y-auto">
<div className="flex-1 bg-white dark:bg-gray-950 overflow-y-auto">
{mobileListContent}
{/* Scroll-down hint to reveal map */}
<div className="flex flex-col items-center py-4 text-gray-300">
@@ -827,7 +827,7 @@ export default function Home() {
<span className="text-[10px]"> </span>
</div>
</div>
<div className="h-[35vh] shrink-0 relative border-t">
<div className="h-[35vh] shrink-0 relative border-t dark:border-gray-800">
<MapView
restaurants={filteredRestaurants}
selected={selected}
@@ -853,7 +853,7 @@ export default function Home() {
</div>
{/* Footer */}
<footer className="shrink-0 border-t bg-white/60 backdrop-blur-sm py-2.5 flex items-center justify-center gap-2 text-[11px] text-gray-400 group">
<footer className="shrink-0 border-t dark:border-gray-800 bg-white/60 dark:bg-gray-900/60 backdrop-blur-sm py-2.5 flex items-center justify-center gap-2 text-[11px] text-gray-400 dark:text-gray-500 group">
<div className="relative">
<img
src="/icon.jpg"