Skip to content

Commit 5831d55

Browse files
committed
feat: add mark timeline transition
Signed-off-by: Innei <[email protected]>
1 parent 211f951 commit 5831d55

File tree

7 files changed

+79
-68
lines changed

7 files changed

+79
-68
lines changed

src/components/ui/input/Input.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const Input = forwardRef<
2222
'bg-base-100 px-3 py-[calc(theme(spacing.2)-1px)] placeholder:text-zinc-400 focus:outline-none focus:ring-2',
2323
'border-zinc-900/10 dark:border-zinc-700',
2424
'focus:border-accent-focus dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500',
25+
props.type === 'password' ? 'font-mono' : 'font-[system-ui]',
2526
className,
2627
)}
2728
{...props}

src/components/ui/markdown/index.demo.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const MarkdownCustomize: DocumentComponent = () => {
1212
return (
1313
<QueryClientProvider client={queryClient}>
1414
<ThemeProvider>
15-
<main className="relative m-auto mt-6 max-w-[800px] border border-accent/10">
15+
<main className="relative m-auto mt-6 max-w-[800px]">
1616
<Markdown
1717
extendsRules={{
1818
codeBlock: {

src/components/ui/markdown/markdown.module.css

+37
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,41 @@
108108
p {
109109
@apply break-words;
110110
}
111+
112+
mark {
113+
--lightness: 80%;
114+
--highlighted: 1;
115+
--highlight: hsl(var(--inc) / var(--lightness));
116+
background: transparent;
117+
}
118+
119+
@supports (animation-timeline: view()) {
120+
mark {
121+
--highlighted: 0;
122+
animation: highlight steps(1) both;
123+
animation-timeline: view();
124+
animation-range: entry 100% cover 10%;
125+
}
126+
}
127+
128+
[data-theme='dark'] mark {
129+
--lightness: 35%;
130+
}
131+
132+
mark span {
133+
background: linear-gradient(
134+
120deg,
135+
var(--highlight, lightblue) 50%,
136+
transparent 50%
137+
)
138+
110% 0 / 200% 100% no-repeat;
139+
background-position: calc((1 - var(--highlighted)) * 110%) 0;
140+
transition: background-position 1s;
141+
}
142+
}
143+
144+
@keyframes highlight {
145+
to {
146+
--highlighted: 1;
147+
}
111148
}

src/components/ui/markdown/parsers/mark.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const MarkRule: MarkdownToJSX.Rule = {
1717
key={state?.key}
1818
className="rounded-md bg-always-yellow-400 bg-opacity-80 px-1 text-black"
1919
>
20-
{output(node.content, state!)}
20+
<span>{output(node.content, state!)}</span>
2121
</mark>
2222
)
2323
},

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

+5-54
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
'use client'
22

3-
import { useCallback } from 'react'
43
import { flushSync } from 'react-dom'
5-
import { atom } from 'jotai'
64
import { useTheme } from 'next-themes'
75
import { tv } from 'tailwind-variants'
86

97
import { useIsClient } from '~/hooks/common/use-is-client'
10-
import { isUndefined } from '~/lib/_'
11-
import { jotaiStore } from '~/lib/store'
8+
import { transitionViewIfSupported } from '~/lib/dom'
129

1310
const styles = tv({
1411
base: 'rounded-inherit inline-flex h-[32px] w-[32px] items-center justify-center border-0 text-current',
@@ -88,16 +85,9 @@ const DarkIcon = () => {
8885
)
8986
}
9087

91-
const mousePositionAtom = atom({ x: 0, y: 0 })
9288
export const ThemeSwitcher = () => {
93-
const handleClient: React.MouseEventHandler = useCallback((e) => {
94-
jotaiStore.set(mousePositionAtom, {
95-
x: e.clientX,
96-
y: e.clientY,
97-
})
98-
}, [])
9989
return (
100-
<div className="relative inline-block" onClick={handleClient}>
90+
<div className="relative inline-block">
10191
<ThemeIndicator />
10292
<ButtonGroup />
10393
</div>
@@ -125,48 +115,9 @@ const ButtonGroup = () => {
125115
const { setTheme } = useTheme()
126116

127117
const buildThemeTransition = (theme: 'light' | 'dark' | 'system') => {
128-
if (
129-
!('startViewTransition' in document) ||
130-
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
131-
) {
132-
setTheme(theme)
133-
return
134-
}
135-
136-
const $document = document.documentElement
137-
138-
const mousePosition = jotaiStore.get(mousePositionAtom)
139-
const { x, y } = mousePosition
140-
141-
if (isUndefined(x) && isUndefined(y)) return
142-
143-
const endRadius = Math.hypot(
144-
Math.max(x, window.innerWidth - x),
145-
Math.max(y, window.innerHeight - y),
146-
)
147-
148-
document
149-
.startViewTransition(() => {
150-
flushSync(() => setTheme(theme))
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-
})
118+
transitionViewIfSupported(() => {
119+
flushSync(() => setTheme(theme))
120+
})
170121
}
171122

172123
return (

src/lib/dom.ts

+12
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@ export const stopPropagation: ReactEventHandler<any> = (e) =>
44
e.stopPropagation()
55

66
export const preventDefault: ReactEventHandler<any> = (e) => e.preventDefault()
7+
8+
export const transitionViewIfSupported = (updateCb: () => any) => {
9+
if (window.matchMedia(`(prefers-reduced-motion: reduce)`).matches) {
10+
updateCb()
11+
return
12+
}
13+
if (document.startViewTransition) {
14+
document.startViewTransition(updateCb)
15+
} else {
16+
updateCb()
17+
}
18+
}

src/styles/variables.css

+22-12
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,34 @@ html {
3434
color: theme(colors.always.neutral.800);
3535
}
3636

37-
::view-transition-old(root),
3837
::view-transition-new(root) {
39-
animation: none;
40-
mix-blend-mode: normal;
38+
animation: turnOff 800ms ease-in-out;
4139
}
4240
::view-transition-old(root) {
43-
z-index: 9999;
44-
}
45-
::view-transition-new(root) {
46-
z-index: 1;
41+
animation: none;
4742
}
48-
[data-theme='dark']::view-transition-old(root) {
49-
z-index: 1;
43+
44+
@keyframes turnOn {
45+
0% {
46+
clip-path: polygon(0% 0%, 100% 0, 100% 0, 0 0);
47+
}
48+
100% {
49+
clip-path: polygon(0% 0%, 100% 0, 100% 100%, 0 100%);
50+
}
5051
}
52+
5153
[data-theme='dark']::view-transition-new(root) {
52-
z-index: 9999;
54+
animation: turnOn 800ms ease-in-out;
55+
}
56+
::view-transition-old(root) {
57+
animation: none;
5358
}
5459

55-
[data-theme='light']::view-transition-new(root) {
56-
z-index: 9999;
60+
@keyframes turnOff {
61+
0% {
62+
clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0% 100%);
63+
}
64+
100% {
65+
clip-path: polygon(0 100%, 100% 100%, 100% 0, 0 0);
66+
}
5767
}

0 commit comments

Comments
 (0)