Code Snippet Playground
blockdevopsMulti-file interactive code runner, editor tabs, and terminal console output for components with live render sandbox, syntax highlighting, and test execution.
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/code-snippet-playground.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/code-snippet-playground.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/code-snippet-playground.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/code-snippet-playground.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/code-snippet-playgroundInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TestLoginterface TestLog {
id: string
name: string
duration: string
status: 'pass' | 'fail'
}CodeFileinterface CodeFile {
id: string
name: string
path: string
language: string
size: string
content: string
}Files installed (2)
app/components/blocks/CodeSnippetPlayground.vue25.2 kB
<script setup lang="ts"> import { computed, onMounted, onUnmounted, ref } from 'vue' import type { HTMLAttributes } from 'vue' import { Braces, Check, CheckCircle2, Code2, Copy, Eye, FileCode, Layers, Loader2, Play, RotateCcw, Terminal, Zap, } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { files } from './code-snippet-playground-files' interface TestLog { id: string name: string duration: string status: 'pass' | 'fail' } const props = defineProps<{ class?: HTMLAttributes['class'] }>() const testLogs: TestLog[] = [ { id: '1', name: 'renders default button with slot text', duration: '4ms', status: 'pass' }, { id: '2', name: 'applies variant classes correctly', duration: '7ms', status: 'pass' }, { id: '3', name: 'applies size styles correctly', duration: '5ms', status: 'pass' }, { id: '4', name: 'handles click events when active', duration: '8ms', status: 'pass' }, { id: '5', name: 'respects disabled state attribute', duration: '6ms', status: 'pass' }, { id: '6', name: 'forwards custom class names with cn()', duration: '12ms', status: 'pass' }, ] // --- State --- const activeFileId = ref('button-vue') const activeRightTab = ref<'preview' | 'console'>('preview') const hoveredLine = ref<number | null>(null) const isCopied = ref(false) const isSnippetCopied = ref(false) const isRunning = ref(false) const lastRunTime = ref('14:32:08') const executionCount = ref(1) // Interactive Preview Sandbox State type ButtonVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' type ButtonSize = 'default' | 'sm' | 'lg' | 'icon' const selectedVariant = ref<ButtonVariant>('default') const selectedSize = ref<ButtonSize>('default') const isDisabled = ref(false) const isLoading = ref(false) const withIcon = ref(true) const clickCount = ref(0) const lastInteraction = ref('Ready for test interactions') // --- Computed --- const activeFile = computed(() => { return files.find((f) => f.id === activeFileId.value) || files[0] }) const activeFileLines = computed(() => { return activeFile.value.content.split('\n') }) const generatedSnippet = computed(() => { const v = selectedVariant.value !== 'default' ? ` variant="${selectedVariant.value}"` : '' const s = selectedSize.value !== 'default' ? ` size="${selectedSize.value}"` : '' const d = isDisabled.value ? ' disabled' : '' if (selectedSize.value === 'icon') { return `<Button${v}${s}${d}>\n <Zap class="size-4" />\n</Button>` } if (isLoading.value) { return `<Button${v}${s}${d}>\n <Loader2 class="size-4 animate-spin" />\n Please wait\n</Button>` } if (withIcon.value) { return `<Button${v}${s}${d}>\n <Zap class="size-4" />\n Interactive Button\n</Button>` } return `<Button${v}${s}${d}>Interactive Button</Button>` }) // --- Syntax Highlighting Engine --- function highlightSyntaxLine(line: string): string { if (!line.trim()) return ' ' const escaped = line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') // Single line comments if (/^\s*\/\//.test(escaped) || /^\s*\/\*/.test(escaped) || /^\s*\*/.test(escaped) || /^\s*<!--/.test(escaped)) { return `<span class="text-zinc-500 italic">${escaped}</span>` } // Every emitted <span> is parked behind a letter-only placeholder so later // passes cannot match inside the markup they already produced. const parked: string[] = [] const park = (html: string) => { const key = String(parked.length) .split('') .map((d) => String.fromCharCode(97 + Number(d))) .join('') parked.push(html) return `\u0000${key}\u0000` } let out = escaped // Strings out = out.replace(/(["'`])(?:(?=(\\?))\2.)*?\1/g, (m) => park(`<span class="text-success font-normal">${m}</span>`)) // Trailing comments out = out.replace(/(\/\/.*$)/, (m) => park(`<span class="text-zinc-500 italic">${m}</span>`)) // Keywords out = out.replace( /\b(import|export|from|const|let|var|function|return|interface|type|default|as|typeof|withDefaults|defineProps|defineEmits|describe|it|expect|test|async|await|extends|new|true|false|null|undefined)\b/g, (m) => park(`<span class="text-chart-1 font-semibold">${m}</span>`), ) // Types out = out.replace( /\b(string|boolean|number|void|HTMLAttributes|VariantProps|ButtonVariants|ButtonProps|Props|Variant|Size|HTMLButtonElement|Record)\b/g, (m) => park(`<span class="text-warning font-medium">${m}</span>`), ) // Functions / methods out = out.replace( /\b(cn|buttonVariants|cva|mount|render|screen|getByRole|fireEvent|vi|trigger|classes|attributes|emitted|toBe|toContain|toHaveProperty|toBeDefined|toBeInTheDocument|toHaveAttribute|toHaveBeenCalledTimes|toHaveBeenCalled|forwardRef|displayName|ref|computed|onMounted|onUnmounted|fn)\b/g, (m) => park(`<span class="text-info">${m}</span>`), ) // Vue/TSX tags out = out.replace( /(<\/?(?:template|script|Primitive|Button|Slot|Comp|slot|div|span|button)\b(?:\s|\/|>)?)/g, (m) => park(`<span class="text-info font-medium">${m}</span>`), ) // Attributes / directives out = out.replace( /(\b(?:data-slot|data-variant|data-size|className|variant|size|asChild|as|type|class|lang|setup|ref|onClick|disabled)\b|:[a-zA-Z0-9_-]+)/g, (m) => park(`<span class="text-chart-2">${m}</span>`), ) // Numbers out = out.replace(/\b(\d+)\b/g, (m) => park(`<span class="text-warning">${m}</span>`)) // Restore every parked span return out.replace(/\u0000([a-j]+)\u0000/g, (_, key: string) => { const idx = Number( key .split('') .map((c: string) => String(c.charCodeAt(0) - 97)) .join(''), ) return parked[idx] ?? '' }) } // --- Action Handlers --- function handleRunCode() { if (isRunning.value) return isRunning.value = true activeRightTab.value = 'console' setTimeout(() => { isRunning.value = false executionCount.value++ const now = new Date() lastRunTime.value = now.toTimeString().split(' ')[0] }, 480) } function handleCopyFile() { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(activeFile.value.content) isCopied.value = true setTimeout(() => { isCopied.value = false }, 2000) } } function handleCopySnippet() { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(generatedSnippet.value) isSnippetCopied.value = true setTimeout(() => { isSnippetCopied.value = false }, 2000) } } function handleResetPlayground() { activeFileId.value = 'button-vue' selectedVariant.value = 'default' selectedSize.value = 'default' isDisabled.value = false isLoading.value = false withIcon.value = true clickCount.value = 0 lastInteraction.value = 'Playground reset to initial state' } function handleButtonClick() { if (isDisabled.value || isLoading.value) return clickCount.value++ lastInteraction.value = `Dispatched onClick event #${clickCount.value} at ${new Date().toLocaleTimeString()}` } function handleKeyDown(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault() handleRunCode() } } onMounted(() => { if (typeof window !== 'undefined') { window.addEventListener('keydown', handleKeyDown) } }) onUnmounted(() => { if (typeof window !== 'undefined') { window.removeEventListener('keydown', handleKeyDown) } }) </script> <template> <Card :class="cn('border-border bg-card w-full overflow-hidden shadow-xs', props.class)" data-slot="code-snippet-playground" > <!-- Top Toolbar --> <CardHeader class="border-border bg-muted/20 flex flex-col gap-3 border-b p-4 sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-lg"> <Code2 class="size-4.5" /> </div> <div> <div class="flex items-center gap-2"> <CardTitle class="text-base font-semibold tracking-tight">Button.vue · Reka UI Primitive</CardTitle> <Badge variant="outline" class="text-muted-foreground font-mono text-xs"> Vue 3.5 </Badge> <Badge variant="secondary" class="font-mono text-xs"> TypeScript 5.6 </Badge> </div> <CardDescription class="text-muted-foreground text-xs"> Multi-file component studio with live preview, syntax engine, and Vitest suite </CardDescription> </div> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2"> <div class="border-border/80 bg-background/80 flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs"> <span :class="cn('size-2 rounded-full transition-colors', isRunning ? 'bg-warning animate-pulse' : 'bg-success')" /> <span class="text-muted-foreground font-mono"> {{ isRunning ? 'Running suite...' : '6 tests passing' }} </span> </div> <Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs shadow-none" title="Reset Playground" @click="handleResetPlayground" > <RotateCcw class="size-3.5" /> <span class="hidden sm:inline">Reset</span> </Button> <Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs shadow-none" title="Copy active file contents" @click="handleCopyFile" > <Check v-if="isCopied" class="text-success size-3.5" /> <Copy v-else class="size-3.5" /> <span>{{ isCopied ? 'Copied!' : 'Copy File' }}</span> </Button> <Button size="sm" class="h-8 gap-1.5 text-xs font-medium" :disabled="isRunning" @click="handleRunCode"> <Loader2 v-if="isRunning" class="size-3.5 animate-spin" /> <Play v-else class="size-3.5 fill-current" /> <span>Run Code</span> <kbd class="border-primary-foreground/30 bg-primary-foreground/10 hidden rounded border px-1 font-mono text-xs sm:inline" > ⌘↵ </kbd> </Button> </div> </CardHeader> <!-- Main 2-Column Studio Grid --> <div class="divide-border grid grid-cols-1 divide-y lg:grid-cols-12 lg:divide-x lg:divide-y-0"> <!-- Left Column: File Tabs & Dark Code Editor (60-65% width = 7/12 cols) --> <section class="flex min-h-[560px] flex-col bg-zinc-950 text-zinc-100 lg:col-span-7"> <!-- File Navigation Tabs --> <div class="flex items-center justify-between border-b border-zinc-800/80 bg-zinc-900/70 px-2"> <div class="flex scrollbar-none items-center gap-1 overflow-x-auto py-1.5"> <button v-for="file in files" :key="file.id" type="button" :class=" cn( 'group flex cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 font-mono text-xs transition-colors', activeFileId === file.id ? 'border border-zinc-800 bg-zinc-950 text-zinc-100 shadow-xs' : 'text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-200', ) " @click="activeFileId = file.id" > <FileCode v-if="file.name.endsWith('.vue')" :class="cn('size-3.5 transition-colors', activeFileId === file.id ? 'text-success' : 'text-zinc-500')" /> <Braces v-else-if="file.name.endsWith('.variants.ts')" :class="cn('size-3.5 transition-colors', activeFileId === file.id ? 'text-chart-1' : 'text-zinc-500')" /> <Layers v-else-if="file.name === 'index.ts'" :class="cn('size-3.5 transition-colors', activeFileId === file.id ? 'text-warning' : 'text-zinc-500')" /> <CheckCircle2 v-else :class="cn('size-3.5 transition-colors', activeFileId === file.id ? 'text-info' : 'text-zinc-500')" /> <span>{{ file.name }}</span> </button> </div> <span class="hidden font-mono text-xs text-zinc-500 xl:inline"> {{ activeFile.size }} </span> </div> <!-- Breadcrumb / Path Info --> <div class="flex items-center justify-between border-b border-zinc-800/60 bg-zinc-950/80 px-4 py-1.5 font-mono text-xs text-zinc-400" > <div class="flex items-center gap-1.5 overflow-hidden text-ellipsis whitespace-nowrap"> <span class="text-zinc-600">src /</span> <span>{{ activeFile.path }}</span> </div> <span class="text-zinc-500">{{ activeFileLines.length }} lines</span> </div> <!-- Code Editor Body --> <div class="relative flex flex-1 overflow-x-auto bg-zinc-950 py-3 font-mono text-xs leading-relaxed select-text" > <!-- Gutter Line Numbers --> <div class="flex flex-col border-r border-zinc-800/60 px-3 text-right text-zinc-600 select-none"> <span v-for="(_, index) in activeFileLines" :key="index" :class=" cn('h-5 leading-5 transition-colors', hoveredLine === index + 1 ? 'font-semibold text-zinc-300' : '') " > {{ index + 1 }} </span> </div> <!-- Code Lines with Syntax Coloring --> <div class="flex-1 px-4 whitespace-pre"> <div v-for="(line, index) in activeFileLines" :key="index" :class=" cn( 'group flex h-5 items-center rounded-xs px-1 leading-5 transition-colors', hoveredLine === index + 1 ? 'bg-zinc-800/40' : '', ) " @mouseenter="hoveredLine = index + 1" @mouseleave="hoveredLine = null" > <span v-html="highlightSyntaxLine(line)" /> </div> </div> </div> <!-- Editor Status Bar --> <div class="flex items-center justify-between border-t border-zinc-800/60 bg-zinc-900/90 px-3 py-1 font-mono text-xs text-zinc-400" > <div class="flex items-center gap-3"> <span>UTF-8</span> <span>2 Spaces</span> <span class="text-zinc-500">Vue SFC / TS</span> </div> <div class="flex items-center gap-3"> <span>Ln {{ hoveredLine ?? 1 }}, Col 1</span> <span class="text-success">Prettier ✓</span> </div> </div> </section> <!-- Right Column: Live Sandbox & Terminal Console (35-40% width = 5/12 cols) --> <section class="bg-background flex min-h-[560px] flex-col lg:col-span-5"> <Tabs v-model="activeRightTab" class="flex h-full flex-col"> <!-- Right Tab Header --> <div class="border-border bg-muted/30 border-b px-3 py-2"> <TabsList class="grid w-full grid-cols-2"> <TabsTrigger value="preview" class="gap-1.5 text-xs"> <Eye class="size-3.5" /> <span>Live Preview</span> </TabsTrigger> <TabsTrigger value="console" class="gap-1.5 text-xs"> <Terminal class="size-3.5" /> <span>Terminal</span> <Badge variant="secondary" class="text-success ml-1 px-1 py-0 font-mono text-xs"> PASS </Badge> </TabsTrigger> </TabsList> </div> <!-- TAB 1: Live Component Sandbox --> <TabsContent value="preview" class="m-0 flex flex-1 flex-col gap-4 p-4"> <!-- Variant & Style Controls --> <div class="border-border bg-card space-y-3 rounded-lg border p-3 shadow-xs"> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold">Variant</span> <span class="text-muted-foreground font-mono text-xs">{{ selectedVariant }}</span> </div> <div class="flex flex-wrap gap-1.5"> <button v-for="v in ['default', 'secondary', 'destructive', 'outline', 'ghost', 'link']" :key="v" type="button" :class=" cn( 'min-h-6 cursor-pointer rounded-md px-2.5 py-1 font-mono text-xs capitalize transition-colors', selectedVariant === v ? 'bg-primary text-primary-foreground font-medium shadow-xs' : 'bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground', ) " @click="selectedVariant = v" > {{ v }} </button> </div> <Separator class="my-2" /> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold">Size & Options</span> </div> <div class="flex flex-wrap items-center gap-1.5"> <button v-for="s in ['sm', 'default', 'lg', 'icon'] as ButtonSize[]" :key="s" type="button" :class=" cn( 'min-h-6 cursor-pointer rounded-md px-2 py-0.5 font-mono text-xs uppercase transition-colors', selectedSize === s ? 'bg-secondary text-secondary-foreground font-semibold' : 'bg-muted/40 text-muted-foreground hover:bg-muted', ) " @click="selectedSize = s" > {{ s }} </button> <div class="ml-auto flex items-center gap-2"> <label class="text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none"> <input v-model="isDisabled" type="checkbox" class="accent-primary" /> <span>Disabled</span> </label> <label class="text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none"> <input v-model="isLoading" type="checkbox" class="accent-primary" /> <span>Loading</span> </label> <label class="text-muted-foreground flex cursor-pointer items-center gap-1.5 text-xs select-none"> <input v-model="withIcon" type="checkbox" class="accent-primary" /> <span>Icon</span> </label> </div> </div> </div> <!-- Render Stage Canvas --> <div class="border-border bg-muted/20 relative flex min-h-[160px] flex-1 flex-col items-center justify-center rounded-lg border border-dashed p-6" > <div class="flex flex-col items-center gap-3"> <Button :variant="selectedVariant" :size="selectedSize" :disabled="isDisabled || isLoading" class="transition-[color,background-color,border-color,box-shadow,opacity,transform,scale,translate,rotate] active:scale-95" @click="handleButtonClick" > <Loader2 v-if="isLoading" class="size-4 animate-spin" /> <Zap v-else-if="withIcon || selectedSize === 'icon'" class="size-4" /> <span v-if="selectedSize !== 'icon'">Interactive Button</span> </Button> <p class="text-muted-foreground font-mono text-xs"> Clicks: <span class="text-foreground font-semibold">{{ clickCount }}</span> </p> </div> <!-- Last Interaction Status --> <div class="bg-background/80 text-muted-foreground absolute right-2 bottom-2 left-2 flex items-center justify-between rounded-md px-2 py-1 text-xs backdrop-blur-xs" > <span class="truncate font-mono">{{ lastInteraction }}</span> <span class="text-success shrink-0 font-medium">Rendered OK</span> </div> </div> <!-- Dynamic Snippet Output --> <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-medium">Usage Code</span> <button type="button" class="text-muted-foreground hover:text-foreground flex min-h-6 cursor-pointer items-center gap-1" @click="handleCopySnippet" > <Check v-if="isSnippetCopied" class="text-success size-3" /> <Copy v-else class="size-3" /> <span>{{ isSnippetCopied ? 'Copied' : 'Copy' }}</span> </button> </div> <pre class="border-border bg-muted/40 text-foreground overflow-x-auto rounded-md border p-2.5 font-mono text-xs leading-relaxed" ><code>{{ generatedSnippet }}</code></pre> </div> </TabsContent> <!-- TAB 2: Terminal Console Execution Output --> <TabsContent value="console" class="m-0 flex flex-1 flex-col bg-zinc-950 p-4 font-mono text-xs text-zinc-300"> <!-- Console Toolbar --> <div class="mb-3 flex items-center justify-between border-b border-zinc-800 pb-2 text-zinc-400"> <div class="flex items-center gap-2"> <span class="font-semibold text-zinc-200">Vitest Test Runner</span> <Badge variant="outline" class="border-zinc-700 bg-zinc-900 font-mono text-xs text-zinc-300"> v2.1.8 </Badge> </div> <div class="flex items-center gap-3"> <span class="text-zinc-500">Run #{{ executionCount }} ({{ lastRunTime }})</span> <button type="button" class="cursor-pointer text-zinc-400 hover:text-zinc-100" title="Re-run tests" @click="handleRunCode" > <RotateCcw class="size-3.5" /> </button> </div> </div> <!-- Terminal Execution Logs --> <div class="flex-1 space-y-2 overflow-y-auto pr-1"> <div class="text-zinc-500"> $ vitest run packages/registry-vue/components/button/__tests__/button.spec.ts </div> <div class="text-success flex items-center gap-2 font-semibold"> <CheckCircle2 class="size-3.5 shrink-0" /> <span>PASS packages/registry-vue/components/button/__tests__/button.spec.ts (6 tests)</span> <span class="font-normal text-zinc-500">42ms</span> </div> <div class="ml-4 space-y-1.5 border-l border-zinc-800 pl-3"> <div v-for="log in testLogs" :key="log.id" class="flex items-center justify-between text-zinc-300"> <div class="flex items-center gap-2"> <Check class="text-success size-3" /> <span>{{ log.name }}</span> </div> <span class="text-zinc-500">{{ log.duration }}</span> </div> </div> <!-- Summary Card --> <div class="mt-4 space-y-1 rounded-md border border-zinc-800/80 bg-zinc-900/60 p-2.5 text-zinc-400"> <div class="flex justify-between"> <span>Test Files</span> <span class="text-success font-medium">1 passed (1)</span> </div> <div class="flex justify-between"> <span>Tests</span> <span class="text-success font-medium">6 passed (6)</span> </div> <div class="flex justify-between"> <span>Start at</span> <span class="text-zinc-300">{{ lastRunTime }}</span> </div> <div class="flex justify-between"> <span>Duration</span> <span class="text-zinc-300">42ms (transform 12ms, setup 0ms, collect 8ms, tests 42ms)</span> </div> </div> </div> <!-- Bottom Console Status Banner --> <div class="border-success/30 bg-success/10 text-success mt-3 flex items-center justify-between rounded-md border px-3 py-2" > <div class="flex items-center gap-2 font-semibold"> <CheckCircle2 class="size-4" /> <span>PASS · 6 passed, 0 failed in 42ms</span> </div> <span class="text-success/80 text-xs">Exit code: 0</span> </div> </TabsContent> </Tabs> </section> </div> </Card> </template>app/components/blocks/code-snippet-playground-files.ts4.9 kB
// Sample source shown in the playground's editor tabs. Kept in a plain .ts // module on purpose: Vite's dependency scanner reads .vue <script> blocks with // a line-based import regex, so sample lines like `import Button from '../Button.vue'` // inside a template string were treated as real imports and failed the scan. export interface CodeFile { id: string name: string path: string language: string size: string content: string } export const files: CodeFile[] = [ { id: 'button-vue', name: 'Button.vue', path: 'packages/registry-vue/components/button/Button.vue', language: 'vue', size: '1.2 KB', content: `<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { Primitive } from 'reka-ui' import { cn } from '@/lib/utils' import { buttonVariants } from './button.variants' type Variant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' type Size = 'default' | 'sm' | 'lg' | 'xs' | 'icon' interface Props { as?: string asChild?: boolean variant?: Variant size?: Size type?: 'button' | 'submit' | 'reset' class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { as: 'button', type: 'button', }) <\/script> <template> <Primitive data-slot="button" :data-variant="variant" :data-size="size" :as="as" :as-child="asChild" :type="as === 'button' && !asChild ? type : undefined" :class="cn(buttonVariants({ variant, size }), props.class)" > <slot /> </Primitive> </template>`, }, { id: 'button-variants', name: 'button.variants.ts', path: 'packages/registry-vue/components/button/button.variants.ts', language: 'typescript', size: '1.4 KB', content: `import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' export const buttonVariants = cva( 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0', { variants: { variant: { default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90', outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-9 px-4 py-2', sm: 'h-8 rounded-md px-3 text-xs', lg: 'h-10 rounded-md px-6', icon: 'size-9', }, }, defaultVariants: { variant: 'default', size: 'default', }, }, ) export type ButtonVariants = VariantProps<typeof buttonVariants>`, }, { id: 'index-ts', name: 'index.ts', path: 'packages/registry-vue/components/button/index.ts', language: 'typescript', size: '240 B', content: `export { default as Button } from './Button.vue' export { buttonVariants, type ButtonVariants } from './button.variants'`, }, { id: 'button-test', name: 'button.test.ts', path: 'packages/registry-vue/components/button/__tests__/button.spec.ts', language: 'typescript', size: '1.6 KB', content: `import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import Button from '../Button.vue' describe('Button Primitive', () => { it('renders default button with slot text', () => { const wrapper = mount(Button, { slots: { default: 'Submit' } }) expect(wrapper.text()).toBe('Submit') expect(wrapper.attributes('data-slot')).toBe('button') }) it('applies variant classes correctly', () => { const wrapper = mount(Button, { props: { variant: 'destructive' } }) expect(wrapper.attributes('data-variant')).toBe('destructive') expect(wrapper.classes()).toContain('bg-destructive') }) it('applies size styles correctly', () => { const wrapper = mount(Button, { props: { size: 'sm' } }) expect(wrapper.attributes('data-size')).toBe('sm') expect(wrapper.classes()).toContain('h-8') }) it('handles click events when active', async () => { const wrapper = mount(Button) await wrapper.trigger('click') expect(wrapper.emitted('click')).toBeTruthy() }) it('respects disabled state attribute', () => { const wrapper = mount(Button, { attrs: { disabled: true } }) expect(wrapper.attributes('disabled')).toBeDefined() }) it('forwards custom class names with cn()', () => { const wrapper = mount(Button, { props: { class: 'custom-btn' } }) expect(wrapper.classes()).toContain('custom-btn') }) })`, }, ]
Raw manifest:https://uipkge.dev/r/vue/code-snippet-playground.json