Skip to content

Commit

Permalink
feat: theme-switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
coderz-w committed Oct 16, 2024
1 parent 162ab77 commit 19d501e
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 20 deletions.
11 changes: 11 additions & 0 deletions public/image/viewTransition.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/(app)/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client';

export default function Home() {
return <div>哈哈qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq哈</div>;
return <div className="">哈哈qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq哈</div>;
}
11 changes: 4 additions & 7 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Metadata, Viewport } from 'next';

import WebAppProviders from '@/components/providers/root';
import AccentColorStyleInjector from '@/components/modules/shared/AccentColorStyleInjector';
import Root from '@/components/layout/Root';
import { seo } from '~/seo';

export const metadata: Metadata = {
Expand Down Expand Up @@ -62,7 +63,9 @@ export default async function RootLayout({ children }: PropsWithChildren) {
</head>
<body>
<WebAppProviders>
<div data-theme>{children}</div>
<div data-theme>
<Root>{children}</Root>
</div>
</WebAppProviders>
</body>
</html>
Expand All @@ -86,9 +89,3 @@ const SayHi = () => {
/>
);
};

declare global {
interface Window {
version: string;
}
}
7 changes: 7 additions & 0 deletions src/components/layout/Content/Content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PropsWithChildren } from 'react';

const Content = ({ children }: PropsWithChildren) => {
return <main className="relative z-[1] px-4 pt-[4.5rem] fill-content md:px-0">{children}</main>;
};

export default Content;
2 changes: 2 additions & 0 deletions src/components/layout/Content/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Content from './Content';
export default Content;
20 changes: 20 additions & 0 deletions src/components/layout/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ThemeSwitcher from '@/components/ui/ThemeSwitcher';

const Footer = () => {
return (
<footer
data-hide-print
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"
>
<div className="px-4 sm:px-8 h-full">
<div className="relative mx-auto max-w-7xl lg:px-8 h-full">
<div className="mt-6 block text-center md:absolute md:bottom-0 md:right-0 md:mt-0">
<ThemeSwitcher></ThemeSwitcher>
</div>
</div>
</div>
</footer>
);
};

export default Footer;
2 changes: 2 additions & 0 deletions src/components/layout/Footer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Footer from './Footer';
export default Footer;
23 changes: 23 additions & 0 deletions src/components/layout/Root/Root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { PropsWithChildren } from 'react';

// import { ClientOnly } from '~/components/common/ClientOnly';

import Content from '../Content';
import Footer from '../Footer';
// import { Header } from '../Header';

const Root = ({ children }: PropsWithChildren) => {
return (
<>
{/* <Header /> */}
<Content>{children}</Content>

<Footer />
{/* <ClientOnly>
<FABContainer />
</ClientOnly> */}
</>
);
};

export default Root;
2 changes: 2 additions & 0 deletions src/components/layout/Root/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Root from './Root';
export default Root;
157 changes: 157 additions & 0 deletions src/components/ui/ThemeSwitcher/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
'use client';

import { useTheme } from 'next-themes';
import { flushSync } from 'react-dom';
import { tv } from 'tailwind-variants';

import useIsClient from '@/hooks/common/use-is-client';
import transitionViewIfSupported from '@/lib/transitionViewIfSupported';

const styles = tv({
base: 'rounded-inherit inline-flex h-[32px] w-[32px] items-center justify-center border-0 text-current',
variants: {
status: {
active: '',
},
},
});

const iconClassNames = 'h-4 w-4 text-current';

const SunIcon = () => {
return (
<svg
className={iconClassNames}
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2" />
<path d="M12 21v2" />
<path d="M4.22 4.22l1.42 1.42" />
<path d="M18.36 18.36l1.42 1.42" />
<path d="M1 12h2" />
<path d="M21 12h2" />
<path d="M4.22 19.78l1.42-1.42" />
<path d="M18.36 5.64l1.42-1.42" />
</svg>
);
};

