ketekny-ui-kit 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,185 +1,154 @@
1
1
  # Ketekny UI Kit
2
2
 
3
- A comprehensive Vue 3 UI component library built with Tailwind CSS. This library provides a set of reusable, customizable components for modern web applications.
4
-
3
+ Vue 3 UI component library styled with Tailwind CSS.
5
4
 
6
5
  ## Installation
7
6
 
7
+ 1. Install the package:
8
+
8
9
  ```bash
9
10
  npm install ketekny-ui-kit
10
11
  ```
11
12
 
12
- ## Tailwind Configuration
13
+ 2. Ensure required peer/runtime dependencies exist in your app:
14
+
15
+ ```bash
16
+ npm install vue tailwindcss postcss autoprefixer
17
+ ```
18
+
19
+ If your project already has Vue + Tailwind configured, you can skip step 2.
13
20
 
14
- Add the preset to your `tailwind.config.js`:
21
+ ## Tailwind Setup (Required)
15
22
 
16
- ```javascript
17
- // tailwind.config.js
18
- const preset = require('ketekny-ui-kit/tailwind-preset.js')
23
+ Use the provided Tailwind preset so semantic colors and component color tokens are always available.
19
24
 
20
- module.exports = {
21
- presets: [preset],
25
+ ### `tailwind.config.js` (ESM)
26
+
27
+ ```js
28
+ import tailwindPreset from "ketekny-ui-kit/tailwind-preset.js";
29
+
30
+ /** @type {import('tailwindcss').Config} */
31
+ export default {
32
+ presets: [tailwindPreset],
22
33
  content: [
23
- './src/**/*.{vue,js,ts}',
24
- './node_modules/ketekny-ui-kit/**/*.{vue,js,ts}'
34
+ "./index.html",
35
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
36
+ "./node_modules/ketekny-ui-kit/**/*.{vue,js,ts,jsx,tsx}",
25
37
  ],
26
- safelist: [
27
- { pattern: /text-semantic-(success|danger|info|warning)-text/ },
28
- { pattern: /border-semantic-(success|danger|info|warning)-border/ },
29
- { pattern: /bg-semantic-(success|warning|error|info)-(bg|disabled|button)/ },
30
- ],
31
- }
38
+ };
32
39
  ```
33
40
 
34
- ## Components
35
-
36
- ### UI Components
37
- - `kButton` - Versatile button component
38
- - `kInput` - Input field with validation
39
- - `kDateSelector` - Date picker with year/month modes
40
- - `kSelect` - Dropdown select component
41
- - `kToggle` - Toggle switch
42
- - `kChip` - Tag/label component
43
- - `kSpinner` - Loading spinner
44
- - `kIcon` - Icon component using Lucide icons
45
- - `kMessage` - Alert/message component
46
- - `kCode` - Code display component
47
- - `kSearch` - Search input component
48
- - `kTags` - Tag input component
49
- - `kSelectButton` - Button group select
50
- - `kArrayList` - Array/list management
51
- - `kToolbar` - Toolbar component
52
- - `kUploader` - File upload component
53
- - `kEditor` - Rich text editor
54
- - `kDialog` - Modal dialog
55
- - `kDrawer` - Slide-out drawer
56
- - `kTabs` - Tab navigation
57
- - `kTable` - Data table
58
- - `kDatatable` - Advanced data table
59
-
60
- ### Layout Components
61
- - `kAppHeader` - Application header
62
- - `kAppFooter` - Application footer
63
- - `kAppMain` - Main content area
64
- - `kHero` - Hero section
65
-
66
- ### Plugins
67
- - `toastPlugin` - Toast notifications
68
- - `confirmPlugin` - Confirmation dialogs
69
- - `alertPlugin` - Alert dialogs
70
- - `inputDialogPlugin` - Input dialogs
71
- - `tooltipPlugin` - Tooltip functionality
72
-
73
- ## Usage
74
-
75
- ```javascript
76
- // main.js
77
- import { createApp } from 'vue'
78
- import App from './App.vue'
79
-
80
- // Import components
81
- import {
82
- kButton,
83
- kInput,
84
- kDateSelector,
85
- kSelect,
86
- kToggle
87
- } from 'ketekny-ui-kit'
41
+ ### `tailwind.config.cjs` (CommonJS)
42
+
43
+ ```js
44
+ const tailwindPreset = require("ketekny-ui-kit/tailwind-preset.js").default;
88
45
 
89
- // Import plugins
90
- import toastPlugin from 'ketekny-ui-kit'
91
- import confirmPlugin from 'ketekny-ui-kit'
46
+ /** @type {import('tailwindcss').Config} */
47
+ module.exports = {
48
+ presets: [tailwindPreset],
49
+ content: [
50
+ "./index.html",
51
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
52
+ "./node_modules/ketekny-ui-kit/**/*.{vue,js,ts,jsx,tsx}",
53
+ ],
54
+ };
55
+ ```
92
56
 
