mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Merge pull request #29 from sanjanamilind14/feat/chroma-grid
[ADDED: Chroma Grid]
This commit is contained in:
@@ -68,7 +68,8 @@ export const CATEGORIES = [
|
||||
'Decay Card',
|
||||
'Flowing Menu',
|
||||
'Elastic Slider',
|
||||
'Stack'
|
||||
'Stack',
|
||||
'Chroma Grid'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,6 +57,7 @@ const components = {
|
||||
'elastic-slider': () => import('../demo/Components/ElasticSliderDemo.vue'),
|
||||
'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue'),
|
||||
'stack': () => import('../demo/Components/StackDemo.vue'),
|
||||
'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'),
|
||||
};
|
||||
|
||||
const backgrounds = {
|
||||
|
||||
44
src/constants/code/Components/chromaGridCode.ts
Normal file
44
src/constants/code/Components/chromaGridCode.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import code from '@content/Components/ChromaGrid/ChromaGrid.vue?raw';
|
||||
import type { CodeObject } from '../../../types/code';
|
||||
|
||||
export const chromaGrid: CodeObject = {
|
||||
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/ChromaGrid`,
|
||||
installation: `npm install gsap`,
|
||||
usage: `<template>
|
||||
<div style="height: 600px; position: relative">
|
||||
<ChromaGrid
|
||||
:items="items"
|
||||
:radius="300"
|
||||
:damping="0.45"
|
||||
:fadeOut="0.6"
|
||||
ease="power3.out"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChromaGrid from "./ChromaGrid.vue";
|
||||
|
||||
const items = [
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=1",
|
||||
title: "Sarah Johnson",
|
||||
subtitle: "Frontend Developer",
|
||||
handle: "@sarahjohnson",
|
||||
borderColor: "#3B82F6",
|
||||
gradient: "linear-gradient(145deg, #3B82F6, #000)",
|
||||
url: "https://github.com/sarahjohnson"
|
||||
},
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=2",
|
||||
title: "Mike Chen",
|
||||
subtitle: "Backend Engineer",
|
||||
handle: "@mikechen",
|
||||
borderColor: "#10B981",
|
||||
gradient: "linear-gradient(180deg, #10B981, #000)",
|
||||
url: "https://linkedin.com/in/mikechen"
|
||||
}
|
||||
]
|
||||
</script>`,
|
||||
code
|
||||
};
|
||||
225
src/content/Components/ChromaGrid/ChromaGrid.vue
Normal file
225
src/content/Components/ChromaGrid/ChromaGrid.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import gsap from 'gsap'
|
||||
|
||||
interface CardItem {
|
||||
image: string
|
||||
title: string
|
||||
subtitle: string
|
||||
handle?: string
|
||||
borderColor?: string
|
||||
gradient?: string
|
||||
url?: string
|
||||
location?: string
|
||||
}
|
||||
|
||||
interface GridMotionProps {
|
||||
items?: CardItem[]
|
||||
className?: string
|
||||
radius?: number
|
||||
damping?: number
|
||||
fadeOut?: number
|
||||
ease?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<GridMotionProps>(), {
|
||||
items: () => [],
|
||||
className: '',
|
||||
radius: 300,
|
||||
damping: 0.45,
|
||||
fadeOut: 0.6,
|
||||
ease: 'power3.out',
|
||||
})
|
||||
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const fadeRef = ref<HTMLElement | null>(null)
|
||||
const setX = ref<any>(null)
|
||||
const setY = ref<any>(null)
|
||||
const pos = ref({ x: 0, y: 0 })
|
||||
|
||||
const demo: CardItem[] = [
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=8",
|
||||
title: "Alex Rivera",
|
||||
subtitle: "Full Stack Developer",
|
||||
handle: "@alexrivera",
|
||||
borderColor: "#4F46E5",
|
||||
gradient: "linear-gradient(145deg,#4F46E5,#000)",
|
||||
url: "https://github.com/",
|
||||
},
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=11",
|
||||
title: "Jordan Chen",
|
||||
subtitle: "DevOps Engineer",
|
||||
handle: "@jordanchen",
|
||||
borderColor: "#10B981",
|
||||
gradient: "linear-gradient(210deg,#10B981,#000)",
|
||||
url: "https://linkedin.com/in/",
|
||||
},
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=3",
|
||||
title: "Morgan Blake",
|
||||
subtitle: "UI/UX Designer",
|
||||
handle: "@morganblake",
|
||||
borderColor: "#F59E0B",
|
||||
gradient: "linear-gradient(165deg,#F59E0B,#000)",
|
||||
url: "https://dribbble.com/",
|
||||
},
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=16",
|
||||
title: "Casey Park",
|
||||
subtitle: "Data Scientist",
|
||||
handle: "@caseypark",
|
||||
borderColor: "#EF4444",
|
||||
gradient: "linear-gradient(195deg,#EF4444,#000)",
|
||||
url: "https://kaggle.com/",
|
||||
},
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=25",
|
||||
title: "Sam Kim",
|
||||
subtitle: "Mobile Developer",
|
||||
handle: "@thesamkim",
|
||||
borderColor: "#8B5CF6",
|
||||
gradient: "linear-gradient(225deg,#8B5CF6,#000)",
|
||||
url: "https://github.com/",
|
||||
},
|
||||
{
|
||||
image: "https://i.pravatar.cc/300?img=60",
|
||||
title: "Tyler Rodriguez",
|
||||
subtitle: "Cloud Architect",
|
||||
handle: "@tylerrod",
|
||||
borderColor: "#06B6D4",
|
||||
gradient: "linear-gradient(135deg,#06B6D4,#000)",
|
||||
url: "https://aws.amazon.com/",
|
||||
},
|
||||
]
|
||||
|
||||
const data = computed(() => (props.items.length ? props.items : demo))
|
||||
|
||||
onMounted(() => {
|
||||
const el = rootRef.value
|
||||
if (!el) return
|
||||
|
||||
setX.value = gsap.quickSetter(el, '--x', 'px')
|
||||
setY.value = gsap.quickSetter(el, '--y', 'px')
|
||||
const { width, height } = el.getBoundingClientRect()
|
||||
pos.value = { x: width / 2, y: height / 2 }
|
||||
setX.value(pos.value.x)
|
||||
setY.value(pos.value.y)
|
||||
})
|
||||
|
||||
const moveTo = (x: number, y: number) => {
|
||||
gsap.to(pos.value, {
|
||||
x,
|
||||
y,
|
||||
duration: props.damping,
|
||||
ease: props.ease,
|
||||
onUpdate: () => {
|
||||
setX.value?.(pos.value.x)
|
||||
setY.value?.(pos.value.y)
|
||||
},
|
||||
overwrite: true,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMove = (e: PointerEvent) => {
|
||||
const r = rootRef.value?.getBoundingClientRect()
|
||||
if (!r) return
|
||||
moveTo(e.clientX - r.left, e.clientY - r.top)
|
||||
gsap.to(fadeRef.value, { opacity: 0, duration: 0.25, overwrite: true })
|
||||
}
|
||||
|
||||
const handleLeave = () => {
|
||||
gsap.to(fadeRef.value, {
|
||||
opacity: 1,
|
||||
duration: props.fadeOut,
|
||||
overwrite: true,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCardClick = (url?: string) => {
|
||||
if (url) window.open(url, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
const handleCardMove = (e: MouseEvent) => {
|
||||
const c = e.currentTarget as HTMLElement
|
||||
const rect = c.getBoundingClientRect()
|
||||
c.style.setProperty('--mouse-x', `${e.clientX - rect.left}px`)
|
||||
c.style.setProperty('--mouse-y', `${e.clientY - rect.top}px`)
|
||||
}
|
||||
|
||||
const spotlightStyle = {
|
||||
backdropFilter: 'grayscale(1) brightness(0.78)',
|
||||
WebkitBackdropFilter: 'grayscale(1) brightness(0.78)',
|
||||
background: 'rgba(0,0,0,0.001)',
|
||||
maskImage:
|
||||
'radial-gradient(circle var(--r) at var(--x) var(--y),transparent 0%,transparent 15%,rgba(0,0,0,0.10) 30%,rgba(0,0,0,0.22)45%,rgba(0,0,0,0.35)60%,rgba(0,0,0,0.50)75%,rgba(0,0,0,0.68)88%,white 100%)',
|
||||
WebkitMaskImage:
|
||||
'radial-gradient(circle var(--r) at var(--x) var(--y),transparent 0%,transparent 15%,rgba(0,0,0,0.10) 30%,rgba(0,0,0,0.22)45%,rgba(0,0,0,0.35)60%,rgba(0,0,0,0.50)75%,rgba(0,0,0,0.68)88%,white 100%)',
|
||||
}
|
||||
|
||||
const fadeStyle = {
|
||||
...spotlightStyle,
|
||||
maskImage:
|
||||
'radial-gradient(circle var(--r) at var(--x) var(--y),white 0%,white 15%,rgba(255,255,255,0.90)30%,rgba(255,255,255,0.78)45%,rgba(255,255,255,0.65)60%,rgba(255,255,255,0.50)75%,rgba(255,255,255,0.32)88%,transparent 100%)',
|
||||
WebkitMaskImage:
|
||||
'radial-gradient(circle var(--r) at var(--x) var(--y),white 0%,white 15%,rgba(255,255,255,0.90)30%,rgba(255,255,255,0.78)45%,rgba(255,255,255,0.65)60%,rgba(255,255,255,0.50)75%,rgba(255,255,255,0.32)88%,transparent 100%)',
|
||||
opacity: 1,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="relative w-full h-full flex flex-wrap justify-center items-start gap-3"
|
||||
:style="{
|
||||
'--r': `${props.radius}px`,
|
||||
'--x': '50%',
|
||||
'--y': '50%',
|
||||
}"
|
||||
@pointermove="handleMove"
|
||||
@pointerleave="handleLeave"
|
||||
>
|
||||
<article
|
||||
v-for="(c, i) in data"
|
||||
:key="i"
|
||||
class="group relative flex flex-col w-[300px] rounded-[20px] overflow-hidden border-2 border-transparent transition-colors duration-300 cursor-pointer"
|
||||
:style="{
|
||||
'--card-border': c.borderColor || 'transparent',
|
||||
background: c.gradient,
|
||||
'--spotlight-color': 'rgba(255,255,255,0.3)',
|
||||
}"
|
||||
@mousemove="handleCardMove"
|
||||
@click="() => handleCardClick(c.url)"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 pointer-events-none transition-opacity duration-500 z-20 opacity-0 group-hover:opacity-100"
|
||||
:style="{
|
||||
background: 'radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 70%)',
|
||||
}"
|
||||
/>
|
||||
<div class="relative z-10 flex-1 p-[10px] box-border">
|
||||
<img
|
||||
:src="c.image"
|
||||
:alt="c.title"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover rounded-[10px]"
|
||||
/>
|
||||
</div>
|
||||
<footer class="relative z-10 p-3 text-white font-sans grid grid-cols-[1fr_auto] gap-x-3 gap-y-1">
|
||||
<h3 class="m-0 text-[1.05rem] font-semibold">{{ c.title }}</h3>
|
||||
<span v-if="c.handle" class="text-[0.95rem] opacity-80 text-right">{{ c.handle }}</span>
|
||||
<p class="m-0 text-[0.85rem] opacity-85">{{ c.subtitle }}</p>
|
||||
<span v-if="c.location" class="text-[0.85rem] opacity-85 text-right">{{ c.location }}</span>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<div class="absolute inset-0 pointer-events-none z-30" :style="spotlightStyle" />
|
||||
<div
|
||||
ref="fadeRef"
|
||||
class="absolute inset-0 pointer-events-none transition-opacity duration-[250ms] z-40"
|
||||
:style="fadeStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
77
src/demo/Components/ChromaGridDemo.vue
Normal file
77
src/demo/Components/ChromaGridDemo.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container relative overflow-hidden py-6">
|
||||
<ChromaGrid />
|
||||
</div>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['gsap']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="chromaGrid" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="chromaGrid.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import Dependencies from '../../components/code/Dependencies.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
|
||||
|
||||
import { chromaGrid } from '../../constants/code/Components/chromaGridCode'
|
||||
import ChromaGrid from '../../content/Components/ChromaGrid/ChromaGrid.vue'
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'items',
|
||||
type: 'Array',
|
||||
default: 'Demo []',
|
||||
description: 'Array of ChromaItem objects to display in the grid',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: "''",
|
||||
description: 'Additional CSS classes to apply to the grid container',
|
||||
},
|
||||
{
|
||||
name: 'radius',
|
||||
type: 'number',
|
||||
default: '300',
|
||||
description: 'Size of the spotlight effect in pixels',
|
||||
},
|
||||
{
|
||||
name: 'damping',
|
||||
type: 'number',
|
||||
default: '0.45',
|
||||
description: 'Cursor follow animation duration in seconds',
|
||||
},
|
||||
{
|
||||
name: 'fadeOut',
|
||||
type: 'number',
|
||||
default: '0.6',
|
||||
description: 'Fade-out animation duration in seconds when mouse leaves',
|
||||
},
|
||||
{
|
||||
name: 'ease',
|
||||
type: 'string',
|
||||
default: "'power3.out'",
|
||||
description: 'GSAP easing function for animations',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user