@lyeve-labs/ui-kit 0.8.2
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/README.md +199 -0
- package/dist/components/Accordion.svelte +48 -0
- package/dist/components/Accordion.svelte.d.ts +10 -0
- package/dist/components/AccordionItem.svelte +55 -0
- package/dist/components/AccordionItem.svelte.d.ts +10 -0
- package/dist/components/Alert.svelte +60 -0
- package/dist/components/Alert.svelte.d.ts +13 -0
- package/dist/components/Autocomplete.svelte +198 -0
- package/dist/components/Autocomplete.svelte.d.ts +23 -0
- package/dist/components/Avatar.svelte +65 -0
- package/dist/components/Avatar.svelte.d.ts +9 -0
- package/dist/components/AvatarGroup.svelte +45 -0
- package/dist/components/AvatarGroup.svelte.d.ts +14 -0
- package/dist/components/Badge.svelte +48 -0
- package/dist/components/Badge.svelte.d.ts +13 -0
- package/dist/components/Banner.svelte +73 -0
- package/dist/components/Banner.svelte.d.ts +13 -0
- package/dist/components/Breadcrumb.svelte +47 -0
- package/dist/components/Breadcrumb.svelte.d.ts +10 -0
- package/dist/components/Button.svelte +71 -0
- package/dist/components/Button.svelte.d.ts +18 -0
- package/dist/components/ButtonGroup.svelte +33 -0
- package/dist/components/ButtonGroup.svelte.d.ts +9 -0
- package/dist/components/Card.svelte +72 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/Checkbox.svelte +80 -0
- package/dist/components/Checkbox.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +274 -0
- package/dist/components/DatePicker.svelte.d.ts +17 -0
- package/dist/components/Divider.svelte +23 -0
- package/dist/components/Divider.svelte.d.ts +8 -0
- package/dist/components/Drawer.svelte +120 -0
- package/dist/components/Drawer.svelte.d.ts +16 -0
- package/dist/components/Dropdown.svelte +88 -0
- package/dist/components/Dropdown.svelte.d.ts +23 -0
- package/dist/components/EmptyState.svelte +51 -0
- package/dist/components/EmptyState.svelte.d.ts +13 -0
- package/dist/components/FileInput.svelte +102 -0
- package/dist/components/FileInput.svelte.d.ts +15 -0
- package/dist/components/Indicator.svelte +38 -0
- package/dist/components/Indicator.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +60 -0
- package/dist/components/Input.svelte.d.ts +17 -0
- package/dist/components/Kbd.svelte +18 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Label.svelte +30 -0
- package/dist/components/Label.svelte.d.ts +11 -0
- package/dist/components/Modal.svelte +105 -0
- package/dist/components/Modal.svelte.d.ts +14 -0
- package/dist/components/MultiSelect.svelte +212 -0
- package/dist/components/MultiSelect.svelte.d.ts +22 -0
- package/dist/components/NumberInput.svelte +98 -0
- package/dist/components/NumberInput.svelte.d.ts +15 -0
- package/dist/components/PageHeader.svelte +22 -0
- package/dist/components/PageHeader.svelte.d.ts +10 -0
- package/dist/components/Pagination.svelte +112 -0
- package/dist/components/Pagination.svelte.d.ts +10 -0
- package/dist/components/Progress.svelte +70 -0
- package/dist/components/Progress.svelte.d.ts +15 -0
- package/dist/components/Radio.svelte +65 -0
- package/dist/components/Radio.svelte.d.ts +14 -0
- package/dist/components/RadioGroup.svelte +96 -0
- package/dist/components/RadioGroup.svelte.d.ts +22 -0
- package/dist/components/SearchInput.svelte +81 -0
- package/dist/components/SearchInput.svelte.d.ts +10 -0
- package/dist/components/Select.svelte +69 -0
- package/dist/components/Select.svelte.d.ts +18 -0
- package/dist/components/Skeleton.svelte +20 -0
- package/dist/components/Skeleton.svelte.d.ts +9 -0
- package/dist/components/Spinner.svelte +19 -0
- package/dist/components/Spinner.svelte.d.ts +7 -0
- package/dist/components/Stat.svelte +45 -0
- package/dist/components/Stat.svelte.d.ts +14 -0
- package/dist/components/StepIndicator.svelte +142 -0
- package/dist/components/StepIndicator.svelte.d.ts +13 -0
- package/dist/components/Table.svelte +36 -0
- package/dist/components/Table.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +39 -0
- package/dist/components/Tabs.svelte.d.ts +13 -0
- package/dist/components/Tag.svelte +56 -0
- package/dist/components/Tag.svelte.d.ts +11 -0
- package/dist/components/Textarea.svelte +75 -0
- package/dist/components/Textarea.svelte.d.ts +26 -0
- package/dist/components/ThemeToggle.svelte +33 -0
- package/dist/components/ThemeToggle.svelte.d.ts +6 -0
- package/dist/components/Toaster.svelte +54 -0
- package/dist/components/Toaster.svelte.d.ts +18 -0
- package/dist/components/Toggle.svelte +74 -0
- package/dist/components/Toggle.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +44 -0
- package/dist/components/Tooltip.svelte.d.ts +11 -0
- package/dist/components/dialog/ConfirmDialog.svelte +95 -0
- package/dist/components/dialog/ConfirmDialog.svelte.d.ts +8 -0
- package/dist/components/dialog/Dialog.svelte +225 -0
- package/dist/components/dialog/Dialog.svelte.d.ts +9 -0
- package/dist/components/dialog/DialogContainer.svelte +17 -0
- package/dist/components/dialog/DialogContainer.svelte.d.ts +18 -0
- package/dist/components/dialog/dialog-manager.svelte.d.ts +54 -0
- package/dist/components/dialog/dialog-manager.svelte.js +164 -0
- package/dist/components/dialog/types.d.ts +33 -0
- package/dist/components/dialog/types.js +10 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +72 -0
- package/dist/stores/toast.svelte.d.ts +18 -0
- package/dist/stores/toast.svelte.js +28 -0
- package/dist/styles/theme.css +148 -0
- package/dist/utils/cn.d.ts +13 -0
- package/dist/utils/cn.js +24 -0
- package/dist/utils/theme.d.ts +26 -0
- package/dist/utils/theme.js +55 -0
- package/package.json +81 -0
- package/src/lib/styles/theme.css +148 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// DatePicker: a text trigger + calendar popover. Binds `value` to an ISO date
|
|
3
|
+
// string ("YYYY-MM-DD"). Pure local-date math (no timezone surprises).
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(''),
|
|
6
|
+
label = undefined,
|
|
7
|
+
hint = undefined,
|
|
8
|
+
error = undefined,
|
|
9
|
+
placeholder = 'Select a date',
|
|
10
|
+
min = undefined,
|
|
11
|
+
max = undefined,
|
|
12
|
+
disabled = false,
|
|
13
|
+
required = false,
|
|
14
|
+
id = undefined,
|
|
15
|
+
class: cls = '',
|
|
16
|
+
onchange = undefined,
|
|
17
|
+
}: {
|
|
18
|
+
value?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
hint?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
min?: string;
|
|
24
|
+
max?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
id?: string;
|
|
28
|
+
class?: string;
|
|
29
|
+
onchange?: (value: string) => void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
33
|
+
const MONTHS = [
|
|
34
|
+
'January',
|
|
35
|
+
'February',
|
|
36
|
+
'March',
|
|
37
|
+
'April',
|
|
38
|
+
'May',
|
|
39
|
+
'June',
|
|
40
|
+
'July',
|
|
41
|
+
'August',
|
|
42
|
+
'September',
|
|
43
|
+
'October',
|
|
44
|
+
'November',
|
|
45
|
+
'December',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
let open = $state(false);
|
|
49
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
50
|
+
|
|
51
|
+
function parseISO(s?: string): { y: number; m: number; d: number } | null {
|
|
52
|
+
if (!s) return null;
|
|
53
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
return { y: +m[1], m: +m[2], d: +m[3] };
|
|
56
|
+
}
|
|
57
|
+
function pad(n: number) {
|
|
58
|
+
return n < 10 ? `0${n}` : `${n}`;
|
|
59
|
+
}
|
|
60
|
+
function toISO(y: number, m: number, d: number) {
|
|
61
|
+
return `${y}-${pad(m)}-${pad(d)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const today = new Date();
|
|
65
|
+
const initial = parseISO(value) ?? {
|
|
66
|
+
y: today.getFullYear(),
|
|
67
|
+
m: today.getMonth() + 1,
|
|
68
|
+
d: today.getDate(),
|
|
69
|
+
};
|
|
70
|
+
let viewYear = $state(initial.y);
|
|
71
|
+
let viewMonth = $state(initial.m); // 1-12
|
|
72
|
+
|
|
73
|
+
const display = $derived.by(() => {
|
|
74
|
+
const p = parseISO(value);
|
|
75
|
+
if (!p) return '';
|
|
76
|
+
return new Date(p.y, p.m - 1, p.d).toLocaleDateString(undefined, {
|
|
77
|
+
year: 'numeric',
|
|
78
|
+
month: 'short',
|
|
79
|
+
day: 'numeric',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Grid of day numbers (null = leading blank) for the viewed month.
|
|
84
|
+
const cells = $derived.by(() => {
|
|
85
|
+
const firstDow = new Date(viewYear, viewMonth - 1, 1).getDay();
|
|
86
|
+
const days = new Date(viewYear, viewMonth, 0).getDate();
|
|
87
|
+
const out: (number | null)[] = [];
|
|
88
|
+
for (let i = 0; i < firstDow; i++) out.push(null);
|
|
89
|
+
for (let d = 1; d <= days; d++) out.push(d);
|
|
90
|
+
return out;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const todayISO = toISO(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
94
|
+
|
|
95
|
+
function isDisabled(d: number) {
|
|
96
|
+
const iso = toISO(viewYear, viewMonth, d);
|
|
97
|
+
if (min && iso < min) return true;
|
|
98
|
+
if (max && iso > max) return true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function pick(d: number) {
|
|
103
|
+
if (isDisabled(d)) return;
|
|
104
|
+
value = toISO(viewYear, viewMonth, d);
|
|
105
|
+
onchange?.(value);
|
|
106
|
+
open = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function prevMonth() {
|
|
110
|
+
if (viewMonth === 1) {
|
|
111
|
+
viewMonth = 12;
|
|
112
|
+
viewYear -= 1;
|
|
113
|
+
} else {
|
|
114
|
+
viewMonth -= 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function nextMonth() {
|
|
118
|
+
if (viewMonth === 12) {
|
|
119
|
+
viewMonth = 1;
|
|
120
|
+
viewYear += 1;
|
|
121
|
+
} else {
|
|
122
|
+
viewMonth += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function openCal() {
|
|
127
|
+
if (disabled) return;
|
|
128
|
+
const p = parseISO(value);
|
|
129
|
+
if (p) {
|
|
130
|
+
viewYear = p.y;
|
|
131
|
+
viewMonth = p.m;
|
|
132
|
+
}
|
|
133
|
+
open = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleOutside(e: MouseEvent) {
|
|
137
|
+
if (containerEl && !containerEl.contains(e.target as Node)) open = false;
|
|
138
|
+
}
|
|
139
|
+
function handleKey(e: KeyboardEvent) {
|
|
140
|
+
if (e.key === 'Escape') open = false;
|
|
141
|
+
}
|
|
142
|
+
$effect(() => {
|
|
143
|
+
if (open) {
|
|
144
|
+
document.addEventListener('click', handleOutside, { capture: true });
|
|
145
|
+
document.addEventListener('keydown', handleKey);
|
|
146
|
+
}
|
|
147
|
+
return () => {
|
|
148
|
+
document.removeEventListener('click', handleOutside, { capture: true });
|
|
149
|
+
document.removeEventListener('keydown', handleKey);
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<div class="flex flex-col gap-1.5 {cls}" bind:this={containerEl}>
|
|
155
|
+
{#if label}
|
|
156
|
+
<label for={id} class="text-sm font-medium text-fg">
|
|
157
|
+
{label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
|
|
158
|
+
</label>
|
|
159
|
+
{/if}
|
|
160
|
+
|
|
161
|
+
<div class="relative">
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
{id}
|
|
165
|
+
{disabled}
|
|
166
|
+
onclick={() => (open ? (open = false) : openCal())}
|
|
167
|
+
class="w-full flex items-center justify-between rounded-lg border bg-surface-2 px-3 py-2 text-sm
|
|
168
|
+
transition-colors outline-none disabled:opacity-50 disabled:cursor-not-allowed
|
|
169
|
+
{error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
|
|
170
|
+
>
|
|
171
|
+
<span class={display ? 'text-fg' : 'text-faint'}>{display || placeholder}</span>
|
|
172
|
+
<span class="text-faint shrink-0" aria-hidden="true">
|
|
173
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
174
|
+
<rect
|
|
175
|
+
x="2.5"
|
|
176
|
+
y="3.5"
|
|
177
|
+
width="11"
|
|
178
|
+
height="10"
|
|
179
|
+
rx="1.5"
|
|
180
|
+
stroke="currentColor"
|
|
181
|
+
stroke-width="1.3"
|
|
182
|
+
/>
|
|
183
|
+
<path
|
|
184
|
+
d="M2.5 6.5h11M5.5 2v3M10.5 2v3"
|
|
185
|
+
stroke="currentColor"
|
|
186
|
+
stroke-width="1.3"
|
|
187
|
+
stroke-linecap="round"
|
|
188
|
+
/>
|
|
189
|
+
</svg>
|
|
190
|
+
</span>
|
|
191
|
+
</button>
|
|
192
|
+
|
|
193
|
+
{#if open}
|
|
194
|
+
<div
|
|
195
|
+
class="absolute z-50 mt-1 w-[17rem] rounded-xl border border-line bg-surface shadow-2xl p-3"
|
|
196
|
+
>
|
|
197
|
+
<!-- Header -->
|
|
198
|
+
<div class="flex items-center justify-between mb-2">
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
aria-label="Previous month"
|
|
202
|
+
onclick={prevMonth}
|
|
203
|
+
class="p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors"
|
|
204
|
+
>
|
|
205
|
+
<svg width="14" height="14" viewBox="0 0 12 12" fill="none"
|
|
206
|
+
><path
|
|
207
|
+
d="M7.5 2L4 6l3.5 4"
|
|
208
|
+
stroke="currentColor"
|
|
209
|
+
stroke-width="1.5"
|
|
210
|
+
stroke-linecap="round"
|
|
211
|
+
stroke-linejoin="round"
|
|
212
|
+
/></svg
|
|
213
|
+
>
|
|
214
|
+
</button>
|
|
215
|
+
<span class="text-sm font-medium text-fg">{MONTHS[viewMonth - 1]} {viewYear}</span>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
aria-label="Next month"
|
|
219
|
+
onclick={nextMonth}
|
|
220
|
+
class="p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors"
|
|
221
|
+
>
|
|
222
|
+
<svg width="14" height="14" viewBox="0 0 12 12" fill="none"
|
|
223
|
+
><path
|
|
224
|
+
d="M4.5 2L8 6l-3.5 4"
|
|
225
|
+
stroke="currentColor"
|
|
226
|
+
stroke-width="1.5"
|
|
227
|
+
stroke-linecap="round"
|
|
228
|
+
stroke-linejoin="round"
|
|
229
|
+
/></svg
|
|
230
|
+
>
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Weekday labels -->
|
|
235
|
+
<div class="grid grid-cols-7 mb-1">
|
|
236
|
+
{#each WEEKDAYS as wd}
|
|
237
|
+
<span class="text-center text-[0.65rem] font-medium text-faint py-1">{wd}</span>
|
|
238
|
+
{/each}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- Day grid -->
|
|
242
|
+
<div class="grid grid-cols-7 gap-0.5">
|
|
243
|
+
{#each cells as d}
|
|
244
|
+
{#if d === null}
|
|
245
|
+
<span></span>
|
|
246
|
+
{:else}
|
|
247
|
+
{@const iso = toISO(viewYear, viewMonth, d)}
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
disabled={isDisabled(d)}
|
|
251
|
+
onclick={() => pick(d)}
|
|
252
|
+
aria-current={iso === todayISO ? 'date' : undefined}
|
|
253
|
+
aria-label={iso}
|
|
254
|
+
class="h-8 w-8 mx-auto flex items-center justify-center rounded-md text-sm transition-colors
|
|
255
|
+
disabled:opacity-30 disabled:cursor-not-allowed
|
|
256
|
+
{iso === value
|
|
257
|
+
? 'bg-brand text-ink font-medium'
|
|
258
|
+
: iso === todayISO
|
|
259
|
+
? 'text-brand font-medium hover:bg-surface-2'
|
|
260
|
+
: 'text-fg hover:bg-surface-2'}">{d}</button
|
|
261
|
+
>
|
|
262
|
+
{/if}
|
|
263
|
+
{/each}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{#if error}
|
|
270
|
+
<p class="text-xs text-danger">{error}</p>
|
|
271
|
+
{:else if hint}
|
|
272
|
+
<p class="text-xs text-faint">{hint}</p>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
min?: string;
|
|
8
|
+
max?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
id?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
onchange?: (value: string) => void;
|
|
14
|
+
};
|
|
15
|
+
declare const DatePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
16
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
17
|
+
export default DatePicker;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
class: cls = '',
|
|
6
|
+
children,
|
|
7
|
+
}: {
|
|
8
|
+
class?: string;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
} = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
{#if children}
|
|
14
|
+
<div class="flex items-center gap-3 {cls}">
|
|
15
|
+
<span class="flex-1 border-t border-line"></span>
|
|
16
|
+
<span class="text-xs text-faint font-medium uppercase tracking-wider whitespace-nowrap">
|
|
17
|
+
{@render children()}
|
|
18
|
+
</span>
|
|
19
|
+
<span class="flex-1 border-t border-line"></span>
|
|
20
|
+
</div>
|
|
21
|
+
{:else}
|
|
22
|
+
<hr class="border-t border-line {cls}" />
|
|
23
|
+
{/if}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Side = 'left' | 'right';
|
|
5
|
+
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
open?: boolean;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
side?: Side;
|
|
12
|
+
size?: Size;
|
|
13
|
+
onclose?: () => void;
|
|
14
|
+
children: Snippet;
|
|
15
|
+
footer?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
open = $bindable(false),
|
|
20
|
+
title = undefined,
|
|
21
|
+
description = undefined,
|
|
22
|
+
side = 'right',
|
|
23
|
+
size = 'md',
|
|
24
|
+
onclose = undefined,
|
|
25
|
+
children,
|
|
26
|
+
footer,
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const widths: Record<Size, string> = {
|
|
30
|
+
sm: 'w-72',
|
|
31
|
+
md: 'w-80',
|
|
32
|
+
lg: 'w-96',
|
|
33
|
+
xl: 'w-[480px]',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function close() {
|
|
37
|
+
open = false;
|
|
38
|
+
onclose?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function onkeydown(e: KeyboardEvent) {
|
|
42
|
+
if (e.key === 'Escape') {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<svelte:window onkeydown={open ? onkeydown : undefined} />
|
|
50
|
+
|
|
51
|
+
{#if open}
|
|
52
|
+
<div class="fixed inset-0 z-50 flex {side === 'right' ? 'justify-end' : 'justify-start'}">
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
|
|
56
|
+
aria-label="Close"
|
|
57
|
+
onclick={close}
|
|
58
|
+
></button>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
class="relative flex h-full flex-col {widths[size]} bg-surface shadow-2xl
|
|
62
|
+
{side === 'right' ? 'border-l' : 'border-r'} border-line
|
|
63
|
+
{side === 'right'
|
|
64
|
+
? 'animate-[drawer-in-right_150ms_ease-out]'
|
|
65
|
+
: 'animate-[drawer-in-left_150ms_ease-out]'}"
|
|
66
|
+
role="dialog"
|
|
67
|
+
aria-modal="true"
|
|
68
|
+
>
|
|
69
|
+
{#if title}
|
|
70
|
+
<div class="flex shrink-0 items-start justify-between gap-4 border-b border-line px-5 py-4">
|
|
71
|
+
<div>
|
|
72
|
+
<h2 class="font-semibold text-fg">{title}</h2>
|
|
73
|
+
{#if description}<p class="mt-0.5 text-sm text-muted">{description}</p>{/if}
|
|
74
|
+
</div>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onclick={close}
|
|
78
|
+
class="-mt-0.5 text-xl leading-none text-faint transition-colors hover:text-fg"
|
|
79
|
+
aria-label="Close">×</button
|
|
80
|
+
>
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<div class="flex-1 overflow-y-auto px-5 py-4">
|
|
85
|
+
{@render children()}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{#if footer}
|
|
89
|
+
<div
|
|
90
|
+
class="flex shrink-0 items-center justify-end gap-2 border-t border-line bg-surface-2/40 px-5 py-3"
|
|
91
|
+
>
|
|
92
|
+
{@render footer()}
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
@keyframes drawer-in-right {
|
|
101
|
+
from {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
transform: translateX(16px);
|
|
104
|
+
}
|
|
105
|
+
to {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
transform: translateX(0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
@keyframes drawer-in-left {
|
|
111
|
+
from {
|
|
112
|
+
opacity: 0;
|
|
113
|
+
transform: translateX(-16px);
|
|
114
|
+
}
|
|
115
|
+
to {
|
|
116
|
+
opacity: 1;
|
|
117
|
+
transform: translateX(0);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Side = 'left' | 'right';
|
|
3
|
+
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
4
|
+
interface Props {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
side?: Side;
|
|
9
|
+
size?: Size;
|
|
10
|
+
onclose?: () => void;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
footer?: Snippet;
|
|
13
|
+
}
|
|
14
|
+
declare const Drawer: import("svelte").Component<Props, {}, "open">;
|
|
15
|
+
type Drawer = ReturnType<typeof Drawer>;
|
|
16
|
+
export default Drawer;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface DropdownItem {
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: Component<{ size?: number; class?: string }>;
|
|
7
|
+
variant?: 'default' | 'danger';
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onclick: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
items,
|
|
14
|
+
align = 'right',
|
|
15
|
+
class: cls = '',
|
|
16
|
+
trigger,
|
|
17
|
+
}: {
|
|
18
|
+
items: DropdownItem[];
|
|
19
|
+
align?: 'left' | 'right';
|
|
20
|
+
class?: string;
|
|
21
|
+
trigger: Snippet<[{ open: boolean; toggle: () => void }]>;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
let open = $state(false);
|
|
25
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
26
|
+
|
|
27
|
+
function toggle() {
|
|
28
|
+
open = !open;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleItemClick(item: DropdownItem) {
|
|
32
|
+
if (item.disabled) return;
|
|
33
|
+
open = false;
|
|
34
|
+
item.onclick();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleOutsideClick(e: MouseEvent) {
|
|
38
|
+
if (containerEl && !containerEl.contains(e.target as Node)) open = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
42
|
+
if (e.key === 'Escape') open = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (open) {
|
|
47
|
+
document.addEventListener('click', handleOutsideClick, { capture: true });
|
|
48
|
+
document.addEventListener('keydown', handleKeydown);
|
|
49
|
+
}
|
|
50
|
+
return () => {
|
|
51
|
+
document.removeEventListener('click', handleOutsideClick, { capture: true });
|
|
52
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div bind:this={containerEl} class="relative inline-block {cls}">
|
|
58
|
+
{@render trigger({ open, toggle })}
|
|
59
|
+
|
|
60
|
+
{#if open}
|
|
61
|
+
<div
|
|
62
|
+
role="menu"
|
|
63
|
+
class="absolute z-50 mt-1 py-1 min-w-36 rounded-xl border border-line
|
|
64
|
+
bg-surface shadow-2xl
|
|
65
|
+
{align === 'right' ? 'right-0' : 'left-0'}"
|
|
66
|
+
>
|
|
67
|
+
{#each items as item}
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
role="menuitem"
|
|
71
|
+
disabled={item.disabled}
|
|
72
|
+
onclick={() => handleItemClick(item)}
|
|
73
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors
|
|
74
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
75
|
+
{item.variant === 'danger'
|
|
76
|
+
? 'text-danger hover:bg-danger/10'
|
|
77
|
+
: 'text-fg hover:bg-surface-2'}"
|
|
78
|
+
>
|
|
79
|
+
{#if item.icon}
|
|
80
|
+
{@const Icon = item.icon}
|
|
81
|
+
<Icon size={14} class="shrink-0" />
|
|
82
|
+
{/if}
|
|
83
|
+
{item.label}
|
|
84
|
+
</button>
|
|
85
|
+
{/each}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
interface DropdownItem {
|
|
3
|
+
label: string;
|
|
4
|
+
icon?: Component<{
|
|
5
|
+
size?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
}>;
|
|
8
|
+
variant?: 'default' | 'danger';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onclick: () => void;
|
|
11
|
+
}
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
items: DropdownItem[];
|
|
14
|
+
align?: 'left' | 'right';
|
|
15
|
+
class?: string;
|
|
16
|
+
trigger: Snippet<[{
|
|
17
|
+
open: boolean;
|
|
18
|
+
toggle: () => void;
|
|
19
|
+
}]>;
|
|
20
|
+
};
|
|
21
|
+
declare const Dropdown: Component<$$ComponentProps, {}, "">;
|
|
22
|
+
type Dropdown = ReturnType<typeof Dropdown>;
|
|
23
|
+
export default Dropdown;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
icon?: string;
|
|
6
|
+
iconSnippet?: Snippet;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
action?: Snippet;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
icon = undefined,
|
|
16
|
+
iconSnippet = undefined,
|
|
17
|
+
title,
|
|
18
|
+
description = undefined,
|
|
19
|
+
class: klass = '',
|
|
20
|
+
action,
|
|
21
|
+
children,
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
class="flex flex-col items-center justify-center text-center rounded-xl border border-dashed border-line bg-surface/40 px-6 py-12 {klass}"
|
|
27
|
+
>
|
|
28
|
+
{#if iconSnippet}
|
|
29
|
+
<div
|
|
30
|
+
class="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-surface-2 text-muted"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
{@render iconSnippet()}
|
|
34
|
+
</div>
|
|
35
|
+
{:else if icon}
|
|
36
|
+
<div
|
|
37
|
+
class="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-surface-2 text-2xl"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
>
|
|
40
|
+
{icon}
|
|
41
|
+
</div>
|
|
42
|
+
{/if}
|
|
43
|
+
<h3 class="text-base font-semibold text-fg">{title}</h3>
|
|
44
|
+
{#if description}
|
|
45
|
+
<p class="mt-1 max-w-sm text-sm text-muted">{description}</p>
|
|
46
|
+
{/if}
|
|
47
|
+
{#if children}<div class="mt-1 max-w-sm text-sm text-muted">{@render children()}</div>{/if}
|
|
48
|
+
{#if action}
|
|
49
|
+
<div class="mt-5">{@render action()}</div>
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
icon?: string;
|
|
4
|
+
iconSnippet?: Snippet;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
action?: Snippet;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
declare const EmptyState: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
13
|
+
export default EmptyState;
|