@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,159 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ export interface SpinnerProps {
5
+ /** Predefined size or custom value */
6
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | string
7
+ /** Animation speed in seconds */
8
+ speed?: number
9
+ /** Accessible label for screen readers */
10
+ label?: string
11
+ }
12
+
13
+ const props = withDefaults(defineProps<SpinnerProps>(), {
14
+ size: 'md',
15
+ speed: 0.9,
16
+ label: 'Loading'
17
+ })
18
+
19
+ const sizeMap: Record<string, string> = {
20
+ xs: 'var(--spinner-xs)',
21
+ sm: 'var(--spinner-sm)',
22
+ md: 'var(--spinner-md)',
23
+ lg: 'var(--spinner-lg)',
24
+ xl: 'var(--spinner-xl)'
25
+ }
26
+
27
+ const resolvedSize = computed(() => sizeMap[props.size] ?? props.size)
28
+ const resolvedSpeed = computed(() => `${props.speed}s`)
29
+ </script>
30
+
31
+ <template>
32
+ <div
33
+ class="ui-spinner"
34
+ role="status"
35
+ :aria-label="label"
36
+ :style="{
37
+ '--spinner-size': resolvedSize,
38
+ '--spinner-speed': resolvedSpeed
39
+ }"
40
+ >
41
+ <div class="ui-spinner__dot" />
42
+ <div class="ui-spinner__dot" />
43
+ <div class="ui-spinner__dot" />
44
+ <div class="ui-spinner__dot" />
45
+ <div class="ui-spinner__dot" />
46
+ <div class="ui-spinner__dot" />
47
+ <div class="ui-spinner__dot" />
48
+ <div class="ui-spinner__dot" />
49
+ <span class="ui-spinner__sr-only">{{ label }}</span>
50
+ </div>
51
+ </template>
52
+
53
+ <style scoped>
54
+ .ui-spinner {
55
+ --spinner-size: 2rem;
56
+ --spinner-speed: 0.9s;
57
+ position: relative;
58
+ display: inline-flex;
59
+ align-items: center;
60
+ justify-content: flex-start;
61
+ height: var(--spinner-size);
62
+ width: var(--spinner-size);
63
+ }
64
+
65
+ .ui-spinner__dot {
66
+ position: absolute;
67
+ top: 0;
68
+ left: 0;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: flex-start;
72
+ height: 100%;
73
+ width: 100%;
74
+ }
75
+
76
+ .ui-spinner__dot::before {
77
+ content: '';
78
+ height: 20%;
79
+ width: 20%;
80
+ border-radius: 50%;
81
+ background-color: currentColor;
82
+ transform: scale(0);
83
+ opacity: 0.5;
84
+ animation: ui-spinner-pulse calc(var(--spinner-speed) * 1.111) ease-in-out infinite;
85
+ }
86
+
87
+ .ui-spinner__dot:nth-child(2) {
88
+ transform: rotate(45deg);
89
+ }
90
+ .ui-spinner__dot:nth-child(2)::before {
91
+ animation-delay: calc(var(--spinner-speed) * -0.875);
92
+ }
93
+
94
+ .ui-spinner__dot:nth-child(3) {
95
+ transform: rotate(90deg);
96
+ }
97
+ .ui-spinner__dot:nth-child(3)::before {
98
+ animation-delay: calc(var(--spinner-speed) * -0.75);
99
+ }
100
+
101
+ .ui-spinner__dot:nth-child(4) {
102
+ transform: rotate(135deg);
103
+ }
104
+ .ui-spinner__dot:nth-child(4)::before {
105
+ animation-delay: calc(var(--spinner-speed) * -0.625);
106
+ }
107
+
108
+ .ui-spinner__dot:nth-child(5) {
109
+ transform: rotate(180deg);
110
+ }
111
+ .ui-spinner__dot:nth-child(5)::before {
112
+ animation-delay: calc(var(--spinner-speed) * -0.5);
113
+ }
114
+
115
+ .ui-spinner__dot:nth-child(6) {
116
+ transform: rotate(225deg);
117
+ }
118
+ .ui-spinner__dot:nth-child(6)::before {
119
+ animation-delay: calc(var(--spinner-speed) * -0.375);
120
+ }
121
+
122
+ .ui-spinner__dot:nth-child(7) {
123
+ transform: rotate(270deg);
124
+ }
125
+ .ui-spinner__dot:nth-child(7)::before {
126
+ animation-delay: calc(var(--spinner-speed) * -0.25);
127
+ }
128
+
129
+ .ui-spinner__dot:nth-child(8) {
130
+ transform: rotate(315deg);
131
+ }
132
+ .ui-spinner__dot:nth-child(8)::before {
133
+ animation-delay: calc(var(--spinner-speed) * -0.125);
134
+ }
135
+
136
+ .ui-spinner__sr-only {
137
+ position: absolute;
138
+ width: 1px;
139
+ height: 1px;
140
+ padding: 0;
141
+ margin: -1px;
142
+ overflow: hidden;
143
+ clip: rect(0, 0, 0, 0);
144
+ white-space: nowrap;
145
+ border: 0;
146
+ }
147
+
148
+ @keyframes ui-spinner-pulse {
149
+ 0%,
150
+ 100% {
151
+ transform: scale(0);
152
+ opacity: 0.5;
153
+ }
154
+ 50% {
155
+ transform: scale(1);
156
+ opacity: 1;
157
+ }
158
+ }
159
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as Spinner } from './Spinner.vue'
2
+ export type { SpinnerProps } from './Spinner.vue'
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import type { InjectionKey, ComputedRef } from 'vue'
3
+
4
+ export type Theme = 'light' | 'dark'
5
+ export type Mood = 'warm' | 'cool' | 'vibrant' | 'muted' | 'earthy'
6
+ export type Depth = 'flat' | 'subtle' | 'elevated' | 'dimensional'
7
+ export type Motion = 'minimal' | 'smooth' | 'spring' | 'snappy'
8
+ export type Texture = 'none' | 'subtle' | 'medium'
9
+
10
+ export interface SpireConfig {
11
+ theme?: Theme
12
+ mood?: Mood
13
+ depth?: Depth
14
+ motion?: Motion
15
+ texture?: Texture
16
+ }
17
+
18
+ export interface SpireProviderProps {
19
+ theme?: Theme
20
+ mood?: Mood
21
+ depth?: Depth
22
+ motion?: Motion
23
+ texture?: Texture
24
+ tag?: string
25
+ }
26
+
27
+ export const spireConfigKey: InjectionKey<ComputedRef<SpireConfig>> = Symbol('spire-config')
28
+ </script>
29
+
30
+ <script setup lang="ts">
31
+ import { provide, computed } from 'vue'
32
+
33
+ const props = withDefaults(defineProps<SpireProviderProps>(), {
34
+ tag: 'div'
35
+ })
36
+
37
+ const config = computed<SpireConfig>(() => ({
38
+ theme: props.theme,
39
+ mood: props.mood,
40
+ depth: props.depth,
41
+ motion: props.motion,
42
+ texture: props.texture
43
+ }))
44
+
45
+ provide(spireConfigKey, config)
46
+
47
+ const dataAttributes = computed(() => {
48
+ const attrs: Record<string, string | undefined> = {}
49
+
50
+ if (props.theme) attrs['data-theme'] = props.theme
51
+ if (props.mood) attrs['data-mood'] = props.mood
52
+ if (props.depth) attrs['data-depth'] = props.depth
53
+ if (props.motion) attrs['data-motion'] = props.motion
54
+ if (props.texture) attrs['data-texture'] = props.texture
55
+
56
+ return attrs
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <component :is="tag" class="spire-provider" v-bind="dataAttributes">
62
+ <slot />
63
+ </component>
64
+ </template>
65
+
66
+ <style scoped>
67
+ .spire-provider {
68
+ display: block;
69
+ min-height: inherit;
70
+ }
71
+ </style>
@@ -0,0 +1,11 @@
1
+ export { default as SpireProvider } from './SpireProvider.vue'
2
+ export { spireConfigKey } from './SpireProvider.vue'
3
+ export type {
4
+ Theme,
5
+ Mood,
6
+ Depth,
7
+ Motion,
8
+ Texture,
9
+ SpireConfig,
10
+ SpireProviderProps
11
+ } from './SpireProvider.vue'
@@ -0,0 +1,221 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { h, nextTick } from 'vue'
4
+ import StepperRoot from './StepperRoot.vue'
5
+ import StepperItem from './StepperItem.vue'
6
+ import StepperTrigger from './StepperTrigger.vue'
7
+ import StepperContent from './StepperContent.vue'
8
+ import StepperSeparator from './StepperSeparator.vue'
9
+
10
+ function createStepper(props: Record<string, unknown> = {}) {
11
+ return mount(StepperRoot, {
12
+ props,
13
+ slots: {
14
+ default: () => [
15
+ h(StepperItem, { index: 0 }, {
16
+ default: () => [
17
+ h(StepperTrigger, null, { default: () => 'Step 1' }),
18
+ h(StepperSeparator),
19
+ h(StepperContent, null, { default: () => 'Content 1' })
20
+ ]
21
+ }),
22
+ h(StepperItem, { index: 1 }, {
23
+ default: () => [
24
+ h(StepperTrigger, null, { default: () => 'Step 2' }),
25
+ h(StepperSeparator),
26
+ h(StepperContent, null, { default: () => 'Content 2' })
27
+ ]
28
+ }),
29
+ h(StepperItem, { index: 2 }, {
30
+ default: () => [
31
+ h(StepperTrigger, null, { default: () => 'Step 3' }),
32
+ h(StepperContent, null, { default: () => 'Content 3' })
33
+ ]
34
+ })
35
+ ]
36
+ }
37
+ })
38
+ }
39
+
40
+ describe('Stepper', () => {
41
+ describe('Rendering', () => {
42
+ it('renders all stepper items', () => {
43
+ const wrapper = createStepper()
44
+ expect(wrapper.findAll('.ui-stepper__item')).toHaveLength(3)
45
+ })
46
+
47
+ it('renders triggers with correct text', () => {
48
+ const wrapper = createStepper()
49
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
50
+ expect(triggers[0].text()).toContain('Step 1')
51
+ expect(triggers[1].text()).toContain('Step 2')
52
+ expect(triggers[2].text()).toContain('Step 3')
53
+ })
54
+
55
+ it('renders step indicators with numbers', () => {
56
+ const wrapper = createStepper()
57
+ const indicators = wrapper.findAll('.ui-stepper__indicator')
58
+ expect(indicators[0].text()).toContain('1')
59
+ expect(indicators[1].text()).toContain('2')
60
+ expect(indicators[2].text()).toContain('3')
61
+ })
62
+
63
+ it('renders separators', () => {
64
+ const wrapper = createStepper()
65
+ expect(wrapper.findAll('.ui-stepper__separator')).toHaveLength(2)
66
+ })
67
+ })
68
+
69
+ describe('Step states', () => {
70
+ it('marks first step as current by default', () => {
71
+ const wrapper = createStepper()
72
+ const items = wrapper.findAll('.ui-stepper__item')
73
+ expect(items[0].attributes('data-state')).toBe('current')
74
+ })
75
+
76
+ it('marks previous steps as completed', () => {
77
+ const wrapper = createStepper({ modelValue: 2 })
78
+ const items = wrapper.findAll('.ui-stepper__item')
79
+ expect(items[0].attributes('data-state')).toBe('completed')
80
+ expect(items[1].attributes('data-state')).toBe('completed')
81
+ expect(items[2].attributes('data-state')).toBe('current')
82
+ })
83
+
84
+ it('marks future steps as pending', () => {
85
+ const wrapper = createStepper({ modelValue: 0 })
86
+ const items = wrapper.findAll('.ui-stepper__item')
87
+ expect(items[1].attributes('data-state')).toBe('pending')
88
+ expect(items[2].attributes('data-state')).toBe('pending')
89
+ })
90
+
91
+ it('marks error steps correctly', () => {
92
+ const wrapper = createStepper({ modelValue: 2, errorSteps: [1] })
93
+ const items = wrapper.findAll('.ui-stepper__item')
94
+ expect(items[1].attributes('data-state')).toBe('error')
95
+ })
96
+ })
97
+
98
+ describe('Navigation', () => {
99
+ it('emits update:modelValue when clicking a step', async () => {
100
+ const wrapper = createStepper({ modelValue: 0 })
101
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
102
+
103
+ await triggers[1].trigger('click')
104
+
105
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([1])
106
+ })
107
+
108
+ it('prevents navigation to future steps in linear mode', async () => {
109
+ const wrapper = createStepper({ modelValue: 0, linear: true })
110
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
111
+
112
+ await triggers[2].trigger('click')
113
+
114
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
115
+ })
116
+
117
+ it('allows navigation to next step in linear mode', async () => {
118
+ const wrapper = createStepper({ modelValue: 0, linear: true })
119
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
120
+
121
+ await triggers[1].trigger('click')
122
+
123
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([1])
124
+ })
125
+
126
+ it('allows navigation to previous steps in linear mode', async () => {
127
+ const wrapper = createStepper({ modelValue: 2, linear: true })
128
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
129
+
130
+ await triggers[0].trigger('click')
131
+
132
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([0])
133
+ })
134
+ })
135
+
136
+ describe('Content visibility', () => {
137
+ it('shows content for current step only', () => {
138
+ const wrapper = createStepper({ modelValue: 1 })
139
+ const contents = wrapper.findAll('.ui-stepper__content')
140
+
141
+ expect(contents[0].attributes('aria-hidden')).toBe('true')
142
+ expect(contents[1].attributes('aria-hidden')).toBe('false')
143
+ expect(contents[2].attributes('aria-hidden')).toBe('true')
144
+ })
145
+ })
146
+
147
+ describe('Orientation', () => {
148
+ it('applies horizontal class by default', () => {
149
+ const wrapper = createStepper()
150
+ expect(wrapper.find('.ui-stepper').classes()).toContain('ui-stepper--horizontal')
151
+ })
152
+
153
+ it('applies vertical class when set', () => {
154
+ const wrapper = createStepper({ orientation: 'vertical' })
155
+ expect(wrapper.find('.ui-stepper').classes()).toContain('ui-stepper--vertical')
156
+ })
157
+ })
158
+
159
+ describe('Accessibility', () => {
160
+ it('has role="tablist" on root', () => {
161
+ const wrapper = createStepper()
162
+ expect(wrapper.find('.ui-stepper').attributes('role')).toBe('tablist')
163
+ })
164
+
165
+ it('triggers have role="tab"', () => {
166
+ const wrapper = createStepper()
167
+ const trigger = wrapper.find('.ui-stepper__trigger')
168
+ expect(trigger.attributes('role')).toBe('tab')
169
+ })
170
+
171
+ it('content has role="tabpanel"', () => {
172
+ const wrapper = createStepper()
173
+ const content = wrapper.find('.ui-stepper__content')
174
+ expect(content.attributes('role')).toBe('tabpanel')
175
+ })
176
+
177
+ it('trigger has aria-selected for current step', () => {
178
+ const wrapper = createStepper({ modelValue: 1 })
179
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
180
+ expect(triggers[0].attributes('aria-selected')).toBe('false')
181
+ expect(triggers[1].attributes('aria-selected')).toBe('true')
182
+ expect(triggers[2].attributes('aria-selected')).toBe('false')
183
+ })
184
+
185
+ it('trigger has aria-controls pointing to content', () => {
186
+ const wrapper = createStepper()
187
+ const trigger = wrapper.find('.ui-stepper__trigger')
188
+ const content = wrapper.find('.ui-stepper__content')
189
+ expect(trigger.attributes('aria-controls')).toBe(content.attributes('id'))
190
+ })
191
+
192
+ it('content has aria-labelledby pointing to trigger', () => {
193
+ const wrapper = createStepper()
194
+ const trigger = wrapper.find('.ui-stepper__trigger')
195
+ const content = wrapper.find('.ui-stepper__content')
196
+ expect(content.attributes('aria-labelledby')).toBe(trigger.attributes('id'))
197
+ })
198
+
199
+ it('disabled steps have aria-disabled', async () => {
200
+ const wrapper = createStepper({ modelValue: 0, linear: true })
201
+ const triggers = wrapper.findAll('.ui-stepper__trigger')
202
+ await nextTick()
203
+ expect(triggers[2].attributes('aria-disabled')).toBe('true')
204
+ })
205
+ })
206
+
207
+ describe('Separator styling', () => {
208
+ it('marks separator as completed when step is completed', () => {
209
+ const wrapper = createStepper({ modelValue: 2 })
210
+ const separators = wrapper.findAll('.ui-stepper__separator')
211
+ expect(separators[0].classes()).toContain('ui-stepper__separator--completed')
212
+ expect(separators[1].classes()).toContain('ui-stepper__separator--completed')
213
+ })
214
+
215
+ it('does not mark separator as completed for pending steps', () => {
216
+ const wrapper = createStepper({ modelValue: 0 })
217
+ const separators = wrapper.findAll('.ui-stepper__separator')
218
+ expect(separators[0].classes()).not.toContain('ui-stepper__separator--completed')
219
+ })
220
+ })
221
+ })
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from 'vue'
3
+ import { StepperKey, StepperItemKey } from './keys'
4
+
5
+ const stepper = inject(StepperKey)
6
+ const stepperItem = inject(StepperItemKey)
7
+
8
+ if (!stepper || !stepperItem) {
9
+ throw new Error('StepperContent must be used within StepperItem')
10
+ }
11
+
12
+ const isActive = computed(() => stepperItem.state.value === 'current')
13
+
14
+ const contentClasses = computed(() => [
15
+ 'ui-stepper__content',
16
+ `ui-stepper__content--${stepper.orientation.value}`,
17
+ {
18
+ 'ui-stepper__content--active': isActive.value
19
+ }
20
+ ])
21
+ </script>
22
+
23
+ <template>
24
+ <div
25
+ v-show="isActive"
26
+ :id="stepperItem.contentId"
27
+ :class="contentClasses"
28
+ role="tabpanel"
29
+ :aria-labelledby="stepperItem.triggerId"
30
+ :aria-hidden="!isActive"
31
+ >
32
+ <slot />
33
+ </div>
34
+ </template>
35
+
36
+ <style scoped>
37
+ .ui-stepper__content {
38
+ padding: var(--space-4);
39
+ }
40
+
41
+ .ui-stepper__content--horizontal {
42
+ width: 100%;
43
+ text-align: center;
44
+ }
45
+
46
+ .ui-stepper__content--vertical {
47
+ flex: 1;
48
+ padding-left: var(--space-4);
49
+ padding-bottom: var(--space-6);
50
+ }
51
+ </style>
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ import { computed, provide, inject, onMounted } from 'vue'
3
+ import { useId } from '../../composables'
4
+ import { StepperKey, StepperItemKey } from './keys'
5
+
6
+ export interface StepperItemProps {
7
+ /** Step index (0-based) */
8
+ index: number
9
+ /** Disable this step */
10
+ disabled?: boolean
11
+ }
12
+
13
+ const props = withDefaults(defineProps<StepperItemProps>(), {
14
+ disabled: false
15
+ })
16
+
17
+ const injectedStepper = inject(StepperKey)
18
+
19
+ if (!injectedStepper) {
20
+ throw new Error('StepperItem must be used within StepperRoot')
21
+ }
22
+
23
+ const stepper = injectedStepper
24
+
25
+ const triggerId = useId('stepper-trigger')
26
+ const contentId = useId('stepper-content')
27
+
28
+ const state = computed(() => stepper.getStepState(props.index))
29
+ const isDisabled = computed(() => props.disabled || !stepper.canNavigateTo(props.index))
30
+
31
+ function goToStep() {
32
+ if (isDisabled.value) return
33
+ stepper.goToStep(props.index)
34
+ }
35
+
36
+ onMounted(() => {
37
+ stepper.registerStep(props.index)
38
+ })
39
+
40
+ provide(StepperItemKey, {
41
+ index: props.index,
42
+ state,
43
+ isDisabled,
44
+ triggerId,
45
+ contentId,
46
+ goToStep
47
+ })
48
+
49
+ const itemClasses = computed(() => [
50
+ 'ui-stepper__item',
51
+ `ui-stepper__item--${stepper.orientation.value}`,
52
+ `ui-stepper__item--${state.value}`,
53
+ {
54
+ 'ui-stepper__item--disabled': isDisabled.value
55
+ }
56
+ ])
57
+ </script>
58
+
59
+ <template>
60
+ <div
61
+ :class="itemClasses"
62
+ :data-state="state"
63
+ >
64
+ <slot />
65
+ </div>
66
+ </template>
67
+
68
+ <style scoped>
69
+ .ui-stepper__item {
70
+ display: flex;
71
+ position: relative;
72
+ }
73
+
74
+ .ui-stepper__item--horizontal {
75
+ flex-direction: column;
76
+ flex: 1;
77
+ align-items: center;
78
+ }
79
+
80
+ .ui-stepper__item--vertical {
81
+ flex-direction: row;
82
+ gap: var(--space-3);
83
+ }
84
+
85
+ .ui-stepper__item--disabled {
86
+ opacity: 0.5;
87
+ cursor: not-allowed;
88
+ }
89
+ </style>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import { computed, provide, toRef, ref, watch } from 'vue'
3
+ import { StepperKey } from './keys'
4
+ import type { StepperOrientation, StepState } from './keys'
5
+
6
+ export interface StepperRootProps {
7
+ /** Current step index (0-based, v-model) */
8
+ modelValue?: number
9
+ /** Layout direction */
10
+ orientation?: StepperOrientation
11
+ /** Prevents clicking future steps */
12
+ linear?: boolean
13
+ /** Mark specific steps as having errors */
14
+ errorSteps?: number[]
15
+ }
16
+
17
+ const props = withDefaults(defineProps<StepperRootProps>(), {
18
+ modelValue: 0,
19
+ orientation: 'horizontal',
20
+ linear: false,
21
+ errorSteps: () => []
22
+ })
23
+
24
+ const emit = defineEmits<{
25
+ 'update:modelValue': [value: number]
26
+ }>()
27
+
28
+ const internalValue = ref(props.modelValue)
29
+ const stepCount = ref(0)
30
+ const registeredSteps = ref<Set<number>>(new Set())
31
+
32
+ watch(() => props.modelValue, (newValue) => {
33
+ internalValue.value = newValue
34
+ })
35
+
36
+ const isControlled = computed(() => props.modelValue !== undefined)
37
+ const currentStep = computed(() => isControlled.value ? props.modelValue! : internalValue.value)
38
+
39
+ function goToStep(step: number) {
40
+ if (!canNavigateTo(step)) return
41
+ internalValue.value = step
42
+ emit('update:modelValue', step)
43
+ }
44
+
45
+ function getStepState(index: number): StepState {
46
+ if (props.errorSteps.includes(index)) return 'error'
47
+ if (index < currentStep.value) return 'completed'
48
+ if (index === currentStep.value) return 'current'
49
+ return 'pending'
50
+ }
51
+
52
+ function canNavigateTo(index: number): boolean {
53
+ if (props.linear) {
54
+ return index <= currentStep.value + 1
55
+ }
56
+ return true
57
+ }
58
+
59
+ function registerStep(index: number) {
60
+ registeredSteps.value.add(index)
61
+ stepCount.value = registeredSteps.value.size
62
+ }
63
+
64
+ provide(StepperKey, {
65
+ orientation: toRef(props, 'orientation'),
66
+ linear: toRef(props, 'linear'),
67
+ currentStep,
68
+ goToStep,
69
+ getStepState,
70
+ canNavigateTo,
71
+ registerStep,
72
+ stepCount
73
+ })
74
+ </script>
75
+
76
+ <template>
77
+ <div
78
+ class="ui-stepper"
79
+ :class="[`ui-stepper--${orientation}`]"
80
+ role="tablist"
81
+ :aria-orientation="orientation"
82
+ >
83
+ <slot />
84
+ </div>
85
+ </template>
86
+
87
+ <style scoped>
88
+ .ui-stepper {
89
+ display: flex;
90
+ width: 100%;
91
+ }
92
+
93
+ .ui-stepper--horizontal {
94
+ flex-direction: row;
95
+ align-items: flex-start;
96
+ }
97
+
98
+ .ui-stepper--vertical {
99
+ flex-direction: column;
100
+ }
101
+ </style>