@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,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string
|
|
4
|
+
checked?: boolean
|
|
5
|
+
indeterminate?: boolean
|
|
6
|
+
label?: string
|
|
7
|
+
description?: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
class?: string
|
|
10
|
+
onchange?: (e: Event) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
name,
|
|
15
|
+
checked = $bindable(false),
|
|
16
|
+
indeterminate = false,
|
|
17
|
+
label = '',
|
|
18
|
+
description = '',
|
|
19
|
+
disabled = false,
|
|
20
|
+
class: className = '',
|
|
21
|
+
onchange
|
|
22
|
+
}: Props = $props()
|
|
23
|
+
|
|
24
|
+
let inputEl: HTMLInputElement
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (inputEl) inputEl.indeterminate = indeterminate
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<label class="inline-flex items-start gap-3 {disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} {className}">
|
|
32
|
+
<input
|
|
33
|
+
bind:this={inputEl}
|
|
34
|
+
type="checkbox"
|
|
35
|
+
{name}
|
|
36
|
+
bind:checked
|
|
37
|
+
{disabled}
|
|
38
|
+
{onchange}
|
|
39
|
+
class="mt-0.5 h-4 w-4 shrink-0 rounded border-[var(--karbon-border-input,rgba(255,255,255,0.10))] bg-[var(--karbon-bg-input,rgba(255,255,255,0.06))] text-[var(--karbon-primary)] focus:ring-2 focus:ring-[var(--karbon-primary)]/20 focus:ring-offset-0 transition-colors cursor-pointer disabled:cursor-not-allowed"
|
|
40
|
+
/>
|
|
41
|
+
{#if label || description}
|
|
42
|
+
<div class="select-none">
|
|
43
|
+
{#if label}
|
|
44
|
+
<span class="text-sm font-medium text-[var(--karbon-text,#1a1635)]">{label}</span>
|
|
45
|
+
{/if}
|
|
46
|
+
{#if description}
|
|
47
|
+
<p class="text-xs text-[var(--karbon-text-3,#8e8aae)] mt-0.5">{description}</p>
|
|
48
|
+
{/if}
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
</label>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
name: string
|
|
4
|
+
value?: string
|
|
5
|
+
label?: string
|
|
6
|
+
presets?: string[]
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
class?: string
|
|
9
|
+
onchange?: (e: Event) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const defaultPresets = [
|
|
13
|
+
'#cc1a1a', '#ef4444', '#f59e0b', '#22c55e', '#10b981',
|
|
14
|
+
'#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#ec4899',
|
|
15
|
+
'#f43f5e', '#14b8a6', '#000000', '#6b7280', '#ffffff'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
name,
|
|
20
|
+
value = $bindable('#cc1a1a'),
|
|
21
|
+
label = '',
|
|
22
|
+
presets = defaultPresets,
|
|
23
|
+
disabled = false,
|
|
24
|
+
class: className = '',
|
|
25
|
+
onchange
|
|
26
|
+
}: Props = $props()
|
|
27
|
+
|
|
28
|
+
let showPicker = $state(false)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<svelte:window onclick={() => showPicker = false} />
|
|
32
|
+
|
|
33
|
+
<div class="space-y-1.5 {className}">
|
|
34
|
+
{#if label}
|
|
35
|
+
<label for={name} class="text-sm font-medium text-[var(--karbon-text,#1a1635)] block mb-1.5">{label}</label>
|
|
36
|
+
{/if}
|
|
37
|
+
|
|
38
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
39
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
40
|
+
<div class="relative" onclick={(e) => e.stopPropagation()}>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onclick={() => { if (!disabled) showPicker = !showPicker }}
|
|
44
|
+
class="flex items-center gap-2.5 rounded-lg border border-[var(--karbon-border,rgba(0,0,0,0.07))] px-3 py-2 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))]"
|
|
45
|
+
{disabled}
|
|
46
|
+
>
|
|
47
|
+
<span
|
|
48
|
+
class="w-6 h-6 rounded-md border border-[var(--karbon-border,rgba(0,0,0,0.07))] shrink-0"
|
|
49
|
+
style="background: {value}"
|
|
50
|
+
></span>
|
|
51
|
+
<span class="text-sm font-mono text-[var(--karbon-text-2,#5a567e)]">{value}</span>
|
|
52
|
+
</button>
|
|
53
|
+
|
|
54
|
+
<input type="hidden" {name} bind:value {onchange} />
|
|
55
|
+
|
|
56
|
+
{#if showPicker}
|
|
57
|
+
<div class="absolute z-50 mt-1 w-64 rounded-xl border border-[var(--karbon-border,rgba(0,0,0,0.07))] shadow-xl p-4 bg-[var(--karbon-bg-card,#fff)]">
|
|
58
|
+
<!-- Native color input -->
|
|
59
|
+
<div class="mb-3">
|
|
60
|
+
<input
|
|
61
|
+
type="color"
|
|
62
|
+
bind:value
|
|
63
|
+
class="w-full h-10 rounded-lg cursor-pointer border-0 p-0"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Hex input -->
|
|
68
|
+
<div class="mb-3">
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
bind:value
|
|
72
|
+
maxlength="7"
|
|
73
|
+
class="w-full rounded-lg border border-[var(--karbon-border,rgba(0,0,0,0.07))] px-3 py-1.5 text-sm font-mono text-center focus:outline-none focus:ring-2 focus:ring-[var(--karbon-primary)]/20 bg-[var(--karbon-bg-input,rgba(255,255,255,0.06))] text-[var(--karbon-text,#1a1635)]"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Presets -->
|
|
78
|
+
{#if presets.length > 0}
|
|
79
|
+
<div class="grid grid-cols-5 gap-1.5">
|
|
80
|
+
{#each presets as color}
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onclick={() => value = color}
|
|
84
|
+
class="w-full aspect-square rounded-lg border transition-transform cursor-pointer hover:scale-110
|
|
85
|
+
{value === color ? 'border-[var(--karbon-primary)] ring-2 ring-[var(--karbon-primary)]/20 scale-110' : 'border-[var(--karbon-border,rgba(0,0,0,0.07))]'}"
|
|
86
|
+
style="background: {color}"
|
|
87
|
+
aria-label={color}
|
|
88
|
+
></button>
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FormVariant } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
name: string
|
|
6
|
+
value?: string
|
|
7
|
+
label?: string
|
|
8
|
+
error?: string
|
|
9
|
+
placeholder?: string
|
|
10
|
+
min?: string
|
|
11
|
+
max?: string
|
|
12
|
+
required?: boolean
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
variant?: FormVariant
|
|
15
|
+
class?: string
|
|
16
|
+
onchange?: (e: Event) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
name,
|
|
21
|
+
value = $bindable(''),
|
|
22
|
+
label = '',
|
|
23
|
+
error = '',
|
|
24
|
+
placeholder = '',
|
|
25
|
+
min = '',
|
|
26
|
+
max = '',
|
|
27
|
+
required = false,
|
|
28
|
+
disabled = false,
|
|
29
|
+
variant = 'dark',
|
|
30
|
+
class: className = '',
|
|
31
|
+
onchange
|
|
32
|
+
}: Props = $props()
|
|
33
|
+
|
|
34
|
+
let showCalendar = $state(false)
|
|
35
|
+
|
|
36
|
+
const themes = {
|
|
37
|
+
dark: {
|
|
38
|
+
label: 'text-[11px] font-medium text-gray-500 uppercase tracking-wider',
|
|
39
|
+
input: 'border-white/8 bg-white/3 text-white focus:border-[var(--karbon-primary)]/50 focus:bg-white/5 focus:ring-[3px] focus:ring-[var(--karbon-primary)]/8',
|
|
40
|
+
error: 'text-red-400',
|
|
41
|
+
calendar: 'bg-[var(--karbon-bg-card,#0a0820)] border-white/10 text-white',
|
|
42
|
+
dayHover: 'hover:bg-white/10',
|
|
43
|
+
dayOther: 'text-gray-600'
|
|
44
|
+
},
|
|
45
|
+
light: {
|
|
46
|
+
label: 'text-sm font-medium text-gray-700',
|
|
47
|
+
input: 'border-gray-300 bg-white text-gray-900 focus:border-[var(--karbon-primary)] focus:ring-2 focus:ring-[var(--karbon-primary)]/20',
|
|
48
|
+
error: 'text-[var(--karbon-danger)]',
|
|
49
|
+
calendar: 'bg-white border-gray-200 text-gray-900',
|
|
50
|
+
dayHover: 'hover:bg-gray-100',
|
|
51
|
+
dayOther: 'text-gray-300'
|
|
52
|
+
}
|
|
53
|
+
} as const
|
|
54
|
+
|
|
55
|
+
const theme = $derived(themes[variant])
|
|
56
|
+
|
|
57
|
+
// Calendar state
|
|
58
|
+
const selectedDate = $derived(value ? new Date(value + 'T00:00:00') : null)
|
|
59
|
+
let viewYear = $state(selectedDate?.getFullYear() ?? new Date().getFullYear())
|
|
60
|
+
let viewMonth = $state(selectedDate?.getMonth() ?? new Date().getMonth())
|
|
61
|
+
|
|
62
|
+
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
|
|
63
|
+
const dayNames = ['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di']
|
|
64
|
+
|
|
65
|
+
const calendarDays = $derived((() => {
|
|
66
|
+
const first = new Date(viewYear, viewMonth, 1)
|
|
67
|
+
const lastDay = new Date(viewYear, viewMonth + 1, 0).getDate()
|
|
68
|
+
let startDay = first.getDay() - 1
|
|
69
|
+
if (startDay < 0) startDay = 6
|
|
70
|
+
|
|
71
|
+
const days: { date: number; month: number; year: number; current: boolean }[] = []
|
|
72
|
+
|
|
73
|
+
// Previous month padding
|
|
74
|
+
const prevLastDay = new Date(viewYear, viewMonth, 0).getDate()
|
|
75
|
+
for (let i = startDay - 1; i >= 0; i--) {
|
|
76
|
+
days.push({ date: prevLastDay - i, month: viewMonth - 1, year: viewYear, current: false })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Current month
|
|
80
|
+
for (let i = 1; i <= lastDay; i++) {
|
|
81
|
+
days.push({ date: i, month: viewMonth, year: viewYear, current: true })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Next month padding
|
|
85
|
+
const remaining = 42 - days.length
|
|
86
|
+
for (let i = 1; i <= remaining; i++) {
|
|
87
|
+
days.push({ date: i, month: viewMonth + 1, year: viewYear, current: false })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return days
|
|
91
|
+
})())
|
|
92
|
+
|
|
93
|
+
function prevMonth() {
|
|
94
|
+
if (viewMonth === 0) { viewMonth = 11; viewYear-- }
|
|
95
|
+
else viewMonth--
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function nextMonth() {
|
|
99
|
+
if (viewMonth === 11) { viewMonth = 0; viewYear++ }
|
|
100
|
+
else viewMonth++
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function selectDay(day: { date: number; month: number; year: number }) {
|
|
104
|
+
const m = day.month < 0 ? 11 : day.month > 11 ? 0 : day.month
|
|
105
|
+
const y = day.month < 0 ? day.year - 1 : day.month > 11 ? day.year + 1 : day.year
|
|
106
|
+
const d = String(day.date).padStart(2, '0')
|
|
107
|
+
const mo = String(m + 1).padStart(2, '0')
|
|
108
|
+
value = `${y}-${mo}-${d}`
|
|
109
|
+
showCalendar = false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isSelected(day: { date: number; month: number; year: number }) {
|
|
113
|
+
if (!selectedDate) return false
|
|
114
|
+
const m = day.month < 0 ? 11 : day.month > 11 ? 0 : day.month
|
|
115
|
+
return selectedDate.getDate() === day.date && selectedDate.getMonth() === m && selectedDate.getFullYear() === day.year
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isToday(day: { date: number; month: number; year: number; current: boolean }) {
|
|
119
|
+
if (!day.current) return false
|
|
120
|
+
const now = new Date()
|
|
121
|
+
return day.date === now.getDate() && viewMonth === now.getMonth() && viewYear === now.getFullYear()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatDisplay(val: string): string {
|
|
125
|
+
if (!val) return ''
|
|
126
|
+
const d = new Date(val + 'T00:00:00')
|
|
127
|
+
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<svelte:window onclick={() => showCalendar = false} />
|
|
132
|
+
|
|
133
|
+
<div class="space-y-1.5 {className}">
|
|
134
|
+
{#if label}
|
|
135
|
+
<label for={name} class="{theme.label} block mb-1.5">{label}</label>
|
|
136
|
+
{/if}
|
|
137
|
+
|
|
138
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
139
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
140
|
+
<div class="relative" onclick={(e) => e.stopPropagation()}>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onclick={() => { if (!disabled) showCalendar = !showCalendar }}
|
|
144
|
+
class="w-full rounded-lg border px-3 py-2.5 md:py-3 text-[13px] md:text-sm text-left focus:outline-none transition-all {theme.input} {error ? 'border-red-500/50' : ''} {!value ? 'text-gray-500' : ''} cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
|
145
|
+
{disabled}
|
|
146
|
+
>
|
|
147
|
+
{value ? formatDisplay(value) : placeholder || 'Sélectionner une date'}
|
|
148
|
+
<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="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>
|
|
149
|
+
</button>
|
|
150
|
+
|
|
151
|
+
<input type="hidden" {name} bind:value {required} {onchange} />
|
|
152
|
+
|
|
153
|
+
{#if showCalendar}
|
|
154
|
+
<div class="absolute z-50 mt-1 w-72 rounded-xl border shadow-xl p-3 {theme.calendar}">
|
|
155
|
+
<!-- Header -->
|
|
156
|
+
<div class="flex items-center justify-between mb-3">
|
|
157
|
+
<button type="button" onclick={prevMonth} aria-label="Mois précédent" class="p-1 rounded-lg {theme.dayHover} cursor-pointer">
|
|
158
|
+
<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="m15 18-6-6 6-6"/></svg>
|
|
159
|
+
</button>
|
|
160
|
+
<span class="text-sm font-semibold">{monthNames[viewMonth]} {viewYear}</span>
|
|
161
|
+
<button type="button" onclick={nextMonth} aria-label="Mois suivant" class="p-1 rounded-lg {theme.dayHover} cursor-pointer">
|
|
162
|
+
<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="m9 18 6-6-6-6"/></svg>
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- Day names -->
|
|
167
|
+
<div class="grid grid-cols-7 mb-1">
|
|
168
|
+
{#each dayNames as d}
|
|
169
|
+
<div class="text-center text-[10px] font-medium text-[var(--karbon-text-4,#b5b2cc)] py-1">{d}</div>
|
|
170
|
+
{/each}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- Days grid -->
|
|
174
|
+
<div class="grid grid-cols-7">
|
|
175
|
+
{#each calendarDays as day}
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onclick={() => selectDay(day)}
|
|
179
|
+
class="h-8 w-full rounded-lg text-xs font-medium transition-colors cursor-pointer
|
|
180
|
+
{isSelected(day) ? 'bg-[var(--karbon-primary)] text-white' : ''}
|
|
181
|
+
{isToday(day) && !isSelected(day) ? 'ring-1 ring-[var(--karbon-primary)]' : ''}
|
|
182
|
+
{!day.current ? theme.dayOther : ''}
|
|
183
|
+
{!isSelected(day) ? theme.dayHover : ''}"
|
|
184
|
+
>
|
|
185
|
+
{day.date}
|
|
186
|
+
</button>
|
|
187
|
+
{/each}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{#if error}
|
|
194
|
+
<p class="text-xs {theme.error}">{error}</p>
|
|
195
|
+
{/if}
|
|
196
|
+
</div>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { FormVariant } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
name: string
|
|
7
|
+
type?: 'text' | 'email' | 'password' | 'search' | 'tel' | 'url' | 'number'
|
|
8
|
+
value?: string
|
|
9
|
+
placeholder?: string
|
|
10
|
+
label?: string
|
|
11
|
+
error?: string
|
|
12
|
+
required?: boolean
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
readonly?: boolean
|
|
15
|
+
autocomplete?: string
|
|
16
|
+
clearable?: boolean
|
|
17
|
+
icon?: Snippet
|
|
18
|
+
variant?: FormVariant
|
|
19
|
+
class?: string
|
|
20
|
+
inputClass?: string
|
|
21
|
+
labelClass?: string
|
|
22
|
+
wrapperClass?: string
|
|
23
|
+
oninput?: (e: Event) => void
|
|
24
|
+
onchange?: (e: Event) => void
|
|
25
|
+
onfocus?: (e: FocusEvent) => void
|
|
26
|
+
onblur?: (e: FocusEvent) => void
|
|
27
|
+
onkeydown?: (e: KeyboardEvent) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
name,
|
|
32
|
+
type = 'text',
|
|
33
|
+
value = $bindable(''),
|
|
34
|
+
placeholder = '',
|
|
35
|
+
label = '',
|
|
36
|
+
error = '',
|
|
37
|
+
required = false,
|
|
38
|
+
disabled = false,
|
|
39
|
+
readonly = false,
|
|
40
|
+
autocomplete = '',
|
|
41
|
+
clearable = false,
|
|
42
|
+
icon,
|
|
43
|
+
variant = 'dark',
|
|
44
|
+
class: className = '',
|
|
45
|
+
inputClass = '',
|
|
46
|
+
labelClass = '',
|
|
47
|
+
wrapperClass = '',
|
|
48
|
+
oninput,
|
|
49
|
+
onchange,
|
|
50
|
+
onfocus,
|
|
51
|
+
onblur,
|
|
52
|
+
onkeydown
|
|
53
|
+
}: Props = $props()
|
|
54
|
+
|
|
55
|
+
let focused = $state(false)
|
|
56
|
+
let showPassword = $state(false)
|
|
57
|
+
|
|
58
|
+
const isPassword = $derived(type === 'password')
|
|
59
|
+
const inputType = $derived(isPassword && showPassword ? 'text' : type)
|
|
60
|
+
const hasRightAction = $derived(isPassword || (clearable && value))
|
|
61
|
+
const hasIcon = $derived(!!icon)
|
|
62
|
+
|
|
63
|
+
function handleFocus(e: FocusEvent) {
|
|
64
|
+
focused = true
|
|
65
|
+
onfocus?.(e)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleBlur(e: FocusEvent) {
|
|
69
|
+
focused = false
|
|
70
|
+
onblur?.(e)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleClear() {
|
|
74
|
+
value = ''
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const themes = {
|
|
78
|
+
dark: {
|
|
79
|
+
label: 'text-[11px] font-medium text-gray-500 uppercase tracking-wider',
|
|
80
|
+
input: 'border-white/8 bg-white/3 text-white placeholder-gray-700 focus:border-[var(--karbon-primary)]/50 focus:bg-white/5 focus:ring-[3px] focus:ring-[var(--karbon-primary)]/8',
|
|
81
|
+
icon: 'text-gray-600',
|
|
82
|
+
iconFocused: 'text-[var(--karbon-primary)]',
|
|
83
|
+
action: 'text-gray-600 hover:text-gray-400',
|
|
84
|
+
error: 'text-red-400',
|
|
85
|
+
glow: true
|
|
86
|
+
},
|
|
87
|
+
light: {
|
|
88
|
+
label: 'text-sm font-medium text-gray-700',
|
|
89
|
+
input: 'border-gray-300 bg-white text-gray-900 placeholder-gray-400 focus:border-[var(--karbon-primary)] focus:ring-2 focus:ring-[var(--karbon-primary)]/20',
|
|
90
|
+
icon: 'text-gray-400',
|
|
91
|
+
iconFocused: 'text-[var(--karbon-primary)]',
|
|
92
|
+
action: 'text-gray-400 hover:text-gray-600',
|
|
93
|
+
error: 'text-[var(--karbon-danger)]',
|
|
94
|
+
glow: false
|
|
95
|
+
}
|
|
96
|
+
} as const
|
|
97
|
+
|
|
98
|
+
const theme = $derived(themes[variant])
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<div class="{wrapperClass || 'space-y-1.5'}">
|
|
102
|
+
{#if label}
|
|
103
|
+
<label for={name} class="{theme.label} block mb-1.5 {labelClass}">
|
|
104
|
+
{label}
|
|
105
|
+
</label>
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
<div class="relative {className}">
|
|
109
|
+
{#if icon}
|
|
110
|
+
<div
|
|
111
|
+
class="absolute left-3 top-1/2 -translate-y-1/2 transition-colors {focused ? theme.iconFocused : theme.icon}"
|
|
112
|
+
class:z-10={variant === 'dark'}
|
|
113
|
+
>
|
|
114
|
+
{@render icon()}
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
117
|
+
|
|
118
|
+
<input
|
|
119
|
+
id={name}
|
|
120
|
+
{name}
|
|
121
|
+
type={inputType}
|
|
122
|
+
bind:value
|
|
123
|
+
{placeholder}
|
|
124
|
+
{required}
|
|
125
|
+
{disabled}
|
|
126
|
+
{readonly}
|
|
127
|
+
{autocomplete}
|
|
128
|
+
{oninput}
|
|
129
|
+
{onchange}
|
|
130
|
+
{onkeydown}
|
|
131
|
+
onfocus={handleFocus}
|
|
132
|
+
onblur={handleBlur}
|
|
133
|
+
class="w-full rounded-lg border {hasIcon ? 'pl-9' : 'pl-3'} {hasRightAction ? 'pr-10' : 'pr-3'} py-2.5 md:py-3 text-[13px] md:text-sm focus:outline-none transition-all {theme.input} {error ? 'border-red-500/50' : ''} {variant === 'dark' ? 'relative z-[1]' : ''} {inputClass}"
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{#if variant === 'dark' && theme.glow}
|
|
137
|
+
<div
|
|
138
|
+
class="absolute -inset-px rounded-lg opacity-0 transition-opacity duration-300 pointer-events-none"
|
|
139
|
+
class:opacity-100={focused}
|
|
140
|
+
style="background: linear-gradient(135deg, rgba(204, 26, 26, 0.1), transparent 50%);"
|
|
141
|
+
></div>
|
|
142
|
+
{/if}
|
|
143
|
+
|
|
144
|
+
{#if isPassword}
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onclick={() => showPassword = !showPassword}
|
|
148
|
+
aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
|
149
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 {theme.action} transition-colors cursor-pointer z-10"
|
|
150
|
+
tabindex={-1}
|
|
151
|
+
>
|
|
152
|
+
{#if showPassword}
|
|
153
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>
|
|
154
|
+
{:else}
|
|
155
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
|
|
156
|
+
{/if}
|
|
157
|
+
</button>
|
|
158
|
+
{:else if clearable && value}
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onclick={handleClear}
|
|
162
|
+
aria-label="Effacer"
|
|
163
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 {theme.action} transition-colors cursor-pointer z-10"
|
|
164
|
+
tabindex={-1}
|
|
165
|
+
>
|
|
166
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" 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>
|
|
167
|
+
</button>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{#if error}
|
|
172
|
+
<p class="text-xs {theme.error}">{error}</p>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { RadioOption, RadioDirection } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
name: string
|
|
6
|
+
options: RadioOption[]
|
|
7
|
+
value?: string
|
|
8
|
+
label?: string
|
|
9
|
+
direction?: RadioDirection
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
class?: string
|
|
12
|
+
onchange?: (e: Event) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
name,
|
|
17
|
+
options,
|
|
18
|
+
value = $bindable(''),
|
|
19
|
+
label = '',
|
|
20
|
+
direction = 'column',
|
|
21
|
+
disabled = false,
|
|
22
|
+
class: className = '',
|
|
23
|
+
onchange
|
|
24
|
+
}: Props = $props()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<fieldset class={className} {disabled}>
|
|
28
|
+
{#if label}
|
|
29
|
+
<legend class="text-sm font-medium text-[var(--karbon-text,#1a1635)] mb-2">{label}</legend>
|
|
30
|
+
{/if}
|
|
31
|
+
|
|
32
|
+
<div class="{direction === 'row' ? 'flex flex-wrap items-center gap-4' : 'flex flex-col gap-2.5'}">
|
|
33
|
+
{#each options as opt}
|
|
34
|
+
<label class="inline-flex items-start gap-2.5 {opt.disabled || disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}">
|
|
35
|
+
<input
|
|
36
|
+
type="radio"
|
|
37
|
+
{name}
|
|
38
|
+
value={opt.value}
|
|
39
|
+
checked={value === opt.value}
|
|
40
|
+
disabled={opt.disabled || disabled}
|
|
41
|
+
{onchange}
|
|
42
|
+
onchange={() => value = opt.value}
|
|
43
|
+
class="mt-0.5 h-4 w-4 shrink-0 border-[var(--karbon-border-input,rgba(255,255,255,0.10))] bg-[var(--karbon-bg-input,rgba(255,255,255,0.06))] text-[var(--karbon-primary)] focus:ring-2 focus:ring-[var(--karbon-primary)]/20 focus:ring-offset-0 transition-colors cursor-pointer disabled:cursor-not-allowed"
|
|
44
|
+
/>
|
|
45
|
+
<div class="select-none">
|
|
46
|
+
<span class="text-sm font-medium text-[var(--karbon-text,#1a1635)]">{opt.label}</span>
|
|
47
|
+
{#if opt.description}
|
|
48
|
+
<p class="text-xs text-[var(--karbon-text-3,#8e8aae)] mt-0.5">{opt.description}</p>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
</label>
|
|
52
|
+
{/each}
|
|
53
|
+
</div>
|
|
54
|
+
</fieldset>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FormVariant, SelectOption } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
name: string
|
|
6
|
+
options: SelectOption[]
|
|
7
|
+
value?: string
|
|
8
|
+
placeholder?: string
|
|
9
|
+
label?: string
|
|
10
|
+
error?: string
|
|
11
|
+
required?: boolean
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
variant?: FormVariant
|
|
14
|
+
class?: string
|
|
15
|
+
onchange?: (e: Event) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
name,
|
|
20
|
+
options,
|
|
21
|
+
value = $bindable(''),
|
|
22
|
+
placeholder = '',
|
|
23
|
+
label = '',
|
|
24
|
+
error = '',
|
|
25
|
+
required = false,
|
|
26
|
+
disabled = false,
|
|
27
|
+
variant = 'dark',
|
|
28
|
+
class: className = '',
|
|
29
|
+
onchange
|
|
30
|
+
}: Props = $props()
|
|
31
|
+
|
|
32
|
+
const themes = {
|
|
33
|
+
dark: {
|
|
34
|
+
label: 'text-[11px] font-medium text-gray-500 uppercase tracking-wider',
|
|
35
|
+
select: 'border-white/8 bg-white/3 text-white focus:border-[var(--karbon-primary)]/50 focus:bg-white/5 focus:ring-[3px] focus:ring-[var(--karbon-primary)]/8',
|
|
36
|
+
error: 'text-red-400'
|
|
37
|
+
},
|
|
38
|
+
light: {
|
|
39
|
+
label: 'text-sm font-medium text-gray-700',
|
|
40
|
+
select: 'border-gray-300 bg-white text-gray-900 focus:border-[var(--karbon-primary)] focus:ring-2 focus:ring-[var(--karbon-primary)]/20',
|
|
41
|
+
error: 'text-[var(--karbon-danger)]'
|
|
42
|
+
}
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
const theme = $derived(themes[variant])
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div class="space-y-1.5 {className}">
|
|
49
|
+
{#if label}
|
|
50
|
+
<label for={name} class="{theme.label} block mb-1.5">{label}</label>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
<select
|
|
54
|
+
id={name}
|
|
55
|
+
{name}
|
|
56
|
+
bind:value
|
|
57
|
+
{required}
|
|
58
|
+
{disabled}
|
|
59
|
+
{onchange}
|
|
60
|
+
class="w-full rounded-lg border px-3 py-2.5 md:py-3 text-[13px] md:text-sm focus:outline-none transition-all appearance-none bg-[length:16px] bg-[right_0.75rem_center] bg-no-repeat bg-[url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2216%22 height=%2216%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%239ca3af%22 stroke-width=%222%22><path d=%22m6 9 6 6 6-6%22/></svg>')] {theme.select} {error ? 'border-red-500/50' : ''}"
|
|
61
|
+
>
|
|
62
|
+
{#if placeholder}
|
|
63
|
+
<option value="" disabled selected class="text-gray-500">{placeholder}</option>
|
|
64
|
+
{/if}
|
|
65
|
+
{#each options as opt}
|
|
66
|
+
<option value={opt.value} disabled={opt.disabled}>{opt.label}</option>
|
|
67
|
+
{/each}
|
|
68
|
+
</select>
|
|
69
|
+
|
|
70
|
+
{#if error}
|
|
71
|
+
<p class="text-xs {theme.error}">{error}</p>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|