93
- const app = createApp(App)
57
+ Notes:
58
+ - The preset contains the semantic color definitions and safelist patterns used by dynamic classes (alerts/toasts/button intents).
59
+ - If you replace `safelist` in your own config, keep equivalent patterns or dynamic semantic classes may be purged.
94
60
 
95
- // Register components globally
96
- app.component('kButton', kButton)
97
- app.component('kInput', kInput)
98
- app.component('kDateSelector', kDateSelector)
99
- app.component('kSelect', kSelect)
100
- app.component('kToggle', kToggle)
61
+ ## App CSS Setup
101
62
 
102
- // Use plugins
103
- app.use(toastPlugin)
104
- app.use(confirmPlugin)
63
+ Import the kit stylesheet in your app entry (`main.js` / `main.ts`):
105
64
 
106
- app.mount('#app')
65
+ ```js
66
+ import "ketekny-ui-kit/styles.css";
107
67
  ```
108
68
 
109
- ## Example Component Usage
110
-
111
- ```vue
112
- <template>
113
- <div class="p-4 space-y-4">
114
- <!-- Button -->
115
- <k-button @click="handleClick" variant="primary">
116
- Click me
117
- </k-button>
118
-
119
- <!-- Input -->
120
- <k-input
121
- v-model="email"
122
- label="Email"
123
- placeholder="Enter your email"
124
- type="email"
125
- />
126
-
127
- <!-- Date Selector -->
128
- <k-date-selector
129
- v-model="date"
130
- label="Select Date"
131
- type="date"
132
- />
133
-
134
- <!-- Select -->
135
- <k-select
136
- v-model="selectedOption"
137
- :options="options"
138
- label="Choose option"
139
- />
140
- </div>
141
- </template>
142
-
143
- <script>
144
- import { kButton, kInput, kDateSelector, kSelect } from 'ketekny-ui-kit'
69
+ ## Vue Setup
145
70
 
146
- export default {
147
- components: {
148
- kButton,
149
- kInput,
150
- kDateSelector,
151
- kSelect
152
- },
153
- data() {
154
- return {
155
- email: '',
156
- date: null,
157
- selectedOption: null,
158
- options: [
159
- { value: 'option1', label: 'Option 1' },
160
- { value: 'option2', label: 'Option 2' }
161
- ]
162
- }
163
- },
164
- methods: {
165
- handleClick() {
166
- // Handle button click
167
- }
168
- }
169
- }
170
- </script>
171
- ```
71
+ `main.js` / `main.ts` example:
172
72
 
173
- ## Dependencies
73
+ ```js
74
+ import { createApp } from "vue";
75
+ import App from "./App.vue";
76
+ import "./style.css";
77
+
78
+ import {
79
+ kButton,
80
+ kInput,
81
+ kSelect,
82
+ kDateSelector,
83
+ kToggle,
84
+ toastPlugin,
85
+ confirmPlugin,
86
+ alertPlugin,
87
+ inputDialogPlugin,
88
+ tooltipPlugin,
89
+ } from "ketekny-ui-kit";
90
+
91
+ const app = createApp(App);
92
+
93
+ app.component("kButton", kButton);
94
+ app.component("kInput", kInput);
95
+ app.component("kSelect", kSelect);
96
+ app.component("kDateSelector", kDateSelector);
97
+ app.component("kToggle", kToggle);
98
+
99
+ app.use(toastPlugin);
100
+ app.use(confirmPlugin);
101
+ app.use(alertPlugin);
102
+ app.use(inputDialogPlugin);
103
+ app.use(tooltipPlugin);
104
+
105
+ app.mount("#app");
106
+ ```
174
107
 
