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
35import hero from ' ./hero.yml'
46
57const 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
917function 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
2024function 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 >
0 commit comments