@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.
Files changed (83) hide show
  1. package/README.md +35 -0
  2. package/dist/favicon.ico +0 -0
  3. package/dist/index.esm.js +21608 -0
  4. package/dist/index.umd.js +57 -0
  5. package/dist/vue-ui.css +1 -0
  6. package/package.json +71 -0
  7. package/src/App.vue +147 -0
  8. package/src/__tests__/App.spec.js +11 -0
  9. package/src/components/Accordion.vue +270 -0
  10. package/src/components/AccordionItem.vue +106 -0
  11. package/src/components/Alert.vue +207 -0
  12. package/src/components/Avatar.vue +92 -0
  13. package/src/components/Badge.vue +39 -0
  14. package/src/components/Breadcrumb.vue +67 -0
  15. package/src/components/Button.vue +61 -0
  16. package/src/components/ButtonGroup.vue +43 -0
  17. package/src/components/Calendar.vue +158 -0
  18. package/src/components/Card.vue +13 -0
  19. package/src/components/CardBody.vue +33 -0
  20. package/src/components/CardContent.vue +13 -0
  21. package/src/components/CardFooter.vue +33 -0
  22. package/src/components/CardHeader.vue +13 -0
  23. package/src/components/CardTitle.vue +13 -0
  24. package/src/components/Checkbox.vue +83 -0
  25. package/src/components/DataTable.vue +52 -0
  26. package/src/components/DataTableCell.vue +56 -0
  27. package/src/components/DataTableFilters.vue +74 -0
  28. package/src/components/DataTableHeader.vue +11 -0
  29. package/src/components/DataTablePagination.vue +103 -0
  30. package/src/components/DataTableRow.vue +58 -0
  31. package/src/components/DataTableToolBar.vue +51 -0
  32. package/src/components/DatePicker.vue +69 -0
  33. package/src/components/Divider.vue +74 -0
  34. package/src/components/Dropdown.vue +113 -0
  35. package/src/components/DropdownItem.vue +59 -0
  36. package/src/components/FileUpload.vue +155 -0
  37. package/src/components/Footer.vue +71 -0
  38. package/src/components/FormField.vue +134 -0
  39. package/src/components/Header.vue +51 -0
  40. package/src/components/Icon.vue +34 -0
  41. package/src/components/Image.vue +117 -0
  42. package/src/components/Input.vue +17 -0
  43. package/src/components/InputGroup.vue +38 -0
  44. package/src/components/Label.vue +44 -0
  45. package/src/components/Link.vue +64 -0
  46. package/src/components/ListItem.vue +82 -0
  47. package/src/components/Logo.vue +70 -0
  48. package/src/components/MainNavigation.vue +37 -0
  49. package/src/components/MenuItem.vue +61 -0
  50. package/src/components/Modal.vue +77 -0
  51. package/src/components/ModalBody.vue +32 -0
  52. package/src/components/ModalFooter.vue +32 -0
  53. package/src/components/ModalHeader.vue +18 -0
  54. package/src/components/Notification.vue +126 -0
  55. package/src/components/Option.vue +72 -0
  56. package/src/components/ProgressBar.vue +88 -0
  57. package/src/components/Radio.vue +91 -0
  58. package/src/components/Search.vue +286 -0
  59. package/src/components/Select.vue +123 -0
  60. package/src/components/Sidebar.vue +52 -0
  61. package/src/components/Slider.vue +40 -0
  62. package/src/components/Spinner.vue +85 -0
  63. package/src/components/Stepper.vue +9 -0
  64. package/src/components/StepperItem.vue +28 -0
  65. package/src/components/Switch.vue +120 -0
  66. package/src/components/Tab.vue +49 -0
  67. package/src/components/TabPanel.vue +27 -0
  68. package/src/components/Text.vue +80 -0
  69. package/src/components/Textarea.vue +127 -0
  70. package/src/components/Timeline.vue +20 -0
  71. package/src/components/TimelineItem.vue +62 -0
  72. package/src/components/Toast.vue +122 -0
  73. package/src/components/Tooltip.vue +102 -0
  74. package/src/components/Typography.vue +51 -0
  75. package/src/index.js +233 -0
  76. package/src/layouts/AuthLayout.vue +124 -0
  77. package/src/layouts/DefaultLayout.vue +74 -0
  78. package/src/layouts/ErrorLayout.vue +92 -0
  79. package/src/main.js +13 -0
  80. package/src/router/index.js +8 -0
  81. package/src/stores/counter.js +12 -0
  82. package/src/styles/base.css +48 -0
  83. 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,9 @@
1
+ <template>
2
+ <div class="flex items-center space-x-4">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ // Simple wrapper, steps are handled by <StepperItem />
9
+ </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>