@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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/dist/components/Accordion.svelte +48 -0
  4. package/dist/components/Accordion.svelte.d.ts +10 -0
  5. package/dist/components/AccordionItem.svelte +55 -0
  6. package/dist/components/AccordionItem.svelte.d.ts +10 -0
  7. package/dist/components/Alert.svelte +60 -0
  8. package/dist/components/Alert.svelte.d.ts +13 -0
  9. package/dist/components/Autocomplete.svelte +198 -0
  10. package/dist/components/Autocomplete.svelte.d.ts +23 -0
  11. package/dist/components/Avatar.svelte +65 -0
  12. package/dist/components/Avatar.svelte.d.ts +9 -0
  13. package/dist/components/AvatarGroup.svelte +45 -0
  14. package/dist/components/AvatarGroup.svelte.d.ts +14 -0
  15. package/dist/components/Badge.svelte +48 -0
  16. package/dist/components/Badge.svelte.d.ts +13 -0
  17. package/dist/components/Banner.svelte +73 -0
  18. package/dist/components/Banner.svelte.d.ts +13 -0
  19. package/dist/components/Breadcrumb.svelte +47 -0
  20. package/dist/components/Breadcrumb.svelte.d.ts +10 -0
  21. package/dist/components/Button.svelte +71 -0
  22. package/dist/components/Button.svelte.d.ts +18 -0
  23. package/dist/components/ButtonGroup.svelte +33 -0
  24. package/dist/components/ButtonGroup.svelte.d.ts +9 -0
  25. package/dist/components/Card.svelte +72 -0
  26. package/dist/components/Card.svelte.d.ts +17 -0
  27. package/dist/components/Checkbox.svelte +80 -0
  28. package/dist/components/Checkbox.svelte.d.ts +14 -0
  29. package/dist/components/DatePicker.svelte +274 -0
  30. package/dist/components/DatePicker.svelte.d.ts +17 -0
  31. package/dist/components/Divider.svelte +23 -0
  32. package/dist/components/Divider.svelte.d.ts +8 -0
  33. package/dist/components/Drawer.svelte +120 -0
  34. package/dist/components/Drawer.svelte.d.ts +16 -0
  35. package/dist/components/Dropdown.svelte +88 -0
  36. package/dist/components/Dropdown.svelte.d.ts +23 -0
  37. package/dist/components/EmptyState.svelte +51 -0
  38. package/dist/components/EmptyState.svelte.d.ts +13 -0
  39. package/dist/components/FileInput.svelte +102 -0
  40. package/dist/components/FileInput.svelte.d.ts +15 -0
  41. package/dist/components/Indicator.svelte +38 -0
  42. package/dist/components/Indicator.svelte.d.ts +11 -0
  43. package/dist/components/Input.svelte +60 -0
  44. package/dist/components/Input.svelte.d.ts +17 -0
  45. package/dist/components/Kbd.svelte +18 -0
  46. package/dist/components/Kbd.svelte.d.ts +8 -0
  47. package/dist/components/Label.svelte +30 -0
  48. package/dist/components/Label.svelte.d.ts +11 -0
  49. package/dist/components/Modal.svelte +105 -0
  50. package/dist/components/Modal.svelte.d.ts +14 -0
  51. package/dist/components/MultiSelect.svelte +212 -0
  52. package/dist/components/MultiSelect.svelte.d.ts +22 -0
  53. package/dist/components/NumberInput.svelte +98 -0
  54. package/dist/components/NumberInput.svelte.d.ts +15 -0
  55. package/dist/components/PageHeader.svelte +22 -0
  56. package/dist/components/PageHeader.svelte.d.ts +10 -0
  57. package/dist/components/Pagination.svelte +112 -0
  58. package/dist/components/Pagination.svelte.d.ts +10 -0
  59. package/dist/components/Progress.svelte +70 -0
  60. package/dist/components/Progress.svelte.d.ts +15 -0
  61. package/dist/components/Radio.svelte +65 -0
  62. package/dist/components/Radio.svelte.d.ts +14 -0
  63. package/dist/components/RadioGroup.svelte +96 -0
  64. package/dist/components/RadioGroup.svelte.d.ts +22 -0
  65. package/dist/components/SearchInput.svelte +81 -0
  66. package/dist/components/SearchInput.svelte.d.ts +10 -0
  67. package/dist/components/Select.svelte +69 -0
  68. package/dist/components/Select.svelte.d.ts +18 -0
  69. package/dist/components/Skeleton.svelte +20 -0
  70. package/dist/components/Skeleton.svelte.d.ts +9 -0
  71. package/dist/components/Spinner.svelte +19 -0
  72. package/dist/components/Spinner.svelte.d.ts +7 -0
  73. package/dist/components/Stat.svelte +45 -0
  74. package/dist/components/Stat.svelte.d.ts +14 -0
  75. package/dist/components/StepIndicator.svelte +142 -0
  76. package/dist/components/StepIndicator.svelte.d.ts +13 -0
  77. package/dist/components/Table.svelte +36 -0
  78. package/dist/components/Table.svelte.d.ts +11 -0
  79. package/dist/components/Tabs.svelte +39 -0
  80. package/dist/components/Tabs.svelte.d.ts +13 -0
  81. package/dist/components/Tag.svelte +56 -0
  82. package/dist/components/Tag.svelte.d.ts +11 -0
  83. package/dist/components/Textarea.svelte +75 -0
  84. package/dist/components/Textarea.svelte.d.ts +26 -0
  85. package/dist/components/ThemeToggle.svelte +33 -0
  86. package/dist/components/ThemeToggle.svelte.d.ts +6 -0
  87. package/dist/components/Toaster.svelte +54 -0
  88. package/dist/components/Toaster.svelte.d.ts +18 -0
  89. package/dist/components/Toggle.svelte +74 -0
  90. package/dist/components/Toggle.svelte.d.ts +11 -0
  91. package/dist/components/Tooltip.svelte +44 -0
  92. package/dist/components/Tooltip.svelte.d.ts +11 -0
  93. package/dist/components/dialog/ConfirmDialog.svelte +95 -0
  94. package/dist/components/dialog/ConfirmDialog.svelte.d.ts +8 -0
  95. package/dist/components/dialog/Dialog.svelte +225 -0
  96. package/dist/components/dialog/Dialog.svelte.d.ts +9 -0
  97. package/dist/components/dialog/DialogContainer.svelte +17 -0
  98. package/dist/components/dialog/DialogContainer.svelte.d.ts +18 -0
  99. package/dist/components/dialog/dialog-manager.svelte.d.ts +54 -0
  100. package/dist/components/dialog/dialog-manager.svelte.js +164 -0
  101. package/dist/components/dialog/types.d.ts +33 -0
  102. package/dist/components/dialog/types.js +10 -0
  103. package/dist/index.d.ts +63 -0
  104. package/dist/index.js +72 -0
  105. package/dist/stores/toast.svelte.d.ts +18 -0
  106. package/dist/stores/toast.svelte.js +28 -0
  107. package/dist/styles/theme.css +148 -0
  108. package/dist/utils/cn.d.ts +13 -0
  109. package/dist/utils/cn.js +24 -0
  110. package/dist/utils/theme.d.ts +26 -0
  111. package/dist/utils/theme.js +55 -0
  112. package/package.json +81 -0
  113. 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,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ class?: string;
4
+ children?: Snippet;
5
+ };
6
+ declare const Divider: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type Divider = ReturnType<typeof Divider>;
8
+ export default Divider;
@@ -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;