175
- This library depends on:
176
- - Vue 3.x
177
- - Tailwind CSS 3.x
178
- - Lucide Vue Next (icons)
179
- - Vue Datepicker
180
- - Moment.js
181
- - PrimeVue
182
- - Quill (rich text editor)
108
+ ## Available Plugins
109
+
110
+ - `toastPlugin` adds `$toast` (`success`, `error`, `warning`, `info`, `show`)
111
+ - `confirmPlugin` adds `$confirm(options)`
112
+ - `alertPlugin` adds `$alert.success|warning|error|info(...)`
113
+ - `inputDialogPlugin` adds `$prompt(options)`
114
+ - `tooltipPlugin` registers `v-tooltip`
115
+
116
+ ## Available Exports
117
+
118
+ ### Components
119
+
120
+ - `kMessage`
121
+ - `kButton`
122
+ - `kChip`
123
+ - `kCode`
124
+ - `kDialog`
125
+ - `kDrawer`
126
+ - `kInput`
127
+ - `kDateSelector`
128
+ - `kToolbar`
129
+ - `kSelect`
130
+ - `kTable`
131
+ - `kTabs`
132
+ - `kToggle`
133
+ - `kUploader`
134
+ - `kEditor`
135
+ - `kSpinner`
136
+ - `kSelectButton`
137
+ - `kDatatable`
138
+ - `kIcon`
139
+ - `kMenu`
140
+ - `kTags`
141
+ - `kSearch`
142
+ - `kArrayList`
143
+ - `kSkeleton`
144
+ - `kAppHeader`
145
+ - `kAppFooter`
146
+ - `kAppMain`
147
+ - `kHero`
148
+
149
+ ### Tailwind preset export
150
+
151
+ - `tailwindPreset`
183
152
 
184
153
  ## License
185
154
 
@@ -187,4 +156,4 @@ ISC
187
156
 
188
157
  ## Links
189
158
 
