@sabrenski/spire-ui 0.0.5 → 0.0.7

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 (86) hide show
  1. package/dist/index.d.ts +170 -4
  2. package/dist/spire-ui.css +1 -1
  3. package/dist/spire-ui.es.js +7040 -6773
  4. package/dist/spire-ui.umd.js +10 -10
  5. package/package.json +83 -70
  6. package/src/components/Accordion/AccordionContent.vue +5 -2
  7. package/src/components/Accordion/AccordionItem.vue +4 -0
  8. package/src/components/Accordion/AccordionRoot.vue +4 -2
  9. package/src/components/Accordion/AccordionTrigger.vue +4 -1
  10. package/src/components/Avatar/Avatar.vue +4 -0
  11. package/src/components/Badge/Badge.vue +4 -0
  12. package/src/components/BadgeContainer/BadgeContainer.vue +4 -1
  13. package/src/components/Breadcrumb/BreadcrumbLink.vue +4 -1
  14. package/src/components/Breadcrumb/BreadcrumbRoot.vue +4 -1
  15. package/src/components/Button/Button.vue +5 -1
  16. package/src/components/Callout/Callout.vue +4 -0
  17. package/src/components/Card/Card.vue +5 -1
  18. package/src/components/Card/CardContent.vue +5 -1
  19. package/src/components/Card/CardFooter.vue +5 -1
  20. package/src/components/Card/CardHeader.vue +5 -1
  21. package/src/components/Card/CardImage.vue +4 -2
  22. package/src/components/Chart/BarChart.vue +4 -0
  23. package/src/components/Chart/BaseChart.vue +52 -47
  24. package/src/components/Chart/DonutChart.vue +4 -2
  25. package/src/components/Chart/LineChart.vue +4 -0
  26. package/src/components/Checkbox/Checkbox.test.ts +94 -0
  27. package/src/components/Checkbox/Checkbox.vue +170 -1
  28. package/src/components/ChoiceChip/ChoiceChip.vue +11 -5
  29. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +4 -2
  30. package/src/components/ColorPicker/ColorArea.vue +4 -2
  31. package/src/components/ColorPicker/ColorPicker.vue +4 -2
  32. package/src/components/ColorPicker/ColorSlider.vue +5 -1
  33. package/src/components/Combobox/Combobox.vue +97 -91
  34. package/src/components/DataTable/DataTable.vue +5 -1
  35. package/src/components/DatePicker/DatePicker.vue +5 -1
  36. package/src/components/Drawer/Drawer.vue +13 -3
  37. package/src/components/Dropdown/Dropdown.vue +4 -2
  38. package/src/components/Dropdown/DropdownItem.vue +4 -0
  39. package/src/components/Dropdown/DropdownSubTrigger.vue +4 -2
  40. package/src/components/EmptyState/EmptyState.vue +5 -1
  41. package/src/components/FileUpload/FileUpload.vue +12 -6
  42. package/src/components/Heading/Heading.vue +4 -0
  43. package/src/components/Icon/Icon.vue +5 -2
  44. package/src/components/Input/Input.vue +5 -1
  45. package/src/components/Layout/Container.vue +4 -0
  46. package/src/components/Layout/Grid.vue +4 -1
  47. package/src/components/Layout/GridItem.vue +4 -1
  48. package/src/components/Layout/Stack.vue +4 -0
  49. package/src/components/Modal/Modal.test.ts +68 -13
  50. package/src/components/Modal/Modal.vue +94 -91
  51. package/src/components/Pagination/Pagination.vue +5 -1
  52. package/src/components/Popover/Popover.vue +4 -1
  53. package/src/components/Progress/Progress.vue +5 -0
  54. package/src/components/Radio/Radio.test.ts +88 -0
  55. package/src/components/Radio/Radio.vue +169 -1
  56. package/src/components/Rating/Rating.vue +5 -1
  57. package/src/components/SegmentedControl/SegmentedControl.vue +5 -1
  58. package/src/components/Select/Select.vue +61 -55
  59. package/src/components/Sidebar/SidebarGroup.vue +4 -0
  60. package/src/components/Sidebar/SidebarItem.vue +4 -0
  61. package/src/components/Sidebar/SidebarLayout.vue +5 -2
  62. package/src/components/Sidebar/SidebarRoot.vue +4 -2
  63. package/src/components/Skeleton/Skeleton.vue +5 -1
  64. package/src/components/Slider/Slider.vue +5 -1
  65. package/src/components/Spinner/Spinner.vue +4 -1
  66. package/src/components/SpireProvider/SpireProvider.vue +4 -1
  67. package/src/components/Stepper/StepperItem.vue +4 -0
  68. package/src/components/Stepper/StepperRoot.vue +4 -2
  69. package/src/components/Stepper/StepperTrigger.vue +6 -2
  70. package/src/components/Switch/Switch.vue +5 -1
  71. package/src/components/Tabs/Tabs.vue +4 -1
  72. package/src/components/Text/Text.vue +4 -0
  73. package/src/components/Textarea/Textarea.vue +13 -7
  74. package/src/components/TimePicker/TimePicker.vue +5 -1
  75. package/src/components/Timeline/Timeline.vue +4 -0
  76. package/src/components/Timeline/TimelineItem.vue +4 -0
  77. package/src/components/Toast/ToastItem.vue +5 -1
  78. package/src/components/Toast/ToastProvider.vue +5 -3
  79. package/src/components/ToggleButton/ToggleButton.vue +5 -1
  80. package/src/components/ToggleGroup/ToggleGroup.vue +5 -1
  81. package/src/components/Tooltip/Tooltip.vue +9 -1
  82. package/src/components/TreeView/TreeView.vue +4 -1
  83. package/src/components/TreeView/TreeViewItem.vue +4 -0
  84. package/src/index.ts +3 -0
  85. package/src/styles/main.css +21 -21
  86. package/src/types/common.ts +4 -0
