@karbonjs/ui-svelte 0.1.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +30 -0
  3. package/src/accordion/Accordion.svelte +63 -0
  4. package/src/alert/AlertMessage.svelte +44 -0
  5. package/src/avatar/Avatar.svelte +48 -0
  6. package/src/badge/Badge.svelte +24 -0
  7. package/src/breadcrumb/Breadcrumb.svelte +34 -0
  8. package/src/button/Button.svelte +89 -0
  9. package/src/carousel/Carousel.svelte +118 -0
  10. package/src/data/DataTable.svelte +18 -0
  11. package/src/data/Pagination.svelte +45 -0
  12. package/src/divider/Divider.svelte +27 -0
  13. package/src/dropdown/Dropdown.svelte +61 -0
  14. package/src/form/Checkbox.svelte +51 -0
  15. package/src/form/ColorPicker.svelte +95 -0
  16. package/src/form/DatePicker.svelte +196 -0
  17. package/src/form/FormInput.svelte +174 -0
  18. package/src/form/Radio.svelte +54 -0
  19. package/src/form/Select.svelte +73 -0
  20. package/src/form/Slider.svelte +74 -0
  21. package/src/form/Textarea.svelte +86 -0
  22. package/src/form/Toggle.svelte +55 -0
  23. package/src/image/Image.svelte +89 -0
  24. package/src/image/ImgZoom.svelte +96 -0
  25. package/src/index.ts +71 -0
  26. package/src/kbd/Kbd.svelte +19 -0
  27. package/src/layout/Card.svelte +67 -0
  28. package/src/layout/EmptyState.svelte +25 -0
  29. package/src/layout/PageHeader.svelte +27 -0
  30. package/src/overlay/Dialog.svelte +135 -0
  31. package/src/overlay/ImgBox.svelte +174 -0
  32. package/src/overlay/Modal.svelte +98 -0
  33. package/src/overlay/Toast.svelte +92 -0
  34. package/src/progress/Progress.svelte +50 -0
  35. package/src/skeleton/Skeleton.svelte +50 -0
  36. package/src/tabs/Tabs.svelte +59 -0
  37. package/src/tooltip/Tooltip.svelte +49 -0
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { DialogVariant, OverlayBackdrop } from '@karbonjs/ui-core'
4
+
5
+ interface Props {
6
+ open: boolean
7
+ title: string
8
+ description?: string
9
+ variant?: DialogVariant
10
+ backdrop?: OverlayBackdrop
11
+ confirmLabel?: string
12
+ cancelLabel?: string
13
+ loading?: boolean
14
+ class?: string
15
+ onconfirm: () => void
16
+ oncancel: () => void
17
+ icon?: Snippet
18
+ }
19
+
20
+ let {
21
+ open = $bindable(false),
22
+ title,
23
+ description = '',
24
+ variant = 'info',
25
+ backdrop = 'none',
26
+ confirmLabel = 'Confirmer',
27
+ cancelLabel = 'Annuler',
28
+ loading = false,
29
+ class: className = '',
30
+ onconfirm,
31
+ oncancel,
32
+ icon
33
+ }: Props = $props()
34
+
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'
40
+ }
41
+
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'
47
+ }
48
+
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'
54
+ }
55
+
56
+ function handleKeydown(e: KeyboardEvent) {
57
+ if (e.key === 'Escape') oncancel()
58
+ }
59
+ </script>
60
+
61
+ <svelte:window onkeydown={open ? handleKeydown : undefined} />
62
+
63
+ {#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">
66
+ <!-- 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}
70
+ <div
71
+ class="relative z-10 w-full max-w-md rounded-xl shadow-xl bg-[var(--karbon-bg-card,#fff)] p-6 {className}"
72
+ role="alertdialog"
73
+ aria-modal="true"
74
+ aria-labelledby="karbon-dialog-title"
75
+ aria-describedby={description ? 'karbon-dialog-desc' : undefined}
76
+ >
77
+ <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}
95
+
96
+ <h3 id="karbon-dialog-title" class="text-lg font-semibold text-[var(--karbon-text,#1a1635)]">
97
+ {title}
98
+ </h3>
99
+
100
+ {#if description}
101
+ <p id="karbon-dialog-desc" class="mt-2 text-sm text-[var(--karbon-text-3,#8e8aae)]">
102
+ {description}
103
+ </p>
104
+ {/if}
105
+ </div>
106
+
107
+ <div class="mt-6 flex gap-3">
108
+ <button
109
+ onclick={oncancel}
110
+ 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"
116
+ >
117
+ {cancelLabel}
118
+ </button>
119
+ <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
125
+ inline-flex items-center justify-center gap-2"
126
+ >
127
+ {#if loading}
128
+ <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>
129
+ {/if}
130
+ {confirmLabel}
131
+ </button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ {/if}
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import type { OverlayBackdrop } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ images: string[]
6
+ index?: number
7
+ open: boolean
8
+ backdrop?: OverlayBackdrop
9
+ captions?: string[]
10
+ class?: string
11
+ onclose: () => void
12
+ }
13
+
14
+ let {
15
+ images,
16
+ index = $bindable(0),
17
+ open = $bindable(false),
18
+ backdrop = 'dark',
19
+ captions = [],
20
+ class: className = '',
21
+ onclose
22
+ }: Props = $props()
23
+
24
+ let scale = $state(1)
25
+ let translateX = $state(0)
26
+ let translateY = $state(0)
27
+ let dragging = $state(false)
28
+ let startX = 0
29
+ let startY = 0
30
+
31
+ const backdropClasses: Record<string, string> = {
32
+ blur: 'bg-black/80 backdrop-blur-md',
33
+ dark: 'bg-black/90',
34
+ transparent: 'bg-transparent',
35
+ none: ''
36
+ }
37
+
38
+ const hasPrev = $derived(index > 0)
39
+ const hasNext = $derived(index < images.length - 1)
40
+ const caption = $derived(captions[index] ?? '')
41
+
42
+ function prev() {
43
+ if (hasPrev) { index--; resetTransform() }
44
+ }
45
+
46
+ function next() {
47
+ if (hasNext) { index++; resetTransform() }
48
+ }
49
+
50
+ function zoomIn() {
51
+ scale = Math.min(scale + 0.5, 4)
52
+ }
53
+
54
+ function zoomOut() {
55
+ scale = Math.max(scale - 0.5, 0.5)
56
+ if (scale <= 1) resetTransform()
57
+ }
58
+
59
+ function resetTransform() {
60
+ scale = 1
61
+ translateX = 0
62
+ translateY = 0
63
+ }
64
+
65
+ function handleKeydown(e: KeyboardEvent) {
66
+ if (!open) return
67
+ if (e.key === 'Escape') onclose()
68
+ if (e.key === 'ArrowLeft') prev()
69
+ if (e.key === 'ArrowRight') next()
70
+ if (e.key === '+' || e.key === '=') zoomIn()
71
+ if (e.key === '-') zoomOut()
72
+ }
73
+
74
+ function handleMouseDown(e: MouseEvent) {
75
+ if (scale <= 1) return
76
+ dragging = true
77
+ startX = e.clientX - translateX
78
+ startY = e.clientY - translateY
79
+ }
80
+
81
+ function handleMouseMove(e: MouseEvent) {
82
+ if (!dragging) return
83
+ translateX = e.clientX - startX
84
+ translateY = e.clientY - startY
85
+ }
86
+
87
+ function handleMouseUp() {
88
+ dragging = false
89
+ }
90
+
91
+ function handleWheel(e: WheelEvent) {
92
+ e.preventDefault()
93
+ if (e.deltaY < 0) zoomIn()
94
+ else zoomOut()
95
+ }
96
+ </script>
97
+
98
+ <svelte:window onkeydown={handleKeydown} onmouseup={handleMouseUp} onmousemove={handleMouseMove} />
99
+
100
+ {#if open && images.length > 0}
101
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
102
+ <div class="fixed inset-0 z-[70] flex items-center justify-center {backdropClasses[backdrop]} {className}">
103
+ <!-- Close button -->
104
+ <button
105
+ onclick={onclose}
106
+ aria-label="Fermer"
107
+ class="absolute top-4 right-4 z-10 rounded-full p-2 text-white/60 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
108
+ >
109
+ <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="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
110
+ </button>
111
+
112
+ <!-- Zoom controls -->
113
+ <div class="absolute top-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2 bg-black/40 rounded-full px-3 py-1.5">
114
+ <button onclick={zoomOut} aria-label="Dézoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
115
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
116
+ </button>
117
+ <span class="text-white/80 text-xs font-medium min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
118
+ <button onclick={zoomIn} aria-label="Zoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
119
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/></svg>
120
+ </button>
121
+ </div>
122
+
123
+ <!-- Prev -->
124
+ {#if hasPrev}
125
+ <button
126
+ onclick={prev}
127
+ aria-label="Image précédente"
128
+ class="absolute left-4 z-10 rounded-full p-2 text-white/60 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
129
+ >
130
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
131
+ </button>
132
+ {/if}
133
+
134
+ <!-- Next -->
135
+ {#if hasNext}
136
+ <button
137
+ onclick={next}
138
+ aria-label="Image suivante"
139
+ class="absolute right-4 z-10 rounded-full p-2 text-white/60 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
140
+ >
141
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
142
+ </button>
143
+ {/if}
144
+
145
+ <!-- Image -->
146
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
147
+ <div
148
+ class="flex items-center justify-center w-full h-full p-16 {scale > 1 ? 'cursor-grab' : ''} {dragging ? 'cursor-grabbing' : ''}"
149
+ onclick={(e) => { if (e.target === e.currentTarget && scale <= 1) onclose() }}
150
+ onmousedown={handleMouseDown}
151
+ onwheel={handleWheel}
152
+ >
153
+ <img
154
+ src={images[index]}
155
+ alt={caption || `Image ${index + 1}`}
156
+ class="max-w-full max-h-full object-contain select-none transition-transform duration-150"
157
+ style="transform: scale({scale}) translate({translateX / scale}px, {translateY / scale}px)"
158
+ draggable="false"
159
+ />
160
+ </div>
161
+
162
+ <!-- Caption + counter -->
163
+ {#if caption || images.length > 1}
164
+ <div class="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 text-center">
165
+ {#if caption}
166
+ <p class="text-white/80 text-sm mb-1">{caption}</p>
167
+ {/if}
168
+ {#if images.length > 1}
169
+ <span class="text-white/40 text-xs">{index + 1} / {images.length}</span>
170
+ {/if}
171
+ </div>
172
+ {/if}
173
+ </div>
174
+ {/if}
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { ModalSize, OverlayBackdrop } from '@karbonjs/ui-core'
4
+
5
+ interface Props {
6
+ open: boolean
7
+ title?: string
8
+ size?: ModalSize
9
+ backdrop?: OverlayBackdrop
10
+ closable?: boolean
11
+ closeOnOverlay?: boolean
12
+ class?: string
13
+ onclose: () => void
14
+ children: Snippet
15
+ footer?: Snippet
16
+ }
17
+
18
+ let {
19
+ open = $bindable(false),
20
+ title = '',
21
+ size = 'md',
22
+ backdrop = 'none',
23
+ closable = true,
24
+ closeOnOverlay = true,
25
+ class: className = '',
26
+ onclose,
27
+ children,
28
+ footer
29
+ }: Props = $props()
30
+
31
+ const sizeClasses: Record<string, string> = {
32
+ sm: 'max-w-sm',
33
+ md: 'max-w-lg',
34
+ lg: 'max-w-2xl',
35
+ xl: 'max-w-4xl',
36
+ full: 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]'
37
+ }
38
+
39
+ const backdropClasses: Record<string, string> = {
40
+ blur: 'bg-black/30 backdrop-blur-sm',
41
+ dark: 'bg-black/50',
42
+ transparent: 'bg-transparent',
43
+ none: 'hidden'
44
+ }
45
+
46
+ function handleOverlayClick() {
47
+ if (closeOnOverlay) onclose()
48
+ }
49
+
50
+ function handleKeydown(e: KeyboardEvent) {
51
+ if (e.key === 'Escape' && closable) onclose()
52
+ }
53
+ </script>
54
+
55
+ <svelte:window onkeydown={handleKeydown} />
56
+
57
+ {#if open}
58
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
59
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
60
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
61
+ {#if backdrop !== 'none'}
62
+ <div class="fixed inset-0 {backdropClasses[backdrop]}" onclick={handleOverlayClick}></div>
63
+ {/if}
64
+ <div
65
+ class="relative z-10 w-full {sizeClasses[size]} rounded-xl shadow-xl bg-[var(--karbon-bg-card,#fff)] flex flex-col {className}"
66
+ role="dialog"
67
+ aria-modal="true"
68
+ aria-label={title || undefined}
69
+ >
70
+ {#if title || closable}
71
+ <div class="flex items-center justify-between px-6 pt-6 {title ? 'mb-4' : ''}">
72
+ {#if title}
73
+ <h3 class="text-lg font-semibold text-[var(--karbon-text,#1a1635)]">{title}</h3>
74
+ {/if}
75
+ {#if closable}
76
+ <button
77
+ onclick={onclose}
78
+ aria-label="Fermer"
79
+ class="rounded-lg p-1 ml-auto text-[var(--karbon-text-4,#b5b2cc)] transition-colors duration-150 hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] hover:text-[var(--karbon-text-2,#5a567e)]"
80
+ >
81
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
82
+ </button>
83
+ {/if}
84
+ </div>
85
+ {/if}
86
+
87
+ <div class="px-6 {footer ? 'pb-4' : 'pb-6'} {!title && !closable ? 'pt-6' : ''} overflow-y-auto">
88
+ {@render children()}
89
+ </div>
90
+
91
+ {#if footer}
92
+ <div class="px-6 pb-6 pt-2 flex items-center justify-end gap-3">
93
+ {@render footer()}
94
+ </div>
95
+ {/if}
96
+ </div>
97
+ </div>
98
+ {/if}
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ import type { ToastVariant, ToastPosition } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ message: string
6
+ variant?: ToastVariant
7
+ duration?: number
8
+ dismissible?: boolean
9
+ position?: ToastPosition
10
+ visible?: boolean
11
+ class?: string
12
+ ondismiss?: () => void
13
+ }
14
+
15
+ let {
16
+ message,
17
+ variant = 'info',
18
+ duration = 4000,
19
+ dismissible = true,
20
+ position = 'top-right',
21
+ visible = $bindable(true),
22
+ class: className = '',
23
+ ondismiss
24
+ }: Props = $props()
25
+
26
+ const variantClasses: Record<string, string> = {
27
+ success: 'border-emerald-500/20 text-emerald-400',
28
+ error: 'border-red-500/20 text-red-400',
29
+ warning: 'border-amber-500/20 text-amber-300',
30
+ info: 'border-blue-500/20 text-blue-400'
31
+ }
32
+
33
+ const positionClasses: Record<string, string> = {
34
+ 'top-right': 'top-4 right-4',
35
+ 'top-left': 'top-4 left-4',
36
+ 'top-center': 'top-4 left-1/2 -translate-x-1/2',
37
+ 'bottom-right': 'bottom-4 right-4',
38
+ 'bottom-left': 'bottom-4 left-4',
39
+ 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2'
40
+ }
41
+
42
+ function dismiss() {
43
+ visible = false
44
+ ondismiss?.()
45
+ }
46
+
47
+ $effect(() => {
48
+ if (visible && duration > 0) {
49
+ const timer = setTimeout(dismiss, duration)
50
+ return () => clearTimeout(timer)
51
+ }
52
+ })
53
+ </script>
54
+
55
+ {#if visible}
56
+ <div
57
+ class="fixed z-[60] {positionClasses[position]} w-full max-w-sm"
58
+ role="alert"
59
+ aria-live="assertive"
60
+ >
61
+ <div
62
+ class="
63
+ flex items-start gap-3 px-4 py-3 rounded-xl border shadow-lg
64
+ bg-[var(--karbon-bg-card,#fff)]
65
+ {variantClasses[variant]}
66
+ {className}
67
+ "
68
+ >
69
+ {#if variant === 'success'}
70
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>
71
+ {:else if variant === 'error'}
72
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><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>
73
+ {:else if variant === 'warning'}
74
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><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>
75
+ {:else}
76
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 mt-0.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
77
+ {/if}
78
+
79
+ <span class="flex-1 text-sm font-medium text-[var(--karbon-text,#1a1635)]">{message}</span>
80
+
81
+ {#if dismissible}
82
+ <button
83
+ onclick={dismiss}
84
+ aria-label="Fermer"
85
+ class="shrink-0 rounded-lg p-0.5 text-[var(--karbon-text-4,#b5b2cc)] transition-colors duration-150 hover:text-[var(--karbon-text-2,#5a567e)] cursor-pointer"
86
+ >
87
+ <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"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
88
+ </button>
89
+ {/if}
90
+ </div>
91
+ </div>
92
+ {/if}
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { ProgressVariant, ProgressSize } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ value: number
6
+ max?: number
7
+ variant?: ProgressVariant
8
+ size?: ProgressSize
9
+ showLabel?: boolean
10
+ class?: string
11
+ }
12
+
13
+ let {
14
+ value,
15
+ max = 100,
16
+ variant = 'primary',
17
+ size = 'md',
18
+ showLabel = false,
19
+ class: className = ''
20
+ }: Props = $props()
21
+
22
+ const percent = $derived(Math.min(Math.max((value / max) * 100, 0), 100))
23
+
24
+ const variantClasses: Record<string, string> = {
25
+ primary: 'bg-[var(--karbon-primary)]',
26
+ success: 'bg-emerald-500',
27
+ warning: 'bg-amber-500',
28
+ danger: 'bg-red-500'
29
+ }
30
+
31
+ const sizeClasses: Record<string, string> = {
32
+ sm: 'h-1',
33
+ md: 'h-2',
34
+ lg: 'h-3'
35
+ }
36
+ </script>
37
+
38
+ <div class={className}>
39
+ {#if showLabel}
40
+ <div class="flex items-center justify-between mb-1">
41
+ <span class="text-xs font-medium text-[var(--karbon-text-2,#5a567e)]">{Math.round(percent)}%</span>
42
+ </div>
43
+ {/if}
44
+ <div class="w-full rounded-full bg-[var(--karbon-border,rgba(0,0,0,0.07))] overflow-hidden {sizeClasses[size]}" role="progressbar" aria-valuenow={value} aria-valuemax={max}>
45
+ <div
46
+ class="h-full rounded-full transition-all duration-300 ease-out {variantClasses[variant]}"
47
+ style="width: {percent}%"
48
+ ></div>
49
+ </div>
50
+ </div>
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { SkeletonVariant } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ variant?: SkeletonVariant
6
+ width?: string
7
+ height?: string
8
+ lines?: number
9
+ class?: string
10
+ }
11
+
12
+ let {
13
+ variant = 'text',
14
+ width = '100%',
15
+ height,
16
+ lines = 1,
17
+ class: className = ''
18
+ }: Props = $props()
19
+
20
+ const baseClass = 'animate-pulse bg-[var(--karbon-border,rgba(0,0,0,0.07))]'
21
+
22
+ const variantDefaults: Record<string, { h: string; rounded: string }> = {
23
+ text: { h: '0.875rem', rounded: 'rounded' },
24
+ circle: { h: '3rem', rounded: 'rounded-full' },
25
+ rect: { h: '8rem', rounded: 'rounded-lg' }
26
+ }
27
+
28
+ const v = $derived(variantDefaults[variant])
29
+ </script>
30
+
31
+ {#if variant === 'text' && lines > 1}
32
+ <div class="space-y-2 {className}">
33
+ {#each Array(lines) as _, i}
34
+ <div
35
+ class="{baseClass} {v.rounded}"
36
+ style="width: {i === lines - 1 ? '66%' : width}; height: {height ?? v.h}"
37
+ ></div>
38
+ {/each}
39
+ </div>
40
+ {:else if variant === 'circle'}
41
+ <div
42
+ class="{baseClass} {v.rounded} aspect-square {className}"
43
+ style="width: {width === '100%' ? height ?? v.h : width}; height: {height ?? v.h}"
44
+ ></div>
45
+ {:else}
46
+ <div
47
+ class="{baseClass} {v.rounded} {className}"
48
+ style="width: {width}; height: {height ?? v.h}"
49
+ ></div>
50
+ {/if}
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { TabItem } from '@karbonjs/ui-core'
4
+
5
+ interface Props {
6
+ tabs: TabItem[]
7
+ active?: string
8
+ class?: string
9
+ onchange?: (id: string) => void
10
+ children?: Snippet<[{ tab: TabItem }]>
11
+ }
12
+
13
+ let {
14
+ tabs,
15
+ active = $bindable(tabs[0]?.id ?? ''),
16
+ class: className = '',
17
+ onchange,
18
+ children
19
+ }: Props = $props()
20
+
21
+ function select(id: string) {
22
+ active = id
23
+ onchange?.(id)
24
+ }
25
+ </script>
26
+
27
+ <div class={className}>
28
+ <div class="flex border-b border-[var(--karbon-border,rgba(0,0,0,0.07))]" role="tablist">
29
+ {#each tabs as tab}
30
+ <button
31
+ type="button"
32
+ role="tab"
33
+ aria-selected={active === tab.id}
34
+ onclick={() => { if (!tab.disabled) select(tab.id) }}
35
+ disabled={tab.disabled}
36
+ class="px-4 py-2.5 text-sm font-medium transition-colors relative cursor-pointer
37
+ disabled:opacity-40 disabled:cursor-not-allowed
38
+ {active === tab.id
39
+ ? 'text-[var(--karbon-primary)]'
40
+ : 'text-[var(--karbon-text-3,#8e8aae)] hover:text-[var(--karbon-text,#1a1635)]'}"
41
+ >
42
+ {tab.label}
43
+ {#if active === tab.id}
44
+ <span class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--karbon-primary)]"></span>
45
+ {/if}
46
+ </button>
47
+ {/each}
48
+ </div>
49
+
50
+ {#if children}
51
+ {#each tabs as tab}
52
+ {#if active === tab.id}
53
+ <div class="pt-4" role="tabpanel">
54
+ {@render children({ tab })}
55
+ </div>
56
+ {/if}
57
+ {/each}
58
+ {/if}
59
+ </div>