Skip to content

Commit b592383

Browse files
committed
feat: add animated beam component
1 parent 6442e91 commit b592383

File tree

13 files changed

+248
-27
lines changed

13 files changed

+248
-27
lines changed

docs/.vitepress/components/demo-block/src/index.vue

+53-19
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,62 @@
11
<script lang='ts' setup name="demo-block">
2+
import { Icon } from '@iconify/vue'
23
import { useClipboard, useToggle } from '@vueuse/core'
3-
import { computed } from 'vue'
4-
import DotPattern from '../../../../components/DotPattern/DotPattern.vue'
5-
import { cn } from '../../../../lib/utils'
4+
import { computed, ref } from 'vue'
5+
import { MagicString } from 'vue/compiler-sfc'
6+
import DotPattern from '../../../../src/components/spark-ui/DotPattern/DotPattern.vue'
7+
import { cn } from '../../../../src/lib/utils'
68
import { demoProps } from './index'
79
810
const props = defineProps(demoProps)
911
10-
const decodedHighlightedCode = computed(() =>
11-
decodeURIComponent(props.highlightedCode),
12-
)
12+
const decodedHighlightedCode = computed(() => {
13+
try {
14+
const decodeHighlightedCode = decodeURIComponent(props.highlightedCode)
15+
const updatedCode = updateImportPaths(decodeHighlightedCode)
16+
return updatedCode
17+
}
18+
catch (error) {
19+
console.error('Error decoding highlighted code:', error)
20+
return props.highlightedCode
21+
}
22+
})
23+
24+
function updateImportPaths(code: string): string {
25+
const magicString = new MagicString(code)
26+
27+
magicString.replaceAll('../../components/spark-ui/', '@/components/')
28+
magicString.replaceAll('../../../lib/utils', '@/libs/utils')
29+
30+
return magicString.toString()
31+
}
1332
const { copy, copied } = useClipboard({ source: decodeURIComponent(props.code) })
1433
const [value, toggle] = useToggle()
34+
const refreshKey = ref(0)
35+
function handleRefreshComponent() {
36+
refreshKey.value += 1
37+
}
1538
</script>
1639

1740
<template>
18-
<div class="mt-6">
41+
<div class="mt-6 relative">
1942
<div
20-
class="relative flex h-96 w-full bg-[#fffefe] p-6 flex-col items-center justify-center overflow-hidden rounded-lg border-parent dark:border-none bg-background md:shadow-md c-#282f38 overflow-x-scroll dark:bg-[#000000] flex-wrap [&:o-button-base]:!c-context vp-raw bg"
43+
class="relative flex h-[600px] w-full bg-[#fffefe] p-6 flex-col items-center justify-center overflow-hidden rounded-lg border-parent dark:border-none bg-background md:shadow-md c-#282f38 overflow-x-scroll dark:bg-[#000000] flex-wrap [&:o-button-base]:!c-context vp-raw bg"
2144
>
22-
<div class="w-full py-6 h-full">
23-
<div class="border-child relative rounded-md w-full h-full flex items-center justify-center dark:border-none">
24-
<p
25-
class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white"
26-
>
27-
<slot />
45+
<DotPattern
46+
class="absolute inset-0 size-full" :class="cn(
47+
'[mask-image:radial-gradient(600px_circle_at_center,white,transparent)]',
48+
)"
49+
/>
50+
<button type="button" class="absolute right-6 text-black top-2 hover:bg-[#F4F4F5] dark:hover:bg-[#27272A] p-2 rounded-md" @click="handleRefreshComponent">
51+
<Icon
52+
icon="ic:round-replay" class="text-black w-5 h-5 dark:text-white"
53+
/>
54+
</button>
55+
<div class="w-3/4 py-6">
56+
<div class="border-child bg-white shadow-lg dark:bg-black relative rounded-md w-full h-full flex items-center justify-center dark:border-none">
57+
<p class="z-10">
58+
<slot :key="refreshKey" />
2859
</p>
29-
<DotPattern
30-
:class="cn(
31-
'[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]',
32-
)"
33-
/>
3460
</div>
3561
<div class="relative">
3662
<div class="flex justify-end pt-3 gap-2">
@@ -108,4 +134,12 @@ const [value, toggle] = useToggle()
108134
.border-child {
109135
border: 1px solid rgb(239, 239, 239);
110136
}
137+
138+
.rotate-0 {
139+
transform: rotate(0deg);
140+
}
141+
142+
.rotate-45 {
143+
transform: rotate(45deg);
144+
}
111145
</style>

