feat: implement dark mode support with theme provider and toggle component
This commit is contained in:
parent
0086c0142b
commit
cf70aa4a67
12 changed files with 248 additions and 31 deletions
|
|
@ -321,7 +321,7 @@ export default function AddPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(247,229,236,0.85)_45%,rgba(244,240,243,1)_100%)] text-foreground">
|
||||
<div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ export default function DashboardPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(247,229,236,0.85)_45%,rgba(244,240,243,1)_100%)] text-foreground">
|
||||
<div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto w-full max-w-6xl space-y-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str
|
|||
post = await postPromise;
|
||||
} catch (loadError) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(247,229,236,0.85)_45%,rgba(244,240,243,1)_100%)] text-foreground">
|
||||
<div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
|
||||
<Header />
|
||||
<main className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<section className="border-b border-border/70 pb-6">
|
||||
|
|
@ -226,7 +226,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str
|
|||
const tags = post.tags ?? [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(247,229,236,0.85)_45%,rgba(244,240,243,1)_100%)] text-foreground">
|
||||
<div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
|
|
@ -249,10 +249,10 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str
|
|||
alt={post.author ?? post._id}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
className="w-full border border-border bg-white object-contain image-scale"
|
||||
className="w-full border border-border bg-card object-contain image-scale"
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-4/5 w-full border border-border bg-black/5" />
|
||||
<div className="aspect-4/5 w-full border border-border bg-muted/30" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -294,7 +294,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str
|
|||
href={post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="border border-border px-4 py-2 text-sm text-foreground/80 transition hover:bg-black/5"
|
||||
className="border border-border px-4 py-2 text-sm text-foreground/80 transition hover:bg-accent"
|
||||
>
|
||||
원본 보기
|
||||
</a>
|
||||
|
|
@ -302,7 +302,7 @@ export default async function DetailPage({ params }: { params: Promise<{ id: str
|
|||
<div className="flex flex-row gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="border border-border px-4 py-2 text-sm text-foreground/70 transition hover:bg-black/5 w-full"
|
||||
className="border border-border px-4 py-2 text-sm text-foreground/70 transition hover:bg-accent w-full"
|
||||
>
|
||||
돌아가기
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export default function EditPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(247,229,236,0.85)_45%,rgba(244,240,243,1)_100%)] text-foreground">
|
||||
<div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
src: url("/fonts/Orbit-Regular.woff2") format("truetype");
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* Custom dark variant for class-based dark mode in Tailwind v4 */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-background: hsl(340 40% 98%);
|
||||
--color-foreground: hsl(315 21% 8%);
|
||||
--color-card: hsl(340 40% 98%);
|
||||
|
|
@ -45,10 +48,52 @@
|
|||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer theme {
|
||||
.dark {
|
||||
--color-background: hsl(240 10% 4%);
|
||||
--color-foreground: hsl(0 0% 95%);
|
||||
--color-card: hsl(240 10% 5%);
|
||||
--color-card-foreground: hsl(0 0% 95%);
|
||||
--color-popover: hsl(240 10% 4%);
|
||||
--color-popover-foreground: hsl(0 0% 95%);
|
||||
--color-primary: hsl(340 50% 70%);
|
||||
--color-primary-foreground: hsl(240 10% 4%);
|
||||
--color-secondary: hsl(240 5% 15%);
|
||||
--color-secondary-foreground: hsl(0 0% 95%);
|
||||
--color-muted: hsl(240 5% 12%);
|
||||
--color-muted-foreground: hsl(240 5% 65%);
|
||||
--color-accent: hsl(240 5% 15%);
|
||||
--color-accent-foreground: hsl(0 0% 95%);
|
||||
--color-destructive: hsl(0 62.8% 30.6%);
|
||||
--color-destructive-foreground: hsl(0 0% 95%);
|
||||
--color-border: hsl(240 5% 18%);
|
||||
--color-input: hsl(240 5% 18%);
|
||||
--color-ring: hsl(340 50% 70%);
|
||||
--color-chart-1: hsl(220 70% 50%);
|
||||
--color-chart-2: hsl(160 60% 45%);
|
||||
--color-chart-3: hsl(30 80% 55%);
|
||||
--color-chart-4: hsl(280 65% 60%);
|
||||
--color-chart-5: hsl(340 75% 55%);
|
||||
--color-sidebar: hsl(240 10% 5%);
|
||||
--color-sidebar-foreground: hsl(240 5% 90%);
|
||||
--color-sidebar-primary: hsl(340 50% 70%);
|
||||
--color-sidebar-primary-foreground: hsl(240 10% 4%);
|
||||
--color-sidebar-accent: hsl(240 5% 15%);
|
||||
--color-sidebar-accent-foreground: hsl(0 0% 95%);
|
||||
--color-sidebar-border: hsl(240 5% 18%);
|
||||
--color-sidebar-ring: hsl(340 50% 70%);
|
||||
|
||||
--scrollbar: hsla(240 5% 40% / 0.5);
|
||||
--scrollbar-hover: hsla(240 5% 40% / 0.8);
|
||||
--selection: #ff1493;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--scrollbar: hsla(340 10% 60% / 0.5);
|
||||
--scrollbar-hover: hsla(340 10% 60% / 0.8);
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--selection: #ff69b4;
|
||||
}
|
||||
|
||||
.image-scale {
|
||||
|
|
@ -87,21 +132,31 @@
|
|||
background: var(--scrollbar-hover);
|
||||
}
|
||||
|
||||
/* ::-webkit-scrollbar:not(.highlighttable, .highlight table, .gist .highlight) {
|
||||
background: var(--theme);
|
||||
}
|
||||
*/
|
||||
/* reset */
|
||||
::-webkit-scrollbar {
|
||||
width: 19px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
/* from PaperMod https://github.com/adityatelange/hugo-PaperMod/blob/c98a924842fc7ee0c14212c316c69ede3ad76ca3/assets/css/includes/scroll-bar.css */
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground min-h-screen font-sans antialiased;
|
||||
font-family: "Orbit", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bg-main-gradient {
|
||||
background-image: radial-gradient(circle at top, rgba(255, 255, 255, 0.9), rgba(247, 229, 236, 0.85) 45%, rgba(244, 240, 243, 1) 100%);
|
||||
}
|
||||
|
||||
.dark .bg-main-gradient {
|
||||
background-image:
|
||||
radial-gradient(circle at top left, rgba(255, 105, 180, 0.03), transparent 40%),
|
||||
radial-gradient(circle at top, rgba(80, 80, 100, 0.08), rgba(10, 10, 15, 0.95) 40%, rgba(5, 5, 8, 1) 100%);
|
||||
}
|
||||
|
||||
.dark img {
|
||||
filter: invert(1) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import "./globals.css";
|
|||
import "react-photo-album/masonry.css";
|
||||
|
||||
|
||||
import { ThemeProvider } from "../components/theme-provider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Akiyama Mizuki",
|
||||
description: "Gallery"
|
||||
|
|
@ -17,8 +19,13 @@ export default function RootLayout({
|
|||
<html
|
||||
lang="en"
|
||||
className={`h-full antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<body className="min-h-full flex flex-col bg-background text-foreground transition-colors duration-300">
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type GalleryPhoto = {
|
|||
key: string;
|
||||
alt: string;
|
||||
author: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
type Me = {
|
||||
|
|
@ -414,6 +415,7 @@ export default function App() {
|
|||
key: upload._id,
|
||||
alt: `tweet ${upload.tweetId} media ${upload.mediaIndex + 1}`,
|
||||
author: upload.author?.trim() || "unknown",
|
||||
source: upload.tweet?.url || "",
|
||||
});
|
||||
};
|
||||
image.onerror = () => {
|
||||
|
|
@ -424,6 +426,7 @@ export default function App() {
|
|||
key: upload._id,
|
||||
alt: `tweet ${upload.tweetId} media ${upload.mediaIndex + 1}`,
|
||||
author: upload.author?.trim() || "unknown",
|
||||
source: upload.tweet?.url || "",
|
||||
});
|
||||
};
|
||||
}),
|
||||
|
|
@ -443,7 +446,7 @@ export default function App() {
|
|||
}, [items]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(247,229,236,0.85)_45%,rgba(244,240,243,1)_100%)] text-foreground">
|
||||
<div className="min-h-screen bg-main-gradient text-foreground transition-colors duration-500">
|
||||
<div className="sticky top-0 z-50">
|
||||
<Header />
|
||||
<div className="border-b border-border bg-background/85 backdrop-blur px-6 py-2">
|
||||
|
|
@ -482,7 +485,7 @@ export default function App() {
|
|||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="aspect-4/5 animate-pulse bg-black/8"
|
||||
className="aspect-4/5 animate-pulse bg-muted/50"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -515,10 +518,19 @@ export default function App() {
|
|||
decoding="async"
|
||||
className="block w-full"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-x-3 bottom-3 translate-y-3 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100">
|
||||
<div className="inline-flex max-w-full items-center rounded-t-sm rounded-b-lg bg-black/72 px-3 py-1.5 text-xs text-white backdrop-blur-sm">
|
||||
<div className="absolute inset-x-3 bottom-3 translate-y-3 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
window.open(photo.source, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
className="inline-flex max-w-full items-center rounded-t-sm rounded-b-lg bg-black/72 px-3 py-1.5 text-xs text-white backdrop-blur-sm pointer-events-auto"
|
||||
title="원본 이미지 열기"
|
||||
>
|
||||
<span className="truncate">© {photo.author}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
),
|
||||
|
|
@ -564,7 +576,7 @@ export default function App() {
|
|||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-black/5"
|
||||
className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-accent"
|
||||
onClick={async () => {
|
||||
await copyImage(contextMenu.photo);
|
||||
setContextMenu(null);
|
||||
|
|
@ -574,7 +586,7 @@ export default function App() {
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-black/5"
|
||||
className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-accent"
|
||||
onClick={() => {
|
||||
saveImage(contextMenu.photo);
|
||||
setContextMenu(null);
|
||||
|
|
@ -591,7 +603,7 @@ export default function App() {
|
|||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={`loading-more-${index}`}
|
||||
className="aspect-4/5 animate-pulse bg-black/8"
|
||||
className="aspect-4/5 animate-pulse bg-muted/50"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -615,7 +627,7 @@ export default function App() {
|
|||
<div className="mt-5 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded px-4 py-2 text-sm text-foreground/70 hover:bg-black/5 disabled:opacity-50"
|
||||
className="rounded px-4 py-2 text-sm text-foreground/70 hover:bg-accent disabled:opacity-50"
|
||||
onClick={() => setDeleteTarget(null)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue