feat(ui): drop animation

This commit is contained in:
Ou 2024-10-18 01:29:25 +08:00
parent 115f4994bd
commit 46ac93c570
3 changed files with 76 additions and 67 deletions

View File

@ -41,7 +41,8 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
<div <div
ref={ref} ref={ref}
className={clsx( className={clsx(
"flex flex-col h-500px rounded-2xl bg-op-40 p-4 backdrop-blur-5", "flex flex-col h-500px rounded-2xl p-4",
"bg-op-40 backdrop-blur-5 transition-opacity-300",
isDragged && "op-50", isDragged && "op-50",
`bg-${sources[id].color}`, `bg-${sources[id].color}`,
)} )}
@ -56,44 +57,6 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
) )
}) })
export function CardOverlay({ id }: { id: SourceID }) {
return (
<div className={clsx(
"flex flex-col h-500px rounded-2xl p-4",
"backdrop-blur-5 bg-op-50",
`bg-${sources[id].color}`,
)}
>
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
<div className="flex gap-2 items-center">
<div
className={clsx("w-8 h-8 rounded-full bg-cover")}
style={{
backgroundImage: `url(/icons/${id.split("-")[0]}.png)`,
}}
/>
<span className="flex flex-col">
<span className="flex items-center gap-2">
<span className="text-xl font-bold">
{sources[id].name}
</span>
{sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
</span>
<span className="text-xs"></span>
</span>
</div>
<div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}>
<button
type="button"
className={clsx("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
/>
</div>
</div>
<div className={`h-full p-2 overflow-x-auto bg-base bg-op-50! rounded-2xl sprinkle-${sources[id].color}`} />
</div>
)
}
function NewsCard({ id, inView, handleListeners }: NewsCardProps) { function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom) const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom) const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom)
@ -136,9 +99,9 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}> <div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div <div
className={clsx("w-8 h-8 rounded-full")} className={clsx("w-8 h-8 rounded-full bg-cover")}
style={{ style={{
background: `center / contain no-repeat url(/icons/${id.split("-")[0]}.png)`, backgroundImage: `url(/icons/${id.split("-")[0]}.png)`,
}} }}
/> />
<span className="flex flex-col"> <span className="flex flex-col">
@ -148,7 +111,7 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
</span> </span>
{sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>} {sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
</span> </span>
<span className="text-xs"><UpdateTime query={query} /></span> <span className="text-xs op-70"><UpdateTime query={query} /></span>
</span> </span>
</div> </div>
<div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}> <div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}>
@ -179,8 +142,9 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
options={{ options={{
overflow: { x: "hidden" }, overflow: { x: "hidden" },
}} }}
defer
> >
<div className={clsx("duration-500 transition-opacity", isFreshFetching && "op-20")}> <div className={clsx("transition-opacity-500", isFreshFetching && "op-20")}>
{sources[id].type === "hottest" ? <NewsList query={query} /> : <NewsListTimeLine query={query} />} {sources[id].type === "hottest" ? <NewsList query={query} /> : <NewsListTimeLine query={query} />}
</div> </div>
</OverlayScrollbar> </OverlayScrollbar>

View File