const SystemIcon = () => {
return (
<svg
className={iconClassNames}
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<path d="M8 21h8" />
<path d="M12 17v4" />
</svg>
);
};

const DarkIcon = () => {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
className={iconClassNames}
>
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
);
};

const ThemeSwitcher = () => {
return (
<div className="relative inline-block">
<ThemeIndicator />
<ButtonGroup />
</div>
);
};

export default ThemeSwitcher;

const ThemeIndicator = () => {
const { theme } = useTheme();
const isClient = useIsClient();

if (!theme) return null;
if (!isClient) return null;

return (
<div
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"
style={{
left: { light: 4, system: 36, dark: 68 }[theme],
}}
/>
);
};

const ButtonGroup = () => {
const { setTheme } = useTheme();

const buildThemeTransition = (theme: 'light' | 'dark' | 'system') => {
transitionViewIfSupported(() => flushSync(() => setTheme(theme)));
};

return (
<div className="w-fit-content inline-flex rounded-full border border-zinc-200 p-[3px] dark:border-zinc-700">
<button
aria-label="Switch to light theme"
type="button"
className={styles.base}
onClick={() => {
buildThemeTransition('light');
}}
>
<SunIcon />
</button>
<button
aria-label="Switch to system theme"
className={styles.base}
type="button"
onClick={() => {
buildThemeTransition('system');
}}
>
<SystemIcon />
</button>
<button
aria-label="Switch to dark theme"
className={styles.base}
type="button"
onClick={() => {
buildThemeTransition('dark');
}}
>
<DarkIcon />
</button>
</div>
);
};
3 changes: 3 additions & 0 deletions src/components/ui/ThemeSwitcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ThemeSwitcher from './ThemeSwitcher';

export default ThemeSwitcher;
13 changes: 13 additions & 0 deletions src/hooks/common/use-is-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react';

const useIsClient = () => {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

return isClient;
};

export default useIsClient;
21 changes: 21 additions & 0 deletions src/lib/transitionViewIfSupported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const transitionViewIfSupported = (updateCb: () => any) => {
if (window.matchMedia(`(prefers-reduced-motion: reduce)`).matches) {
updateCb();

return;
}

if (document.startViewTransition) {
document.startViewTransition(updateCb);
} else {
updateCb();
}
};

export default transitionViewIfSupported;

declare global {
interface Document {
startViewTransition: (cb: any) => void;
}
}
42 changes: 30 additions & 12 deletions src/styles/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,45 @@ details summary {
background-color: var(--accent-color);
}

/* 视图过渡组 */
::view-transition-group(root) {
animation-duration: 1.2s;
animation-timing-function: cubic-bezier(0.19, 1, 0.22, 1);
}

/* 新视图的过渡效果 */
::view-transition-new(root) {
animation: turnOff 800ms ease-in-out;
animation-name: reveal-light;
}
::view-transition-old(root) {

/* 旧视图的过渡效果 */
::view-transition-old(root),
.dark::view-transition-old(root) {
animation: none;
z-index: -1;
}

@keyframes turnOn {
0% {
clip-path: polygon(0% 0%, 100% 0, 100% 0, 0 0);
/* 暗模式下的新视图过渡效果 */
.dark::view-transition-new(root) {
animation-name: reveal-dark;
}

@keyframes reveal-light {
from {
clip-path: polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%);
}
100% {
clip-path: polygon(0% 0%, 100% 0, 100% 100%, 0 100%);
to {
clip-path: polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%);
}
}

[data-theme='dark']::view-transition-new(root) {
animation: turnOn 800ms ease-in-out;
}
::view-transition-old(root) {
animation: none;
@keyframes reveal-dark {
from {
clip-path: polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%);
}
to {
clip-path: polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%);
}
}

@keyframes turnOff {
Expand Down

0 comments on commit 19d501e

Please sign in to comment.