@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,27 +1,127 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
+
2
5
  interface Props {
3
6
  title: string
4
7
  description?: string
5
- icon?: any
6
- iconColor?: string
8
+ icon?: Snippet
9
+ color?: ButtonColor
10
+ size?: 'sm' | 'md' | 'lg'
11
+ variant?: 'default' | 'bordered' | 'filled' | 'clean'
12
+ backHref?: string
13
+ backLabel?: string
14
+ badge?: string
15
+ breadcrumbs?: { label: string, href?: string }[]
16
+ actions?: Snippet
17
+ class?: string
18
+ classes?: { root?: string, title?: string, description?: string, icon?: string, actions?: string, breadcrumb?: string }
7
19
  }
8
20
 
9
21
  let {
10
22
  title,
11
23
  description = '',
12
- icon: Icon,
13
- iconColor = 'var(--karbon-primary, #cc1a1a)'
24
+ icon,
25
+ color,
26
+ size = 'md',
27
+ variant = 'default',
28
+ backHref,
29
+ backLabel = 'Retour',
30
+ badge,
31
+ breadcrumbs,
32
+ actions,
33
+ class: className = '',
34
+ classes = {}
14
35
  }: Props = $props()
36
+
37
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
38
+ const accentLight = $derived(color ? `var(--karbon-${color}-400)` : 'var(--karbon-primary)')
39
+
40
+ const sizeMap = {
41
+ sm: { title: 'text-lg', desc: 'text-xs', iconBox: 32, iconSize: 16, pad: 'pb-3', gap: 'gap-2.5' },
42
+ md: { title: 'text-xl', desc: 'text-sm', iconBox: 40, iconSize: 20, pad: 'pb-4', gap: 'gap-3' },
43
+ lg: { title: 'text-2xl', desc: 'text-base', iconBox: 48, iconSize: 24, pad: 'pb-5', gap: 'gap-4' },
44
+ }
45
+ const s = $derived(sizeMap[size])
46
+
47
+ function rootStyle(): string {
48
+ switch (variant) {
49
+ case 'default': return `padding-bottom:${size === 'sm' ? '0.75rem' : size === 'lg' ? '1.25rem' : '1rem'};border-bottom:1px solid var(--karbon-border);margin-bottom:0.25rem;`
50
+ case 'bordered': return `padding:${size === 'sm' ? '0.75rem' : size === 'lg' ? '1.25rem' : '1rem'};border:1px solid var(--karbon-border);border-radius:0.75rem;background:var(--karbon-bg-card);`
51
+ case 'filled': return `padding:${size === 'sm' ? '0.75rem' : size === 'lg' ? '1.5rem' : '1rem'};border-radius:0.75rem;background:color-mix(in srgb,${accent} 8%,transparent);border:1px solid color-mix(in srgb,${accent} 15%,transparent);`
52
+ case 'clean': return ''
53
+ default: return ''
54
+ }
55
+ }
15
56
  </script>
16
57
 
