@karbonjs/ui-svelte 0.2.5 → 0.3.0

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 (40) hide show
  1. package/package.json +3 -2
  2. package/src/accordion/Accordion.svelte +198 -23
  3. package/src/alert/AlertMessage.svelte +114 -20
  4. package/src/avatar/Avatar.svelte +16 -2
  5. package/src/badge/Badge.svelte +99 -10
  6. package/src/breadcrumb/Breadcrumb.svelte +124 -13
  7. package/src/button/Button.svelte +106 -21
  8. package/src/button/ButtonBrand.svelte +229 -0
  9. package/src/carousel/Carousel.svelte +161 -28
  10. package/src/code/CodeBlock.svelte +323 -0
  11. package/src/data/DataTable.svelte +319 -8
  12. package/src/data/Pagination.svelte +168 -28
  13. package/src/divider/Divider.svelte +91 -10
  14. package/src/dropdown/Dropdown.svelte +171 -27
  15. package/src/editor/RichTextEditor.svelte +861 -107
  16. package/src/form/Checkbox.svelte +110 -18
  17. package/src/form/ColorPicker.svelte +28 -16
  18. package/src/form/DatePicker.svelte +20 -10
  19. package/src/form/{FormInput.svelte → Input.svelte} +41 -14
  20. package/src/form/Radio.svelte +86 -18
  21. package/src/form/Select.svelte +246 -33
  22. package/src/form/Slider.svelte +22 -7
  23. package/src/form/Textarea.svelte +53 -10
  24. package/src/form/Toggle.svelte +72 -18
  25. package/src/image/Image.svelte +6 -4
  26. package/src/image/ImageCompare.svelte +182 -0
  27. package/src/image/ImgZoom.svelte +131 -49
  28. package/src/index.ts +7 -1
  29. package/src/kbd/Kbd.svelte +4 -3
  30. package/src/layout/Card.svelte +12 -6
  31. package/src/layout/EmptyState.svelte +75 -8
  32. package/src/layout/PageHeader.svelte +111 -11
  33. package/src/overlay/Dialog.svelte +147 -67
  34. package/src/overlay/ImgBox.svelte +125 -21
  35. package/src/overlay/Modal.svelte +110 -28
  36. package/src/overlay/Toast.svelte +152 -55
  37. package/src/progress/Progress.svelte +137 -26
  38. package/src/skeleton/Skeleton.svelte +6 -4
  39. package/src/tabs/Tabs.svelte +133 -22
  40. package/src/tooltip/Tooltip.svelte +110 -20
@@ -1,91 +1,188 @@
1
1
  <script lang="ts">
2
- import type { ToastVariant, ToastPosition } from '@karbonjs/ui-core'
2
+ import type { Snippet } from 'svelte'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
3
4
 
4
5
  interface Props {
5
- message: string
6
- variant?: ToastVariant
6
+ type?: 'success' | 'error' | 'warning' | 'info'
7
+ variant?: 'default' | 'filled' | 'bordered'
8
+ color?: ButtonColor
9
+ title?: string
10
+ message?: string
7
11
  duration?: number
8
12
  dismissible?: boolean
9
- position?: ToastPosition
10
- visible?: boolean
13
+ showProgress?: boolean
14
+ position?: 'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center'
15
+ icon?: Snippet | false
16
+ action?: Snippet
11
17
  class?: string
12
- ondismiss?: () => void
18
+ classes?: { root?: string, icon?: string, close?: string, progress?: string }
19
+ onclose?: () => void
20
+ children?: Snippet
13
21
  }
14
22
 
15
23
  let {
16
- message,
17
- variant = 'info',
18
- duration = 4000,
24
+ type = 'info',
25
+ variant = 'default',
26
+ color,
27
+ title = '',
28
+ message = '',
29
+ duration = 5000,
19
30
  dismissible = true,
31
+ showProgress = true,
20
32
  position = 'top-right',
21
- visible = $bindable(true),
33
+ icon,
34
+ action,
22
35
  class: className = '',
23
- ondismiss
36
+ classes = {},
37
+ onclose,
38
+ children
24
39
  }: Props = $props()
25
40
 
