@spavn/ui 0.0.1 → 0.0.2

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.
@@ -9,6 +9,9 @@ export const badgeVariants = cva(
9
9
  secondary: 'border-transparent bg-secondary text-secondary-foreground',
10
10
  destructive: 'border-transparent bg-destructive text-destructive-foreground shadow-depth-1',
11
11
  outline: 'text-foreground',
12
+ soft: 'border-transparent',
13
+ surface: '',
14
+ dot: 'border-transparent bg-transparent text-foreground font-normal gap-1.5',
12
15
  },
13
16
  size: {
14
17
  sm: 'px-2 py-0 text-[10px]',
@@ -1,61 +1,116 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  import { cn } from '@/lib/utils'
4
+ import { type SpavnColor, type SpavnRadius, colorClasses, radiusMap, colorHoverSolid, colorHoverOutline, colorHoverGhost, colorHoverLink } from '@/lib/types'
3
5
  import { buttonVariants, type ButtonVariants } from './variants'
4
6
 
5
7
  interface Props {
6
8
  variant?: ButtonVariants['variant']
7
9
  size?: ButtonVariants['size']
8
10
  elevation?: ButtonVariants['elevation']
11
+ color?: SpavnColor
12
+ radius?: SpavnRadius
9
13
  class?: string
10
14
  disabled?: boolean
11
15
  type?: 'button' | 'submit' | 'reset'
12
16
  loading?: boolean
17
+ loadingPosition?: 'left' | 'right' | 'center'
13
18
  }
14
19
 
15
20
  const props = withDefaults(defineProps<Props>(), {
16
21
  type: 'button',
17
22
  loading: false,
23
+ loadingPosition: 'center',
24
+ })
25
+
26
+ const colorStyle = computed(() => {
27
+ if (!props.color || props.color === 'default') return ''
28
+ const c = props.color as Exclude<SpavnColor, 'default'>
29
+ const v = props.variant || 'default'
30
+
31
+ // Base color classes from shared utility
32
+ const styleMap: Record<string, 'solid' | 'soft' | 'surface' | 'outline'> = {
33
+ default: 'solid',
34
+ destructive: 'solid',
35
+ soft: 'soft',
36
+ surface: 'surface',
37
+ outline: 'outline',
38
+ secondary: 'solid',
39
+ ghost: 'soft',
40
+ link: 'outline',
41
+ plain: 'soft',
42
+ }
43
+ const base = colorClasses(props.color, styleMap[v] || 'solid')
44
+
45
+ // Static hover/active class maps (no template literals)
46
+ const hoverMap: Record<string, string> = {
47
+ default: colorHoverSolid[c],
48
+ destructive: colorHoverSolid[c],
49
+ secondary: colorHoverSolid[c],
50
+ outline: colorHoverOutline[c],
51
+ ghost: colorHoverGhost[c],
52
+ soft: '',
53
+ surface: '',
54
+ link: colorHoverLink[c],
55
+ plain: colorHoverGhost[c],
56
+ }
57
+
58
+ return `${base} ${hoverMap[v] || ''}`
59
+ })
60
+
61
+ const radiusClass = computed(() => props.radius ? radiusMap[props.radius] : '')
62
+
63
+ const iconSizeClass = computed(() => {
64
+ const map: Record<string, string> = {
65
+ sm: 'h-3.5 w-3.5',
66
+ default: 'h-4 w-4',
67
+ lg: 'h-5 w-5',
68
+ icon: 'h-4 w-4',
69
+ }
70
+ return map[props.size || 'default'] || 'h-4 w-4'
18
71
  })
19
72
  </script>
20
73
 
21
74
  <template>
22
75
  <button
23
76
  :type="type"
24
- :class="cn(buttonVariants({ variant, size, elevation }), props.class)"
77
+ :class="cn(buttonVariants({ variant, size, elevation }), colorStyle, radiusClass, props.class)"
25
78
  :disabled="disabled || loading"
26
79
  >
27
- <!-- Loading Spinner -->
80
+ <!-- Center loading (current behavior) -->
28
81
  <span
29
- v-if="loading"
82
+ v-if="loading && loadingPosition === 'center'"
30
83
  class="absolute inset-0 flex items-center justify-center"
31
84
  >
32
- <svg
33
- class="animate-spin h-4 w-4"
34
- xmlns="http://www.w3.org/2000/svg"
35
- fill="none"
36
- viewBox="0 0 24 24"
37
- >
38
- <circle
39
- class="opacity-25"
40
- cx="12"
41
- cy="12"
42
- r="10"
43
- stroke="currentColor"
44
- stroke-width="4"
45
- />
46
- <path
47
- class="opacity-75"
48
- fill="currentColor"
49
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
50
- />
85
+ <svg class="animate-spin" :class="iconSizeClass" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
86
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
87
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
51
88
  </svg>
52
89
  </span>
53
-
90
+
54
91
  <span
55
- :class="{ 'opacity-0': loading }"
92
+ :class="{ 'opacity-0': loading && loadingPosition === 'center' }"
56
93
  class="inline-flex items-center justify-center gap-2"
57
94
  >
95
+ <!-- Left: loading spinner or left-icon slot -->
96
+ <svg v-if="loading && loadingPosition === 'left'" class="animate-spin" :class="iconSizeClass" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
97
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
98
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
99
+ </svg>
100
+ <span v-else-if="$slots['left-icon']" :class="iconSizeClass" class="inline-flex items-center justify-center shrink-0">
101
+ <slot name="left-icon" />
102
+ </span>
103
+
58
104
  <slot />
105
+
106
+ <!-- Right: loading spinner or right-icon slot -->
107
+ <svg v-if="loading && loadingPosition === 'right'" class="animate-spin" :class="iconSizeClass" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
108
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
109
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
110
+ </svg>
111
+ <span v-else-if="$slots['right-icon']" :class="iconSizeClass" class="inline-flex items-center justify-center shrink-0">
112
+ <slot name="right-icon" />
113
+ </span>
59
114
  </span>
60
115
  </button>
61
116
  </template>
@@ -42,6 +42,21 @@ export const buttonVariants = cva(
42
42
  'text-primary underline-offset-4 hover:underline',
43
43
  'hover:translate-y-0',
44
44
  ].join(' '),
45
+ soft: [
46
+ 'border-transparent',
47
+ 'hover:opacity-80 hover:shadow-depth-2',
48
+ 'active:opacity-90',
49
+ ].join(' '),
50
+ surface: [
51
+ 'hover:opacity-80 hover:shadow-depth-2',
52
+ 'active:opacity-90',
53
+ ].join(' '),
54
+ plain: [
55
+ 'border-transparent bg-transparent',
56
+ 'hover:bg-accent hover:text-accent-foreground',
57
+ 'active:bg-accent/80',
58
+ 'hover:translate-y-0',
59
+ ].join(' '),
45
60
  },