@ -8,6 +8,7 @@ import {
MouseSensor, MouseSensor,
TouchSensor, TouchSensor,
closestCenter, closestCenter,
defaultDropAnimationSideEffects,
useSensor, useSensor,
useSensors, useSensors,
} from "@dnd-kit/core" } from "@dnd-kit/core"
@ -17,8 +18,10 @@ import { useAtom } from "jotai"
import type { SourceID } from "@shared/types" import type { SourceID } from "@shared/types"
import { CSS } from "@dnd-kit/utilities" import { CSS } from "@dnd-kit/utilities"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { sources } from "@shared/sources"
import clsx from "clsx"
import type { ItemsProps } from "./card" import type { ItemsProps } from "./card"
import { CardOverlay, CardWrapper } from "./card" import { CardWrapper } from "./card"
import { currentSourcesAtom } from "~/atoms" import { currentSourcesAtom } from "~/atoms"
export function Dnd() { export function Dnd() {
@ -35,8 +38,8 @@ export function Dnd() {
variants={{ variants={{
visible: { visible: {
transition: { transition: {
delayChildren: 0.3, delayChildren: 0.2,
staggerChildren: 0.2, staggerChildren: 0.1,
}, },
}, },
}} }}
@ -65,13 +68,7 @@ interface DndProps {
setItems: (update: SourceID[]) => void setItems: (update: SourceID[]) => void
} }
const measuringConfig = { function DndWrapper({ items, setItems, children }: PropsWithChildren<DndProps>) {
droppable: {
strategy: MeasuringStrategy.Always,
},
}
export function DndWrapper({ items, setItems, children }: PropsWithChildren<DndProps>) {
const [activeId, setActiveId] = useState<string | null>(null) const [activeId, setActiveId] = useState<string | null>(null)
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor)) const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
@ -97,7 +94,11 @@ export function DndWrapper({ items, setItems, children }: PropsWithChildren<DndP
return ( return (
<DndContext <DndContext
sensors={sensors} sensors={sensors}
measuring={measuringConfig} measuring={{
droppable: {
strategy: MeasuringStrategy.Always,
},
}}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
@ -107,19 +108,61 @@ export function DndWrapper({ items, setItems, children }: PropsWithChildren<DndP
{children} {children}
</SortableContext> </SortableContext>
<DragOverlay <DragOverlay
adjustScale className="transition-opacity-300"
dropAnimation={{ dropAnimation={{
duration: 300,
easing: "cubic-bezier(0.25, 1, 0.5, 1)", easing: "cubic-bezier(0.25, 1, 0.5, 1)",
duration: 300,
sideEffects: defaultDropAnimationSideEffects({
className: {
active: "op-100",
dragOverlay: "op-0",
},
}),
}} }}
> >
{/* {!!activeId && <CardOverlay id={activeId as SourceID} />} */} {!!activeId && <CardOverlay id={activeId as SourceID} />}
<CardOverlay id={activeId as SourceID} />
</DragOverlay> </DragOverlay>
</DndContext> </DndContext>
) )
} }
function CardOverlay({ id }: { id: SourceID }) {
return (
<div className={clsx(
"flex flex-col rounded-2xl p-4",
"backdrop-blur-5 bg-op-40",
`bg-${sources[id].color}`,
)}
>
<div className={clsx("flex justify-between mx-2 items-center")}>
<div className="flex gap-2 items-center">
<div
className={clsx("w-8 h-8 rounded-full bg-cover")}
style={{
backgroundImage: `url(/icons/${id.split("-")[0]}.png)`,
}}
/>
<span className="flex flex-col">
<span className="flex items-center gap-2">
<span className="text-xl font-bold">
{sources[id].name}
</span>
{sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
</span>
<span className="text-xs op-70"></span>
</span>
</div>
<div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}>
<button
type="button"
className={clsx("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
/>
</div>
</div>
</div>
)
}
const animateLayoutChanges: AnimateLayoutChanges = (args) => { const animateLayoutChanges: AnimateLayoutChanges = (args) => {
const { isSorting, wasDragging } = args const { isSorting, wasDragging } = args
if (isSorting || wasDragging) { if (isSorting || wasDragging) {

View File

@ -16,7 +16,7 @@ const defaultScrollbarParams: UseOverlayScrollbarsParams = {
defer: true, defer: true,
} }
export function OverlayScrollbar({ children, options, events, defer, ...props }: PropsWithChildren<Props>) { export function OverlayScrollbar({ disabled, children, options, events, defer, ...props }: PropsWithChildren<Props>) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const scrollbarParams = useMemo(() => defu<UseOverlayScrollbarsParams, Array<UseOverlayScrollbarsParams> >({ const scrollbarParams = useMemo(() => defu<UseOverlayScrollbarsParams, Array<UseOverlayScrollbarsParams> >({
options, options,
@ -27,6 +27,7 @@ export function OverlayScrollbar({ children, options, events, defer, ...props }:
const [initialize] = useOverlayScrollbars(scrollbarParams) const [initialize] = useOverlayScrollbars(scrollbarParams)
useEffect(() => { useEffect(() => {
if (!disabled) {
initialize({ initialize({
target: ref.current!, target: ref.current!,
cancel: { cancel: {
@ -34,7 +35,8 @@ export function OverlayScrollbar({ children, options, events, defer, ...props }:
nativeScrollbarsOverlaid: true, nativeScrollbarsOverlaid: true,
}, },
}) })
}, [initialize]) }
}, [initialize, disabled])
return ( return (
<div ref={ref} {...props}> <div ref={ref} {...props}>
@ -75,7 +77,7 @@ export function GlobalOverlayScrollbar({ children, ...props }: PropsWithChildren
events: { events: {
scroll: (_, e) => onScroll(e), scroll: (_, e) => onScroll(e),
}, },
defer: true, defer: false,
}) })
useEffect(() => { useEffect(() => {