Skip to content

Commit b5b2b04

Browse files
committed
feat: view transition
Signed-off-by: Innei <[email protected]>
1 parent d58fced commit b5b2b04

File tree

7 files changed

+109
-30
lines changed

7 files changed

+109
-30
lines changed

global.d.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,18 @@ declare global {
1313
} & P &
1414
PropsWithChildren
1515
>
16+
17+
// TODO should remove in next TypeScript version
18+
interface Document {
19+
startViewTransition(callback?: () => void | Promise<void>): ViewTransition
20+
}
21+
22+
interface ViewTransition {
23+
finished: Promise<void>
24+
ready: Promise<void>
25+
updateCallbackDone: () => void
26+
skipTransition(): void
27+
}
1628
}
1729

18-
1930
export {}

next.config.mts

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ const isProd = process.env.NODE_ENV === 'production'
1717
let nextConfig: NextConfig = {
1818
experimental: {
1919
appDir: true,
20+
serverComponentsExternalPackages: ['socket.io-client', 'ws'],
2021
},
22+
2123
webpack: (config, options) => {
2224
if (
2325
process.env.SENTRY === 'true' &&
@@ -26,10 +28,6 @@ let nextConfig: NextConfig = {
2628
) {
2729
config.plugins.push(
2830
sentryWebpackPlugin({
29-
include: '.next',
30-
ignore: ['node_modules', 'cypress', 'test'],
31-
urlPrefix: '~/_next',
32-
3331
org: 'inneis-site',
3432
headers: {
3533
Authorization: `DSN ${process.env.NEXT_PUBLIC_SENTRY_DSN}`,

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@tanstack/react-query": "4.29.12",
5353
"@tanstack/react-query-devtools": "4.29.12",
5454
"@tanstack/react-query-persist-client": "4.29.12",
55+
"@uidotdev/usehooks": "2.0.1",
5556
"axios": "1.4.0",
5657
"clsx": "1.2.1",
5758
"daisyui": "3.1.0",

pnpm-lock.yaml

+15-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ui/theme-switcher/ThemeSwitcher.tsx

+63-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
'use client'
22

3+
import { useCallback } from 'react'
4+
import { atom } from 'jotai'
35
import { useTheme } from 'next-themes'
46
import { tv } from 'tailwind-variants'
57

68
import { useIsClient } from '~/hooks/common/use-is-client'
9+
import { isUndefined } from '~/lib/_'
10+
import { jotaiStore } from '~/lib/store'
711

812
const styles = tv({
913
base: 'rounded-inherit inline-flex h-[32px] w-[32px] items-center justify-center border-0 text-current',
@@ -82,9 +86,17 @@ const DarkIcon = () => {
8286
</svg>
8387
)
8488
}
89+
90+
const mousePositionAtom = atom({ x: 0, y: 0 })
8591
export const ThemeSwitcher = () => {
92+
const handleClient: React.MouseEventHandler = useCallback((e) => {
93+
jotaiStore.set(mousePositionAtom, {
94+
x: e.clientX,
95+
y: e.clientY,
96+
})
97+
}, [])
8698
return (
87-
<div className="relative inline-block">
99+
<div className="relative inline-block" onClick={handleClient}>
88100
<ButtonGroup />
89101
<ThemeIndicator />
90102
</div>
@@ -95,6 +107,7 @@ const ThemeIndicator = () => {
95107
const { theme } = useTheme()
96108

97109
const isClient = useIsClient()
110+
98111
if (!isClient) return null
99112
if (!theme) return null
100113
return (
@@ -109,6 +122,53 @@ const ThemeIndicator = () => {
109122

110123
const ButtonGroup = () => {
111124
const { setTheme } = useTheme()
125+
126+
const buildThemeTransition = (theme: 'light' | 'dark') => {
127+
if (
128+
!('startViewTransition' in document) ||
129+
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
130+
) {
131+
setTheme(theme)
132+
return
133+
}
134+
135+
const $document = document.documentElement
136+
137+
const mousePosition = jotaiStore.get(mousePositionAtom)
138+
const { x, y } = mousePosition
139+
140+
if (isUndefined(x) && isUndefined(y)) return
141+
142+
const endRadius = Math.hypot(
143+
Math.max(x, window.innerWidth - x),
144+
Math.max(y, window.innerHeight - y),
145+
)
146+
147+
document
148+
.startViewTransition(() => {
149+
setTheme(theme)
150+
return Promise.resolve()
151+
})
152+
?.ready.then(() => {
153+
if (mousePosition.x === 0) return
154+
const clipPath = [
155+
`circle(0px at ${x}px ${y}px)`,
156+
`circle(${endRadius}px at ${x}px ${y}px)`,
157+
]
158+
159+
$document.animate(
160+
{
161+
clipPath,
162+
},
163+
{
164+
duration: 300,
165+
easing: 'ease-in',
166+
pseudoElement: '::view-transition-new(root)',
167+
},
168+
)
169+
})
170+
}
171+
112172
return (
113173
<div
114174
role="radiogroup"
@@ -123,7 +183,7 @@ const ButtonGroup = () => {
123183
type="button"
124184
className={styles({})}
125185
onClick={() => {
126-
setTheme('light')
186+
buildThemeTransition('light')
127187
}}
128188
>
129189
<SunIcon />
@@ -151,7 +211,7 @@ const ButtonGroup = () => {
151211
role="radio"
152212
type="button"
153213
onClick={() => {
154-
setTheme('dark')
214+
buildThemeTransition('dark')
155215
}}
156216
>
157217
<DarkIcon />

src/lib/_.ts

+3
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,6 @@ export const throttle = <F extends (...args: any[]) => any>(
5555
}
5656
}
5757
}
58+
59+
export const isUndefined = (val: any): val is undefined =>
60+
typeof val === 'undefined'

src/styles/variables.css

+13-21
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,27 @@ html {
1616

1717
::selection {
1818
background-color: var(--theme-color);
19-
color: theme(colors.neutral-content);
20-
}
21-
22-
html.dark {
23-
--border-color: #333;
24-
}
25-
26-
@media (prefers-color-scheme: dark) {
27-
html.not(.light) {
28-
--border-color: #333;
29-
}
30-
}
31-
32-
@media (prefers-color-scheme: light) {
33-
html.not(.dark) {
34-
--border-color: #eee;
35-
}
19+
color: theme(colors.always.white);
3620
}
3721

3822
::view-transition-old(root),
3923
::view-transition-new(root) {
4024
animation: none;
4125
mix-blend-mode: normal;
4226
}
43-
44-
.dark::view-transition-old(root) {
27+
::view-transition-old(root) {
4528
z-index: 9999;
4629
}
47-
48-
.dark::view-transition-new(root) {
30+
::view-transition-new(root) {
31+
z-index: 1;
32+
}
33+
[data-theme='dark']::view-transition-old(root) {
4934
z-index: 1;
5035
}
36+
[data-theme='dark']::view-transition-new(root) {
37+
z-index: 9999;
38+
}
39+
40+
[data-theme='light']::view-transition-new(root) {
41+
z-index: 9999;
42+
}

0 commit comments

Comments
 (0)