@karbonjs/ui-svelte 0.2.5 → 0.3.1
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.
- package/package.json +3 -2
- package/src/accordion/Accordion.svelte +198 -23
- package/src/alert/AlertMessage.svelte +114 -20
- package/src/avatar/Avatar.svelte +16 -2
- package/src/badge/Badge.svelte +99 -10
- package/src/breadcrumb/Breadcrumb.svelte +124 -13
- package/src/button/Button.svelte +106 -21
- package/src/button/ButtonBrand.svelte +229 -0
- package/src/carousel/Carousel.svelte +161 -28
- package/src/code/CodeBlock.svelte +323 -0
- package/src/data/DataTable.svelte +321 -8
- package/src/data/Pagination.svelte +168 -28
- package/src/divider/Divider.svelte +91 -10
- package/src/dropdown/Dropdown.svelte +171 -27
- package/src/editor/RichTextEditor.svelte +862 -108
- package/src/form/Checkbox.svelte +110 -18
- package/src/form/ColorPicker.svelte +28 -16
- package/src/form/DatePicker.svelte +20 -10
- package/src/form/{FormInput.svelte → Input.svelte} +41 -14
- package/src/form/Radio.svelte +86 -18
- package/src/form/Select.svelte +246 -33
- package/src/form/Slider.svelte +22 -7
- package/src/form/Textarea.svelte +53 -10
- package/src/form/Toggle.svelte +72 -18
- package/src/image/Image.svelte +6 -4
- package/src/image/ImageCompare.svelte +182 -0
- package/src/image/ImgZoom.svelte +131 -49
- package/src/index.ts +7 -1
- package/src/kbd/Kbd.svelte +4 -3
- package/src/layout/Card.svelte +12 -6
- package/src/layout/EmptyState.svelte +75 -8
- package/src/layout/PageHeader.svelte +111 -11
- package/src/overlay/Dialog.svelte +147 -67
- package/src/overlay/ImgBox.svelte +125 -21
- package/src/overlay/Modal.svelte +110 -28
- package/src/overlay/Toast.svelte +152 -55
- package/src/progress/Progress.svelte +137 -26
- package/src/skeleton/Skeleton.svelte +6 -4
- package/src/tabs/Tabs.svelte +133 -22
- 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.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
-
"
|
|
18
|
+
"simple-icons": "^16.12.0",
|
|
19
|
+
"@karbonjs/ui-core": "0.3.1"
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
function sanitizeSvg(html: string): string {
|
|
50
|
+
return html.replace(/on\w+\s*=/gi, '').replace(/<script/gi, '<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
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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 = '
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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}
|
package/src/avatar/Avatar.svelte
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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}
|
package/src/badge/Badge.svelte
CHANGED
|
@@ -1,24 +1,113 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ButtonColor } from '@karbonjs/ui-core'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
|
-
variant?:
|
|
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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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>
|