@sabrenski/spire-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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/index.d.ts +4981 -0
  4. package/dist/spire-ui.css +1 -0
  5. package/dist/spire-ui.es.js +18403 -0
  6. package/dist/spire-ui.umd.js +45 -0
  7. package/package.json +83 -0
  8. package/src/components/Accordion/Accordion.test.ts +218 -0
  9. package/src/components/Accordion/AccordionContent.vue +112 -0
  10. package/src/components/Accordion/AccordionItem.vue +87 -0
  11. package/src/components/Accordion/AccordionRoot.vue +111 -0
  12. package/src/components/Accordion/AccordionTrigger.vue +125 -0
  13. package/src/components/Accordion/index.ts +11 -0
  14. package/src/components/Accordion/keys.ts +23 -0
  15. package/src/components/Avatar/Avatar.test.ts +181 -0
  16. package/src/components/Avatar/Avatar.vue +150 -0
  17. package/src/components/Avatar/index.ts +2 -0
  18. package/src/components/Badge/Badge.test.ts +141 -0
  19. package/src/components/Badge/Badge.vue +133 -0
  20. package/src/components/Badge/index.ts +2 -0
  21. package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
  22. package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
  23. package/src/components/BadgeContainer/index.ts +2 -0
  24. package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
  25. package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
  26. package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
  27. package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
  28. package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
  29. package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
  30. package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
  31. package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
  32. package/src/components/Breadcrumb/index.ts +13 -0
  33. package/src/components/Breadcrumb/keys.ts +7 -0
  34. package/src/components/Button/Button.test.ts +231 -0
  35. package/src/components/Button/Button.vue +349 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Callout/Callout.test.ts +260 -0
  38. package/src/components/Callout/Callout.vue +341 -0
  39. package/src/components/Callout/index.ts +2 -0
  40. package/src/components/Card/Card.test.ts +565 -0
  41. package/src/components/Card/Card.vue +209 -0
  42. package/src/components/Card/CardContent.vue +57 -0
  43. package/src/components/Card/CardFooter.vue +72 -0
  44. package/src/components/Card/CardHeader.vue +111 -0
  45. package/src/components/Card/CardImage.vue +124 -0
  46. package/src/components/Card/index.ts +14 -0
  47. package/src/components/Chart/BarChart.vue +208 -0
  48. package/src/components/Chart/BaseChart.vue +444 -0
  49. package/src/components/Chart/Chart.test.ts +359 -0
  50. package/src/components/Chart/DonutChart.vue +283 -0
  51. package/src/components/Chart/LineChart.vue +211 -0
  52. package/src/components/Chart/index.ts +20 -0
  53. package/src/components/Chart/useChartTheme.ts +192 -0
  54. package/src/components/Checkbox/Checkbox.test.ts +209 -0
  55. package/src/components/Checkbox/Checkbox.vue +285 -0
  56. package/src/components/Checkbox/index.ts +2 -0
  57. package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
  58. package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
  59. package/src/components/ChoiceChip/index.ts +2 -0
  60. package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
  61. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
  62. package/src/components/ChoiceChipGroup/index.ts +2 -0
  63. package/src/components/ColorPicker/ColorArea.vue +159 -0
  64. package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
  65. package/src/components/ColorPicker/ColorPicker.vue +339 -0
  66. package/src/components/ColorPicker/ColorSlider.vue +191 -0
  67. package/src/components/ColorPicker/index.ts +7 -0
  68. package/src/components/Combobox/Combobox.test.ts +891 -0
  69. package/src/components/Combobox/Combobox.vue +934 -0
  70. package/src/components/Combobox/index.ts +2 -0
  71. package/src/components/DataTable/DataTable.test.ts +1221 -0
  72. package/src/components/DataTable/DataTable.vue +1415 -0
  73. package/src/components/DataTable/index.ts +10 -0
  74. package/src/components/DatePicker/DatePicker.test.ts +625 -0
  75. package/src/components/DatePicker/DatePicker.vue +1586 -0
  76. package/src/components/DatePicker/index.ts +2 -0
  77. package/src/components/Drawer/Drawer.test.ts +336 -0
  78. package/src/components/Drawer/Drawer.vue +466 -0
  79. package/src/components/Drawer/index.ts +2 -0
  80. package/src/components/Dropdown/Dropdown.test.ts +607 -0
  81. package/src/components/Dropdown/Dropdown.vue +807 -0
  82. package/src/components/Dropdown/DropdownItem.vue +227 -0
  83. package/src/components/Dropdown/DropdownSeparator.vue +14 -0
  84. package/src/components/Dropdown/DropdownSub.vue +104 -0
  85. package/src/components/Dropdown/DropdownSubContent.vue +187 -0
  86. package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
  87. package/src/components/Dropdown/index.ts +14 -0
  88. package/src/components/EmptyState/EmptyState.test.ts +180 -0
  89. package/src/components/EmptyState/EmptyState.vue +137 -0
  90. package/src/components/EmptyState/index.ts +2 -0
  91. package/src/components/FileUpload/FileUpload.test.ts +1151 -0
  92. package/src/components/FileUpload/FileUpload.vue +1042 -0
  93. package/src/components/FileUpload/index.ts +2 -0
  94. package/src/components/Heading/Heading.test.ts +107 -0
  95. package/src/components/Heading/Heading.vue +67 -0
  96. package/src/components/Heading/index.ts +2 -0
  97. package/src/components/Icon/Icon.test.ts +157 -0
  98. package/src/components/Icon/Icon.vue +86 -0
  99. package/src/components/Icon/index.ts +2 -0
  100. package/src/components/Input/Input.test.ts +273 -0
  101. package/src/components/Input/Input.vue +388 -0
  102. package/src/components/Input/index.ts +2 -0
  103. package/src/components/Layout/Container.vue +67 -0
  104. package/src/components/Layout/Grid.vue +159 -0
  105. package/src/components/Layout/GridItem.vue +154 -0
  106. package/src/components/Layout/Layout.test.ts +202 -0
  107. package/src/components/Layout/Stack.vue +128 -0
  108. package/src/components/Layout/index.ts +9 -0
  109. package/src/components/Layout/keys.ts +7 -0
  110. package/src/components/Modal/Modal.test.ts +311 -0
  111. package/src/components/Modal/Modal.vue +336 -0
  112. package/src/components/Modal/index.ts +2 -0
  113. package/src/components/Pagination/Pagination.test.ts +303 -0
  114. package/src/components/Pagination/Pagination.vue +212 -0
  115. package/src/components/Pagination/index.ts +3 -0
  116. package/src/components/Pagination/utils.ts +86 -0
  117. package/src/components/Popover/Popover.test.ts +285 -0
  118. package/src/components/Popover/Popover.vue +441 -0
  119. package/src/components/Popover/index.ts +2 -0
  120. package/src/components/Progress/Progress.test.ts +361 -0
  121. package/src/components/Progress/Progress.vue +363 -0
  122. package/src/components/Progress/index.ts +7 -0
  123. package/src/components/Radio/Radio.test.ts +216 -0
  124. package/src/components/Radio/Radio.vue +214 -0
  125. package/src/components/Radio/index.ts +2 -0
  126. package/src/components/Rating/Rating.test.ts +319 -0
  127. package/src/components/Rating/Rating.vue +247 -0
  128. package/src/components/Rating/index.ts +2 -0
  129. package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
  130. package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
  131. package/src/components/SegmentedControl/index.ts +2 -0
  132. package/src/components/Select/Select.test.ts +589 -0
  133. package/src/components/Select/Select.vue +666 -0
  134. package/src/components/Select/index.ts +2 -0
  135. package/src/components/Sidebar/Sidebar.test.ts +301 -0
  136. package/src/components/Sidebar/SidebarGroup.vue +103 -0
  137. package/src/components/Sidebar/SidebarItem.vue +196 -0
  138. package/src/components/Sidebar/SidebarLayout.vue +42 -0
  139. package/src/components/Sidebar/SidebarRoot.vue +122 -0
  140. package/src/components/Sidebar/index.ts +11 -0
  141. package/src/components/Sidebar/keys.ts +14 -0
  142. package/src/components/Skeleton/Skeleton.test.ts +130 -0
  143. package/src/components/Skeleton/Skeleton.vue +104 -0
  144. package/src/components/Skeleton/index.ts +2 -0
  145. package/src/components/Slider/Slider.test.ts +416 -0
  146. package/src/components/Slider/Slider.vue +435 -0
  147. package/src/components/Slider/index.ts +2 -0
  148. package/src/components/Slider/utils.ts +91 -0
  149. package/src/components/Spinner/Spinner.test.ts +79 -0
  150. package/src/components/Spinner/Spinner.vue +159 -0
  151. package/src/components/Spinner/index.ts +2 -0
  152. package/src/components/SpireProvider/SpireProvider.vue +71 -0
  153. package/src/components/SpireProvider/index.ts +11 -0
  154. package/src/components/Stepper/Stepper.test.ts +221 -0
  155. package/src/components/Stepper/StepperContent.vue +51 -0
  156. package/src/components/Stepper/StepperItem.vue +89 -0
  157. package/src/components/Stepper/StepperRoot.vue +101 -0
  158. package/src/components/Stepper/StepperSeparator.vue +52 -0
  159. package/src/components/Stepper/StepperTrigger.vue +144 -0
  160. package/src/components/Stepper/index.ts +11 -0
  161. package/src/components/Stepper/keys.ts +27 -0
  162. package/src/components/Switch/Switch.test.ts +214 -0
  163. package/src/components/Switch/Switch.vue +235 -0
  164. package/src/components/Switch/index.ts +2 -0
  165. package/src/components/Tabs/Tabs.test.ts +363 -0
  166. package/src/components/Tabs/Tabs.vue +318 -0
  167. package/src/components/Tabs/index.ts +2 -0
  168. package/src/components/Text/Text.test.ts +154 -0
  169. package/src/components/Text/Text.vue +100 -0
  170. package/src/components/Text/index.ts +2 -0
  171. package/src/components/Textarea/Textarea.test.ts +432 -0
  172. package/src/components/Textarea/Textarea.vue +411 -0
  173. package/src/components/Textarea/index.ts +2 -0
  174. package/src/components/TimePicker/TimePicker.test.ts +352 -0
  175. package/src/components/TimePicker/TimePicker.vue +569 -0
  176. package/src/components/TimePicker/index.ts +2 -0
  177. package/src/components/Timeline/Timeline.test.ts +193 -0
  178. package/src/components/Timeline/Timeline.vue +111 -0
  179. package/src/components/Timeline/TimelineItem.vue +167 -0
  180. package/src/components/Timeline/index.ts +13 -0
  181. package/src/components/Timeline/keys.ts +21 -0
  182. package/src/components/Toast/ToastItem.test.ts +289 -0
  183. package/src/components/Toast/ToastItem.vue +370 -0
  184. package/src/components/Toast/ToastProvider.test.ts +158 -0
  185. package/src/components/Toast/ToastProvider.vue +181 -0
  186. package/src/components/Toast/index.ts +83 -0
  187. package/src/components/Toast/toastState.test.ts +165 -0
  188. package/src/components/Toast/toastState.ts +161 -0
  189. package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
  190. package/src/components/ToggleButton/ToggleButton.vue +197 -0
  191. package/src/components/ToggleButton/index.ts +2 -0
  192. package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
  193. package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
  194. package/src/components/ToggleGroup/index.ts +2 -0
  195. package/src/components/Tooltip/Tooltip.test.ts +238 -0
  196. package/src/components/Tooltip/Tooltip.vue +217 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/components/TreeView/TreeView.test.ts +357 -0
  199. package/src/components/TreeView/TreeView.vue +251 -0
  200. package/src/components/TreeView/TreeViewItem.vue +288 -0
  201. package/src/components/TreeView/index.ts +11 -0
  202. package/src/components/TreeView/keys.ts +35 -0
  203. package/src/composables/index.ts +12 -0
  204. package/src/composables/useClickOutside.ts +36 -0
  205. package/src/composables/useClipboard.ts +35 -0
  206. package/src/composables/useEventListener.ts +48 -0
  207. package/src/composables/useFocusTrap.ts +58 -0
  208. package/src/composables/useHoverReveal.ts +98 -0
  209. package/src/composables/useId.ts +10 -0
  210. package/src/composables/useMagnetic.ts +171 -0
  211. package/src/composables/useRelativePosition.ts +127 -0
  212. package/src/composables/useRipple.ts +146 -0
  213. package/src/composables/useScrollLock.ts +25 -0
  214. package/src/composables/useSpireConfig.ts +27 -0
  215. package/src/composables/useStagger.ts +224 -0
  216. package/src/config/icons.test.ts +115 -0
  217. package/src/config/icons.ts +170 -0
  218. package/src/index.ts +361 -0
  219. package/src/styles/depth.css +129 -0
  220. package/src/styles/effects.css +169 -0
  221. package/src/styles/fallback.css +152 -0
  222. package/src/styles/main.css +25 -0
  223. package/src/styles/mood.css +211 -0
  224. package/src/styles/motion.css +159 -0
  225. package/src/styles/reset.css +97 -0
  226. package/src/styles/theme.css +708 -0
  227. package/src/styles/tokens.css +183 -0
  228. package/src/utils/.gitkeep +0 -0
  229. package/src/utils/color.ts +277 -0
  230. package/src/utils/date.test.ts +522 -0
  231. package/src/utils/date.ts +380 -0
  232. package/src/utils/index.ts +23 -0
  233. package/src/utils/object.test.ts +80 -0
  234. package/src/utils/object.ts +25 -0
  235. package/src/utils/string.test.ts +64 -0
  236. package/src/utils/string.ts +32 -0
  237. package/src/utils/time.ts +156 -0
