@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karbonjs/ui-svelte",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Karbon UI components for Svelte 5",
5
5
  "type": "module",
6
6
  "svelte": "src/index.ts",
@@ -15,7 +15,8 @@
15
15
  "src"
16
16
  ],
17
17
  "dependencies": {
18
- "@karbonjs/ui-core": "0.2.5"
18
+ "simple-icons": "^16.12.0",
19
+ "@karbonjs/ui-core": "0.3.0"
19
20
  },
20
21
  "peerDependencies": {
21
22
  "svelte": "^5.0.0"
@@ -1,63 +1,238 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
- import type { AccordionItem } from '@karbonjs/ui-core'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
+
5
+ interface AccordionItem {
6
+ id: string
7
+ title: string
8
+ content?: string
9
+ description?: string
10
+ icon?: string
11
+ disabled?: boolean
12
+ defaultOpen?: boolean
13
+ }
4
14
 
5
15
  interface Props {
6
16
  items: AccordionItem[]
7
17
  multiple?: boolean
18
+ variant?: 'default' | 'bordered' | 'separated' | 'ghost' | 'filled' | 'colored'
19
+ color?: ButtonColor
20
+ size?: 'sm' | 'md' | 'lg'
21
+ bg?: boolean | string
22
+ border?: boolean | string
23
+ highlightActive?: boolean
24
+ arrow?: 'chevron' | 'plus' | 'arrow' | 'dot' | 'none'
25
+ arrowPosition?: 'left' | 'right'
8
26
  class?: string
9
- children?: Snippet<[{ item: AccordionItem; index: number }]>
27
+ classes?: { root?: string, item?: string, trigger?: string, content?: string, arrow?: string }
28
+ onchange?: (openIds: string[]) => void
29
+ children?: Snippet<[{ item: AccordionItem, index: number }]>
10
30
  }
11
31
 
12
32
  let {
13
33
  items,
14
34
  multiple = false,
35
+ variant = 'default',
36
+ bg = false,
37
+ border = false,
38
+ highlightActive = true,
39
+ color,
40
+ size = 'md',
41
+ arrow = 'chevron',
42
+ arrowPosition = 'right',
15
43
  class: className = '',
44
+ classes = {},
45
+ onchange,
16
46
  children
17
47
  }: Props = $props()
18
48
 
19
- let openIds = $state<Set<string>>(new Set())
49
+ function sanitizeSvg(html: string): string {
50
+ return html.replace(/on\w+\s*=/gi, '').replace(/<script/gi, '&lt;script')
51
+ }
52
+
53
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
54
+ const accentBg = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
55
+
56
+ let openIds = $state<Set<string>>(new Set(items.filter(i => i.defaultOpen).map(i => i.id)))
20
57
 
21
58
  function toggle(id: string) {
22
59
  const next = new Set(openIds)
23
- if (next.has(id)) {
24
- next.delete(id)
25
- } else {
26
- if (!multiple) next.clear()
27
- next.add(id)
28
- }
60
+ if (next.has(id)) next.delete(id)
61
+ else { if (!multiple) next.clear(); next.add(id) }
29
62
  openIds = next
63
+ onchange?.([...next])
64
+ }
65
+
66
+ const sizeMap = {
67
+ sm: { px: 'px-3', py: 'py-2.5', text: 'text-xs', content: 'text-xs', icon: 14, arrowBox: 20 },
68
+ md: { px: 'px-4', py: 'py-3.5', text: 'text-sm', content: 'text-sm', icon: 16, arrowBox: 24 },
69
+ lg: { px: 'px-5', py: 'py-4', text: 'text-base', content: 'text-base', icon: 18, arrowBox: 28 },
70
+ }
71
+ const s = $derived(sizeMap[size])
72
+
73
+ // Arrow SVGs
74
+ const arrows: Record<string, { open: string, closed: string }> = {
75
+ chevron: {
76
+ closed: '<path d="m6 9 6 6 6-6"/>',
77
+ open: '<path d="m6 9 6 6 6-6"/>',
78
+ },
79
+ plus: {
80
+ closed: '<path d="M12 5v14"/><path d="M5 12h14"/>',
81
+ open: '<path d="M5 12h14"/>',
82
+ },
83
+ arrow: {
84
+ closed: '<path d="m9 18 6-6-6-6"/>',
85
+ open: '<path d="m9 18 6-6-6-6"/>',
86
+ },
87
+ dot: {
88
+ closed: '<circle cx="12" cy="12" r="4"/>',
89
+ open: '<circle cx="12" cy="12" r="4"/>',
90
+ },
91
+ }
92
+
93
+ function arrowRotation(isOpen: boolean): string {
94
+ if (arrow === 'plus' || arrow === 'dot') return '' // plus uses different SVG, no rotation
95
+ if (arrow === 'arrow') return isOpen ? 'rotate(90deg)' : 'rotate(0deg)'
96
+ return isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
97
+ }
98
+
99
+ function rootStyle(): string {
100
+ switch (variant) {
101
+ case 'default': {
102
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : 'var(--karbon-border)'
103
+ return `border-radius:0.75rem;border:1px solid ${bc};overflow:hidden;`
104
+ }
105
+ case 'bordered': {
106
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : 'var(--karbon-border)'
107
+ return `border-radius:0.75rem;border:1px solid ${bc};overflow:hidden;`
108
+ }
109
+ case 'separated': return ''
110
+ case 'ghost': return ''
111
+ case 'filled': return `border-radius:0.75rem;overflow:hidden;`
112
+ case 'colored': return `border-radius:0.75rem;overflow:hidden;`
113
+ default: return ''
114
+ }
115
+ }
116
+
117
+ function itemStyle(isOpen: boolean, isLast: boolean): string {
118
+ switch (variant) {
119
+ case 'separated': {
120
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : (isOpen ? accent : 'var(--karbon-border)')
121
+ return `border-radius:0.75rem;border:1px solid ${bc};overflow:hidden;margin-bottom:0.5rem;${isOpen ? `box-shadow:0 0 0 1px color-mix(in srgb,${accent} 15%,transparent),0 2px 8px color-mix(in srgb,${accent} 8%,transparent);` : ''}`
122
+ }
123
+ case 'filled': {
124
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : (isOpen ? `color-mix(in srgb,${accent} 15%,transparent)` : 'var(--karbon-border)')
125
+ return `background:${isOpen ? `color-mix(in srgb,${accentBg} 6%,transparent)` : 'transparent'};${!isLast ? `border-bottom:1px solid ${bc};` : ''}`
126
+ }
127
+ case 'colored': {
128
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : (isOpen ? `color-mix(in srgb,${accent} 20%,transparent)` : 'var(--karbon-border)')
129
+ return `background:${isOpen ? `color-mix(in srgb,${accentBg} 12%,transparent)` : 'var(--karbon-bg-2)'};${!isLast ? `border-bottom:1px solid ${bc};` : ''}${isOpen ? `box-shadow:inset 3px 0 0 ${accent};` : ''}transition:all 0.2s ease;`
130
+ }
131
+ case 'ghost': {
132
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : 'var(--karbon-border)'
133
+ return `${!isLast ? `border-bottom:1px solid ${bc};` : ''}`
134
+ }
135
+ default: {
136
+ const bc = border ? (typeof border === 'string' ? border : 'var(--karbon-border)') : 'var(--karbon-border)'
137
+ return `${!isLast ? `border-bottom:1px solid ${bc};` : ''}`
138
+ }
139
+ }
140
+ }
141
+
142
+ function triggerStyle(isOpen: boolean): string {
143
+ if (!highlightActive) return 'color:var(--karbon-text);'
144
+ if (variant === 'bordered' && isOpen) {
145
+ return `background:color-mix(in srgb,${accent} 6%,transparent);color:${accent};`
146
+ }
147
+ if (variant === 'colored' && isOpen) {
148
+ return `color:${accent};`
149
+ }
150
+ return `color:${isOpen ? accent : 'var(--karbon-text)'};`
151
+ }
152
+
153
+ function arrowStyle(isOpen: boolean): string {
154
+ const base = `width:${s.arrowBox}px;height:${s.arrowBox}px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;transition:all 0.25s cubic-bezier(0.16,1,0.3,1);flex-shrink:0;`
155
+ if (isOpen) {
156
+ return `${base}background:color-mix(in srgb,${accent} 15%,transparent);color:${accent};transform:${arrowRotation(true)};`
157
+ }
158
+ return `${base}background:transparent;color:var(--karbon-text-4);transform:${arrowRotation(false)};`
30
159
  }
31
160
  </script>
32
161
 
33
- <div class="rounded-xl border border-[var(--karbon-border,rgba(0,0,0,0.07))] overflow-hidden divide-y divide-[var(--karbon-border,rgba(0,0,0,0.07))] {className}">
162
+ <div class="{classes?.root ?? className}" style="{rootStyle()}{bg === true ? 'background:var(--karbon-bg-card);' : typeof bg === 'string' ? `background:${bg};` : ''}">
34
163
  {#each items as item, index}
35
- <div>
164
+ {@const isOpen = openIds.has(item.id)}
165
+ {@const isLast = index === items.length - 1}
166
+ <div class="{classes?.item ?? ''}" style={itemStyle(isOpen, isLast)}>
167
+ <!-- Trigger -->
36
168
  <button
37
169
  type="button"
38
170
  onclick={() => { if (!item.disabled) toggle(item.id) }}
39
171
  disabled={item.disabled}
40
- class="w-full flex items-center justify-between px-4 py-3 text-sm font-medium text-left transition-colors
41
- text-[var(--karbon-text,#1a1635)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))]
42
- disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
172
+ class="w-full flex items-center gap-3 {s.px} {s.py} {s.text} font-medium text-left transition-all
173
+ disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer
174
+ {classes?.trigger ?? ''}"
175
+ style={triggerStyle(isOpen)}
176
+ onmouseenter={(e) => { if (!item.disabled && !isOpen) (e.currentTarget as HTMLElement).style.background = 'var(--karbon-nav-hover-bg)' }}
177
+ onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.cssText = triggerStyle(isOpen) }}
43
178
  >
44
- <span>{item.title}</span>
45
- <svg
46
- xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
47
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
48
- class="shrink-0 text-[var(--karbon-text-4,#b5b2cc)] transition-transform duration-200 {openIds.has(item.id) ? 'rotate-180' : ''}"
49
- ><path d="m6 9 6 6 6-6"/></svg>
179
+ <!-- Left arrow -->
180
+ {#if arrowPosition === 'left' && arrow !== 'none'}
181
+ <span style={arrowStyle(isOpen)} class="{classes?.arrow ?? ''}">
182
+ <svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill={arrow === 'dot' ? 'currentColor' : 'none'} stroke={arrow === 'dot' ? 'none' : 'currentColor'} stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
183
+ {@html isOpen ? arrows[arrow].open : arrows[arrow].closed}
184
+ </svg>
185
+ </span>
186
+ {/if}
187
+
188
+ <!-- Item icon -->
189
+ {#if item.icon}
190
+ <span class="shrink-0 opacity-60">{@html sanitizeSvg(item.icon)}</span>
191
+ {/if}
192
+
193
+ <!-- Title + description -->
194
+ <div class="flex-1 min-w-0">
195
+ <div class="flex items-center gap-2">
196
+ <span>{item.title}</span>
197
+ {#if item.description && !isOpen}
198
+ <span class="font-normal truncate hidden sm:inline" style="color:var(--karbon-text-4);font-size:0.85em;">— {item.description}</span>
199
+ {/if}
200
+ </div>
201
+ {#if item.description && isOpen}
202
+ <p class="font-normal mt-0.5" style="color:var(--karbon-text-3);font-size:0.85em;">{item.description}</p>
203
+ {/if}
204
+ </div>
205
+
206
+ <!-- Right arrow -->
207
+ {#if arrowPosition === 'right' && arrow !== 'none'}
208
+ <span style={arrowStyle(isOpen)} class="{classes?.arrow ?? ''}">
209
+ <svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill={arrow === 'dot' ? 'currentColor' : 'none'} stroke={arrow === 'dot' ? 'none' : 'currentColor'} stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
210
+ {@html isOpen ? arrows[arrow].open : arrows[arrow].closed}
211
+ </svg>
212
+ </span>
213
+ {/if}
50
214
  </button>
51
215
 
52
- {#if openIds.has(item.id)}
53
- <div class="px-4 pb-3 text-sm text-[var(--karbon-text-2,#5a567e)]">
216
+ <!-- Content -->
217
+ {#if isOpen}
218
+ <div
219
+ class="{s.px} pb-4 {s.content} {classes?.content ?? ''}"
220
+ style="color:var(--karbon-text-2);animation:karbon-accordion-slide 0.25s cubic-bezier(0.16,1,0.3,1);"
221
+ >
54
222
  {#if children}
55
223
  {@render children({ item, index })}
56
224
  {:else if item.content}
57
- <p>{item.content}</p>
225
+ <p class="leading-relaxed">{item.content}</p>
58
226
  {/if}
59
227
  </div>
60
228
  {/if}
61
229
  </div>
62
230
  {/each}
63
231
  </div>
232
+
233
+ <style>
234
+ @keyframes karbon-accordion-slide {
235
+ from { opacity: 0; transform: translateY(-8px); }
236
+ to { opacity: 1; transform: translateY(0); }
237
+ }
238
+ </style>
@@ -1,44 +1,138 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
3
  import type { AlertType } from '@karbonjs/ui-core'
4
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
5
 
5
6
  interface Props {
6
7
  type?: AlertType
8
+ variant?: 'soft' | 'filled' | 'outline' | 'bordered'
9
+ color?: ButtonColor
10
+ title?: string
7
11
  message?: string
12
+ dismissible?: boolean
13
+ icon?: Snippet | false
14
+ actions?: Snippet
8
15
  class?: string
16
+ classes?: { root?: string, icon?: string, title?: string, text?: string, close?: string }
17
+ ondismiss?: () => void
9
18
  children?: Snippet
10
19
  }
11
20
 
12
21
  let {
13
- type = 'error',
22
+ type = 'info',
23
+ variant = 'soft',
24
+ color,
25
+ title = '',
14
26
  message = '',
27
+ dismissible = false,
28
+ icon,
29
+ actions,
15
30
  class: className = '',
31
+ classes = {},
32
+ ondismiss,
16
33
  children
17
34
  }: Props = $props()
18
35
 
19
- const variants: Record<string, string> = {
20
- error: 'bg-red-500/8 border border-red-500/20 text-red-400',
21
- success: 'bg-green-500/8 border border-green-500/20 text-green-400',
22
- warning: 'bg-amber-500/8 border border-amber-500/20 text-amber-300',
23
- info: 'bg-blue-500/8 border border-blue-500/20 text-blue-400'
36
+ let visible = $state(true)
37
+
38
+ // Type color mapping when no color prop
39
+ const typeColors: Record<string, string> = {
40
+ error: 'var(--karbon-red-500, #ef4444)',
41
+ success: 'var(--karbon-emerald-500, #10b981)',
42
+ warning: 'var(--karbon-amber-500, #f59e0b)',
43
+ info: 'var(--karbon-blue-500, #3b82f6)',
44
+ }
45
+
46
+ const typeColorsLight: Record<string, string> = {
47
+ error: 'var(--karbon-red-400, #f87171)',
48
+ success: 'var(--karbon-emerald-400, #34d399)',
49
+ warning: 'var(--karbon-amber-400, #fbbf24)',
50
+ info: 'var(--karbon-blue-400, #60a5fa)',
51
+ }
52
+
53
+ const baseColor = $derived(color ? `var(--karbon-${color}-500)` : typeColors[type])
54
+ const lightColor = $derived(color ? `var(--karbon-${color}-400)` : typeColorsLight[type])
55
+
56
+ const style = $derived.by(() => {
57
+ switch (variant) {
58
+ case 'soft':
59
+ return `background:color-mix(in srgb,${baseColor} 10%,transparent);color:${lightColor};border:1px solid color-mix(in srgb,${baseColor} 15%,transparent);`
60
+ case 'filled':
61
+ return `background:${baseColor};color:white;border:none;`
62
+ case 'outline':
63
+ return `background:transparent;color:${lightColor};border:1px solid color-mix(in srgb,${baseColor} 30%,transparent);`
64
+ case 'bordered':
65
+ return `background:color-mix(in srgb,${baseColor} 6%,transparent);color:${lightColor};border:none;border-left:3px solid ${baseColor};`
66
+ default: return ''
67
+ }
68
+ })
69
+
70
+ // SVG icons per type
71
+ const typeIcons: Record<string, string> = {
72
+ 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"/>',
73
+ success: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>',
74
+ 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"/>',
75
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
76
+ }
77
+
78
+ function dismiss() {
79
+ visible = false
80
+ ondismiss?.()
24
81
  }
25
82
  </script>
26
83
 
27
- {#if message || children}
28
- <div class="flex items-center gap-2.5 px-4 py-3 rounded-[0.625rem] text-[0.825rem] font-medium {variants[type]} {className}">
29
- {#if type === 'error'}
30
- <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="shrink-0"><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>
31
- {:else if type === 'success'}
32
- <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="shrink-0"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>
33
- {:else if type === 'warning'}
34
- <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="shrink-0"><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>
35
- {:else}
36
- <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="shrink-0"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
84
+ {#if visible && (message || children || title)}
85
+ <div
86
+ class="flex gap-3 rounded-xl px-4 py-3 text-sm {classes?.root ?? className}"
87
+ style="{style}animation:karbon-alert-in 0.25s ease;"
88
+ role="alert"
89
+ >
90
+ <!-- Icon -->
91
+ {#if icon !== false}
92
+ <div class="shrink-0 mt-0.5 {classes?.icon ?? ''}">
93
+ {#if icon}
94
+ {@render icon()}
95
+ {:else}
96
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
97
+ stroke={variant === 'filled' ? 'white' : 'currentColor'}
98
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
99
+ >{@html typeIcons[type] || typeIcons.info}</svg>
100
+ {/if}
101
+ </div>
37
102
  {/if}
38
- {#if children}
39
- {@render children()}
40
- {:else}
41
- <span>{message}</span>
103
+
104
+ <!-- Content -->
105
+ <div class="flex-1 min-w-0">
106
+ {#if title}
107
+ <p class="font-semibold {message || children ? 'mb-1' : ''} {classes?.title ?? ''}"
108
+ style={variant === 'filled' ? 'color:white;' : ''}
109
+ >{title}</p>
110
+ {/if}
111
+ {#if children}
112
+ <div class="opacity-90 {classes?.text ?? ''}">{@render children()}</div>
113
+ {:else if message}
114
+ <p class="opacity-90 {classes?.text ?? ''}">{message}</p>
115
+ {/if}
116
+ {#if actions}
117
+ <div class="mt-2.5 flex items-center gap-2">
118
+ {@render actions()}
119
+ </div>
120
+ {/if}
121
+ </div>
122
+
123
+ <!-- Dismiss -->
124
+ {#if dismissible}
125
+ <button
126
+ type="button"
127
+ onclick={dismiss}
128
+ class="shrink-0 mt-0.5 rounded-lg p-1 transition-opacity opacity-50 hover:opacity-100 cursor-pointer {classes?.close ?? ''}"
129
+ aria-label="Fermer"
130
+ >
131
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
132
+ stroke={variant === 'filled' ? 'white' : 'currentColor'}
133
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
134
+ ><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
135
+ </button>
42
136
  {/if}
43
137
  </div>
44
138
  {/if}
@@ -6,7 +6,9 @@
6
6
  alt?: string
7
7
  name?: string
8
8
  size?: AvatarSize
9
+ color?: string
9
10
  class?: string
11
+ classes?: { root?: string }
10
12
  }
11
13
 
12
14
  let {
@@ -14,7 +16,9 @@
14
16
  alt = '',
15
17
  name = '',
16
18
  size = 'md',
17
- class: className = ''
19
+ color,
20
+ class: className = '',
21
+ classes = {}
18
22
  }: Props = $props()
19
23
 
20
24
  let errored = $state(false)
@@ -32,9 +36,19 @@
32
36
  )
33
37
 
34
38
  const showImage = $derived(src && !errored)
39
+
40
+ const colorStyle = $derived.by(() => {
41
+ if (!color) return ''
42
+ return `background:color-mix(in srgb,var(--karbon-${color}-500) 15%,transparent);color:var(--karbon-${color}-400)`
43
+ })
44
+
45
+ const defaultBgClass = $derived(color ? '' : 'bg-[var(--karbon-bg-2,#e8e6f0)] text-[var(--karbon-text-2,#5a567e)]')
35
46
  </script>
36
47
 
37
- <div class="relative inline-flex items-center justify-center shrink-0 rounded-full overflow-hidden bg-[var(--karbon-bg-2,#e8e6f0)] text-[var(--karbon-text-2,#5a567e)] font-semibold {sizeClasses[size]} {className}">
48
+ <div
49
+ class="relative inline-flex items-center justify-center shrink-0 rounded-full overflow-hidden font-semibold {defaultBgClass} {sizeClasses[size]} {classes?.root ?? className}"
50
+ style={colorStyle}
51
+ >
38
52
  {#if showImage}
39
53
  <img
40
54
  {src}
@@ -1,24 +1,113 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
- import type { BadgeVariant } from '@karbonjs/ui-core'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
4
 
5
5
  interface Props {
6
- variant?: BadgeVariant
6
+ variant?: 'soft' | 'solid' | 'outline' | 'dot' | 'flat'
7
+ color?: ButtonColor
8
+ size?: 'xs' | 'sm' | 'md' | 'lg'
9
+ shape?: 'pill' | 'rounded' | 'square'
10
+ closable?: boolean
11
+ dot?: boolean
12
+ icon?: Snippet
7
13
  class?: string
14
+ classes?: { root?: string, dot?: string, icon?: string, close?: string }
15
+ onclose?: () => void
8
16
  children: Snippet
9
17
  }
10
18
 
11
- let { variant = 'default', class: className = '', children }: Props = $props()
19
+ let {
20
+ variant = 'soft',
21
+ color,
22
+ size = 'sm',
23
+ shape = 'pill',
24
+ closable = false,
25
+ dot = false,
26
+ icon,
27
+ class: className = '',
28
+ classes = {},
29
+ onclose,
30
+ children
31
+ }: Props = $props()
12
32
 
13
- const variants: Record<string, string> = {
14
- default: 'bg-[var(--karbon-bg-2,rgba(255,255,255,0.08))] text-[var(--karbon-text-2,#a1a1aa)]',
15
- success: 'bg-emerald-500/15 text-emerald-500',
16
- warning: 'bg-amber-500/15 text-amber-500',
17
- danger: 'bg-red-500/15 text-red-500',
18
- info: 'bg-blue-500/15 text-blue-500'
33
+ function c(shade: number): string {
34
+ return color ? `var(--karbon-${color}-${shade})` : ''
35
+ }
36
+
37
+ const accent = $derived(color ? c(500) : 'var(--karbon-primary)')
38
+ const accentLight = $derived(color ? c(400) : 'var(--karbon-primary)')
39
+
40
+ // Preset color variants (when no color prop)
41
+ const presetStyles: Record<string, string> = {
42
+ soft: 'background:var(--karbon-bg-2);color:var(--karbon-text-2);',
43
+ solid: 'background:var(--karbon-primary);color:white;',
44
+ outline: 'background:transparent;color:var(--karbon-text-2);border:1px solid var(--karbon-border);',
45
+ dot: 'background:var(--karbon-bg-2);color:var(--karbon-text-2);',
46
+ flat: 'background:transparent;color:var(--karbon-text-2);',
47
+ }
48
+
49
+ const colorStyle = $derived.by(() => {
50
+ if (!color) return presetStyles[variant] || presetStyles.soft
51
+ switch (variant) {
52
+ case 'soft': return `background:color-mix(in srgb,${accent} 15%,transparent);color:${accentLight};`
53
+ case 'solid': return `background:${accent};color:white;`
54
+ case 'outline': return `background:transparent;color:${accentLight};border:1px solid color-mix(in srgb,${accentLight} 40%,transparent);`
55
+ case 'dot': return `background:color-mix(in srgb,${accent} 10%,transparent);color:${accentLight};`
56
+ case 'flat': return `background:transparent;color:${accentLight};`
57
+ default: return `background:color-mix(in srgb,${accent} 15%,transparent);color:${accentLight};`
58
+ }
59
+ })
60
+
61
+ const sizeClasses: Record<string, string> = {
62
+ xs: 'px-1.5 py-px text-[10px] gap-1',
63
+ sm: 'px-2 py-0.5 text-[11px] gap-1',
64
+ md: 'px-2.5 py-0.5 text-xs gap-1.5',
65
+ lg: 'px-3 py-1 text-sm gap-1.5',
66
+ }
67
+
68
+ const dotSizes: Record<string, string> = {
69
+ xs: 'w-1 h-1',
70
+ sm: 'w-1.5 h-1.5',
71
+ md: 'w-2 h-2',
72
+ lg: 'w-2 h-2',
73
+ }
74
+
75
+ const iconSizes: Record<string, number> = { xs: 8, sm: 10, md: 12, lg: 14 }
76
+
77
+ const shapeClasses: Record<string, string> = {
78
+ pill: 'rounded-full',
79
+ rounded: 'rounded-md',
80
+ square: 'rounded-none',
19
81
  }
20
82
  </script>
21
83
 
22
- <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {variants[variant]} {className}">
84
+ <span
85
+ class="inline-flex items-center font-medium {sizeClasses[size]} {shapeClasses[shape]} {classes?.root ?? className}"
86
+ style={colorStyle}
87
+ >
88
+ {#if (variant === 'dot' || dot) && !icon}
89
+ <span
90
+ class="rounded-full shrink-0 {dotSizes[size]} {classes?.dot ?? ''}"
91
+ style="background: {color ? accentLight : 'currentColor'};"
92
+ ></span>
93
+ {/if}
94
+
95
+ {#if icon}
96
+ <span class="shrink-0 {classes?.icon ?? ''}">
97
+ {@render icon()}
98
+ </span>
99
+ {/if}
100
+
23
101
  {@render children()}
102
+
103
+ {#if closable}
104
+ <button
105
+ type="button"
106
+ onclick={(e) => { e.stopPropagation(); onclose?.() }}
107
+ class="shrink-0 ml-0.5 rounded-full transition-opacity opacity-60 hover:opacity-100 cursor-pointer {classes?.close ?? ''}"
108
+ aria-label="Fermer"
109
+ >
110
+ <svg xmlns="http://www.w3.org/2000/svg" width={iconSizes[size]} height={iconSizes[size]} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
111
+ </button>
112
+ {/if}
24
113
  </span>