|
1 | 1 | <script setup lang="ts"> |
2 | | -import { useElementSize } from '@vueuse/core' |
3 | | -import { motion, MotionConfig } from 'motion-v' |
| 2 | +import { useClipboard, useElementSize } from '@vueuse/core' |
| 3 | +import { AnimatePresence, motion, MotionConfig } from 'motion-v' |
4 | 4 | // @ts-expect-error yaml is not typed |
5 | 5 | import hero from './hero.yml' |
6 | 6 |
|
7 | 7 | const currentTab = ref(0) |
8 | 8 | const contentRef = ref<HTMLElement | null>(null) |
9 | 9 | const { height } = useElementSize(contentRef) |
| 10 | +const { copy, copied } = useClipboard() |
10 | 11 |
|
11 | 12 | const tabs = hero.tabs as { name: string, code: string }[] |
12 | 13 |
|
@@ -132,10 +133,10 @@ function getCodeBlock(tab: { name: string, code: string }) { |
132 | 133 | <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" /> |
133 | 134 |
|
134 | 135 | <!-- Code Preview Card --> |
135 | | - <MotionConfig :transition="{ duration: 0.15, type: 'spring', bounce: 0 }"> |
| 136 | + <MotionConfig :transition="{ duration: 0.5, type: 'spring', bounce: 0 }"> |
136 | 137 | <motion.div |
137 | 138 | :animate="{ height: height > 0 ? height : undefined }" |
138 | | - class="code-preview relative overflow-hidden rounded-sm backdrop-blur-lg" |
| 139 | + class="code-preview relative overflow-hidden rounded-sm ring-1 ring-white/10 backdrop-blur-lg" |
139 | 140 | > |
140 | 141 | <div ref="contentRef"> |
141 | 142 | <div class="absolute -top-px left-0 right-0 h-px" /> |
@@ -167,24 +168,47 @@ function getCodeBlock(tab: { name: string, code: string }) { |
167 | 168 | </div> |
168 | 169 |
|
169 | 170 | <!-- Code content area --> |
170 | | - <div class="flex flex-col items-start px-1 text-sm mt-4"> |
| 171 | + <div class="flex flex-col items-start px-1 text-sm"> |
| 172 | + <!-- Copy button (top-right) --> |
| 173 | + <div class="absolute top-2 right-4"> |
| 174 | + <UButton |
| 175 | + variant="outline" |
| 176 | + size="xs" |
| 177 | + class="border-none bg-transparent size-5" |
| 178 | + :aria-label="copied ? 'Copied' : 'Copy code'" |
| 179 | + @click="copy(currentCode)" |
| 180 | + > |
| 181 | + <UIcon :name="copied ? 'i-lucide-check' : 'i-lucide-copy'" class="size-3" /> |
| 182 | + <span class="sr-only">Copy code</span> |
| 183 | + </UButton> |
| 184 | + </div> |
| 185 | + |
171 | 186 | <div class="w-full overflow-x-auto"> |
172 | | - <div class="relative flex items-start px-1 text-sm min-w-max"> |
173 | | - <!-- Line numbers gutter --> |
174 | | - <div |
175 | | - aria-hidden="true" |
176 | | - class="text-slate-600 select-none pl-2 pr-4 font-mono text-xs sm:text-sm leading-6" |
| 187 | + <AnimatePresence mode="wait"> |
| 188 | + <motion.div |
| 189 | + :key="currentTab" |
| 190 | + :initial="{ opacity: 0 }" |
| 191 | + :animate="{ opacity: 1 }" |
| 192 | + :exit="{ opacity: 0 }" |
| 193 | + :transition="{ duration: 0.5 }" |
| 194 | + class="relative flex items-start px-1 text-sm min-w-max" |
177 | 195 | > |
178 | | - <div v-for="i in lineCount" :key="i"> |
179 | | - {{ String(i).padStart(2, '0') }} |
| 196 | + <!-- Line numbers gutter --> |
| 197 | + <div |
| 198 | + aria-hidden="true" |
| 199 | + class="border-slate-300/5 text-slate-600 select-none border-r pr-4 font-mono" |
| 200 | + > |
| 201 | + <div v-for="i in lineCount" :key="i"> |
| 202 | + {{ String(i).padStart(2, '0') }} |
| 203 | + </div> |
180 | 204 | </div> |
181 | | - </div> |
182 | 205 |
|
183 | | - <!-- Code via MDC --> |
184 | | - <div class="hero-code"> |
185 | | - <MDC :value="getCodeBlock(tabs[currentTab]!)" tag="div" /> |
186 | | - </div> |
187 | | - </div> |
| 206 | + <!-- Code via MDC --> |
| 207 | + <div class="hero-code pl-4"> |
| 208 | + <MDC :value="getCodeBlock(tabs[currentTab]!)" tag="div" /> |
| 209 | + </div> |
| 210 | + </motion.div> |
| 211 | + </AnimatePresence> |
188 | 212 | </div> |
189 | 213 |
|
190 | 214 | <!-- Demo CTA (bottom-right) --> |
|
0 commit comments