17
- <div class="flex items-start gap-3 pb-4 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))] mb-1">
18
- {#if Icon}
19
- <Icon class="w-5 h-5 shrink-0" style="color: {iconColor}" />
58
+ <div class="{classes?.root ?? className}" style={rootStyle()}>
59
+ <!-- Breadcrumbs -->
60
+ {#if breadcrumbs && breadcrumbs.length > 0}
61
+ <nav class="flex items-center gap-1.5 mb-2 {classes?.breadcrumb ?? ''}" aria-label="Breadcrumb">
62
+ {#each breadcrumbs as crumb, i}
63
+ {#if i > 0}
64
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--karbon-text-4);"><path d="m9 18 6-6-6-6"/></svg>
65
+ {/if}
66
+ {#if crumb.href && i < breadcrumbs.length - 1}
67
+ <a href={crumb.href} class="text-xs transition-colors" style="color:var(--karbon-text-3);"
68
+ onmouseenter={(e) => { (e.currentTarget as HTMLElement).style.color = accent }}
69
+ onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.color = 'var(--karbon-text-3)' }}
70
+ >{crumb.label}</a>
71
+ {:else}
72
+ <span class="text-xs font-medium" style="color:var(--karbon-text-2);">{crumb.label}</span>
73
+ {/if}
74
+ {/each}
75
+ </nav>
20
76
  {/if}
21
- <div>
22
- <h1 class="text-[var(--karbon-text,#1a1635)] text-[1.1rem] font-bold m-0">{title}</h1>
23
- {#if description}
24
- <p class="text-[var(--karbon-text-3,#8e8aae)] text-[0.8rem] mt-0.5">{description}</p>
77
+
78
+ <!-- Back button -->
79
+ {#if backHref}
80
+ <a
81
+ href={backHref}
82
+ class="inline-flex items-center gap-1.5 text-xs font-medium mb-2 transition-colors"
83
+ style="color:var(--karbon-text-3);"
84
+ onmouseenter={(e) => { (e.currentTarget as HTMLElement).style.color = accent }}
85
+ onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.color = 'var(--karbon-text-3)' }}
86
+ >
87
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
88
+ {backLabel}
89
+ </a>
90
+ {/if}
91
+
92
+ <!-- Main row -->
93
+ <div class="flex items-start {s.gap}">
94
+ <!-- Icon -->
95
+ {#if icon}
96
+ <div
97
+ class="shrink-0 rounded-xl flex items-center justify-center {classes?.icon ?? ''}"
98
+ style="width:{s.iconBox}px;height:{s.iconBox}px;background:color-mix(in srgb,{accent} 12%,transparent);color:{accentLight};"
99
+ >
100
+ {@render icon()}
101
+ </div>
102
+ {/if}
103
+
104
+ <!-- Title + description -->
105
+ <div class="flex-1 min-w-0">
106
+ <div class="flex items-center gap-2">
107
+ <h1 class="{s.title} font-bold {classes?.title ?? ''}" style="color:var(--karbon-text);margin:0;">{title}</h1>
108
+ {#if badge}
109
+ <span
110
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold"
111
+ style="background:color-mix(in srgb,{accent} 15%,transparent);color:{accentLight};"
112
+ >{badge}</span>
113
+ {/if}
114
+ </div>
115
+ {#if description}
116
+ <p class="{s.desc} mt-1 {classes?.description ?? ''}" style="color:var(--karbon-text-3);margin:0;">{description}</p>
117
+ {/if}
118
+ </div>
119
+
120
+ <!-- Actions -->
121
+ {#if actions}
122
+ <div class="shrink-0 flex items-center gap-2 {classes?.actions ?? ''}">
123
+ {@render actions()}
124
+ </div>
25
125
  {/if}
26
126
  </div>
27
127
  </div>
@@ -1,128 +1,208 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
- import type { DialogVariant, OverlayBackdrop } from '@karbonjs/ui-core'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
4
 
5
5
  interface Props {
6
6
  open: boolean
7
7
  title: string
8
- description?: string
9
- variant?: DialogVariant
10
- backdrop?: OverlayBackdrop
8
+ message?: string
9
+ variant?: 'info' | 'warning' | 'danger' | 'success'
10
+ backdrop?: 'blur' | 'dark' | 'transparent'
11
+ color?: ButtonColor
11
12
  confirmLabel?: string
12
13
  cancelLabel?: string
14
+ confirmInput?: string
15
+ confirmInputLabel?: string
16
+ confirmInputPlaceholder?: string
13
17
  loading?: boolean
14
18
  class?: string
19
+ classes?: { overlay?: string, content?: string }
15
20
  onconfirm: () => void
16
21
  oncancel: () => void
17
22
  icon?: Snippet
23
+ children?: Snippet
18
24
  }
19
25
 
20
26
  let {
21
27
  open = $bindable(false),
22
28
  title,
23
- description = '',
29
+ message = '',
24
30
  variant = 'info',
25
- backdrop = 'none',
31
+ backdrop = 'blur',
32
+ color,
26
33
  confirmLabel = 'Confirmer',
27
34
  cancelLabel = 'Annuler',
35
+ confirmInput,
36
+ confirmInputLabel,
37
+ confirmInputPlaceholder,
28
38
  loading = false,
29
39
  class: className = '',
40
+ classes = {},
30
41
  onconfirm,
31
42
  oncancel,
32
- icon
43
+ icon,
44
+ children
33
45
  }: Props = $props()
34
46
 
35
- const backdropClasses: Record<string, string> = {
36
- blur: 'bg-black/30 backdrop-blur-sm',
37
- dark: 'bg-black/50',
38
- transparent: 'bg-transparent',
39
- none: 'hidden'
47
+ let visible = $state(false)
48
+ let inputValue = $state('')
49
+
50
+ $effect(() => {
51
+ if (open) {
52
+ inputValue = ''
53
+ requestAnimationFrame(() => { visible = true })
54
+ document.body.style.overflow = 'hidden'
55
+ } else {
56
+ visible = false
57
+ document.body.style.overflow = ''
58
+ }
59
+ return () => { document.body.style.overflow = '' }
60
+ })
61
+
62
+ const confirmInputLabelText = $derived(confirmInputLabel ?? `Tapez "${confirmInput}" pour confirmer`)
63
+ const isConfirmDisabled = $derived(loading || (confirmInput ? inputValue !== confirmInput : false))
64
+
65
+ // Variant colors
66
+ const variantColors: Record<string, { bg: string, text: string, btn: string, btnHover: string }> = {
67
+ info: { bg: 'var(--karbon-blue-500)', text: 'var(--karbon-blue-400)', btn: 'var(--karbon-blue-500)', btnHover: 'var(--karbon-blue-600)' },
68
+ warning: { bg: 'var(--karbon-amber-500)', text: 'var(--karbon-amber-400)', btn: 'var(--karbon-amber-500)', btnHover: 'var(--karbon-amber-600)' },
69
+ danger: { bg: 'var(--karbon-red-500)', text: 'var(--karbon-red-400)', btn: 'var(--karbon-red-500)', btnHover: 'var(--karbon-red-600)' },
70
+ success: { bg: 'var(--karbon-emerald-500)', text: 'var(--karbon-emerald-400)', btn: 'var(--karbon-emerald-500)', btnHover: 'var(--karbon-emerald-600)' },
40
71
  }
41
72
 
42
- const variantIcon: Record<string, string> = {
43
- info: 'bg-blue-500/10 text-blue-500',
44
- warning: 'bg-amber-500/10 text-amber-500',
45
- danger: 'bg-red-500/10 text-red-500',
46
- success: 'bg-emerald-500/10 text-emerald-500'
73
+ const vc = $derived(color
74
+ ? { bg: `var(--karbon-${color}-500)`, text: `var(--karbon-${color}-400)`, btn: `var(--karbon-${color}-500)`, btnHover: `var(--karbon-${color}-600)` }
75
+ : variantColors[variant]
76
+ )
77
+
78
+ // SVG icons per variant
79
+ const variantIcons: Record<string, string> = {
80
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
81
+ 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"/>',
82
+ danger: '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
83
+ success: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>',
84
+ }
85
+
86
+ const backdropStyles: Record<string, string> = {
87
+ blur: 'background:rgba(0,0,0,0.5);backdrop-filter:blur(8px);',
88
+ dark: 'background:rgba(0,0,0,0.6);',
89
+ transparent: 'background:transparent;',
90
+ }
91
+
92
+ let btnHovered = $state(false)
93
+
94
+ function cancel() {
95
+ visible = false
96
+ setTimeout(() => oncancel(), 150)
47
97
  }
48
98
 
49
- const confirmClasses: Record<string, string> = {
50
- info: 'bg-[var(--karbon-primary)] hover:bg-[var(--karbon-primary-hover)]',
51
- warning: 'bg-amber-500 hover:bg-amber-600',
52
- danger: 'bg-red-500 hover:bg-red-600',
53
- success: 'bg-emerald-500 hover:bg-emerald-600'
99
+ function confirm() {
100
+ if (isConfirmDisabled) return
101
+ onconfirm()
54
102
  }
55
103
 
56
104
  function handleKeydown(e: KeyboardEvent) {
57
- if (e.key === 'Escape') oncancel()
105
+ if (e.key === 'Escape' && open) cancel()
106
+ if (e.key === 'Enter' && open && !isConfirmDisabled) confirm()
58
107
  }
59
108
  </script>
60
109
 
61
- <svelte:window onkeydown={open ? handleKeydown : undefined} />
110
+ <svelte:window onkeydown={handleKeydown} />
62
111
 
63
112
  {#if open}
64
- <!-- svelte-ignore a11y_no_static_element_interactions -->
65
- <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
113
+ <div
114
+ class="fixed inset-0 z-50 flex items-center justify-center p-4"
115
+ style="opacity:{visible ? 1 : 0};transition:opacity 0.15s ease;"
116
+ >
117
+ <!-- Backdrop -->
66
118
  <!-- svelte-ignore a11y_click_events_have_key_events -->
67
- {#if backdrop !== 'none'}
68
- <div class="fixed inset-0 {backdropClasses[backdrop]}" onclick={oncancel}></div>
69
- {/if}
119
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
70
120
  <div
71
- class="relative z-10 w-full max-w-md rounded-xl shadow-xl bg-[var(--karbon-bg-card,#fff)] p-6 {className}"
121
+ class="fixed inset-0 {classes?.overlay ?? ''}"
122
+ style="{backdropStyles[backdrop]}transition:opacity 0.15s ease;opacity:{visible ? 1 : 0};"
123
+ onclick={cancel}
124
+ ></div>
125
+
126
+ <!-- Content -->
127
+ <div
128
+ class="relative z-10 w-full max-w-md rounded-2xl p-6 {classes?.content ?? className}"
129
+ style="background:var(--karbon-bg-card);border:1px solid var(--karbon-border);
130
+ box-shadow:0 25px 60px -12px rgba(0,0,0,0.4);
131
+ transform:{visible ? 'scale(1)' : 'scale(0.95)'};
132
+ transition:transform 0.2s cubic-bezier(0.16,1,0.3,1);
133
+ opacity:{visible ? 1 : 0};"
72
134
  role="alertdialog"
73
135
  aria-modal="true"
74
- aria-labelledby="karbon-dialog-title"
75
- aria-describedby={description ? 'karbon-dialog-desc' : undefined}
76
136
  >
137
+ <!-- Icon + Title -->
77
138
  <div class="flex flex-col items-center text-center">
78
- {#if icon}
79
- <div class="w-12 h-12 rounded-full {variantIcon[variant]} flex items-center justify-center mb-4">
80
- {@render icon()}
81
- </div>
82
- {:else}
83
- <div class="w-12 h-12 rounded-full {variantIcon[variant]} flex items-center justify-center mb-4">
84
- {#if variant === 'danger'}
85
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
86
- {:else if variant === 'warning'}
87
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
88
- {:else if variant === 'success'}
89
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>
90
- {:else}
91
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
92
- {/if}
93
- </div>
94
- {/if}
139
+ <div
140
+ class="w-14 h-14 rounded-2xl flex items-center justify-center mb-4"
141
+ style="background:color-mix(in srgb,{vc.bg} 12%,transparent);"
142
+ >
143
+ {#if icon}
144
+ <span style="color:{vc.text};">{@render icon()}</span>
145
+ {:else}
146
+ <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke={vc.text} stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{@html variantIcons[variant] || variantIcons.info}</svg>
147
+ {/if}
148
+ </div>
95
149
 
96
- <h3 id="karbon-dialog-title" class="text-lg font-semibold text-[var(--karbon-text,#1a1635)]">
97
- {title}
98
- </h3>
150
+ <h3 class="text-lg font-semibold" style="color:var(--karbon-text);">{title}</h3>
99
151
 
100
- {#if description}
101
- <p id="karbon-dialog-desc" class="mt-2 text-sm text-[var(--karbon-text-3,#8e8aae)]">
102
- {description}
103
- </p>
152
+ {#if message}
153
+ <p class="mt-2 text-sm leading-relaxed" style="color:var(--karbon-text-3);">{message}</p>
104
154
  {/if}
105
155
  </div>
106
156
 
157
+ <!-- Custom children -->
158
+ {#if children}
159
+ <div class="mt-4">
160
+ {@render children()}
161
+ </div>
162
+ {/if}
163
+
164
+ <!-- Confirm input -->
165
+ {#if confirmInput}
166
+ <div class="mt-5 text-left">
167
+ <label class="block text-xs font-medium mb-1.5" style="color:var(--karbon-text-2);">
168
+ {confirmInputLabelText}
169
+ </label>
170
+ <input
171
+ type="text"
172
+ bind:value={inputValue}
173
+ placeholder={confirmInputPlaceholder ?? confirmInput}
174
+ class="w-full px-3 py-2.5 rounded-lg text-sm outline-none transition-colors"
175
+ style="background:var(--karbon-bg-input);border:1px solid {inputValue === confirmInput ? vc.bg : 'var(--karbon-border-input)'};color:var(--karbon-text);"
176
+ onfocus={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = `0 0 0 3px color-mix(in srgb, ${vc.bg} 15%, transparent)` }}
177
+ onblur={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = 'none' }}
178
+ />
179
+ {#if confirmInput && inputValue && inputValue !== confirmInput}
180
+ <p class="mt-1 text-[11px]" style="color:var(--karbon-red-400);">Le texte ne correspond pas</p>
181
+ {/if}
182
+ </div>
183
+ {/if}
184
+
185
+ <!-- Buttons -->
107
186
  <div class="mt-6 flex gap-3">
108
187
  <button
109
- onclick={oncancel}
188
+ onclick={cancel}
110
189
  disabled={loading}
111
- class="flex-1 px-4 py-2.5 rounded-lg text-sm font-medium transition-colors
112
- border border-[var(--karbon-border,rgba(0,0,0,0.07))]
113
- text-[var(--karbon-text-2,#5a567e)]
114
- hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))]
115
- disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
190
+ class="flex-1 px-4 py-2.5 rounded-xl text-sm font-medium transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
191
+ style="color:var(--karbon-text-2);border:1px solid var(--karbon-border);"
192
+ onmouseenter={(e) => { (e.currentTarget as HTMLElement).style.background = 'var(--karbon-nav-hover-bg)' }}
193
+ onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent' }}
116
194
  >
117
195
  {cancelLabel}
118
196
  </button>
119
197
  <button
120
- onclick={onconfirm}
121
- disabled={loading}
122
- class="flex-1 px-4 py-2.5 rounded-lg text-sm font-semibold text-white transition-colors
123
- {confirmClasses[variant]}
124
- disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer
198
+ onclick={confirm}
199
+ disabled={isConfirmDisabled}
200
+ onmouseenter={() => btnHovered = true}
201
+ onmouseleave={() => btnHovered = false}
202
+ class="flex-1 px-4 py-2.5 rounded-xl text-sm font-semibold text-white transition-all cursor-pointer
203
+ disabled:opacity-40 disabled:cursor-not-allowed
125
204
  inline-flex items-center justify-center gap-2"
205
+ style="background:{btnHovered && !isConfirmDisabled ? vc.btnHover : vc.btn};"
126
206
  >
127
207
  {#if loading}
128
208
  <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" class="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>