@karbonjs/ui-svelte 0.2.4 → 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.
- 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 +319 -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 +861 -107
- 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
|
@@ -1,27 +1,108 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { ButtonColor } from '@karbonjs/ui-core'
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
|
-
direction?:
|
|
6
|
+
direction?: 'horizontal' | 'vertical'
|
|
7
|
+
variant?: 'solid' | 'dashed' | 'dotted' | 'gradient'
|
|
8
|
+
color?: ButtonColor
|
|
9
|
+
thickness?: number
|
|
6
10
|
label?: string
|
|
11
|
+
labelPosition?: 'left' | 'center' | 'right'
|
|
12
|
+
icon?: Snippet
|
|
13
|
+
spacing?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
|
|
7
14
|
class?: string
|
|
15
|
+
classes?: { root?: string, line?: string, label?: string }
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
let {
|
|
11
19
|
direction = 'horizontal',
|
|
20
|
+
variant = 'solid',
|
|
21
|
+
color,
|
|
22
|
+
thickness = 1,
|
|
12
23
|
label = '',
|
|
13
|
-
|
|
24
|
+
labelPosition = 'center',
|
|
25
|
+
icon,
|
|
26
|
+
spacing = 'md',
|
|
27
|
+
class: className = '',
|
|
28
|
+
classes = {}
|
|
14
29
|
}: Props = $props()
|
|
30
|
+
|
|
31
|
+
const lineColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-border)')
|
|
32
|
+
const labelColor = $derived(color ? `var(--karbon-${color}-400)` : 'var(--karbon-text-4)')
|
|
33
|
+
|
|
34
|
+
const spacingMap = { none: '0', sm: '0.5rem', md: '1rem', lg: '1.5rem', xl: '2.5rem' }
|
|
35
|
+
const pad = $derived(spacingMap[spacing])
|
|
36
|
+
|
|
37
|
+
function lineStyle(): string {
|
|
38
|
+
switch (variant) {
|
|
39
|
+
case 'solid': return `background:${lineColor};`
|
|
40
|
+
case 'dashed': return `background:transparent;border-top:${thickness}px dashed ${lineColor};height:0;`
|
|
41
|
+
case 'dotted': return `background:transparent;border-top:${thickness}px dotted ${lineColor};height:0;`
|
|
42
|
+
case 'gradient': return `background:linear-gradient(90deg,transparent,${lineColor},transparent);`
|
|
43
|
+
default: return `background:${lineColor};`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function verticalLineStyle(): string {
|
|
48
|
+
switch (variant) {
|
|
49
|
+
case 'solid': return `background:${lineColor};`
|
|
50
|
+
case 'dashed': return `background:transparent;border-left:${thickness}px dashed ${lineColor};width:0;`
|
|
51
|
+
case 'dotted': return `background:transparent;border-left:${thickness}px dotted ${lineColor};width:0;`
|
|
52
|
+
case 'gradient': return `background:linear-gradient(180deg,transparent,${lineColor},transparent);`
|
|
53
|
+
default: return `background:${lineColor};`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasLabel = $derived(!!label || !!icon)
|
|
15
58
|
</script>
|
|
16
59
|
|
|
17
60
|
{#if direction === 'vertical'}
|
|
18
|
-
<div
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
61
|
+
<div
|
|
62
|
+
class="inline-flex self-stretch items-center {classes?.root ?? className}"
|
|
63
|
+
style="padding:0 {pad};"
|
|
64
|
+
>
|
|
65
|
+
{#if hasLabel}
|
|
66
|
+
<div class="flex flex-col items-center gap-2 h-full">
|
|
67
|
+
<div class="flex-1" style="{verticalLineStyle()}width:{variant === 'solid' || variant === 'gradient' ? `${thickness}px` : '0'};min-height:8px;"></div>
|
|
68
|
+
{#if icon}
|
|
69
|
+
<span class="shrink-0" style="color:{labelColor};">{@render icon()}</span>
|
|
70
|
+
{:else}
|
|
71
|
+
<span class="text-[10px] font-medium shrink-0 [writing-mode:vertical-lr] {classes?.label ?? ''}" style="color:{labelColor};">{label}</span>
|
|
72
|
+
{/if}
|
|
73
|
+
<div class="flex-1" style="{verticalLineStyle()}width:{variant === 'solid' || variant === 'gradient' ? `${thickness}px` : '0'};min-height:8px;"></div>
|
|
74
|
+
</div>
|
|
75
|
+
{:else}
|
|
76
|
+
<div style="{verticalLineStyle()}width:{variant === 'solid' || variant === 'gradient' ? `${thickness}px` : '0'};height:100%;"></div>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
{:else if hasLabel}
|
|
80
|
+
<div
|
|
81
|
+
class="flex items-center gap-3 {classes?.root ?? className}"
|
|
82
|
+
style="padding:{pad} 0;"
|
|
83
|
+
>
|
|
84
|
+
<div
|
|
85
|
+
class="{labelPosition === 'left' ? 'w-8' : 'flex-1'} {classes?.line ?? ''}"
|
|
86
|
+
style="{lineStyle()}height:{variant === 'solid' || variant === 'gradient' ? `${thickness}px` : '0'};min-width:8px;"
|
|
87
|
+
></div>
|
|
88
|
+
|
|
89
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
90
|
+
{#if icon}
|
|
91
|
+
<span style="color:{labelColor};">{@render icon()}</span>
|
|
92
|
+
{/if}
|
|
93
|
+
{#if label}
|
|
94
|
+
<span class="text-xs font-medium {classes?.label ?? ''}" style="color:{labelColor};">{label}</span>
|
|
95
|
+
{/if}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
class="{labelPosition === 'right' ? 'w-8' : 'flex-1'} {classes?.line ?? ''}"
|
|
100
|
+
style="{lineStyle()}height:{variant === 'solid' || variant === 'gradient' ? `${thickness}px` : '0'};min-width:8px;"
|
|
101
|
+
></div>
|
|
24
102
|
</div>
|
|
25
103
|
{:else}
|
|
26
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
class="w-full {classes?.root ?? className}"
|
|
106
|
+
style="{lineStyle()}height:{variant === 'solid' || variant === 'gradient' ? `${thickness}px` : '0'};margin:{pad} 0;"
|
|
107
|
+
></div>
|
|
27
108
|
{/if}
|
|
@@ -1,59 +1,203 @@
|
|
|
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 DropdownItem {
|
|
6
|
+
label: string
|
|
7
|
+
value?: string
|
|
8
|
+
icon?: string
|
|
9
|
+
description?: string
|
|
10
|
+
badge?: string
|
|
11
|
+
danger?: boolean
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
divider?: boolean
|
|
14
|
+
group?: string
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
interface Props {
|
|
6
|
-
items:
|
|
7
|
-
align?:
|
|
18
|
+
items: DropdownItem[]
|
|
19
|
+
align?: 'left' | 'right'
|
|
20
|
+
position?: 'bottom' | 'top'
|
|
21
|
+
width?: string
|
|
22
|
+
color?: ButtonColor
|
|
23
|
+
searchable?: boolean
|
|
24
|
+
searchPlaceholder?: string
|
|
8
25
|
class?: string
|
|
26
|
+
classes?: { root?: string, menu?: string, item?: string, group?: string, search?: string }
|
|
9
27
|
trigger: Snippet
|
|
10
|
-
onselect?: (value: string) => void
|
|
28
|
+
onselect?: (value: string, item: DropdownItem) => void
|
|
11
29
|
}
|
|
12
30
|
|
|
13
31
|
let {
|
|
14
32
|
items,
|
|
15
33
|
align = 'left',
|
|
34
|
+
position = 'bottom',
|
|
35
|
+
width = '14rem',
|
|
36
|
+
color,
|
|
37
|
+
searchable = false,
|
|
38
|
+
searchPlaceholder = 'Rechercher...',
|
|
16
39
|
class: className = '',
|
|
40
|
+
classes = {},
|
|
17
41
|
trigger,
|
|
18
42
|
onselect
|
|
19
43
|
}: Props = $props()
|
|
20
44
|
|
|
21
45
|
let open = $state(false)
|
|
46
|
+
let search = $state('')
|
|
47
|
+
let menuEl: HTMLDivElement | undefined = $state()
|
|
48
|
+
let focusIndex = $state(-1)
|
|
49
|
+
|
|
50
|
+
function sanitizeSvg(html: string): string {
|
|
51
|
+
return html.replace(/on\w+\s*=/gi, '').replace(/<script/gi, '<script')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
55
|
+
|
|
56
|
+
const filteredItems = $derived(
|
|
57
|
+
searchable && search
|
|
58
|
+
? items.filter(i => !i.divider && i.label.toLowerCase().includes(search.toLowerCase()))
|
|
59
|
+
: items
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Group items
|
|
63
|
+
const groupedItems = $derived.by(() => {
|
|
64
|
+
const groups: { label: string | null, items: DropdownItem[] }[] = []
|
|
65
|
+
let currentGroup: string | null = null
|
|
66
|
+
for (const item of filteredItems) {
|
|
67
|
+
if (item.group && item.group !== currentGroup) {
|
|
68
|
+
currentGroup = item.group
|
|
69
|
+
groups.push({ label: currentGroup, items: [] })
|
|
70
|
+
}
|
|
71
|
+
if (groups.length === 0) groups.push({ label: null, items: [] })
|
|
72
|
+
groups[groups.length - 1].items.push(item)
|
|
73
|
+
}
|
|
74
|
+
return groups
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
function toggle(e: MouseEvent) {
|
|
78
|
+
e.stopPropagation()
|
|
79
|
+
open = !open
|
|
80
|
+
if (open) { search = ''; focusIndex = -1 }
|
|
81
|
+
}
|
|
22
82
|
|
|
23
|
-
function
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
onselect?.(item.value ?? item.label)
|
|
83
|
+
function select(item: DropdownItem) {
|
|
84
|
+
if (item.divider || item.disabled) return
|
|
85
|
+
onselect?.(item.value ?? item.label, item)
|
|
27
86
|
open = false
|
|
28
87
|
}
|
|
88
|
+
|
|
89
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
90
|
+
if (!open) return
|
|
91
|
+
const selectableItems = filteredItems.filter(i => !i.divider && !i.disabled)
|
|
92
|
+
if (e.key === 'ArrowDown') {
|
|
93
|
+
e.preventDefault()
|
|
94
|
+
focusIndex = Math.min(focusIndex + 1, selectableItems.length - 1)
|
|
95
|
+
} else if (e.key === 'ArrowUp') {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
focusIndex = Math.max(focusIndex - 1, 0)
|
|
98
|
+
} else if (e.key === 'Enter' && focusIndex >= 0) {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
select(selectableItems[focusIndex])
|
|
101
|
+
} else if (e.key === 'Escape') {
|
|
102
|
+
open = false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clickOutside(e: MouseEvent) {
|
|
107
|
+
if (menuEl && !menuEl.contains(e.target as Node)) open = false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const posClass = $derived(position === 'top' ? 'bottom-full mb-1' : 'top-full mt-1')
|
|
111
|
+
const alignClass = $derived(align === 'right' ? 'right-0' : 'left-0')
|
|
29
112
|
</script>
|
|
30
113
|
|
|
31
|
-
<svelte:window onclick={
|
|
114
|
+
<svelte:window onclick={clickOutside} onkeydown={handleKeydown} />
|
|
32
115
|
|
|
33
|
-
<div class="relative inline-block {className}">
|
|
34
|
-
|
|
116
|
+
<div class="relative inline-block {classes?.root ?? className}" bind:this={menuEl}>
|
|
117
|
+
<!-- Trigger -->
|
|
118
|
+
<button type="button" onclick={toggle} class="cursor-pointer bg-transparent border-none p-0 m-0">
|
|
35
119
|
{@render trigger()}
|
|
36
120
|
</button>
|
|
37
121
|
|
|
122
|
+
<!-- Menu -->
|
|
38
123
|
{#if open}
|
|
39
|
-
<div
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
124
|
+
<div
|
|
125
|
+
class="absolute z-50 {posClass} {alignClass} rounded-xl overflow-hidden {classes?.menu ?? ''}"
|
|
126
|
+
style="min-width:{width};background:var(--karbon-bg-card);border:1px solid var(--karbon-border);box-shadow:0 10px 40px rgba(0,0,0,0.25);animation:karbon-dropdown-in 0.15s cubic-bezier(0.16,1,0.3,1);"
|
|
127
|
+
role="menu"
|
|
128
|
+
>
|
|
129
|
+
<!-- Search -->
|
|
130
|
+
{#if searchable}
|
|
131
|
+
<div class="p-2 {classes?.search ?? ''}" style="border-bottom:1px solid var(--karbon-border);">
|
|
132
|
+
<div class="relative">
|
|
133
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-2.5 top-1/2 -translate-y-1/2" style="color:var(--karbon-text-4);"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
134
|
+
<input
|
|
135
|
+
type="text"
|
|
136
|
+
bind:value={search}
|
|
137
|
+
placeholder={searchPlaceholder}
|
|
138
|
+
class="w-full pl-8 pr-3 py-1.5 rounded-lg text-xs outline-none"
|
|
139
|
+
style="background:var(--karbon-bg-input);border:1px solid var(--karbon-border-input);color:var(--karbon-text);"
|
|
140
|
+
onclick={(e) => e.stopPropagation()}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
145
|
+
|
|
146
|
+
<!-- Items -->
|
|
147
|
+
<div class="py-1 max-h-72 overflow-y-auto">
|
|
148
|
+
{#if filteredItems.length === 0}
|
|
149
|
+
<div class="px-3 py-4 text-center text-xs" style="color:var(--karbon-text-4);">Aucun resultat</div>
|
|
44
150
|
{:else}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
151
|
+
{#each groupedItems as group}
|
|
152
|
+
{#if group.label}
|
|
153
|
+
<div class="px-3 pt-2 pb-1 {classes?.group ?? ''}">
|
|
154
|
+
<span class="text-[10px] font-semibold uppercase tracking-wider" style="color:var(--karbon-text-4);">{group.label}</span>
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
{#each group.items as item}
|
|
158
|
+
{#if item.divider}
|
|
159
|
+
<div class="my-1" style="border-top:1px solid var(--karbon-border);"></div>
|
|
160
|
+
{:else}
|
|
161
|
+
{@const isFocused = focusIndex >= 0 && filteredItems.filter(i => !i.divider && !i.disabled).indexOf(item) === focusIndex}
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onclick={(e) => { e.stopPropagation(); select(item) }}
|
|
165
|
+
disabled={item.disabled}
|
|
166
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-left transition-colors
|
|
167
|
+
disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer {classes?.item ?? ''}"
|
|
168
|
+
style="color:{item.danger ? 'var(--karbon-red-400)' : 'var(--karbon-text)'};background:{isFocused ? 'var(--karbon-nav-hover-bg)' : 'transparent'};"
|
|
169
|
+
onmouseenter={(e) => { (e.currentTarget as HTMLElement).style.background = item.danger ? 'color-mix(in srgb, var(--karbon-red-500) 8%, transparent)' : 'var(--karbon-nav-hover-bg)'; focusIndex = -1 }}
|
|
170
|
+
onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent' }}
|
|
171
|
+
role="menuitem"
|
|
172
|
+
>
|
|
173
|
+
{#if item.icon}
|
|
174
|
+
<span class="shrink-0 opacity-60">{@html sanitizeSvg(item.icon)}</span>
|
|
175
|
+
{/if}
|
|
176
|
+
<div class="flex-1 min-w-0">
|
|
177
|
+
<span class="block">{item.label}</span>
|
|
178
|
+
{#if item.description}
|
|
179
|
+
<span class="block text-[11px] mt-0.5" style="color:var(--karbon-text-4);">{item.description}</span>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
{#if item.badge}
|
|
183
|
+
<span
|
|
184
|
+
class="shrink-0 rounded-full px-1.5 py-px text-[10px] font-medium"
|
|
185
|
+
style="background:color-mix(in srgb,{accent} 15%,transparent);color:{accent};"
|
|
186
|
+
>{item.badge}</span>
|
|
187
|
+
{/if}
|
|
188
|
+
</button>
|
|
189
|
+
{/if}
|
|
190
|
+
{/each}
|
|
191
|
+
{/each}
|
|
55
192
|
{/if}
|
|
56
|
-
|
|
193
|
+
</div>
|
|
57
194
|
</div>
|
|
58
195
|
{/if}
|
|
59
196
|
</div>
|
|
197
|
+
|
|
198
|
+
<style>
|
|
199
|
+
@keyframes karbon-dropdown-in {
|
|
200
|
+
from { opacity: 0; transform: translateY(-4px) scale(0.98); }
|
|
201
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
202
|
+
}
|
|
203
|
+
</style>
|