mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Small fixes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import code from '@content/Components/AnimatedList/AnimatedList.vue?raw'
|
||||
import type { CodeObject } from '../../../types/code'
|
||||
import code from '@content/Components/AnimatedList/AnimatedList.vue?raw';
|
||||
import type { CodeObject } from '../../../types/code';
|
||||
|
||||
export const animatedList: CodeObject = {
|
||||
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/AnimatedList`,
|
||||
@@ -24,4 +24,4 @@ const items = [
|
||||
]
|
||||
</script>`,
|
||||
code
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,4 +13,4 @@ export const folder: CodeObject = {
|
||||
const items = ['Doc 1', 'Doc 2', 'Doc 3'];
|
||||
</script>`,
|
||||
code
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,87 +1,94 @@
|
||||
<template>
|
||||
<component :is="as" :class="[
|
||||
'relative inline-block overflow-hidden !bg-transparent !border-none !rounded-[20px]',
|
||||
customClass
|
||||
]" v-bind="restAttrs" :style="componentStyle">
|
||||
<div class="absolute w-[300%] h-[50%] opacity-70 bottom-[-11px] right-[-250%] rounded-full animate-star-movement-bottom z-0"
|
||||
:style="{
|
||||
background: `radial-gradient(circle, ${color}, transparent 10%)`,
|
||||
animationDuration: speed
|
||||
}"></div>
|
||||
<component
|
||||
:is="as"
|
||||
:class="['relative inline-block overflow-hidden !bg-transparent !border-none !rounded-[20px]', customClass]"
|
||||
v-bind="restAttrs"
|
||||
:style="componentStyle"
|
||||
>
|
||||
<div
|
||||
class="absolute w-[300%] h-[50%] opacity-70 bottom-[-11px] right-[-250%] rounded-full animate-star-movement-bottom z-0"
|
||||
:style="{
|
||||
background: `radial-gradient(circle, ${color}, transparent 10%)`,
|
||||
animationDuration: speed
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div class="absolute w-[300%] h-[50%] opacity-70 top-[-10px] left-[-250%] rounded-full animate-star-movement-top z-0"
|
||||
:style="{
|
||||
background: `radial-gradient(circle, ${color}, transparent 10%)`,
|
||||
animationDuration: speed
|
||||
}"></div>
|
||||
<div
|
||||
class="absolute w-[300%] h-[50%] opacity-70 top-[-10px] left-[-250%] rounded-full animate-star-movement-top z-0"
|
||||
:style="{
|
||||
background: `radial-gradient(circle, ${color}, transparent 10%)`,
|
||||
animationDuration: speed
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="relative z-10 border border-[#222] bg-black text-white text-[16px] text-center px-[26px] py-[16px] rounded-[20px]">
|
||||
<slot />
|
||||
</div>
|
||||
</component>
|
||||
<div
|
||||
class="relative z-10 border border-[#333] bg-[#0b0b0b] text-white text-[16px] text-center px-[64px] py-[24px] rounded-[20px]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, useAttrs } from 'vue';
|
||||
|
||||
interface StarBorderProps {
|
||||
as?: string;
|
||||
customClass?: string;
|
||||
color?: string;
|
||||
speed?: string;
|
||||
thickness?: number;
|
||||
as?: string;
|
||||
customClass?: string;
|
||||
color?: string;
|
||||
speed?: string;
|
||||
thickness?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<StarBorderProps>(), {
|
||||
as: 'button',
|
||||
customClass: '',
|
||||
color: 'white',
|
||||
speed: '6s',
|
||||
thickness: 1
|
||||
as: 'button',
|
||||
customClass: '',
|
||||
color: 'white',
|
||||
speed: '6s',
|
||||
thickness: 1
|
||||
});
|
||||
|
||||
const restAttrs = useAttrs();
|
||||
|
||||
const componentStyle = computed(() => {
|
||||
const base = {
|
||||
padding: `${props.thickness}px 0`
|
||||
};
|
||||
const userStyle = (restAttrs.style as Record<string, string>) || {};
|
||||
return { ...base, ...userStyle };
|
||||
const base = {
|
||||
padding: `${props.thickness}px 0`
|
||||
};
|
||||
const userStyle = (restAttrs.style as Record<string, string>) || {};
|
||||
return { ...base, ...userStyle };
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes star-movement-bottom {
|
||||
0% {
|
||||
transform: translate(0%, 0%);
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
transform: translate(0%, 0%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-100%, 0%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-100%, 0%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes star-movement-top {
|
||||
0% {
|
||||
transform: translate(0%, 0%);
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
transform: translate(0%, 0%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(100%, 0%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translate(100%, 0%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-star-movement-bottom {
|
||||
animation: star-movement-bottom linear infinite alternate;
|
||||
animation: star-movement-bottom linear infinite alternate;
|
||||
}
|
||||
|
||||
.animate-star-movement-top {
|
||||
animation: star-movement-top linear infinite alternate;
|
||||
animation: star-movement-top linear infinite alternate;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
:animate="getItemInView(index) ? { scale: 1, opacity: 1 } : { scale: 0.7, opacity: 0 }"
|
||||
:transition="{ duration: 0.2, delay: 0.1 }"
|
||||
@mouseenter="() => setSelectedIndex(index)"
|
||||
@click="() => {
|
||||
setSelectedIndex(index)
|
||||
emit('itemSelected', item, index)
|
||||
}"
|
||||
@click="
|
||||
() => {
|
||||
setSelectedIndex(index);
|
||||
emit('itemSelected', item, index);
|
||||
}
|
||||
"
|
||||
>
|
||||
<div :class="`p-4 bg-[#111] rounded-lg ${selectedIndex === index ? 'bg-[#222]' : ''} ${itemClassName}`">
|
||||
<p class="text-white m-0">{{ item }}</p>
|
||||
@@ -47,24 +49,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue'
|
||||
import { Motion } from 'motion-v'
|
||||
import { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
|
||||
import { Motion } from 'motion-v';
|
||||
|
||||
interface AnimatedListProps {
|
||||
items?: string[]
|
||||
showGradients?: boolean
|
||||
enableArrowNavigation?: boolean
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
displayScrollbar?: boolean
|
||||
initialSelectedIndex?: number
|
||||
items?: string[];
|
||||
showGradients?: boolean;
|
||||
enableArrowNavigation?: boolean;
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
displayScrollbar?: boolean;
|
||||
initialSelectedIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AnimatedListProps>(), {
|
||||
items: () => [
|
||||
'Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5',
|
||||
'Item 6', 'Item 7', 'Item 8', 'Item 9', 'Item 10',
|
||||
'Item 11', 'Item 12', 'Item 13', 'Item 14', 'Item 15'
|
||||
'Item 1',
|
||||
'Item 2',
|
||||
'Item 3',
|
||||
'Item 4',
|
||||
'Item 5',
|
||||
'Item 6',
|
||||
'Item 7',
|
||||
'Item 8',
|
||||
'Item 9',
|
||||
'Item 10',
|
||||
'Item 11',
|
||||
'Item 12',
|
||||
'Item 13',
|
||||
'Item 14',
|
||||
'Item 15'
|
||||
],
|
||||
showGradients: true,
|
||||
enableArrowNavigation: true,
|
||||
@@ -72,114 +86,108 @@ const props = withDefaults(defineProps<AnimatedListProps>(), {
|
||||
itemClassName: '',
|
||||
displayScrollbar: true,
|
||||
initialSelectedIndex: -1
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
itemSelected: [item: string, index: number]
|
||||
}>()
|
||||
itemSelected: [item: string, index: number];
|
||||
}>();
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
const listRef = useTemplateRef<HTMLDivElement>('listRef')
|
||||
const selectedIndex = ref(props.initialSelectedIndex)
|
||||
const keyboardNav = ref(false)
|
||||
const topGradientOpacity = ref(0)
|
||||
const bottomGradientOpacity = ref(1)
|
||||
const itemsInView = ref<boolean[]>([])
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
|
||||
const listRef = useTemplateRef<HTMLDivElement>('listRef');
|
||||
const selectedIndex = ref(props.initialSelectedIndex);
|
||||
const keyboardNav = ref(false);
|
||||
const topGradientOpacity = ref(0);
|
||||
const bottomGradientOpacity = ref(1);
|
||||
const itemsInView = ref<boolean[]>([]);
|
||||
|
||||
const setSelectedIndex = (index: number) => {
|
||||
selectedIndex.value = index
|
||||
}
|
||||
selectedIndex.value = index;
|
||||
};
|
||||
|
||||
const getItemInView = (index: number) => {
|
||||
return itemsInView.value[index] ?? false
|
||||
}
|
||||
return itemsInView.value[index] ?? false;
|
||||
};
|
||||
|
||||
const handleScroll = (e: Event) => {
|
||||
const target = e.target as HTMLDivElement
|
||||
const { scrollTop, scrollHeight, clientHeight } = target
|
||||
topGradientOpacity.value = Math.min(scrollTop / 50, 1)
|
||||
const bottomDistance = scrollHeight - (scrollTop + clientHeight)
|
||||
bottomGradientOpacity.value =
|
||||
scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1)
|
||||
|
||||
updateItemsInView()
|
||||
}
|
||||
const target = e.target as HTMLDivElement;
|
||||
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||
topGradientOpacity.value = Math.min(scrollTop / 50, 1);
|
||||
const bottomDistance = scrollHeight - (scrollTop + clientHeight);
|
||||
bottomGradientOpacity.value = scrollHeight <= clientHeight ? 0 : Math.min(bottomDistance / 50, 1);
|
||||
|
||||
updateItemsInView();
|
||||
};
|
||||
|
||||
const updateItemsInView = () => {
|
||||
if (!listRef.value) return
|
||||
|
||||
const container = listRef.value
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
|
||||
if (!listRef.value) return;
|
||||
|
||||
const container = listRef.value;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
itemsInView.value = props.items.map((_, index) => {
|
||||
const item = container.querySelector(`[data-index="${index}"]`) as HTMLElement
|
||||
if (!item) return false
|
||||
|
||||
const itemRect = item.getBoundingClientRect()
|
||||
const viewHeight = containerRect.height
|
||||
const itemTop = itemRect.top - containerRect.top
|
||||
const itemBottom = itemTop + itemRect.height
|
||||
|
||||
return itemTop < viewHeight && itemBottom > 0
|
||||
})
|
||||
}
|
||||
const item = container.querySelector(`[data-index="${index}"]`) as HTMLElement;
|
||||
if (!item) return false;
|
||||
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const viewHeight = containerRect.height;
|
||||
const itemTop = itemRect.top - containerRect.top;
|
||||
const itemBottom = itemTop + itemRect.height;
|
||||
|
||||
return itemTop < viewHeight && itemBottom > 0;
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {
|
||||
e.preventDefault()
|
||||
keyboardNav.value = true
|
||||
setSelectedIndex(Math.min(selectedIndex.value + 1, props.items.length - 1))
|
||||
e.preventDefault();
|
||||
keyboardNav.value = true;
|
||||
setSelectedIndex(Math.min(selectedIndex.value + 1, props.items.length - 1));
|
||||
} else if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {
|
||||
e.preventDefault()
|
||||
keyboardNav.value = true
|
||||
setSelectedIndex(Math.max(selectedIndex.value - 1, 0))
|
||||
e.preventDefault();
|
||||
keyboardNav.value = true;
|
||||
setSelectedIndex(Math.max(selectedIndex.value - 1, 0));
|
||||
} else if (e.key === 'Enter') {
|
||||
if (selectedIndex.value >= 0 && selectedIndex.value < props.items.length) {
|
||||
e.preventDefault()
|
||||
emit('itemSelected', props.items[selectedIndex.value], selectedIndex.value)
|
||||
e.preventDefault();
|
||||
emit('itemSelected', props.items[selectedIndex.value], selectedIndex.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch([selectedIndex, keyboardNav], () => {
|
||||
if (!keyboardNav.value || selectedIndex.value < 0 || !listRef.value) return
|
||||
const container = listRef.value
|
||||
const selectedItem = container.querySelector(
|
||||
`[data-index="${selectedIndex.value}"]`
|
||||
) as HTMLElement | null
|
||||
if (!keyboardNav.value || selectedIndex.value < 0 || !listRef.value) return;
|
||||
const container = listRef.value;
|
||||
const selectedItem = container.querySelector(`[data-index="${selectedIndex.value}"]`) as HTMLElement | null;
|
||||
if (selectedItem) {
|
||||
const extraMargin = 50
|
||||
const containerScrollTop = container.scrollTop
|
||||
const containerHeight = container.clientHeight
|
||||
const itemTop = selectedItem.offsetTop
|
||||
const itemBottom = itemTop + selectedItem.offsetHeight
|
||||
const extraMargin = 50;
|
||||
const containerScrollTop = container.scrollTop;
|
||||
const containerHeight = container.clientHeight;
|
||||
const itemTop = selectedItem.offsetTop;
|
||||
const itemBottom = itemTop + selectedItem.offsetHeight;
|
||||
if (itemTop < containerScrollTop + extraMargin) {
|
||||
container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' })
|
||||
} else if (
|
||||
itemBottom >
|
||||
containerScrollTop + containerHeight - extraMargin
|
||||
) {
|
||||
container.scrollTo({ top: itemTop - extraMargin, behavior: 'smooth' });
|
||||
} else if (itemBottom > containerScrollTop + containerHeight - extraMargin) {
|
||||
container.scrollTo({
|
||||
top: itemBottom - containerHeight + extraMargin,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
keyboardNav.value = false
|
||||
})
|
||||
keyboardNav.value = false;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.enableArrowNavigation) {
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
itemsInView.value = new Array(props.items.length).fill(true)
|
||||
setTimeout(updateItemsInView, 100)
|
||||
})
|
||||
|
||||
itemsInView.value = new Array(props.items.length).fill(true);
|
||||
setTimeout(updateItemsInView, 100);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.enableArrowNavigation) {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,51 +1,34 @@
|
||||
<template>
|
||||
<div :style="{transform: `scale(${props.size})`}" :class="class">
|
||||
<div
|
||||
:class="folderClass"
|
||||
:style="folderStyle"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="folder_back">
|
||||
<div
|
||||
v-for="(item, i) in papers"
|
||||
:key="i"
|
||||
:class="`paper paper-${i + 1}`"
|
||||
@mousemove="(e) => handlePaperMouseMove(e, i)"
|
||||
@mouseleave="(e) => handlePaperMouseLeave(e, i)"
|
||||
:style="open ? {
|
||||
'--magnet-x': `${paperOffsets[i]?.x || 0}px`,
|
||||
'--magnet-y': `${paperOffsets[i]?.y || 0}px`,
|
||||
} : {}"
|
||||
>
|
||||
<slot
|
||||
:name="`item-${i + 1}`"
|
||||
:item="item"
|
||||
:index="i"
|
||||
:isOpen="open"
|
||||
>
|
||||
<div :style="{ transform: `scale(${props.size})` }" :class="props.class">
|
||||
<div :class="folderClass" :style="folderStyle" @click="handleClick">
|
||||
<div
|
||||
class="relative w-[100px] h-[80px] rounded-tl-0 rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px]"
|
||||
:style="{ backgroundColor: folderBackColor }"
|
||||
>
|
||||
<span
|
||||
class="absolute z-0 bottom-[98%] left-0 w-[30px] h-[10px] rounded-tl-[5px] rounded-tr-[5px] rounded-bl-0 rounded-br-0"
|
||||
:style="{ backgroundColor: folderBackColor }"
|
||||
></span>
|
||||
<div v-for="(item, i) in papers" :key="i" :class="getPaperClasses(i)" :style="getPaperStyle(i)">
|
||||
<slot :name="`item-${i + 1}`" :item="item" :index="i" :isOpen="open">
|
||||
{{ item }}
|
||||
</slot>
|
||||
</div>
|
||||
<div class="folder_front"></div>
|
||||
<div class="folder_front right"></div>
|
||||
<div :class="frontClass" :style="{ backgroundColor: props.color }"></div>
|
||||
<div :class="rightClass" :style="{ backgroundColor: props.color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface PaperOffset {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
color?: string
|
||||
size?: number
|
||||
items?: (string | null)[]
|
||||
class?: string
|
||||
color?: string;
|
||||
size?: number;
|
||||
items?: (string | null)[];
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -53,212 +36,112 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
size: 1,
|
||||
items: () => [],
|
||||
class: ''
|
||||
})
|
||||
});
|
||||
|
||||
const darkenColor = (hex: string, percent: number): string => {
|
||||
let color = hex.startsWith('#') ? hex.slice(1) : hex
|
||||
let color = hex.startsWith('#') ? hex.slice(1) : hex;
|
||||
if (color.length === 3) {
|
||||
color = color
|
||||
.split('')
|
||||
.map((c) => c + c)
|
||||
.join('')
|
||||
.map(c => c + c)
|
||||
.join('');
|
||||
}
|
||||
const num = parseInt(color, 16)
|
||||
let r = (num >> 16) & 0xff
|
||||
let g = (num >> 8) & 0xff
|
||||
let b = num & 0xff
|
||||
r = Math.max(0, Math.min(255, Math.floor(r * (1 - percent))))
|
||||
g = Math.max(0, Math.min(255, Math.floor(g * (1 - percent))))
|
||||
b = Math.max(0, Math.min(255, Math.floor(b * (1 - percent))))
|
||||
return (
|
||||
'#' +
|
||||
((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()
|
||||
)
|
||||
}
|
||||
const num = parseInt(color, 16);
|
||||
let r = (num >> 16) & 0xff;
|
||||
let g = (num >> 8) & 0xff;
|
||||
let b = num & 0xff;
|
||||
r = Math.max(0, Math.min(255, Math.floor(r * (1 - percent))));
|
||||
g = Math.max(0, Math.min(255, Math.floor(g * (1 - percent))));
|
||||
b = Math.max(0, Math.min(255, Math.floor(b * (1 - percent))));
|
||||
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
||||
};
|
||||
|
||||
const open = ref(false)
|
||||
const maxItems = 3
|
||||
const paperOffsets = ref<PaperOffset[]>(
|
||||
Array.from({ length: maxItems }, () => ({ x: 0, y: 0 }))
|
||||
)
|
||||
const open = ref(false);
|
||||
const maxItems = 3;
|
||||
|
||||
const papers = computed(() => {
|
||||
const result = props.items.slice(0, maxItems)
|
||||
const result = props.items.slice(0, maxItems);
|
||||
while (result.length < maxItems) {
|
||||
result.push(null)
|
||||
result.push(null);
|
||||
}
|
||||
return result
|
||||
})
|
||||
return result;
|
||||
});
|
||||
|
||||
const folderBackColor = computed(() => darkenColor(props.color, 0.08))
|
||||
const paper1 = computed(() => darkenColor('#ffffff', 0.1))
|
||||
const paper2 = computed(() => darkenColor('#ffffff', 0.05))
|
||||
const paper3 = computed(() => '#ffffff')
|
||||
const folderBackColor = computed(() => darkenColor(props.color, 0.08));
|
||||
const paper1 = computed(() => darkenColor('#ffffff', 0.1));
|
||||
const paper2 = computed(() => darkenColor('#ffffff', 0.05));
|
||||
const paper3 = computed(() => '#ffffff');
|
||||
|
||||
const folderStyle = computed(() => ({
|
||||
'--folder-color': props.color,
|
||||
'--folder-back-color': folderBackColor.value,
|
||||
'--paper-1': paper1.value,
|
||||
'--paper-2': paper2.value,
|
||||
'--paper-3': paper3.value,
|
||||
}))
|
||||
transform: open.value ? 'translateY(-8px)' : undefined
|
||||
}));
|
||||
|
||||
const folderClass = computed(() => `folder ${open.value ? 'open' : ''}`.trim())
|
||||
const folderClass = computed(() =>
|
||||
`group relative transition-all duration-200 ease-in cursor-pointer ${
|
||||
!open.value ? 'hover:-translate-y-2' : ''
|
||||
}`.trim()
|
||||
);
|
||||
|
||||
const getPaperClasses = (index: number) => {
|
||||
let sizeClasses = '';
|
||||
if (index === 0) sizeClasses = 'w-[70%] h-[80%]';
|
||||
if (index === 1) sizeClasses = open.value ? 'w-[80%] h-[80%]' : 'w-[80%] h-[70%]';
|
||||
if (index === 2) sizeClasses = open.value ? 'w-[90%] h-[80%]' : 'w-[90%] h-[60%]';
|
||||
|
||||
return `absolute z-20 bottom-[10%] left-1/2 transition-all duration-300 ease-in-out overflow-hidden ${
|
||||
!open.value ? 'transform -translate-x-1/2 translate-y-[10%] group-hover:translate-y-0' : 'hover:scale-110'
|
||||
} ${sizeClasses}`.trim();
|
||||
};
|
||||
|
||||
const getOpenTransform = (index: number) => {
|
||||
if (index === 0) return 'translate(-120%, -70%) rotate(-15deg)';
|
||||
if (index === 1) return 'translate(10%, -70%) rotate(15deg)';
|
||||
if (index === 2) return 'translate(-50%, -100%) rotate(5deg)';
|
||||
return '';
|
||||
};
|
||||
|
||||
const getPaperStyle = (index: number) => {
|
||||
const backgroundColor = index === 0 ? paper1.value : index === 1 ? paper2.value : paper3.value;
|
||||
const baseStyle = {
|
||||
backgroundColor,
|
||||
borderRadius: '10px',
|
||||
transition: 'all 0.3s ease-in-out'
|
||||
};
|
||||
|
||||
if (open.value) {
|
||||
const transformStyle = getOpenTransform(index);
|
||||
return {
|
||||
...baseStyle,
|
||||
transform: transformStyle
|
||||
};
|
||||
}
|
||||
|
||||
return baseStyle;
|
||||
};
|
||||
|
||||
const frontClass = computed(() => {
|
||||
const baseClasses = 'absolute z-30 w-full h-full origin-bottom transition-all duration-300 ease-in-out';
|
||||
const borderRadius = 'rounded-tl-[5px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px]';
|
||||
|
||||
if (open.value) {
|
||||
return `${baseClasses} ${borderRadius} [transform:skew(15deg)_scaleY(0.6)]`;
|
||||
} else {
|
||||
return `${baseClasses} ${borderRadius} group-hover:[transform:skew(15deg)_scaleY(0.6)]`;
|
||||
}
|
||||
});
|
||||
|
||||
const rightClass = computed(() => {
|
||||
const baseClasses = 'absolute z-30 w-full h-full origin-bottom transition-all duration-300 ease-in-out';
|
||||
const borderRadius = 'rounded-tl-[5px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px]';
|
||||
|
||||
if (open.value) {
|
||||
return `${baseClasses} ${borderRadius} [transform:skew(-15deg)_scaleY(0.6)]`;
|
||||
} else {
|
||||
return `${baseClasses} ${borderRadius} group-hover:[transform:skew(-15deg)_scaleY(0.6)]`;
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
open.value = !open.value
|
||||
if (!open.value) {
|
||||
paperOffsets.value = Array.from({ length: maxItems }, () => ({ x: 0, y: 0 }))
|
||||
}
|
||||
}
|
||||
|
||||
const handlePaperMouseMove = (e: MouseEvent, index: number) => {
|
||||
if (!open.value) return
|
||||
|
||||
const target = e.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 2
|
||||
const offsetX = (e.clientX - centerX) * 0.15
|
||||
const offsetY = (e.clientY - centerY) * 0.15
|
||||
|
||||
paperOffsets.value = paperOffsets.value.map((offset, i) =>
|
||||
i === index ? { x: offsetX, y: offsetY } : offset
|
||||
)
|
||||
}
|
||||
|
||||
const handlePaperMouseLeave = (e: MouseEvent, index: number) => {
|
||||
paperOffsets.value = paperOffsets.value.map((offset, i) =>
|
||||
i === index ? { x: 0, y: 0 } : offset
|
||||
)
|
||||
}
|
||||
open.value = !open.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:root {
|
||||
--folder-color: #70a1ff;
|
||||
--folder-back-color: #4785ff;
|
||||
--paper-1: #e6e6e6;
|
||||
--paper-2: #f2f2f2;
|
||||
--paper-3: #ffffff;
|
||||
}
|
||||
|
||||
.folder {
|
||||
transition: all 0.2s ease-in;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder:not(.folder--click):hover {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.folder:not(.folder--click):hover .paper {
|
||||
transform: translate(-50%, 0%);
|
||||
}
|
||||
|
||||
.folder:not(.folder--click):hover .folder_front {
|
||||
transform: skew(15deg) scaleY(0.6);
|
||||
}
|
||||
|
||||
.folder:not(.folder--click):hover .right {
|
||||
transform: skew(-15deg) scaleY(0.6);
|
||||
}
|
||||
|
||||
.folder.open {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.folder.open .paper:nth-child(1) {
|
||||
transform: translate(-120%, -70%) rotateZ(-15deg);
|
||||
}
|
||||
|
||||
.folder.open .paper:nth-child(1):hover {
|
||||
transform: translate(-120%, -70%) rotateZ(-15deg) scale(1.1);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.folder.open .paper:nth-child(2) {
|
||||
transform: translate(10%, -70%) rotateZ(15deg);
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.folder.open .paper:nth-child(2):hover {
|
||||
transform: translate(10%, -70%) rotateZ(15deg) scale(1.1);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.folder.open .paper:nth-child(3) {
|
||||
transform: translate(-50%, -100%) rotateZ(5deg);
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.folder.open .paper:nth-child(3):hover {
|
||||
transform: translate(-50%, -100%) rotateZ(5deg) scale(1.1);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.folder.open .folder_front {
|
||||
transform: skew(15deg) scaleY(0.6);
|
||||
}
|
||||
|
||||
.folder.open .right {
|
||||
transform: skew(-15deg) scaleY(0.6);
|
||||
}
|
||||
|
||||
.folder_back {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 80px;
|
||||
background: var(--folder-back-color);
|
||||
border-radius: 0px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.folder_back::after {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
bottom: 98%;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 30px;
|
||||
height: 10px;
|
||||
background: var(--folder-back-color);
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.paper {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 10%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 10%);
|
||||
width: 70%;
|
||||
height: 80%;
|
||||
background: var(--paper-1);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.paper:nth-child(2) {
|
||||
background: var(--paper-2);
|
||||
width: 80%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.paper:nth-child(3) {
|
||||
background: var(--paper-3);
|
||||
width: 90%;
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
.folder_front {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--folder-color);
|
||||
border-radius: 5px 10px 10px 10px;
|
||||
transform-origin: bottom;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +1,27 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container overflow-hidden h-[400px]">
|
||||
<StarBorder as="button" :color="color" :speed="speedProp" :thickness="thickness">
|
||||
Star Border
|
||||
</StarBorder>
|
||||
</div>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container overflow-hidden h-[400px]">
|
||||
<StarBorder as="button" :color="color" :speed="speedProp" :thickness="thickness">Star Border</StarBorder>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSelect title="Color" v-model="color" :options="colorOptions" />
|
||||
<PreviewSlider title="Thickness" v-model="thickness" :min="0.5" :max="8" :step="0.5" value-unit="px" />
|
||||
<PreviewSlider title="Speed" v-model="speed" :min="1" :max="10" :step="0.5" value-unit="s" />
|
||||
</Customize>
|
||||
<Customize>
|
||||
<PreviewSelect title="Color" v-model="color" :options="colorOptions" />
|
||||
<PreviewSlider title="Thickness" v-model="thickness" :min="0.5" :max="8" :step="0.5" value-unit="px" />
|
||||
<PreviewSlider title="Speed" v-model="speed" :min="1" :max="10" :step="0.5" value-unit="s" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
</template>
|
||||
<PropTable :data="propData" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="starBorder" />
|
||||
</template>
|
||||
<template #code>
|
||||
<CodeExample :code-object="starBorder" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="starBorder.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
<template #cli>
|
||||
<CliInstallation :command="starBorder.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -38,46 +36,50 @@ import PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||
import StarBorder from '../../content/Animations/StarBorder/StarBorder.vue';
|
||||
import { starBorder } from '@/constants/code/Animations/starBorderCode';
|
||||
|
||||
const thickness = ref<number>(3)
|
||||
const speed = ref<number>(6)
|
||||
const speedProp = ref<string>('6s')
|
||||
const color = ref<string>('magenta')
|
||||
const colorOptions = [{ label: 'Magenta', value: "magenta" }, { label: 'Cyan', value: "cyan" }, { label: 'white', value: "white" }]
|
||||
const thickness = ref<number>(3);
|
||||
const speed = ref<number>(6);
|
||||
const speedProp = ref<string>('6s');
|
||||
const color = ref<string>('magenta');
|
||||
const colorOptions = [
|
||||
{ label: 'Magenta', value: 'magenta' },
|
||||
{ label: 'Cyan', value: 'cyan' },
|
||||
{ label: 'white', value: 'white' }
|
||||
];
|
||||
|
||||
watch(speed, () => {
|
||||
speedProp.value = (speed.value).toString() + 's'
|
||||
})
|
||||
speedProp.value = speed.value.toString() + 's';
|
||||
});
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'as',
|
||||
type: 'string',
|
||||
default: 'button',
|
||||
description: 'Allows specifying the type of the parent component to be rendered.'
|
||||
},
|
||||
{
|
||||
name: 'customClass',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Allows adding custom classes to the component.'
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
type: 'string',
|
||||
default: 'white',
|
||||
description: 'Changes the main color of the border (fades to transparent)'
|
||||
},
|
||||
{
|
||||
name: 'speed',
|
||||
type: 'string',
|
||||
default: '6s',
|
||||
description: 'Changes the speed of the animation.'
|
||||
},
|
||||
{
|
||||
name: 'thickness',
|
||||
type: 'number',
|
||||
default: '3',
|
||||
description: 'Controls the thickness of the star border effect.'
|
||||
}
|
||||
{
|
||||
name: 'as',
|
||||
type: 'string',
|
||||
default: 'button',
|
||||
description: 'Allows specifying the type of the parent component to be rendered.'
|
||||
},
|
||||
{
|
||||
name: 'customClass',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Allows adding custom classes to the component.'
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
type: 'string',
|
||||
default: 'white',
|
||||
description: 'Changes the main color of the border (fades to transparent)'
|
||||
},
|
||||
{
|
||||
name: 'speed',
|
||||
type: 'string',
|
||||
default: '6s',
|
||||
description: 'Changes the speed of the animation.'
|
||||
},
|
||||
{
|
||||
name: 'thickness',
|
||||
type: 'number',
|
||||
default: '3',
|
||||
description: 'Controls the thickness of the star border effect.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -31,21 +31,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue'
|
||||
import PropTable from '../../components/common/PropTable.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 AnimatedList from '../../content/Components/AnimatedList/AnimatedList.vue'
|
||||
import { animatedList } from '../../constants/code/Components/animatedListCode'
|
||||
import { useForceRerender } from '@/composables/useForceRerender'
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import PropTable from '../../components/common/PropTable.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 AnimatedList from '../../content/Components/AnimatedList/AnimatedList.vue';
|
||||
import { animatedList } from '../../constants/code/Components/animatedListCode';
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
|
||||
const { rerenderKey } = useForceRerender()
|
||||
const showGradients = ref(true)
|
||||
const enableArrowNavigation = ref(true)
|
||||
const displayScrollbar = ref(true)
|
||||
const { rerenderKey } = useForceRerender();
|
||||
const showGradients = ref(true);
|
||||
const enableArrowNavigation = ref(true);
|
||||
const displayScrollbar = ref(true);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
@@ -96,5 +96,5 @@ const propData = [
|
||||
default: '-',
|
||||
description: 'Emitted when an item is selected. Receives (item: string, index: number)'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container">
|
||||
<Folder :items="items" :size="size" :color="color">
|
||||
</Folder>
|
||||
<Folder :size="size" :color="color"></Folder>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
@@ -37,7 +36,6 @@ import PreviewSlider from '@/components/common/PreviewSlider.vue';
|
||||
import PropTable from '@/components/common/PropTable.vue';
|
||||
import TabbedLayout from '@/components/common/TabbedLayout.vue';
|
||||
|
||||
const items = ['Doc 1', 'Doc 2', 'Doc 3'];
|
||||
const color = ref('#5227FF');
|
||||
const size = ref(2);
|
||||
|
||||
@@ -65,6 +63,6 @@ const propData = [
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Size multiplier for the folder.'
|
||||
},
|
||||
}
|
||||
];
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user