1+ <script setup>
2+ import { ref , computed , onMounted } from ' vue'
3+ import { useClipboard } from ' @vueuse/core'
4+
5+ const props = defineProps ({
6+ name: {
7+ type: String ,
8+ required: true
9+ },
10+ description: {
11+ type: String ,
12+ default: ' '
13+ },
14+ code: {
15+ type: String ,
16+ required: true
17+ },
18+ component: {
19+ type: Object ,
20+ required: true
21+ }
22+ })
23+
24+ const { copied , copy } = useClipboard ()
25+ const tabValue = ref (' preview' )
26+ const resizableRef = ref ()
27+ const currentSize = ref (100 )
28+
29+ const resizeComponent = (percentage ) => {
30+ currentSize .value = percentage
31+ if (resizableRef .value ) {
32+ resizableRef .value .style .width = ` ${ percentage} %`
33+ }
34+ }
35+
36+ const copyCode = () => {
37+ copy (props .code )
38+ }
39+
40+ onMounted (() => {
41+ resizableRef .value .style .width = ' 100%'
42+ })
43+ </script >
44+
45+ <template >
46+ <div class =" component-viewer border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900" >
47+ <!-- Header -->
48+ <div class =" flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800" >
49+ <div class =" flex items-center gap-4" >
50+ <!-- Tabs -->
51+ <div class =" flex items-center gap-1 bg-gray-200 dark:bg-gray-700 rounded-md p-1" >
52+ <button
53+ @click =" tabValue = 'preview'"
54+ :class =" [
55+ 'px-3 py-1.5 text-sm rounded-sm transition-colors',
56+ tabValue === 'preview' ? 'bg-white dark:bg-gray-900 shadow-sm text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:bg-white/50 dark:hover:bg-gray-800/50'
57+ ]"
58+ >
59+ Preview
60+ </button >
61+ <button
62+ @click =" tabValue = 'code'"
63+ :class =" [
64+ 'px-3 py-1.5 text-sm rounded-sm transition-colors',
65+ tabValue === 'code' ? 'bg-white dark:bg-gray-900 shadow-sm text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:bg-white/50 dark:hover:bg-gray-800/50'
66+ ]"
67+ >
68+ Code
69+ </button >
70+ </div >
71+
72+ <!-- Description -->
73+ <div v-if =" description" class =" text-sm text-gray-600 dark:text-gray-400" >
74+ {{ description }}
75+ </div >
76+ </div >
77+
78+ <!-- Controls -->
79+ <div class =" flex items-center gap-2" >
80+ <!-- Responsive controls -->
81+ <div class =" flex items-center gap-1 border border-gray-200 dark:border-gray-600 rounded-md p-1" >
82+ <button
83+ @click =" resizeComponent(100)"
84+ :class =" [
85+ 'p-1.5 rounded-sm transition-colors',
86+ currentSize === 100 ? 'bg-gray-200 dark:bg-gray-600' : 'hover:bg-gray-100 dark:hover:bg-gray-700'
87+ ]"
88+ title =" Desktop"
89+ >
90+ <svg class =" w-4 h-4" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24" >
91+ <rect x =" 2" y =" 4" width =" 20" height =" 12" rx =" 2" />
92+ <path d =" M2 16h20" />
93+ </svg >
94+ </button >
95+ <button
96+ @click =" resizeComponent(768)"
97+ :class =" [
98+ 'p-1.5 rounded-sm transition-colors',
99+ currentSize === 768 ? 'bg-gray-200 dark:bg-gray-600' : 'hover:bg-gray-100 dark:hover:bg-gray-700'
100+ ]"
101+ title =" Tablet"
102+ >
103+ <svg class =" w-4 h-4" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24" >
104+ <rect x =" 5" y =" 2" width =" 14" height =" 20" rx =" 2" />
105+ </svg >
106+ </button >
107+ <button
108+ @click =" resizeComponent(375)"
109+ :class =" [
110+ 'p-1.5 rounded-sm transition-colors',
111+ currentSize === 375 ? 'bg-gray-200 dark:bg-gray-600' : 'hover:bg-gray-100 dark:hover:bg-gray-700'
112+ ]"
113+ title =" Mobile"
114+ >
115+ <svg class =" w-4 h-4" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24" >
116+ <rect x =" 7" y =" 1" width =" 10" height =" 22" rx =" 2" />
117+ </svg >
118+ </button >
119+ </div >
120+
121+ <!-- Copy button -->
122+ <button
123+ @click =" copyCode"
124+ class =" p-2 border border-gray-200 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
125+ title =" Copy code"
126+ >
127+ <svg v-if =" copied" class =" w-4 h-4 text-green-600" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24" >
128+ <path stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2" d =" M5 13l4 4L19 7" />
129+ </svg >
130+ <svg v-else class =" w-4 h-4" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24" >
131+ <rect x =" 9" y =" 9" width =" 13" height =" 13" rx =" 2" ry =" 2" />
132+ <path d =" M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
133+ </svg >
134+ </button >
135+ </div >
136+ </div >
137+
138+ <!-- Content -->
139+ <div class =" p-0" >
140+ <!-- Preview -->
141+ <div v-show =" tabValue === 'preview'" class =" relative overflow-hidden" >
142+ <div class =" bg-gray-50 dark:bg-gray-800 p-8" >
143+ <div
144+ ref =" resizableRef"
145+ class =" transition-all duration-300 ease-in-out mx-auto bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-sm"
146+ :style =" { maxWidth: currentSize === 100 ? '100%' : `${currentSize}px` }"
147+ >
148+ <component :is =" component" />
149+ </div >
150+ </div >
151+ </div >
152+
153+ <!-- Code -->
154+ <div v-show =" tabValue === 'code'" class =" relative" >
155+ <pre class =" p-4 text-sm overflow-x-auto bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100" ><code v-html =" code" ></code ></pre >
156+ </div >
157+ </div >
158+ </div >
159+ </template >
160+
161+ <style scoped>
162+ pre {
163+ white-space : pre-wrap ;
164+ word-wrap : break-word ;
165+ }
166+
167+ code {
168+ font-family : ' Fira Code' , ' Monaco' , ' Cascadia Code' , ' Roboto Mono' , monospace ;
169+ }
170+ </style >
0 commit comments