-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
78b4e2a
commit 35af35e
Showing
17 changed files
with
713 additions
and
140 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
--- | ||
import type { HTMLAttributes } from 'astro/types' | ||
import { twMerge } from 'tailwind-merge' | ||
interface Props extends HTMLAttributes<'button'> { | ||
variant?: 'primary' | 'secondary' | 'tertiary' | ||
} | ||
const { variant } = Astro.props | ||
const variantClasses = { | ||
primary: 'bg-primary/50', | ||
secondary: 'bg-neutral-700/50 ', | ||
tertiary: 'bg-neutral-800', | ||
} as const | ||
--- | ||
|
||
<button | ||
class={twMerge( | ||
'disabled:opacity-80 disabled:cursor-not-allowed shine flex w-fit items-center rounded-lg px-3 py-1.5 text-sm text-neutral-200 active:scale-95 hover:enabled:brightness-110 transition-all', | ||
variantClasses[variant || 'primary'] | ||
)} | ||
type='button' | ||
> | ||
<slot /> | ||
</button> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import { | ||
cloneElement, | ||
createContext, | ||
useContext, | ||
useEffect, | ||
useRef, | ||
useState, | ||
} from 'react' | ||
import homeIcon from '../../assets/home.svg?raw' | ||
import devtoolIcon from '../../assets/devtool.svg?raw' | ||
import { | ||
AnimatePresence, | ||
motion, | ||
useMotionValueEvent, | ||
useSpring, | ||
useTransform, | ||
} from 'motion/react' | ||
import { MouseProvider, useMouse } from './MouseProvider' | ||
import { Tooltip } from '../Tooltip' | ||
|
||
const DockItem = ({ | ||
icon, | ||
href, | ||
index, | ||
total, | ||
title, | ||
}: { | ||
icon: React.ReactElement | ||
href: string | ||
index: number | ||
total: number | ||
title: string | ||
}) => { | ||
const mouse = useMouse() | ||
|
||
const dock = useContext(DockContext) | ||
if (!mouse) { | ||
throw new Error('MouseProvider not found') | ||
} | ||
|
||
const dimension = useTransform(mouse?.position.x, (mouseX) => { | ||
if (!dock?.isHovered) { | ||
return 48 | ||
} | ||
|
||
const rangeStart = dock?.dock?.getBoundingClientRect()?.left ?? 0 | ||
const dockWidth = dock?.dock?.clientWidth ?? 1 | ||
|
||
// 0-1 range | ||
const normalizedMouseX = (mouseX - rangeStart) / dockWidth | ||
|
||
// Calculate normalized position of the current item's center | ||
const itemCenterX = (index + 0.5) * (1 / total) | ||
|
||
// Calculate the difference between mouse position and item center | ||
const distanceFromCenter = Math.abs(normalizedMouseX - itemCenterX) | ||
|
||
// Maximum extra scaling amount | ||
const scalingMagnitude = 0.6 | ||
// How quickly the scale decreases according to distance | ||
const distanceSensitivity = 3 | ||
|
||
const scaleFactor = Math.max( | ||
1, | ||
1 + scalingMagnitude * (1 - distanceFromCenter * distanceSensitivity) | ||
) | ||
|
||
// Apply scale factor to the base size (48) | ||
return 48 * scaleFactor | ||
}) | ||
|
||
const spring = useSpring(48, { | ||
damping: 10, | ||
stiffness: 150, | ||
mass: 0.01, | ||
}) | ||
|
||
console.log('1111', dimension.get()) | ||
|
||
useMotionValueEvent(dimension, 'change', (latest) => { | ||
if (dock?.isHovered) { | ||
spring.set(latest) | ||
} else { | ||
spring.set(48) | ||
} | ||
}) | ||
|
||
return ( | ||
<motion.a | ||
className='relative flex items-center justify-center rounded-lg text-stone-400 shadow-[0_-1px_hsl(0_0%_0%/0.5)_inset,0_2px_4px_hsl(0_0%_0%_/_0.5),0_1px_hsl(0_0%_100%/0.5)_inset] transition-colors [background:linear-gradient(hsl(0_0%_100%/0.15),#0000),hsl(0_0%_4%)] hover:text-stone-300' | ||
href={href} | ||
style={{ | ||
width: spring, | ||
height: spring, | ||
}} | ||
> | ||
<Tooltip text={title}> | ||
{cloneElement(icon, { | ||
className: 'transition-colors size-3/5', | ||
})} | ||
</Tooltip> | ||
</motion.a> | ||
) | ||
} | ||
|
||
type DockContext = { | ||
isHovered: boolean | ||
dock: HTMLDivElement | null | ||
} | ||
|
||
const DockContext = createContext<DockContext | null>(null) | ||
|
||
const dockItems = [ | ||
{ | ||
icon: <span dangerouslySetInnerHTML={{ __html: homeIcon }} />, | ||
href: '/', | ||
title: 'Home', | ||
}, | ||
{ | ||
icon: <span dangerouslySetInnerHTML={{ __html: devtoolIcon }} />, | ||
href: '/', | ||
title: 'Projects', | ||
}, | ||
] | ||
|
||
export const Dock = () => { | ||
const ref = useRef<HTMLDivElement | null>(null) | ||
|
||
const [isHovered, setIsHovered] = useState(false) | ||
|
||
const [dock, setDock] = useState<HTMLDivElement | null>(null) | ||
|
||
useEffect(() => { | ||
setDock(ref.current) | ||
}, []) | ||
|
||
return ( | ||
<MouseProvider> | ||
<DockContext.Provider value={{ isHovered, dock }}> | ||
<footer className='fixed -bottom-3 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2'> | ||
<motion.div | ||
className='box-content flex h-12 items-end justify-center gap-4 rounded-xl border border-solid border-zinc-800 bg-neutral-800/60 px-4 py-1.5 backdrop-blur-sm' | ||
onHoverStart={() => { | ||
setIsHovered(true) | ||
}} | ||
onHoverEnd={() => { | ||
setIsHovered(false) | ||
}} | ||
ref={ref} | ||
> | ||
{dockItems.map((item, index) => ( | ||
<DockItem | ||
key={index} | ||
{...item} | ||
title={item.title} | ||
index={index} | ||
total={dockItems.length} | ||
/> | ||
))} | ||
</motion.div> | ||
</footer> | ||
</DockContext.Provider> | ||
</MouseProvider> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { type MotionValue, useMotionValue, useVelocity } from 'motion/react' | ||
import { createContext, useContext, useMemo, type ReactNode } from 'react' | ||
import { useEvent } from 'react-use' | ||
|
||
type MouseType = { | ||
position: { | ||
x: MotionValue<number> | ||
y: MotionValue<number> | ||
} | ||
velocity: { | ||
x: MotionValue<number> | ||
y: MotionValue<number> | ||
} | ||
} | ||
|
||
const useMousePosition = () => { | ||
const x = useMotionValue(0) | ||
const y = useMotionValue(0) | ||
|
||
useEvent('mousemove', (e) => { | ||
x.set(e.clientX) | ||
y.set(e.clientY) | ||
}) | ||
|
||
return useMemo(() => ({ x, y }), [x, y]) | ||
} | ||
|
||
const MouseContext = createContext<MouseType | null>(null) | ||
|
||
export const useMouse = () => { | ||
if (!MouseContext) { | ||
throw new Error('useMouse must be used within a MouseProvider') | ||
} | ||
|
||
return useContext(MouseContext) | ||
} | ||
|
||
export const MouseProvider = ({ children }: { children: ReactNode }) => { | ||
const { x, y } = useMousePosition() | ||
const velocityX = useVelocity(x) | ||
const velocityY = useVelocity(y) | ||
|
||
const mouse = useMemo( | ||
() => ({ | ||
position: { x, y }, | ||
velocity: { x: velocityX, y: velocityY }, | ||
}), | ||
[x, y, velocityX, velocityY] | ||
) | ||
|
||
return <MouseContext.Provider value={mouse}>{children}</MouseContext.Provider> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
--- | ||
import type { HTMLAttributes } from 'astro/types' | ||
import { twMerge } from 'tailwind-merge' | ||
interface Props extends HTMLAttributes<'button'> { | ||
variant?: 'primary' | 'secondary' | 'tertiary' | ||
} | ||
const { variant } = Astro.props | ||
const variantClasses = { | ||
primary: 'bg-primary/50', | ||
secondary: 'bg-neutral-700/50 ', | ||
tertiary: 'bg-neutral-800', | ||
} as const | ||
--- | ||
|
||
<div></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.