@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,211 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { ChartData, ChartOptions } from 'chart.js'
4
+ import BaseChart from './BaseChart.vue'
5
+ import type { LegendItem, TooltipData, PointClickData } from './BaseChart.vue'
6
+ import { useChartTheme } from './useChartTheme'
7
+
8
+ type LineChartData = ChartData<'line'>
9
+ type LineChartOptions = ChartOptions<'line'>
10
+
11
+ export interface LineChartSeries {
12
+ name: string
13
+ data: number[]
14
+ }
15
+
16
+ export interface LineChartProps {
17
+ /** X-axis labels */
18
+ labels: string[]
19
+ /** Data series */
20
+ series: LineChartSeries[]
21
+ /** Chart height */
22
+ height?: string | number
23
+ /** Show area fill under lines */
24
+ fill?: boolean
25
+ /** Line curve tension (0 = straight, 0.4 = smooth) */
26
+ tension?: number
27
+ /** Show points on the line */
28
+ showPoints?: boolean
29
+ /** Point radius */
30
+ pointRadius?: number
31
+ /** Show grid lines */
32
+ showGrid?: boolean
33
+ /** Show X axis */
34
+ showXAxis?: boolean
35
+ /** Show Y axis */
36
+ showYAxis?: boolean
37
+ /** Accessible label */
38
+ ariaLabel?: string
39
+ /** Show built-in legend */
40
+ showLegend?: boolean
41
+ }
42
+
43
+ const props = withDefaults(defineProps<LineChartProps>(), {
44
+ height: 300,
45
+ fill: false,
46
+ tension: 0.4,
47
+ showPoints: true,
48
+ pointRadius: 4,
49
+ showGrid: true,
50
+ showXAxis: true,
51
+ showYAxis: true,
52
+ showLegend: true
53
+ })
54
+
55
+ defineSlots<{
56
+ legend?: (props: { items: LegendItem[]; toggle: (index: number) => void }) => unknown
57
+ tooltip?: (props: { data: TooltipData | null }) => unknown
58
+ }>()
59
+
60
+ const emit = defineEmits<{
61
+ pointClick: [data: PointClickData]
62
+ }>()
63
+
64
+ const { theme, getColor, getColorWithOpacity } = useChartTheme()
65
+
66
+ const chartData = computed(() => ({
67
+ labels: props.labels,
68
+ datasets: props.series.map((series, index) => ({
69
+ label: series.name,
70
+ data: series.data,
71
+ borderColor: getColor(index),
72
+ backgroundColor: props.fill ? getColorWithOpacity(index, 0.1) : 'transparent',
73
+ borderWidth: 2,
74
+ fill: props.fill,
75
+ tension: props.tension,
76
+ pointRadius: props.showPoints ? props.pointRadius : 0,
77
+ pointHoverRadius: props.showPoints ? props.pointRadius + 2 : 4,
78
+ pointBackgroundColor: getColor(index),
79
+ pointBorderColor: theme.value.colors.tooltipText,
80
+ pointBorderWidth: 2
81
+ }))
82
+ }) as LineChartData)
83
+
84
+ const chartOptions = computed(() => ({
85
+ scales: {
86
+ x: {
87
+ display: props.showXAxis,
88
+ grid: {
89
+ display: false
90
+ },
91
+ border: {
92
+ display: false
93
+ },
94
+ ticks: {
95
+ color: theme.value.colors.textMuted,
96
+ font: {
97
+ family: theme.value.fontFamily,
98
+ size: 12
99
+ }
100
+ }
101
+ },
102
+ y: {
103
+ display: props.showYAxis,
104
+ grid: {
105
+ display: props.showGrid,
106
+ color: theme.value.colors.gridLines
107
+ },
108
+ border: {
109
+ display: false
110
+ },
111
+ ticks: {
112
+ color: theme.value.colors.textMuted,
113
+ font: {
114
+ family: theme.value.fontFamily,
115
+ size: 12
116
+ }
117
+ }
118
+ }
119
+ },
120
+ interaction: {
121
+ mode: 'index' as const,
122
+ intersect: false
123
+ }
124
+ }) as LineChartOptions)
125
+ </script>
126
+
127
+ <template>
128
+ <BaseChart
129
+ type="line"
130
+ :data="chartData"
131
+ :options="chartOptions"
132
+ :height="height"
133
+ :aria-label="ariaLabel"
134
+ @point-click="emit('pointClick', $event)"
135
+ >
136
+ <template v-if="showLegend || $slots.legend" #legend="{ items, toggle }">
137
+ <slot name="legend" :items="items" :toggle="toggle">
138
+ <div class="ui-line-chart__legend">
139
+ <button
140
+ v-for="(item, index) in items"
141
+ :key="index"
142
+ type="button"
143
+ class="ui-line-chart__legend-item"
144
+ :class="{ 'ui-line-chart__legend-item--hidden': item.hidden }"
145
+ @click="toggle(index)"
146
+ >
147
+ <span
148
+ class="ui-line-chart__legend-color"
149
+ :style="{ backgroundColor: item.color }"
150
+ />
151
+ <span class="ui-line-chart__legend-label">{{ item.label }}</span>
152
+ </button>
153
+ </div>
154
+ </slot>
155
+ </template>
156
+ <template #tooltip="{ data }">
157
+ <slot name="tooltip" :data="data" />
158
+ </template>
159
+ </BaseChart>
160
+ </template>
161
+
162
+ <style scoped>
163
+ .ui-line-chart__legend {
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ gap: var(--space-3);
167
+ margin-bottom: var(--space-4);
168
+ }
169
+
170
+ .ui-line-chart__legend-item {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: var(--space-2);
174
+ padding: var(--space-1) var(--space-2);
175
+ background: transparent;
176
+ border: none;
177
+ border-radius: var(--radius-sm);
178
+ cursor: pointer;
179
+ font-family: var(--font-sans);
180
+ font-size: var(--text-sm);
181
+ color: var(--chart-legend-text, var(--text-secondary));
182
+ transition: opacity var(--duration-fast) var(--ease-default);
183
+ }
184
+
185
+ .ui-line-chart__legend-item:hover {
186
+ background-color: var(--color-stone-100);
187
+ }
188
+
189
+ [data-theme='dark'] .ui-line-chart__legend-item:hover {
190
+ background-color: var(--color-stone-800);
191
+ }
192
+
193
+ .ui-line-chart__legend-item--hidden {
194
+ opacity: 0.4;
195
+ }
196
+
197
+ .ui-line-chart__legend-item--hidden .ui-line-chart__legend-color {
198
+ opacity: 0.3;
199
+ }
200
+
201
+ .ui-line-chart__legend-color {
202
+ width: 12px;
203
+ height: 3px;
204
+ border-radius: var(--radius-full);
205
+ flex-shrink: 0;
206
+ }
207
+
208
+ .ui-line-chart__legend-label {
209
+ white-space: nowrap;
210
+ }
211
+ </style>
@@ -0,0 +1,20 @@
1
+ export { default as BaseChart } from './BaseChart.vue'
2
+ export type {
3
+ BaseChartProps,
4
+ ChartDataset,
5
+ TooltipData,
6
+ LegendItem,
7
+ PointClickData
8
+ } from './BaseChart.vue'
9
+
10
+ export { default as LineChart } from './LineChart.vue'
11
+ export type { LineChartProps, LineChartSeries } from './LineChart.vue'
12
+
13
+ export { default as BarChart } from './BarChart.vue'
14
+ export type { BarChartProps, BarChartSeries } from './BarChart.vue'
15
+
16
+ export { default as DonutChart } from './DonutChart.vue'
17
+ export type { DonutChartProps, DonutChartSegment, SegmentClickData } from './DonutChart.vue'
18
+
19
+ export { useChartTheme } from './useChartTheme'
20
+ export type { ChartTheme, ChartThemeColors } from './useChartTheme'
@@ -0,0 +1,192 @@
1
+ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
2
+
3
+ export interface ChartThemeColors {
4
+ primary: string
5
+ success: string
6
+ warning: string
7
+ error: string
8
+ info: string
9
+ text: string
10
+ textMuted: string
11
+ gridLines: string
12
+ tooltipBg: string
13
+ tooltipText: string
14
+ tooltipBorder: string
15
+ legendText: string
16
+ }
17
+
18
+ export interface ChartTheme {
19
+ colors: ChartThemeColors
20
+ fontFamily: string
21
+ palette: string[]
22
+ }
23
+
24
+ function getCSSVariable(name: string): string {
25
+ if (typeof window === 'undefined') return ''
26
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim()
27
+ }
28
+
29
+ function extractTheme(): ChartTheme {
30
+ const colors: ChartThemeColors = {
31
+ primary: getCSSVariable('--chart-1') || getCSSVariable('--action-primary') || '#6366f1',
32
+ success: getCSSVariable('--chart-2') || getCSSVariable('--status-success') || '#14b8a6',
33
+ warning: getCSSVariable('--chart-3') || getCSSVariable('--status-warning') || '#f59e0b',
34
+ error: getCSSVariable('--chart-4') || getCSSVariable('--status-error') || '#f87171',
35
+ info: getCSSVariable('--chart-5') || getCSSVariable('--status-info') || '#3b82f6',
36
+ text: getCSSVariable('--chart-text') || getCSSVariable('--text-primary') || '#1c1917',
37
+ textMuted: getCSSVariable('--chart-text-muted') || getCSSVariable('--text-secondary') || '#78716c',
38
+ gridLines: getCSSVariable('--chart-grid') || getCSSVariable('--border-default') || '#e7e5e4',
39
+ tooltipBg: getCSSVariable('--chart-tooltip-bg') || getCSSVariable('--tooltip-bg') || '#1c1917',
40
+ tooltipText: getCSSVariable('--chart-tooltip-text') || getCSSVariable('--tooltip-text') || '#fafaf9',
41
+ tooltipBorder: getCSSVariable('--chart-tooltip-border') || 'transparent',
42
+ legendText: getCSSVariable('--chart-legend-text') || getCSSVariable('--text-secondary') || '#78716c'
43
+ }
44
+
45
+ const fontFamily = getCSSVariable('--font-sans') || 'system-ui, -apple-system, sans-serif'
46
+
47
+ const palette = [
48
+ getCSSVariable('--chart-1') || colors.primary,
49
+ getCSSVariable('--chart-2') || colors.success,
50
+ getCSSVariable('--chart-3') || colors.warning,
51
+ getCSSVariable('--chart-4') || colors.error,
52
+ getCSSVariable('--chart-5') || colors.info,
53
+ getCSSVariable('--chart-6') || '#8b5cf6',
54
+ getCSSVariable('--chart-7') || '#ec4899',
55
+ getCSSVariable('--chart-8') || '#06b6d4'
56
+ ]
57
+
58
+ return { colors, fontFamily, palette }
59
+ }
60
+
61
+ /**
62
+ * Composable that extracts CSS variables and provides reactive chart theming.
63
+ * Automatically updates when the theme changes (dark mode toggle).
64
+ */
65
+ export function useChartTheme() {
66
+ const theme = ref<ChartTheme>(extractTheme())
67
+ const themeVersion = ref(0)
68
+
69
+ let observer: MutationObserver | null = null
70
+
71
+ function updateTheme() {
72
+ theme.value = extractTheme()
73
+ themeVersion.value++
74
+ }
75
+
76
+ onMounted(() => {
77
+ updateTheme()
78
+
79
+ let pendingUpdate: number | null = null
80
+
81
+ observer = new MutationObserver((mutations) => {
82
+ for (const mutation of mutations) {
83
+ if (
84
+ mutation.type === 'attributes' &&
85
+ (mutation.attributeName === 'data-theme' || mutation.attributeName === 'class')
86
+ ) {
87
+ if (pendingUpdate !== null) {
88
+ cancelAnimationFrame(pendingUpdate)
89
+ }
90
+ pendingUpdate = requestAnimationFrame(() => {
91
+ requestAnimationFrame(() => {
92
+ updateTheme()
93
+ pendingUpdate = null
94
+ })
95
+ })
96
+ break
97
+ }
98
+ }
99
+ })
100
+
101
+ observer.observe(document.documentElement, {
102
+ attributes: true,
103
+ attributeFilter: ['data-theme', 'class']
104
+ })
105
+ })
106
+
107
+ onUnmounted(() => {
108
+ observer?.disconnect()
109
+ })
110
+
111
+ const gridColor = computed(() => theme.value.colors.gridLines)
112
+ const textColor = computed(() => theme.value.colors.text)
113
+ const mutedTextColor = computed(() => theme.value.colors.textMuted)
114
+
115
+ function getDefaultOptions() {
116
+ return {
117
+ responsive: true,
118
+ maintainAspectRatio: false,
119
+ plugins: {
120
+ legend: {
121
+ display: false
122
+ },
123
+ tooltip: {
124
+ enabled: false
125
+ }
126
+ },
127
+ scales: {
128
+ x: {
129
+ grid: {
130
+ display: false
131
+ },
132
+ border: {
133
+ display: false
134
+ },
135
+ ticks: {
136
+ color: theme.value.colors.textMuted,
137
+ font: {
138
+ family: theme.value.fontFamily,
139
+ size: 12
140
+ }
141
+ }
142
+ },
143
+ y: {
144
+ grid: {
145
+ color: theme.value.colors.gridLines
146
+ },
147
+ border: {
148
+ display: false
149
+ },
150
+ ticks: {
151
+ color: theme.value.colors.textMuted,
152
+ font: {
153
+ family: theme.value.fontFamily,
154
+ size: 12
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ function getColor(index: number): string {
163
+ return theme.value.palette[index % theme.value.palette.length]
164
+ }
165
+
166
+ function getColorWithOpacity(index: number, opacity: number): string {
167
+ const color = getColor(index)
168
+ if (color.startsWith('oklch') || color.startsWith('rgb')) {
169
+ return color.replace(')', ` / ${opacity})`).replace('rgb(', 'rgba(').replace('oklch(', 'oklch(')
170
+ }
171
+ if (color.startsWith('#')) {
172
+ const hex = color.slice(1)
173
+ const r = parseInt(hex.slice(0, 2), 16)
174
+ const g = parseInt(hex.slice(2, 4), 16)
175
+ const b = parseInt(hex.slice(4, 6), 16)
176
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`
177
+ }
178
+ return color
179
+ }
180
+
181
+ return {
182
+ theme,
183
+ themeVersion,
184
+ gridColor,
185
+ textColor,
186
+ mutedTextColor,
187
+ getDefaultOptions,
188
+ getColor,
189
+ getColorWithOpacity,
190
+ updateTheme
191
+ }
192
+ }
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import Checkbox from './Checkbox.vue'
4
+
5
+ describe('Checkbox', () => {
6
+ describe('Rendering', () => {
7
+ it('renders as a label element', () => {
8
+ const wrapper = mount(Checkbox)
9
+ expect(wrapper.find('label').exists()).toBe(true)
10
+ })
11
+
12
+ it('renders native checkbox input', () => {
13
+ const wrapper = mount(Checkbox)
14
+ expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
15
+ })
16
+
17
+ it('renders custom box element', () => {
18
+ const wrapper = mount(Checkbox)
19
+ expect(wrapper.find('.ui-checkbox__box').exists()).toBe(true)
20
+ })
21
+
22
+ it('does not render check icon when unchecked', () => {
23
+ const wrapper = mount(Checkbox, {
24
+ props: { modelValue: false }
25
+ })
26
+ expect(wrapper.find('.ui-checkbox__check').exists()).toBe(false)
27
+ })
28
+
29
+ it('renders check icon when checked', () => {
30
+ const wrapper = mount(Checkbox, {
31
+ props: { modelValue: true }
32
+ })
33
+ expect(wrapper.find('.ui-checkbox__check').exists()).toBe(true)
34
+ })
35
+ })
36
+
37
+ describe('v-model', () => {
38
+ it('reflects unchecked state', () => {
39
+ const wrapper = mount(Checkbox, {
40
+ props: { modelValue: false }
41
+ })
42
+ const input = wrapper.find('input')
43
+ expect((input.element as HTMLInputElement).checked).toBe(false)
44
+ })
45
+
46
+ it('reflects checked state', () => {
47
+ const wrapper = mount(Checkbox, {
48
+ props: { modelValue: true }
49
+ })
50
+ const input = wrapper.find('input')
51
+ expect((input.element as HTMLInputElement).checked).toBe(true)
52
+ expect(wrapper.find('.ui-checkbox').classes()).toContain('ui-checkbox--checked')
53
+ })
54
+
55
+ it('emits update:modelValue on change', async () => {
56
+ const wrapper = mount(Checkbox, {
57
+ props: { modelValue: false }
58
+ })
59
+ await wrapper.find('input').setValue(true)
60
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([true])
61
+ })
62
+
63
+ it('emits change event on change', async () => {
64
+ const wrapper = mount(Checkbox, {
65
+ props: { modelValue: false }
66
+ })
67
+ await wrapper.find('input').setValue(true)
68
+ expect(wrapper.emitted('change')?.[0]).toEqual([true])
69
+ })
70
+
71
+ it('unchecks when toggled from true', async () => {
72
+ const wrapper = mount(Checkbox, {
73
+ props: { modelValue: true }
74
+ })
75
+ await wrapper.find('input').setValue(false)
76
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([false])
77
+ })
78
+ })
79
+
80
+ describe('Indeterminate state', () => {
81
+ it('applies indeterminate class', () => {
82
+ const wrapper = mount(Checkbox, {
83
+ props: { indeterminate: true }
84
+ })
85
+ expect(wrapper.find('.ui-checkbox').classes()).toContain('ui-checkbox--indeterminate')
86
+ })
87
+
88
+ it('renders indeterminate icon instead of check', () => {
89
+ const wrapper = mount(Checkbox, {
90
+ props: { modelValue: true, indeterminate: true }
91
+ })
92
+ expect(wrapper.find('.ui-checkbox__check').exists()).toBe(false)
93
+ expect(wrapper.find('.ui-checkbox__indeterminate').exists()).toBe(true)
94
+ })
95
+
96
+ it('renders indeterminate icon even when unchecked', () => {
97
+ const wrapper = mount(Checkbox, {
98
+ props: { modelValue: false, indeterminate: true }
99
+ })
100
+ expect(wrapper.find('.ui-checkbox__indeterminate').exists()).toBe(true)
101
+ })
102
+ })
103
+
104
+ describe('Size variants', () => {
105
+ const sizes = ['sm', 'md', 'lg'] as const
106
+
107
+ sizes.forEach(size => {
108
+ it(`applies ${size} size class`, () => {
109
+ const wrapper = mount(Checkbox, {
110
+ props: { size }
111
+ })
112
+ expect(wrapper.find('.ui-checkbox').classes()).toContain(`ui-checkbox--${size}`)
113
+ })
114
+ })
115
+
116
+ it('defaults to md size', () => {
117
+ const wrapper = mount(Checkbox)
118
+ expect(wrapper.find('.ui-checkbox').classes()).toContain('ui-checkbox--md')
119
+ })
120
+ })
121
+
122
+ describe('Disabled state', () => {
123
+ it('applies disabled attribute to input', () => {
124
+ const wrapper = mount(Checkbox, {
125
+ props: { disabled: true }
126
+ })
127
+ expect(wrapper.find('input').attributes('disabled')).toBeDefined()
128
+ })
129
+
130
+ it('applies disabled class', () => {
131
+ const wrapper = mount(Checkbox, {
132
+ props: { disabled: true }
133
+ })
134
+ expect(wrapper.find('.ui-checkbox').classes()).toContain('ui-checkbox--disabled')
135
+ })
136
+ })
137
+
138
+ describe('Label and description', () => {
139
+ it('renders label text', () => {
140
+ const wrapper = mount(Checkbox, {
141
+ props: { label: 'Accept terms' }
142
+ })
143
+ expect(wrapper.find('.ui-checkbox__label').text()).toBe('Accept terms')
144
+ })
145
+
146
+ it('renders description text', () => {
147
+ const wrapper = mount(Checkbox, {
148
+ props: { description: 'By checking this you agree' }
149
+ })
150
+ expect(wrapper.find('.ui-checkbox__description').text()).toBe('By checking this you agree')
151
+ })
152
+
153
+ it('renders both label and description', () => {
154
+ const wrapper = mount(Checkbox, {
155
+ props: {
156
+ label: 'Accept terms',
157
+ description: 'By checking this you agree'
158
+ }
159
+ })
160
+ expect(wrapper.find('.ui-checkbox__label').exists()).toBe(true)
161
+ expect(wrapper.find('.ui-checkbox__description').exists()).toBe(true)
162
+ })
163
+
164
+ it('does not render content wrapper when no label or description', () => {
165
+ const wrapper = mount(Checkbox)
166
+ expect(wrapper.find('.ui-checkbox__content').exists()).toBe(false)
167
+ })
168
+ })
169
+
170
+ describe('Accessibility', () => {
171
+ it('auto-generates unique id', () => {
172
+ const wrapper = mount(Checkbox)
173
+ expect(wrapper.find('input').attributes('id')).toMatch(/^checkbox-/)
174
+ })
175
+
176
+ it('uses provided id', () => {
177
+ const wrapper = mount(Checkbox, {
178
+ props: { id: 'custom-checkbox' }
179
+ })
180
+ expect(wrapper.find('input').attributes('id')).toBe('custom-checkbox')
181
+ })
182
+
183
+ it('input is visually hidden but accessible', () => {
184
+ const wrapper = mount(Checkbox)
185
+ expect(wrapper.find('.ui-checkbox__input').exists()).toBe(true)
186
+ })
187
+
188
+ it('custom box has aria-hidden', () => {
189
+ const wrapper = mount(Checkbox)
190
+ expect(wrapper.find('.ui-checkbox__box').attributes('aria-hidden')).toBe('true')
191
+ })
192
+ })
193
+
194
+ describe('Form attributes', () => {
195
+ it('applies name attribute', () => {
196
+ const wrapper = mount(Checkbox, {
197
+ props: { name: 'terms' }
198
+ })
199
+ expect(wrapper.find('input').attributes('name')).toBe('terms')
200
+ })
201
+
202
+ it('applies value attribute', () => {
203
+ const wrapper = mount(Checkbox, {
204
+ props: { value: 'accepted' }
205
+ })
206
+ expect(wrapper.find('input').attributes('value')).toBe('accepted')
207
+ })
208
+ })
209
+ })