190
- - [npm](https://www.npmjs.com/package/ketekny-ui-kit)
159
+ - [npm](https://www.npmjs.com/package/ketekny-ui-kit)
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.6",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
8
8
  "index.js",
9
+ "styles.css",
9
10
  "src/",
10
11
  "dist/",
11
12
  "tailwind-preset.js",
@@ -32,17 +32,18 @@ function createNodes() {
32
32
 
33
33
  const bubble = document.createElement('div')
34
34
  bubble.className = `
35
- px-2.5 py-1 rounded-md text-xs font-medium
36
- text-white bg-gray-900 shadow-lg ring-1 ring-black/10
35
+ px-3 py-2 rounded-lg text-sm font-medium leading-5
36
+ text-slate-50 bg-slate-900 shadow-xl ring-1 ring-black/20
37
37
  opacity-0 scale-95 transition duration-150 ease-out
38
38
  `.replace(/\s+/g, ' ').trim()
39
39
  bubble.style.whiteSpace = 'normal' // allow wrap when needed
40
- bubble.style.maxWidth = 'min(32rem, calc(100vw - 2rem))' // clamp width
40
+ bubble.style.maxWidth = 'min(36rem, calc(100vw - 1.5rem))' // clamp width
41
41
  bubble.style.wordBreak = 'break-word'
42
+ bubble.style.letterSpacing = '0.01em'
42
43
  bubble.style.position = 'relative'
43
44
 
44
45
  const arrow = document.createElement('div')
45
- arrow.className = 'absolute w-2 h-2 rotate-45 bg-gray-900'
46
+ arrow.className = 'absolute w-2 h-2 rotate-45 bg-slate-900'
46
47
  arrow.setAttribute('aria-hidden', 'true')
47
48
 
48
49
  wrapper.appendChild(bubble)
@@ -1,4 +1,4 @@
1
- import { createApp, h } from 'vue'
1
+ import { createApp } from 'vue'
2
2
  import kToast from '../ui/kToast.vue'
3
3
 
4
4
  export default {
@@ -8,11 +8,22 @@ export default {
8
8
  document.body.appendChild(container)
9
9
  const toastInstance = toastApp.mount(container)
10
10
 
11
+ const call = (type, messageOrConfig, optionsOrTime) => {
12
+ // Backward compatible:
13
+ // 1) $toast.success("msg", 3000)
14
+ // 2) $toast.success({ message, title, duration, actionLabel, onAction, closable })
15
+ toastInstance.addToast(type, messageOrConfig, optionsOrTime)
16
+ }
17
+
11
18
  app.config.globalProperties.$toast = {
12
- success: (msg, time) => toastInstance.addToast('success', msg, time),
13
- error: (msg, time) => toastInstance.addToast('error', msg, time),
14
- info: (msg, time) => toastInstance.addToast('info', msg, time),
15
- warning: (msg, time) => toastInstance.addToast('warning', msg, time),
19
+ success: (messageOrConfig, optionsOrTime) => call('success', messageOrConfig, optionsOrTime),
20
+ error: (messageOrConfig, optionsOrTime) => call('error', messageOrConfig, optionsOrTime),
21
+ info: (messageOrConfig, optionsOrTime) => call('info', messageOrConfig, optionsOrTime),
22
+ warning: (messageOrConfig, optionsOrTime) => call('warning', messageOrConfig, optionsOrTime),
23
+ show: (config = {}) => {
24
+ const type = ['success', 'error', 'warning', 'info'].includes(config.type) ? config.type : 'info'
25
+ call(type, config)
26
+ },
16
27
  }
17
28
  }
18
29
  }
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <div class="overflow-hidden border rounded-lg shadow-sm component-card cp-card cp-border">
3
+ <div class="flex items-center justify-between px-4 py-3 border-b cp-border cp-muted-50">
4
+ <h3 class="text-lg font-semibold cp-text">{{ componentName }} Description</h3>
5
+
6
+ <div class="flex items-center gap-1">
7
+ <button
8
+ v-for="tab in tabs"
9
+ :key="tab.id"
10
+ @click="activeTab = tab.id"
11
+ :class="['px-3 py-1.5 text-sm font-medium rounded-md transition-colors', activeTab === tab.id ? 'cp-tab-active' : 'cp-tab']">
12
+ {{ tab.label }}
13
+ </button>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="p-4">
18
+ <div v-show="activeTab === 'props'">
19
+ <div v-if="props.length > 0" class="overflow-x-auto">
20
+ <table class="w-full text-sm">
21
+ <thead>
22
+ <tr class="border-b cp-border">
23
+ <th class="px-4 py-3 font-semibold text-left cp-text">Name</th>
24
+ <th class="px-4 py-3 font-semibold text-left cp-text">Type</th>
25
+ <th class="px-4 py-3 font-semibold text-left cp-text">Default</th>
26
+ <th class="px-4 py-3 font-semibold text-left cp-text">Description</th>
27
+ </tr>
28
+ </thead>
29
+
30
+ <tbody>
31
+ <tr v-for="(prop, index) in props" :key="prop.name" :class="index % 2 === 0 ? 'cp-muted-30' : ''">
32
+ <td class="px-4 py-3">
33
+ <code class="text-sm font-mono px-1.5 py-0.5 rounded cp-chip cp-chip-text">{{ prop.name }}</code>
34
+ </td>
35
+ <td class="px-4 py-3"><span class="font-mono text-sm text-blue-600">{{ prop.type }}</span></td>
36
+ <td class="px-4 py-3">
37
+ <code v-if="prop.default !== undefined" class="font-mono text-sm text-emerald-600">{{ prop.default }}</code>
38
+ <span v-else class="cp-text-muted">-</span>
39
+ </td>
40
+ <td class="px-4 py-3 cp-text-muted">{{ prop.description }}</td>
41
+ </tr>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+
46
+ <div v-else class="flex items-center justify-center py-8 cp-text-muted">
47
+ <span>No props defined for this component</span>
48
+ </div>
49
+ </div>
50
+
51
+ <div v-show="activeTab === 'slots'">
52
+ <div v-if="slots.length > 0" class="overflow-x-auto">
53
+ <table class="w-full text-sm">
54
+ <thead>
55
+ <tr class="border-b cp-border">
56
+ <th class="px-4 py-3 font-semibold text-left cp-text">Name</th>
57
+ <th class="px-4 py-3 font-semibold text-left cp-text">Scoped</th>
58
+ <th class="px-4 py-3 font-semibold text-left cp-text">Description</th>
59
+ </tr>
60
+ </thead>
61
+
62
+ <tbody>
63
+ <tr v-for="(slot, index) in slots" :key="slot.name" :class="index % 2 === 0 ? 'cp-muted-30' : ''">
64
+ <td class="px-4 py-3">
65
+ <code class="text-sm font-mono px-1.5 py-0.5 rounded cp-chip cp-chip-text">{{ slot.name }}</code>
66
+ </td>
67
+ <td class="px-4 py-3">
68
+ <span :class="['inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium', slot.scoped ? 'bg-emerald-100 text-emerald-700' : 'cp-badge-muted']">
69
+ {{ slot.scoped ? "Yes" : "No" }}
70
+ </span>
71
+ </td>
72
+ <td class="px-4 py-3 cp-text-muted">{{ slot.description }}</td>
73
+ </tr>
74
+ </tbody>
75
+ </table>
76
+ </div>
77
+
78
+ <div v-else class="flex items-center justify-center py-8 cp-text-muted">
79
+ <span>No slots defined for this component</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </template>
85
+
86
+ <script>
87
+ export default {
88
+ name: "ComponentDescription",
89
+ props: {
90
+ componentName: { type: String, required: true },
91
+ props: { type: Array, default: () => [] },
92
+ slots: { type: Array, default: () => [] },
93
+ },
94
+ data() {
95
+ return {
96
+ activeTab: "props",
97
+ tabs: [
98
+ { id: "props", label: "Props" },
99
+ { id: "slots", label: "Slots" },
100
+ ],
101
+ };
102
+ },
103
+ };
104
+ </script>
105
+
106
+ <style scoped>
107
+ .component-card {
108
+ --cp-foreground: #0a0a0a;
109
+ --cp-card: #ffffff;
110
+ --cp-border: #e5e5e5;
111
+ --cp-muted: #f5f5f5;
112
+ --cp-muted-foreground: #737373;
113
+ --cp-primary: #0a0a0a;
114
+ --cp-primary-foreground: #fafafa;
115
+ --cp-accent: #f5f5f5;
116
+ --cp-accent-foreground: #0a0a0a;
117
+ }
118
+
119
+ .cp-text { color: var(--cp-foreground); }
120
+ .cp-text-muted { color: var(--cp-muted-foreground); }
121
+ .cp-card { background-color: var(--cp-card); }
122
+ .cp-border { border-color: var(--cp-border); }
123
+ .cp-muted-50 { background-color: color-mix(in srgb, var(--cp-muted) 50%, transparent); }
124
+ .cp-muted-30 { background-color: color-mix(in srgb, var(--cp-muted) 30%, transparent); }
125
+ .cp-tab { color: var(--cp-muted-foreground); }
126
+ .cp-tab:hover { color: var(--cp-foreground); background-color: var(--cp-accent); }
127
+ .cp-tab-active { background-color: var(--cp-primary); color: var(--cp-primary-foreground); }
128
+ .cp-chip { background-color: var(--cp-accent); }
129
+ .cp-chip-text { color: var(--cp-accent-foreground); }
130
+ .cp-badge-muted { background-color: var(--cp-muted); color: var(--cp-muted-foreground); }
131
+ </style>
@@ -0,0 +1,171 @@
1
+ <template>
2
+ <div class="overflow-hidden border rounded-lg shadow-sm component-card cp-card cp-border">
3
+ <div class="flex items-center justify-between px-4 py-3 border-b cp-border cp-muted-50">
4
+ <h3 class="text-lg font-semibold cp-text">{{ componentName }} Preview</h3>
5
+
6
+ <div class="flex items-center gap-1">
7
+ <button
8
+ v-for="tab in tabs"
9
+ :key="tab.id"
10
+ @click="activeTab = tab.id"
11
+ :class="['px-3 py-1.5 text-sm font-medium rounded-md transition-colors', activeTab === tab.id ? 'cp-tab-active' : 'cp-tab']">
12
+ {{ tab.label }}
13
+ </button>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="p-4">
18
+ <div v-show="activeTab === 'preview'" class="min-h-[120px] rounded-lg p-6 border cp-muted-30 cp-border-50">
19
+ <div class="flex justify-end mb-4">
20
+ <div class="inline-flex p-1 rounded-md cp-muted-50">
21
+ <button
22
+ v-for="vp in previewViewports"
23
+ :key="vp.id"
24
+ @click="activeViewport = vp.id"
25
+ :class="['px-3 py-1.5 text-xs font-semibold rounded-md transition-colors', activeViewport === vp.id ? 'cp-tab-active' : 'cp-tab']">
26
+ {{ vp.label }}
27
+ </button>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="flex justify-center w-full">
32
+ <div :class="['w-full transition-all duration-200', previewViewportClass]">
33
+ <slot name="preview"></slot>
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ <div v-show="activeTab === 'code'" class="relative">
39
+ <div class="absolute z-10 top-3 right-3">
40
+ <button @click="copyCode" class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors cp-chip cp-chip-hover cp-chip-text">
41
+ <svg
42
+ v-if="!copied"
43
+ xmlns="http://www.w3.org/2000/svg"
44
+ width="16"
45
+ height="16"
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ stroke-width="2"
50
+ stroke-linecap="round"
51
+ stroke-linejoin="round">
52
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
53
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
54
+ </svg>
55
+
56
+ <svg
57
+ v-else
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ width="16"
60
+ height="16"
61
+ viewBox="0 0 24 24"
62
+ fill="none"
63
+ stroke="currentColor"
64
+ stroke-width="2"
65
+ stroke-linecap="round"
66
+ stroke-linejoin="round"
67
+ class="text-emerald-600">
68
+ <polyline points="20 6 9 17 4 12" />
69
+ </svg>
70
+
71
+ {{ copied ? "Copied!" : "Copy" }}
72
+ </button>
73
+ </div>
74
+
75
+ <pre class="p-4 overflow-x-auto rounded-lg cp-pre">
76
+ <code class="font-mono text-sm" v-html="highlightedCode"></code>
77
+ </pre>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </template>
82
+
83
+ <script>
84
+ import hljs from "highlight.js/lib/core";
85
+ import xml from "highlight.js/lib/languages/xml";
86
+
87
+ hljs.registerLanguage("xml", xml);
88
+
89
+ export default {
90
+ name: "ComponentShowcase",
91
+ props: {
92
+ componentName: { type: String, required: true },
93
+ code: { type: String, default: "" },
94
+ },
95
+ data() {
96
+ return {
97
+ activeTab: "preview",
98
+ copied: false,
99
+ activeViewport: "desktop",
100
+ tabs: [
101
+ { id: "preview", label: "Preview" },
102
+ { id: "code", label: "Code" },
103
+ ],
104
+ previewViewports: [
105
+ { id: "desktop", label: "Desktop" },
106
+ { id: "tablet", label: "Tablet" },
107
+ { id: "mobile", label: "Mobile" },
108
+ ],
109
+ };
110
+ },
111
+ computed: {
112
+ highlightedCode() {
113
+ if (!this.code) return "";
114
+ try {
115
+ return hljs.highlight(this.code.trim(), { language: "xml" }).value;
116
+ } catch {
117
+ return this.escapeHtml(this.code.trim());
118
+ }
119
+ },
120
+ previewViewportClass() {
121
+ if (this.activeViewport === "mobile") return "max-w-[430px]";
122
+ if (this.activeViewport === "tablet") return "max-w-[820px]";
123
+ return "max-w-none";
124
+ },
125
+ },
126
+ methods: {
127
+ async copyCode() {
128
+ try {
129
+ await navigator.clipboard.writeText(this.code.trim());
130
+ this.copied = true;
131
+ setTimeout(() => (this.copied = false), 2000);
132
+ } catch (err) {
133
+ console.error("Failed to copy code:", err);
134
+ }
135
+ },
136
+ escapeHtml(text) {
137
+ const div = document.createElement("div");
138
+ div.textContent = text;
139
+ return div.innerHTML;
140
+ },
141
+ },
142
+ };
143
+ </script>
144
+
145
+ <style scoped>
146
+ .component-card {
147
+ --cp-foreground: #0a0a0a;
148
+ --cp-card: #ffffff;
149
+ --cp-border: #e5e5e5;
150
+ --cp-muted: #f5f5f5;
151
+ --cp-muted-foreground: #737373;
152
+ --cp-primary: #0a0a0a;
153
+ --cp-primary-foreground: #fafafa;
154
+ --cp-accent: #f5f5f5;
155
+ --cp-accent-foreground: #0a0a0a;
156
+ }
157
+
158
+ .cp-text { color: var(--cp-foreground); }
159
+ .cp-card { background-color: var(--cp-card); }
160
+ .cp-border { border-color: var(--cp-border); }
161
+ .cp-border-50 { border-color: color-mix(in srgb, var(--cp-border) 50%, transparent); }
162
+ .cp-muted-50 { background-color: color-mix(in srgb, var(--cp-muted) 50%, transparent); }
163
+ .cp-muted-30 { background-color: color-mix(in srgb, var(--cp-muted) 30%, transparent); }
164
+ .cp-tab { color: var(--cp-muted-foreground); }
165
+ .cp-tab:hover { color: var(--cp-foreground); background-color: var(--cp-accent); }
166
+ .cp-tab-active { background-color: var(--cp-primary); color: var(--cp-primary-foreground); }
167
+ .cp-chip { background-color: var(--cp-accent); }
168
+ .cp-chip-hover:hover { background-color: color-mix(in srgb, var(--cp-accent) 80%, transparent); }
169
+ .cp-chip-text { color: var(--cp-accent-foreground); }
170
+ .cp-pre { background-color: var(--cp-muted); color: var(--cp-foreground); }
171
+ </style>