docs/.vitepress/theme/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MotionPlugin } from '@vueuse/motion'
12
import Theme from 'vitepress/theme'
23
import { h, watch } from 'vue'
34
import DemoBlock from '../components/demo-block'
@@ -16,6 +17,7 @@ export default {
1617
},
1718
enhanceApp({ app, router }) {
1819
app.component('Demo', DemoBlock)
20+
app.use(MotionPlugin)
1921
if (typeof window === 'undefined')
2022
return
2123

docs/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ features:
4747
link: "#"
4848
---
4949

50-
<demo src="./example/hello.vue" />
50+
<demo src="./src/example/animatedBeamDemo/Demo.vue" />

docs/package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
"docs"
1010
],
1111
"scripts": {
12-
"dev": "vitepress dev --port 5555 --open --host",
12+
"dev": "vitepress dev --port 5555",
1313
"build": "vitepress build",
1414
"serve": "vitepress serve"
1515
},
1616
"dependencies": {
17+
"@vueuse/motion": "^2.2.5",
1718
"clsx": "^2.1.1",
1819
"fs-extra": "^11.2.0",
1920
"markdown-it": "^14.1.0",
@@ -25,6 +26,9 @@
2526
"@iconify-json/fluent-emoji": "^1.2.0",
2627
"@iconify-json/logos": "^1.2.1",
2728
"@iconify/vue": "^4.1.2",
28-
"@types/markdown-it": "^13.0.9"
29+
"@tsconfig/node20": "^20.1.4",
30+
"@types/markdown-it": "^13.0.9",
31+
"@types/node": "^22.7.4",
32+
"@vue/tsconfig": "^0.5.1"
2933
}
3034
}

docs/components/DotPattern/Demo.vue docs/src/components/spark-ui/DotPattern/Demo.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang='ts'>
2-
import { cn } from '../../lib/utils'
2+
import { cn } from '../../../lib/utils'
33
import DotPattern from './DotPattern.vue'
44
</script>
55

