@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.
- package/LICENSE +21 -0
- package/package.json +30 -0
- package/src/accordion/Accordion.svelte +63 -0
- package/src/alert/AlertMessage.svelte +44 -0
- package/src/avatar/Avatar.svelte +48 -0
- package/src/badge/Badge.svelte +24 -0
- package/src/breadcrumb/Breadcrumb.svelte +34 -0
- package/src/button/Button.svelte +89 -0
- package/src/carousel/Carousel.svelte +118 -0
- package/src/data/DataTable.svelte +18 -0
- package/src/data/Pagination.svelte +45 -0
- package/src/divider/Divider.svelte +27 -0
- package/src/dropdown/Dropdown.svelte +61 -0
- package/src/form/Checkbox.svelte +51 -0
- package/src/form/ColorPicker.svelte +95 -0
- package/src/form/DatePicker.svelte +196 -0
- package/src/form/FormInput.svelte +174 -0
- package/src/form/Radio.svelte +54 -0
- package/src/form/Select.svelte +73 -0
- package/src/form/Slider.svelte +74 -0
- package/src/form/Textarea.svelte +86 -0
- package/src/form/Toggle.svelte +55 -0
- package/src/image/Image.svelte +89 -0
- package/src/image/ImgZoom.svelte +96 -0
- package/src/index.ts +71 -0
- package/src/kbd/Kbd.svelte +19 -0
- package/src/layout/Card.svelte +67 -0
- package/src/layout/EmptyState.svelte +25 -0
- package/src/layout/PageHeader.svelte +27 -0
- package/src/overlay/Dialog.svelte +135 -0
- package/src/overlay/ImgBox.svelte +174 -0
- package/src/overlay/Modal.svelte +98 -0
- package/src/overlay/Toast.svelte +92 -0
- package/src/progress/Progress.svelte +50 -0
- package/src/skeleton/Skeleton.svelte +50 -0
- package/src/tabs/Tabs.svelte +59 -0
- 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>
|