Skip to content

Commit 4f51d19

Browse files
docs: add landing page (#3448)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
1 parent 196ffbc commit 4f51d19

File tree

16 files changed

+917
-141
lines changed

16 files changed

+917
-141
lines changed

docs/app/components/Footer.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script setup lang="ts">
2+
const route = useRoute()
3+
24
const links = [{
35
label: 'Figma',
46
to: '/figma'
@@ -16,7 +18,7 @@ const links = [{
1618
</script>
1719

1820
<template>
19-
<USeparator icon="i-simple-icons-nuxtdotjs" class="h-px" />
21+
<USeparator :icon="route.path === '/' ? undefined : 'i-simple-icons-nuxtdotjs'" class="h-px" />
2022

2123
<UFooter>
2224
<template #left>

docs/app/components/SkyBg.vue

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<script setup lang="ts">
2+
interface Star {
3+
x: number
4+
y: number
5+
size: number
6+
twinkleDelay: number
7+
id: string
8+
}
9+
10+
const props = withDefaults(defineProps<{
11+
starCount?: number
12+
color?: string
13+
size?: { min: number, max: number }
14+
speed?: 'slow' | 'normal' | 'fast'
15+
}>(), {
16+
starCount: 50,
17+
color: 'var(--ui-primary)',
18+
size: () => ({
19+
min: 1,
20+
max: 3
21+
}),
22+
speed: 'normal'
23+
})
24+
25+
// Generate random stars
26+
const generateStars = (count: number): Star[] => {
27+
return Array.from({ length: count }, () => {
28+
const x = Math.floor(Math.random() * 100)
29+
const y = Math.floor(Math.random() * 100)
30+
const size = Math.random() * (props.size.max - props.size.min) + props.size.min
31+
const twinkleDelay = Math.random() * 5
32+
33+
return { x, y, size, twinkleDelay, id: Math.random().toString(36).substring(2, 9) }
34+
})
35+
}
36+
37+
// Generate all stars
38+
const stars = ref<Star[]>(generateStars(props.starCount))
39+
40+
// Compute twinkle animation duration based on speed
41+
const twinkleDuration = computed(() => {
42+
const speedMap: Record<string, string> = {
43+
slow: '4s',
44+
normal: '2s',
45+
fast: '1s'
46+
}
47+
return speedMap[props.speed]
48+
})
49+
</script>
50+
51+
<template>
52+
<div class="absolute pointer-events-none z-[-1] inset-y-0 left-4 right-4 lg:right-[50%] overflow-hidden">
53+
<ClientOnly>
54+
<div
55+
v-for="star in stars"
56+
:key="star.id"
57+
class="star absolute"
58+
:style="{
59+
'left': `${star.x}%`,
60+
'top': `${star.y}%`,
61+
'transform': 'translate(-50%, -50%)',
62+
'--star-size': `${star.size}px`,
63+
'--star-color': color,
64+
'--twinkle-delay': `${star.twinkleDelay}s`,
65+
'--twinkle-duration': twinkleDuration
66+
}"
67+
/>
68+
</ClientOnly>
69+
</div>
70+
</template>
71+
72+
<style scoped>
73+
.star {
74+
width: var(--star-size);
75+
height: var(--star-size);
76+
background-color: var(--star-color);
77+
border-radius: 50%;
78+
animation: twinkle var(--twinkle-duration) ease-in-out infinite;
79+
animation-delay: var(--twinkle-delay);
80+
will-change: opacity;
81+
}
82+
83+
@keyframes twinkle {
84+
0%, 100% {
85+
opacity: 0.2;
86+
}
87+
50% {
88+
opacity: 1;
89+
}
90+
}
91+
</style>

docs/app/components/StarsBg.vue

+95-32
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<script setup lang="ts">
2+
const props = withDefaults(defineProps<{
3+
contributors?: {
4+
username: string
5+
}[]
6+
level?: number
7+
max?: number
8+
paused?: boolean
9+
}>(), {
10+
level: 0,
11+
max: 4,
12+
paused: false
13+
})
14+
15+
const contributors = computed(() => props.contributors?.slice(0, 5) ?? [])
16+
17+
const el = ref(null)
18+
const { width } = useElementSize(el)
19+
</script>
20+
21+
<template>
22+
<div
23+
class="isolate rounded-full relative circle w-full aspect-[1/1] p-8 sm:p-12 md:p-14 lg:p-10 xl:p-16 before:absolute before:inset-px before:bg-(--ui-bg) before:rounded-full z-(--level)"
24+
:class="{ 'animation-paused': paused }"
25+
:style="{
26+
'--duration': `${((level + 1) * 8)}s`,
27+
'--level': level + 1
28+
}"
29+
>
30+
<HomeContributors
31+
v-if="(level + 1) < max"
32+
:max="max"
33+
:level="level + 1"
34+
:contributors="props.contributors?.slice(5) ?? []"
35+
:paused="paused"
36+
/>
37+
38+
<div
39+
ref="el"
40+
class="avatars absolute inset-0 grid"
41+
:style="{
42+
'--total': contributors.length,
43+
'--offset': `${width / 2}px`
44+
}"
45+
>
46+
<UTooltip
47+
v-for="(contributor, index) in contributors"
48+
:key="contributor.username"
49+
:text="contributor.username"
50+
:delay-duration="0"
51+
>
52+
<NuxtLink
53+
:to="`https://github.com/${contributor.username}`"
54+
:aria-label="contributor.username"
55+
target="_blank"
56+
class="avatar flex absolute top-1/2 left-1/2"
57+
tabindex="-1"
58+
:style="{
59+
'--index': index + 1
60+
}"
61+
>
62+
<img
63+
width="56"
64+
height="56"
65+
:src="`https://ipx.nuxt.com/s_56x56/gh_avatar/${contributor.username}`"
66+
:srcset="`https://ipx.nuxt.com/s_112x112/gh_avatar/${contributor.username} 2x`"
67+
:alt="contributor.username"
68+
class="ring-2 ring-(--ui-border) lg:hover:ring-(--ui-border-inverted) transition rounded-full size-7"
69+
loading="lazy"
70+
>
71+
</NuxtLink>
72+
</UTooltip>
73+
</div>
74+
</div>
75+
</template>
76+
77+
<style scoped>
78+
.circle:after {
79+
--start: 0deg;
80+
--end: 360deg;
81+
--border-color: var(--ui-border);
82+
--highlight-color: var(--ui-color-neutral-400);
83+
84+
content: '';
85+
position: absolute;
86+
top: 0;
87+
left: 0;
88+
right: 0;
89+
bottom: 0;
90+
margin: -1px;
91+
opacity: 1;
92+
border-radius: 9999px;
93+
z-index: -1;
94+
background: var(--border-color);
95+
96+
@supports (background: paint(houdini)) {
97+
background: linear-gradient(var(--angle), var(--border-color), var(--border-color), var(--border-color), var(--border-color), var(--highlight-color));
98+
animation: var(--duration) rotate linear infinite;
99+
}
100+
}
101+
102+
.dark .circle:after {
103+
--highlight-color: var(--color-white);
104+
}
105+
106+
.animation-paused.circle:after,
107+
.animation-paused .avatars {
108+
animation-play-state: paused;
109+
}
110+
111+
.avatars {
112+
--start: calc(var(--level) * 36deg);
113+
--end: calc(360deg + (var(--level) * 36deg));
114+
transform: rotate(var(--angle));
115+
animation: calc(var(--duration) + 60s) rotate linear infinite;
116+
}
117+
118+
.avatar {
119+
--deg: calc(var(--index) * (360deg / var(--total)));
120+
--transformX: calc(cos(var(--deg)) * var(--offset));
121+
--transformY: calc(sin(var(--deg)) * var(--offset));
122+
transform: translate(calc(-50% + var(--transformX)), calc(-50% + var(--transformY))) rotate(calc(360deg - var(--angle)));
123+
}
124+
125+
@keyframes rotate {
126+
from {
127+
--angle: var(--start);
128+
}
129+
to {
130+
--angle: var(--end);
131+
}
132+
}
133+
134+
@property --angle {
135+
syntax: '<angle>';
136+
initial-value: 0deg;
137+
inherits: true;
138+
}
139+
</style>

docs/app/composables/useLinks.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,17 @@ export function useLinks() {
1212
to: '/components',
1313
active: route.path === '/components',
1414
children: [{
15-
label: 'Layout',
16-
to: '/components#layout',
17-
description: 'Container, grid, divider and responsive layout.',
18-
icon: 'i-lucide-layout',
19-
active: route.fullPath === '/components#layout'
15+
label: 'Element',
16+
to: '/components#element',
17+
description: 'Button, badge, icon, alert, and small UI elements.',
18+
icon: 'i-lucide-mouse-pointer',
19+
active: route.fullPath === '/components#element'
2020
}, {
2121
label: 'Form',
2222
to: '/components#form',
2323
description: 'Input, select, checkbox, radio and form validation.',
2424
icon: 'i-lucide-text-cursor-input',
2525
active: route.fullPath === '/components#form'
26-
}, {
27-
label: 'Element',
28-
to: '/components#element',
29-
description: 'Button, badge, icon, alert, and small UI elements.',
30-
icon: 'i-lucide-mouse-pointer',
31-
active: route.fullPath === '/components#element'
3226
}, {
3327
label: 'Data',
3428
to: '/components#data',
@@ -47,6 +41,12 @@ export function useLinks() {
4741
description: 'Modal, tooltip, dialog and popover.',
4842
icon: 'i-lucide-layers',
4943
active: route.fullPath === '/components#overlay'
44+
}, {
45+
label: 'Layout',
46+
to: '/components#layout',
47+
description: 'Container, grid, divider and responsive layout.',
48+
icon: 'i-lucide-layout',
49+
active: route.fullPath === '/components#layout'
5050
}]
5151
}, {
5252
label: 'Pro',

docs/app/layouts/default.vue

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
<template>
2-
<div>
3-
<slot />
4-
</div>
2+
<slot />
53
</template>

0 commit comments

Comments
 (0)