@@ -0,0 +1,363 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ export type ProgressVariant = 'linear' | 'circular'
5
+ export type ProgressSize = 'sm' | 'md' | 'lg'
6
+ export type ProgressColor = 'primary' | 'success' | 'warning' | 'error'
7
+
8
+ export interface ProgressProps {
9
+ /** Current progress value (0-100) */
10
+ value?: number
11
+ /** Display variant */
12
+ variant?: ProgressVariant
13
+ /** Size of the progress indicator */
14
+ size?: ProgressSize
15
+ /** Color theme */
16
+ color?: ProgressColor
17
+ /** Show indeterminate animation (unknown duration) */
18
+ indeterminate?: boolean
19
+ /** Show diagonal stripes (linear only) */
20
+ striped?: boolean
21
+ /** Animate stripes (linear only, requires striped) */
22
+ animated?: boolean
23
+ /** Show percentage value label */
24
+ showValue?: boolean
25
+ /** Accessible label describing the progress */
26
+ label?: string
27
+ /** Stroke width for circular variant (default auto based on size) */
28
+ strokeWidth?: number
29
+ }
30
+
31
+ const props = withDefaults(defineProps<ProgressProps>(), {
32
+ value: 0,
33
+ variant: 'linear',
34
+ size: 'md',
35
+ color: 'primary',
36
+ indeterminate: false,
37
+ striped: false,
38
+ animated: false,
39
+ showValue: false
40
+ })
41
+
42
+ const clampedValue = computed(() => Math.min(100, Math.max(0, props.value)))
43
+
44
+ const linearClasses = computed(() => [
45
+ 'ui-progress-linear',
46
+ `ui-progress-linear--${props.size}`,
47
+ `ui-progress-linear--${props.color}`,
48
+ {
49
+ 'ui-progress-linear--indeterminate': props.indeterminate,
50
+ 'ui-progress-linear--striped': props.striped && !props.indeterminate,
51
+ 'ui-progress-linear--animated': props.animated && props.striped && !props.indeterminate
52
+ }
53
+ ])
54
+
55
+ const circularClasses = computed(() => [
56
+ 'ui-progress-circular',
57
+ `ui-progress-circular--${props.size}`,
58
+ `ui-progress-circular--${props.color}`,
59
+ {
60
+ 'ui-progress-circular--indeterminate': props.indeterminate
61
+ }
62
+ ])
63
+
64
+ const circularSize = computed(() => {
65
+ const sizes = { sm: 32, md: 48, lg: 64 }
66
+ return sizes[props.size]
67
+ })
68
+
69
+ const circularStrokeWidth = computed(() => {
70
+ if (props.strokeWidth) return props.strokeWidth
71
+ const widths = { sm: 3, md: 4, lg: 5 }
72
+ return widths[props.size]
73
+ })
74
+
75
+ const circularRadius = computed(() => (circularSize.value - circularStrokeWidth.value) / 2)
76
+ const circularCircumference = computed(() => 2 * Math.PI * circularRadius.value)
77
+ const circularOffset = computed(() =>
78
+ circularCircumference.value - (clampedValue.value / 100) * circularCircumference.value
79
+ )
80
+
81
+ const ariaAttrs = computed(() => {
82
+ const attrs: Record<string, string | number | undefined> = {
83
+ role: 'progressbar',
84
+ 'aria-valuemin': 0,
85
+ 'aria-valuemax': 100,
86
+ 'aria-label': props.label
87
+ }
88
+ if (!props.indeterminate) {
89
+ attrs['aria-valuenow'] = clampedValue.value
90
+ }
91
+ return attrs
92
+ })
93
+
94
+ const indicatorStyle = computed(() => ({
95
+ width: `${clampedValue.value}%`
96
+ }))
97
+ </script>
98
+
99
+ <template>
100
+ <!-- Linear Progress -->
101
+ <div
102
+ v-if="variant === 'linear'"
103
+ :class="linearClasses"
104
+ v-bind="ariaAttrs"
105
+ >
106
+ <div class="ui-progress-linear__track">
107
+ <div
108
+ class="ui-progress-linear__indicator"
109
+ :style="!indeterminate ? indicatorStyle : undefined"
110
+ />
111
+ </div>
112
+ <span v-if="showValue && !indeterminate" class="ui-progress-linear__value">
113
+ {{ clampedValue }}%
114
+ </span>
115
+ </div>
116
+
117
+ <!-- Circular Progress -->
118
+ <div
119
+ v-else
120
+ :class="circularClasses"
121
+ v-bind="ariaAttrs"
122
+ >
123
+ <svg
124
+ :width="circularSize"
125
+ :height="circularSize"
126
+ :viewBox="`0 0 ${circularSize} ${circularSize}`"
127
+ class="ui-progress-circular__svg"
128
+ >
129
+ <!-- Track circle -->
130
+ <circle
131
+ class="ui-progress-circular__track"
132
+ :cx="circularSize / 2"
133
+ :cy="circularSize / 2"
134
+ :r="circularRadius"
135
+ fill="none"
136
+ :stroke-width="circularStrokeWidth"
137
+ />
138
+ <!-- Indicator circle -->
139
+ <circle
140
+ class="ui-progress-circular__indicator"
141
+ :cx="circularSize / 2"
142
+ :cy="circularSize / 2"
143
+ :r="circularRadius"
144
+ fill="none"
145
+ :stroke-width="circularStrokeWidth"
146
+ :stroke-dasharray="circularCircumference"
147
+ :stroke-dashoffset="indeterminate ? circularCircumference * 0.75 : circularOffset"
148
+ stroke-linecap="round"
149
+ />
150
+ </svg>
151
+ <span v-if="showValue && !indeterminate" class="ui-progress-circular__value">
152
+ {{ clampedValue }}%
153
+ </span>
154
+ </div>
155
+ </template>
156
+
157
+ <style scoped>
158
+ /* ============================================
159
+ LINEAR PROGRESS
160
+ ============================================ */
161
+
162
+ .ui-progress-linear {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: var(--space-2);
166
+ width: 100%;
167
+ font-family: var(--font-sans);
168
+ }
169
+
170
+ .ui-progress-linear__track {
171
+ flex: 1;
172
+ background-color: var(--progress-track);
173
+ border-radius: var(--radius-full);
174
+ overflow: hidden;
175
+ }
176
+
177
+ /* Sizes */
178
+ .ui-progress-linear--sm .ui-progress-linear__track {
179
+ height: 4px;
180
+ }
181
+
182
+ .ui-progress-linear--md .ui-progress-linear__track {
183
+ height: 8px;
184
+ }
185
+
186
+ .ui-progress-linear--lg .ui-progress-linear__track {
187
+ height: 12px;
188
+ }
189
+
190
+ /* Indicator */
191
+ .ui-progress-linear__indicator {
192
+ height: 100%;
193
+ border-radius: var(--radius-full);
194
+ transition: width var(--duration-normal) var(--ease-out);
195
+ }
196
+
197
+ /* Colors */
198
+ .ui-progress-linear--primary .ui-progress-linear__indicator {
199
+ background-color: var(--progress-primary);
200
+ }
201
+
202
+ .ui-progress-linear--success .ui-progress-linear__indicator {
203
+ background-color: var(--progress-success);
204
+ }
205
+
206
+ .ui-progress-linear--warning .ui-progress-linear__indicator {
207
+ background-color: var(--progress-warning);
208
+ }
209
+
210
+ .ui-progress-linear--error .ui-progress-linear__indicator {
211
+ background-color: var(--progress-error);
212
+ }
213
+
214
+ /* Striped pattern */
215
+ .ui-progress-linear--striped .ui-progress-linear__indicator {
216
+ background-image: linear-gradient(
217
+ 45deg,
218
+ rgba(255, 255, 255, 0.15) 25%,
219
+ transparent 25%,
220
+ transparent 50%,
221
+ rgba(255, 255, 255, 0.15) 50%,
222
+ rgba(255, 255, 255, 0.15) 75%,
223
+ transparent 75%,
224
+ transparent
225
+ );
226
+ background-size: 1rem 1rem;
227
+ }
228
+
229
+ /* Animated stripes (barber pole effect) */
230
+ .ui-progress-linear--animated .ui-progress-linear__indicator {
231
+ animation: progress-stripes 1s linear infinite;
232
+ }
233
+
234
+ @keyframes progress-stripes {
235
+ from {
236
+ background-position: 1rem 0;
237
+ }
238
+ to {
239
+ background-position: 0 0;
240
+ }
241
+ }
242
+
243
+ /* Indeterminate animation */
244
+ .ui-progress-linear--indeterminate .ui-progress-linear__indicator {
245
+ width: 30%;
246
+ animation: progress-indeterminate 1.5s cubic-bezier(0.65, 0, 0.35, 1) infinite;
247
+ }
248
+
249
+ @keyframes progress-indeterminate {
250
+ 0% {
251
+ transform: translateX(-100%);
252
+ }
253
+ 100% {
254
+ transform: translateX(400%);
255
+ }
256
+ }
257
+
258
+ /* Value label */
259
+ .ui-progress-linear__value {
260
+ flex-shrink: 0;
261
+ font-size: var(--text-sm);
262
+ font-weight: var(--font-medium);
263
+ color: var(--progress-value-text);
264
+ min-width: 3ch;
265
+ text-align: right;
266
+ font-variant-numeric: tabular-nums;
267
+ }
268
+
269
+ /* ============================================
270
+ CIRCULAR PROGRESS
271
+ ============================================ */
272
+
273
+ .ui-progress-circular {
274
+ position: relative;
275
+ display: inline-flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ font-family: var(--font-sans);
279
+ }
280
+
281
+ .ui-progress-circular__svg {
282
+ transform: rotate(-90deg);
283
+ }
284
+
285
+ /* Track circle */
286
+ .ui-progress-circular__track {
287
+ stroke: var(--progress-track);
288
+ }
289
+
290
+ /* Indicator circle */
291
+ .ui-progress-circular__indicator {
292
+ transition: stroke-dashoffset var(--duration-normal) var(--ease-out);
293
+ }
294
+
295
+ /* Colors */
296
+ .ui-progress-circular--primary .ui-progress-circular__indicator {
297
+ stroke: var(--progress-primary);
298
+ }
299
+
300
+ .ui-progress-circular--success .ui-progress-circular__indicator {
301
+ stroke: var(--progress-success);
302
+ }
303
+
304
+ .ui-progress-circular--warning .ui-progress-circular__indicator {
305
+ stroke: var(--progress-warning);
306
+ }
307
+
308
+ .ui-progress-circular--error .ui-progress-circular__indicator {
309
+ stroke: var(--progress-error);
310
+ }
311
+
312
+ /* Indeterminate animation (Google-style spinner) */
313
+ .ui-progress-circular--indeterminate .ui-progress-circular__svg {
314
+ animation: progress-circular-rotate 2s linear infinite;
315
+ }
316
+
317
+ .ui-progress-circular--indeterminate .ui-progress-circular__indicator {
318
+ animation: progress-circular-dash 1.5s ease-in-out infinite;
319
+ stroke-dasharray: 1, 200;
320
+ stroke-dashoffset: 0;
321
+ }
322
+
323
+ @keyframes progress-circular-rotate {
324
+ 100% {
325
+ transform: rotate(270deg);
326
+ }
327
+ }
328
+
329
+ @keyframes progress-circular-dash {
330
+ 0% {
331
+ stroke-dasharray: 1, 200;
332
+ stroke-dashoffset: 0;
333
+ }
334
+ 50% {
335
+ stroke-dasharray: 89, 200;
336
+ stroke-dashoffset: -35;
337
+ }
338
+ 100% {
339
+ stroke-dasharray: 89, 200;
340
+ stroke-dashoffset: -124;
341
+ }
342
+ }
343
+
344
+ /* Value label (centered inside circle) */
345
+ .ui-progress-circular__value {
346
+ position: absolute;
347
+ font-weight: var(--font-medium);
348
+ color: var(--progress-value-text);
349
+ font-variant-numeric: tabular-nums;
350
+ }
351
+
352
+ .ui-progress-circular--sm .ui-progress-circular__value {
353
+ font-size: var(--text-xs);
354
+ }
355
+
356
+ .ui-progress-circular--md .ui-progress-circular__value {
357
+ font-size: var(--text-sm);
358
+ }
359
+
360
+ .ui-progress-circular--lg .ui-progress-circular__value {
361
+ font-size: var(--text-base);
362
+ }
363
+ </style>
@@ -0,0 +1,7 @@
1
+ export { default as Progress } from './Progress.vue'
2
+ export type {
3
+ ProgressProps,
4
+ ProgressVariant,
5
+ ProgressSize,
6
+ ProgressColor
7
+ } from './Progress.vue'
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import Radio from './Radio.vue'
4
+
5
+ describe('Radio', () => {
6
+ describe('Rendering', () => {
7
+ it('renders as a label element', () => {
8
+ const wrapper = mount(Radio, {
9
+ props: { value: 'a' }
10
+ })
11
+ expect(wrapper.find('label').exists()).toBe(true)
12
+ })
13
+
14
+ it('renders native radio input', () => {
15
+ const wrapper = mount(Radio, {
16
+ props: { value: 'a' }
17
+ })
18
+ expect(wrapper.find('input[type="radio"]').exists()).toBe(true)
19
+ })
20
+
21
+ it('renders custom box element', () => {
22
+ const wrapper = mount(Radio, {
23
+ props: { value: 'a' }
24
+ })
25
+ expect(wrapper.find('.ui-radio__box').exists()).toBe(true)
26
+ })
27
+
28
+ it('does not render dot when unchecked', () => {
29
+ const wrapper = mount(Radio, {
30
+ props: { modelValue: 'b', value: 'a' }
31
+ })
32
+ expect(wrapper.find('.ui-radio__dot').exists()).toBe(false)
33
+ })
34
+
35
+ it('renders dot when checked', () => {
36
+ const wrapper = mount(Radio, {
37
+ props: { modelValue: 'a', value: 'a' }
38
+ })
39
+ expect(wrapper.find('.ui-radio__dot').exists()).toBe(true)
40
+ })
41
+ })
42
+
43
+ describe('v-model', () => {
44
+ it('is unchecked when modelValue differs from value', () => {
45
+ const wrapper = mount(Radio, {
46
+ props: { modelValue: 'other', value: 'a' }
47
+ })
48
+ const input = wrapper.find('input')
49
+ expect((input.element as HTMLInputElement).checked).toBe(false)
50
+ })
51
+
52
+ it('is checked when modelValue matches value', () => {
53
+ const wrapper = mount(Radio, {
54
+ props: { modelValue: 'a', value: 'a' }
55
+ })
56
+ const input = wrapper.find('input')
57
+ expect((input.element as HTMLInputElement).checked).toBe(true)
58
+ expect(wrapper.find('.ui-radio').classes()).toContain('ui-radio--checked')
59
+ })
60
+
61
+ it('emits update:modelValue on change', async () => {
62
+ const wrapper = mount(Radio, {
63
+ props: { modelValue: 'other', value: 'a' }
64
+ })
65
+ await wrapper.find('input').trigger('change')
66
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['a'])
67
+ })
68
+
69
+ it('emits change event on change', async () => {
70
+ const wrapper = mount(Radio, {
71
+ props: { modelValue: 'other', value: 'a' }
72
+ })
73
+ await wrapper.find('input').trigger('change')
74
+ expect(wrapper.emitted('change')?.[0]).toEqual(['a'])
75
+ })
76
+
77
+ it('works with number values', () => {
78
+ const wrapper = mount(Radio, {
79
+ props: { modelValue: 1, value: 1 }
80
+ })
81
+ expect(wrapper.find('.ui-radio').classes()).toContain('ui-radio--checked')
82
+ })
83
+
84
+ it('works with boolean values', () => {
85
+ const wrapper = mount(Radio, {
86
+ props: { modelValue: true, value: true }
87
+ })
88
+ expect(wrapper.find('.ui-radio').classes()).toContain('ui-radio--checked')
89
+ })
90
+ })
91
+
92
+ describe('Radio group behavior', () => {
93
+ it('uses name attribute for grouping', () => {
94
+ const wrapper = mount(Radio, {
95
+ props: { value: 'a', name: 'group1' }
96
+ })
97
+ expect(wrapper.find('input').attributes('name')).toBe('group1')
98
+ })
99
+
100
+ it('passes value to native input', () => {
101
+ const wrapper = mount(Radio, {
102
+ props: { value: 'option-a' }
103
+ })
104
+ expect(wrapper.find('input').attributes('value')).toBe('option-a')
105
+ })
106
+ })
107
+
108
+ describe('Size variants', () => {
109
+ const sizes = ['sm', 'md', 'lg'] as const
110
+
111
+ sizes.forEach(size => {
112
+ it(`applies ${size} size class`, () => {
113
+ const wrapper = mount(Radio, {
114
+ props: { value: 'a', size }
115
+ })
116
+ expect(wrapper.find('.ui-radio').classes()).toContain(`ui-radio--${size}`)
117
+ })
118
+ })
119
+
120
+ it('defaults to md size', () => {
121
+ const wrapper = mount(Radio, {
122
+ props: { value: 'a' }
123
+ })
124
+ expect(wrapper.find('.ui-radio').classes()).toContain('ui-radio--md')
125
+ })
126
+ })
127
+
128
+ describe('Disabled state', () => {
129
+ it('applies disabled attribute to input', () => {
130
+ const wrapper = mount(Radio, {
131
+ props: { value: 'a', disabled: true }
132
+ })
133
+ expect(wrapper.find('input').attributes('disabled')).toBeDefined()
134
+ })
135
+
136
+ it('applies disabled class', () => {
137
+ const wrapper = mount(Radio, {
138
+ props: { value: 'a', disabled: true }
139
+ })
140
+ expect(wrapper.find('.ui-radio').classes()).toContain('ui-radio--disabled')
141
+ })
142
+
143
+ it('does not emit events when disabled', async () => {
144
+ const wrapper = mount(Radio, {
145
+ props: { value: 'a', modelValue: 'other', disabled: true }
146
+ })
147
+ await wrapper.find('input').trigger('change')
148
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
149
+ })
150
+ })
151
+
152
+ describe('Label and description', () => {
153
+ it('renders label text', () => {
154
+ const wrapper = mount(Radio, {
155
+ props: { value: 'a', label: 'Option A' }
156
+ })
157
+ expect(wrapper.find('.ui-radio__label').text()).toBe('Option A')
158
+ })
159
+
160
+ it('renders description text', () => {
161
+ const wrapper = mount(Radio, {
162
+ props: { value: 'a', description: 'This is the first option' }
163
+ })
164
+ expect(wrapper.find('.ui-radio__description').text()).toBe('This is the first option')
165
+ })
166
+
167
+ it('renders both label and description', () => {
168
+ const wrapper = mount(Radio, {
169
+ props: {
170
+ value: 'a',
171
+ label: 'Option A',
172
+ description: 'This is the first option'
173
+ }
174
+ })
175
+ expect(wrapper.find('.ui-radio__label').exists()).toBe(true)
176
+ expect(wrapper.find('.ui-radio__description').exists()).toBe(true)
177
+ })
178
+
179
+ it('does not render content wrapper when no label or description', () => {
180
+ const wrapper = mount(Radio, {
181
+ props: { value: 'a' }
182
+ })
183
+ expect(wrapper.find('.ui-radio__content').exists()).toBe(false)
184
+ })
185
+ })
186
+
187
+ describe('Accessibility', () => {
188
+ it('auto-generates unique id', () => {
189
+ const wrapper = mount(Radio, {
190
+ props: { value: 'a' }
191
+ })
192
+ expect(wrapper.find('input').attributes('id')).toMatch(/^radio-/)
193
+ })
194
+
195
+ it('uses provided id', () => {
196
+ const wrapper = mount(Radio, {
197
+ props: { value: 'a', id: 'custom-radio' }
198
+ })
199
+ expect(wrapper.find('input').attributes('id')).toBe('custom-radio')
200
+ })
201
+
202
+ it('input is visually hidden but accessible', () => {
203
+ const wrapper = mount(Radio, {
204
+ props: { value: 'a' }
205
+ })
206
+ expect(wrapper.find('.ui-radio__input').exists()).toBe(true)
207
+ })
208
+
209
+ it('custom box has aria-hidden', () => {
210
+ const wrapper = mount(Radio, {
211
+ props: { value: 'a' }
212
+ })
213
+ expect(wrapper.find('.ui-radio__box').attributes('aria-hidden')).toBe('true')
214
+ })
215
+ })
216
+ })