@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
@@ -11,6 +11,7 @@
11
11
  fallback?: string
12
12
  imgbox?: boolean
13
13
  class?: string
14
+ classes?: { root?: string, img?: string }
14
15
  onclick?: () => void
15
16
  }
16
17
 
@@ -23,6 +24,7 @@
23
24
  fallback = '',
24
25
  imgbox = false,
25
26
  class: className = '',
27
+ classes = {},
26
28
  onclick
27
29
  }: Props = $props()
28
30
 
@@ -65,12 +67,12 @@
65
67
  </script>
66
68
 
67
69
  {#if isClickable}
68
- <button type="button" onclick={handleClick} class="group overflow-hidden {roundedClasses[rounded]} {aspectClasses[aspect]} cursor-pointer bg-transparent border-none p-0 m-0 block {className}">
69
- <img src={imgSrc} {alt} onerror={handleError} class="w-full h-full object-cover transition-all duration-300 {hoverClasses[hover]}" loading="lazy" />
70
+ <button type="button" onclick={handleClick} class="group overflow-hidden {roundedClasses[rounded]} {aspectClasses[aspect]} cursor-pointer bg-transparent border-none p-0 m-0 block {classes?.root ?? className}">
71
+ <img src={imgSrc} {alt} onerror={handleError} class="w-full h-full object-cover transition-all duration-300 {hoverClasses[hover]} {classes?.img ?? ''}" loading="lazy" />
70
72
  </button>
71
73
  {:else}
72
- <div class="group overflow-hidden {roundedClasses[rounded]} {aspectClasses[aspect]} {className}">
73
- <img src={imgSrc} {alt} onerror={handleError} class="w-full h-full object-cover transition-all duration-300 {hoverClasses[hover]}" loading="lazy" />
74
+ <div class="group overflow-hidden {roundedClasses[rounded]} {aspectClasses[aspect]} {classes?.root ?? className}">
75
+ <img src={imgSrc} {alt} onerror={handleError} class="w-full h-full object-cover transition-all duration-300 {hoverClasses[hover]} {classes?.img ?? ''}" loading="lazy" />
74
76
  </div>
75
77
  {/if}
76
78
 
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import type { ButtonColor } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ before: string
6
+ after: string
7
+ beforeLabel?: string
8
+ afterLabel?: string
9
+ initialPosition?: number
10
+ orientation?: 'horizontal' | 'vertical'
11
+ color?: ButtonColor
12
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
13
+ showLabels?: boolean
14
+ showHandle?: boolean
15
+ width?: string
16
+ height?: string
17
+ class?: string
18
+ classes?: { root?: string, before?: string, after?: string, handle?: string, label?: string }
19
+ }
20
+
21
+ let {
22
+ before,
23
+ after,
24
+ beforeLabel = 'Avant',
25
+ afterLabel = 'Apres',
26
+ initialPosition = 50,
27
+ orientation = 'horizontal',
28
+ color,
29
+ rounded = 'lg',
30
+ showLabels = true,
31
+ showHandle = true,
32
+ width,
33
+ height,
34
+ class: className = '',
35
+ classes = {}
36
+ }: Props = $props()
37
+
38
+ let position = $state(initialPosition)
39
+ let dragging = $state(false)
40
+ let containerEl: HTMLDivElement
41
+
42
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
43
+ const isHorizontal = $derived(orientation === 'horizontal')
44
+
45
+ const roundedMap: Record<string, string> = {
46
+ none: '0', sm: '0.25rem', md: '0.5rem', lg: '0.75rem', xl: '1rem'
47
+ }
48
+ const rad = $derived(roundedMap[rounded])
49
+
50
+ function updatePosition(e: MouseEvent | TouchEvent) {
51
+ if (!containerEl) return
52
+ const rect = containerEl.getBoundingClientRect()
53
+ let clientPos: number
54
+ let size: number
55
+
56
+ if ('touches' in e) {
57
+ clientPos = isHorizontal ? e.touches[0].clientX - rect.left : e.touches[0].clientY - rect.top
58
+ } else {
59
+ clientPos = isHorizontal ? e.clientX - rect.left : e.clientY - rect.top
60
+ }
61
+ size = isHorizontal ? rect.width : rect.height
62
+ position = Math.min(Math.max((clientPos / size) * 100, 0), 100)
63
+ }
64
+
65
+ function handleMouseDown(e: MouseEvent) {
66
+ e.preventDefault()
67
+ dragging = true
68
+ updatePosition(e)
69
+ }
70
+
71
+ function handleTouchStart(e: TouchEvent) {
72
+ dragging = true
73
+ updatePosition(e)
74
+ }
75
+
76
+ function handleMove(e: MouseEvent) {
77
+ if (dragging) updatePosition(e)
78
+ }
79
+
80
+ function handleTouchMove(e: TouchEvent) {
81
+ if (dragging) { e.preventDefault(); updatePosition(e) }
82
+ }
83
+
84
+ function handleEnd() {
85
+ dragging = false
86
+ }
87
+ </script>
88
+
89
+ <svelte:window
90
+ onmousemove={handleMove}
91
+ onmouseup={handleEnd}
92
+ ontouchmove={handleTouchMove}
93
+ ontouchend={handleEnd}
94
+ />
95
+
96
+ <div
97
+ bind:this={containerEl}
98
+ class="karbon-imgcompare relative select-none overflow-hidden {classes?.root ?? className}"
99
+ style="border-radius:{rad};{width ? `width:${width};` : 'width:100%;'}{height ? `height:${height};` : ''}aspect-ratio:{height ? 'auto' : '16/10'};cursor:{dragging ? 'grabbing' : 'col-resize'};"
100
+ onmousedown={handleMouseDown}
101
+ ontouchstart={handleTouchStart}
102
+ role="slider"
103
+ aria-valuenow={Math.round(position)}
104
+ aria-valuemin={0}
105
+ aria-valuemax={100}
106
+ aria-label="Comparateur d'images"
107
+ tabindex={0}
108
+ onkeydown={(e) => {
109
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); position = Math.max(position - 2, 0) }
110
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); position = Math.min(position + 2, 100) }
111
+ }}
112
+ >
113
+ <!-- After image (background, full) -->
114
+ <img
115
+ src={after}
116
+ alt={afterLabel}
117
+ class="absolute inset-0 w-full h-full object-cover {classes?.after ?? ''}"
118
+ draggable="false"
119
+ />
120
+
121
+ <!-- Before image (clipped via clip-path) -->
122
+ <img
123
+ src={before}
124
+ alt={beforeLabel}
125
+ class="absolute inset-0 w-full h-full object-cover {classes?.before ?? ''}"
126
+ style="clip-path:{isHorizontal ? `inset(0 ${100 - position}% 0 0)` : `inset(0 0 ${100 - position}% 0)`};"
127
+ draggable="false"
128
+ />
129
+
130
+ <!-- Divider line -->
131
+ <div
132
+ class="absolute"
133
+ style="{isHorizontal
134
+ ? `left:${position}%;top:0;bottom:0;width:2px;transform:translateX(-1px);`
135
+ : `top:${position}%;left:0;right:0;height:2px;transform:translateY(-1px);`
136
+ }background:{accent};z-index:5;pointer-events:none;"
137
+ ></div>
138
+
139
+ <!-- Handle -->
140
+ {#if showHandle}
141
+ <div
142
+ class="absolute z-10"
143
+ style="{isHorizontal
144
+ ? `left:${position}%;top:50%;transform:translate(-50%,-50%);`
145
+ : `top:${position}%;left:50%;transform:translate(-50%,-50%);`
146
+ }pointer-events:none;"
147
+ >
148
+ <div
149
+ class="rounded-full flex items-center justify-center shadow-lg {classes?.handle ?? ''}"
150
+ style="width:36px;height:36px;background:{accent};color:white;box-shadow:0 2px 10px rgba(0,0,0,0.3),0 0 0 3px rgba(255,255,255,0.3);"
151
+ >
152
+ {#if isHorizontal}
153
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15-5-5 5-5"/><path d="m17 9 5 5-5 5"/></svg>
154
+ {:else}
155
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 7-5-5-5 5"/><path d="m9 17 5 5 5-5"/></svg>
156
+ {/if}
157
+ </div>
158
+ </div>
159
+ {/if}
160
+
161
+ <!-- Labels -->
162
+ {#if showLabels}
163
+ <div
164
+ class="absolute z-5 pointer-events-none {classes?.label ?? ''}"
165
+ style="{isHorizontal ? 'top:12px;left:12px;' : 'top:12px;left:12px;'}"
166
+ >
167
+ <span
168
+ class="rounded-md px-2 py-1 text-[11px] font-semibold"
169
+ style="background:rgba(0,0,0,0.5);color:white;backdrop-filter:blur(4px);"
170
+ >{beforeLabel}</span>
171
+ </div>
172
+ <div
173
+ class="absolute z-5 pointer-events-none {classes?.label ?? ''}"
174
+ style="{isHorizontal ? 'top:12px;right:12px;' : 'bottom:12px;right:12px;'}"
175
+ >
176
+ <span
177
+ class="rounded-md px-2 py-1 text-[11px] font-semibold"
178
+ style="background:rgba(0,0,0,0.5);color:white;backdrop-filter:blur(4px);"
179
+ >{afterLabel}</span>
180
+ </div>
181
+ {/if}
182
+ </div>
@@ -1,56 +1,76 @@
1
1
  <script lang="ts">
2
- import type { ImageRounded, ImgZoomTrigger } from '@karbonjs/ui-core'
2
+ import type { ButtonColor } from '@karbonjs/ui-core'
3
3
 
4
4
  interface Props {
5
5
  src: string
6
6
  zoomSrc?: string
7
7
  alt?: string
8
8
  zoom?: number
9
- trigger?: ImgZoomTrigger
10
- rounded?: ImageRounded
9
+ trigger?: 'hover' | 'click'
10
+ mode?: 'overlay' | 'lens' | 'side'
11
+ lensSize?: number
12
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
13
+ showHint?: boolean
14
+ color?: ButtonColor
15
+ width?: string
16
+ height?: string
11
17
  class?: string
18
+ classes?: { root?: string, img?: string, lens?: string, overlay?: string }
12
19
  }
13
20
 
14
21
  let {
15
22
  src,
16
23
  zoomSrc,
17
24
  alt = '',
18
- zoom = 2,
25
+ zoom = 2.5,
19
26
  trigger = 'hover',
27
+ mode = 'overlay',
28
+ lensSize = 120,
20
29
  rounded = 'md',
21
- class: className = ''
30
+ showHint = true,
31
+ color,
32
+ width,
33
+ height,
34
+ class: className = '',
35
+ classes = {}
22
36
  }: Props = $props()
23
37
 
24
- let container = $state<HTMLElement>(undefined!)
38
+ let container: HTMLElement
25
39
  let zooming = $state(false)
26
40
  let posX = $state(50)
27
41
  let posY = $state(50)
42
+ let mouseX = $state(0)
43
+ let mouseY = $state(0)
44
+ let hovered = $state(false)
28
45
 
29
- const roundedClasses: Record<string, string> = {
30
- none: 'rounded-none',
31
- sm: 'rounded',
32
- md: 'rounded-lg',
33
- lg: 'rounded-xl',
34
- full: 'rounded-full'
35
- }
36
-
46
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
37
47
  const zoomImage = $derived(zoomSrc || src)
38
48
 
49
+ const roundedMap: Record<string, string> = {
50
+ none: '0', sm: '0.25rem', md: '0.5rem', lg: '0.75rem', xl: '1rem', full: '9999px'
51
+ }
52
+ const rad = $derived(roundedMap[rounded])
53
+
39
54
  function updatePosition(e: MouseEvent) {
55
+ if (!container) return
40
56
  const rect = container.getBoundingClientRect()
41
57
  posX = ((e.clientX - rect.left) / rect.width) * 100
42
58
  posY = ((e.clientY - rect.top) / rect.height) * 100
59
+ mouseX = e.clientX - rect.left
60
+ mouseY = e.clientY - rect.top
43
61
  }
44
62
 
45
63
  function handleMouseEnter(e: MouseEvent) {
64
+ hovered = true
46
65
  if (trigger === 'hover') { zooming = true; updatePosition(e) }
47
66
  }
48
67
 
49
68
  function handleMouseMove(e: MouseEvent) {
50
- if (zooming) updatePosition(e)
69
+ if (zooming || hovered) updatePosition(e)
51
70
  }
52
71
 
53
72
  function handleMouseLeave() {
73
+ hovered = false
54
74
  if (trigger === 'hover') zooming = false
55
75
  }
56
76
 
@@ -58,39 +78,101 @@
58
78
  zooming = !zooming
59
79
  if (zooming) updatePosition(e)
60
80
  }
61
-
62
- function handleKeydown(e: KeyboardEvent) {
63
- if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); zooming = !zooming }
64
- }
65
81
  </script>