@@ -1,12 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
  import { useId } from '../../composables'
4
+ import type { ClassValue } from '../../types/common'
4
5
 
5
6
  export interface RadioProps {
7
+ /** Additional CSS classes */
8
+ class?: ClassValue
6
9
  /** Selected value (v-model) - shared across radio group */
7
10
  modelValue?: string | number | boolean
8
11
  /** Value of this radio option */
9
12
  value: string | number | boolean
13
+ /** Visual variant */
14
+ variant?: 'default' | 'pill'
10
15
  /** Radio size */
11
16
  size?: 'sm' | 'md' | 'lg'
12
17
  /** Disabled state */
@@ -22,6 +27,7 @@ export interface RadioProps {
22
27
  }
23
28
 
24
29
  const props = withDefaults(defineProps<RadioProps>(), {
30
+ variant: 'default',
25
31
  size: 'md',
26
32
  disabled: false
27
33
  })
@@ -44,9 +50,53 @@ function handleChange() {
44
50
  </script>
45
51
 
46
52
  <template>
53
+ <!-- Pill variant -->
47
54
  <label
48
- class="ui-radio"
55
+ v-if="variant === 'pill'"
49
56
  :class="[
57
+ props.class,
58
+ 'ui-radio-pill',
59
+ `ui-radio-pill--${size}`,
60
+ {
61
+ 'ui-radio-pill--disabled': disabled,
62
+ 'ui-radio-pill--checked': isChecked
63
+ }
64
+ ]"
65
+ >
66
+ <input
67
+ :id="radioId"
68
+ type="radio"
69
+ :checked="isChecked"
70
+ :disabled="disabled"
71
+ :name="name"
72
+ :value="value"
73
+ class="ui-radio-pill__input"
74
+ @change="handleChange"
75
+ />
76
+
77
+ <span class="ui-radio-pill__indicator" aria-hidden="true">
78
+ <svg
79
+ class="ui-radio-pill__check"
80
+ viewBox="0 0 16 16"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ stroke-width="2.5"
84
+ stroke-linecap="round"
85
+ stroke-linejoin="round"
86
+ >
87
+ <path d="M3.5 8.5L6.5 11L12.5 5" />
88
+ </svg>
89
+ </span>
90
+
91
+ <span v-if="label" class="ui-radio-pill__label">{{ label }}</span>
92
+ </label>
93
+
94
+ <!-- Default variant -->
95
+ <label
96
+ v-else
97
+ :class="[
98
+ props.class,
99
+ 'ui-radio',
50
100
  `ui-radio--${size}`,
51
101
  {
52
102
  'ui-radio--disabled': disabled,
@@ -211,4 +261,122 @@ function handleChange() {
211
261
  .ui-radio--lg .ui-radio__description {
212
262
  font-size: var(--text-sm);
213
263
  }
264
+
265
+ /* Pill variant styles */
266
+ .ui-radio-pill {
267
+ display: inline-flex;
268
+ align-items: center;
269
+ gap: var(--space-2);
270
+ height: 36px;
271
+ padding: 0 var(--space-3);
272
+ background: var(--chip-bg);
273
+ border: 1px solid var(--chip-border);
274
+ border-radius: var(--radius-full);
275
+ font-family: var(--font-sans);
276
+ font-size: var(--text-sm);
277
+ font-weight: var(--font-medium);
278
+ color: var(--chip-text);
279
+ cursor: pointer;
280
+ transition:
281
+ background-color var(--duration-fast) var(--ease-default),
282
+ border-color var(--duration-fast) var(--ease-default),
283
+ color var(--duration-fast) var(--ease-default);
284
+ -webkit-tap-highlight-color: transparent;
285
+ }
286
+
287
+ .ui-radio-pill--sm {
288
+ height: 32px;
289
+ font-size: var(--text-xs);
290
+ }
291
+
292
+ .ui-radio-pill--lg {
293
+ height: 44px;
294
+ font-size: var(--text-md);
295
+ padding: 0 var(--space-4);
296
+ }
297
+
298
+ .ui-radio-pill__input {
299
+ position: absolute;
300
+ width: 1px;
301
+ height: 1px;
302
+ padding: 0;
303
+ margin: -1px;
304
+ overflow: hidden;
305
+ clip: rect(0, 0, 0, 0);
306
+ white-space: nowrap;
307
+ border: 0;
308
+ }
309
+
310
+ .ui-radio-pill:not(.ui-radio-pill--disabled):hover {
311
+ background: var(--chip-bg-hover);
312
+ border-color: var(--chip-border-hover);
313
+ }
314
+
315
+ .ui-radio-pill:not(.ui-radio-pill--disabled):active {
316
+ background: var(--chip-bg-active);
317
+ border-color: var(--chip-border-active);
318
+ }
319
+
320
+ .ui-radio-pill--checked {
321
+ background: var(--chip-bg-selected);
322
+ border-color: var(--chip-border-selected);
323
+ color: var(--chip-text-selected);
324
+ }
325
+
326
+ .ui-radio-pill--checked:not(.ui-radio-pill--disabled):hover {
327
+ background: var(--chip-bg-selected-hover);
328
+ border-color: var(--chip-border-selected-hover);
329
+ }
330
+
331
+ .ui-radio-pill--checked:not(.ui-radio-pill--disabled):active {
332
+ background: var(--chip-bg-selected-active);
333
+ border-color: var(--chip-border-selected-active);
334
+ }
335
+
336
+ .ui-radio-pill:has(.ui-radio-pill__input:focus-visible) {
337
+ outline: 2px solid var(--ring-color);
338
+ outline-offset: 2px;
339
+ }
340
+
341
+ .ui-radio-pill--disabled {
342
+ opacity: 0.5;
343
+ cursor: not-allowed;
344
+ }
345
+
346
+ .ui-radio-pill__indicator {
347
+ display: flex;
348
+ align-items: center;
349
+ justify-content: center;
350
+ width: 0;
351
+ margin-right: calc(-1 * var(--space-2));
352
+ overflow: hidden;
353
+ transition:
354
+ width var(--duration-fast) var(--ease-out-expo),
355
+ margin var(--duration-fast) var(--ease-out-expo);
356
+ }
357
+
358
+ .ui-radio-pill--checked .ui-radio-pill__indicator {
359
+ width: 1rem;
360
+ margin-right: 0;
361
+ }
362
+
363
+ .ui-radio-pill__check {
364
+ width: 0.875rem;
365
+ height: 0.875rem;
366
+ flex-shrink: 0;
367
+ opacity: 0;
368
+ transform: scale(0.5);
369
+ transition:
370
+ opacity var(--duration-fast) var(--ease-default),
371
+ transform var(--duration-fast) var(--ease-out-back);
372
+ }
373
+
374
+ .ui-radio-pill--checked .ui-radio-pill__check {
375
+ opacity: 1;
376
+ transform: scale(1);
377
+ }
378
+
379
+ .ui-radio-pill__label {
380
+ white-space: nowrap;
381
+ }
214
382
  </style>
@@ -2,11 +2,14 @@
2
2
  import { computed, ref } from 'vue'
3
3
  import Icon from '../Icon/Icon.vue'
4
4
  import { useInternalIcon } from '../../config/icons'
5
+ import type { ClassValue } from '../../types/common'
5
6
 
6
7
  const StarIcon = useInternalIcon('star')
7
8
  const StarFilledIcon = useInternalIcon('starFilled')
8
9
 
9
10
  export interface RatingProps {
11
+ /** Additional CSS classes */
12
+ class?: ClassValue
10
13
  /** Current rating value (0 to max) */
11
14
  modelValue: number
12
15
  /** Maximum rating value */
@@ -134,8 +137,9 @@ const iconSize = computed(() => {
134
137
 
135
138
  <template>
136
139
  <div
137
- class="ui-rating"
138
140
  :class="[
141
+ props.class,
142
+ 'ui-rating',
139
143
  `ui-rating--${size}`,
140
144
  { 'ui-rating--readonly': readonly }
141
145
  ]"
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watch, onMounted, nextTick } from 'vue'
3
3
  import { useId } from '../../composables'
4
+ import type { ClassValue } from '../../types/common'
4
5
 
5
6
  export interface SegmentedOption {
6
7
  label: string
@@ -9,6 +10,8 @@ export interface SegmentedOption {
9
10
  }
10
11
 
11
12
  export interface SegmentedControlProps {
13
+ /** Additional CSS classes */
14
+ class?: ClassValue
12
15
  /** Array of options to display */
13
16
  options: SegmentedOption[]
14
17
  /** Selected value (v-model) */
@@ -146,8 +149,9 @@ function setItemRef(el: HTMLButtonElement | null, index: number) {
146
149
  role="radiogroup"
147
150
  :aria-label="label"
148
151
  :aria-disabled="disabled || undefined"
149
- class="ui-segmented"
150
152
  :class="[
153
+ props.class,
154
+ 'ui-segmented',
151
155
  `ui-segmented--${size}`,
152
156
  { 'ui-segmented--disabled': disabled }
153
157
  ]"
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
3
3
  import { useId } from '../../composables'
4
+ import type { ClassValue } from '../../types/common'
4
5
 
5
6
  export interface SelectOption {
6
7
  /** Display text shown to user */
@@ -12,6 +13,8 @@ export interface SelectOption {
12
13
  }
13
14
 
14
15
  export interface SelectProps {
16
+ /** Additional CSS classes */
17
+ class?: ClassValue
15
18
  /** Selected value (v-model) */
16
19
  modelValue?: string | number | null
17
20
  /** Available options */
@@ -301,8 +304,9 @@ onBeforeUnmount(() => {
301
304
 
302
305
  <template>
303
306
  <div
304
- class="ui-select"
305
307
  :class="[
308
+ props.class,
309
+ 'ui-select',
306
310
  `ui-select--${size}`,
307
311
  {
308
312
  'ui-select--block': block,
@@ -602,65 +606,67 @@ onBeforeUnmount(() => {
602
606
  </style>
603
607
 
604
608
  <style>
605
- .ui-select__listbox {
606
- z-index: 9999;
607
- margin: 0;
608
- padding: var(--space-1);
609
- list-style: none;
610
- background-color: var(--select-menu-bg);
611
- border: 1px solid var(--select-menu-border);
612
- border-radius: var(--radius-lg);
613
- box-shadow: var(--shadow-lg);
614
- max-height: 256px;
615
- overflow-y: auto;
616
- overscroll-behavior: contain;
617
- }
618
-
619
- .ui-select__option {
620
- display: flex;
621
- align-items: center;
622
- justify-content: space-between;
623
- gap: var(--space-2);
624
- padding: var(--space-2) var(--space-3);
625
- border-radius: var(--radius-md);
626
- font-family: var(--font-sans);
627
- font-size: var(--text-sm);
628
- color: var(--select-option-text);
629
- cursor: pointer;
630
- transition: background-color var(--duration-fast) var(--ease-default);
631
- }
609
+ @layer spire-components {
610
+ .ui-select__listbox {
611
+ z-index: 9999;
612
+ margin: 0;
613
+ padding: var(--space-1);
614
+ list-style: none;
615
+ background-color: var(--select-menu-bg);
616
+ border: 1px solid var(--select-menu-border);
617
+ border-radius: var(--radius-lg);
618
+ box-shadow: var(--shadow-lg);
619
+ max-height: 256px;
620
+ overflow-y: auto;
621
+ overscroll-behavior: contain;
622
+ }
632
623
 
633
- .ui-select__option--highlighted {
634
- background-color: var(--select-option-hover);
635
- }
624
+ .ui-select__option {
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: space-between;
628
+ gap: var(--space-2);
629
+ padding: var(--space-2) var(--space-3);
630
+ border-radius: var(--radius-md);
631
+ font-family: var(--font-sans);
632
+ font-size: var(--text-sm);
633
+ color: var(--select-option-text);
634
+ cursor: pointer;
635
+ transition: background-color var(--duration-fast) var(--ease-default);
636
+ }
636
637
 
637
- .ui-select__option--selected {
638
- color: var(--select-option-selected);
639
- font-weight: var(--font-medium);
640
- }
638
+ .ui-select__option--highlighted {
639
+ background-color: var(--select-option-hover);
640
+ }
641
641
 
642
- .ui-select__option--disabled {
643
- opacity: 0.5;
644
- cursor: not-allowed;
645
- }
642
+ .ui-select__option--selected {
643
+ color: var(--select-option-selected);
644
+ font-weight: var(--font-medium);
645
+ }
646
646
 
647
- .ui-select__check {
648
- flex-shrink: 0;
649
- width: 1rem;
650
- height: 1rem;
651
- color: var(--select-option-selected);
652
- }
647
+ .ui-select__option--disabled {
648
+ opacity: 0.5;
649
+ cursor: not-allowed;
650
+ }
653
651
 
654
- .ui-select-menu-enter-active,
655
- .ui-select-menu-leave-active {
656
- transition:
657
- opacity var(--duration-fast) var(--ease-default),
658
- transform var(--duration-fast) var(--ease-default);
659
- }
652
+ .ui-select__check {
653
+ flex-shrink: 0;
654
+ width: 1rem;
655
+ height: 1rem;
656
+ color: var(--select-option-selected);
657
+ }
660
658
 
661
- .ui-select-menu-enter-from,
662
- .ui-select-menu-leave-to {
663
- opacity: 0;
664
- transform: translateY(-4px);
659
+ .ui-select-menu-enter-active,
660
+ .ui-select-menu-leave-active {
661
+ transition:
662
+ opacity var(--duration-fast) var(--ease-default),
663
+ transform var(--duration-fast) var(--ease-default);
664
+ }
665
+
666
+ .ui-select-menu-enter-from,
667
+ .ui-select-menu-leave-to {
668
+ opacity: 0;
669
+ transform: translateY(-4px);
670
+ }
665
671
  }
666
672
  </style>
@@ -2,8 +2,11 @@
2
2
  import { computed, inject, provide } from 'vue'
3
3
  import { useId } from '../../composables'
4
4
  import { SidebarKey, SidebarGroupKey } from './keys'
5
+ import type { ClassValue } from '../../types/common'
5
6
 
6
7
  export interface SidebarGroupProps {
8
+ /** Additional CSS classes */
9
+ class?: ClassValue
7
10
  /** Group label displayed when expanded */
8
11
  label?: string
9
12
  /** Show separator line when collapsed */
@@ -26,6 +29,7 @@ const isCollapsed = computed(() => sidebar.collapsed.value)
26
29
  provide(SidebarGroupKey, { groupId })
27
30
 
28
31
  const groupClasses = computed(() => [
32
+ props.class,
29
33
  'ui-sidebar-group',
30
34
  {
31
35
  'ui-sidebar-group--collapsed': isCollapsed.value,
@@ -3,11 +3,14 @@ import { computed, inject, type Component } from 'vue'
3
3
  import { SidebarKey } from './keys'
4
4
  import Icon from '../Icon/Icon.vue'
5
5
  import Tooltip from '../Tooltip/Tooltip.vue'
6
+ import type { ClassValue } from '../../types/common'
6
7
 
7
8
  type HugeIconData = [string, Record<string, unknown>][]
8
9
  type IconInput = Component | HugeIconData
9
10
 
10
11
  export interface SidebarItemProps {
12
+ /** Additional CSS classes */
13
+ class?: ClassValue
11
14
  /** Text label for the item */
12
15
  label: string
13
16
  /** Icon component or HugeIcons data */
@@ -41,6 +44,7 @@ if (!sidebar) {
41
44
  const isCollapsed = computed(() => sidebar.collapsed.value)
42
45
 
43
46
  const itemClasses = computed(() => [
47
+ props.class,
44
48
  'ui-sidebar-item',
45
49
  {
46
50
  'ui-sidebar-item--active': props.active,
@@ -1,5 +1,9 @@
1
1
  <script setup lang="ts">
2
+ import type { ClassValue } from '../../types/common'
3
+
2
4
  export interface SidebarLayoutProps {
5
+ /** Additional CSS classes */
6
+ class?: ClassValue
3
7
  /** Position of the sidebar */
4
8
  sidebarPosition?: 'left' | 'right'
5
9
  }
@@ -11,8 +15,7 @@ const props = withDefaults(defineProps<SidebarLayoutProps>(), {
11
15
 
12
16
  <template>
13
17
  <div
14
- class="ui-sidebar-layout"
15
- :class="[`ui-sidebar-layout--${sidebarPosition}`]"
18
+ :class="[props.class, 'ui-sidebar-layout', `ui-sidebar-layout--${sidebarPosition}`]"
16
19
  >
17
20
  <slot name="sidebar" />
18
21
  <main class="ui-sidebar-layout__main">
@@ -1,8 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, provide, toRef, ref, watch } from 'vue'
3
3
  import { SidebarKey } from './keys'
4
+ import type { ClassValue } from '../../types/common'
4
5
 
5
6
  export interface SidebarRootProps {
7
+ /** Additional CSS classes */
8
+ class?: ClassValue
6
9
  /** Whether the sidebar is collapsed */
7
10
  modelValue?: boolean
8
11
  /** Width when expanded */
@@ -54,8 +57,7 @@ defineExpose({ toggle, collapsed })
54
57
 
55
58
  <template>
56
59
  <aside
57
- class="ui-sidebar"
58
- :class="{ 'ui-sidebar--collapsed': collapsed }"
60
+ :class="[props.class, 'ui-sidebar', { 'ui-sidebar--collapsed': collapsed }]"
59
61
  :style="{ '--sidebar-current-width': currentWidth }"
60
62
  role="navigation"
61
63
  >
@@ -1,9 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
+ import type { ClassValue } from '../../types/common'
3
4
 
4
5
  export type SkeletonVariant = 'text' | 'circle' | 'rect'
5
6
 
6
7
  export interface SkeletonProps {
8
+ /** Additional CSS classes */
9
+ class?: ClassValue
7
10
  /** Shape variant */
8
11
  variant?: SkeletonVariant
9
12
  /** Width (px, %, or CSS value) */
@@ -34,8 +37,9 @@ const style = computed(() => ({
34
37
 
35
38
  <template>
36
39
  <span
37
- class="ui-skeleton"
38
40
  :class="[
41
+ props.class,
42
+ 'ui-skeleton',
39
43
  `ui-skeleton--${variant}`,
40
44
  `ui-skeleton--${animation}`
41
45
  ]"
@@ -1,8 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, onUnmounted } from 'vue'
3
3
  import { valueToPercent, getValueFromPointer, getClosestThumb, clamp, snapToStep } from './utils'
4
+ import type { ClassValue } from '../../types/common'
4
5
 
5
6
  export interface SliderProps {
7
+ /** Additional CSS classes */
8
+ class?: ClassValue
6
9
  /** Current value (number for single, [min, max] tuple for range) */
7
10
  modelValue: number | [number, number]
8
11
  /** Minimum value */
@@ -231,8 +234,9 @@ onUnmounted(() => {
231
234
 
232
235
  <template>
233
236
  <div
234
- class="ui-slider"
235
237
  :class="[
238
+ props.class,
239
+ 'ui-slider',
236
240
  `ui-slider--${size}`,
237
241
  {
238
242
  'ui-slider--disabled': disabled,
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
+ import type { ClassValue } from '../../types/common'
3
4
 
4
5
  export interface SpinnerProps {
6
+ /** Additional CSS classes */
7
+ class?: ClassValue
5
8
  /** Predefined size or custom value */
6
9
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | string
7
10
  /** Animation speed in seconds */
@@ -30,7 +33,7 @@ const resolvedSpeed = computed(() => `${props.speed}s`)
30
33
 
31
34
  <template>
32
35
  <div
33
- class="ui-spinner"
36
+ :class="[props.class, 'ui-spinner']"
34
37
  role="status"
35
38
  :aria-label="label"
36
39
  :style="{
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { InjectionKey, ComputedRef } from 'vue'
3
+ import type { ClassValue } from '../../types/common'
3
4
 
4
5
  export type Theme = 'light' | 'dark'
5
6
  export type Mood = 'warm' | 'cool' | 'vibrant' | 'muted' | 'earthy'
@@ -16,6 +17,8 @@ export interface SpireConfig {
16
17
  }
17
18
 
18
19
  export interface SpireProviderProps {
20
+ /** Additional CSS classes */
21
+ class?: ClassValue
19
22
  theme?: Theme
20
23
  mood?: Mood
21
24
  depth?: Depth
@@ -58,7 +61,7 @@ const dataAttributes = computed(() => {
58
61
  </script>
59
62
 
60
63
  <template>
61
- <component :is="tag" class="spire-provider" v-bind="dataAttributes">
64
+ <component :is="tag" :class="[props.class, 'spire-provider']" v-bind="dataAttributes">
62
65
  <slot />
63
66
  </component>
64
67
  </template>
@@ -2,8 +2,11 @@
2
2
  import { computed, provide, inject, onMounted } from 'vue'
3
3
  import { useId } from '../../composables'
4
4
  import { StepperKey, StepperItemKey } from './keys'
5
+ import type { ClassValue } from '../../types/common'
5
6
 
6
7
  export interface StepperItemProps {
8
+ /** Additional CSS classes */
9
+ class?: ClassValue
7
10
  /** Step index (0-based) */
8
11
  index: number
9
12
  /** Disable this step */
@@ -47,6 +50,7 @@ provide(StepperItemKey, {
47
50
  })
48
51
 
49
52
  const itemClasses = computed(() => [
53
+ props.class,
50
54
  'ui-stepper__item',
51
55
  `ui-stepper__item--${stepper.orientation.value}`,
52
56
  `ui-stepper__item--${state.value}`,
@@ -2,8 +2,11 @@
2
2
  import { computed, provide, toRef, ref, watch } from 'vue'
3
3
  import { StepperKey } from './keys'
4
4
  import type { StepperOrientation, StepState } from './keys'
5
+ import type { ClassValue } from '../../types/common'
5
6
 
6
7
  export interface StepperRootProps {
8
+ /** Additional CSS classes */
9
+ class?: ClassValue
7
10
  /** Current step index (0-based, v-model) */
8
11
  modelValue?: number
9
12
  /** Layout direction */
@@ -75,8 +78,7 @@ provide(StepperKey, {
75
78
 
76
79
  <template>
77
80
  <div
78
- class="ui-stepper"
79
- :class="[`ui-stepper--${orientation}`]"
81
+ :class="[props.class, 'ui-stepper', `ui-stepper--${orientation}`]"
80
82
  role="tablist"
81
83
  :aria-orientation="orientation"
82
84
  >
@@ -2,14 +2,15 @@
2
2
  import { computed, inject, h, type Component } from 'vue'
3
3
  import { StepperKey, StepperItemKey } from './keys'
4
4
  import { useInternalIcon } from '../../config/icons'
5
+ import type { ClassValue } from '../../types/common'
5
6
 
6
7
  export interface StepperTriggerProps {
8
+ /** Additional CSS classes */
9
+ class?: ClassValue
7
10
  /** Custom icon to display instead of step number */
8
11
  icon?: Component
9
12
  }
10
13
 
11
- defineProps<StepperTriggerProps>()
12
-
13
14
  const stepper = inject(StepperKey)
14
15
  const stepperItem = inject(StepperItemKey)
15
16
 
@@ -19,7 +20,10 @@ if (!stepper || !stepperItem) {
19
20
 
20
21
  const CheckIcon = useInternalIcon('check')
21
22
 
23
+ const props = defineProps<StepperTriggerProps>()
24
+
22
25
  const triggerClasses = computed(() => [
26
+ props.class,
23
27
  'ui-stepper__trigger',
24
28
  `ui-stepper__trigger--${stepperItem.state.value}`
25
29
  ])
@@ -2,8 +2,11 @@
2
2
  import { computed } from 'vue'
3
3
  import Spinner from '../Spinner/Spinner.vue'
4
4
  import { useId } from '../../composables'
5
+ import type { ClassValue } from '../../types/common'
5
6
 
6
7
  export interface SwitchProps {
8
+ /** Additional CSS classes */
9
+ class?: ClassValue
7
10
  /** Checked state (v-model) */
8
11
  modelValue?: boolean
9
12
  /** Switch size */
@@ -67,8 +70,9 @@ const spinnerSize = computed(() => spinnerSizeMap[props.size])
67
70
  :aria-checked="modelValue"
68
71
  :aria-label="label"
69
72
  :disabled="isDisabled"
70
- class="ui-switch"
71
73
  :class="[
74
+ props.class,
75
+ 'ui-switch',
72
76
  `ui-switch--${size}`,
73
77
  {
74
78
  'ui-switch--checked': modelValue,