@stlhorizon/vue-ui 0.0.1
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 +35 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.esm.js +21608 -0
- package/dist/index.umd.js +57 -0
- package/dist/vue-ui.css +1 -0
- package/package.json +71 -0
- package/src/App.vue +147 -0
- package/src/__tests__/App.spec.js +11 -0
- package/src/components/Accordion.vue +270 -0
- package/src/components/AccordionItem.vue +106 -0
- package/src/components/Alert.vue +207 -0
- package/src/components/Avatar.vue +92 -0
- package/src/components/Badge.vue +39 -0
- package/src/components/Breadcrumb.vue +67 -0
- package/src/components/Button.vue +61 -0
- package/src/components/ButtonGroup.vue +43 -0
- package/src/components/Calendar.vue +158 -0
- package/src/components/Card.vue +13 -0
- package/src/components/CardBody.vue +33 -0
- package/src/components/CardContent.vue +13 -0
- package/src/components/CardFooter.vue +33 -0
- package/src/components/CardHeader.vue +13 -0
- package/src/components/CardTitle.vue +13 -0
- package/src/components/Checkbox.vue +83 -0
- package/src/components/DataTable.vue +52 -0
- package/src/components/DataTableCell.vue +56 -0
- package/src/components/DataTableFilters.vue +74 -0
- package/src/components/DataTableHeader.vue +11 -0
- package/src/components/DataTablePagination.vue +103 -0
- package/src/components/DataTableRow.vue +58 -0
- package/src/components/DataTableToolBar.vue +51 -0
- package/src/components/DatePicker.vue +69 -0
- package/src/components/Divider.vue +74 -0
- package/src/components/Dropdown.vue +113 -0
- package/src/components/DropdownItem.vue +59 -0
- package/src/components/FileUpload.vue +155 -0
- package/src/components/Footer.vue +71 -0
- package/src/components/FormField.vue +134 -0
- package/src/components/Header.vue +51 -0
- package/src/components/Icon.vue +34 -0
- package/src/components/Image.vue +117 -0
- package/src/components/Input.vue +17 -0
- package/src/components/InputGroup.vue +38 -0
- package/src/components/Label.vue +44 -0
- package/src/components/Link.vue +64 -0
- package/src/components/ListItem.vue +82 -0
- package/src/components/Logo.vue +70 -0
- package/src/components/MainNavigation.vue +37 -0
- package/src/components/MenuItem.vue +61 -0
- package/src/components/Modal.vue +77 -0
- package/src/components/ModalBody.vue +32 -0
- package/src/components/ModalFooter.vue +32 -0
- package/src/components/ModalHeader.vue +18 -0
- package/src/components/Notification.vue +126 -0
- package/src/components/Option.vue +72 -0
- package/src/components/ProgressBar.vue +88 -0
- package/src/components/Radio.vue +91 -0
- package/src/components/Search.vue +286 -0
- package/src/components/Select.vue +123 -0
- package/src/components/Sidebar.vue +52 -0
- package/src/components/Slider.vue +40 -0
- package/src/components/Spinner.vue +85 -0
- package/src/components/Stepper.vue +9 -0
- package/src/components/StepperItem.vue +28 -0
- package/src/components/Switch.vue +120 -0
- package/src/components/Tab.vue +49 -0
- package/src/components/TabPanel.vue +27 -0
- package/src/components/Text.vue +80 -0
- package/src/components/Textarea.vue +127 -0
- package/src/components/Timeline.vue +20 -0
- package/src/components/TimelineItem.vue +62 -0
- package/src/components/Toast.vue +122 -0
- package/src/components/Tooltip.vue +102 -0
- package/src/components/Typography.vue +51 -0
- package/src/index.js +233 -0
- package/src/layouts/AuthLayout.vue +124 -0
- package/src/layouts/DefaultLayout.vue +74 -0
- package/src/layouts/ErrorLayout.vue +92 -0
- package/src/main.js +13 -0
- package/src/router/index.js +8 -0
- package/src/stores/counter.js +12 -0
- package/src/styles/base.css +48 -0
- package/src/utils/cn.js +10 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative" ref="searchRef">
|
|
3
|
+
<!-- Input -->
|
|
4
|
+
<div class="relative">
|
|
5
|
+
<!-- Left Icon -->
|
|
6
|
+
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 pointer-events-none">
|
|
7
|
+
<SearchIcon :class="iconClasses" />
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<!-- Search Input -->
|
|
11
|
+
<input
|
|
12
|
+
ref="inputRef"
|
|
13
|
+
:value="modelValue"
|
|
14
|
+
:placeholder="placeholder"
|
|
15
|
+
:disabled="disabled"
|
|
16
|
+
:class="inputClasses"
|
|
17
|
+
@input="handleInput"
|
|
18
|
+
@focus="handleFocus"
|
|
19
|
+
@blur="handleBlur"
|
|
20
|
+
@keydown="handleKeydown"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<!-- Right Actions -->
|
|
24
|
+
<div
|
|
25
|
+
v-if="modelValue || loading"
|
|
26
|
+
class="absolute right-3 top-1/2 transform -translate-y-1/2"
|
|
27
|
+
>
|
|
28
|
+
<!-- Clear -->
|
|
29
|
+
<button
|
|
30
|
+
v-if="!loading && clearable"
|
|
31
|
+
@click="handleClear"
|
|
32
|
+
class="text-slate-400 hover:text-slate-600 transition-colors p-1 rounded-full hover:bg-slate-100"
|
|
33
|
+
:aria-label="clearLabel"
|
|
34
|
+
>
|
|
35
|
+
<XMarkIcon class="w-4 h-4" />
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
<!-- Loading -->
|
|
39
|
+
<div v-else-if="loading" class="animate-spin">
|
|
40
|
+
<LoadingIcon class="w-4 h-4 text-slate-400" />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Dropdown -->
|
|
46
|
+
<Transition
|
|
47
|
+
enter-active-class="transition-all duration-200"
|
|
48
|
+
enter-from-class="opacity-0 scale-95 translate-y-1"
|
|
49
|
+
enter-to-class="opacity-100 scale-100 translate-y-0"
|
|
50
|
+
leave-active-class="transition-all duration-150"
|
|
51
|
+
leave-from-class="opacity-100 scale-100 translate-y-0"
|
|
52
|
+
leave-to-class="opacity-0 scale-95 translate-y-1"
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
v-if="showResults && (results.length > 0 || showNoResults)"
|
|
56
|
+
:class="resultsClasses"
|
|
57
|
+
>
|
|
58
|
+
<!-- Results -->
|
|
59
|
+
<div v-if="results.length > 0" class="max-h-64 overflow-y-auto">
|
|
60
|
+
<button
|
|
61
|
+
v-for="(result, index) in results"
|
|
62
|
+
:key="result.id || index"
|
|
63
|
+
:class="getResultClasses(index)"
|
|
64
|
+
@click="selectResult(result, index)"
|
|
65
|
+
@mouseenter="focusedIndex = index"
|
|
66
|
+
>
|
|
67
|
+
<component
|
|
68
|
+
v-if="result.icon"
|
|
69
|
+
:is="result.icon"
|
|
70
|
+
class="w-4 h-4 mr-3 flex-shrink-0"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<div class="flex-1 text-left">
|
|
74
|
+
<div
|
|
75
|
+
class="font-medium text-slate-900"
|
|
76
|
+
v-html="highlightMatch(result.title)"
|
|
77
|
+
/>
|
|
78
|
+
<div
|
|
79
|
+
v-if="result.description"
|
|
80
|
+
class="text-sm text-slate-500 truncate"
|
|
81
|
+
v-html="highlightMatch(result.description)"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div v-if="result.category" class="text-xs text-slate-400 ml-3">
|
|
86
|
+
{{ result.category }}
|
|
87
|
+
</div>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- No Results -->
|
|
92
|
+
<div
|
|
93
|
+
v-else-if="showNoResults"
|
|
94
|
+
class="px-4 py-3 text-sm text-slate-500 text-center"
|
|
95
|
+
>
|
|
96
|
+
{{ noResultsText }}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Footer Slot -->
|
|
100
|
+
<div v-if="$slots.footer" class="border-t border-slate-200 p-2">
|
|
101
|
+
<slot name="footer" />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</Transition>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<script setup>
|
|
109
|
+
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
110
|
+
import { cva } from 'class-variance-authority'
|
|
111
|
+
import { cn } from '../utils/cn.js'
|
|
112
|
+
|
|
113
|
+
// Icons
|
|
114
|
+
const SearchIcon = {
|
|
115
|
+
template: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
116
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/>
|
|
117
|
+
</svg>`
|
|
118
|
+
}
|
|
119
|
+
const XMarkIcon = {
|
|
120
|
+
template: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
121
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
|
122
|
+
</svg>`
|
|
123
|
+
}
|
|
124
|
+
const LoadingIcon = {
|
|
125
|
+
template: `<svg fill="none" viewBox="0 0 24 24" class="animate-spin">
|
|
126
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
127
|
+
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
|
128
|
+
</svg>`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const props = defineProps({
|
|
132
|
+
modelValue: { type: String, default: '' },
|
|
133
|
+
results: { type: Array, default: () => [] },
|
|
134
|
+
placeholder: { type: String, default: 'Search...' },
|
|
135
|
+
disabled: { type: Boolean, default: false },
|
|
136
|
+
loading: { type: Boolean, default: false },
|
|
137
|
+
clearable: { type: Boolean, default: true },
|
|
138
|
+
clearLabel: { type: String, default: 'Clear search' },
|
|
139
|
+
size: {
|
|
140
|
+
type: String,
|
|
141
|
+
default: 'md',
|
|
142
|
+
validator: (v) => ['sm', 'md', 'lg'].includes(v)
|
|
143
|
+
},
|
|
144
|
+
debounce: { type: Number, default: 300 },
|
|
145
|
+
minLength: { type: Number, default: 1 },
|
|
146
|
+
showNoResults: { type: Boolean, default: true },
|
|
147
|
+
noResultsText: { type: String, default: 'No results found' },
|
|
148
|
+
highlightMatches: { type: Boolean, default: true }
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const emit = defineEmits([
|
|
152
|
+
'update:modelValue',
|
|
153
|
+
'search',
|
|
154
|
+
'select',
|
|
155
|
+
'clear',
|
|
156
|
+
'focus',
|
|
157
|
+
'blur'
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
// State
|
|
161
|
+
const searchRef = ref(null)
|
|
162
|
+
const inputRef = ref(null)
|
|
163
|
+
const showResults = ref(false)
|
|
164
|
+
const focusedIndex = ref(-1)
|
|
165
|
+
const debounceTimer = ref(null)
|
|
166
|
+
|
|
167
|
+
// Handlers
|
|
168
|
+
const handleInput = (e) => {
|
|
169
|
+
const value = e.target.value
|
|
170
|
+
emit('update:modelValue', value)
|
|
171
|
+
|
|
172
|
+
if (debounceTimer.value) clearTimeout(debounceTimer.value)
|
|
173
|
+
|
|
174
|
+
debounceTimer.value = setTimeout(() => {
|
|
175
|
+
if (value.length >= props.minLength) {
|
|
176
|
+
emit('search', value)
|
|
177
|
+
showResults.value = true
|
|
178
|
+
} else {
|
|
179
|
+
showResults.value = false
|
|
180
|
+
}
|
|
181
|
+
}, props.debounce)
|
|
182
|
+
}
|
|
183
|
+
const handleFocus = (e) => {
|
|
184
|
+
emit('focus', e)
|
|
185
|
+
if (props.modelValue.length >= props.minLength) showResults.value = true
|
|
186
|
+
}
|
|
187
|
+
const handleBlur = (e) => {
|
|
188
|
+
emit('blur', e)
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
showResults.value = false
|
|
191
|
+
focusedIndex.value = -1
|
|
192
|
+
}, 150)
|
|
193
|
+
}
|
|
194
|
+
const handleClear = () => {
|
|
195
|
+
emit('update:modelValue', '')
|
|
196
|
+
emit('clear')
|
|
197
|
+
showResults.value = false
|
|
198
|
+
inputRef.value?.focus()
|
|
199
|
+
}
|
|
200
|
+
const selectResult = (result) => {
|
|
201
|
+
emit('select', result)
|
|
202
|
+
showResults.value = false
|
|
203
|
+
focusedIndex.value = -1
|
|
204
|
+
}
|
|
205
|
+
const handleKeydown = (e) => {
|
|
206
|
+
if (!showResults.value || props.results.length === 0) return
|
|
207
|
+
switch (e.key) {
|
|
208
|
+
case 'ArrowDown':
|
|
209
|
+
e.preventDefault()
|
|
210
|
+
focusedIndex.value = Math.min(focusedIndex.value + 1, props.results.length - 1)
|
|
211
|
+
break
|
|
212
|
+
case 'ArrowUp':
|
|
213
|
+
e.preventDefault()
|
|
214
|
+
focusedIndex.value = Math.max(focusedIndex.value - 1, -1)
|
|
215
|
+
break
|
|
216
|
+
case 'Enter':
|
|
217
|
+
e.preventDefault()
|
|
218
|
+
if (focusedIndex.value >= 0) selectResult(props.results[focusedIndex.value])
|
|
219
|
+
break
|
|
220
|
+
case 'Escape':
|
|
221
|
+
e.preventDefault()
|
|
222
|
+
showResults.value = false
|
|
223
|
+
focusedIndex.value = -1
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Helpers
|
|
229
|
+
const highlightMatch = (text) => {
|
|
230
|
+
if (!props.highlightMatches || !props.modelValue || !text) return text
|
|
231
|
+
const regex = new RegExp(`(${props.modelValue})`, 'gi')
|
|
232
|
+
return text.replace(regex, '<mark class="bg-yellow-200">$1</mark>')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Variants
|
|
236
|
+
const inputVariants = cva(
|
|
237
|
+
'block w-full rounded-lg border bg-white transition-colors duration-200 placeholder:text-slate-400 disabled:bg-slate-50 disabled:text-slate-500 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
|
238
|
+
{
|
|
239
|
+
variants: {
|
|
240
|
+
size: {
|
|
241
|
+
sm: 'px-9 py-1.5 text-sm',
|
|
242
|
+
md: 'px-10 py-2 text-sm',
|
|
243
|
+
lg: 'px-12 py-3 text-base'
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
defaultVariants: { size: 'md' }
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const iconVariants = cva('text-slate-400', {
|
|
251
|
+
variants: {
|
|
252
|
+
size: {
|
|
253
|
+
sm: 'w-4 h-4',
|
|
254
|
+
md: 'w-4 h-4',
|
|
255
|
+
lg: 'w-5 h-5'
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
defaultVariants: { size: 'md' }
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const inputClasses = computed(() => cn(inputVariants({ size: props.size })))
|
|
262
|
+
const iconClasses = computed(() => cn(iconVariants({ size: props.size })))
|
|
263
|
+
const resultsClasses = computed(() =>
|
|
264
|
+
cn('absolute z-50 w-full mt-1 bg-white rounded-lg shadow-lg border border-slate-200 max-h-96 overflow-hidden')
|
|
265
|
+
)
|
|
266
|
+
const getResultClasses = (i) =>
|
|
267
|
+
cn(
|
|
268
|
+
'flex items-center w-full px-4 py-3 text-left transition-colors duration-150 hover:bg-slate-50 focus:bg-slate-50 focus:outline-none',
|
|
269
|
+
{ 'bg-slate-50': focusedIndex.value === i }
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Outside click
|
|
273
|
+
const handleClickOutside = (e) => {
|
|
274
|
+
if (searchRef.value && !searchRef.value.contains(e.target)) {
|
|
275
|
+
showResults.value = false
|
|
276
|
+
focusedIndex.value = -1
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onMounted(() => document.addEventListener('click', handleClickOutside))
|
|
281
|
+
onUnmounted(() => {
|
|
282
|
+
document.removeEventListener('click', handleClickOutside)
|
|
283
|
+
if (debounceTimer.value) clearTimeout(debounceTimer.value)
|
|
284
|
+
})
|
|
285
|
+
watch(() => props.results, () => (focusedIndex.value = -1))
|
|
286
|
+
</script>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<select
|
|
4
|
+
:id="id || selectId"
|
|
5
|
+
ref="selectRef"
|
|
6
|
+
:value="modelValue"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
:required="required"
|
|
9
|
+
:class="selectClasses"
|
|
10
|
+
:aria-describedby="ariaDescribedBy"
|
|
11
|
+
:aria-invalid="hasError"
|
|
12
|
+
@change="handleChange"
|
|
13
|
+
@blur="handleBlur"
|
|
14
|
+
@focus="handleFocus"
|
|
15
|
+
>
|
|
16
|
+
<option v-if="placeholder" value="" disabled>
|
|
17
|
+
{{ placeholder }}
|
|
18
|
+
</option>
|
|
19
|
+
|
|
20
|
+
<slot />
|
|
21
|
+
</select>
|
|
22
|
+
|
|
23
|
+
<!-- Custom dropdown arrow -->
|
|
24
|
+
<div class="absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none">
|
|
25
|
+
<ChevronDownIcon :class="iconClasses" />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import { computed, ref, useId } from 'vue'
|
|
32
|
+
import { cva } from 'class-variance-authority'
|
|
33
|
+
import { cn } from '../utils/cn.js'
|
|
34
|
+
|
|
35
|
+
// ChevronDown as inline component
|
|
36
|
+
const ChevronDownIcon = {
|
|
37
|
+
template: `
|
|
38
|
+
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
39
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
40
|
+
</svg>
|
|
41
|
+
`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const props = defineProps({
|
|
45
|
+
modelValue: [String, Number, Boolean],
|
|
46
|
+
placeholder: String,
|
|
47
|
+
disabled: Boolean,
|
|
48
|
+
required: Boolean,
|
|
49
|
+
error: String,
|
|
50
|
+
id: String,
|
|
51
|
+
size: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: 'md',
|
|
54
|
+
validator: (v) => ['sm', 'md', 'lg'].includes(v)
|
|
55
|
+
},
|
|
56
|
+
variant: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: 'default',
|
|
59
|
+
validator: (v) => ['default', 'error', 'success'].includes(v)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const emit = defineEmits(['update:modelValue', 'change', 'blur', 'focus'])
|
|
64
|
+
const selectRef = ref(null)
|
|
65
|
+
const selectId = useId()
|
|
66
|
+
|
|
67
|
+
// events
|
|
68
|
+
const handleChange = (event) => {
|
|
69
|
+
const value = event.target.value
|
|
70
|
+
emit('update:modelValue', value)
|
|
71
|
+
emit('change', value)
|
|
72
|
+
}
|
|
73
|
+
const handleBlur = (event) => emit('blur', event)
|
|
74
|
+
const handleFocus = (event) => emit('focus', event)
|
|
75
|
+
|
|
76
|
+
// accessibility
|
|
77
|
+
const hasError = computed(() => !!props.error || props.variant === 'error')
|
|
78
|
+
const ariaDescribedBy = computed(() =>
|
|
79
|
+
props.error ? `${selectId}-error` : undefined
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// variants with cva
|
|
83
|
+
const selectVariants = cva(
|
|
84
|
+
'block w-full rounded-lg border bg-background pr-10 appearance-none transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-0 disabled:bg-slate-50 disabled:text-slate-500 disabled:cursor-not-allowed',
|
|
85
|
+
{
|
|
86
|
+
variants: {
|
|
87
|
+
size: {
|
|
88
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
89
|
+
md: 'px-3 py-2 text-sm',
|
|
90
|
+
lg: 'px-4 py-3 text-base'
|
|
91
|
+
},
|
|
92
|
+
variant: {
|
|
93
|
+
default: 'border-slate-300 focus:border-blue-500 focus:ring-blue-500',
|
|
94
|
+
error: 'border-red-300 focus:border-red-500 focus:ring-red-500',
|
|
95
|
+
success:
|
|
96
|
+
'border-green-300 focus:border-green-500 focus:ring-green-500'
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
defaultVariants: {
|
|
100
|
+
size: 'md',
|
|
101
|
+
variant: 'default'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const selectClasses = computed(() =>
|
|
107
|
+
cn(selectVariants({ size: props.size, variant: props.variant }))
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const iconClasses = computed(() =>
|
|
111
|
+
cn({
|
|
112
|
+
sm: 'w-4 h-4',
|
|
113
|
+
md: 'w-4 h-4',
|
|
114
|
+
lg: 'w-5 h-5'
|
|
115
|
+
}[props.size])
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// expose focus/blur
|
|
119
|
+
defineExpose({
|
|
120
|
+
focus: () => selectRef.value?.focus(),
|
|
121
|
+
blur: () => selectRef.value?.blur()
|
|
122
|
+
})
|
|
123
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside :class="cn('bg-white border-r border-gray-200 h-full', className)">
|
|
3
|
+
<div class="p-6">
|
|
4
|
+
<!-- Sidebar Header -->
|
|
5
|
+
<div class="mb-8">
|
|
6
|
+
<slot name="header">
|
|
7
|
+
<Logo />
|
|
8
|
+
</slot>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- Navigation -->
|
|
12
|
+
<nav class="space-y-2">
|
|
13
|
+
<slot name="navigation">
|
|
14
|
+
<SidebarNavigation :items="items" />
|
|
15
|
+
</slot>
|
|
16
|
+
</nav>
|
|
17
|
+
|
|
18
|
+
<!-- Footer -->
|
|
19
|
+
<div class="mt-auto pt-8">
|
|
20
|
+
<slot name="footer">
|
|
21
|
+
<Divider class="mb-4" />
|
|
22
|
+
<div class="flex items-center space-x-3">
|
|
23
|
+
<Avatar size="sm" />
|
|
24
|
+
<div>
|
|
25
|
+
<Text class="text-sm font-medium">John Doe</Text>
|
|
26
|
+
<Text class="text-xs text-gray-500">john@example.com</Text>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</slot>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</aside>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import { cn } from '../utils/cn.js'
|
|
37
|
+
|
|
38
|
+
defineProps({
|
|
39
|
+
items: {
|
|
40
|
+
type: Array,
|
|
41
|
+
default: () => [
|
|
42
|
+
{ label: 'Dashboard', href: '/dashboard', icon: 'home', active: true },
|
|
43
|
+
{ label: 'Users', href: '/users', icon: 'users' },
|
|
44
|
+
{ label: 'Settings', href: '/settings', icon: 'settings' }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
className: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: ''
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full">
|
|
3
|
+
<input
|
|
4
|
+
type="range"
|
|
5
|
+
:min="min"
|
|
6
|
+
:max="max"
|
|
7
|
+
:step="step"
|
|
8
|
+
v-model="internalValue"
|
|
9
|
+
@input="updateValue"
|
|
10
|
+
class="w-full accent-blue-500 cursor-pointer"
|
|
11
|
+
/>
|
|
12
|
+
<div class="flex justify-between text-xs text-slate-500 mt-1">
|
|
13
|
+
<span>{{ min }}</span>
|
|
14
|
+
<span>{{ internalValue }}</span>
|
|
15
|
+
<span>{{ max }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import { ref, watch } from "vue"
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
modelValue: { type: Number, default: 0 },
|
|
25
|
+
min: { type: Number, default: 0 },
|
|
26
|
+
max: { type: Number, default: 100 },
|
|
27
|
+
step: { type: Number, default: 1 }
|
|
28
|
+
})
|
|
29
|
+
const emit = defineEmits(["update:modelValue"])
|
|
30
|
+
|
|
31
|
+
const internalValue = ref(props.modelValue)
|
|
32
|
+
|
|
33
|
+
watch(() => props.modelValue, (val) => {
|
|
34
|
+
internalValue.value = val
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const updateValue = () => {
|
|
38
|
+
emit("update:modelValue", Number(internalValue.value))
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="spinnerClasses" role="status" :aria-label="label">
|
|
3
|
+
<svg
|
|
4
|
+
:class="svgClasses"
|
|
5
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
6
|
+
fill="none"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
>
|
|
9
|
+
<circle
|
|
10
|
+
class="opacity-25"
|
|
11
|
+
cx="12"
|
|
12
|
+
cy="12"
|
|
13
|
+
r="10"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
stroke-width="4"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
class="opacity-75"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
<span v-if="label" class="sr-only">{{ label }}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup>
|
|
28
|
+
import { computed } from 'vue'
|
|
29
|
+
import { cva } from 'class-variance-authority'
|
|
30
|
+
import { cn } from '../utils/cn.js'
|
|
31
|
+
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
size: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: 'md',
|
|
36
|
+
validator: (value) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(value)
|
|
37
|
+
},
|
|
38
|
+
variant: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: 'default',
|
|
41
|
+
validator: (value) => ['default', 'primary', 'secondary'].includes(value)
|
|
42
|
+
},
|
|
43
|
+
label: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: 'Loading...'
|
|
46
|
+
},
|
|
47
|
+
class: String
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const spinnerVariants = cva(
|
|
51
|
+
'inline-flex items-center justify-center',
|
|
52
|
+
{
|
|
53
|
+
variants: {
|
|
54
|
+
variant: {
|
|
55
|
+
default: 'text-muted-foreground',
|
|
56
|
+
primary: 'text-primary',
|
|
57
|
+
secondary: 'text-secondary-foreground'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const sizeVariants = cva(
|
|
64
|
+
'animate-spin',
|
|
65
|
+
{
|
|
66
|
+
variants: {
|
|
67
|
+
size: {
|
|
68
|
+
xs: 'h-3 w-3',
|
|
69
|
+
sm: 'h-4 w-4',
|
|
70
|
+
md: 'h-6 w-6',
|
|
71
|
+
lg: 'h-8 w-8',
|
|
72
|
+
xl: 'h-12 w-12'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const spinnerClasses = computed(() =>
|
|
79
|
+
cn(spinnerVariants({ variant: props.variant }), props.class)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const svgClasses = computed(() =>
|
|
83
|
+
sizeVariants({ size: props.size })
|
|
84
|
+
)
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center">
|
|
3
|
+
<div
|
|
4
|
+
:class="[
|
|
5
|
+
'flex items-center justify-center w-8 h-8 rounded-full border-2',
|
|
6
|
+
active ? 'bg-blue-500 border-blue-500 text-white' :
|
|
7
|
+
completed ? 'bg-green-500 border-green-500 text-white' :
|
|
8
|
+
'border-slate-300 text-slate-500'
|
|
9
|
+
]"
|
|
10
|
+
>
|
|
11
|
+
<span>{{ step }}</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="ml-2">
|
|
14
|
+
<div class="font-medium">{{ title }}</div>
|
|
15
|
+
<div v-if="description" class="text-sm text-slate-400">{{ description }}</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
step: { type: Number, required: true },
|
|
23
|
+
title: { type: String, required: true },
|
|
24
|
+
description: { type: String, default: "" },
|
|
25
|
+
active: { type: Boolean, default: false },
|
|
26
|
+
completed: { type: Boolean, default: false }
|
|
27
|
+
})
|
|
28
|
+
</script>
|