mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
feat: Add folder component
This commit is contained in:
@@ -61,6 +61,7 @@ export const CATEGORIES = [
|
||||
'Spotlight Card',
|
||||
'Circular Gallery',
|
||||
'Flying Posters',
|
||||
'Folder',
|
||||
'Card Swap',
|
||||
'Infinite Scroll',
|
||||
'Tilted Card',
|
||||
|
||||
@@ -49,6 +49,7 @@ const components = {
|
||||
'spotlight-card': () => import('../demo/Components/SpotlightCardDemo.vue'),
|
||||
'circular-gallery': () => import('../demo/Components/CircularGalleryDemo.vue'),
|
||||
'flying-posters': () => import('../demo/Components/FlyingPostersDemo.vue'),
|
||||
'folder': () => import('../demo/Components/FolderDemo.vue'),
|
||||
'card-swap': () => import('../demo/Components/CardSwapDemo.vue'),
|
||||
'infinite-scroll': () => import('../demo/Components/InfiniteScrollDemo.vue'),
|
||||
'glass-icons': () => import('../demo/Components/GlassIconsDemo.vue'),
|
||||
|
||||
16
src/constants/code/Components/folderCode.ts
Normal file
16
src/constants/code/Components/folderCode.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import code from '@content/Components/Folder/Folder.vue?raw';
|
||||
import type { CodeObject } from '../../../types/code';
|
||||
|
||||
export const folder: CodeObject = {
|
||||
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/Folder`,
|
||||
usage: `<template>
|
||||
<Folder :items="items" :size="2" color="#5227FF" class="my-folder-class" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Folder from "./Folder.vue";
|
||||
|
||||
const items = ['Doc 1', 'Doc 2', 'Doc 3'];
|
||||
</script>`,
|
||||
code
|
||||
};
|
||||
274
src/content/Components/Folder/Folder.vue
Normal file
274
src/content/Components/Folder/Folder.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<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"
|
||||
>
|
||||
{{ item }}
|
||||
</slot>
|
||||
</div>
|
||||
<div class="folder_front"></div>
|
||||
<div class="folder_front right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Definir tipos
|
||||
interface PaperOffset {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
color?: string
|
||||
size?: number
|
||||
items?: (string | null)[]
|
||||
class?: string
|
||||
}
|
||||
|
||||
// Props con valores por defecto
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: '#5227FF',
|
||||
size: 1,
|
||||
items: () => [],
|
||||
class: ''
|
||||
})
|
||||
|
||||
// Función para oscurecer colores
|
||||
const darkenColor = (hex: string, percent: number): string => {
|
||||
let color = hex.startsWith('#') ? hex.slice(1) : hex
|
||||
if (color.length === 3) {
|
||||
color = color
|
||||
.split('')
|
||||
.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()
|
||||
)
|
||||
}
|
||||
|
||||
// Estado reactivo
|
||||
const open = ref(false)
|
||||
const maxItems = 3
|
||||
const paperOffsets = ref<PaperOffset[]>(
|
||||
Array.from({ length: maxItems }, () => ({ x: 0, y: 0 }))
|
||||
)
|
||||
|
||||
// Computed properties
|
||||
const papers = computed(() => {
|
||||
const result = props.items.slice(0, maxItems)
|
||||
while (result.length < maxItems) {
|
||||
result.push(null)
|
||||
}
|
||||
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 folderStyle = computed(() => ({
|
||||
'--folder-color': props.color,
|
||||
'--folder-back-color': folderBackColor.value,
|
||||
'--paper-1': paper1.value,
|
||||
'--paper-2': paper2.value,
|
||||
'--paper-3': paper3.value,
|
||||
}))
|
||||
|
||||
const folderClass = computed(() => `folder ${open.value ? 'open' : ''}`.trim())
|
||||
|
||||
const scaleStyle = computed(() => ({
|
||||
transform: `scale(${props.size})`
|
||||
}))
|
||||
|
||||
// Métodos
|
||||
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
|
||||
)
|
||||
}
|
||||
</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>
|
||||
70
src/demo/Components/FolderDemo.vue
Normal file
70
src/demo/Components/FolderDemo.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container">
|
||||
<Folder :items="items" :size="size" :color="color">
|
||||
</Folder>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewColor title="Folder Color" v-model="color" />
|
||||
<PreviewSlider title="Folder Size" v-model="size" :min="1" :max="3" :step="0.1" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="folder" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="folder.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { folder } from '@/constants/code/Components/folderCode';
|
||||
|
||||
import Folder from '@/content/Components/Folder/Folder.vue';
|
||||
import CliInstallation from '@/components/code/CliInstallation.vue';
|
||||
import CodeExample from '@/components/code/CodeExample.vue';
|
||||
import Customize from '@/components/common/Customize.vue';
|
||||
import PreviewColor from '@/components/common/PreviewColor.vue';
|
||||
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);
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'color',
|
||||
type: 'string',
|
||||
default: '#5227FF',
|
||||
description: 'The color of the folder.'
|
||||
},
|
||||
{
|
||||
name: 'class',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Additional CSS classes for the folder container.'
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
type: '(string | null)[]',
|
||||
default: '[]',
|
||||
description: 'An array of up to 3 items to display in the folder.'
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
type: 'number',
|
||||
default: '1',
|
||||
description: 'Size multiplier for the folder.'
|
||||
},
|
||||
];
|
||||
</script>
|
||||
Reference in New Issue
Block a user