@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.
- package/package.json +3 -2
- package/src/accordion/Accordion.svelte +198 -23
- package/src/alert/AlertMessage.svelte +114 -20
- package/src/avatar/Avatar.svelte +16 -2
- package/src/badge/Badge.svelte +99 -10
- package/src/breadcrumb/Breadcrumb.svelte +124 -13
- package/src/button/Button.svelte +106 -21
- package/src/button/ButtonBrand.svelte +229 -0
- package/src/carousel/Carousel.svelte +161 -28
- package/src/code/CodeBlock.svelte +323 -0
- package/src/data/DataTable.svelte +319 -8
- package/src/data/Pagination.svelte +168 -28
- package/src/divider/Divider.svelte +91 -10
- package/src/dropdown/Dropdown.svelte +171 -27
- package/src/editor/RichTextEditor.svelte +861 -107
- package/src/form/Checkbox.svelte +110 -18
- package/src/form/ColorPicker.svelte +28 -16
- package/src/form/DatePicker.svelte +20 -10
- package/src/form/{FormInput.svelte → Input.svelte} +41 -14
- package/src/form/Radio.svelte +86 -18
- package/src/form/Select.svelte +246 -33
- package/src/form/Slider.svelte +22 -7
- package/src/form/Textarea.svelte +53 -10
- package/src/form/Toggle.svelte +72 -18
- package/src/image/Image.svelte +6 -4
- package/src/image/ImageCompare.svelte +182 -0
- package/src/image/ImgZoom.svelte +131 -49
- package/src/index.ts +7 -1
- package/src/kbd/Kbd.svelte +4 -3
- package/src/layout/Card.svelte +12 -6
- package/src/layout/EmptyState.svelte +75 -8
- package/src/layout/PageHeader.svelte +111 -11
- package/src/overlay/Dialog.svelte +147 -67
- package/src/overlay/ImgBox.svelte +125 -21
- package/src/overlay/Modal.svelte +110 -28
- package/src/overlay/Toast.svelte +152 -55
- package/src/progress/Progress.svelte +137 -26
- package/src/skeleton/Skeleton.svelte +6 -4
- package/src/tabs/Tabs.svelte +133 -22
- package/src/tooltip/Tooltip.svelte +110 -20
package/src/form/Select.svelte
CHANGED
|
@@ -1,73 +1,286 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { FormVariant, FormInputVariant, SelectOption, SelectClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
name: string
|
|
6
7
|
options: SelectOption[]
|
|
7
8
|
value?: string
|
|
9
|
+
values?: string[]
|
|
8
10
|
placeholder?: string
|
|
9
11
|
label?: string
|
|
10
12
|
error?: string
|
|
13
|
+
errorIcon?: boolean
|
|
11
14
|
required?: boolean
|
|
12
15
|
disabled?: boolean
|
|
16
|
+
multiple?: boolean
|
|
17
|
+
searchable?: boolean
|
|
18
|
+
clearable?: boolean
|
|
13
19
|
variant?: FormVariant
|
|
20
|
+
inputVariant?: FormInputVariant
|
|
21
|
+
color?: ButtonColor
|
|
22
|
+
classes?: SelectClasses
|
|
14
23
|
class?: string
|
|
15
|
-
onchange?: (
|
|
24
|
+
onchange?: (value: string | string[]) => void
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
let {
|
|
19
28
|
name,
|
|
20
29
|
options,
|
|
21
30
|
value = $bindable(''),
|
|
22
|
-
|
|
31
|
+
values = $bindable([]),
|
|
32
|
+
placeholder = 'Selectionner...',
|
|
23
33
|
label = '',
|
|
24
34
|
error = '',
|
|
35
|
+
errorIcon = true,
|
|
25
36
|
required = false,
|
|
26
37
|
disabled = false,
|
|
38
|
+
multiple = false,
|
|
39
|
+
searchable = false,
|
|
40
|
+
clearable = false,
|
|
27
41
|
variant = 'dark',
|
|
42
|
+
inputVariant = 'outlined',
|
|
43
|
+
color,
|
|
44
|
+
classes,
|
|
28
45
|
class: className = '',
|
|
29
46
|
onchange
|
|
30
47
|
}: Props = $props()
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
let open = $state(false)
|
|
50
|
+
let focused = $state(false)
|
|
51
|
+
let search = $state('')
|
|
52
|
+
let triggerEl: HTMLDivElement | undefined = $state()
|
|
53
|
+
let dropdownEl: HTMLDivElement | undefined = $state()
|
|
54
|
+
|
|
55
|
+
const focusColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
56
|
+
const isDark = $derived(variant === 'dark')
|
|
57
|
+
|
|
58
|
+
const filteredOptions = $derived(
|
|
59
|
+
searchable && search
|
|
60
|
+
? options.filter(o => !o.disabled && o.label.toLowerCase().includes(search.toLowerCase()))
|
|
61
|
+
: options
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const selectedLabel = $derived(() => {
|
|
65
|
+
if (multiple) {
|
|
66
|
+
return values.length ? `${values.length} selectionne${values.length > 1 ? 's' : ''}` : ''
|
|
67
|
+
}
|
|
68
|
+
const opt = options.find(o => o.value === value)
|
|
69
|
+
return opt?.label || ''
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const selectedOptions = $derived(
|
|
73
|
+
multiple ? options.filter(o => values.includes(o.value)) : []
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
function toggle() {
|
|
77
|
+
if (disabled) return
|
|
78
|
+
open = !open
|
|
79
|
+
if (open) {
|
|
80
|
+
search = ''
|
|
81
|
+
focused = true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function selectOption(opt: SelectOption) {
|
|
86
|
+
if (opt.disabled) return
|
|
87
|
+
if (multiple) {
|
|
88
|
+
if (values.includes(opt.value)) {
|
|
89
|
+
values = values.filter(v => v !== opt.value)
|
|
90
|
+
} else {
|
|
91
|
+
values = [...values, opt.value]
|
|
92
|
+
}
|
|
93
|
+
onchange?.(values)
|
|
94
|
+
} else {
|
|
95
|
+
value = opt.value
|
|
96
|
+
open = false
|
|
97
|
+
onchange?.(value)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function removeChip(val: string) {
|
|
102
|
+
values = values.filter(v => v !== val)
|
|
103
|
+
onchange?.(values)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearAll() {
|
|
107
|
+
if (multiple) {
|
|
108
|
+
values = []
|
|
109
|
+
onchange?.(values)
|
|
110
|
+
} else {
|
|
111
|
+
value = ''
|
|
112
|
+
onchange?.('')
|
|
42
113
|
}
|
|
43
|
-
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleClickOutside(e: MouseEvent) {
|
|
117
|
+
if (triggerEl && !triggerEl.contains(e.target as Node) && dropdownEl && !dropdownEl.contains(e.target as Node)) {
|
|
118
|
+
open = false
|
|
119
|
+
focused = false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
124
|
+
if (e.key === 'Escape') {
|
|
125
|
+
open = false
|
|
126
|
+
focused = false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
44
129
|
|
|
45
|
-
const
|
|
130
|
+
const variantClasses = $derived.by(() => {
|
|
131
|
+
if (inputVariant === 'underline') return 'rounded-none bg-transparent'
|
|
132
|
+
if (inputVariant === 'filled') return isDark ? 'rounded-lg bg-white/8' : 'rounded-lg bg-gray-100'
|
|
133
|
+
return isDark ? 'rounded-lg border border-white/8 bg-white/3' : 'rounded-lg border border-gray-300 bg-white'
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const inlineStyle = $derived.by(() => {
|
|
137
|
+
let s = ''
|
|
138
|
+
if (inputVariant === 'underline') {
|
|
139
|
+
const bc = focused ? focusColor : error ? 'var(--karbon-danger)' : isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.12)'
|
|
140
|
+
s = `border:none;border-bottom:1px solid ${bc};border-radius:0;`
|
|
141
|
+
} else if (focused) {
|
|
142
|
+
s = `border-color:${focusColor};box-shadow:0 0 0 3px color-mix(in srgb,${focusColor} 12%,transparent);`
|
|
143
|
+
}
|
|
144
|
+
return s
|
|
145
|
+
})
|
|
46
146
|
</script>
|
|
47
147
|
|
|
48
|
-
<
|
|
148
|
+
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
|
|
149
|
+
|
|
150
|
+
<div class="space-y-1.5 {classes?.root ?? ''} {className}">
|
|
49
151
|
{#if label}
|
|
50
|
-
<
|
|
152
|
+
<span class="{isDark ? 'text-[11px] font-medium text-gray-500 uppercase tracking-wider' : 'text-sm font-medium text-gray-700'} block mb-1.5 {classes?.label ?? ''}">{label}</span>
|
|
51
153
|
{/if}
|
|
52
154
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
155
|
+
<!-- Hidden input for form submission -->
|
|
156
|
+
<input type="hidden" {name} value={multiple ? values.join(',') : value} />
|
|
157
|
+
|
|
158
|
+
<div class="relative">
|
|
159
|
+
<!-- Trigger -->
|
|
160
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
161
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
162
|
+
<div
|
|
163
|
+
bind:this={triggerEl}
|
|
164
|
+
onclick={toggle}
|
|
165
|
+
class="relative w-full min-h-[42px] md:min-h-[46px] flex items-center gap-2 px-3 py-2 text-[13px] md:text-sm cursor-pointer transition-all
|
|
166
|
+
{variantClasses}
|
|
167
|
+
{error ? 'border-red-500/50' : ''}
|
|
168
|
+
{disabled ? 'opacity-40 cursor-not-allowed pointer-events-none' : ''}
|
|
169
|
+
{classes?.select ?? ''}"
|
|
170
|
+
style={inlineStyle}
|
|
171
|
+
>
|
|
172
|
+
<!-- Content -->
|
|
173
|
+
<div class="flex-1 flex flex-wrap items-center gap-1.5 min-w-0">
|
|
174
|
+
{#if multiple && selectedOptions.length > 0}
|
|
175
|
+
{#each selectedOptions as opt}
|
|
176
|
+
<span
|
|
177
|
+
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-colors"
|
|
178
|
+
style="background: color-mix(in srgb, {focusColor} 15%, transparent); color: {focusColor};"
|
|
179
|
+
>
|
|
180
|
+
{opt.label}
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onclick={(e) => { e.stopPropagation(); removeChip(opt.value) }}
|
|
184
|
+
class="hover:opacity-70 cursor-pointer"
|
|
185
|
+
aria-label="Retirer {opt.label}"
|
|
186
|
+
>
|
|
187
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
188
|
+
</button>
|
|
189
|
+
</span>
|
|
190
|
+
{/each}
|
|
191
|
+
{:else if !multiple && selectedLabel()}
|
|
192
|
+
<span class="{isDark ? 'text-white' : 'text-gray-900'}">{selectedLabel()}</span>
|
|
193
|
+
{:else}
|
|
194
|
+
<span class="{isDark ? 'text-gray-600' : 'text-gray-400'}">{placeholder}</span>
|
|
195
|
+
{/if}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Actions -->
|
|
199
|
+
<div class="flex items-center gap-1 shrink-0">
|
|
200
|
+
{#if clearable && (value || values.length > 0)}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onclick={(e) => { e.stopPropagation(); clearAll() }}
|
|
204
|
+
class="{isDark ? 'text-gray-600 hover:text-gray-400' : 'text-gray-400 hover:text-gray-600'} transition-colors cursor-pointer"
|
|
205
|
+
aria-label="Tout effacer"
|
|
206
|
+
>
|
|
207
|
+
<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"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
208
|
+
</button>
|
|
209
|
+
{/if}
|
|
210
|
+
<svg
|
|
211
|
+
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"
|
|
212
|
+
class="{isDark ? 'text-gray-600' : 'text-gray-400'} transition-transform duration-200 {open ? 'rotate-180' : ''}"
|
|
213
|
+
><path d="m6 9 6 6 6-6"/></svg>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Dropdown -->
|
|
218
|
+
{#if open}
|
|
219
|
+
<div
|
|
220
|
+
bind:this={dropdownEl}
|
|
221
|
+
class="absolute z-50 mt-1 w-full rounded-lg shadow-xl overflow-hidden"
|
|
222
|
+
style="background: {isDark ? 'var(--karbon-bg-card, #1a1a2e)' : '#fff'}; border: 1px solid {isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'};"
|
|
223
|
+
>
|
|
224
|
+
{#if searchable}
|
|
225
|
+
<div class="p-2" style="border-bottom: 1px solid {isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'};">
|
|
226
|
+
<input
|
|
227
|
+
type="text"
|
|
228
|
+
bind:value={search}
|
|
229
|
+
placeholder="Rechercher..."
|
|
230
|
+
class="w-full px-2.5 py-1.5 text-xs rounded-md outline-none"
|
|
231
|
+
style="background: {isDark ? 'rgba(255,255,255,0.05)' : '#f3f4f6'}; color: {isDark ? 'white' : '#111'};"
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
{/if}
|
|
235
|
+
|
|
236
|
+
<div class="max-h-60 overflow-y-auto py-1">
|
|
237
|
+
{#if filteredOptions.length === 0}
|
|
238
|
+
<div class="px-3 py-4 text-center text-xs {isDark ? 'text-gray-600' : 'text-gray-400'}">
|
|
239
|
+
Aucun resultat
|
|
240
|
+
</div>
|
|
241
|
+
{:else}
|
|
242
|
+
{#each filteredOptions as opt}
|
|
243
|
+
{@const isSelected = multiple ? values.includes(opt.value) : value === opt.value}
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onclick={() => selectOption(opt)}
|
|
247
|
+
disabled={opt.disabled}
|
|
248
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors cursor-pointer
|
|
249
|
+
{opt.disabled ? 'opacity-30 cursor-not-allowed' : ''}
|
|
250
|
+
{isSelected && !isDark ? 'bg-gray-50 font-medium' : ''}
|
|
251
|
+
{isSelected && isDark ? 'font-medium' : ''}"
|
|
252
|
+
style="color: {isDark ? (isSelected ? focusColor : 'rgba(255,255,255,0.8)') : (isSelected ? focusColor : '#374151')}; background: {isSelected ? `color-mix(in srgb, ${focusColor} 8%, transparent)` : 'transparent'};"
|
|
253
|
+
onmouseenter={(e) => { if (!opt.disabled) (e.currentTarget as HTMLElement).style.background = isDark ? 'rgba(255,255,255,0.05)' : '#f9fafb' }}
|
|
254
|
+
onmouseleave={(e) => { (e.currentTarget as HTMLElement).style.background = isSelected ? `color-mix(in srgb, ${focusColor} 8%, transparent)` : 'transparent' }}
|
|
255
|
+
>
|
|
256
|
+
{#if multiple}
|
|
257
|
+
<div
|
|
258
|
+
class="w-4 h-4 shrink-0 rounded border flex items-center justify-center transition-colors"
|
|
259
|
+
style="border-color: {isSelected ? focusColor : isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)'}; background: {isSelected ? focusColor : 'transparent'};"
|
|
260
|
+
>
|
|
261
|
+
{#if isSelected}
|
|
262
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
263
|
+
{/if}
|
|
264
|
+
</div>
|
|
265
|
+
{/if}
|
|
266
|
+
<span class="flex-1 truncate">{opt.label}</span>
|
|
267
|
+
{#if !multiple && isSelected}
|
|
268
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
269
|
+
{/if}
|
|
270
|
+
</button>
|
|
271
|
+
{/each}
|
|
272
|
+
{/if}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
64
275
|
{/if}
|
|
65
|
-
|
|
66
|
-
<option value={opt.value} disabled={opt.disabled}>{opt.label}</option>
|
|
67
|
-
{/each}
|
|
68
|
-
</select>
|
|
276
|
+
</div>
|
|
69
277
|
|
|
70
278
|
{#if error}
|
|
71
|
-
<p class="text-xs {
|
|
279
|
+
<p class="flex items-center gap-1.5 text-xs {isDark ? 'text-red-400' : 'text-[var(--karbon-danger)]'} {classes?.error ?? ''}">
|
|
280
|
+
{#if errorIcon}
|
|
281
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><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>
|
|
282
|
+
{/if}
|
|
283
|
+
<span>{error}</span>
|
|
284
|
+
</p>
|
|
72
285
|
{/if}
|
|
73
286
|
</div>
|
package/src/form/Slider.svelte
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { SliderClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
name: string
|
|
4
6
|
value?: number
|
|
@@ -8,6 +10,8 @@
|
|
|
8
10
|
label?: string
|
|
9
11
|
showValue?: boolean
|
|
10
12
|
disabled?: boolean
|
|
13
|
+
color?: ButtonColor
|
|
14
|
+
classes?: SliderClasses
|
|
11
15
|
class?: string
|
|
12
16
|
oninput?: (e: Event) => void
|
|
13
17
|
}
|
|
@@ -21,21 +25,24 @@
|
|
|
21
25
|
label = '',
|
|
22
26
|
showValue = true,
|
|
23
27
|
disabled = false,
|
|
28
|
+
color,
|
|
29
|
+
classes,
|
|
24
30
|
class: className = '',
|
|
25
31
|
oninput
|
|
26
32
|
}: Props = $props()
|
|
27
33
|
|
|
28
34
|
const percent = $derived(((value - min) / (max - min)) * 100)
|
|
35
|
+
const trackColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
29
36
|
</script>
|
|
30
37
|
|
|
31
|
-
<div class="space-y-2 {className}">
|
|
38
|
+
<div class="space-y-2 {classes?.root ?? ''} {className}">
|
|
32
39
|
{#if label || showValue}
|
|
33
40
|
<div class="flex items-center justify-between">
|
|
34
41
|
{#if label}
|
|
35
|
-
<label for={name} class="text-sm font-medium text-[var(--karbon-text,#1a1635)]">{label}</label>
|
|
42
|
+
<label for={name} class="text-sm font-medium text-[var(--karbon-text,#1a1635)] {classes?.label ?? ''}">{label}</label>
|
|
36
43
|
{/if}
|
|
37
44
|
{#if showValue}
|
|
38
|
-
<span class="text-sm font-semibold
|
|
45
|
+
<span class="text-sm font-semibold tabular-nums {classes?.value ?? ''}" style="color: {trackColor}">{value}</span>
|
|
39
46
|
{/if}
|
|
40
47
|
</div>
|
|
41
48
|
{/if}
|
|
@@ -51,12 +58,10 @@
|
|
|
51
58
|
{disabled}
|
|
52
59
|
{oninput}
|
|
53
60
|
class="w-full h-2 rounded-full appearance-none cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed
|
|
54
|
-
bg-[linear-gradient(to_right,var(--karbon-primary)_{percent}%,var(--karbon-border,rgba(0,0,0,0.07))_{percent}%)]
|
|
55
61
|
[&::-webkit-slider-thumb]:appearance-none
|
|
56
62
|
[&::-webkit-slider-thumb]:w-4.5
|
|
57
63
|
[&::-webkit-slider-thumb]:h-4.5
|
|
58
64
|
[&::-webkit-slider-thumb]:rounded-full
|
|
59
|
-
[&::-webkit-slider-thumb]:bg-[var(--karbon-primary)]
|
|
60
65
|
[&::-webkit-slider-thumb]:border-2
|
|
61
66
|
[&::-webkit-slider-thumb]:border-white
|
|
62
67
|
[&::-webkit-slider-thumb]:shadow-md
|
|
@@ -66,9 +71,19 @@
|
|
|
66
71
|
[&::-moz-range-thumb]:w-4
|
|
67
72
|
[&::-moz-range-thumb]:h-4
|
|
68
73
|
[&::-moz-range-thumb]:rounded-full
|
|
69
|
-
[&::-moz-range-thumb]:bg-[var(--karbon-primary)]
|
|
70
74
|
[&::-moz-range-thumb]:border-2
|
|
71
75
|
[&::-moz-range-thumb]:border-white
|
|
72
|
-
[&::-moz-range-thumb]:shadow-md
|
|
76
|
+
[&::-moz-range-thumb]:shadow-md
|
|
77
|
+
{classes?.input ?? ''}"
|
|
78
|
+
style="background: linear-gradient(to right, {trackColor} {percent}%, var(--karbon-border, rgba(0,0,0,0.07)) {percent}%); --thumb-color: {trackColor};"
|
|
73
79
|
/>
|
|
74
80
|
</div>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
84
|
+
background-color: var(--thumb-color, var(--karbon-primary));
|
|
85
|
+
}
|
|
86
|
+
input[type="range"]::-moz-range-thumb {
|
|
87
|
+
background-color: var(--thumb-color, var(--karbon-primary));
|
|
88
|
+
}
|
|
89
|
+
</style>
|
package/src/form/Textarea.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { FormVariant } from '@karbonjs/ui-core'
|
|
2
|
+
import type { FormVariant, FormInputVariant, TextareaClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
name: string
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
placeholder?: string
|
|
8
8
|
label?: string
|
|
9
9
|
error?: string
|
|
10
|
+
errorIcon?: boolean
|
|
10
11
|
rows?: number
|
|
11
12
|
maxlength?: number
|
|
12
13
|
showCount?: boolean
|
|
@@ -14,6 +15,9 @@
|
|
|
14
15
|
disabled?: boolean
|
|
15
16
|
readonly?: boolean
|
|
16
17
|
variant?: FormVariant
|
|
18
|
+
inputVariant?: FormInputVariant
|
|
19
|
+
color?: ButtonColor
|
|
20
|
+
classes?: TextareaClasses
|
|
17
21
|
class?: string
|
|
18
22
|
oninput?: (e: Event) => void
|
|
19
23
|
}
|
|
@@ -24,6 +28,7 @@
|
|
|
24
28
|
placeholder = '',
|
|
25
29
|
label = '',
|
|
26
30
|
error = '',
|
|
31
|
+
errorIcon = true,
|
|
27
32
|
rows = 4,
|
|
28
33
|
maxlength,
|
|
29
34
|
showCount = false,
|
|
@@ -31,32 +36,62 @@
|
|
|
31
36
|
disabled = false,
|
|
32
37
|
readonly = false,
|
|
33
38
|
variant = 'dark',
|
|
39
|
+
inputVariant = 'outlined',
|
|
40
|
+
color,
|
|
41
|
+
classes,
|
|
34
42
|
class: className = '',
|
|
35
43
|
oninput
|
|
36
44
|
}: Props = $props()
|
|
37
45
|
|
|
46
|
+
let focused = $state(false)
|
|
47
|
+
|
|
48
|
+
const focusColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
49
|
+
|
|
38
50
|
const themes = {
|
|
39
51
|
dark: {
|
|
40
52
|
label: 'text-[11px] font-medium text-gray-500 uppercase tracking-wider',
|
|
41
|
-
|
|
53
|
+
base: 'text-white placeholder-gray-700',
|
|
42
54
|
error: 'text-red-400',
|
|
43
|
-
count: 'text-gray-600'
|
|
55
|
+
count: 'text-gray-600',
|
|
56
|
+
variants: {
|
|
57
|
+
outlined: 'border border-white/8 bg-white/3',
|
|
58
|
+
filled: 'border-0 bg-white/8',
|
|
59
|
+
underline: 'rounded-none bg-transparent'
|
|
60
|
+
}
|
|
44
61
|
},
|
|
45
62
|
light: {
|
|
46
63
|
label: 'text-sm font-medium text-gray-700',
|
|
47
|
-
|
|
64
|
+
base: 'text-gray-900 placeholder-gray-400',
|
|
48
65
|
error: 'text-[var(--karbon-danger)]',
|
|
49
|
-
count: 'text-gray-400'
|
|
66
|
+
count: 'text-gray-400',
|
|
67
|
+
variants: {
|
|
68
|
+
outlined: 'border border-gray-300 bg-white',
|
|
69
|
+
filled: 'border-0 bg-gray-100',
|
|
70
|
+
underline: 'rounded-none bg-transparent'
|
|
71
|
+
}
|
|
50
72
|
}
|
|
51
73
|
} as const
|
|
52
74
|
|
|
53
75
|
const theme = $derived(themes[variant])
|
|
76
|
+
const variantClass = $derived(theme.variants[inputVariant])
|
|
54
77
|
const charCount = $derived(value.length)
|
|
78
|
+
|
|
79
|
+
const inlineStyle = $derived.by(() => {
|
|
80
|
+
let s = ''
|
|
81
|
+
if (inputVariant === 'underline') {
|
|
82
|
+
const bc = focused ? focusColor : error ? 'var(--karbon-danger)' : 'var(--karbon-border-input)'
|
|
83
|
+
s += `border:none;border-bottom:1px solid ${bc};border-radius:0;`
|
|
84
|
+
if (focused) s += 'box-shadow:none;'
|
|
85
|
+
} else if (focused) {
|
|
86
|
+
s += `border-color:${focusColor};box-shadow:0 0 0 3px color-mix(in srgb,${focusColor} 12%,transparent);`
|
|
87
|
+
}
|
|
88
|
+
return s
|
|
89
|
+
})
|
|
55
90
|
</script>
|
|
56
91
|
|
|
57
|
-
<div class="space-y-1.5 {className}">
|
|
92
|
+
<div class="space-y-1.5 {classes?.root ?? ''} {className}">
|
|
58
93
|
{#if label}
|
|
59
|
-
<label for={name} class="{theme.label} block mb-1.5">{label}</label>
|
|
94
|
+
<label for={name} class="{theme.label} block mb-1.5 {classes?.label ?? ''}">{label}</label>
|
|
60
95
|
{/if}
|
|
61
96
|
|
|
62
97
|
<textarea
|
|
@@ -70,17 +105,25 @@
|
|
|
70
105
|
{disabled}
|
|
71
106
|
{readonly}
|
|
72
107
|
{oninput}
|
|
73
|
-
|
|
108
|
+
onfocus={() => focused = true}
|
|
109
|
+
onblur={() => focused = false}
|
|
110
|
+
class="w-full {inputVariant !== 'underline' ? 'rounded-lg' : ''} px-3 py-2.5 text-[13px] md:text-sm focus:outline-none transition-all resize-y {theme.base} {variantClass} {error ? 'border-red-500/50' : ''} {disabled ? 'opacity-40 cursor-not-allowed pointer-events-none' : ''} {classes?.textarea ?? ''}"
|
|
111
|
+
style={inlineStyle}
|
|
74
112
|
></textarea>
|
|
75
113
|
|
|
76
114
|
<div class="flex items-center justify-between">
|
|
77
115
|
{#if error}
|
|
78
|
-
<p class="text-xs {theme.error}
|
|
116
|
+
<p class="flex items-center gap-1.5 text-xs {theme.error} {classes?.error ?? ''}">
|
|
117
|
+
{#if errorIcon}
|
|
118
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><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>
|
|
119
|
+
{/if}
|
|
120
|
+
<span>{error}</span>
|
|
121
|
+
</p>
|
|
79
122
|
{:else}
|
|
80
123
|
<span></span>
|
|
81
124
|
{/if}
|
|
82
125
|
{#if showCount && maxlength}
|
|
83
|
-
<span class="text-xs {theme.count}">{charCount}/{maxlength}</span>
|
|
126
|
+
<span class="text-xs {theme.count} {classes?.count ?? ''}">{charCount}/{maxlength}</span>
|
|
84
127
|
{/if}
|
|
85
128
|
</div>
|
|
86
129
|
</div>
|
package/src/form/Toggle.svelte
CHANGED
|
@@ -1,48 +1,102 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { ToggleClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
name: string
|
|
6
6
|
checked?: boolean
|
|
7
7
|
label?: string
|
|
8
|
-
|
|
8
|
+
description?: string
|
|
9
|
+
size?: 'sm' | 'md' | 'lg'
|
|
9
10
|
disabled?: boolean
|
|
11
|
+
color?: ButtonColor
|
|
12
|
+
showIcons?: boolean
|
|
13
|
+
classes?: ToggleClasses
|
|
10
14
|
class?: string
|
|
11
|
-
onchange?: (
|
|
15
|
+
onchange?: (checked: boolean) => void
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
let {
|
|
15
19
|
name,
|
|
16
20
|
checked = $bindable(false),
|
|
17
21
|
label = '',
|
|
22
|
+
description = '',
|
|
18
23
|
size = 'md',
|
|
19
24
|
disabled = false,
|
|
25
|
+
color,
|
|
26
|
+
showIcons = false,
|
|
27
|
+
classes,
|
|
20
28
|
class: className = '',
|
|
21
29
|
onchange
|
|
22
30
|
}: Props = $props()
|
|
23
31
|
|
|
24
|
-
const
|
|
25
|
-
sm: { track: 'w-8 h-[18px]', dot: 'h-3.5 w-3.5', translate: 'translate-x-3.5' },
|
|
26
|
-
md: { track: 'w-10 h-[22px]', dot: 'h-4.5 w-4.5', translate: 'translate-x-4.5' }
|
|
27
|
-
} as const
|
|
32
|
+
const trackColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
28
33
|
|
|
34
|
+
// pad = space between dot edge and track edge (both sides)
|
|
35
|
+
// translate = trackWidth - dotSize - (2 * pad)
|
|
36
|
+
const sizes = {
|
|
37
|
+
sm: { track: 'w-7 h-4', dot: 12, pad: 2, translate: 14, text: 'text-xs', desc: 'text-[11px]', iconSize: 7 },
|
|
38
|
+
md: { track: 'w-10 h-[22px]', dot: 16, pad: 3, translate: 19, text: 'text-sm', desc: 'text-xs', iconSize: 9 },
|
|
39
|
+
lg: { track: 'w-14 h-7', dot: 20, pad: 4, translate: 30, text: 'text-base', desc: 'text-sm', iconSize: 11 },
|
|
40
|
+
}
|
|
29
41
|
const s = $derived(sizes[size])
|
|
42
|
+
|
|
43
|
+
function toggle() {
|
|
44
|
+
if (disabled) return
|
|
45
|
+
checked = !checked
|
|
46
|
+
onchange?.(checked)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
50
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
toggle()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
30
55
|
</script>
|
|
31
56
|
|
|
32
|
-
<
|
|
33
|
-
|
|
57
|
+
<input type="hidden" {name} value={checked ? 'on' : ''} />
|
|
58
|
+
|
|
59
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
60
|
+
<div
|
|
61
|
+
class="inline-flex items-center gap-2.5 {disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} {classes?.root ?? ''} {className}"
|
|
62
|
+
onclick={toggle}
|
|
63
|
+
onkeydown={handleKeydown}
|
|
64
|
+
role="switch"
|
|
65
|
+
aria-checked={checked}
|
|
66
|
+
aria-disabled={disabled}
|
|
67
|
+
tabindex={disabled ? -1 : 0}
|
|
68
|
+
>
|
|
69
|
+
<!-- Track -->
|
|
34
70
|
<span
|
|
35
|
-
class="relative inline-flex shrink-0 items-center rounded-full transition-
|
|
36
|
-
|
|
37
|
-
{checked ? 'bg-[var(--karbon-primary)]' : 'bg-[var(--karbon-border,rgba(0,0,0,0.07))]'}
|
|
38
|
-
peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--karbon-primary)]/20"
|
|
39
|
-
aria-hidden="true"
|
|
71
|
+
class="relative inline-flex shrink-0 items-center rounded-full transition-all duration-200 {s.track} {classes?.track ?? ''}"
|
|
72
|
+
style="background: {checked ? trackColor : 'var(--karbon-border, rgba(255,255,255,0.12))'};{checked ? `box-shadow: 0 0 8px color-mix(in srgb, ${trackColor} 30%, transparent);` : ''}"
|
|
40
73
|
>
|
|
74
|
+
<!-- Icons inside track -->
|
|
75
|
+
{#if showIcons}
|
|
76
|
+
<span class="absolute left-1.5 top-1/2 -translate-y-1/2 transition-opacity duration-150 {checked ? 'opacity-100' : 'opacity-0'}">
|
|
77
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.iconSize} height={s.iconSize} viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
78
|
+
</span>
|
|
79
|
+
<span class="absolute right-1.5 top-1/2 -translate-y-1/2 transition-opacity duration-150 {checked ? 'opacity-0' : 'opacity-50'}">
|
|
80
|
+
<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="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
81
|
+
</span>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<!-- Dot -->
|
|
41
85
|
<span
|
|
42
|
-
class="inline-block rounded-full bg-white shadow-sm transition-transform duration-200 {
|
|
86
|
+
class="inline-block rounded-full bg-white shadow-sm transition-transform duration-200 {classes?.dot ?? ''}"
|
|
87
|
+
style="width: {s.dot}px; height: {s.dot}px; transform: translateX({checked ? s.translate : s.pad}px);"
|
|
43
88
|
></span>
|
|
44
89
|
</span>
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
|
|
91
|
+
<!-- Label + description -->
|
|
92
|
+
{#if label || description}
|
|
93
|
+
<div class="select-none min-w-0">
|
|
94
|
+
{#if label}
|
|
95
|
+
<span class="{s.text} font-medium text-[var(--karbon-text)] {classes?.label ?? ''}">{label}</span>
|
|
96
|
+
{/if}
|
|
97
|
+
{#if description}
|
|
98
|
+
<p class="{s.desc} text-[var(--karbon-text-3)] mt-0.5 leading-relaxed">{description}</p>
|
|
99
|
+
{/if}
|
|
100
|
+
</div>
|
|
47
101
|
{/if}
|
|
48
|
-
</
|
|
102
|
+
</div>
|