Skip to content

Commit 19d501e

Browse files
committed
feat: theme-switcher
1 parent 162ab77 commit 19d501e

File tree

14 files changed

+296
-20
lines changed

14 files changed

+296
-20
lines changed

public/image/viewTransition.svg

Lines changed: 11 additions & 0 deletions
Loading

src/app/(app)/(home)/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client';
22

33
export default function Home() {
4-
return <div>哈哈qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq哈</div>;
4+
return <div className="">哈哈qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq哈</div>;
55
}

src/app/layout.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Metadata, Viewport } from 'next';
55

66
import WebAppProviders from '@/components/providers/root';
77
import AccentColorStyleInjector from '@/components/modules/shared/AccentColorStyleInjector';
8+
import Root from '@/components/layout/Root';
89
import { seo } from '~/seo';
910

1011
export const metadata: Metadata = {
@@ -62,7 +63,9 @@ export default async function RootLayout({ children }: PropsWithChildren) {
6263
</head>
6364
<body>
6465
<WebAppProviders>
65-
<div data-theme>{children}</div>
66+
<div data-theme>
67+
<Root>{children}</Root>
68+
</div>
6669
</WebAppProviders>
6770
</body>
6871
</html>
@@ -86,9 +89,3 @@ const SayHi = () => {
8689
/>
8790
);
8891
};
89-
90-
declare global {
91-
interface Window {
92-
version: string;
93-
}
94-
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { PropsWithChildren } from 'react';
2+
3+
const Content = ({ children }: PropsWithChildren) => {
4+
return <main className="relative z-[1] px-4 pt-[4.5rem] fill-content md:px-0">{children}</main>;
5+
};
6+
7+
export default Content;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Content from './Content';
2+
export default Content;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import ThemeSwitcher from '@/components/ui/ThemeSwitcher';
2+
3+
const Footer = () => {
4+
return (
5+
<footer
6+
data-hide-print
7+
className="relative z-[1] h-40 mt-32 border-t border-x-uk-separator-opaque-light py-6 text-base-content/80 dark:border-white/10"
8+
>
9+
<div className="px-4 sm:px-8 h-full">
10+
<div className="relative mx-auto max-w-7xl lg:px-8 h-full">
11+
<div className="mt-6 block text-center md:absolute md:bottom-0 md:right-0 md:mt-0">
12+
<ThemeSwitcher></ThemeSwitcher>
13+
</div>
14+
</div>
15+
</div>
16+
</footer>
17+
);
18+
};
19+
20+
export default Footer;

src/components/layout/Footer/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Footer from './Footer';
2+
export default Footer;

src/components/layout/Root/Root.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { PropsWithChildren } from 'react';
2+
3+
// import { ClientOnly } from '~/components/common/ClientOnly';
4+
5+
import Content from '../Content';
6+
import Footer from '../Footer';
7+
// import { Header } from '../Header';
8+
9+
const Root = ({ children }: PropsWithChildren) => {
10+
return (
11+
<>
12+
{/* <Header /> */}
13+
<Content>{children}</Content>
14+
15+
<Footer />
16+
{/* <ClientOnly>
17+
<FABContainer />
18+
</ClientOnly> */}
19+
</>
20+
);
21+
};
22+
23+
export default Root;

src/components/layout/Root/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Root from './Root';
2+
export default Root;
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use client';
2+
3+
import { useTheme } from 'next-themes';
4+
import { flushSync } from 'react-dom';
5+
import { tv } from 'tailwind-variants';
6+
7+
import useIsClient from '@/hooks/common/use-is-client';
8+
import transitionViewIfSupported from '@/lib/transitionViewIfSupported';
9+
10+
const styles = tv({
11+
base: 'rounded-inherit inline-flex h-[32px] w-[32px] items-center justify-center border-0 text-current',
12+
variants: {
13+
status: {
14+
active: '',
15+
},
16+
},
17+
});
18+
19+
const iconClassNames = 'h-4 w-4 text-current';
20+
21+
const SunIcon = () => {
22+
return (
23+
<svg
24+
className={iconClassNames}
25+
fill="none"
26+
height="24"
27+
shapeRendering="geometricPrecision"
28+
stroke="currentColor"
29+
strokeLinecap="round"
30+
strokeLinejoin="round"
31+
strokeWidth="1.5"
32+
viewBox="0 0 24 24"
33+
width="24"
34+
>
35+
<circle cx="12" cy="12" r="5" />
36+
<path d="M12 1v2" />
37+
<path d="M12 21v2" />
38+
<path d="M4.22 4.22l1.42 1.42" />
39+
<path d="M18.36 18.36l1.42 1.42" />
40+
<path d="M1 12h2" />
41+
<path d="M21 12h2" />
42+
<path d="M4.22 19.78l1.42-1.42" />
43+
<path d="M18.36 5.64l1.42-1.42" />
44+
</svg>
45+
);
46+
};
47+
48+
const SystemIcon = () => {
49+
return (
50+
<svg
51+
className={iconClassNames}
52+
fill="none"
53+
height="24"
54+
shapeRendering="geometricPrecision"
55+
stroke="currentColor"
56+
strokeLinecap="round"
57+
strokeLinejoin="round"
58+
strokeWidth="1.5"
59+
viewBox="0 0 24 24"
60+
width="24"
61+
>
62+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
63+
<path d="M8 21h8" />
64+
<path d="M12 17v4" />
65+
</svg>
66+
);
67+
};
68+
69+
const DarkIcon = () => {
70+
return (
71+
<svg
72+
fill="none"
73+
height="24"
74+
shapeRendering="geometricPrecision"
75+
stroke="currentColor"
76+
strokeLinecap="round"
77+
strokeLinejoin="round"
78+
strokeWidth="1.5"
79+
viewBox="0 0 24 24"
80+
width="24"
81+
className={iconClassNames}
82+
>
83+
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
84+
</svg>
85+
);
86+
};
87+
88+
const ThemeSwitcher = () => {
89+
return (
90+
<div className="relative inline-block">
91+
<ThemeIndicator />
92+
<ButtonGroup />
93+
</div>
94+
);
95+
};
96+
97+
export default ThemeSwitcher;
98+
99+
const ThemeIndicator = () => {
100+
const { theme } = useTheme();
101+
const isClient = useIsClient();
102+
103+
if (!theme) return null;
104+
if (!isClient) return null;
105+
106+
return (
107+
<div
108+
className="absolute top-[4px] z-[-1] size-[32px] rounded-full bg-base-100 shadow-[0_1px_2px_0_rgba(127.5,127.5,127.5,.2),_0_1px_3px_0_rgba(127.5,127.5,127.5,.1)] duration-200"
109+
style={{
110+
left: { light: 4, system: 36, dark: 68 }[theme],
111+
}}
112+
/>
113+
);
114+
};
115+
116+
const ButtonGroup = () => {
117+
const { setTheme } = useTheme();
118+
119+
const buildThemeTransition = (theme: 'light' | 'dark' | 'system') => {
120+
transitionViewIfSupported(() => flushSync(() => setTheme(theme)));
121+
};
122+
123+
return (
124+
<div className="w-fit-content inline-flex rounded-full border border-zinc-200 p-[3px] dark:border-zinc-700">
125+
<button
126+
aria-label="Switch to light theme"
127+
type="button"
128+
className={styles.base}
129+
onClick={() => {
130+
buildThemeTransition('light');
131+
}}
132+
>
133+
<SunIcon />
134+
</button>
135+
<button
136+
aria-label="Switch to system theme"
137+
className={styles.base}
138+
type="button"
139+
onClick={() => {
140+
buildThemeTransition('system');
141+
}}
142+
>
143+
<SystemIcon />
144+
</button>
145+
<button
146+
aria-label="Switch to dark theme"
147+
className={styles.base}
148+
type="button"
149+
onClick={() => {
150+
buildThemeTransition('dark');
151+
}}
152+
>
153+
<DarkIcon />
154+
</button>
155+
</div>
156+
);
157+
};

0 commit comments

Comments
 (0)