Skip to content

Commit 33b9dcf

Browse files
committed
feat(docs): animate hero tabs with motion-v
1 parent 9c7aa2f commit 33b9dcf

File tree

5 files changed

+640
-265
lines changed

5 files changed

+640
-265
lines changed

docs/app/components/content/landing/LandingHero.vue

Lines changed: 120 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
<script setup lang="ts">
2+
import { motion, AnimatePresence, MotionConfig } from 'motion-v'
3+
import { useElementSize, useClipboard } from '@vueuse/core'
24
// @ts-expect-error yaml is not typed
35
import hero from './hero.yml'
46
57
const currentTab = ref(0)
6-
const tabs = hero.tabs as { name: string, code: string, lang: string }[]
8+
const contentRef = ref<HTMLElement | null>(null)
9+
const { height } = useElementSize(contentRef)
10+
const { copy, copied } = useClipboard()
11+
12+
const tabs = hero.tabs as { name: string, code: string }[]
13+
14+
const currentCode = computed(() => tabs[currentTab.value]?.code.trim() ?? '')
15+
const lineCount = computed(() => currentCode.value.split('\n').length)
716
8-
// Map file extensions to languages
917
function getLang(filename: string) {
10-
if (filename.endsWith('.ts'))
11-
return 'ts'
12-
if (filename.endsWith('.vue'))
13-
return 'vue'
14-
if (filename.endsWith('.js'))
15-
return 'js'
18+
if (filename.endsWith('.ts')) return 'ts'
19+
if (filename.endsWith('.vue')) return 'vue'
20+
if (filename.endsWith('.js')) return 'js'
1621
return 'ts'
1722
}
1823
19-
// Format code as markdown code block for MDC
2024
function getCodeBlock(tab: { name: string, code: string }) {
2125
return `\`\`\`${getLang(tab.name)}\n${tab.code.trim()}\n\`\`\``
2226
}
@@ -29,10 +33,18 @@ function getCodeBlock(tab: { name: string, code: string }) {
2933

3034
<!-- Background Grid -->
3135
<div class="absolute inset-0 left-5 right-5 lg:left-16 lg:right-14 xl:left-16 xl:right-14">
32-
<div class="absolute inset-0 bg-grid text-stone-500/50 dark:text-white/[0.02]" />
36+
<div class="absolute inset-0 bg-grid text-stone-100 dark:text-white/[0.02]" />
3337
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[--ui-bg]" />
3438
</div>
3539

40+
<!-- Vertical lines on sides -->
41+
<div class="hidden absolute top-0 left-5 w-px h-[calc(100%_+_30px)] bg-stone-200 dark:bg-[#26242C] pointer-events-none lg:block lg:left-16 xl:left-16" />
42+
<div class="hidden absolute top-0 right-5 w-px h-[calc(100%_+_30px)] bg-stone-200 dark:bg-[#26242C] pointer-events-none lg:block lg:right-14 xl:right-14" />
43+
44+
<!-- Plus icons at top of lines -->
45+
<UIcon name="i-lucide-plus" class="hidden absolute top-[4.5rem] size-6 left-[3.275rem] pointer-events-none lg:block text-neutral-300 dark:text-neutral-600" />
46+
<UIcon name="i-lucide-plus" class="hidden absolute top-[4.5rem] size-6 right-[2.775rem] pointer-events-none lg:block text-neutral-300 dark:text-neutral-600" />
47+
3648
<!-- Content -->
3749
<div class="px-4 py-8 md:w-10/12 mx-auto relative z-10">
3850
<div class="mx-auto grid lg:max-w-8xl xl:max-w-full grid-cols-1 items-center gap-x-8 gap-y-16 px-4 py-2 lg:grid-cols-2 lg:px-8 lg:py-4 xl:gap-x-16 xl:px-0">
@@ -53,8 +65,8 @@ function getCodeBlock(tab: { name: string, code: string }) {
5365
</div>
5466

5567
<!-- npm install command -->
56-
<div class="relative flex items-center gap-2 w-full sm:w-[90%]">
57-
<div class="gradient-box w-full flex items-center justify-between gap-2 px-3 py-2 rounded-sm">
68+
<div class="relative flex items-center gap-2 w-full sm:w-[90%] border border-stone-200/50 dark:border-white/10">
69+
<div class="relative w-full flex items-center justify-between gap-2 px-3 py-2 rounded-sm z-10 bg-stone-50 dark:bg-zinc-950">
5870
<div class="w-full flex flex-col min-[350px]:flex-row min-[350px]:items-center gap-0.5 min-[350px]:gap-2 min-w-0">
5971
<p class="text-xs sm:text-sm font-mono select-none tracking-tighter space-x-1 shrink-0">
6072
<span class="text-sky-500">git:</span><span class="text-red-400">(main)</span>
@@ -99,44 +111,107 @@ function getCodeBlock(tab: { name: string, code: string }) {
99111
<!-- Right: Code preview -->
100112
<div class="relative md:block lg:static xl:pl-10">
101113
<div class="relative">
102-
<div class="from-sky-300 via-sky-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-5 blur-lg" />
103-
<div class="from-stone-300 via-stone-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-5" />
114+
<div class="from-sky-300 via-sky-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-0 dark:opacity-5 blur-lg" />
115+
<div class="from-stone-300 via-stone-300/70 to-blue-300 absolute inset-0 rounded-none bg-gradient-to-tr opacity-0 dark:opacity-5" />
104116

105117
<!-- Code Preview Card -->
106-
<div class="code-preview relative overflow-hidden rounded-sm ring-1 ring-white/10 backdrop-blur-lg">
107-
<div class="pl-4 pt-4">
108-
<!-- Traffic lights -->
109-
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" class="h-2.5 w-auto stroke-slate-500/30">
110-
<circle cx="5" cy="5" r="4.5" />
111-
<circle cx="21" cy="5" r="4.5" />
112-
<circle cx="37" cy="5" r="4.5" />
113-
</svg>
114-
115-
<!-- Tabs -->
116-
<div class="mt-4 flex space-x-2 text-xs">
117-
<button
118-
v-for="(tab, index) in tabs"
119-
:key="tab.name"
120-
class="relative isolate flex h-6 cursor-pointer items-center justify-center rounded-full px-2.5 transition-colors"
121-
:class="currentTab === index ? 'text-stone-300' : 'text-slate-500'"
122-
@click="currentTab = index"
123-
>
124-
{{ tab.name }}
125-
<span
126-
v-if="currentTab === index"
127-
class="absolute inset-0 -z-10 rounded-full bg-stone-800"
128-
/>
129-
</button>
130-
</div>
118+
<MotionConfig :transition="{ duration: 0.5, type: 'spring', bounce: 0 }">
119+
<motion.div
120+
:animate="{ height: height > 0 ? height : undefined }"
121+
class="code-preview relative overflow-hidden rounded-sm ring-1 ring-white/10 backdrop-blur-lg"
122+
>
123+
<div ref="contentRef">
124+
<div class="absolute -top-px left-0 right-0 h-px" />
125+
<div class="absolute -bottom-px left-11 right-20 h-px" />
126+
<div class="pl-4 pt-4">
127+
<!-- Traffic lights -->
128+
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" class="h-2.5 w-auto stroke-slate-500/30">
129+
<circle cx="5" cy="5" r="4.5" />
130+
<circle cx="21" cy="5" r="4.5" />
131+
<circle cx="37" cy="5" r="4.5" />
132+
</svg>
131133

132-
<!-- Code content -->
133-
<div class="flex flex-col items-start px-1 text-sm pb-4">
134-
<div class="w-full overflow-x-auto hero-code">
135-
<MDC :value="getCodeBlock(tabs[currentTab])" tag="div" />
134+
<!-- Tabs with layoutId animation -->
135+
<div class="mt-4 flex space-x-2 text-xs">
136+
<button
137+
v-for="(tab, index) in tabs"
138+
:key="tab.name"
139+
class="relative isolate flex h-6 cursor-pointer items-center justify-center rounded-full px-2.5 transition-colors"
140+
:class="currentTab === index ? 'text-stone-300' : 'text-slate-500'"
141+
@click="currentTab = index"
142+
>
143+
{{ tab.name }}
144+
<motion.div
145+
v-if="currentTab === index"
146+
layoutId="tab-code-preview"
147+
class="bg-stone-800 absolute inset-0 -z-10 rounded-full"
148+
/>
149+
</button>
150+
</div>
151+
152+
<!-- Code content area -->
153+
<div class="flex flex-col items-start px-1 text-sm">
154+
<!-- Copy button (top-right) -->
155+
<div class="absolute top-2 right-4">
156+
<UButton
157+
variant="outline"
158+
size="xs"
159+
class="border-none bg-transparent size-5"
160+
:aria-label="copied ? 'Copied' : 'Copy code'"
161+
@click="copy(currentCode)"
162+
>
163+
<UIcon :name="copied ? 'i-lucide-check' : 'i-lucide-copy'" class="size-3" />
164+
<span class="sr-only">Copy code</span>
165+
</UButton>
166+
</div>
167+
168+
<div class="w-full overflow-x-auto">
169+
<AnimatePresence mode="wait">
170+
<motion.div
171+
:key="currentTab"
172+
:initial="{ opacity: 0 }"
173+
:animate="{ opacity: 1 }"
174+
:exit="{ opacity: 0 }"
175+
:transition="{ duration: 0.5 }"
176+
class="relative flex items-start px-1 text-sm min-w-max"
177+
>
178+
<!-- Line numbers gutter -->
179+
<div
180+
aria-hidden="true"
181+
class="border-slate-300/5 text-slate-600 select-none border-r pr-4 font-mono"
182+
>
183+
<div v-for="i in lineCount" :key="i">
184+
{{ String(i).padStart(2, '0') }}
185+
</div>
186+
</div>
187+
188+
<!-- Code via MDC -->
189+
<div class="hero-code pl-4">
190+
<MDC :value="getCodeBlock(tabs[currentTab]!)" tag="div" />
191+
</div>
192+
</motion.div>
193+
</AnimatePresence>
194+
</div>
195+
196+
<!-- Demo CTA (bottom-right) -->
197+
<motion.div layout class="self-end mt-3">
198+
<NuxtLink
199+
to="http://demo.nuxt-better-auth.onmax.me/"
200+
target="_blank"
201+
class="shadow-md border dark:border-stone-700 border-stone-300 mb-4 ml-auto mr-4 mt-auto flex cursor-pointer items-center gap-2 px-3 py-1 transition-all ease-in-out hover:opacity-70"
202+
>
203+
<!-- Pixel art play icon -->
204+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
205+
<path fill="currentColor" d="M10 20H8V4h2v2h2v3h2v2h2v2h-2v2h-2v3h-2z" />
206+
</svg>
207+
<p class="text-sm">Demo</p>
208+
</NuxtLink>
209+
</motion.div>
210+
</div>
136211
</div>
137212
</div>
138-
</div>
139-
</div>
213+
</motion.div>
214+
</MotionConfig>
140215
</div>
141216
</div>
142217
</div>

docs/nuxt.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import yaml from '@rollup/plugin-yaml'
22

33
export default defineNuxtConfig({
44
extends: ['docus'],
5+
modules: ['motion-v/nuxt'],
56

67
css: ['~/assets/css/main.css'],
78

@@ -23,7 +24,7 @@ export default defineNuxtConfig({
2324

2425
mdc: {
2526
highlight: {
26-
theme: { default: 'vitesse-dark', dark: 'vitesse-dark', light: 'vitesse-light' },
27+
theme: { default: 'synthwave-84', dark: 'synthwave-84', light: 'one-light' },
2728
langs: ['bash', 'json', 'js', 'ts', 'vue', 'html', 'css', 'yaml', 'sql'],
2829
},
2930
},

docs/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
},
1111
"dependencies": {
1212
"@nuxt/content": "^3.7.1",
13-
"@nuxt/ui": "^4.0.0",
14-
"@vercel/analytics": "^1.4.0",
13+
"@nuxt/ui": "^4.2.1",
14+
"@vercel/analytics": "^1.6.1",
15+
"@vercel/speed-insights": "^1.3.1",
16+
"@vueuse/core": "^14.1.0",
17+
"motion-v": "^1.7.4",
1518
"nuxt": "^4.2.2"
1619
},
1720
"devDependencies": {

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
],
8787
"patchedDependencies": {
8888
"@peculiar/x509@1.14.2": "patches/@peculiar__x509@1.14.2.patch"
89+
},
90+
"overrides": {
91+
"reka-ui": "^2.6.1"
8992
}
9093
}
9194
}

0 commit comments

Comments
 (0)