66
82
 
67
- {#if trigger === 'click'}
68
- <button
69
- type="button"
70
- bind:this={container}
71
- class="relative overflow-hidden {roundedClasses[rounded]} bg-transparent border-none p-0 m-0 block {zooming ? 'cursor-zoom-out' : 'cursor-zoom-in'} {className}"
72
- onmousemove={handleMouseMove}
73
- onmouseleave={handleMouseLeave}
74
- onclick={handleClick}
75
- >
76
- <img {src} {alt} class="w-full h-full object-cover block" loading="lazy" />
77
- {#if zooming}
78
- <span class="absolute inset-0 pointer-events-none" style="background-image: url({zoomImage}); background-size: {zoom * 100}%; background-position: {posX}% {posY}%; background-repeat: no-repeat;"></span>
79
- {/if}
80
- </button>
81
- {:else}
82
- <div
83
- bind:this={container}
84
- role="img"
85
- aria-label={alt || 'Zoomable image'}
86
- class="relative overflow-hidden {roundedClasses[rounded]} {className}"
87
- onmouseenter={handleMouseEnter}
88
- onmousemove={handleMouseMove}
89
- onmouseleave={handleMouseLeave}
90
- >
91
- <img {src} {alt} class="w-full h-full object-cover block" loading="lazy" />
92
- {#if zooming}
93
- <div class="absolute inset-0 pointer-events-none" style="background-image: url({zoomImage}); background-size: {zoom * 100}%; background-position: {posX}% {posY}%; background-repeat: no-repeat;"></div>
94
- {/if}
95
- </div>
96
- {/if}
83
+ <div
84
+ bind:this={container}
85
+ class="karbon-imgzoom relative overflow-hidden inline-block {classes?.root ?? className}"
86
+ style="border-radius:{rad};{width ? `width:${width};` : ''}{height ? `height:${height};` : ''}cursor:{zooming ? 'crosshair' : trigger === 'click' ? 'zoom-in' : 'crosshair'};"
87
+ role="img"
88
+ aria-label={alt || 'Image zoomable'}
89
+ tabindex={0}
90
+ onmouseenter={handleMouseEnter}
91
+ onmousemove={handleMouseMove}
92
+ onmouseleave={handleMouseLeave}
93
+ onclick={trigger === 'click' ? handleClick : undefined}
94
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); zooming = !zooming } }}
95
+ >
96
+ <!-- Base image -->
97
+ <img
98
+ {src}
99
+ {alt}
100
+ class="w-full h-full object-cover block transition-transform duration-200 {classes?.img ?? ''}"
101
+ style={zooming && mode === 'overlay' ? `transform:scale(${zoom});transform-origin:${posX}% ${posY}%;` : ''}
102
+ loading="lazy"
103
+ draggable="false"
104
+ />
105
+
106
+ <!-- Hint icon -->
107
+ {#if showHint && !zooming && hovered}
108
+ <div
109
+ class="absolute top-3 right-3 rounded-full px-2 py-1 flex items-center gap-1 pointer-events-none"
110
+ style="background:rgba(0,0,0,0.5);color:white;font-size:11px;backdrop-filter:blur(4px);opacity:0.8;"
111
+ >
112
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6"/><path d="M8 11h6"/></svg>
113
+ <span>Zoom</span>
114
+ </div>
115
+ {/if}
116
+
117
+ <!-- Lens mode -->
118
+ {#if mode === 'lens' && zooming}
119
+ <div
120
+ class="absolute rounded-full pointer-events-none shadow-xl {classes?.lens ?? ''}"
121
+ style="
122
+ width:{lensSize}px;height:{lensSize}px;
123
+ left:{mouseX - lensSize / 2}px;top:{mouseY - lensSize / 2}px;
124
+ background-image:url({zoomImage});
125
+ background-size:{zoom * 100}%;
126
+ background-position:{posX}% {posY}%;
127
+ background-repeat:no-repeat;
128
+ border:3px solid {accent};
129
+ box-shadow:0 0 0 1px rgba(0,0,0,0.1),0 8px 25px rgba(0,0,0,0.3);
130
+ z-index:10;
131
+ "
132
+ ></div>
133
+ <!-- Crosshair on source -->
134
+ <div
135
+ class="absolute pointer-events-none"
136
+ style="
137
+ width:{lensSize / zoom}px;height:{lensSize / zoom}px;
138
+ left:{mouseX - lensSize / zoom / 2}px;top:{mouseY - lensSize / zoom / 2}px;
139
+ border:1.5px solid {accent};
140
+ border-radius:2px;
141
+ background:color-mix(in srgb,{accent} 8%,transparent);
142
+ z-index:5;
143
+ "
144
+ ></div>
145
+ {/if}
146
+
147
+ <!-- Overlay mode zoom layer (CSS transform on img handles this) -->
148
+
149
+ <!-- Side mode -->
150
+ {#if mode === 'side' && zooming}
151
+ <div
152
+ class="absolute top-0 shadow-2xl pointer-events-none {classes?.overlay ?? ''}"
153
+ style="
154
+ left:calc(100% + 12px);
155
+ width:{container?.offsetWidth || 300}px;
156
+ height:{container?.offsetHeight || 300}px;
157
+ background-image:url({zoomImage});
158
+ background-size:{zoom * 100}%;
159
+ background-position:{posX}% {posY}%;
160
+ background-repeat:no-repeat;
161
+ border-radius:{rad};
162
+ border:1px solid var(--karbon-border);
163
+ z-index:10;
164
+ "
165
+ ></div>
166
+ <!-- Crosshair indicator -->
167
+ <div
168
+ class="absolute pointer-events-none"
169
+ style="
170
+ width:{100 / zoom}%;height:{100 / zoom}%;
171
+ left:{posX - 50 / zoom}%;top:{posY - 50 / zoom}%;
172
+ border:2px solid {accent};
173
+ background:color-mix(in srgb,{accent} 10%,transparent);
174
+ z-index:5;
175
+ "
176
+ ></div>
177
+ {/if}
178
+ </div>
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  // button
2
2
  export { default as Button } from './button/Button.svelte'
3
+ export { default as ButtonBrand } from './button/ButtonBrand.svelte'
3
4
 
4
5
  // form
5
- export { default as FormInput } from './form/FormInput.svelte'
6
+ export { default as Input } from './form/Input.svelte'
7
+ export { default as FormInput } from './form/Input.svelte' // alias retro-compat
6
8
  export { default as Select } from './form/Select.svelte'
7
9
  export { default as Checkbox } from './form/Checkbox.svelte'
8
10
  export { default as Toggle } from './form/Toggle.svelte'
@@ -30,8 +32,12 @@ export { default as PageHeader } from './layout/PageHeader.svelte'
30
32
  export { default as EmptyState } from './layout/EmptyState.svelte'
31
33
 
32
34
  // image
35
+ // code
36
+ export { default as CodeBlock } from './code/CodeBlock.svelte'
37
+
33
38
  export { default as Image } from './image/Image.svelte'
34
39
  export { default as ImgZoom } from './image/ImgZoom.svelte'
40
+ export { default as ImageCompare } from './image/ImageCompare.svelte'
35
41
 
36
42
  // carousel
37
43
  export { default as Carousel } from './carousel/Carousel.svelte'
@@ -2,17 +2,18 @@
2
2
  interface Props {
3
3
  keys: string[]
4
4
  class?: string
5
+ classes?: { root?: string, key?: string }
5
6
  }
6
7
 
7
- let { keys, class: className = '' }: Props = $props()
8
+ let { keys, class: className = '', classes = {} }: Props = $props()
8
9
  </script>
9
10
 
10
- <span class="inline-flex items-center gap-1 {className}">
11
+ <span class="inline-flex items-center gap-1 {classes?.root ?? className}">
11
12
  {#each keys as key, i}
12
13
  {#if i > 0}
13
14
  <span class="text-[var(--karbon-text-4,#b5b2cc)] text-xs">+</span>
14
15
  {/if}
15
- <kbd class="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 rounded-md border border-[var(--karbon-border,rgba(0,0,0,0.07))] bg-[var(--karbon-bg-2,#e8e6f0)] text-[var(--karbon-text-2,#5a567e)] text-[11px] font-mono font-medium shadow-sm">
16
+ <kbd class="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 rounded-md border border-[var(--karbon-border,rgba(0,0,0,0.07))] bg-[var(--karbon-bg-2,#e8e6f0)] text-[var(--karbon-text-2,#5a567e)] text-[11px] font-mono font-medium shadow-sm {classes?.key ?? ''}">
16
17
  {key}
17
18
  </kbd>
18
19
  {/each}
@@ -6,9 +6,11 @@
6
6
  variant?: CardVariant
7
7
  padding?: CardPadding
8
8
  hoverable?: boolean
9
+ noPadding?: boolean
9
10
  title?: string
10
11
  icon?: any
11
12
  class?: string
13
+ classes?: { root?: string, header?: string, body?: string }
12
14
  children: Snippet
13
15
  header?: Snippet
14
16
  footer?: Snippet
@@ -18,17 +20,19 @@
18
20
  variant = 'default',
19
21
  padding = 'md',
20
22
  hoverable = false,
23
+ noPadding = false,
21
24
  title = '',
22
25
  icon: Icon,
23
26
  class: className = '',
27
+ classes = {},
24
28
  children,
25
29
  header,
26
30
  footer
27
31
  }: Props = $props()
28
32
 
29
33
  const variantClasses: Record<string, string> = {
30
- default: 'bg-[var(--karbon-bg-card,#fff)] border border-[var(--karbon-border,rgba(0,0,0,0.07))]',
31
- elevated: 'bg-[var(--karbon-bg-card,#fff)] shadow-lg',
34
+ default: 'bg-[var(--karbon-bg-card,#fff)] border border-[var(--karbon-border,rgba(0,0,0,0.07))] shadow-sm',
35
+ elevated: 'bg-[var(--karbon-bg-card,#fff)] border border-[var(--karbon-border,rgba(0,0,0,0.07))] shadow-lg',
32
36
  outlined: 'border-2 border-[var(--karbon-border,rgba(0,0,0,0.07))]',
33
37
  ghost: 'bg-transparent'
34
38
  }
@@ -39,15 +43,17 @@
39
43
  md: 'p-5',
40
44
  lg: 'p-8'
41
45
  }
46
+
47
+ const bodyPadding = $derived(noPadding ? '' : paddingClasses[padding])
42
48
  </script>
43
49
 
44
- <div class="rounded-xl overflow-hidden {variantClasses[variant]} {hoverable ? 'transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5' : ''} {className}">
50
+ <div class="rounded-xl overflow-hidden {variantClasses[variant]} {hoverable ? 'transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5' : ''} {classes?.root ?? className}">
45
51
  {#if header}
46
- <div class="px-5 py-3.5 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))]">
52
+ <div class="px-5 py-3.5 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))] {classes?.header ?? ''}">
47
53
  {@render header()}
48
54
  </div>
49
55
  {:else if title}
50
- <div class="flex items-center gap-2 px-5 py-3.5 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))] text-[var(--karbon-text-2,#5a567e)] text-[0.825rem] font-semibold">
56
+ <div class="flex items-center gap-2 px-5 py-3.5 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))] text-[var(--karbon-text-2,#5a567e)] text-[0.825rem] font-semibold {classes?.header ?? ''}">
51
57
  {#if Icon}
52
58
  <Icon class="w-4 h-4" />
53
59
  {/if}
@@ -55,7 +61,7 @@
55
61
  </div>
56
62
  {/if}
57
63
 
58
- <div class={paddingClasses[padding]}>
64
+ <div class="{bodyPadding} {classes?.body ?? ''}">
59
65
  {@render children()}
60
66
  </div>
61
67
 
@@ -1,25 +1,92 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
+
2
5
  interface Props {
3
6
  title: string
4
7
  description?: string
5
- icon?: any
8
+ icon?: Snippet
9
+ color?: ButtonColor
10
+ size?: 'sm' | 'md' | 'lg'
11
+ variant?: 'default' | 'bordered' | 'filled' | 'minimal'
12
+ actions?: Snippet
13
+ illustration?: Snippet
14
+ class?: string
15
+ classes?: { root?: string, icon?: string, title?: string, description?: string, actions?: string }
6
16
  }
7
17
 
8
18
  let {
9
19
  title,
10
20
  description = '',
11
- icon: Icon
21
+ icon,
22
+ color,
23
+ size = 'md',
24
+ variant = 'default',
25
+ actions,
26
+ illustration,
27
+ class: className = '',
28
+ classes = {}
12
29
  }: Props = $props()
30
+
31
+ const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
32
+ const accentLight = $derived(color ? `var(--karbon-${color}-400)` : 'var(--karbon-text-4)')
33
+
34
+ const sizeMap = {
35
+ sm: { py: 'py-8', title: 'text-sm', desc: 'text-xs', iconBox: 40, iconSize: 20, maxW: '18rem', gap: 'gap-2' },
36
+ md: { py: 'py-12', title: 'text-base', desc: 'text-sm', iconBox: 52, iconSize: 24, maxW: '22rem', gap: 'gap-3' },
37
+ lg: { py: 'py-16', title: 'text-lg', desc: 'text-base', iconBox: 64, iconSize: 28, maxW: '26rem', gap: 'gap-4' },
38
+ }
39
+ const s = $derived(sizeMap[size])
40
+
41
+ function rootStyle(): string {
42
+ switch (variant) {
43
+ case 'bordered': return `border:1px solid var(--karbon-border);border-radius:0.75rem;background:var(--karbon-bg-card);`
44
+ case 'filled': return `border-radius:0.75rem;background:color-mix(in srgb,${accent} 5%,transparent);border:1px solid color-mix(in srgb,${accent} 10%,transparent);`
45
+ case 'minimal': return ''
46
+ default: return ''
47
+ }
48
+ }
13
49
  </script>
14
50
 
15
- <div class="text-center py-12 px-6">
16
- {#if Icon}
17
- <div class="w-14 h-14 rounded-2xl bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] text-[var(--karbon-text-4,#b5b2cc)] flex items-center justify-center mx-auto mb-4">
18
- <Icon class="w-8 h-8" />
51
+ <div
52
+ class="text-center {s.py} px-6 {classes?.root ?? className}"
53
+ style={rootStyle()}
54
+ >
55
+ <!-- Illustration -->
56
+ {#if illustration}
57
+ <div class="mb-4 flex justify-center">
58
+ {@render illustration()}
59
+ </div>
60
+ {:else if icon}
61
+ <!-- Icon -->
62
+ <div
63
+ class="mx-auto mb-4 rounded-2xl flex items-center justify-center {classes?.icon ?? ''}"
64
+ style="width:{s.iconBox}px;height:{s.iconBox}px;background:color-mix(in srgb,{accent} 10%,transparent);color:{accentLight};"
65
+ >
66
+ {@render icon()}
67
+ </div>
68
+ {:else}
69
+ <!-- Default icon -->
70
+ <div
71
+ class="mx-auto mb-4 rounded-2xl flex items-center justify-center {classes?.icon ?? ''}"
72
+ style="width:{s.iconBox}px;height:{s.iconBox}px;background:var(--karbon-nav-hover-bg);color:var(--karbon-text-4);"
73
+ >
74
+ <svg xmlns="http://www.w3.org/2000/svg" width={s.iconSize} height={s.iconSize} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
19
75
  </div>
20
76
  {/if}
21
- <p class="text-[var(--karbon-text-2,#5a567e)] font-semibold text-[0.95rem] m-0">{title}</p>
77
+
78
+ <!-- Title -->
79
+ <p class="{s.title} font-semibold {classes?.title ?? ''}" style="color:var(--karbon-text);margin:0;">{title}</p>
80
+
81
+ <!-- Description -->
22
82
  {#if description}
23
- <p class="text-[var(--karbon-text-3,#8e8aae)] text-[0.8rem] mt-1.5 mx-auto max-w-[22rem]">{description}</p>
83
+ <p class="{s.desc} mt-1.5 mx-auto {classes?.description ?? ''}" style="color:var(--karbon-text-3);margin-top:0.375rem;max-width:{s.maxW};">{description}</p>
84
+ {/if}
85
+
86
+ <!-- Actions -->
87
+ {#if actions}
88
+ <div class="mt-5 flex items-center justify-center {s.gap} {classes?.actions ?? ''}">
89
+ {@render actions()}
90
+ </div>
24
91
  {/if}
25
92
  </div>