mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
Both old names are owned by a different npm account (artemis-prime), so publishing them returns 403 — which is how this surfaced: the 8.0.0 run shipped @hanzogui/logo and @hanzogui/demos depending on [email protected], a version that cannot be published and therefore does not exist. Those two were uninstallable. @hanzo/gui and @hanzogui/* are ours, so the framework now carries the name it is imported by everywhere downstream, and the loader moves under the scope we control. The rewrite is limited to package references. `DEBUG?.startsWith('hanzogui')` is a log-namespace filter and join(dir, 'hanzogui') is a path — rewriting either would change behaviour, so both are untouched. Co-authored-by: Hanzo Dev <[email protected]>
155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
import { createContext, useContext, useEffect, useState } from 'react'
|
|
import type { GuiElement, ViewProps } from '@hanzo/gui'
|
|
import { isWeb, View, withStaticProperties } from '@hanzo/gui'
|
|
|
|
// 3D Card Context - shared hover state between container and items
|
|
const MouseEnterContext = createContext<
|
|
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
|
|
>(undefined)
|
|
|
|
export const useMouseEnter = () => {
|
|
const context = useContext(MouseEnterContext)
|
|
if (context === undefined) {
|
|
if (!isWeb) {
|
|
return [false, () => {}] as [boolean, React.Dispatch<React.SetStateAction<boolean>>]
|
|
}
|
|
throw new Error('useMouseEnter must be used within a Card3D')
|
|
}
|
|
return context
|
|
}
|
|
|
|
export type Card3DContainerProps = ViewProps & {
|
|
/** Custom class name for the container */
|
|
className?: string
|
|
/** Tilt intensity - higher = less tilt. Default: 10 */
|
|
tiltDivisor?: number
|
|
/** Center content in container */
|
|
centered?: boolean
|
|
}
|
|
|
|
const Card3DContainer = ({
|
|
children,
|
|
className,
|
|
tiltDivisor = 10,
|
|
centered = false,
|
|
...rest
|
|
}: Card3DContainerProps) => {
|
|
const [_containerRef, setContainerRef] = useState<GuiElement | null>(null)
|
|
const [perspectiveRef, setPerspectiveRef] = useState<GuiElement | null>(null)
|
|
const [isMouseEntered, setIsMouseEntered] = useState(false)
|
|
|
|
const containerRef = _containerRef as unknown as HTMLDivElement
|
|
|
|
useEffect(() => {
|
|
if (perspectiveRef && isWeb) {
|
|
;(perspectiveRef as unknown as HTMLDivElement).style.perspective = '800px'
|
|
}
|
|
}, [perspectiveRef])
|
|
|
|
useEffect(() => {
|
|
if (containerRef && isWeb) {
|
|
containerRef.style.transformStyle = 'preserve-3d'
|
|
containerRef.style.transition = 'transform 100ms ease-out'
|
|
}
|
|
}, [containerRef])
|
|
|
|
let handleMouseMove: (e: React.MouseEvent<HTMLDivElement>) => void
|
|
let handleMouseEnter: () => void
|
|
let handleMouseLeave: () => void
|
|
|
|
if (isWeb) {
|
|
handleMouseMove = (e) => {
|
|
if (!containerRef) return
|
|
const { left, top, width, height } = containerRef.getBoundingClientRect()
|
|
const x = (e.clientX - left - width / 2) / tiltDivisor
|
|
const y = (e.clientY - top - height / 2) / tiltDivisor
|
|
containerRef.style.transform = `rotateY(${x}deg) rotateX(${-y}deg)`
|
|
}
|
|
|
|
handleMouseEnter = () => {
|
|
setIsMouseEntered(true)
|
|
}
|
|
|
|
handleMouseLeave = () => {
|
|
if (!containerRef) return
|
|
setIsMouseEntered(false)
|
|
containerRef.style.transform = 'rotateY(0deg) rotateX(0deg)'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
|
|
<View
|
|
ref={(ref) => setPerspectiveRef(ref)}
|
|
className={className}
|
|
{...(centered && {
|
|
justify: 'center',
|
|
items: 'center',
|
|
})}
|
|
{...rest}
|
|
>
|
|
<View
|
|
ref={(ref) => setContainerRef(ref)}
|
|
{...(centered && {
|
|
justify: 'center',
|
|
items: 'center',
|
|
})}
|
|
{...(isWeb && {
|
|
onMouseEnter: handleMouseEnter!,
|
|
onMouseMove: handleMouseMove!,
|
|
onMouseLeave: handleMouseLeave!,
|
|
})}
|
|
>
|
|
{children}
|
|
</View>
|
|
</View>
|
|
</MouseEnterContext.Provider>
|
|
)
|
|
}
|
|
|
|
export type Card3DItemProps = {
|
|
/** Z-axis translation on hover in pixels */
|
|
translateZ?: number
|
|
}
|
|
|
|
const Card3DItem = View.styleable<Card3DItemProps>(
|
|
({ translateZ = 0, children, ...rest }, forwardedRef) => {
|
|
const [ref, setRef] = useState<GuiElement | null>(null)
|
|
const [isMouseEntered] = useMouseEnter()
|
|
|
|
useEffect(() => {
|
|
if (isWeb && ref) {
|
|
;(ref as HTMLElement).style.transition = 'transform 200ms ease-out'
|
|
}
|
|
}, [ref])
|
|
|
|
useEffect(() => {
|
|
if (!isWeb || !ref) return
|
|
if (isMouseEntered) {
|
|
;(ref as HTMLElement).style.transform = `translateZ(${translateZ}px)`
|
|
} else {
|
|
;(ref as HTMLElement).style.transform = 'translateZ(0px)'
|
|
}
|
|
}, [isMouseEntered, translateZ, ref])
|
|
|
|
return (
|
|
<View
|
|
ref={(r) => {
|
|
setRef(r)
|
|
if (typeof forwardedRef === 'function') forwardedRef(r)
|
|
else if (forwardedRef) forwardedRef.current = r
|
|
}}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</View>
|
|
)
|
|
}
|
|
)
|
|
|
|
export const Card3D = withStaticProperties(Card3DContainer, {
|
|
Item: Card3DItem,
|
|
})
|
|
|
|
export { MouseEnterContext }
|