46
61
  size: {
47
62
  default: 'h-10 px-4 py-2.5',
@@ -5,11 +5,15 @@ interface Props {
5
5
  class?: string
6
6
  elevation?: 0 | 1 | 2 | 3
7
7
  interactive?: boolean
8
+ direction?: 'vertical' | 'horizontal'
9
+ variant?: 'elevated' | 'outlined' | 'filled' | 'ghost'
8
10
  }
9
11
 
10
12
  const props = withDefaults(defineProps<Props>(), {
11
13
  elevation: 1,
12
14
  interactive: false,
15
+ direction: 'vertical',
16
+ variant: 'elevated',
13
17
  })
14
18
 
15
19
  const elevationClasses = {
@@ -18,14 +22,23 @@ const elevationClasses = {
18
22
  2: 'shadow-depth-2',
19
23
  3: 'shadow-depth-3',
20
24
  }
25
+
26
+ const variantClasses = {
27
+ elevated: 'border bg-card text-card-foreground',
28
+ outlined: 'border bg-transparent text-card-foreground shadow-none',
29
+ filled: 'border-transparent bg-muted text-foreground shadow-none',
30
+ ghost: 'border-transparent bg-transparent text-foreground shadow-none',
31
+ }
21
32
  </script>
22
33
 
23
34
  <template>
24
35
  <div
25
36
  :class="
26
37
  cn(
27
- 'rounded-lg border bg-card text-card-foreground',
28
- elevationClasses[elevation],
38
+ 'rounded-lg',
39
+ variantClasses[variant],
40
+ variant === 'elevated' && elevationClasses[elevation],
41
+ direction === 'horizontal' && 'flex flex-row',
29
42
  interactive && 'transition-elevation hover:shadow-depth-2 cursor-pointer',
30
43
  props.class
31
44
  )
@@ -4,9 +4,36 @@ import {
4
4
  CheckboxIndicator,
5
5
  CheckboxRoot,
6
6
  } from 'radix-vue'
7
+ import { computed } from 'vue'
7
8
  import { cn } from '@/lib/utils'
9
+ import { type SpavnColor, colorCheckedFull } from '@/lib/types'
8
10
 
9
- const props = defineProps<{ class?: string }>()
11
+ interface Props {
12
+ class?: string
13
+ size?: 'sm' | 'default' | 'lg'
14
+ color?: SpavnColor
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ size: 'default',
19
+ })
20
+
21
+ const sizeClasses = {
22
+ sm: 'h-3.5 w-3.5',
23
+ default: 'h-4 w-4',
24
+ lg: 'h-5 w-5',
25
+ }
26
+
27
+ const iconSizeClasses = {
28
+ sm: 'h-3 w-3',
29
+ default: 'h-4 w-4',
30
+ lg: 'h-5 w-5',
31
+ }
32
+
33
+ const colorCls = computed(() => {
34
+ const c = props.color && props.color !== 'default' ? props.color as Exclude<SpavnColor, 'default'> : 'primary'
35
+ return colorCheckedFull[c]
36
+ })
10
37
  </script>
11
38
 
12
39
  <template>
@@ -14,7 +41,9 @@ const props = defineProps<{ class?: string }>()
14
41
  v-bind="$attrs"
15
42
  :class="
16
43
  cn(
17
- 'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow-depth-1 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
44
+ 'peer shrink-0 rounded-sm border shadow-depth-1 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
45
+ sizeClasses[props.size],
46
+ colorCls,
18
47
  props.class
19
48
  )
20
49
  "
@@ -30,7 +59,7 @@ const props = defineProps<{ class?: string }>()
30
59
  stroke-width="2"
31
60
  stroke-linecap="round"
32
61
  stroke-linejoin="round"
33
- class="h-4 w-4"
62
+ :class="iconSizeClasses[props.size]"
34
63
  >
35
64
  <path d="M20 6 9 17l-5-5" />
36
65
  </svg>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, useSlots } from 'vue'
