@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.
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,108 @@
1
1
  <script lang="ts">
2
- import type { DividerDirection } 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
- direction?: DividerDirection
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
- class: className = ''
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 class="inline-block w-px self-stretch bg-[var(--karbon-border,rgba(0,0,0,0.07))] {className}"></div>
19
- {:else if label}
20
- <div class="flex items-center gap-3 {className}">
21
- <div class="flex-1 h-px bg-[var(--karbon-border,rgba(0,0,0,0.07))]"></div>
22
- <span class="text-xs text-[var(--karbon-text-4,#b5b2cc)] font-medium shrink-0">{label}</span>
23
- <div class="flex-1 h-px bg-[var(--karbon-border,rgba(0,0,0,0.07))]"></div>
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 class="w-full h-px bg-[var(--karbon-border,rgba(0,0,0,0.07))] {className}"></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 { DropdownEntry, DropdownAlign } from '@karbonjs/ui-core'
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: DropdownEntry[]
7
- align?: DropdownAlign
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, '&lt;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 handleSelect(item: DropdownEntry) {
24
- if ('divider' in item) return
25
- if (item.disabled) return
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={() => open = false} />
114
+ <svelte:window onclick={clickOutside} onkeydown={handleKeydown} />
32
115
 
33
- <div class="relative inline-block {className}">
34
- <button type="button" onclick={(e) => { e.stopPropagation(); open = !open }} class="cursor-pointer bg-transparent border-none p-0 m-0">
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 class="absolute z-50 mt-1 min-w-[12rem] rounded-xl border border-[var(--karbon-border,rgba(0,0,0,0.07))] bg-[var(--karbon-bg-card,#fff)] shadow-xl py-1
40
- {align === 'right' ? 'right-0' : 'left-0'}">
41
- {#each items as item}
42
- {#if 'divider' in item}
43
- <div class="my-1 border-t border-[var(--karbon-border,rgba(0,0,0,0.07))]"></div>
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
- <button
46
- type="button"
47
- onclick={() => handleSelect(item)}
48
- disabled={item.disabled}
49
- class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors
50
- {item.danger ? 'text-red-400 hover:bg-red-500/8' : 'text-[var(--karbon-text,#1a1635)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))]'}
51
- disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
52
- >
53
- {item.label}
54
- </button>
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
- {/each}
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>