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 +126 -157
- package/package.json +2 -1
- package/src/directives/v-tooltip.js +5 -4
- package/src/plugins/toastPlugin.js +16 -5
- package/src/ui/componentDescription.vue +131 -0
- package/src/ui/componentShowcase.vue +171 -0
- package/src/ui/kArrayList.vue +9 -1
- package/src/ui/kButton.vue +94 -36
- package/src/ui/kChip.vue +29 -16
- package/src/ui/kDatatable.vue +37 -26
- package/src/ui/kInput.vue +113 -85
- package/src/ui/kSearch.vue +4 -1
- package/src/ui/kSelect.vue +89 -18
- package/src/ui/kSelectButton.vue +2 -2
- package/src/ui/kTable.vue +83 -17
- package/src/ui/kTags.vue +20 -5
- package/src/ui/kToast.vue +72 -12
- package/src/ui/kToggle.vue +13 -1
- package/src/ui/kToolbar.vue +198 -53
- package/src/ui/kUploader.vue +8 -4
- package/src/ui/themes/intentColors.js +39 -0
- package/src/ui/themes/kButton.theme.js +38 -0
- package/src/ui/themes/kInput.theme.js +16 -0
- package/styles.css +1 -0
- package/tailwind.config.js +42 -40
- package/dist/ketekny-ui-kit.css +0 -1
- package/dist/ketekny-ui-kit.js +0 -2607
- package/dist/ketekny-ui-kit.umd.cjs +0 -9
- package/src/ui/componentPreview.vue +0 -289
package/README.md
CHANGED
|
@@ -1,185 +1,154 @@
|
|
|
1
1
|
# Ketekny UI Kit
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Tailwind Setup (Required)
|
|
15
22
|
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
34
|
+
"./index.html",
|
|
35
|
+
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
|
36
|
+
"./node_modules/ketekny-ui-kit/**/*.{vue,js,ts,jsx,tsx}",
|
|
25
37
|
],
|
|
26
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
app.use(toastPlugin)
|
|
104
|
-
app.use(confirmPlugin)
|
|
63
|
+
Import the kit stylesheet in your app entry (`main.js` / `main.ts`):
|
|
105
64
|
|
|
106
|
-
|
|
65
|
+
```js
|
|
66
|
+
import "ketekny-ui-kit/styles.css";
|
|
107
67
|
```
|
|
108
68
|
|
|
109
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
|
|
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.
|
|
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-
|
|
36
|
-
text-
|
|
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(
|
|
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-
|
|
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
|
|
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: (
|
|
13
|
-
error: (
|
|
14
|
-
info: (
|
|
15
|
-
warning: (
|
|
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>
|