26
- const variantClasses: Record<string, string> = {
27
- success: 'border-emerald-500/20 text-emerald-400',
28
- error: 'border-red-500/20 text-red-400',
29
- warning: 'border-amber-500/20 text-amber-300',
30
- info: 'border-blue-500/20 text-blue-400'
41
+ let visible = $state(false)
42
+ let alive = $state(true)
43
+ let progress = $state(100)
44
+ let paused = $state(false)
45
+
46
+ const typeColors: Record<string, { bg: string, text: string, border: string }> = {
47
+ success: { bg: 'var(--karbon-emerald-500)', text: 'var(--karbon-emerald-400)', border: 'var(--karbon-emerald-500)' },
48
+ error: { bg: 'var(--karbon-red-500)', text: 'var(--karbon-red-400)', border: 'var(--karbon-red-500)' },
49
+ warning: { bg: 'var(--karbon-amber-500)', text: 'var(--karbon-amber-400)', border: 'var(--karbon-amber-500)' },
50
+ info: { bg: 'var(--karbon-blue-500)', text: 'var(--karbon-blue-400)', border: 'var(--karbon-blue-500)' },
31
51
  }
32
52
 
33
- const positionClasses: Record<string, string> = {
34
- 'top-right': 'top-4 right-4',
35
- 'top-left': 'top-4 left-4',
36
- 'top-center': 'top-4 left-1/2 -translate-x-1/2',
37
- 'bottom-right': 'bottom-4 right-4',
38
- 'bottom-left': 'bottom-4 left-4',
39
- 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2'
53
+ const tc = $derived(color
54
+ ? { bg: `var(--karbon-${color}-500)`, text: `var(--karbon-${color}-400)`, border: `var(--karbon-${color}-500)` }
55
+ : typeColors[type]
56
+ )
57
+
58
+ const typeIcons: Record<string, string> = {
59
+ success: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>',
60
+ error: '<circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/>',
61
+ warning: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
62
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
40
63
  }
41
64
 
42
- function dismiss() {
43
- visible = false
44
- ondismiss?.()
65
+ const posStyles: Record<string, string> = {
66
+ 'top-right': 'top:1rem;right:1rem;',
67
+ 'top-left': 'top:1rem;left:1rem;',
68
+ 'top-center': 'top:1rem;left:50%;transform:translateX(-50%);',
69
+ 'bottom-right': 'bottom:1rem;right:1rem;',
70
+ 'bottom-left': 'bottom:1rem;left:1rem;',
71
+ 'bottom-center': 'bottom:1rem;left:50%;transform:translateX(-50%);',
45
72
  }
46
73
 
74
+ const isTop = $derived(position.startsWith('top'))
75
+ const slideFrom = $derived(isTop ? '-20px' : '20px')
76
+
77
+ const toastStyle = $derived.by(() => {
78
+ switch (variant) {
79
+ case 'filled':
80
+ return `background:${tc.bg};color:white;border:none;`
81
+ case 'bordered':
82
+ return `background:var(--karbon-bg-card);color:var(--karbon-text);border-left:3px solid ${tc.border};border-top:1px solid var(--karbon-border);border-right:1px solid var(--karbon-border);border-bottom:1px solid var(--karbon-border);`
83
+ default:
84
+ return `background:var(--karbon-bg-card);color:var(--karbon-text);border:1px solid var(--karbon-border);`
85
+ }
86
+ })
87
+
88
+ const iconColor = $derived(variant === 'filled' ? 'white' : tc.text)
89
+
90
+ // Animation + countdown
47
91
  $effect(() => {
48
- if (visible && duration > 0) {
49
- const timer = setTimeout(dismiss, duration)
50
- return () => clearTimeout(timer)
92
+ if (alive) {
93
+ requestAnimationFrame(() => { visible = true })
94
+
95
+ if (duration > 0) {
96
+ const interval = 30
97
+ const step = (100 / duration) * interval
98
+ const timer = setInterval(() => {
99
+ if (!paused) {
100
+ progress -= step
101
+ if (progress <= 0) {
102
+ clearInterval(timer)
103
+ dismiss()
104
+ }
105
+ }
106
+ }, interval)
107
+ return () => clearInterval(timer)
108
+ }
51
109
  }
52
110
  })
111
+
112
+ function dismiss() {
113
+ visible = false
114
+ setTimeout(() => {
115
+ alive = false
116
+ onclose?.()
117
+ }, 200)
118
+ }
53
119
  </script>
54
120
 
55
- {#if visible}
121
+ {#if alive}
56
122
  <div
57
- class="fixed z-[60] {positionClasses[position]} w-full max-w-sm"
123
+ class="fixed z-[60] w-full max-w-sm pointer-events-auto"
124
+ style={posStyles[position]}
58
125
  role="alert"
59
126
  aria-live="assertive"
127
+ onmouseenter={() => paused = true}
128
+ onmouseleave={() => paused = false}
60
129
  >
61
130
  <div
62
- class="
63
- flex items-start gap-3 px-4 py-3 rounded-xl border shadow-lg
64
- bg-[var(--karbon-bg-card,#fff)]
65
- {variantClasses[variant]}
66
- {className}
67
- "
131
+ class="rounded-xl shadow-xl overflow-hidden {classes?.root ?? className}"
132
+ style="{toastStyle}
133
+ opacity:{visible ? 1 : 0};
134
+ transform:translateY({visible ? '0' : slideFrom});
135
+ transition:transform 0.25s cubic-bezier(0.16,1,0.3,1),opacity 0.2s ease;"
68
136
  >
69
- {#if variant === 'success'}
70
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>
71
- {:else if variant === 'error'}
72
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>
73
- {:else if variant === 'warning'}
74
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
75
- {:else}
76
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
77
- {/if}
137
+ <div class="flex items-start gap-3 px-4 py-3">
138
+ <!-- Icon -->
139
+ {#if icon !== false}
140
+ <div class="shrink-0 mt-0.5 {classes?.icon ?? ''}">
141
+ {#if icon}
142
+ {@render icon()}
143
+ {:else}
144
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={iconColor} stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{@html typeIcons[type] || typeIcons.info}</svg>
145
+ {/if}
146
+ </div>
147
+ {/if}
148
+
149
+ <!-- Content -->
150
+ <div class="flex-1 min-w-0">
151
+ {#if title}
152
+ <p class="text-sm font-semibold {message || children ? 'mb-0.5' : ''}"
153
+ style="color:white;"
154
+ >{title}</p>
155
+ {/if}
156
+ {#if children}
157
+ <div class="text-[13px]" style="color:rgba(255,255,255,0.75);">{@render children()}</div>
158
+ {:else if message}
159
+ <p class="text-[13px]" style="color:rgba(255,255,255,0.75);">{message}</p>
160
+ {/if}
161
+ {#if action}
162
+ <div class="mt-2">{@render action()}</div>
163
+ {/if}
164
+ </div>
78
165
 
79
- <span class="flex-1 text-sm font-medium text-[var(--karbon-text,#1a1635)]">{message}</span>
166
+ <!-- Close -->
167
+ {#if dismissible}
168
+ <button
169
+ onclick={dismiss}
170
+ aria-label="Fermer"
171
+ class="shrink-0 rounded-md p-0.5 transition-opacity opacity-50 hover:opacity-100 cursor-pointer {classes?.close ?? ''}"
172
+ >
173
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
174
+ </button>
175
+ {/if}
176
+ </div>
80
177
 
81
- {#if dismissible}
82
- <button
83
- onclick={dismiss}
84
- aria-label="Fermer"
85
- class="shrink-0 rounded-lg p-0.5 text-[var(--karbon-text-4,#b5b2cc)] transition-colors duration-150 hover:text-[var(--karbon-text-2,#5a567e)] cursor-pointer"
86
- >
87
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
88
- </button>
178
+ <!-- Progress bar -->
179
+ {#if showProgress && duration > 0}
180
+ <div class="h-[2px] w-full {classes?.progress ?? ''}" style="background:color-mix(in srgb,{tc.bg} 15%,transparent);">
181
+ <div
182
+ class="h-full transition-none"
183
+ style="width:{progress}%;background:{variant === 'filled' ? 'rgba(255,255,255,0.4)' : tc.bg};"
184
+ ></div>
185
+ </div>
89
186
  {/if}
90
187
  </div>
91
188
  </div>
@@ -1,50 +1,161 @@
1
1
  <script lang="ts">
2
- import type { ProgressVariant, ProgressSize } from '@karbonjs/ui-core'
2
+ import type { Snippet } from 'svelte'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
3
4
 
4
5
  interface Props {
5
6
  value: number
6
7
  max?: number
7
- variant?: ProgressVariant
8
- size?: ProgressSize
9
- showLabel?: boolean
8
+ color?: ButtonColor
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
10
+ variant?: 'default' | 'striped' | 'gradient' | 'glow'
11
+ shape?: 'rounded' | 'square' | 'pill'
12
+ label?: boolean | 'inside' | 'outside' | 'top'
13
+ labelFormat?: (value: number, max: number) => string
14
+ indeterminate?: boolean
15
+ animated?: boolean
16
+ segments?: { value: number, color?: ButtonColor, label?: string }[]
10
17
  class?: string
18
+ classes?: { root?: string, track?: string, bar?: string, label?: string }
19
+ children?: Snippet
11
20
  }
12
21
 
13
22
  let {
14
- value,
23
+ value = 0,
15
24
  max = 100,
16
- variant = 'primary',
25
+ color,
17
26
  size = 'md',
18
- showLabel = false,
19
- class: className = ''
27
+ variant = 'default',
28
+ shape = 'rounded',
29
+ label = false,
30
+ labelFormat,
31
+ indeterminate = false,
32
+ animated = false,
33
+ segments,
34
+ class: className = '',
35
+ classes = {},
36
+ children
20
37
  }: Props = $props()
21
38
 
22
39
  const percent = $derived(Math.min(Math.max((value / max) * 100, 0), 100))
40
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
41
+ const accentLight = $derived(color ? `var(--karbon-${color}-400)` : 'var(--karbon-primary)')
23
42
 
24
- const variantClasses: Record<string, string> = {
25
- primary: 'bg-[var(--karbon-primary)]',
26
- success: 'bg-emerald-500',
27
- warning: 'bg-amber-500',
28
- danger: 'bg-red-500'
43
+ const sizeMap = {
44
+ xs: { track: 2, fontSize: '9px', showInside: false },
45
+ sm: { track: 4, fontSize: '10px', showInside: false },
46
+ md: { track: 8, fontSize: '11px', showInside: false },
47
+ lg: { track: 12, fontSize: '11px', showInside: true },
48
+ xl: { track: 20, fontSize: '12px', showInside: true },
29
49
  }
50
+ const s = $derived(sizeMap[size])
30
51
 
31
- const sizeClasses: Record<string, string> = {
32
- sm: 'h-1',
33
- md: 'h-2',
34
- lg: 'h-3'
52
+ const shapeMap = { rounded: '9999px', square: '0', pill: '9999px' }
53
+ const radius = $derived(shapeMap[shape])
54
+
55
+ function formatLabel(v: number, m: number): string {
56
+ if (labelFormat) return labelFormat(v, m)
57
+ return `${Math.round((v / m) * 100)}%`
58
+ }
59
+
60
+ function barStyle(clr?: string): string {
61
+ const bg = clr ? `var(--karbon-${clr}-500)` : accent
62
+ const bgLight = clr ? `var(--karbon-${clr}-400)` : accentLight
63
+ switch (variant) {
64
+ case 'striped':
65
+ return `background:repeating-linear-gradient(45deg,${bg},${bg} 10px,color-mix(in srgb,${bg} 70%,transparent) 10px,color-mix(in srgb,${bg} 70%,transparent) 20px);`
66
+ case 'gradient':
67
+ return `background:linear-gradient(90deg,color-mix(in srgb,${bg} 60%,transparent),${bg},${bgLight});`
68
+ case 'glow':
69
+ return `background:${bg};box-shadow:0 0 8px color-mix(in srgb,${bg} 50%,transparent),0 0 20px color-mix(in srgb,${bg} 20%,transparent);`
70
+ default:
71
+ return `background:${bg};`
72
+ }
35
73
  }
74
+
75
+ const showLabel = $derived(label === true || label === 'outside' || label === 'top' || label === 'inside')
76
+ const labelPos = $derived(label === 'inside' ? 'inside' : label === 'top' ? 'top' : 'outside')
36
77
  </script>
37
78
 
38
- <div class={className}>
39
- {#if showLabel}
40
- <div class="flex items-center justify-between mb-1">
41
- <span class="text-xs font-medium text-[var(--karbon-text-2,#5a567e)]">{Math.round(percent)}%</span>
79
+ <div class="{classes?.root ?? className}">
80
+ <!-- Top label -->
81
+ {#if showLabel && labelPos === 'top'}
82
+ <div class="flex items-center justify-between mb-1.5">
83
+ {#if children}
84
+ <span class="text-xs font-medium {classes?.label ?? ''}" style="color:var(--karbon-text-2);">{@render children()}</span>
85
+ {:else}
86
+ <span></span>
87
+ {/if}
88
+ <span class="text-xs font-semibold {classes?.label ?? ''}" style="color:{accent};">{formatLabel(value, max)}</span>
42
89
  </div>
43
90
  {/if}
44
- <div class="w-full rounded-full bg-[var(--karbon-border,rgba(0,0,0,0.07))] overflow-hidden {sizeClasses[size]}" role="progressbar" aria-valuenow={value} aria-valuemax={max}>
45
- <div
46
- class="h-full rounded-full transition-all duration-300 ease-out {variantClasses[variant]}"
47
- style="width: {percent}%"
48
- ></div>
91
+
92
+ <!-- Track -->
93
+ <div
94
+ class="w-full overflow-hidden relative {classes?.track ?? ''}"
95
+ style="height:{s.track}px;border-radius:{radius};background:var(--karbon-border,rgba(255,255,255,0.08));"
96
+ role="progressbar"
97
+ aria-valuenow={indeterminate ? undefined : value}
98
+ aria-valuemax={max}
99
+ >
100
+ {#if indeterminate}
101
+ <!-- Indeterminate animation -->
102
+ <div
103
+ class="absolute h-full {classes?.bar ?? ''}"
104
+ style="{barStyle()};border-radius:{radius};animation:karbon-progress-indeterminate 1.5s ease-in-out infinite;width:40%;"
105
+ ></div>
106
+ {:else if segments && segments.length > 0}
107
+ <!-- Multi-segments -->
108
+ <div class="flex h-full">
109
+ {#each segments as seg}
110
+ {@const segPercent = Math.min(Math.max((seg.value / max) * 100, 0), 100)}
111
+ <div
112
+ class="h-full transition-all duration-500 ease-out first:rounded-l-inherit last:rounded-r-inherit {classes?.bar ?? ''}"
113
+ style="width:{segPercent}%;{barStyle(seg.color)}"
114
+ title={seg.label || `${Math.round(segPercent)}%`}
115
+ ></div>
116
+ {/each}
117
+ </div>
118
+ {:else}
119
+ <!-- Single bar -->
120
+ <div
121
+ class="h-full transition-all duration-500 ease-out {classes?.bar ?? ''} {animated ? 'karbon-progress-animated' : ''}"
122
+ style="width:{percent}%;border-radius:{radius};{barStyle()}"
123
+ ></div>
124
+
125
+ <!-- Inside label -->
126
+ {#if showLabel && labelPos === 'inside' && s.showInside && percent > 10}
127
+ <span
128
+ class="absolute right-2 top-1/2 -translate-y-1/2 font-bold {classes?.label ?? ''}"
129
+ style="font-size:{s.fontSize};color:white;text-shadow:0 1px 2px rgba(0,0,0,0.3);"
130
+ >{formatLabel(value, max)}</span>
131
+ {/if}
132
+ {/if}
49
133
  </div>
134
+
135
+ <!-- Outside label -->
136
+ {#if showLabel && labelPos === 'outside'}
137
+ <div class="flex items-center justify-between mt-1.5">
138
+ {#if children}
139
+ <span class="text-xs {classes?.label ?? ''}" style="color:var(--karbon-text-3);">{@render children()}</span>
140
+ {:else}
141
+ <span></span>
142
+ {/if}
143
+ <span class="text-xs font-semibold {classes?.label ?? ''}" style="color:{accent};">{formatLabel(value, max)}</span>
144
+ </div>
145
+ {/if}
50
146
  </div>
147
+
148
+ <style>
149
+ @keyframes karbon-progress-indeterminate {
150
+ 0% { left: -40%; }
151
+ 100% { left: 100%; }
152
+ }
153
+ .karbon-progress-animated {
154
+ background-size: 30px 30px !important;
155
+ animation: karbon-progress-stripe 1s linear infinite;
156
+ }
157
+ @keyframes karbon-progress-stripe {
158
+ 0% { background-position: 30px 0; }
159
+ 100% { background-position: 0 0; }
160
+ }
161
+ </style>
@@ -7,6 +7,7 @@
7
7
  height?: string
8
8
  lines?: number
9
9
  class?: string
10
+ classes?: { root?: string }
10
11
  }
11
12
 
12
13
  let {
@@ -14,7 +15,8 @@
14
15
  width = '100%',
15
16
  height,
16
17
  lines = 1,
17
- class: className = ''
18
+ class: className = '',
19
+ classes = {}
18
20
  }: Props = $props()
19
21
 
20
22
  const baseClass = 'animate-pulse bg-[var(--karbon-border,rgba(0,0,0,0.07))]'
@@ -29,7 +31,7 @@
29
31
  </script>
30
32
 
31
33
  {#if variant === 'text' && lines > 1}
32
- <div class="space-y-2 {className}">
34
+ <div class="space-y-2 {classes?.root ?? className}">
33
35
  {#each Array(lines) as _, i}
34
36
  <div
35
37
  class="{baseClass} {v.rounded}"
@@ -39,12 +41,12 @@
39
41
  </div>
40
42
  {:else if variant === 'circle'}
41
43
  <div
42
- class="{baseClass} {v.rounded} aspect-square {className}"
44
+ class="{baseClass} {v.rounded} aspect-square {classes?.root ?? className}"
43
45
  style="width: {width === '100%' ? height ?? v.h : width}; height: {height ?? v.h}"
44
46
  ></div>
45
47
  {:else}
46
48
  <div
47
- class="{baseClass} {v.rounded} {className}"
49
+ class="{baseClass} {v.rounded} {classes?.root ?? className}"
48
50
  style="width: {width}; height: {height ?? v.h}"
49
51
  ></div>
50
52
  {/if}