docs/components/DotPattern/DotPattern.vue docs/src/components/spark-ui/DotPattern/DotPattern.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang='ts'>
2-
import { cn } from '../../lib/utils'
2+
import { cn } from '../../../lib/utils'
33
44
interface DotPatternProps {
55
width?: any
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script lang="ts" setup>
2+
import { computed, onMounted, ref, useSlots } from 'vue'
3+
import { cn } from '../../../lib/utils'
4+
5+
const props = defineProps({
6+
class: {
7+
type: String,
8+
default: '',
9+
},
10+
delay: {
11+
type: Number,
12+
default: 1000,
13+
},
14+
})
15+
16+
const slots = useSlots()
17+
const index = ref(0)
18+
const slotsArray = ref<any>([])
19+
20+
onMounted(loadComponents)
21+
22+
const itemsToShow = computed(() => {
23+
return slotsArray.value.slice(0, index.value)
24+
})
25+
26+
async function loadComponents() {
27+
slotsArray.value = slots.default ? slots.default()[0]?.children : []
28+
29+
while (index.value < slotsArray.value.length) {
30+
index.value++
31+
await delay(props.delay)
32+
}
33+
}
34+
35+
async function delay(ms: number) {
36+
return new Promise(resolve => setTimeout(resolve, ms))
37+
}
38+
39+
function getInitial(idx: number) {
40+
return idx === index.value - 1
41+
? {
42+
scale: 0,
43+
opacity: 0,
44+
}
45+
: undefined
46+
}
47+
function getEnter(idx: number) {
48+
return idx === index.value - 1
49+
? {
50+
scale: 1,
51+
opacity: 1,
52+
y: 0,
53+
transition: {
54+
type: 'spring',
55+
stiffness: 250,
56+
damping: 40,
57+
},
58+
}
59+
: undefined
60+
}
61+
62+
function getLeave() {
63+
return {
64+
scale: 0,
65+
opacity: 0,
66+
y: 0,
67+
transition: {
68+
type: 'spring',
69+
stiffness: 350,
70+
damping: 40,
71+
},
72+
}
73+
}
74+
</script>
75+
76+
<template>
77+
<div :class="cn('border w-[600px] h-[370px] overflow-auto rounded-lg', $props.class)">
78+
<transition-group name="animated-beam" tag="div" class="flex flex-col-reverse items-center p-2" move-class="move">
79+
<div
80+
v-for="(item, idx) in itemsToShow" :key="idx" v-motion :initial="getInitial(idx)"
81+
:enter="getEnter(idx)" :leave="getLeave()" :class="cn('mx-auto w-full mb-4')"
82+
>
83+
<component :is="item" />
84+
</div>
85+
</transition-group>
86+
</div>
87+
</template>
88+
89+
<style scoped>
90+
.move {
91+
transition: transform 0.4s ease-out;
92+
}
93+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang='ts'>
2+
import { cn } from '../../../lib/utils'
3+
4+
const props = defineProps(['name', 'description', 'icon', 'color', 'time'])
5+
6+
const className = cn(
7+
'relative mx-auto min-h-fit w-full max-w-[400px] cursor-pointer overflow-hidden rounded-2xl p-2',
8+
// animation styles
9+
'transition-all duration-200 ease-in-out hover:scale-[103%]',
10+
// light styles
11+
'bg-white [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]',
12+
// dark styles
13+
'transform-gpu dark:bg-transparent dark:backdrop-blur-md dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]',
14+
)
15+
</script>
16+
17+
<template>
18+
<figure :class="className">
19+
<div class="flex flex-row border rounded-xl items-center px-2 gap-4">
20+
<div class="flex size-10 items-center justify-center rounded-2xl" :style="{ backgroundColor: props.color }">
21+
<span class="text-lg">{{ props.icon }}</span>
22+
</div>
23+
<div class="flex flex-col space-y-1 overflow-hidden">
24+
<div class="flex flex-row items-center whitespace-pre text-lg font-medium ">
25+
<span class="text-sm text-black dark:text-white font-semibold sm:text-lg">{{ props.name }}</span>
26+
<span class="mx-1">·</span>
27+
<span class="text-xs text-gray-500 dark:text-gray-200">{{ props.time }}</span>
28+
</div>
29+
<p class="text-sm font-normal dark:text-white">
30+
{{ props.description }}
31+
</p>
32+
</div>
33+
</div>
34+
</figure>
35+
</template>
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<script setup lang='ts'>
2+
import AnimatedList from '../../components/spark-ui/animatedBeam/AnimatedList.vue'
3+
import Notification from '../../components/spark-ui/animatedBeam/Notification.vue'
4+
5+
let notifications = [
6+
{
7+
name: 'Payment received',
8+
description: 'Spark UI',
9+
time: '15m ago',
10+
icon: '💸',
11+
color: '#00C9A7',
12+
},
13+
{
14+
name: 'User signed up',
15+
description: 'Spark UI',
16+
time: '10m ago',
17+
icon: '👤',
18+
color: '#FFB800',
19+
},
20+
{
21+
name: 'New message',
22+
description: 'Spark UI',
23+
time: '5m ago',
24+
icon: '💬',
25+
color: '#FF3D71',
26+
},
27+
{
28+
name: 'New event',
29+
description: 'Spark UI',
30+
time: '2m ago',
31+
icon: '🗞️',
32+
color: '#1E86FF',
33+
},
34+
]
35+
36+
notifications = Array.from({ length: 30 }, () => notifications).flat()
37+
</script>
38+
39+
<template>
40+
<AnimatedList>
41+
<template #default>
42+
<Notification
43+
v-for="(item, index) in notifications" :key="index" :name="item.name" :description="item.description"
44+
:icon="item.icon" :color="item.color" :time="item.time"
45+
/>
46+
</template>
47+
</AnimatedList>
48+
</template>
File renamed without changes.
File renamed without changes.

docs/tsconfig.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
"compilerOptions": {
44
"baseUrl": ".",
55
"paths": {
6-
"guide": [
7-
"./guide/*"
8-
]
6+
"@/*": ["./src/*"]
97
}
108
},
119
"include": [

docs/vite.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { fileURLToPath, URL } from 'node:url'
12
import UnoCSS from 'unocss/vite'
23
import { defineConfig } from 'vite'
34

@@ -15,4 +16,10 @@ export default defineConfig({
1516
plugins: [
1617
UnoCSS(),
1718
],
19+
resolve: {
20+
alias: {
21+
find: '@',
22+
replacement: fileURLToPath(new URL('./src', import.meta.url)),
23+
},
24+
},
1825
})

0 commit comments

Comments
 (0)