@lyeve-labs/ui-kit 0.8.4 → 0.10.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/dist/components/AccordionItem.svelte +1 -1
- package/dist/components/Alert.svelte +42 -15
- package/dist/components/Alert.svelte.d.ts +2 -2
- package/dist/components/Autocomplete.svelte +32 -12
- package/dist/components/Avatar.svelte +4 -8
- package/dist/components/Badge.svelte +4 -4
- package/dist/components/Badge.svelte.d.ts +2 -2
- package/dist/components/Banner.svelte +10 -9
- package/dist/components/Banner.svelte.d.ts +2 -2
- package/dist/components/Breadcrumb.svelte +4 -1
- package/dist/components/Button.svelte +4 -2
- package/dist/components/Button.svelte.d.ts +1 -0
- package/dist/components/Card.svelte +1 -1
- package/dist/components/Checkbox.svelte +7 -4
- package/dist/components/Checkbox.svelte.d.ts +1 -0
- package/dist/components/DatePicker.svelte +19 -11
- package/dist/components/Drawer.svelte +13 -2
- package/dist/components/Dropdown.svelte +1 -1
- package/dist/components/FileInput.svelte +14 -5
- package/dist/components/Indicator.svelte +3 -3
- package/dist/components/Indicator.svelte.d.ts +2 -2
- package/dist/components/Input.svelte +17 -8
- package/dist/components/Modal.svelte +11 -2
- package/dist/components/MultiSelect.svelte +33 -12
- package/dist/components/NumberInput.svelte +68 -13
- package/dist/components/NumberInput.svelte.d.ts +3 -0
- package/dist/components/PageHeader.svelte +3 -1
- package/dist/components/Pagination.svelte +1 -1
- package/dist/components/Progress.svelte +4 -3
- package/dist/components/Progress.svelte.d.ts +2 -2
- package/dist/components/Radio.svelte +6 -3
- package/dist/components/Radio.svelte.d.ts +1 -0
- package/dist/components/RadioGroup.svelte +6 -5
- package/dist/components/SearchInput.svelte +64 -49
- package/dist/components/SearchInput.svelte.d.ts +3 -0
- package/dist/components/Select.svelte +29 -12
- package/dist/components/Select.svelte.d.ts +2 -0
- package/dist/components/StepIndicator.svelte +7 -5
- package/dist/components/Table.svelte +3 -1
- package/dist/components/Tabs.svelte +1 -1
- package/dist/components/Tag.svelte +3 -3
- package/dist/components/Tag.svelte.d.ts +2 -2
- package/dist/components/Textarea.svelte +28 -16
- package/dist/components/Textarea.svelte.d.ts +1 -0
- package/dist/components/ThemeToggle.svelte +1 -1
- package/dist/components/Toaster.svelte +34 -12
- package/dist/components/Toggle.svelte +6 -3
- package/dist/components/Toggle.svelte.d.ts +1 -0
- package/dist/components/dialog/Dialog.svelte +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/internal/field.d.ts +48 -0
- package/dist/internal/field.js +62 -0
- package/dist/internal/tone.d.ts +27 -0
- package/dist/internal/tone.js +29 -0
- package/dist/stores/toast.svelte.d.ts +6 -1
- package/dist/stores/toast.svelte.js +1 -1
- package/dist/styles/theme.css +30 -4
- package/dist/utils/broken-image.d.ts +21 -0
- package/dist/utils/broken-image.js +40 -0
- package/package.json +7 -6
- package/src/lib/styles/theme.css +30 -4
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
CONTROL_BASE,
|
|
4
|
+
FIELD_ERROR,
|
|
5
|
+
FIELD_HINT,
|
|
6
|
+
FIELD_LABEL,
|
|
7
|
+
FIELD_WRAP,
|
|
8
|
+
controlBorder,
|
|
9
|
+
describedBy,
|
|
10
|
+
} from '../internal/field.js';
|
|
11
|
+
|
|
2
12
|
interface Props {
|
|
3
13
|
value?: string;
|
|
4
14
|
label?: string;
|
|
@@ -31,10 +41,10 @@
|
|
|
31
41
|
const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
|
|
32
42
|
</script>
|
|
33
43
|
|
|
34
|
-
<div class="
|
|
44
|
+
<div class="{FIELD_WRAP} {klass}">
|
|
35
45
|
{#if label}
|
|
36
|
-
<label for={fieldId} class=
|
|
37
|
-
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
46
|
+
<label for={fieldId} class={FIELD_LABEL}>
|
|
47
|
+
{label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
|
|
38
48
|
</label>
|
|
39
49
|
{/if}
|
|
40
50
|
|
|
@@ -46,15 +56,14 @@
|
|
|
46
56
|
{required}
|
|
47
57
|
bind:value
|
|
48
58
|
aria-invalid={error ? 'true' : undefined}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
|
|
59
|
+
aria-describedby={describedBy(fieldId, error, hint)}
|
|
60
|
+
class="{CONTROL_BASE} {controlBorder(!!error)}"
|
|
52
61
|
{...rest}
|
|
53
62
|
/>
|
|
54
63
|
|
|
55
64
|
{#if error}
|
|
56
|
-
<p
|
|
65
|
+
<p id={fieldId ? `${fieldId}-error` : undefined} class={FIELD_ERROR}>{error}</p>
|
|
57
66
|
{:else if hint}
|
|
58
|
-
<p
|
|
67
|
+
<p id={fieldId ? `${fieldId}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
|
|
59
68
|
{/if}
|
|
60
69
|
</div>
|
|
@@ -68,10 +68,19 @@
|
|
|
68
68
|
<button
|
|
69
69
|
type="button"
|
|
70
70
|
onclick={close}
|
|
71
|
-
class="
|
|
71
|
+
class="shrink-0 text-faint transition-colors duration-150 hover:text-fg"
|
|
72
72
|
aria-label="Close"
|
|
73
73
|
>
|
|
74
|
-
|
|
74
|
+
<svg
|
|
75
|
+
width="16"
|
|
76
|
+
height="16"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
stroke-width="2"
|
|
81
|
+
stroke-linecap="round"
|
|
82
|
+
aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg
|
|
83
|
+
>
|
|
75
84
|
</button>
|
|
76
85
|
</div>
|
|
77
86
|
{/if}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
FIELD_ERROR,
|
|
4
|
+
FIELD_HINT,
|
|
5
|
+
FIELD_LABEL,
|
|
6
|
+
FIELD_WRAP,
|
|
7
|
+
controlBorder,
|
|
8
|
+
describedBy,
|
|
9
|
+
} from '../internal/field.js';
|
|
2
10
|
// MultiSelect: pick several options, shown as removable chips, with an optional
|
|
3
11
|
// search box. Binds to a string[] of selected values.
|
|
4
12
|
interface Option {
|
|
@@ -85,10 +93,10 @@
|
|
|
85
93
|
});
|
|
86
94
|
</script>
|
|
87
95
|
|
|
88
|
-
<div class="
|
|
96
|
+
<div class="{FIELD_WRAP} {cls}" bind:this={containerEl}>
|
|
89
97
|
{#if label}
|
|
90
|
-
<label for={id} class=
|
|
91
|
-
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
98
|
+
<label for={id} class={FIELD_LABEL}>
|
|
99
|
+
{label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
|
|
92
100
|
</label>
|
|
93
101
|
{/if}
|
|
94
102
|
|
|
@@ -108,11 +116,11 @@
|
|
|
108
116
|
open ? (open = false) : open_();
|
|
109
117
|
}
|
|
110
118
|
}}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
aria-describedby={describedBy(id, error, hint)}
|
|
120
|
+
class="w-full min-h-control flex flex-wrap items-center gap-1.5 rounded-lg border bg-surface-2
|
|
121
|
+
px-2.5 py-1.5 text-left text-sm transition-colors duration-150 outline-none cursor-pointer
|
|
114
122
|
{disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
115
|
-
{error
|
|
123
|
+
{controlBorder(!!error)}"
|
|
116
124
|
>
|
|
117
125
|
{#if selected.length === 0}
|
|
118
126
|
<span class="text-faint px-0.5">{placeholder}</span>
|
|
@@ -129,8 +137,21 @@
|
|
|
129
137
|
e.stopPropagation();
|
|
130
138
|
remove(opt.value);
|
|
131
139
|
}}
|
|
132
|
-
class="hover:text-brand-light
|
|
140
|
+
class="transition-colors duration-150 hover:text-brand-light"
|
|
133
141
|
>
|
|
142
|
+
<svg
|
|
143
|
+
width="11"
|
|
144
|
+
height="11"
|
|
145
|
+
viewBox="0 0 24 24"
|
|
146
|
+
fill="none"
|
|
147
|
+
stroke="currentColor"
|
|
148
|
+
stroke-width="2.5"
|
|
149
|
+
stroke-linecap="round"
|
|
150
|
+
aria-hidden="true"
|
|
151
|
+
>
|
|
152
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
153
|
+
</svg>
|
|
154
|
+
</button>
|
|
134
155
|
</span>
|
|
135
156
|
{/each}
|
|
136
157
|
{/if}
|
|
@@ -174,11 +195,11 @@
|
|
|
174
195
|
aria-selected={isSelected(opt.value)}
|
|
175
196
|
disabled={opt.disabled}
|
|
176
197
|
onclick={() => toggle(opt)}
|
|
177
|
-
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-fg transition-colors
|
|
198
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-fg transition-colors duration-150
|
|
178
199
|
hover:bg-surface-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
179
200
|
>
|
|
180
201
|
<span
|
|
181
|
-
class="flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors
|
|
202
|
+
class="flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors duration-150
|
|
182
203
|
{isSelected(opt.value) ? 'bg-brand border-brand' : 'bg-surface-2 border-line'}"
|
|
183
204
|
>
|
|
184
205
|
{#if isSelected(opt.value)}
|
|
@@ -205,8 +226,8 @@
|
|
|
205
226
|
</div>
|
|
206
227
|
|
|
207
228
|
{#if error}
|
|
208
|
-
<p
|
|
229
|
+
<p id={id ? `${id}-error` : undefined} class={FIELD_ERROR}>{error}</p>
|
|
209
230
|
{:else if hint}
|
|
210
|
-
<p
|
|
231
|
+
<p id={id ? `${id}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
|
|
211
232
|
{/if}
|
|
212
233
|
</div>
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
FIELD_ERROR,
|
|
4
|
+
FIELD_HINT,
|
|
5
|
+
FIELD_LABEL,
|
|
6
|
+
FIELD_WRAP,
|
|
7
|
+
describedBy,
|
|
8
|
+
} from '../internal/field.js';
|
|
9
|
+
|
|
2
10
|
interface Props {
|
|
3
11
|
value?: number;
|
|
4
12
|
min?: number;
|
|
5
13
|
max?: number;
|
|
6
14
|
step?: number;
|
|
7
15
|
disabled?: boolean;
|
|
16
|
+
required?: boolean;
|
|
8
17
|
id?: string;
|
|
9
18
|
name?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
hint?: string;
|
|
10
21
|
error?: string;
|
|
11
22
|
class?: string;
|
|
12
23
|
onchange?: (v: number) => void;
|
|
@@ -18,13 +29,18 @@
|
|
|
18
29
|
max = undefined,
|
|
19
30
|
step = 1,
|
|
20
31
|
disabled = false,
|
|
32
|
+
required = false,
|
|
21
33
|
id = undefined,
|
|
22
34
|
name = undefined,
|
|
35
|
+
label = undefined,
|
|
36
|
+
hint = undefined,
|
|
23
37
|
error = undefined,
|
|
24
38
|
class: cls = '',
|
|
25
39
|
onchange = undefined,
|
|
26
40
|
}: Props = $props();
|
|
27
41
|
|
|
42
|
+
const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
|
|
43
|
+
|
|
28
44
|
function dec() {
|
|
29
45
|
const next = value - step;
|
|
30
46
|
if (min !== undefined && next < min) return;
|
|
@@ -49,33 +65,57 @@
|
|
|
49
65
|
|
|
50
66
|
let canDec = $derived(!(disabled || (min !== undefined && value <= min)));
|
|
51
67
|
let canInc = $derived(!(disabled || (max !== undefined && value >= max)));
|
|
68
|
+
|
|
69
|
+
const step_ =
|
|
70
|
+
'flex w-control shrink-0 items-center justify-center border-line bg-surface-2 text-muted ' +
|
|
71
|
+
'transition-colors duration-150 hover:bg-line hover:text-fg ' +
|
|
72
|
+
'disabled:opacity-50 disabled:cursor-not-allowed';
|
|
52
73
|
</script>
|
|
53
74
|
|
|
54
|
-
<div class="
|
|
55
|
-
|
|
75
|
+
<div class="{FIELD_WRAP} {cls}">
|
|
76
|
+
{#if label}
|
|
77
|
+
<label for={fieldId} class={FIELD_LABEL}>
|
|
78
|
+
{label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
|
|
79
|
+
</label>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
82
|
+
<div class="flex h-control">
|
|
56
83
|
<button
|
|
57
84
|
type="button"
|
|
58
85
|
onclick={dec}
|
|
59
86
|
disabled={!canDec}
|
|
60
87
|
aria-label="Decrease"
|
|
61
|
-
class="
|
|
62
|
-
bg-surface-2 text-lg leading-none text-muted transition-colors
|
|
63
|
-
hover:bg-line hover:text-fg
|
|
64
|
-
disabled:cursor-not-allowed disabled:opacity-40">−</button
|
|
88
|
+
class="{step_} rounded-l-lg border border-r-0"
|
|
65
89
|
>
|
|
90
|
+
<svg
|
|
91
|
+
width="14"
|
|
92
|
+
height="14"
|
|
93
|
+
viewBox="0 0 24 24"
|
|
94
|
+
fill="none"
|
|
95
|
+
stroke="currentColor"
|
|
96
|
+
stroke-width="2"
|
|
97
|
+
stroke-linecap="round"
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
>
|
|
100
|
+
<path d="M5 12h14" />
|
|
101
|
+
</svg>
|
|
102
|
+
</button>
|
|
66
103
|
|
|
67
104
|
<input
|
|
68
105
|
type="number"
|
|
69
|
-
{
|
|
106
|
+
id={fieldId}
|
|
70
107
|
{name}
|
|
71
108
|
{min}
|
|
72
109
|
{max}
|
|
73
110
|
{step}
|
|
74
111
|
{disabled}
|
|
112
|
+
{required}
|
|
75
113
|
{value}
|
|
76
114
|
oninput={handleInput}
|
|
115
|
+
aria-invalid={error ? 'true' : undefined}
|
|
116
|
+
aria-describedby={describedBy(fieldId, error, hint)}
|
|
77
117
|
class="min-w-0 flex-1 border-y bg-surface-2 text-center text-sm text-fg
|
|
78
|
-
|
|
118
|
+
outline-none transition-colors duration-150 focus:border-brand
|
|
79
119
|
disabled:cursor-not-allowed disabled:opacity-50
|
|
80
120
|
[appearance:textfield]
|
|
81
121
|
[&::-webkit-inner-spin-button]:appearance-none
|
|
@@ -88,11 +128,26 @@
|
|
|
88
128
|
onclick={inc}
|
|
89
129
|
disabled={!canInc}
|
|
90
130
|
aria-label="Increase"
|
|
91
|
-
class="
|
|
92
|
-
bg-surface-2 text-lg leading-none text-muted transition-colors
|
|
93
|
-
hover:bg-line hover:text-fg
|
|
94
|
-
disabled:cursor-not-allowed disabled:opacity-40">+</button
|
|
131
|
+
class="{step_} rounded-r-lg border border-l-0"
|
|
95
132
|
>
|
|
133
|
+
<svg
|
|
134
|
+
width="14"
|
|
135
|
+
height="14"
|
|
136
|
+
viewBox="0 0 24 24"
|
|
137
|
+
fill="none"
|
|
138
|
+
stroke="currentColor"
|
|
139
|
+
stroke-width="2"
|
|
140
|
+
stroke-linecap="round"
|
|
141
|
+
aria-hidden="true"
|
|
142
|
+
>
|
|
143
|
+
<path d="M12 5v14M5 12h14" />
|
|
144
|
+
</svg>
|
|
145
|
+
</button>
|
|
96
146
|
</div>
|
|
97
|
-
|
|
147
|
+
|
|
148
|
+
{#if error}
|
|
149
|
+
<p id={fieldId ? `${fieldId}-error` : undefined} class={FIELD_ERROR}>{error}</p>
|
|
150
|
+
{:else if hint}
|
|
151
|
+
<p id={fieldId ? `${fieldId}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
|
|
152
|
+
{/if}
|
|
98
153
|
</div>
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
<header class="mb-8 flex flex-wrap items-start justify-between gap-4 {klass}">
|
|
15
15
|
<div class="min-w-0">
|
|
16
|
-
|
|
16
|
+
<!-- The app shells render their own h1 for the current route, so a heading
|
|
17
|
+
lookup by name matches two elements. This names the page's own title. -->
|
|
18
|
+
<h1 data-testid="page-title" class="text-2xl font-bold text-fg">{title}</h1>
|
|
17
19
|
{#if description}<p class="mt-1 text-sm text-muted">{description}</p>{/if}
|
|
18
20
|
</div>
|
|
19
21
|
{#if actions}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
let nums = $derived(pageNumbers(safePage, totalPages));
|
|
35
35
|
|
|
36
36
|
const btnBase =
|
|
37
|
-
'inline-flex items-center justify-center w-7 h-7 rounded text-xs font-medium transition-colors';
|
|
37
|
+
'inline-flex items-center justify-center w-7 h-7 rounded text-xs font-medium transition-colors duration-150';
|
|
38
38
|
</script>
|
|
39
39
|
|
|
40
40
|
{#if totalPages > 1}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
type
|
|
2
|
+
import type { AccentTone } from '../internal/tone.js';
|
|
3
3
|
type Size = 'xs' | 'sm' | 'md';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
value: number;
|
|
7
7
|
max?: number;
|
|
8
|
-
tone?:
|
|
8
|
+
tone?: AccentTone;
|
|
9
9
|
size?: Size;
|
|
10
10
|
label?: string;
|
|
11
11
|
showValue?: boolean;
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
: 0,
|
|
31
31
|
);
|
|
32
32
|
|
|
33
|
-
const tones: Record<
|
|
33
|
+
const tones: Record<AccentTone, string> = {
|
|
34
|
+
neutral: 'bg-muted',
|
|
34
35
|
brand: 'bg-brand',
|
|
35
36
|
violet: 'bg-violet',
|
|
36
37
|
success: 'bg-success',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
type
|
|
1
|
+
import type { AccentTone } from '../internal/tone.js';
|
|
2
2
|
type Size = 'xs' | 'sm' | 'md';
|
|
3
3
|
interface Props {
|
|
4
4
|
value: number;
|
|
5
5
|
max?: number;
|
|
6
|
-
tone?:
|
|
6
|
+
tone?: AccentTone;
|
|
7
7
|
size?: Size;
|
|
8
8
|
label?: string;
|
|
9
9
|
showValue?: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { FIELD_HINT } from '../internal/field.js';
|
|
2
3
|
interface Props {
|
|
3
4
|
value: string;
|
|
4
5
|
group?: string;
|
|
@@ -8,6 +9,7 @@
|
|
|
8
9
|
disabled?: boolean;
|
|
9
10
|
id?: string;
|
|
10
11
|
name?: string;
|
|
12
|
+
class?: string;
|
|
11
13
|
onchange?: (value: string) => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
disabled = false,
|
|
21
23
|
id = undefined,
|
|
22
24
|
name = undefined,
|
|
25
|
+
class: cls = '',
|
|
23
26
|
onchange = undefined,
|
|
24
27
|
}: Props = $props();
|
|
25
28
|
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
|
|
32
35
|
<label
|
|
33
36
|
class="inline-flex cursor-pointer select-none items-start gap-2.5
|
|
34
|
-
{disabled ? 'cursor-not-allowed opacity-50' : ''}"
|
|
37
|
+
{disabled ? 'cursor-not-allowed opacity-50' : ''} {cls}"
|
|
35
38
|
>
|
|
36
39
|
<span class="relative mt-0.5 flex shrink-0 items-center justify-center">
|
|
37
40
|
<input
|
|
@@ -46,7 +49,7 @@
|
|
|
46
49
|
class="peer sr-only"
|
|
47
50
|
/>
|
|
48
51
|
<span
|
|
49
|
-
class="flex h-4 w-4 items-center justify-center rounded-full border-2 transition-colors
|
|
52
|
+
class="flex h-4 w-4 items-center justify-center rounded-full border-2 transition-colors duration-150
|
|
50
53
|
{group === value
|
|
51
54
|
? 'border-brand bg-surface-2'
|
|
52
55
|
: 'border-line bg-surface-2 peer-focus-visible:border-brand'}"
|
|
@@ -59,7 +62,7 @@
|
|
|
59
62
|
{#if label || hint}
|
|
60
63
|
<span class="flex flex-col gap-0.5">
|
|
61
64
|
{#if label}<span class="text-sm text-fg">{label}</span>{/if}
|
|
62
|
-
{#if hint}<span class=
|
|
65
|
+
{#if hint}<span class={FIELD_HINT}>{hint}</span>{/if}
|
|
63
66
|
</span>
|
|
64
67
|
{/if}
|
|
65
68
|
</label>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { FIELD_ERROR, FIELD_HINT, FIELD_LABEL } from '../internal/field.js';
|
|
2
3
|
// RadioGroup renders a managed set of radio options bound to a single value.
|
|
3
4
|
// Matches the visual of Radio.svelte; use this when you have a fixed option list.
|
|
4
5
|
interface Option {
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
|
|
47
48
|
<fieldset class="flex flex-col gap-2 {cls}" {disabled}>
|
|
48
49
|
{#if label}
|
|
49
|
-
<legend class="
|
|
50
|
+
<legend class="{FIELD_LABEL} mb-0.5">
|
|
50
51
|
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
51
52
|
</legend>
|
|
52
53
|
{/if}
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
class="peer sr-only"
|
|
71
72
|
/>
|
|
72
73
|
<span
|
|
73
|
-
class="flex h-4 w-4 items-center justify-center rounded-full border-2 transition-colors
|
|
74
|
+
class="flex h-4 w-4 items-center justify-center rounded-full border-2 transition-colors duration-150
|
|
74
75
|
{value === opt.value
|
|
75
76
|
? 'border-brand bg-surface-2'
|
|
76
77
|
: 'border-line bg-surface-2 peer-focus-visible:border-brand'}"
|
|
@@ -82,15 +83,15 @@
|
|
|
82
83
|
</span>
|
|
83
84
|
<span class="flex flex-col gap-0.5">
|
|
84
85
|
<span class="text-sm text-fg">{opt.label}</span>
|
|
85
|
-
{#if opt.hint}<span class=
|
|
86
|
+
{#if opt.hint}<span class={FIELD_HINT}>{opt.hint}</span>{/if}
|
|
86
87
|
</span>
|
|
87
88
|
</label>
|
|
88
89
|
{/each}
|
|
89
90
|
</div>
|
|
90
91
|
|
|
91
92
|
{#if error}
|
|
92
|
-
<p class=
|
|
93
|
+
<p class={FIELD_ERROR}>{error}</p>
|
|
93
94
|
{:else if hint}
|
|
94
|
-
<p class=
|
|
95
|
+
<p class={FIELD_HINT}>{hint}</p>
|
|
95
96
|
{/if}
|
|
96
97
|
</fieldset>
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { CONTROL_BASE, FIELD_LABEL, FIELD_WRAP, controlBorder } from '../internal/field.js';
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
value?: string;
|
|
6
|
+
label?: string;
|
|
4
7
|
placeholder?: string;
|
|
5
8
|
disabled?: boolean;
|
|
9
|
+
id?: string;
|
|
10
|
+
name?: string;
|
|
6
11
|
class?: string;
|
|
7
12
|
oninput?: (value: string) => void;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
let {
|
|
11
16
|
value = $bindable(''),
|
|
17
|
+
label = undefined,
|
|
12
18
|
placeholder = 'Search…',
|
|
13
19
|
disabled = false,
|
|
20
|
+
id = undefined,
|
|
21
|
+
name = undefined,
|
|
14
22
|
class: cls = '',
|
|
15
23
|
oninput = undefined,
|
|
16
24
|
}: Props = $props();
|
|
17
25
|
|
|
26
|
+
const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
|
|
27
|
+
|
|
18
28
|
function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
|
|
19
29
|
value = e.currentTarget.value;
|
|
20
30
|
oninput?.(value);
|
|
@@ -26,56 +36,61 @@
|
|
|
26
36
|
}
|
|
27
37
|
</script>
|
|
28
38
|
|
|
29
|
-
<div class="
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
viewBox="0 0 24 24"
|
|
34
|
-
fill="none"
|
|
35
|
-
stroke="currentColor"
|
|
36
|
-
stroke-width="2"
|
|
37
|
-
stroke-linecap="round"
|
|
38
|
-
stroke-linejoin="round"
|
|
39
|
-
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-faint"
|
|
40
|
-
aria-hidden="true"
|
|
41
|
-
>
|
|
42
|
-
<circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" />
|
|
43
|
-
</svg>
|
|
44
|
-
|
|
45
|
-
<input
|
|
46
|
-
type="search"
|
|
47
|
-
{placeholder}
|
|
48
|
-
{disabled}
|
|
49
|
-
{value}
|
|
50
|
-
oninput={handleInput}
|
|
51
|
-
class="w-full rounded-lg border border-line bg-surface-2
|
|
52
|
-
py-2 pl-9 text-sm text-fg placeholder:text-faint
|
|
53
|
-
focus:border-brand focus:outline-none transition-colors
|
|
54
|
-
disabled:cursor-not-allowed disabled:opacity-50
|
|
55
|
-
[&::-webkit-search-cancel-button]:appearance-none
|
|
56
|
-
{value ? 'pr-8' : 'pr-3'}"
|
|
57
|
-
/>
|
|
39
|
+
<div class="{FIELD_WRAP} {cls}">
|
|
40
|
+
{#if label}
|
|
41
|
+
<label for={fieldId} class={FIELD_LABEL}>{label}</label>
|
|
42
|
+
{/if}
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
44
|
+
<div class="relative">
|
|
45
|
+
<svg
|
|
46
|
+
width="15"
|
|
47
|
+
height="15"
|
|
48
|
+
viewBox="0 0 24 24"
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="2"
|
|
52
|
+
stroke-linecap="round"
|
|
53
|
+
stroke-linejoin="round"
|
|
54
|
+
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-faint"
|
|
55
|
+
aria-hidden="true"
|
|
66
56
|
>
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
57
|
+
<circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" />
|
|
58
|
+
</svg>
|
|
59
|
+
|
|
60
|
+
<input
|
|
61
|
+
type="search"
|
|
62
|
+
id={fieldId}
|
|
63
|
+
{name}
|
|
64
|
+
{placeholder}
|
|
65
|
+
{disabled}
|
|
66
|
+
{value}
|
|
67
|
+
oninput={handleInput}
|
|
68
|
+
class="{CONTROL_BASE} {controlBorder(false)} pl-9
|
|
69
|
+
[&::-webkit-search-cancel-button]:appearance-none
|
|
70
|
+
{value ? 'pr-8' : 'pr-3'}"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
{#if value}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onclick={clear}
|
|
77
|
+
aria-label="Clear search"
|
|
78
|
+
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-faint
|
|
79
|
+
transition-colors duration-150 hover:text-fg"
|
|
76
80
|
>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
<svg
|
|
82
|
+
width="14"
|
|
83
|
+
height="14"
|
|
84
|
+
viewBox="0 0 24 24"
|
|
85
|
+
fill="none"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
stroke-width="2"
|
|
88
|
+
stroke-linecap="round"
|
|
89
|
+
aria-hidden="true"
|
|
90
|
+
>
|
|
91
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
92
|
+
</svg>
|
|
93
|
+
</button>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
81
96
|
</div>
|