mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Demo Improvements and Masonry
This commit is contained in:
126
src/components/common/PreviewSelect.vue
Normal file
126
src/components/common/PreviewSelect.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="preview-select">
|
||||||
|
<span class="select-label">{{ title }}</span>
|
||||||
|
<Select
|
||||||
|
:model-value="modelValue"
|
||||||
|
@update:model-value="handleSelectChange"
|
||||||
|
:options="options"
|
||||||
|
v-bind="selectAttributes"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="custom-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Select from 'primevue/select'
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string
|
||||||
|
modelValue: string | number
|
||||||
|
options: Option[] | string[] | number[]
|
||||||
|
optionLabel?: string
|
||||||
|
optionValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string | number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleSelectChange = (value: string | number) => {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isObjectArray = computed(() => {
|
||||||
|
return props.options.length > 0 && typeof props.options[0] === 'object'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectAttributes = computed(() => {
|
||||||
|
if (isObjectArray.value) {
|
||||||
|
return {
|
||||||
|
optionLabel: props.optionLabel || 'label',
|
||||||
|
optionValue: props.optionValue || 'value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select) {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select:hover) {
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select:focus) {
|
||||||
|
outline: none;
|
||||||
|
border-color: #555;
|
||||||
|
box-shadow: 0 0 0 2px rgba(82, 39, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select-label) {
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select-dropdown) {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
width: 2.5rem;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select-dropdown:hover) {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select-overlay) {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-select-option) {
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -19,7 +19,7 @@ export const CATEGORIES = [
|
|||||||
{
|
{
|
||||||
name: 'Components',
|
name: 'Components',
|
||||||
subcategories: [
|
subcategories: [
|
||||||
|
'Masonry',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const textAnimations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
|
'masonry': () => import("../demo/Components/MasonryDemo.vue"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgrounds = {
|
const backgrounds = {
|
||||||
|
|||||||
32
src/constants/code/Components/masonryCode.ts
Normal file
32
src/constants/code/Components/masonryCode.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import code from '@content/Components/Masonry/Masonry.vue?raw'
|
||||||
|
import type { CodeObject } from '../../../types/code'
|
||||||
|
|
||||||
|
export const masonry: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vuebits.dev/Components/Masonry`,
|
||||||
|
installation: `npm install gsap`,
|
||||||
|
usage: `<template>
|
||||||
|
<Masonry
|
||||||
|
:items="items"
|
||||||
|
:duration="0.6"
|
||||||
|
:stagger="0.05"
|
||||||
|
animate-from="bottom"
|
||||||
|
:scale-on-hover="true"
|
||||||
|
:hover-scale="0.95"
|
||||||
|
:blur-to-focus="true"
|
||||||
|
:color-shift-on-hover="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import Masonry from "./Masonry.vue"
|
||||||
|
|
||||||
|
const items = ref([
|
||||||
|
{ id: '1', img: 'https://picsum.photos/300/400?random=1', url: 'https://picsum.photos', height: 400 },
|
||||||
|
{ id: '2', img: 'https://picsum.photos/300/600?random=2', url: 'https://picsum.photos', height: 600 },
|
||||||
|
{ id: '3', img: 'https://picsum.photos/300/350?random=3', url: 'https://picsum.photos', height: 350 },
|
||||||
|
// ... more items
|
||||||
|
])
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
}
|
||||||
261
src/content/Components/Masonry/Masonry.vue
Normal file
261
src/content/Components/Masonry/Masonry.vue
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="containerRef" class="relative w-full h-full">
|
||||||
|
<div v-for="item in grid" :key="item.id" :data-key="item.id" class="absolute box-content"
|
||||||
|
:style="{ willChange: 'transform, width, height, opacity' }" @click="openUrl(item.url)"
|
||||||
|
@mouseenter="(e) => handleMouseEnter(item.id, e.currentTarget as HTMLElement)"
|
||||||
|
@mouseleave="(e) => handleMouseLeave(item.id, e.currentTarget as HTMLElement)">
|
||||||
|
<div
|
||||||
|
class="relative w-full h-full bg-cover bg-center rounded-[10px] shadow-[0px_10px_50px_-10px_rgba(0,0,0,0.2)] uppercase text-[10px] leading-[10px]"
|
||||||
|
:style="{ backgroundImage: `url(${item.img})` }">
|
||||||
|
<div v-if="colorShiftOnHover"
|
||||||
|
class="color-overlay absolute inset-0 rounded-[10px] bg-gradient-to-tr from-pink-500/50 to-sky-500/50 opacity-0 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watchEffect, nextTick } from 'vue'
|
||||||
|
import { gsap } from 'gsap'
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
id: string
|
||||||
|
img: string
|
||||||
|
url: string
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MasonryProps {
|
||||||
|
items: Item[]
|
||||||
|
ease?: string
|
||||||
|
duration?: number
|
||||||
|
stagger?: number
|
||||||
|
animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random'
|
||||||
|
scaleOnHover?: boolean
|
||||||
|
hoverScale?: number
|
||||||
|
blurToFocus?: boolean
|
||||||
|
colorShiftOnHover?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<MasonryProps>(), {
|
||||||
|
ease: 'power3.out',
|
||||||
|
duration: 0.6,
|
||||||
|
stagger: 0.05,
|
||||||
|
animateFrom: 'bottom',
|
||||||
|
scaleOnHover: true,
|
||||||
|
hoverScale: 0.95,
|
||||||
|
blurToFocus: true,
|
||||||
|
colorShiftOnHover: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const useMedia = (queries: string[], values: number[], defaultValue: number) => {
|
||||||
|
const get = () => values[queries.findIndex((q) => matchMedia(q).matches)] ?? defaultValue
|
||||||
|
const value = ref<number>(get())
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const handler = () => value.value = get()
|
||||||
|
queries.forEach((q) => matchMedia(q).addEventListener('change', handler))
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
queries.forEach((q) => matchMedia(q).removeEventListener('change', handler))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMeasure = () => {
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const size = ref({ width: 0, height: 0 })
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(([entry]) => {
|
||||||
|
const { width, height } = entry.contentRect
|
||||||
|
size.value = { width, height }
|
||||||
|
})
|
||||||
|
|
||||||
|
resizeObserver.observe(containerRef.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [containerRef, size] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadImages = async (urls: string[]): Promise<void> => {
|
||||||
|
await Promise.all(
|
||||||
|
urls.map(
|
||||||
|
(src) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.src = src
|
||||||
|
img.onload = img.onerror = () => resolve()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMedia(
|
||||||
|
[
|
||||||
|
'(min-width:1500px)',
|
||||||
|
'(min-width:1000px)',
|
||||||
|
'(min-width:600px)',
|
||||||
|
'(min-width:400px)'
|
||||||
|
],
|
||||||
|
[5, 4, 3, 2],
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
const [containerRef, size] = useMeasure()
|
||||||
|
const imagesReady = ref(false)
|
||||||
|
const hasMounted = ref(false)
|
||||||
|
|
||||||
|
const grid = computed(() => {
|
||||||
|
if (!size.value.width) return []
|
||||||
|
const colHeights = new Array(columns.value).fill(0)
|
||||||
|
const gap = 16
|
||||||
|
const totalGaps = (columns.value - 1) * gap
|
||||||
|
const columnWidth = (size.value.width - totalGaps) / columns.value
|
||||||
|
|
||||||
|
return props.items.map((child) => {
|
||||||
|
const col = colHeights.indexOf(Math.min(...colHeights))
|
||||||
|
const x = col * (columnWidth + gap)
|
||||||
|
const height = child.height / 2
|
||||||
|
const y = colHeights[col]
|
||||||
|
|
||||||
|
colHeights[col] += height + gap
|
||||||
|
return { ...child, x, y, w: columnWidth, h: height }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const openUrl = (url: string) => {
|
||||||
|
window.open(url, '_blank', 'noopener')
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GridItem extends Item {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialPosition = (item: GridItem) => {
|
||||||
|
const containerRect = containerRef.value?.getBoundingClientRect()
|
||||||
|
if (!containerRect) return { x: item.x, y: item.y }
|
||||||
|
|
||||||
|
let direction = props.animateFrom
|
||||||
|
if (props.animateFrom === 'random') {
|
||||||
|
const dirs = ['top', 'bottom', 'left', 'right']
|
||||||
|
direction = dirs[Math.floor(Math.random() * dirs.length)] as typeof props.animateFrom
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'top':
|
||||||
|
return { x: item.x, y: -200 }
|
||||||
|
case 'bottom':
|
||||||
|
return { x: item.x, y: window.innerHeight + 200 }
|
||||||
|
case 'left':
|
||||||
|
return { x: -200, y: item.y }
|
||||||
|
case 'right':
|
||||||
|
return { x: window.innerWidth + 200, y: item.y }
|
||||||
|
case 'center':
|
||||||
|
return {
|
||||||
|
x: containerRect.width / 2 - item.w / 2,
|
||||||
|
y: containerRect.height / 2 - item.h / 2
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { x: item.x, y: item.y + 100 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = (id: string, element: HTMLElement) => {
|
||||||
|
if (props.scaleOnHover) {
|
||||||
|
gsap.to(`[data-key="${id}"]`, {
|
||||||
|
scale: props.hoverScale,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power2.out'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (props.colorShiftOnHover) {
|
||||||
|
const overlay = element.querySelector('.color-overlay') as HTMLElement
|
||||||
|
if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = (id: string, element: HTMLElement) => {
|
||||||
|
if (props.scaleOnHover) {
|
||||||
|
gsap.to(`[data-key="${id}"]`, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power2.out'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (props.colorShiftOnHover) {
|
||||||
|
const overlay = element.querySelector('.color-overlay') as HTMLElement
|
||||||
|
if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
preloadImages(props.items.map((i) => i.img)).then(() => {
|
||||||
|
imagesReady.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!imagesReady.value) return
|
||||||
|
|
||||||
|
const currentGrid = grid.value
|
||||||
|
void props.items.length
|
||||||
|
void columns.value
|
||||||
|
void size.value.width
|
||||||
|
|
||||||
|
if (!currentGrid.length) return
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
currentGrid.forEach((item, index) => {
|
||||||
|
const selector = `[data-key="${item.id}"]`
|
||||||
|
const animProps = { x: item.x, y: item.y, width: item.w, height: item.h }
|
||||||
|
|
||||||
|
if (!hasMounted.value) {
|
||||||
|
const start = getInitialPosition(item)
|
||||||
|
gsap.fromTo(
|
||||||
|
selector,
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
x: start.x,
|
||||||
|
y: start.y,
|
||||||
|
width: item.w,
|
||||||
|
height: item.h,
|
||||||
|
...(props.blurToFocus && { filter: 'blur(10px)' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
...animProps,
|
||||||
|
...(props.blurToFocus && { filter: 'blur(0px)' }),
|
||||||
|
duration: 0.8,
|
||||||
|
ease: 'power3.out',
|
||||||
|
delay: index * props.stagger
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gsap.to(selector, {
|
||||||
|
...animProps,
|
||||||
|
duration: props.duration,
|
||||||
|
ease: props.ease,
|
||||||
|
overwrite: 'auto'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
hasMounted.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
0
src/content/Components/TiltedCard/TiltedCard.vue
Normal file
0
src/content/Components/TiltedCard/TiltedCard.vue
Normal file
@@ -16,8 +16,8 @@
|
|||||||
class="fade-content-demo-content"
|
class="fade-content-demo-content"
|
||||||
>
|
>
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
<h4>Fade Content Example</h4>
|
<h4>Fade Content</h4>
|
||||||
<p>This content will fade in when it enters the viewport.</p>
|
<p>It will fade in when it enters the viewport.</p>
|
||||||
</div>
|
</div>
|
||||||
</FadeContent>
|
</FadeContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
116
src/demo/Components/MasonryDemo.vue
Normal file
116
src/demo/Components/MasonryDemo.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="masonry-demo">
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container" style="height: 900px; overflow: hidden;">
|
||||||
|
<RefreshButton @refresh="forceRerender" />
|
||||||
|
|
||||||
|
<Masonry :key="rerenderKey" :items="sampleItems" :ease="ease" :duration="duration" :stagger="stagger"
|
||||||
|
:animate-from="animateFrom" :scale-on-hover="scaleOnHover" :hover-scale="hoverScale"
|
||||||
|
:blur-to-focus="blurToFocus" :color-shift-on-hover="colorShiftOnHover" class="masonry-demo-container" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSwitch title="Scale on Hover" v-model="scaleOnHover" @update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Blur to Focus" v-model="blurToFocus" @update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Color Shift on Hover" v-model="colorShiftOnHover" @update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSelect title="Animation Direction" v-model="animateFrom" :options="[
|
||||||
|
{ label: 'Bottom', value: 'bottom' },
|
||||||
|
{ label: 'Top', value: 'top' },
|
||||||
|
{ label: 'Left', value: 'left' },
|
||||||
|
{ label: 'Right', value: 'right' },
|
||||||
|
{ label: 'Center', value: 'center' },
|
||||||
|
{ label: 'Random', value: 'random' }
|
||||||
|
]" @update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSlider title="Duration (s)" v-model="duration" :min="0.1" :max="2" :step="0.1"
|
||||||
|
@update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSlider title="Stagger Delay (s)" v-model="stagger" :min="0.01" :max="0.2" :step="0.01"
|
||||||
|
@update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSlider title="Hover Scale" v-model="hoverScale" :min="0.8" :max="1.2" :step="0.05"
|
||||||
|
@update:model-value="forceRerender" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['gsap']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="masonry" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="masonry.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue'
|
||||||
|
import RefreshButton from '../../components/common/RefreshButton.vue'
|
||||||
|
import PropTable from '../../components/common/PropTable.vue'
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue'
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue'
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue'
|
||||||
|
import Customize from '../../components/common/Customize.vue'
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue'
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue'
|
||||||
|
import PreviewSelect from '../../components/common/PreviewSelect.vue'
|
||||||
|
import Masonry from '../../content/Components/Masonry/Masonry.vue'
|
||||||
|
import { masonry } from '@/constants/code/Components/masonryCode'
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender'
|
||||||
|
|
||||||
|
const ease = ref('power3.out')
|
||||||
|
const duration = ref(0.6)
|
||||||
|
const stagger = ref(0.05)
|
||||||
|
const animateFrom = ref<'bottom' | 'top' | 'left' | 'right' | 'center' | 'random'>('bottom')
|
||||||
|
const scaleOnHover = ref(true)
|
||||||
|
const hoverScale = ref(0.95)
|
||||||
|
const blurToFocus = ref(true)
|
||||||
|
const colorShiftOnHover = ref(false)
|
||||||
|
const { rerenderKey, forceRerender } = useForceRerender()
|
||||||
|
|
||||||
|
const sampleItems = ref([
|
||||||
|
{ id: '1', img: 'https://picsum.photos/300/400?random=1&grayscale', url: 'https://picsum.photos', height: 400 },
|
||||||
|
{ id: '2', img: 'https://picsum.photos/300/600?random=2&grayscale', url: 'https://picsum.photos', height: 600 },
|
||||||
|
{ id: '3', img: 'https://picsum.photos/300/350?random=3&grayscale', url: 'https://picsum.photos', height: 350 },
|
||||||
|
{ id: '4', img: 'https://picsum.photos/300/500?random=4&grayscale', url: 'https://picsum.photos', height: 500 },
|
||||||
|
{ id: '5', img: 'https://picsum.photos/300/450?random=5&grayscale', url: 'https://picsum.photos', height: 450 },
|
||||||
|
{ id: '6', img: 'https://picsum.photos/300/380?random=6&grayscale', url: 'https://picsum.photos', height: 380 },
|
||||||
|
{ id: '7', img: 'https://picsum.photos/300/520?random=7&grayscale', url: 'https://picsum.photos', height: 520 },
|
||||||
|
{ id: '8', img: 'https://picsum.photos/300/420?random=8&grayscale', url: 'https://picsum.photos', height: 420 },
|
||||||
|
{ id: '9', img: 'https://picsum.photos/300/480?random=9&grayscale', url: 'https://picsum.photos', height: 480 },
|
||||||
|
{ id: '10', img: 'https://picsum.photos/300/360?random=10&grayscale', url: 'https://picsum.photos', height: 360 },
|
||||||
|
{ id: '11', img: 'https://picsum.photos/300/550?random=11&grayscale', url: 'https://picsum.photos', height: 550 },
|
||||||
|
{ id: '12', img: 'https://picsum.photos/300/400?random=12&grayscale', url: 'https://picsum.photos', height: 400 },
|
||||||
|
{ id: '13', img: 'https://picsum.photos/300/470?random=13&grayscale', url: 'https://picsum.photos', height: 470 },
|
||||||
|
{ id: '14', img: 'https://picsum.photos/300/390?random=14&grayscale', url: 'https://picsum.photos', height: 390 },
|
||||||
|
{ id: '15', img: 'https://picsum.photos/300/510?random=15&grayscale', url: 'https://picsum.photos', height: 510 },
|
||||||
|
])
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{ name: 'items', type: 'Item[]', default: '[]', description: 'Array of items to display in the masonry grid. Each item must have id, img, url, and height properties.' },
|
||||||
|
{ name: 'ease', type: 'string', default: '"power3.out"', description: 'GSAP easing function for animations.' },
|
||||||
|
{ name: 'duration', type: 'number', default: '0.6', description: 'Duration of the animation in seconds.' },
|
||||||
|
{ name: 'stagger', type: 'number', default: '0.05', description: 'Stagger delay between item animations in seconds.' },
|
||||||
|
{ name: 'animateFrom', type: 'string', default: '"bottom"', description: 'Direction items animate from: "bottom", "top", "left", "right", "center", or "random".' },
|
||||||
|
{ name: 'scaleOnHover', type: 'boolean', default: 'true', description: 'Whether items scale on hover.' },
|
||||||
|
{ name: 'hoverScale', type: 'number', default: '0.95', description: 'Scale factor when hovering over items.' },
|
||||||
|
{ name: 'blurToFocus', type: 'boolean', default: 'true', description: 'Whether items start blurred and focus on entrance.' },
|
||||||
|
{ name: 'colorShiftOnHover', type: 'boolean', default: 'false', description: 'Whether to show color overlay on hover.' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.masonry-demo-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -13,6 +13,7 @@ import PrimeVue from 'primevue/config'
|
|||||||
import Aura from '@primeuix/themes/aura'
|
import Aura from '@primeuix/themes/aura'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Toast from 'primevue/toast'
|
import Toast from 'primevue/toast'
|
||||||
|
import Select from 'primevue/select'
|
||||||
import ToastService from 'primevue/toastservice'
|
import ToastService from 'primevue/toastservice'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@@ -28,5 +29,6 @@ app.use(ToastService)
|
|||||||
// Global components
|
// Global components
|
||||||
app.component('Button', Button)
|
app.component('Button', Button)
|
||||||
app.component('Toast', Toast)
|
app.component('Toast', Toast)
|
||||||
|
app.component('Select', Select)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
Reference in New Issue
Block a user