@karbonjs/ui-svelte 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +321 -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 +862 -108
- 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/Checkbox.svelte
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { CheckboxClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
name: string
|
|
4
6
|
checked?: boolean
|
|
@@ -6,8 +8,14 @@
|
|
|
6
8
|
label?: string
|
|
7
9
|
description?: string
|
|
8
10
|
disabled?: boolean
|
|
11
|
+
color?: ButtonColor
|
|
12
|
+
size?: 'sm' | 'md' | 'lg'
|
|
13
|
+
shape?: 'square' | 'rounded' | 'circle'
|
|
14
|
+
icon?: 'check' | 'cross' | 'dash' | 'heart' | 'star' | 'circle' | 'eye'
|
|
15
|
+
variant?: 'filled' | 'outlined' | 'ghost' | 'elegant'
|
|
16
|
+
classes?: CheckboxClasses
|
|
9
17
|
class?: string
|
|
10
|
-
onchange?: (
|
|
18
|
+
onchange?: (checked: boolean) => void
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
let {
|
|
@@ -17,35 +25,119 @@
|
|
|
17
25
|
label = '',
|
|
18
26
|
description = '',
|
|
19
27
|
disabled = false,
|
|
28
|
+
color,
|
|
29
|
+
size = 'md',
|
|
30
|
+
shape = 'rounded',
|
|
31
|
+
icon = 'check',
|
|
32
|
+
variant = 'filled',
|
|
33
|
+
classes,
|
|
20
34
|
class: className = '',
|
|
21
35
|
onchange
|
|
22
36
|
}: Props = $props()
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
const accentColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
39
|
+
const accentHover = $derived(color ? `var(--karbon-${color}-400)` : 'var(--karbon-primary-hover, var(--karbon-primary))')
|
|
40
|
+
|
|
41
|
+
const sizeMap = {
|
|
42
|
+
sm: { box: 'w-3.5 h-3.5', icon: 10, text: 'text-xs', desc: 'text-[11px]', gap: 'gap-2' },
|
|
43
|
+
md: { box: 'w-4.5 h-4.5', icon: 13, text: 'text-sm', desc: 'text-xs', gap: 'gap-2.5' },
|
|
44
|
+
lg: { box: 'w-5.5 h-5.5', icon: 16, text: 'text-base', desc: 'text-sm', gap: 'gap-3' },
|
|
45
|
+
}
|
|
46
|
+
const s = $derived(sizeMap[size])
|
|
47
|
+
|
|
48
|
+
const shapeMap = { square: 'rounded-none', rounded: 'rounded', circle: 'rounded-full' }
|
|
49
|
+
const shapeClass = $derived(shapeMap[shape])
|
|
50
|
+
|
|
51
|
+
const isActive = $derived(checked || indeterminate)
|
|
52
|
+
|
|
53
|
+
const boxStyle = $derived.by(() => {
|
|
54
|
+
switch (variant) {
|
|
55
|
+
case 'filled':
|
|
56
|
+
return `background:${isActive ? accentColor : 'var(--karbon-bg-input,rgba(255,255,255,0.06))'};border:${isActive ? 'none' : '1.5px solid var(--karbon-border-input,rgba(255,255,255,0.12))'};box-shadow:${isActive ? `0 0 0 2px color-mix(in srgb,${accentColor} 20%,transparent)` : 'none'};`
|
|
57
|
+
case 'outlined':
|
|
58
|
+
return `background:transparent;border:2px solid ${isActive ? accentColor : 'var(--karbon-border-input,rgba(255,255,255,0.12))'};box-shadow:none;`
|
|
59
|
+
case 'ghost':
|
|
60
|
+
return `background:transparent;border:none;box-shadow:none;`
|
|
61
|
+
case 'elegant':
|
|
62
|
+
return `background:${isActive ? `color-mix(in srgb,${accentColor} 12%,transparent)` : 'transparent'};border:1.5px solid ${isActive ? accentColor : 'var(--karbon-border-input,rgba(255,255,255,0.12))'};box-shadow:${isActive ? `0 0 8px color-mix(in srgb,${accentColor} 15%,transparent)` : 'none'};`
|
|
63
|
+
default: return ''
|
|
64
|
+
}
|
|
65
|
+
})
|
|
25
66
|
|
|
26
|
-
$
|
|
27
|
-
|
|
67
|
+
const iconColor = $derived.by(() => {
|
|
68
|
+
switch (variant) {
|
|
69
|
+
case 'filled': return 'white'
|
|
70
|
+
case 'outlined': return accentColor
|
|
71
|
+
case 'ghost': return isActive ? accentColor : 'var(--karbon-text-4)'
|
|
72
|
+
case 'elegant': return accentColor
|
|
73
|
+
default: return 'white'
|
|
74
|
+
}
|
|
28
75
|
})
|
|
76
|
+
|
|
77
|
+
function toggle() {
|
|
78
|
+
if (disabled) return
|
|
79
|
+
checked = !checked
|
|
80
|
+
onchange?.(checked)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
84
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
toggle()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
29
89
|
</script>
|
|
30
90
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
91
|
+
<!-- Hidden real input for form submission -->
|
|
92
|
+
<input type="hidden" {name} value={checked ? 'on' : ''} />
|
|
93
|
+
|
|
94
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
95
|
+
<div
|
|
96
|
+
class="inline-flex items-start {s.gap} {disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} {classes?.root ?? ''} {className}"
|
|
97
|
+
onclick={toggle}
|
|
98
|
+
onkeydown={handleKeydown}
|
|
99
|
+
role="checkbox"
|
|
100
|
+
aria-checked={indeterminate ? 'mixed' : checked}
|
|
101
|
+
aria-disabled={disabled}
|
|
102
|
+
tabindex={disabled ? -1 : 0}
|
|
103
|
+
>
|
|
104
|
+
<!-- Custom checkbox box -->
|
|
105
|
+
<div
|
|
106
|
+
class="shrink-0 {s.box} {shapeClass} flex items-center justify-center transition-all duration-150 mt-0.5"
|
|
107
|
+
style={boxStyle}
|
|
108
|
+
>
|
|
109
|
+
{#if indeterminate}
|
|
110
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill="none" stroke={iconColor} stroke-width="3" stroke-linecap="round">
|
|
111
|
+
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
112
|
+
</svg>
|
|
113
|
+
{:else if checked}
|
|
114
|
+
{#if icon === 'check'}
|
|
115
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill="none" stroke={iconColor} stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
116
|
+
{:else if icon === 'cross'}
|
|
117
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill="none" stroke={iconColor} stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
118
|
+
{:else if icon === 'dash'}
|
|
119
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill="none" stroke={iconColor} stroke-width="3" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
120
|
+
{:else if icon === 'heart'}
|
|
121
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill={iconColor} stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
|
122
|
+
{:else if icon === 'star'}
|
|
123
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill={iconColor} stroke="none"><path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/></svg>
|
|
124
|
+
{:else if icon === 'circle'}
|
|
125
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={Math.round(s.icon * 0.6)} height={Math.round(s.icon * 0.6)} viewBox="0 0 24 24" fill={iconColor} stroke="none"><circle cx="12" cy="12" r="12"/></svg>
|
|
126
|
+
{:else if icon === 'eye'}
|
|
127
|
+
<svg xmlns="http://www.w3.org/2000/svg" width={s.icon} height={s.icon} viewBox="0 0 24 24" fill="none" stroke={iconColor} stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
128
|
+
{/if}
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Label + description -->
|
|
41
133
|
{#if label || description}
|
|
42
|
-
<div class="select-none">
|
|
134
|
+
<div class="select-none min-w-0">
|
|
43
135
|
{#if label}
|
|
44
|
-
<span class="text
|
|
136
|
+
<span class="{s.text} font-medium text-[var(--karbon-text)] {classes?.label ?? ''}">{label}</span>
|
|
45
137
|
{/if}
|
|
46
138
|
{#if description}
|
|
47
|
-
<p class="
|
|
139
|
+
<p class="{s.desc} text-[var(--karbon-text-3)] mt-0.5 leading-relaxed {classes?.description ?? ''}">{description}</p>
|
|
48
140
|
{/if}
|
|
49
141
|
</div>
|
|
50
142
|
{/if}
|
|
51
|
-
</
|
|
143
|
+
</div>
|
|
@@ -1,38 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { ColorPickerClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
const defaultPresets = [
|
|
5
|
+
'#cc1a1a', '#ef4444', '#f59e0b', '#22c55e', '#10b981',
|
|
6
|
+
'#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#ec4899',
|
|
7
|
+
'#f43f5e', '#14b8a6', '#000000', '#6b7280', '#ffffff'
|
|
8
|
+
]
|
|
9
|
+
|
|
2
10
|
interface Props {
|
|
3
11
|
name: string
|
|
4
12
|
value?: string
|
|
5
13
|
label?: string
|
|
6
14
|
presets?: string[]
|
|
7
15
|
disabled?: boolean
|
|
16
|
+
color?: ButtonColor
|
|
17
|
+
classes?: ColorPickerClasses
|
|
8
18
|
class?: string
|
|
9
19
|
onchange?: (e: Event) => void
|
|
10
20
|
}
|
|
11
21
|
|
|
12
|
-
const defaultPresets = [
|
|
13
|
-
'#cc1a1a', '#ef4444', '#f59e0b', '#22c55e', '#10b981',
|
|
14
|
-
'#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#ec4899',
|
|
15
|
-
'#f43f5e', '#14b8a6', '#000000', '#6b7280', '#ffffff'
|
|
16
|
-
]
|
|
17
|
-
|
|
18
22
|
let {
|
|
19
23
|
name,
|
|
20
24
|
value = $bindable('#cc1a1a'),
|
|
21
25
|
label = '',
|
|
22
26
|
presets = defaultPresets,
|
|
23
27
|
disabled = false,
|
|
28
|
+
color,
|
|
29
|
+
classes,
|
|
24
30
|
class: className = '',
|
|
25
31
|
onchange
|
|
26
32
|
}: Props = $props()
|
|
27
33
|
|
|
28
34
|
let showPicker = $state(false)
|
|
35
|
+
let hexFocused = $state(false)
|
|
36
|
+
|
|
37
|
+
const focusColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
29
38
|
</script>
|
|
30
39
|
|
|
31
40
|
<svelte:window onclick={() => showPicker = false} />
|
|
32
41
|
|
|
33
|
-
<div class="space-y-1.5 {className}">
|
|
42
|
+
<div class="space-y-1.5 {classes?.root ?? ''} {className}">
|
|
34
43
|
{#if label}
|
|
35
|
-
<label for={name} class="text-sm font-medium text-[var(--karbon-text,#1a1635)] block mb-1.5">{label}</label>
|
|
44
|
+
<label for={name} class="text-sm font-medium text-[var(--karbon-text,#1a1635)] block mb-1.5 {classes?.label ?? ''}">{label}</label>
|
|
36
45
|
{/if}
|
|
37
46
|
|
|
38
47
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -41,7 +50,7 @@
|
|
|
41
50
|
<button
|
|
42
51
|
type="button"
|
|
43
52
|
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))]"
|
|
53
|
+
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))] {classes?.trigger ?? ''}"
|
|
45
54
|
{disabled}
|
|
46
55
|
>
|
|
47
56
|
<span
|
|
@@ -54,7 +63,7 @@
|
|
|
54
63
|
<input type="hidden" {name} bind:value {onchange} />
|
|
55
64
|
|
|
56
65
|
{#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)]">
|
|
66
|
+
<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)] {classes?.dropdown ?? ''}">
|
|
58
67
|
<!-- Native color input -->
|
|
59
68
|
<div class="mb-3">
|
|
60
69
|
<input
|
|
@@ -70,21 +79,24 @@
|
|
|
70
79
|
type="text"
|
|
71
80
|
bind:value
|
|
72
81
|
maxlength="7"
|
|
73
|
-
|
|
82
|
+
onfocus={() => hexFocused = true}
|
|
83
|
+
onblur={() => hexFocused = false}
|
|
84
|
+
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 bg-[var(--karbon-bg-input,rgba(255,255,255,0.06))] text-[var(--karbon-text,#1a1635)] {classes?.hexInput ?? ''}"
|
|
85
|
+
style={hexFocused ? `border-color: ${focusColor}; box-shadow: 0 0 0 3px color-mix(in srgb, ${focusColor} 12%, transparent);` : ''}
|
|
74
86
|
/>
|
|
75
87
|
</div>
|
|
76
88
|
|
|
77
89
|
<!-- Presets -->
|
|
78
90
|
{#if presets.length > 0}
|
|
79
91
|
<div class="grid grid-cols-5 gap-1.5">
|
|
80
|
-
{#each presets as
|
|
92
|
+
{#each presets as presetColor}
|
|
81
93
|
<button
|
|
82
94
|
type="button"
|
|
83
|
-
onclick={() => value =
|
|
95
|
+
onclick={() => value = presetColor}
|
|
84
96
|
class="w-full aspect-square rounded-lg border transition-transform cursor-pointer hover:scale-110
|
|
85
|
-
{value ===
|
|
86
|
-
style=
|
|
87
|
-
aria-label={
|
|
97
|
+
{value === presetColor ? 'scale-110' : 'border-[var(--karbon-border,rgba(0,0,0,0.07))]'}"
|
|
98
|
+
style={value === presetColor ? `border-color: ${focusColor}; box-shadow: 0 0 0 2px color-mix(in srgb, ${focusColor} 20%, transparent); background: ${presetColor}` : `background: ${presetColor}`}
|
|
99
|
+
aria-label={presetColor}
|
|
88
100
|
></button>
|
|
89
101
|
{/each}
|
|
90
102
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { FormVariant } from '@karbonjs/ui-core'
|
|
2
|
+
import type { FormVariant, DatePickerClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
name: string
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
required?: boolean
|
|
13
13
|
disabled?: boolean
|
|
14
14
|
variant?: FormVariant
|
|
15
|
+
color?: ButtonColor
|
|
16
|
+
classes?: DatePickerClasses
|
|
15
17
|
class?: string
|
|
16
18
|
onchange?: (e: Event) => void
|
|
17
19
|
}
|
|
@@ -27,16 +29,21 @@
|
|
|
27
29
|
required = false,
|
|
28
30
|
disabled = false,
|
|
29
31
|
variant = 'dark',
|
|
32
|
+
color,
|
|
33
|
+
classes,
|
|
30
34
|
class: className = '',
|
|
31
35
|
onchange
|
|
32
36
|
}: Props = $props()
|
|
33
37
|
|
|
34
38
|
let showCalendar = $state(false)
|
|
39
|
+
let focused = $state(false)
|
|
40
|
+
|
|
41
|
+
const focusColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
35
42
|
|
|
36
43
|
const themes = {
|
|
37
44
|
dark: {
|
|
38
45
|
label: 'text-[11px] font-medium text-gray-500 uppercase tracking-wider',
|
|
39
|
-
input: 'border-white/8 bg-white/3 text-white
|
|
46
|
+
input: 'border-white/8 bg-white/3 text-white',
|
|
40
47
|
error: 'text-red-400',
|
|
41
48
|
calendar: 'bg-[var(--karbon-bg-card,#0a0820)] border-white/10 text-white',
|
|
42
49
|
dayHover: 'hover:bg-white/10',
|
|
@@ -44,7 +51,7 @@
|
|
|
44
51
|
},
|
|
45
52
|
light: {
|
|
46
53
|
label: 'text-sm font-medium text-gray-700',
|
|
47
|
-
input: 'border-gray-300 bg-white text-gray-900
|
|
54
|
+
input: 'border-gray-300 bg-white text-gray-900',
|
|
48
55
|
error: 'text-[var(--karbon-danger)]',
|
|
49
56
|
calendar: 'bg-white border-gray-200 text-gray-900',
|
|
50
57
|
dayHover: 'hover:bg-gray-100',
|
|
@@ -137,9 +144,9 @@
|
|
|
137
144
|
|
|
138
145
|
<svelte:window onclick={() => showCalendar = false} />
|
|
139
146
|
|
|
140
|
-
<div class="space-y-1.5 {className}">
|
|
147
|
+
<div class="space-y-1.5 {classes?.root ?? ''} {className}">
|
|
141
148
|
{#if label}
|
|
142
|
-
<label for={name} class="{theme.label} block mb-1.5">{label}</label>
|
|
149
|
+
<label for={name} class="{theme.label} block mb-1.5 {classes?.label ?? ''}">{label}</label>
|
|
143
150
|
{/if}
|
|
144
151
|
|
|
145
152
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -148,7 +155,10 @@
|
|
|
148
155
|
<button
|
|
149
156
|
type="button"
|
|
150
157
|
onclick={() => { if (!disabled) showCalendar = !showCalendar }}
|
|
151
|
-
|
|
158
|
+
onfocus={() => focused = true}
|
|
159
|
+
onblur={() => focused = false}
|
|
160
|
+
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 {classes?.trigger ?? ''}"
|
|
161
|
+
style={focused ? `border-color: ${focusColor}; box-shadow: 0 0 0 3px color-mix(in srgb, ${focusColor} 12%, transparent);` : ''}
|
|
152
162
|
{disabled}
|
|
153
163
|
>
|
|
154
164
|
{value ? formatDisplay(value) : placeholder || 'Sélectionner une date'}
|
|
@@ -158,7 +168,7 @@
|
|
|
158
168
|
<input type="hidden" {name} bind:value {required} {onchange} />
|
|
159
169
|
|
|
160
170
|
{#if showCalendar}
|
|
161
|
-
<div class="absolute z-50 mt-1 w-72 rounded-xl border shadow-xl p-3 {theme.calendar}">
|
|
171
|
+
<div class="absolute z-50 mt-1 w-72 rounded-xl border shadow-xl p-3 {theme.calendar} {classes?.calendar ?? ''}">
|
|
162
172
|
<!-- Header -->
|
|
163
173
|
<div class="flex items-center justify-between mb-3">
|
|
164
174
|
<button type="button" onclick={prevMonth} aria-label="Mois précédent" class="p-1 rounded-lg {theme.dayHover} cursor-pointer">
|
|
@@ -184,10 +194,10 @@
|
|
|
184
194
|
type="button"
|
|
185
195
|
onclick={() => selectDay(day)}
|
|
186
196
|
class="h-8 w-full rounded-lg text-xs font-medium transition-colors cursor-pointer
|
|
187
|
-
{isSelected(day) ? '
|
|
188
|
-
{isToday(day) && !isSelected(day) ? 'ring-1 ring-[var(--karbon-primary)]' : ''}
|
|
197
|
+
{isToday(day) && !isSelected(day) ? 'ring-1' : ''}
|
|
189
198
|
{!day.current ? theme.dayOther : ''}
|
|
190
199
|
{!isSelected(day) ? theme.dayHover : ''}"
|
|
200
|
+
style={isSelected(day) ? `background-color: ${focusColor}; color: white;` : isToday(day) ? `--tw-ring-color: ${focusColor};` : ''}
|
|
191
201
|
>
|
|
192
202
|
{day.date}
|
|
193
203
|
</button>
|
|
@@ -198,6 +208,6 @@
|
|
|
198
208
|
</div>
|
|
199
209
|
|
|
200
210
|
{#if error}
|
|
201
|
-
<p class="text-xs {theme.error}">{error}</p>
|
|
211
|
+
<p class="text-xs {theme.error} {classes?.error ?? ''}">{error}</p>
|
|
202
212
|
{/if}
|
|
203
213
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
|
-
import type { FormVariant } from '@karbonjs/ui-core'
|
|
3
|
+
import type { FormVariant, FormInputVariant, FormInputClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
name: string
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
placeholder?: string
|
|
10
10
|
label?: string
|
|
11
11
|
error?: string
|
|
12
|
+
errorIcon?: boolean
|
|
12
13
|
required?: boolean
|
|
13
14
|
disabled?: boolean
|
|
14
15
|
readonly?: boolean
|
|
@@ -16,6 +17,9 @@
|
|
|
16
17
|
clearable?: boolean
|
|
17
18
|
icon?: Snippet
|
|
18
19
|
variant?: FormVariant
|
|
20
|
+
inputVariant?: FormInputVariant
|
|
21
|
+
color?: ButtonColor
|
|
22
|
+
classes?: FormInputClasses
|
|
19
23
|
class?: string
|
|
20
24
|
inputClass?: string
|
|
21
25
|
labelClass?: string
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
placeholder = '',
|
|
35
39
|
label = '',
|
|
36
40
|
error = '',
|
|
41
|
+
errorIcon = true,
|
|
37
42
|
required = false,
|
|
38
43
|
disabled = false,
|
|
39
44
|
readonly = false,
|
|
@@ -41,6 +46,9 @@
|
|
|
41
46
|
clearable = false,
|
|
42
47
|
icon,
|
|
43
48
|
variant = 'dark',
|
|
49
|
+
inputVariant = 'outlined',
|
|
50
|
+
color,
|
|
51
|
+
classes,
|
|
44
52
|
class: className = '',
|
|
45
53
|
inputClass = '',
|
|
46
54
|
labelClass = '',
|
|
@@ -60,6 +68,9 @@
|
|
|
60
68
|
const hasRightAction = $derived(isPassword || (clearable && value))
|
|
61
69
|
const hasIcon = $derived(!!icon)
|
|
62
70
|
|
|
71
|
+
const focusColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
72
|
+
const focusRingColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
73
|
+
|
|
63
74
|
function handleFocus(e: FocusEvent) {
|
|
64
75
|
focused = true
|
|
65
76
|
onfocus?.(e)
|
|
@@ -77,30 +88,39 @@
|
|
|
77
88
|
const themes = {
|
|
78
89
|
dark: {
|
|
79
90
|
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
91
|
icon: 'text-gray-600',
|
|
82
|
-
iconFocused: 'text-[var(--karbon-primary)]',
|
|
83
92
|
action: 'text-gray-600 hover:text-gray-400',
|
|
84
93
|
error: 'text-red-400',
|
|
85
|
-
glow: true
|
|
94
|
+
glow: true,
|
|
95
|
+
base: 'text-white placeholder-gray-700',
|
|
96
|
+
variants: {
|
|
97
|
+
outlined: 'border border-white/8 bg-white/3',
|
|
98
|
+
filled: 'border-0 bg-white/8',
|
|
99
|
+
underline: 'rounded-none bg-transparent'
|
|
100
|
+
}
|
|
86
101
|
},
|
|
87
102
|
light: {
|
|
88
103
|
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
104
|
icon: 'text-gray-400',
|
|
91
|
-
iconFocused: 'text-[var(--karbon-primary)]',
|
|
92
105
|
action: 'text-gray-400 hover:text-gray-600',
|
|
93
106
|
error: 'text-[var(--karbon-danger)]',
|
|
94
|
-
glow: false
|
|
107
|
+
glow: false,
|
|
108
|
+
base: 'text-gray-900 placeholder-gray-400',
|
|
109
|
+
variants: {
|
|
110
|
+
outlined: 'border border-gray-300 bg-white',
|
|
111
|
+
filled: 'border-0 bg-gray-100',
|
|
112
|
+
underline: 'rounded-none bg-transparent'
|
|
113
|
+
}
|
|
95
114
|
}
|
|
96
115
|
} as const
|
|
97
116
|
|
|
98
117
|
const theme = $derived(themes[variant])
|
|
118
|
+
const variantClass = $derived(theme.variants[inputVariant])
|
|
99
119
|
</script>
|
|
100
120
|
|
|
101
|
-
<div class="{wrapperClass || 'space-y-1.5'}">
|
|
121
|
+
<div class="{classes?.root ?? ''} {wrapperClass || 'space-y-1.5'}">
|
|
102
122
|
{#if label}
|
|
103
|
-
<label for={name} class="{theme.label} block mb-1.5 {labelClass}">
|
|
123
|
+
<label for={name} class="{theme.label} block mb-1.5 {classes?.label ?? ''} {labelClass}">
|
|
104
124
|
{label}
|
|
105
125
|
</label>
|
|
106
126
|
{/if}
|
|
@@ -108,8 +128,9 @@
|
|
|
108
128
|
<div class="relative {className}">
|
|
109
129
|
{#if icon}
|
|
110
130
|
<div
|
|
111
|
-
class="absolute left-3 top-1/2 -translate-y-1/2 transition-colors {focused ?
|
|
131
|
+
class="absolute left-3 top-1/2 -translate-y-1/2 transition-colors {classes?.icon ?? ''} {focused ? '' : theme.icon}"
|
|
112
132
|
class:z-10={variant === 'dark'}
|
|
133
|
+
style={focused ? `color: ${focusColor}` : ''}
|
|
113
134
|
>
|
|
114
135
|
{@render icon()}
|
|
115
136
|
</div>
|
|
@@ -130,14 +151,15 @@
|
|
|
130
151
|
{onkeydown}
|
|
131
152
|
onfocus={handleFocus}
|
|
132
153
|
onblur={handleBlur}
|
|
133
|
-
class="w-full rounded-lg
|
|
154
|
+
class="w-full {inputVariant !== 'underline' ? 'rounded-lg' : ''} {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.base} {variantClass} {error ? 'border-red-500/50' : ''} {disabled ? 'opacity-40 cursor-not-allowed pointer-events-none' : ''} {variant === 'dark' ? 'relative z-[1]' : ''} {classes?.input ?? ''} {inputClass}"
|
|
155
|
+
style="{inputVariant === 'underline' ? 'border:none;border-bottom:1px solid ' + (focused ? focusColor : error ? 'var(--karbon-danger)' : 'var(--karbon-border-input)') + ';border-radius:0;' : ''}{focused ? (inputVariant === 'underline' ? 'box-shadow:none;' : 'border-color:' + focusColor + ';box-shadow:0 0 0 3px color-mix(in srgb,' + focusRingColor + ' 12%,transparent);') : ''}"
|
|
134
156
|
/>
|
|
135
157
|
|
|
136
|
-
{#if variant === 'dark' && theme.glow}
|
|
158
|
+
{#if variant === 'dark' && theme.glow && inputVariant === 'outlined'}
|
|
137
159
|
<div
|
|
138
160
|
class="absolute -inset-px rounded-lg opacity-0 transition-opacity duration-300 pointer-events-none"
|
|
139
161
|
class:opacity-100={focused}
|
|
140
|
-
style="background: linear-gradient(135deg,
|
|
162
|
+
style="background: linear-gradient(135deg, color-mix(in srgb, {focusColor} 10%, transparent), transparent 50%);"
|
|
141
163
|
></div>
|
|
142
164
|
{/if}
|
|
143
165
|
|
|
@@ -169,6 +191,11 @@
|
|
|
169
191
|
</div>
|
|
170
192
|
|
|
171
193
|
{#if error}
|
|
172
|
-
<p class="text-xs {theme.error}
|
|
194
|
+
<p class="flex items-center gap-1.5 text-xs {theme.error} {classes?.error ?? ''}">
|
|
195
|
+
{#if errorIcon}
|
|
196
|
+
<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>
|
|
197
|
+
{/if}
|
|
198
|
+
<span>{error}</span>
|
|
199
|
+
</p>
|
|
173
200
|
{/if}
|
|
174
201
|
</div>
|
package/src/form/Radio.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { RadioOption, RadioDirection } from '@karbonjs/ui-core'
|
|
2
|
+
import type { RadioOption, RadioDirection, RadioClasses, ButtonColor } from '@karbonjs/ui-core'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
name: string
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
label?: string
|
|
9
9
|
direction?: RadioDirection
|
|
10
10
|
disabled?: boolean
|
|
11
|
+
color?: ButtonColor
|
|
12
|
+
size?: 'sm' | 'md' | 'lg'
|
|
13
|
+
variant?: 'filled' | 'outlined' | 'ghost' | 'elegant'
|
|
14
|
+
classes?: RadioClasses
|
|
11
15
|
class?: string
|
|
12
|
-
onchange?: (
|
|
16
|
+
onchange?: (value: string) => void
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
let {
|
|
@@ -19,35 +23,99 @@
|
|
|
19
23
|
label = '',
|
|
20
24
|
direction = 'column',
|
|
21
25
|
disabled = false,
|
|
26
|
+
color,
|
|
27
|
+
size = 'md',
|
|
28
|
+
variant = 'filled',
|
|
29
|
+
classes,
|
|
22
30
|
class: className = '',
|
|
23
31
|
onchange
|
|
24
32
|
}: Props = $props()
|
|
33
|
+
|
|
34
|
+
const accentColor = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
35
|
+
|
|
36
|
+
const sizeMap = {
|
|
37
|
+
sm: { box: 16, dot: 8, text: 'text-xs', desc: 'text-[11px]', gap: 'gap-2' },
|
|
38
|
+
md: { box: 20, dot: 11, text: 'text-sm', desc: 'text-xs', gap: 'gap-2.5' },
|
|
39
|
+
lg: { box: 24, dot: 13, text: 'text-base', desc: 'text-sm', gap: 'gap-3' },
|
|
40
|
+
}
|
|
41
|
+
const s = $derived(sizeMap[size])
|
|
42
|
+
|
|
43
|
+
function boxStyle(selected: boolean): string {
|
|
44
|
+
switch (variant) {
|
|
45
|
+
case 'filled':
|
|
46
|
+
return `background:${selected ? accentColor : 'var(--karbon-bg-input,rgba(255,255,255,0.06))'};border:${selected ? 'none' : '1.5px solid var(--karbon-border-input,rgba(255,255,255,0.12))'};box-shadow:${selected ? `0 0 0 2px color-mix(in srgb,${accentColor} 20%,transparent)` : 'none'};`
|
|
47
|
+
case 'outlined':
|
|
48
|
+
return `background:transparent;border:2px solid ${selected ? accentColor : 'var(--karbon-border-input,rgba(255,255,255,0.12))'};`
|
|
49
|
+
case 'ghost':
|
|
50
|
+
return `background:transparent;border:none;`
|
|
51
|
+
case 'elegant':
|
|
52
|
+
return `background:${selected ? `color-mix(in srgb,${accentColor} 12%,transparent)` : 'transparent'};border:1.5px solid ${selected ? accentColor : 'var(--karbon-border-input,rgba(255,255,255,0.12))'};box-shadow:${selected ? `0 0 8px color-mix(in srgb,${accentColor} 15%,transparent)` : 'none'};`
|
|
53
|
+
default: return ''
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dotColor(selected: boolean): string {
|
|
58
|
+
switch (variant) {
|
|
59
|
+
case 'filled': return 'white'
|
|
60
|
+
case 'outlined': return accentColor
|
|
61
|
+
case 'ghost': return selected ? accentColor : 'var(--karbon-text-4)'
|
|
62
|
+
case 'elegant': return accentColor
|
|
63
|
+
default: return 'white'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function select(val: string) {
|
|
68
|
+
if (disabled) return
|
|
69
|
+
value = val
|
|
70
|
+
onchange?.(val)
|
|
71
|
+
}
|
|
25
72
|
</script>
|
|
26
73
|
|
|
27
|
-
<fieldset class={className} {disabled}>
|
|
74
|
+
<fieldset class="{classes?.root ?? ''} {className}" {disabled}>
|
|
28
75
|
{#if label}
|
|
29
|
-
<legend class="text
|
|
76
|
+
<legend class="{s.text} font-medium text-[var(--karbon-text)] mb-2 {classes?.legend ?? ''}">{label}</legend>
|
|
30
77
|
{/if}
|
|
31
78
|
|
|
32
79
|
<div class="{direction === 'row' ? 'flex flex-wrap items-center gap-4' : 'flex flex-col gap-2.5'}">
|
|
33
80
|
{#each options as opt}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
{@const selected = value === opt.value}
|
|
82
|
+
{@const isDisabled = opt.disabled || disabled}
|
|
83
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
84
|
+
<div
|
|
85
|
+
class="inline-flex items-center {s.gap} {isDisabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}"
|
|
86
|
+
onclick={() => { if (!isDisabled) select(opt.value) }}
|
|
87
|
+
onkeydown={(e) => { if (!isDisabled && (e.key === ' ' || e.key === 'Enter')) { e.preventDefault(); select(opt.value) } }}
|
|
88
|
+
role="radio"
|
|
89
|
+
aria-checked={selected}
|
|
90
|
+
aria-disabled={isDisabled}
|
|
91
|
+
tabindex={isDisabled ? -1 : 0}
|
|
92
|
+
>
|
|
93
|
+
<!-- Custom radio circle -->
|
|
94
|
+
<div
|
|
95
|
+
class="shrink-0 rounded-full transition-all duration-150"
|
|
96
|
+
style="width:{s.box}px;height:{s.box}px;display:grid;place-items:center;box-sizing:border-box;{boxStyle(selected)}"
|
|
97
|
+
>
|
|
98
|
+
{#if selected}
|
|
99
|
+
<div
|
|
100
|
+
class="rounded-full"
|
|
101
|
+
style="width:{s.dot}px;height:{s.dot}px;background:{dotColor(selected)};"
|
|
102
|
+
></div>
|
|
103
|
+
{:else if variant === 'ghost'}
|
|
104
|
+
<div
|
|
105
|
+
class="rounded-full"
|
|
106
|
+
style="width:{s.dot}px;height:{s.dot}px;background:var(--karbon-border-input,rgba(255,255,255,0.12));"
|
|
107
|
+
></div>
|
|
108
|
+
{/if}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Label + description -->
|
|
112
|
+
<div class="select-none min-w-0">
|
|
113
|
+
<span class="{s.text} font-medium text-[var(--karbon-text)] {classes?.label ?? ''}">{opt.label}</span>
|
|
46
114
|
{#if opt.description}
|
|
47
|
-
<p class="
|
|
115
|
+
<p class="{s.desc} text-[var(--karbon-text-3)] mt-0.5 leading-relaxed {classes?.description ?? ''}">{opt.description}</p>
|
|
48
116
|
{/if}
|
|
49
117
|
</div>
|
|
50
|
-
</
|
|
118
|
+
</div>
|
|
51
119
|
{/each}
|
|
52
120
|
</div>
|
|
53
121
|
</fieldset>
|