3
3
  import { cn } from '@/lib/utils'
4
+ import { type SpavnRadius, radiusMap } from '@/lib/types'
4
5
 
5
6
  interface Props {
6
7
  class?: string
@@ -8,20 +9,34 @@ interface Props {
8
9
  disabled?: boolean
9
10
  placeholder?: string
10
11
  modelValue?: string | number
11
- size?: 'sm' | 'default' | 'lg'
12
+ size?: 'xs' | 'sm' | 'default' | 'lg' | 'xl'
13
+ variant?: 'outline' | 'filled' | 'underline' | 'plain'
14
+ radius?: SpavnRadius
12
15
  }
13
16
 
14
17
  const props = withDefaults(defineProps<Props>(), {
15
18
  type: 'text',
16
19
  size: 'default',
20
+ variant: 'outline',
17
21
  })
18
22
 
19
- const sizeClasses = {
20
- sm: 'h-8 text-xs',
21
- default: 'h-10 text-sm',
22
- lg: 'h-12 text-base',
23
+ const sizeClasses: Record<string, string> = {
24
+ xs: 'h-7 text-xs px-2',
25
+ sm: 'h-8 text-xs px-3',
26
+ default: 'h-10 text-sm px-3',
27
+ lg: 'h-12 text-base px-4',
28
+ xl: 'h-14 text-base px-4',
23
29
  }
24
30
 
31
+ const variantClasses = {
32
+ outline: 'border border-input bg-background shadow-depth-1 focus-visible:shadow-depth-2 rounded-lg',
33
+ filled: 'border-transparent bg-muted rounded-lg',
34
+ underline: 'border-b border-input bg-transparent rounded-none px-0 focus-visible:shadow-none',
35
+ plain: 'border-transparent bg-transparent rounded-lg focus-visible:shadow-none',
36
+ }
37
+
38
+ const radiusClass = computed(() => props.radius ? radiusMap[props.radius] : '')
39
+
25
40
  const emit = defineEmits<{
26
41
  (e: 'update:modelValue', value: string): void
27
42
  }>()
@@ -30,6 +45,12 @@ const slots = useSlots()
30
45
 
31
46
  const hasLeftIcon = computed(() => !!slots['left-icon'])
32
47
  const hasRightIcon = computed(() => !!slots['right-icon'])
48
+ const hasPrefix = computed(() => !!slots.prefix)
49
+ const hasSuffix = computed(() => !!slots.suffix)
50
+
51
+ const addonHeightClass = computed(() => {
52
+ return sizeClasses[props.size].split(' ').find((c: string) => c.startsWith('h-')) || 'h-10'
53
+ })
33
54
 
34
55
  const handleInput = (event: Event) => {
35
56
  const target = event.target as HTMLInputElement
@@ -39,47 +60,78 @@ const handleInput = (event: Event) => {
39
60
 
40
61
  <template>
41
62
  <div class="relative flex items-center w-full">
42
- <!-- Left Icon -->
63
+ <!-- Prefix addon -->
43
64
  <div
44
- v-if="hasLeftIcon"
45
- class="absolute left-3 flex items-center justify-center text-muted-foreground pointer-events-none"
65
+ v-if="hasPrefix"
66
+ :class="cn(
67
+ 'inline-flex items-center px-3 text-sm text-muted-foreground bg-muted border border-input border-r-0',
68
+ props.variant === 'outline' ? 'rounded-l-lg' : '',
69
+ addonHeightClass
70
+ )"
46
71
  >
47
- <slot name="left-icon" />
72
+ <slot name="prefix" />
48
73
  </div>
49
74
 
50
- <input
51
- :type="type"
52
- :value="modelValue"
53
- :placeholder="placeholder"
54
- :class="
55
- cn(
56
- 'flex w-full rounded-lg border border-input',
57
- 'bg-background',
58
- sizeClasses[props.size],
59
- 'ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium',
60
- 'placeholder:text-muted-foreground',
61
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:border-primary',
62
- 'disabled:cursor-not-allowed disabled:opacity-50',
63
- 'shadow-depth-1 focus-visible:shadow-depth-2 transition-all duration-150 ease-out',
64
- // Padding adjustments for icons
65
- hasLeftIcon && 'pl-10',
66
- hasRightIcon && 'pr-10',
67
- !hasLeftIcon && 'px-3',
68
- !hasRightIcon && !hasLeftIcon && 'py-2',
69
- props.class
70
- )
71
- "
72
- :disabled="disabled"
73
- @input="handleInput"
74
- />
75
+ <!-- Input wrapper -->
76
+ <div class="relative flex items-center w-full">
77
+ <!-- Left Icon -->
78
+ <div
79
+ v-if="hasLeftIcon"
80
+ class="absolute left-3 flex items-center justify-center text-muted-foreground pointer-events-none"
81
+ >
82
+ <slot name="left-icon" />
83
+ </div>
84
+
85
+ <input
86
+ :type="type"
87
+ :value="modelValue"
88
+ :placeholder="placeholder"
89
+ :class="
90
+ cn(
91
+ 'flex w-full',
92
+ variantClasses[props.variant],
93
+ sizeClasses[props.size],
94
+ 'ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium',
95
+ 'placeholder:text-muted-foreground',
96
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:border-primary',
97
+ 'disabled:cursor-not-allowed disabled:opacity-50',
98
+ 'transition-all duration-150 ease-out',
99
+ // Padding adjustments for icons
100
+ hasLeftIcon && 'pl-10',
101
+ hasRightIcon && 'pr-10',
102
+ !hasLeftIcon && props.variant !== 'underline' && '',
103
+ !hasRightIcon && !hasLeftIcon && 'py-2',
104
+ // Addon border radius adjustments
105
+ hasPrefix && 'rounded-l-none border-l-0',
106
+ hasSuffix && 'rounded-r-none border-r-0',
107
+ radiusClass,
108
+ props.class
109
+ )
110
+ "
111
+ :disabled="disabled"
112
+ @input="handleInput"
113
+ />
114
+
115
+ <!-- Right Icon -->
116
+ <div
117
+ v-if="hasRightIcon"
118
+ class="absolute right-3 flex items-center justify-center text-muted-foreground"
119
+ :class="{ 'pointer-events-none': !$slots['right-icon-clickable'] }"
120
+ >
121
+ <slot name="right-icon" />
122
+ </div>
123
+ </div>
75
124
 
76
- <!-- Right Icon -->
125
+ <!-- Suffix addon -->
77
126
  <div
78
- v-if="hasRightIcon"
79
- class="absolute right-3 flex items-center justify-center text-muted-foreground"
80
- :class="{ 'pointer-events-none': !$slots['right-icon-clickable'] }"
127
+ v-if="hasSuffix"
128
+ :class="cn(
129
+ 'inline-flex items-center px-3 text-sm text-muted-foreground bg-muted border border-input border-l-0',
130
+ props.variant === 'outline' ? 'rounded-r-lg' : '',
131
+ addonHeightClass
132
+ )"
81
133
  >
82
- <slot name="right-icon" />
134
+ <slot name="suffix" />
83
135
  </div>
84
136
  </div>
85
137
  </template>
@@ -1,23 +1,33 @@
1
1
  <script setup lang="ts">
2
2
  defineOptions({ inheritAttrs: false })
3
+ import { computed } from 'vue'
3
4
  import {
4
5
  ProgressIndicator,
5
6
  ProgressRoot,
6
7
  } from 'radix-vue'
7
8
  import { cn } from '@/lib/utils'
9
+ import { type SpavnColor, colorBg } from '@/lib/types'
8
10
 
9
11
  const props = defineProps<{
10
12
  class?: string
11
13
  modelValue?: number
12
- size?: 'default' | 'sm' | 'lg'
14
+ size?: 'xs' | 'sm' | 'default' | 'lg' | 'xl'
15
+ color?: SpavnColor
13
16
  ariaLabel?: string
14
17
  }>()
15
18
 
16
- const sizeClasses = {
19
+ const sizeClasses: Record<string, string> = {
20
+ xs: 'h-1',
17
21
  sm: 'h-1',
18
22
  default: 'h-1.5',
19
23
  lg: 'h-2',
24
+ xl: 'h-3',
20
25
  }
26
+
27
+ const indicatorColor = computed(() => {
28
+ if (!props.color || props.color === 'default') return 'bg-primary'
29
+ return colorBg[props.color as Exclude<SpavnColor, 'default'>]
30
+ })
21
31
  </script>
22
32
 
23
33
  <template>
@@ -34,7 +44,7 @@ const sizeClasses = {
34
44
  "
35
45
  >
36
46
  <ProgressIndicator
37
- :class="cn('h-full w-full flex-1 bg-primary rounded-full transition-all duration-300 ease-out')"
47
+ :class="cn('h-full w-full flex-1 rounded-full transition-all duration-300 ease-out', indicatorColor)"
38
48
  :style="{ transform: `translateX(-${100 - (props.modelValue || 0)}%)` }"
39
49
  />
40
50
  </ProgressRoot>
@@ -4,9 +4,36 @@ import {
4
4
  RadioGroupIndicator,
5
5
  RadioGroupItem,
6
6
  } from 'radix-vue'
7
+ import { computed } from 'vue'
7
8
  import { cn } from '@/lib/utils'
9
+ import { type SpavnColor, colorBorderText } from '@/lib/types'
8
10
 
9
- const props = defineProps<{ class?: string }>()
11
+ interface Props {
12
+ class?: string
13
+ size?: 'sm' | 'default' | 'lg'
14
+ color?: SpavnColor
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ size: 'default',
19
+ })
20
+
21
+ const sizeClasses = {
22
+ sm: 'h-3.5 w-3.5',
23
+ default: 'h-4 w-4',
24
+ lg: 'h-5 w-5',
25
+ }
26
+
27
+ const dotSizeClasses = {
28
+ sm: 'h-2 w-2',
29
+ default: 'h-2.5 w-2.5',
30
+ lg: 'h-3 w-3',
31
+ }
32
+
33
+ const colorCls = computed(() => {
34
+ const c = props.color && props.color !== 'default' ? props.color as Exclude<SpavnColor, 'default'> : 'primary'
35
+ return colorBorderText[c]
36
+ })
10
37
  </script>
11
38
 
12
39
  <template>
@@ -14,7 +41,9 @@ const props = defineProps<{ class?: string }>()
14
41
  v-bind="$attrs"
15
42
  :class="
16
43
  cn(
17
- 'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 shadow-depth-1',
44
+ 'aspect-square rounded-full border ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 shadow-depth-1',
45
+ sizeClasses[props.size],
46
+ colorCls,
18
47
  props.class
19
48
  )
20
49
  "
@@ -26,7 +55,7 @@ const props = defineProps<{ class?: string }>()
26
55
  height="24"
27
56
  viewBox="0 0 24 24"
28
57
  fill="currentColor"
29
- class="h-2.5 w-2.5"
58
+ :class="dotSizeClasses[props.size]"
30
59
  >
31
60
  <circle cx="12" cy="12" r="6" />
32
61
  </svg>
@@ -1,10 +1,45 @@
1
1
  <script setup lang="ts">
2
2
  defineOptions({ inheritAttrs: false })
3
+ import { computed } from 'vue'
3
4
  import { Separator } from 'radix-vue'
4
5
  import { cn } from '@/lib/utils'
6
+ import { type SpavnColor, colorBg, colorBorder } from '@/lib/types'
5
7
 
6
- const props = withDefaults(defineProps<{ class?: string; orientation?: 'horizontal' | 'vertical' }>(), {
8
+ interface Props {
9
+ class?: string
10
+ orientation?: 'horizontal' | 'vertical'
11
+ variant?: 'solid' | 'dashed' | 'dotted'
12
+ color?: SpavnColor
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
7
16
  orientation: 'horizontal',
17
+ variant: 'solid',
18
+ })
19
+
20
+ const separatorClasses = computed(() => {
21
+ const isHorizontal = props.orientation === 'horizontal'
22
+ const hasColor = props.color && props.color !== 'default'
23
+ const c = hasColor ? props.color as Exclude<SpavnColor, 'default'> : null
24
+ const colorBgClass = c ? colorBg[c] : 'bg-border'
25
+ const colorBorderClass = c ? colorBorder[c] : 'border-border'
26
+
27
+ if (props.variant === 'solid') {
28
+ return isHorizontal
29
+ ? `h-px w-full ${colorBgClass}`
30
+ : `h-full w-px ${colorBgClass}`
31
+ }
32
+
33
+ if (props.variant === 'dashed') {
34
+ return isHorizontal
35
+ ? `w-full border-b border-dashed ${colorBorderClass}`
36
+ : `h-full border-l border-dashed ${colorBorderClass}`
37
+ }
38
+
39
+ // dotted
40
+ return isHorizontal
41
+ ? `w-full border-b border-dotted ${colorBorderClass}`
42
+ : `h-full border-l border-dotted ${colorBorderClass}`
8
43
  })
9
44
  </script>
10
45
 
@@ -12,12 +47,6 @@ const props = withDefaults(defineProps<{ class?: string; orientation?: 'horizont
12
47
  <Separator
13
48
  v-bind="$attrs"
14
49
  :orientation="props.orientation"
15
- :class="
16
- cn(
17
- 'shrink-0 bg-border',
18
- props.orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
19
- props.class
20
- )
21
- "
50
+ :class="cn('shrink-0', separatorClasses, props.class)"
22
51
  />
23
52
  </template>
@@ -1,9 +1,34 @@
1
1
  <script setup lang="ts">
2
2
  import { cn } from '@/lib/utils'
3
+ import { type SpavnRadius, radiusMap } from '@/lib/types'
3
4
 
4
- const props = defineProps<{ class?: string }>()
5
+ interface Props {
6
+ class?: string
7
+ variant?: 'pulse' | 'wave' | 'none'
8
+ radius?: SpavnRadius
9
+ }
10
+
11
+ const props = withDefaults(defineProps<Props>(), {
12
+ variant: 'pulse',
13
+ })
14
+
15
+ const animationClasses = {
16
+ pulse: 'animate-pulse',
17
+ wave: 'animate-pulse',
18
+ none: '',
19
+ }
20
+
21
+ const radiusClass = props.radius ? radiusMap[props.radius] : 'rounded-md'
5
22
  </script>
6
23
 
7
24
  <template>
8
- <div :class="cn('animate-pulse rounded-md bg-muted', props.class)" />
25
+ <div
26
+ :class="cn(
27
+ 'bg-muted',
28
+ animationClasses[variant],
29
+ variant === 'wave' && 'bg-gradient-to-r from-muted via-muted/50 to-muted bg-[length:200%_100%]',
30
+ radiusClass,
31
+ props.class
32
+ )"
33
+ />
9
34
  </template>