@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,102 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ id?: string;
4
+ name?: string;
5
+ accept?: string;
6
+ multiple?: boolean;
7
+ disabled?: boolean;
8
+ error?: string;
9
+ hint?: string;
10
+ label?: string;
11
+ class?: string;
12
+ onchange?: (files: FileList | null) => void;
13
+ }
14
+
15
+ let {
16
+ id = undefined,
17
+ name = undefined,
18
+ accept = undefined,
19
+ multiple = false,
20
+ disabled = false,
21
+ error = undefined,
22
+ hint = undefined,
23
+ label = undefined,
24
+ class: cls = '',
25
+ onchange = undefined,
26
+ }: Props = $props();
27
+
28
+ let dragOver = $state(false);
29
+
30
+ function handleChange(e: Event & { currentTarget: HTMLInputElement }) {
31
+ onchange?.(e.currentTarget.files);
32
+ }
33
+
34
+ function handleDrop(e: DragEvent) {
35
+ e.preventDefault();
36
+ dragOver = false;
37
+ if (disabled) return;
38
+ onchange?.(e.dataTransfer?.files ?? null);
39
+ }
40
+ </script>
41
+
42
+ <div class="flex flex-col gap-1.5 {cls}">
43
+ {#if label}
44
+ <label for={id} class="text-sm font-medium text-fg">{label}</label>
45
+ {/if}
46
+
47
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
48
+ <label
49
+ class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-xl
50
+ border-2 border-dashed px-4 py-8 text-center transition-colors
51
+ {error
52
+ ? 'border-danger bg-danger/5 hover:bg-danger/8'
53
+ : dragOver
54
+ ? 'border-brand bg-brand/8'
55
+ : 'border-line bg-surface-2/40 hover:border-brand/50 hover:bg-brand/5'}
56
+ {disabled ? 'cursor-not-allowed opacity-50' : ''}"
57
+ ondragover={(e) => {
58
+ e.preventDefault();
59
+ dragOver = true;
60
+ }}
61
+ ondragleave={() => (dragOver = false)}
62
+ ondrop={handleDrop}
63
+ >
64
+ <svg
65
+ width="24"
66
+ height="24"
67
+ viewBox="0 0 24 24"
68
+ fill="none"
69
+ stroke="currentColor"
70
+ stroke-width="1.5"
71
+ stroke-linecap="round"
72
+ stroke-linejoin="round"
73
+ class={error ? 'text-danger' : 'text-faint'}
74
+ aria-hidden="true"
75
+ >
76
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
77
+ <polyline points="17 8 12 3 7 8" />
78
+ <line x1="12" y1="3" x2="12" y2="15" />
79
+ </svg>
80
+ <div class="text-sm">
81
+ <span class="{error ? 'text-danger' : 'text-brand'} font-medium">Click to upload</span>
82
+ <span class="text-muted"> or drag & drop</span>
83
+ {#if accept}<p class="mt-0.5 text-xs text-faint">{accept}</p>{/if}
84
+ </div>
85
+ <input
86
+ type="file"
87
+ {id}
88
+ {name}
89
+ {accept}
90
+ {multiple}
91
+ {disabled}
92
+ onchange={handleChange}
93
+ class="sr-only"
94
+ />
95
+ </label>
96
+
97
+ {#if error}
98
+ <p class="text-xs text-danger">{error}</p>
99
+ {:else if hint}
100
+ <p class="text-xs text-faint">{hint}</p>
101
+ {/if}
102
+ </div>
@@ -0,0 +1,15 @@
1
+ interface Props {
2
+ id?: string;
3
+ name?: string;
4
+ accept?: string;
5
+ multiple?: boolean;
6
+ disabled?: boolean;
7
+ error?: string;
8
+ hint?: string;
9
+ label?: string;
10
+ class?: string;
11
+ onchange?: (files: FileList | null) => void;
12
+ }
13
+ declare const FileInput: import("svelte").Component<Props, {}, "">;
14
+ type FileInput = ReturnType<typeof FileInput>;
15
+ export default FileInput;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ type Tone = 'brand' | 'success' | 'warn' | 'danger' | 'neutral' | 'violet';
3
+ type Size = 'xs' | 'sm' | 'md' | 'lg';
4
+
5
+ interface Props {
6
+ tone?: Tone;
7
+ pulse?: boolean;
8
+ size?: Size;
9
+ class?: string;
10
+ }
11
+
12
+ let { tone = 'success', pulse = false, size = 'sm', class: cls = '' }: Props = $props();
13
+
14
+ const tones: Record<Tone, string> = {
15
+ brand: 'bg-brand',
16
+ success: 'bg-success',
17
+ warn: 'bg-warn',
18
+ danger: 'bg-danger',
19
+ neutral: 'bg-muted',
20
+ violet: 'bg-violet',
21
+ };
22
+
23
+ const sizes: Record<Size, string> = {
24
+ xs: 'w-1.5 h-1.5',
25
+ sm: 'w-2 h-2',
26
+ md: 'w-2.5 h-2.5',
27
+ lg: 'w-3.5 h-3.5',
28
+ };
29
+ </script>
30
+
31
+ <span class="relative inline-flex {cls}" aria-hidden="true">
32
+ {#if pulse}
33
+ <span
34
+ class="absolute inline-flex h-full w-full animate-ping rounded-full opacity-60 {tones[tone]}"
35
+ ></span>
36
+ {/if}
37
+ <span class="relative inline-flex rounded-full {sizes[size]} {tones[tone]}"></span>
38
+ </span>
@@ -0,0 +1,11 @@
1
+ type Tone = 'brand' | 'success' | 'warn' | 'danger' | 'neutral' | 'violet';
2
+ type Size = 'xs' | 'sm' | 'md' | 'lg';
3
+ interface Props {
4
+ tone?: Tone;
5
+ pulse?: boolean;
6
+ size?: Size;
7
+ class?: string;
8
+ }
9
+ declare const Indicator: import("svelte").Component<Props, {}, "">;
10
+ type Indicator = ReturnType<typeof Indicator>;
11
+ export default Indicator;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ value?: string;
4
+ label?: string;
5
+ type?: string;
6
+ placeholder?: string;
7
+ hint?: string;
8
+ error?: string;
9
+ disabled?: boolean;
10
+ required?: boolean;
11
+ id?: string;
12
+ name?: string;
13
+ class?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ let {
18
+ value = $bindable(''),
19
+ label = undefined,
20
+ type = 'text',
21
+ placeholder = '',
22
+ hint = undefined,
23
+ error = undefined,
24
+ disabled = false,
25
+ required = false,
26
+ id = undefined,
27
+ class: klass = '',
28
+ ...rest
29
+ }: Props = $props();
30
+
31
+ const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
32
+ </script>
33
+
34
+ <div class="flex flex-col gap-1.5 {klass}">
35
+ {#if label}
36
+ <label for={fieldId} class="text-sm font-medium text-fg">
37
+ {label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
38
+ </label>
39
+ {/if}
40
+
41
+ <input
42
+ id={fieldId}
43
+ {type}
44
+ {placeholder}
45
+ {disabled}
46
+ {required}
47
+ bind:value
48
+ aria-invalid={error ? 'true' : undefined}
49
+ class="w-full rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg placeholder:text-faint
50
+ transition-colors outline-none disabled:opacity-50 disabled:cursor-not-allowed
51
+ {error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
52
+ {...rest}
53
+ />
54
+
55
+ {#if error}
56
+ <p class="text-xs text-danger">{error}</p>
57
+ {:else if hint}
58
+ <p class="text-xs text-faint">{hint}</p>
59
+ {/if}
60
+ </div>
@@ -0,0 +1,17 @@
1
+ interface Props {
2
+ value?: string;
3
+ label?: string;
4
+ type?: string;
5
+ placeholder?: string;
6
+ hint?: string;
7
+ error?: string;
8
+ disabled?: boolean;
9
+ required?: boolean;
10
+ id?: string;
11
+ name?: string;
12
+ class?: string;
13
+ [key: string]: unknown;
14
+ }
15
+ declare const Input: import("svelte").Component<Props, {}, "value">;
16
+ type Input = ReturnType<typeof Input>;
17
+ export default Input;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ class?: string;
6
+ children: Snippet;
7
+ }
8
+
9
+ let { class: cls = '', children }: Props = $props();
10
+ </script>
11
+
12
+ <kbd
13
+ class="inline-flex items-center justify-center rounded border border-line bg-surface-2
14
+ px-1.5 py-0.5 font-mono text-[11px] text-muted
15
+ shadow-[inset_0_-1px_0] shadow-line {cls}"
16
+ >
17
+ {@render children()}
18
+ </kbd>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ class?: string;
4
+ children: Snippet;
5
+ }
6
+ declare const Kbd: import("svelte").Component<Props, {}, "">;
7
+ type Kbd = ReturnType<typeof Kbd>;
8
+ export default Kbd;
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ let {
5
+ for: htmlFor,
6
+ required = false,
7
+ hint,
8
+ class: cls = '',
9
+ children,
10
+ }: {
11
+ for?: string;
12
+ required?: boolean;
13
+ hint?: string;
14
+ class?: string;
15
+ children: Snippet;
16
+ } = $props();
17
+ </script>
18
+
19
+ <label
20
+ for={htmlFor}
21
+ class="flex items-center gap-1 text-xs font-medium text-muted uppercase tracking-wider {cls}"
22
+ >
23
+ {@render children()}
24
+ {#if required}
25
+ <span class="text-danger font-bold leading-none" aria-label="required">*</span>
26
+ {/if}
27
+ {#if hint}
28
+ <span class="text-faint normal-case font-normal ml-0.5">({hint})</span>
29
+ {/if}
30
+ </label>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ for?: string;
4
+ required?: boolean;
5
+ hint?: string;
6
+ class?: string;
7
+ children: Snippet;
8
+ };
9
+ declare const Label: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Label = ReturnType<typeof Label>;
11
+ export default Label;
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Size = 'sm' | 'md' | 'lg';
5
+
6
+ interface Props {
7
+ open?: boolean;
8
+ title?: string;
9
+ description?: string;
10
+ size?: Size;
11
+ onclose?: () => void;
12
+ children: Snippet;
13
+ footer?: Snippet;
14
+ }
15
+
16
+ let {
17
+ open = $bindable(false),
18
+ title = undefined,
19
+ description = undefined,
20
+ size = 'md',
21
+ onclose = undefined,
22
+ children,
23
+ footer,
24
+ }: Props = $props();
25
+
26
+ const widths: Record<Size, string> = {
27
+ sm: 'max-w-sm',
28
+ md: 'max-w-lg',
29
+ lg: 'max-w-2xl',
30
+ };
31
+
32
+ function close() {
33
+ open = false;
34
+ onclose?.();
35
+ }
36
+
37
+ function onkeydown(e: KeyboardEvent) {
38
+ if (e.key === 'Escape') {
39
+ e.stopPropagation();
40
+ close();
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <svelte:window onkeydown={open ? onkeydown : undefined} />
46
+
47
+ {#if open}
48
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
49
+ <button
50
+ type="button"
51
+ class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
52
+ aria-label="Close"
53
+ onclick={close}
54
+ ></button>
55
+
56
+ <div
57
+ class="relative w-full {widths[size]} rounded-xl border border-line bg-surface shadow-2xl
58
+ animate-[modal-in_120ms_ease-out]"
59
+ role="dialog"
60
+ aria-modal="true"
61
+ >
62
+ {#if title}
63
+ <div class="flex items-start justify-between gap-4 px-5 py-4 border-b border-line">
64
+ <div>
65
+ <h2 class="font-semibold text-fg">{title}</h2>
66
+ {#if description}<p class="text-sm text-muted mt-0.5">{description}</p>{/if}
67
+ </div>
68
+ <button
69
+ type="button"
70
+ onclick={close}
71
+ class="text-faint hover:text-fg transition-colors text-xl leading-none -mt-1"
72
+ aria-label="Close"
73
+ >
74
+ ×
75
+ </button>
76
+ </div>
77
+ {/if}
78
+
79
+ <div class="px-5 py-4">
80
+ {@render children()}
81
+ </div>
82
+
83
+ {#if footer}
84
+ <div
85
+ class="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface-2/40"
86
+ >
87
+ {@render footer()}
88
+ </div>
89
+ {/if}
90
+ </div>
91
+ </div>
92
+ {/if}
93
+
94
+ <style>
95
+ @keyframes modal-in {
96
+ from {
97
+ opacity: 0;
98
+ transform: translateY(6px) scale(0.98);
99
+ }
100
+ to {
101
+ opacity: 1;
102
+ transform: translateY(0) scale(1);
103
+ }
104
+ }
105
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Size = 'sm' | 'md' | 'lg';
3
+ interface Props {
4
+ open?: boolean;
5
+ title?: string;
6
+ description?: string;
7
+ size?: Size;
8
+ onclose?: () => void;
9
+ children: Snippet;
10
+ footer?: Snippet;
11
+ }
12
+ declare const Modal: import("svelte").Component<Props, {}, "open">;
13
+ type Modal = ReturnType<typeof Modal>;
14
+ export default Modal;
@@ -0,0 +1,212 @@
1
+ <script lang="ts">
2
+ // MultiSelect: pick several options, shown as removable chips, with an optional
3
+ // search box. Binds to a string[] of selected values.
4
+ interface Option {
5
+ value: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ }
9
+
10
+ let {
11
+ value = $bindable<string[]>([]),
12
+ options,
13
+ label = undefined,
14
+ hint = undefined,
15
+ error = undefined,
16
+ placeholder = 'Select…',
17
+ searchable = true,
18
+ disabled = false,
19
+ required = false,
20
+ id = undefined,
21
+ class: cls = '',
22
+ onchange = undefined,
23
+ }: {
24
+ value?: string[];
25
+ options: Option[];
26
+ label?: string;
27
+ hint?: string;
28
+ error?: string;
29
+ placeholder?: string;
30
+ searchable?: boolean;
31
+ disabled?: boolean;
32
+ required?: boolean;
33
+ id?: string;
34
+ class?: string;
35
+ onchange?: (value: string[]) => void;
36
+ } = $props();
37
+
38
+ let open = $state(false);
39
+ let query = $state('');
40
+ let containerEl: HTMLDivElement | undefined = $state();
41
+
42
+ const selected = $derived(options.filter((o) => value.includes(o.value)));
43
+ const filtered = $derived(
44
+ options.filter((o) => o.label.toLowerCase().includes(query.trim().toLowerCase())),
45
+ );
46
+
47
+ function isSelected(v: string) {
48
+ return value.includes(v);
49
+ }
50
+
51
+ function toggle(opt: Option) {
52
+ if (opt.disabled) return;
53
+ value = isSelected(opt.value) ? value.filter((v) => v !== opt.value) : [...value, opt.value];
54
+ onchange?.(value);
55
+ }
56
+
57
+ function remove(v: string) {
58
+ value = value.filter((x) => x !== v);
59
+ onchange?.(value);
60
+ }
61
+
62
+ function open_() {
63
+ if (!disabled) open = true;
64
+ }
65
+
66
+ function handleOutside(e: MouseEvent) {
67
+ if (containerEl && !containerEl.contains(e.target as Node)) {
68
+ open = false;
69
+ query = '';
70
+ }
71
+ }
72
+ function handleKey(e: KeyboardEvent) {
73
+ if (e.key === 'Escape') open = false;
74
+ }
75
+
76
+ $effect(() => {
77
+ if (open) {
78
+ document.addEventListener('click', handleOutside, { capture: true });
79
+ document.addEventListener('keydown', handleKey);
80
+ }
81
+ return () => {
82
+ document.removeEventListener('click', handleOutside, { capture: true });
83
+ document.removeEventListener('keydown', handleKey);
84
+ };
85
+ });
86
+ </script>
87
+
88
+ <div class="flex flex-col gap-1.5 {cls}" bind:this={containerEl}>
89
+ {#if label}
90
+ <label for={id} class="text-sm font-medium text-fg">
91
+ {label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
92
+ </label>
93
+ {/if}
94
+
95
+ <div class="relative">
96
+ <!-- Trigger (a div, not a button, so the chip remove-buttons are valid children) -->
97
+ <div
98
+ {id}
99
+ role="button"
100
+ tabindex={disabled ? -1 : 0}
101
+ aria-haspopup="listbox"
102
+ aria-expanded={open}
103
+ aria-disabled={disabled}
104
+ onclick={() => (open ? (open = false) : open_())}
105
+ onkeydown={(e) => {
106
+ if (e.key === 'Enter' || e.key === ' ') {
107
+ e.preventDefault();
108
+ open ? (open = false) : open_();
109
+ }
110
+ }}
111
+ class="w-full min-h-[2.375rem] flex flex-wrap items-center gap-1.5 rounded-lg border bg-surface-2
112
+ px-2.5 py-1.5 text-left text-sm transition-colors outline-none cursor-pointer
113
+ focus-visible:border-brand
114
+ {disabled ? 'opacity-50 cursor-not-allowed' : ''}
115
+ {error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
116
+ >
117
+ {#if selected.length === 0}
118
+ <span class="text-faint px-0.5">{placeholder}</span>
119
+ {:else}
120
+ {#each selected as opt (opt.value)}
121
+ <span
122
+ class="inline-flex items-center gap-1 rounded-md bg-brand/10 text-brand px-2 py-0.5 text-xs"
123
+ >
124
+ {opt.label}
125
+ <button
126
+ type="button"
127
+ aria-label="Remove {opt.label}"
128
+ onclick={(e) => {
129
+ e.stopPropagation();
130
+ remove(opt.value);
131
+ }}
132
+ class="hover:text-brand-light leading-none">×</button
133
+ >
134
+ </span>
135
+ {/each}
136
+ {/if}
137
+ <span class="ml-auto shrink-0 text-faint" aria-hidden="true">
138
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
139
+ <path
140
+ d="M2 4l4 4 4-4"
141
+ stroke="currentColor"
142
+ stroke-width="1.5"
143
+ stroke-linecap="round"
144
+ stroke-linejoin="round"
145
+ />
146
+ </svg>
147
+ </span>
148
+ </div>
149
+
150
+ <!-- Panel -->
151
+ {#if open}
152
+ <div
153
+ role="listbox"
154
+ aria-multiselectable="true"
155
+ class="absolute z-50 mt-1 w-full rounded-xl border border-line bg-surface shadow-2xl overflow-hidden"
156
+ >
157
+ {#if searchable}
158
+ <div class="p-2 border-b border-line">
159
+ <!-- svelte-ignore a11y_autofocus -->
160
+ <input
161
+ autofocus
162
+ bind:value={query}
163
+ placeholder="Search…"
164
+ class="w-full rounded-md bg-surface-2 border border-line px-2.5 py-1.5 text-sm text-fg
165
+ placeholder:text-faint outline-none focus:border-brand"
166
+ />
167
+ </div>
168
+ {/if}
169
+ <div class="max-h-60 overflow-y-auto py-1">
170
+ {#each filtered as opt (opt.value)}
171
+ <button
172
+ type="button"
173
+ role="option"
174
+ aria-selected={isSelected(opt.value)}
175
+ disabled={opt.disabled}
176
+ onclick={() => toggle(opt)}
177
+ class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-fg transition-colors
178
+ hover:bg-surface-2 disabled:opacity-40 disabled:cursor-not-allowed"
179
+ >
180
+ <span
181
+ class="flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors
182
+ {isSelected(opt.value) ? 'bg-brand border-brand' : 'bg-surface-2 border-line'}"
183
+ >
184
+ {#if isSelected(opt.value)}
185
+ <svg width="10" height="8" viewBox="0 0 10 8" fill="none" aria-hidden="true">
186
+ <path
187
+ d="M1 4l3 3 5-6"
188
+ stroke="currentColor"
189
+ stroke-width="1.5"
190
+ stroke-linecap="round"
191
+ stroke-linejoin="round"
192
+ class="text-ink"
193
+ />
194
+ </svg>
195
+ {/if}
196
+ </span>
197
+ <span class="text-left">{opt.label}</span>
198
+ </button>
199
+ {:else}
200
+ <p class="px-3 py-2 text-sm text-faint">No matches</p>
201
+ {/each}
202
+ </div>
203
+ </div>
204
+ {/if}
205
+ </div>
206
+
207
+ {#if error}
208
+ <p class="text-xs text-danger">{error}</p>
209
+ {:else if hint}
210
+ <p class="text-xs text-faint">{hint}</p>
211
+ {/if}
212
+ </div>
@@ -0,0 +1,22 @@
1
+ interface Option {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+ type $$ComponentProps = {
7
+ value?: string[];
8
+ options: Option[];
9
+ label?: string;
10
+ hint?: string;
11
+ error?: string;
12
+ placeholder?: string;
13
+ searchable?: boolean;
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ id?: string;
17
+ class?: string;
18
+ onchange?: (value: string[]) => void;
19
+ };
20
+ declare const MultiSelect: import("svelte").Component<$$ComponentProps, {}, "value">;
21
+ type MultiSelect = ReturnType<typeof MultiSelect>;
22
+ export default MultiSelect;