@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.
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 +321 -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 +862 -108
  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
@@ -8,6 +8,7 @@
8
8
  backdrop?: OverlayBackdrop
9
9
  captions?: string[]
10
10
  class?: string
11
+ classes?: { root?: string, backdrop?: string, image?: string }
11
12
  onclose: () => void
12
13
  }
13
14
 
@@ -18,6 +19,7 @@
18
19
  backdrop = 'blur',
19
20
  captions = [],
20
21
  class: className = '',
22
+ classes = {},
21
23
  onclose
22
24
  }: Props = $props()
23
25
 
@@ -27,6 +29,8 @@
27
29
  let dragging = $state(false)
28
30
  let visible = $state(false)
29
31
  let portal: HTMLDivElement | null = $state(null)
32
+ let transitioning = $state(false)
33
+ let showThumbs = $state(false)
30
34
 
31
35
  const backdropClasses: Record<string, string> = {
32
36
  blur: 'bg-black/70 backdrop-blur-xl',
@@ -58,15 +62,32 @@
58
62
 
59
63
  function close() {
60
64
  visible = false
61
- setTimeout(() => onclose(), 200)
65
+ setTimeout(() => onclose(), 350)
62
66
  }
63
67
 
64
68
  function prev() {
65
- if (hasPrev) { index--; resetTransform() }
69
+ if (hasPrev && !transitioning) {
70
+ transitioning = true
71
+ setTimeout(() => { index--; resetTransform(); transitioning = false }, 150)
72
+ }
66
73
  }
67
74
 
68
75
  function next() {
69
- if (hasNext) { index++; resetTransform() }
76
+ if (hasNext && !transitioning) {
77
+ transitioning = true
78
+ setTimeout(() => { index++; resetTransform(); transitioning = false }, 150)
79
+ }
80
+ }
81
+
82
+ function goTo(i: number) {
83
+ if (i === index || transitioning) return
84
+ transitioning = true
85
+ setTimeout(() => { index = i; resetTransform(); transitioning = false }, 150)
86
+ }
87
+
88
+ function handleDblClick() {
89
+ if (scale > 1) resetTransform()
90
+ else scale = 2.5
70
91
  }
71
92
 
72
93
  function zoomIn() {
@@ -128,21 +149,22 @@
128
149
  <div
129
150
  use:teleport
130
151
  data-imgbox-root
131
- class="imgbox-root {className}"
152
+ class="imgbox-root {classes?.root ?? className}"
132
153
  style="opacity: {visible ? 1 : 0};"
133
154
  >
134
- <!-- Backdrop — click here to close, captures all stray events -->
155
+ <!-- Backdrop -->
135
156
  <div
136
- class="imgbox-backdrop {backdropClasses[backdrop]}"
157
+ class="imgbox-backdrop {backdropClasses[backdrop]} {classes?.backdrop ?? ''}"
137
158
  onclick={() => { if (scale <= 1) close() }}
138
159
  role="presentation"
139
160
  ></div>
140
161
 
141
- <!-- Close button — top right -->
162
+ <!-- Close button -->
142
163
  <button
143
164
  onclick={close}
144
165
  aria-label="Fermer"
145
166
  class="imgbox-close"
167
+ style="opacity:{visible ? 1 : 0};transition:opacity 0.3s ease 0.15s,background 0.15s ease,color 0.15s ease;"
146
168
  >
147
169
  <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" 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>
148
170
  </button>
@@ -151,8 +173,9 @@
151
173
  {#if hasPrev}
152
174
  <button
153
175
  onclick={prev}
154
- aria-label="Image précédente"
176
+ aria-label="Image precedente"
155
177
  class="imgbox-nav imgbox-nav-prev"
178
+ style="opacity:{visible ? 1 : 0};transition:opacity 0.3s ease 0.2s,background 0.15s ease,color 0.15s ease;"
156
179
  >
157
180
  <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="m15 18-6-6 6-6"/></svg>
158
181
  </button>
@@ -164,6 +187,7 @@
164
187
  onclick={next}
165
188
  aria-label="Image suivante"
166
189
  class="imgbox-nav imgbox-nav-next"
190
+ style="opacity:{visible ? 1 : 0};transition:opacity 0.3s ease 0.2s,background 0.15s ease,color 0.15s ease;"
167
191
  >
168
192
  <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="m9 18 6-6-6-6"/></svg>
169
193
  </button>
@@ -179,30 +203,69 @@
179
203
  <img
180
204
  src={images[index]}
181
205
  alt={caption || `Image ${index + 1}`}
182
- class="imgbox-image"
183
- style="transform: scale({visible ? scale : 0.9}) translate({translateX / scale}px, {translateY / scale}px); opacity: {visible ? 1 : 0};"
206
+ class="imgbox-image {classes?.image ?? ''}"
207
+ style="
208
+ transform: scale({visible ? scale : 0.7}) translate({translateX / scale}px, {translateY / scale}px);
209
+ opacity: {visible && !transitioning ? 1 : 0};
210
+ filter: {visible ? 'blur(0)' : 'blur(8px)'};
211
+ transition: transform 0.35s cubic-bezier(0.16,1,0.3,1), opacity 0.3s ease, filter 0.3s ease;
212
+ "
184
213
  draggable="false"
214
+ ondblclick={handleDblClick}
185
215
  />
186
216
  </div>
187
217
 
188
- <!-- Counter -->
218
+ <!-- Caption -->
219
+ {#if caption}
220
+ <div class="imgbox-caption">
221
+ <span style="background:rgba(0,0,0,0.5);color:rgba(255,255,255,0.85);backdrop-filter:blur(8px);padding:6px 14px;border-radius:8px;font-size:13px;">{caption}</span>
222
+ </div>
223
+ {/if}
224
+
225
+ <!-- Counter + thumbnails toggle -->
189
226
  {#if images.length > 1}
190
227
  <div class="imgbox-counter">
191
- <span class="text-white/40 text-xs bg-black/30 rounded-full px-2 py-0.5">{index + 1} / {images.length}</span>
228
+ <div style="display:flex;align-items:center;gap:8px;background:rgba(0,0,0,0.4);border-radius:9999px;padding:4px 12px;backdrop-filter:blur(8px);">
229
+ <span style="color:rgba(255,255,255,0.5);font-size:12px;">{index + 1} / {images.length}</span>
230
+ <button
231
+ onclick={() => showThumbs = !showThumbs}
232
+ style="color:rgba(255,255,255,{showThumbs ? '0.9' : '0.4'});cursor:pointer;background:none;border:none;padding:2px;"
233
+ aria-label="Afficher les miniatures"
234
+ >
235
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/></svg>
236
+ </button>
237
+ </div>
192
238
  </div>
193
239
  {/if}
194
240
 
195
- <!-- Zoom controls — bottom center, full-width wrapper -->
241
+ <!-- Thumbnails strip -->
242
+ {#if showThumbs && images.length > 1}
243
+ <div class="imgbox-thumbs">
244
+ <div style="display:flex;gap:6px;padding:8px 12px;background:rgba(0,0,0,0.5);border-radius:12px;backdrop-filter:blur(12px);">
245
+ {#each images as img, i}
246
+ <button
247
+ onclick={() => goTo(i)}
248
+ style="width:48px;height:36px;border-radius:6px;overflow:hidden;border:{i === index ? '2px solid white' : '2px solid transparent'};opacity:{i === index ? 1 : 0.5};cursor:pointer;padding:0;transition:all 0.15s ease;flex-shrink:0;"
249
+ aria-label="Image {i + 1}"
250
+ >
251
+ <img src={img} alt="" style="width:100%;height:100%;object-fit:cover;" draggable="false" />
252
+ </button>
253
+ {/each}
254
+ </div>
255
+ </div>
256
+ {/if}
257
+
258
+ <!-- Zoom controls -->
196
259
  <div class="imgbox-controls" style="opacity: {visible ? 1 : 0};">
197
260
  <div class="flex items-center gap-1 bg-black/40 rounded-full px-3 py-1.5">
198
- <button onclick={zoomOut} aria-label="Dézoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
261
+ <button onclick={zoomOut} aria-label="Dezoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
199
262
  <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"><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>
200
263
  </button>
201
264
  <span class="text-white/80 text-xs font-medium min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
202
265
  <button onclick={zoomIn} aria-label="Zoomer" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1">
203
266
  <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"><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>
204
267
  </button>
205
- <button onclick={resetTransform} aria-label="Réinitialiser" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1 ml-1 border-l border-white/20 pl-2">
268
+ <button onclick={resetTransform} aria-label="Reinitialiser" class="text-white/60 hover:text-white transition-colors cursor-pointer p-1 ml-1 border-l border-white/20 pl-2">
206
269
  <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="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
207
270
  </button>
208
271
  </div>
@@ -219,7 +282,7 @@
219
282
  align-items: center;
220
283
  justify-content: center;
221
284
  pointer-events: auto;
222
- transition: opacity 0.2s ease;
285
+ transition: opacity 0.3s ease;
223
286
  }
224
287
 
225
288
  .imgbox-backdrop {
@@ -228,10 +291,16 @@
228
291
  z-index: 0;
229
292
  pointer-events: auto;
230
293
  cursor: default;
294
+ transition: opacity 0.4s ease;
231
295
  }
232
296
 
233
297
  .imgbox-close {
234
- @apply rounded-full p-2.5 text-white/70 bg-black/30 transition-all cursor-pointer;
298
+ border-radius: 9999px;
299
+ padding: 0.625rem;
300
+ color: rgba(255, 255, 255, 0.7);
301
+ background: rgba(0, 0, 0, 0.3);
302
+ transition: all 0.15s ease;
303
+ cursor: pointer;
235
304
  position: absolute;
236
305
  top: 16px;
237
306
  right: 16px;
@@ -241,11 +310,17 @@
241
310
  }
242
311
 
243
312
  .imgbox-close:hover {
244
- @apply text-white bg-black/50;
313
+ color: white;
314
+ background: rgba(0, 0, 0, 0.5);
245
315
  }
246
316
 
247
317
  .imgbox-nav {
248
- @apply rounded-full p-3 text-white/60 bg-black/20 transition-all cursor-pointer;
318
+ border-radius: 9999px;
319
+ padding: 0.75rem;
320
+ color: rgba(255, 255, 255, 0.6);
321
+ background: rgba(0, 0, 0, 0.2);
322
+ transition: all 0.15s ease;
323
+ cursor: pointer;
249
324
  position: absolute;
250
325
  top: 50%;
251
326
  transform: translateY(-50%);
@@ -255,7 +330,8 @@
255
330
  }
256
331
 
257
332
  .imgbox-nav:hover {
258
- @apply text-white bg-black/50;
333
+ color: white;
334
+ background: rgba(0, 0, 0, 0.5);
259
335
  }
260
336
 
261
337
  .imgbox-nav-prev { left: 16px; }
@@ -282,7 +358,29 @@
282
358
  pointer-events: none;
283
359
  }
284
360
 
361
+ .imgbox-caption {
362
+ position: absolute;
363
+ top: 16px;
364
+ left: 0;
365
+ right: 0;
366
+ display: flex;
367
+ justify-content: center;
368
+ z-index: 10;
369
+ pointer-events: none;
370
+ }
371
+
285
372
  .imgbox-counter {
373
+ position: absolute;
374
+ bottom: 80px;
375
+ left: 0;
376
+ right: 0;
377
+ display: flex;
378
+ justify-content: center;
379
+ z-index: 10;
380
+ pointer-events: auto;
381
+ }
382
+
383
+ .imgbox-thumbs {
286
384
  position: absolute;
287
385
  bottom: 56px;
288
386
  left: 0;
@@ -290,7 +388,13 @@
290
388
  display: flex;
291
389
  justify-content: center;
292
390
  z-index: 10;
293
- pointer-events: none;
391
+ pointer-events: auto;
392
+ animation: karbon-imgbox-thumbs-in 0.2s ease;
393
+ }
394
+
395
+ @keyframes karbon-imgbox-thumbs-in {
396
+ from { opacity: 0; transform: translateY(8px); }
397
+ to { opacity: 1; transform: translateY(0); }
294
398
  }
295
399
 
296
400
  .imgbox-controls {
@@ -1,16 +1,21 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
- import type { ModalSize, OverlayBackdrop } from '@karbonjs/ui-core'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
4
 
5
5
  interface Props {
6
6
  open: boolean
7
7
  title?: string
8
- size?: ModalSize
9
- backdrop?: OverlayBackdrop
8
+ description?: string
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
10
+ position?: 'center' | 'top' | 'right' | 'bottom'
11
+ backdrop?: 'blur' | 'dark' | 'light' | 'transparent'
10
12
  closable?: boolean
11
13
  closeOnOverlay?: boolean
14
+ color?: ButtonColor
12
15
  class?: string
16
+ classes?: { overlay?: string, content?: string, header?: string, body?: string, footer?: string }
13
17
  onclose: () => void
18
+ icon?: Snippet
14
19
  children: Snippet
15
20
  footer?: Snippet
16
21
  }
@@ -18,78 +23,155 @@
18
23
  let {
19
24
  open = $bindable(false),
20
25
  title = '',
26
+ description = '',
21
27
  size = 'md',
22
- backdrop = 'none',
28
+ position = 'center',
29
+ backdrop = 'blur',
23
30
  closable = true,
24
31
  closeOnOverlay = true,
32
+ color,
25
33
  class: className = '',
34
+ classes = {},
26
35
  onclose,
36
+ icon,
27
37
  children,
28
38
  footer
29
39
  }: Props = $props()
30
40
 
31
- const sizeClasses: Record<string, string> = {
41
+ let visible = $state(false)
42
+ let contentEl: HTMLDivElement | undefined = $state()
43
+
44
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : '')
45
+
46
+ $effect(() => {
47
+ if (open) {
48
+ requestAnimationFrame(() => { visible = true })
49
+ document.body.style.overflow = 'hidden'
50
+ } else {
51
+ visible = false
52
+ document.body.style.overflow = ''
53
+ }
54
+ return () => { document.body.style.overflow = '' }
55
+ })
56
+
57
+ const sizeMap: Record<string, string> = {
58
+ xs: 'max-w-xs',
32
59
  sm: 'max-w-sm',
33
60
  md: 'max-w-lg',
34
61
  lg: 'max-w-2xl',
35
62
  xl: 'max-w-4xl',
36
- full: 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]'
63
+ full: 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]',
37
64
  }
38
65
 
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'
66
+ const backdropStyles: Record<string, string> = {
67
+ blur: 'background:rgba(0,0,0,0.5);backdrop-filter:blur(8px);',
68
+ dark: 'background:rgba(0,0,0,0.6);',
69
+ light: 'background:rgba(255,255,255,0.4);backdrop-filter:blur(4px);',
70
+ transparent: 'background:transparent;',
71
+ }
72
+
73
+ const positionClasses: Record<string, string> = {
74
+ center: 'items-center justify-center',
75
+ top: 'items-start justify-center pt-16',
76
+ right: 'items-stretch justify-end',
77
+ bottom: 'items-end justify-center pb-4',
78
+ }
79
+
80
+ const contentPosition: Record<string, string> = {
81
+ center: 'rounded-xl',
82
+ top: 'rounded-xl',
83
+ right: 'rounded-l-xl rounded-r-none min-h-full',
84
+ bottom: 'rounded-t-xl rounded-b-none',
85
+ }
86
+
87
+ function close() {
88
+ if (!closable) return
89
+ visible = false
90
+ setTimeout(() => onclose(), 150)
44
91
  }
45
92
 
46
93
  function handleOverlayClick() {
47
- if (closeOnOverlay) onclose()
94
+ if (closeOnOverlay) close()
48
95
  }
49
96
 
50
97
  function handleKeydown(e: KeyboardEvent) {
51
- if (e.key === 'Escape' && closable) onclose()
98
+ if (e.key === 'Escape' && closable && open) close()
52
99
  }
53
100
  </script>
54
101
 
55
102
  <svelte:window onkeydown={handleKeydown} />
56
103
 
57
104
  {#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">
105
+ <div
106
+ class="fixed inset-0 z-50 flex p-4 {positionClasses[position]}"
107
+ style="opacity:{visible ? 1 : 0};transition:opacity 0.15s ease;"
108
+ >
109
+ <!-- Backdrop -->
60
110
  <!-- 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}
111
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
112
+ <div
113
+ class="fixed inset-0 {classes?.overlay ?? ''}"
114
+ style="{backdropStyles[backdrop]}transition:opacity 0.15s ease;opacity:{visible ? 1 : 0};"
115
+ onclick={handleOverlayClick}
116
+ ></div>
117
+
118
+ <!-- Content -->
64
119
  <div
65
- class="relative z-10 w-full {sizeClasses[size]} rounded-xl shadow-xl bg-[var(--karbon-bg-card,#fff)] flex flex-col {className}"
120
+ bind:this={contentEl}
121
+ class="relative z-10 w-full {sizeMap[size]} {contentPosition[position]} flex flex-col {classes?.content ?? className}"
122
+ style="background:var(--karbon-bg-card);box-shadow:0 25px 60px -12px rgba(0,0,0,0.4);border:1px solid var(--karbon-border);
123
+ transform:{visible ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(8px)'};
124
+ transition:transform 0.2s cubic-bezier(0.16,1,0.3,1),opacity 0.15s ease;
125
+ opacity:{visible ? 1 : 0};"
66
126
  role="dialog"
67
127
  aria-modal="true"
68
128
  aria-label={title || undefined}
69
129
  >
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>
130
+ <!-- Header -->
131
+ {#if title || closable || icon || description}
132
+ <div class="flex gap-4 px-6 pt-6 pb-2 {classes?.header ?? ''}">
133
+ {#if icon}
134
+ <div class="shrink-0 mt-0.5">
135
+ {#if color}
136
+ <div class="w-10 h-10 rounded-xl flex items-center justify-center" style="background:color-mix(in srgb,{accent} 12%,transparent);color:{accent};">
137
+ {@render icon()}
138
+ </div>
139
+ {:else}
140
+ {@render icon()}
141
+ {/if}
142
+ </div>
74
143
  {/if}
144
+ <div class="flex-1 min-w-0">
145
+ {#if title}
146
+ <h3 class="text-lg font-semibold" style="color:var(--karbon-text);">{title}</h3>
147
+ {/if}
148
+ {#if description}
149
+ <p class="text-sm mt-0.5" style="color:var(--karbon-text-3);">{description}</p>
150
+ {/if}
151
+ </div>
75
152
  {#if closable}
76
153
  <button
77
- onclick={onclose}
154
+ onclick={close}
78
155
  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)]"
156
+ class="shrink-0 rounded-lg p-1.5 transition-colors cursor-pointer"
157
+ style="color:var(--karbon-text-4);"
158
+ onmouseenter={(e) => { (e.currentTarget as HTMLElement).style.background = 'var(--karbon-nav-hover-bg)'; (e.currentTarget as HTMLElement).style.color = 'var(--karbon-text-2)' }}
159
+ onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent'; (e.currentTarget as HTMLElement).style.color = 'var(--karbon-text-4)' }}
80
160
  >
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>
161
+ <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"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
82
162
  </button>
83
163
  {/if}
84
164
  </div>
85
165
  {/if}
86
166
 
87
- <div class="px-6 {footer ? 'pb-4' : 'pb-6'} {!title && !closable ? 'pt-6' : ''} overflow-y-auto">
167
+ <!-- Body -->
168
+ <div class="px-6 py-4 flex-1 overflow-y-auto text-sm {classes?.body ?? ''}" style="color:var(--karbon-text-2);">
88
169
  {@render children()}
89
170
  </div>
90
171
 
172
+ <!-- Footer -->
91
173
  {#if footer}
92
- <div class="px-6 pb-6 pt-2 flex items-center justify-end gap-3">
174
+ <div class="px-6 py-4 flex items-center justify-end gap-2 {classes?.footer ?? ''}" style="border-top:1px solid var(--karbon-border);">
93
175
  {@render footer()}
94
176
  </div>
95
177
  {/if}