@makolabs/ripple 0.0.1 → 0.0.3

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 (144) hide show
  1. package/README.md +575 -8
  2. package/dist/adapters/storage/BaseAdapter.d.ts +20 -0
  3. package/dist/adapters/storage/BaseAdapter.js +171 -0
  4. package/dist/adapters/storage/S3Adapter.d.ts +21 -0
  5. package/dist/adapters/storage/S3Adapter.js +194 -0
  6. package/dist/adapters/storage/index.d.ts +3 -0
  7. package/dist/adapters/storage/index.js +3 -0
  8. package/dist/adapters/storage/types.d.ts +102 -0
  9. package/dist/adapters/storage/types.js +4 -0
  10. package/dist/button/Button.svelte +48 -0
  11. package/dist/button/Button.svelte.d.ts +4 -0
  12. package/dist/button/button.d.ts +113 -0
  13. package/dist/button/button.js +168 -0
  14. package/dist/charts/Chart.svelte +545 -0
  15. package/dist/charts/Chart.svelte.d.ts +4 -0
  16. package/dist/drawer/Drawer.svelte +224 -0
  17. package/dist/drawer/Drawer.svelte.d.ts +4 -0
  18. package/dist/drawer/drawer.d.ts +160 -0
  19. package/dist/drawer/drawer.js +80 -0
  20. package/dist/elements/accordion/Accordion.svelte +98 -0
  21. package/dist/elements/accordion/Accordion.svelte.d.ts +4 -0
  22. package/dist/elements/accordion/accordion.d.ts +227 -0
  23. package/dist/elements/accordion/accordion.js +138 -0
  24. package/dist/elements/alert/Alert.svelte +57 -0
  25. package/dist/elements/alert/Alert.svelte.d.ts +4 -0
  26. package/dist/elements/badge/Badge.svelte +43 -0
  27. package/dist/elements/badge/Badge.svelte.d.ts +4 -0
  28. package/dist/elements/badge/badge.d.ts +181 -0
  29. package/dist/elements/badge/badge.js +65 -0
  30. package/dist/elements/dropdown/Dropdown.svelte +234 -0
  31. package/dist/elements/dropdown/Dropdown.svelte.d.ts +4 -0
  32. package/dist/elements/dropdown/Select.svelte +333 -0
  33. package/dist/elements/dropdown/Select.svelte.d.ts +4 -0
  34. package/dist/elements/dropdown/dropdown.d.ts +251 -0
  35. package/dist/elements/dropdown/dropdown.js +95 -0
  36. package/dist/elements/dropdown/select.d.ts +200 -0
  37. package/dist/elements/dropdown/select.js +82 -0
  38. package/dist/elements/file-upload/FileUpload.svelte +135 -0
  39. package/dist/elements/file-upload/FileUpload.svelte.d.ts +4 -0
  40. package/dist/elements/file-upload/FilesPreview.svelte +93 -0
  41. package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
  42. package/dist/elements/progress/Progress.svelte +145 -0
  43. package/dist/elements/progress/Progress.svelte.d.ts +4 -0
  44. package/dist/elements/timeline/Timeline.svelte +92 -0
  45. package/dist/elements/timeline/Timeline.svelte.d.ts +7 -0
  46. package/dist/file-browser/FileBrowser.svelte +877 -0
  47. package/dist/file-browser/FileBrowser.svelte.d.ts +14 -0
  48. package/dist/file-browser/index.d.ts +1 -0
  49. package/dist/file-browser/index.js +1 -0
  50. package/dist/filters/CompactFilters.svelte +147 -0
  51. package/dist/filters/CompactFilters.svelte.d.ts +4 -0
  52. package/dist/filters/index.d.ts +1 -0
  53. package/dist/filters/index.js +1 -0
  54. package/dist/forms/Checkbox.svelte +54 -0
  55. package/dist/forms/Checkbox.svelte.d.ts +4 -0
  56. package/dist/forms/DateRange.svelte +493 -0
  57. package/dist/forms/DateRange.svelte.d.ts +4 -0
  58. package/dist/forms/Form.svelte +39 -0
  59. package/dist/forms/Form.svelte.d.ts +4 -0
  60. package/dist/forms/Input.svelte +86 -0
  61. package/dist/forms/Input.svelte.d.ts +4 -0
  62. package/dist/forms/NumberInput.svelte +159 -0
  63. package/dist/forms/NumberInput.svelte.d.ts +4 -0
  64. package/dist/forms/RadioInputs.svelte +64 -0
  65. package/dist/forms/RadioInputs.svelte.d.ts +4 -0
  66. package/dist/forms/RadioPill.svelte +66 -0
  67. package/dist/forms/RadioPill.svelte.d.ts +4 -0
  68. package/dist/forms/Slider.svelte +342 -0
  69. package/dist/forms/Slider.svelte.d.ts +4 -0
  70. package/dist/forms/Tags.svelte +181 -0
  71. package/dist/forms/Tags.svelte.d.ts +4 -0
  72. package/dist/forms/Toggle.svelte +132 -0
  73. package/dist/forms/Toggle.svelte.d.ts +4 -0
  74. package/dist/forms/slider.d.ts +143 -0
  75. package/dist/forms/slider.js +62 -0
  76. package/dist/header/Breadcrumbs.svelte +73 -0
  77. package/dist/header/Breadcrumbs.svelte.d.ts +4 -0
  78. package/dist/header/PageHeader.svelte +68 -0
  79. package/dist/header/PageHeader.svelte.d.ts +4 -0
  80. package/dist/header/breadcrumbs.d.ts +226 -0
  81. package/dist/header/breadcrumbs.js +87 -0
  82. package/dist/helper/cls.d.ts +1 -0
  83. package/dist/helper/cls.js +4 -0
  84. package/dist/helper/date.d.ts +7 -0
  85. package/dist/helper/date.js +15 -0
  86. package/dist/helper/nav.svelte.d.ts +6 -0
  87. package/dist/helper/nav.svelte.js +23 -0
  88. package/dist/index.d.ts +856 -1
  89. package/dist/index.js +78 -1
  90. package/dist/layout/card/Card.svelte +41 -0
  91. package/dist/layout/card/Card.svelte.d.ts +4 -0
  92. package/dist/layout/card/MetricCard.svelte +64 -0
  93. package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
  94. package/dist/layout/card/StatsCard.svelte +266 -0
  95. package/dist/layout/card/StatsCard.svelte.d.ts +4 -0
  96. package/dist/layout/card/card.d.ts +128 -0
  97. package/dist/layout/card/card.js +51 -0
  98. package/dist/layout/card/metric-card.d.ts +49 -0
  99. package/dist/layout/card/metric-card.js +10 -0
  100. package/dist/layout/card/stats-card.d.ts +191 -0
  101. package/dist/layout/card/stats-card.js +73 -0
  102. package/dist/layout/navbar/Navbar.svelte +206 -0
  103. package/dist/layout/navbar/Navbar.svelte.d.ts +4 -0
  104. package/dist/layout/navbar/navbar.d.ts +205 -0
  105. package/dist/layout/navbar/navbar.js +98 -0
  106. package/dist/layout/sidebar/NavGroup.svelte +91 -0
  107. package/dist/layout/sidebar/NavGroup.svelte.d.ts +4 -0
  108. package/dist/layout/sidebar/NavItem.svelte +29 -0
  109. package/dist/layout/sidebar/NavItem.svelte.d.ts +4 -0
  110. package/dist/layout/sidebar/Sidebar.svelte +193 -0
  111. package/dist/layout/sidebar/Sidebar.svelte.d.ts +4 -0
  112. package/dist/layout/table/Cells.svelte +111 -0
  113. package/dist/layout/table/Cells.svelte.d.ts +27 -0
  114. package/dist/layout/table/Table.svelte +790 -0
  115. package/dist/layout/table/Table.svelte.d.ts +4 -0
  116. package/dist/layout/table/table.d.ts +256 -0
  117. package/dist/layout/table/table.js +141 -0
  118. package/dist/layout/tabs/Tab.svelte +60 -0
  119. package/dist/layout/tabs/Tab.svelte.d.ts +4 -0
  120. package/dist/layout/tabs/TabContent.svelte +30 -0
  121. package/dist/layout/tabs/TabContent.svelte.d.ts +4 -0
  122. package/dist/layout/tabs/TabGroup.svelte +62 -0
  123. package/dist/layout/tabs/TabGroup.svelte.d.ts +4 -0
  124. package/dist/layout/tabs/tabs.d.ts +140 -0
  125. package/dist/layout/tabs/tabs.js +298 -0
  126. package/dist/modal/Modal.svelte +207 -0
  127. package/dist/modal/Modal.svelte.d.ts +4 -0
  128. package/dist/modal/modal.d.ts +211 -0
  129. package/dist/modal/modal.js +81 -0
  130. package/dist/sonner/sonner.svelte +13 -0
  131. package/dist/sonner/sonner.svelte.d.ts +4 -0
  132. package/dist/types/variants.d.ts +1 -0
  133. package/dist/types/variants.js +1 -0
  134. package/dist/utils/Portal.svelte +108 -0
  135. package/dist/utils/Portal.svelte.d.ts +8 -0
  136. package/dist/utils/dateUtils.d.ts +7 -0
  137. package/dist/utils/dateUtils.js +26 -0
  138. package/dist/variants.d.ts +30 -0
  139. package/dist/variants.js +36 -0
  140. package/package.json +39 -6
  141. package/dist/layout/Card.svelte +0 -179
  142. package/dist/layout/Card.svelte.d.ts +0 -208
  143. package/dist/layout/index.d.ts +0 -1
  144. package/dist/layout/index.js +0 -1
@@ -0,0 +1,65 @@
1
+ import { tv } from 'tailwind-variants';
2
+ export const badge = tv({
3
+ slots: {
4
+ base: 'w-min inline-flex items-center justify-center whitespace-nowrap',
5
+ icon: 'h-4 w-4'
6
+ },
7
+ variants: {
8
+ size: {
9
+ xs: {
10
+ base: 'h-4 px-1.5 text-xs rounded gap-0.5',
11
+ icon: 'h-2.5 w-2.5'
12
+ },
13
+ sm: {
14
+ base: 'h-5 px-2 text-xs rounded gap-1',
15
+ icon: 'h-3 w-3'
16
+ },
17
+ base: {
18
+ base: 'h-6 px-2.5 text-sm rounded-md gap-1.5',
19
+ icon: 'h-3.5 w-3.5'
20
+ },
21
+ lg: {
22
+ base: 'h-7 px-3 text-base rounded-lg gap-2',
23
+ icon: 'h-4 w-4'
24
+ },
25
+ xl: {
26
+ base: 'h-8 px-3.5 text-lg rounded-xl gap-2.5',
27
+ icon: 'h-5 w-5'
28
+ },
29
+ '2xl': {
30
+ base: 'h-10 px-4 text-xl rounded-2xl gap-3',
31
+ icon: 'h-6 w-6'
32
+ }
33
+ },
34
+ color: {
35
+ default: {
36
+ base: 'bg-default-50 text-default-700',
37
+ icon: 'text-default-700'
38
+ },
39
+ primary: {
40
+ base: 'bg-primary-50 text-primary-700',
41
+ icon: 'text-primary-700'
42
+ },
43
+ secondary: {
44
+ base: 'bg-secondary-50 text-secondary-700',
45
+ icon: 'text-secondary-700'
46
+ },
47
+ success: {
48
+ base: 'bg-success-50 text-success-700',
49
+ icon: 'text-success-700'
50
+ },
51
+ warning: {
52
+ base: 'bg-warning-50 text-warning-700',
53
+ icon: 'text-warning-700'
54
+ },
55
+ danger: {
56
+ base: 'bg-danger-50 text-danger-700',
57
+ icon: 'text-danger-700'
58
+ },
59
+ info: {
60
+ base: 'bg-info-50 text-info-700',
61
+ icon: 'text-info-700'
62
+ }
63
+ }
64
+ }
65
+ });
@@ -0,0 +1,234 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+ import { fly } from 'svelte/transition';
4
+ import type { DropdownMenuProps, DropdownItem } from '../../index.js';
5
+ import { dropdownMenu } from '../../index.js';
6
+ import { onMount, onDestroy } from 'svelte';
7
+ import { Size } from '../../variants.js';
8
+ import Portal from '../../utils/Portal.svelte';
9
+
10
+ let {
11
+ sections = [],
12
+ open: isOpen = $bindable(false),
13
+ label = '',
14
+ icon: Icon,
15
+ containerClass = '',
16
+ itemClass = '',
17
+ class: className = '',
18
+ size = Size.BASE,
19
+ disabled = false,
20
+ position = 'bottom-left',
21
+ width = 'w-56',
22
+ header
23
+ }: DropdownMenuProps = $props();
24
+
25
+ // Determine if we're in icon-only mode
26
+ const iconOnly = $derived(!label && !!Icon);
27
+
28
+ let dropdownRef = $state<HTMLDivElement | undefined>();
29
+ let triggerRef = $state<HTMLDivElement | undefined>();
30
+ let triggerRect = $state<DOMRect | null>(null);
31
+
32
+ const {
33
+ base,
34
+ trigger,
35
+ container,
36
+ section,
37
+ item,
38
+ itemIcon,
39
+ header: headerClass,
40
+ headerTitle,
41
+ headerSubtitle
42
+ } = $derived(
43
+ dropdownMenu({
44
+ position,
45
+ size,
46
+ isOpen,
47
+ iconOnly
48
+ })
49
+ );
50
+
51
+ const baseClass = $derived(cn(base(), className));
52
+ const triggerClass_ = $derived(cn(trigger(), ''));
53
+ const containerClass_ = $derived(cn(container(), width, containerClass, 'shadow-lg'));
54
+ const sectionClass = $derived(cn(section()));
55
+ const itemClass_ = $derived(cn(item(), itemClass));
56
+ const iconClass = $derived(cn(itemIcon()));
57
+ const headerClass_ = $derived(cn(headerClass(), header?.class ?? ''));
58
+ const headerTitleClass = $derived(cn(headerTitle(), header?.titleClass ?? ''));
59
+ const headerSubtitleClass = $derived(cn(headerSubtitle(), header?.subtitleClass ?? ''));
60
+
61
+ const dropdownStyles = $derived.by(() => {
62
+ if (!triggerRect) return '';
63
+
64
+ const { top, left, bottom, right, width: triggerWidth, height: triggerHeight } = triggerRect;
65
+ const viewportWidth = window.innerWidth;
66
+ const viewportHeight = window.innerHeight;
67
+
68
+ // Default to 14rem (w-56) if not specified
69
+ const dropdownWidth = width ? parseInt(width.replace(/[^\d]/g, '')) * 0.25 : 14; // Convert Tailwind widths to rem
70
+ const dropdownWidthPx = dropdownWidth * 16;
71
+
72
+ let posStyles = `position: fixed; z-index: 9999;`;
73
+
74
+ if (position.includes('bottom')) {
75
+ posStyles += `top: ${bottom}px;`;
76
+ } else if (position.includes('top')) {
77
+ posStyles += `top: ${top - triggerHeight}px; transform: translateY(-100%);`;
78
+ }
79
+
80
+ if (position.includes('left')) {
81
+ if (left + dropdownWidthPx > viewportWidth - 20) {
82
+ posStyles += `left: auto; right: ${viewportWidth - right}px;`;
83
+ } else {
84
+ posStyles += `left: ${left}px;`;
85
+ }
86
+ } else if (position.includes('right')) {
87
+ if (right - dropdownWidthPx < 20) {
88
+ posStyles += `left: ${left}px; right: auto;`;
89
+ } else {
90
+ posStyles += `left: ${right - triggerWidth}px;`;
91
+ }
92
+ } else {
93
+ const centeredLeft = left + triggerWidth / 2;
94
+ if (centeredLeft + dropdownWidthPx / 2 > viewportWidth - 20) {
95
+ posStyles += `right: 20px; left: auto;`;
96
+ } else if (centeredLeft - dropdownWidthPx / 2 < 20) {
97
+ posStyles += `left: 20px; right: auto;`;
98
+ } else {
99
+ posStyles += `left: ${centeredLeft}px; transform: translateX(-50%);`;
100
+ }
101
+ }
102
+
103
+ const maxHeight = viewportHeight - bottom - 20; // 20px padding from bottom
104
+ if (position.includes('bottom')) {
105
+ posStyles += `max-height: ${maxHeight}px; overflow-y: auto;`;
106
+ }
107
+
108
+ return posStyles;
109
+ });
110
+
111
+ function handleToggle() {
112
+ if (disabled) return;
113
+ isOpen = !isOpen;
114
+ }
115
+
116
+ function handleClickOutside(event: MouseEvent) {
117
+ if (
118
+ isOpen &&
119
+ dropdownRef &&
120
+ !dropdownRef.contains(event.target as Node) &&
121
+ triggerRef &&
122
+ !triggerRef.contains(event.target as Node)
123
+ ) {
124
+ isOpen = false;
125
+ }
126
+ }
127
+
128
+ function handleItemClick(item: DropdownItem) {
129
+ if (item.onclick) item.onclick();
130
+ isOpen = false;
131
+ }
132
+ </script>
133
+
134
+ <svelte:window onclick={handleClickOutside} />
135
+
136
+ <div class={baseClass}>
137
+ <div bind:this={triggerRef}>
138
+ <button
139
+ type="button"
140
+ id="menu-button"
141
+ aria-expanded={isOpen}
142
+ aria-haspopup="true"
143
+ class={triggerClass_}
144
+ onclick={handleToggle}
145
+ {disabled}
146
+ >
147
+ {#if label}
148
+ {label}
149
+ {/if}
150
+
151
+ {#if Icon}
152
+ <Icon class="text-default-400 size-5" />
153
+ {:else if label}
154
+ <svg
155
+ xmlns="http://www.w3.org/2000/svg"
156
+ width="28"
157
+ height="28"
158
+ viewBox="0 0 28 28"
159
+ class="size-5"
160
+ >
161
+ <path
162
+ fill="currentColor"
163
+ d="M4.22 9.47a.75.75 0 0 1 1.06 0L14 18.19l8.72-8.72a.75.75 0 1 1 1.06 1.06l-9.25 9.25a.75.75 0 0 1-1.06 0l-9.25-9.25a.75.75 0 0 1 0-1.06"
164
+ />
165
+ </svg>
166
+ {/if}
167
+ </button>
168
+ </div>
169
+ </div>
170
+
171
+ {#if isOpen}
172
+ <Portal target={triggerRef}>
173
+ <div
174
+ bind:this={dropdownRef}
175
+ class={containerClass_}
176
+ role="menu"
177
+ aria-orientation="vertical"
178
+ aria-labelledby="menu-button"
179
+ style={dropdownStyles}
180
+ transition:fly={{ duration: 150, y: 5, opacity: 0 }}
181
+ >
182
+ {#if header}
183
+ <button class={headerClass_} onclick={header.onclick} aria-label="Header Actions">
184
+ {#if header.content}
185
+ {@render header.content()}
186
+ {:else}
187
+ {#if header.title}
188
+ <span class={headerTitleClass}>{header.title}</span>
189
+ {/if}
190
+ {#if header.subtitle}
191
+ <span class={headerSubtitleClass}>{header.subtitle}</span>
192
+ {/if}
193
+ {/if}
194
+ </button>
195
+ {/if}
196
+
197
+ {#each sections as section_, sectionIndex (sectionIndex)}
198
+ <div class={sectionClass}>
199
+ {#each section_.items as menuItem, itemIndex (itemIndex)}
200
+ {@const itemProps = {
201
+ class: itemClass_,
202
+ role: 'menuitem',
203
+ tabindex: -1,
204
+ id: `menu-item-${sectionIndex}-${itemIndex}`,
205
+ 'data-active': menuItem.active
206
+ }}
207
+ {#if menuItem.href}
208
+ <a href={menuItem.href} {...itemProps}>
209
+ {@render DropItemContent(menuItem)}
210
+ </a>
211
+ {:else}
212
+ <button
213
+ type="button"
214
+ onclick={() => handleItemClick(menuItem)}
215
+ disabled={!menuItem.onclick}
216
+ {...itemProps}
217
+ >
218
+ {@render DropItemContent(menuItem)}
219
+ </button>
220
+ {/if}
221
+ {/each}
222
+ </div>
223
+ {/each}
224
+ </div>
225
+ </Portal>
226
+ {/if}
227
+
228
+ {#snippet DropItemContent(menuItem: DropdownItem)}
229
+ {#if menuItem.icon}
230
+ {@const ItemIcon = menuItem.icon}
231
+ <ItemIcon class={iconClass} />
232
+ {/if}
233
+ <span class="truncate">{menuItem.label}</span>
234
+ {/snippet}
@@ -0,0 +1,4 @@
1
+ import type { DropdownMenuProps } from '../../index.js';
2
+ declare const Dropdown: import("svelte").Component<DropdownMenuProps, {}, "open">;
3
+ type Dropdown = ReturnType<typeof Dropdown>;
4
+ export default Dropdown;
@@ -0,0 +1,333 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import { cn } from '../../helper/cls.js';
4
+ import { selectTV } from './select.js';
5
+ import type { SelectItem, SelectProps } from '../../index.js';
6
+ import Badge from '../badge/Badge.svelte';
7
+ import { Size } from '../../variants.js';
8
+ import Portal from '../../utils/Portal.svelte';
9
+
10
+ let {
11
+ items = [],
12
+ value = $bindable(''),
13
+ multiple = false,
14
+ placeholder = 'Select an option',
15
+ searchable = false,
16
+ disabled = false,
17
+ size = Size.BASE,
18
+ class: className = '',
19
+ containerClass = '',
20
+ listClass = '',
21
+ itemClass = '',
22
+ searchInputClass = '',
23
+ icon: Icon,
24
+ iconClass = '',
25
+ triggerClass = '', // recently, just now
26
+ onselect = () => {},
27
+ onopen = () => {},
28
+ onclose = () => {}
29
+ }: SelectProps = $props();
30
+
31
+ let open = $state(false);
32
+ let searchQuery = $state('');
33
+ let labelRef = $state<HTMLLabelElement | null>(null);
34
+ let searchInputRef = $state<HTMLInputElement | null>(null);
35
+ let highlightedIndex = $state(-1);
36
+
37
+ // Convert value to array for internal processing if multiple is true
38
+ const valueArray = $derived.by(() => {
39
+ if (multiple) {
40
+ return Array.isArray(value) ? value : value ? [value] : [];
41
+ }
42
+ return typeof value === 'string' ? [value] : [];
43
+ });
44
+
45
+ const { base, trigger, triggerIcon, container, searchInput, list, item, emptyMessage } = $derived(
46
+ selectTV({
47
+ size,
48
+ disabled,
49
+ multiple
50
+ })
51
+ );
52
+
53
+ const baseClass = $derived(cn(base(), className));
54
+ const triggerClass_ = $derived(cn(trigger(), triggerClass, baseClass));
55
+ const triggerIconClass = $derived(cn(triggerIcon(), iconClass));
56
+ const containerClass_ = $derived(cn(container(), containerClass));
57
+ const searchInputClass_ = $derived(cn(searchInput(), searchInputClass));
58
+ const listClass_ = $derived(cn(list(), listClass));
59
+ const itemClass_ = $derived(cn(item(), itemClass));
60
+ const emptyMessageClass = $derived(cn(emptyMessage()));
61
+
62
+ const selectedItem = $derived(items.find((item) => item.value === value));
63
+ const selectedItems = $derived(items.filter((item) => valueArray.includes(item.value)));
64
+
65
+ const filteredItems = $derived(
66
+ searchable && searchQuery
67
+ ? items.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
68
+ : items
69
+ );
70
+
71
+ function handleToggle() {
72
+ if (disabled) return;
73
+ open = !open;
74
+
75
+ if (open) {
76
+ highlightedIndex = !multiple ? filteredItems.findIndex((item) => item.value === value) : -1;
77
+
78
+ onopen();
79
+
80
+ if (searchable) {
81
+ tick().then(() => {
82
+ searchInputRef?.focus();
83
+ });
84
+ }
85
+ } else {
86
+ onclose();
87
+ searchQuery = '';
88
+ }
89
+ }
90
+
91
+ function handleSelect(item: SelectItem) {
92
+ if (item.disabled) return;
93
+
94
+ if (multiple) {
95
+ const isSelected = valueArray.includes(item.value);
96
+
97
+ if (isSelected) {
98
+ // Remove from selection
99
+ value = Array.isArray(value) ? value.filter((v) => v !== item.value) : [];
100
+ } else {
101
+ // Add to selection
102
+ value = Array.isArray(value) ? [...value, item.value] : [item.value];
103
+ }
104
+
105
+ // Keep dropdown open when multiple selection is enabled
106
+ if (searchable && searchInputRef) {
107
+ searchInputRef.focus();
108
+ }
109
+ } else {
110
+ // Single selection
111
+ value = item.value;
112
+ open = false;
113
+ }
114
+
115
+ onselect({ value });
116
+ }
117
+
118
+ function removeItem(itemValue: string) {
119
+ if (multiple && Array.isArray(value)) {
120
+ value = value.filter((v) => v !== itemValue);
121
+ onselect({ value });
122
+ }
123
+ }
124
+
125
+ function handleClickOutside(event: MouseEvent) {
126
+ // Check if the click is inside the portal content
127
+ const portalContent = document.querySelector('.ripple-portal .portal-content');
128
+
129
+ // If the click is inside either the label (trigger) or the portal content, don't close
130
+ if (
131
+ (labelRef && labelRef.contains(event.target as Node)) ||
132
+ (portalContent && portalContent.contains(event.target as Node)) ||
133
+ !open
134
+ ) {
135
+ return;
136
+ }
137
+
138
+ // Otherwise close the dropdown
139
+ open = false;
140
+ onclose();
141
+ }
142
+
143
+ function handleKeydown(event: KeyboardEvent) {
144
+ // check if the event is fired from the select
145
+ if (!labelRef || !labelRef.contains(event.target as Node)) return;
146
+
147
+ if (!open) {
148
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
149
+ event.preventDefault();
150
+ open = true;
151
+ onopen();
152
+ return;
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (event.key === 'Escape') {
158
+ event.preventDefault();
159
+ open = false;
160
+ onclose();
161
+ return;
162
+ }
163
+
164
+ const availableItems = filteredItems.filter((item) => !item.disabled);
165
+ if (!availableItems.length) return;
166
+
167
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
168
+ event.preventDefault();
169
+
170
+ if (event.key === 'ArrowDown') {
171
+ highlightedIndex = (highlightedIndex + 1) % availableItems.length;
172
+ } else {
173
+ highlightedIndex = highlightedIndex <= 0 ? availableItems.length - 1 : highlightedIndex - 1;
174
+ }
175
+
176
+ tick().then(() => {
177
+ const highlightedElement = document.querySelector(
178
+ `[data-index="${highlightedIndex}"]`
179
+ ) as HTMLElement;
180
+ if (highlightedElement) {
181
+ highlightedElement.scrollIntoView({ block: 'center', behavior: 'instant' });
182
+ }
183
+ });
184
+ }
185
+
186
+ if (event.key === 'Enter' || event.key === ' ') {
187
+ event.preventDefault();
188
+ if (highlightedIndex >= 0 && highlightedIndex < availableItems.length) {
189
+ handleSelect(availableItems[highlightedIndex]);
190
+ }
191
+ }
192
+ }
193
+ </script>
194
+
195
+ <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
196
+
197
+ <label
198
+ bind:this={labelRef}
199
+ class={triggerClass_}
200
+ aria-disabled={disabled}
201
+ aria-haspopup="listbox"
202
+ aria-labelledby="select-label"
203
+ data-state={open ? 'open' : 'closed'}
204
+ >
205
+ <button
206
+ type="button"
207
+ aria-label="Toggle dropdown"
208
+ {disabled}
209
+ aria-expanded={open}
210
+ onclick={handleToggle}
211
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
212
+ tabindex={disabled ? -1 : 0}
213
+ ></button>
214
+ <span class="flex min-h-[1.5rem] flex-1 flex-wrap items-center gap-1 overflow-hidden">
215
+ {#if multiple && selectedItems.length > 0}
216
+ {#each selectedItems as item (item.value)}
217
+ <Badge {size} color="info" onclose={() => removeItem(item.value)}>
218
+ {item.value}
219
+ </Badge>
220
+ {/each}
221
+ {:else if !multiple && selectedItem}
222
+ <span id="select-label" class="flex-1 truncate text-left">
223
+ {selectedItem.label}
224
+ </span>
225
+ {:else}
226
+ <span id="select-label" class="text-default-500 px-1">
227
+ {placeholder}
228
+ </span>
229
+ {/if}
230
+ </span>
231
+
232
+ <span class="ml-auto flex flex-shrink-0 items-center pl-2">
233
+ {#if Icon}
234
+ <Icon class={triggerIconClass} />
235
+ {:else}
236
+ <svg
237
+ xmlns="http://www.w3.org/2000/svg"
238
+ viewBox="0 0 20 20"
239
+ fill="currentColor"
240
+ class={cn(triggerIconClass, open && 'rotate-180 transform')}
241
+ >
242
+ <path
243
+ fill-rule="evenodd"
244
+ d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
245
+ clip-rule="evenodd"
246
+ />
247
+ </svg>
248
+ {/if}
249
+ </span>
250
+ </label>
251
+
252
+ {#if open}
253
+ <Portal target={labelRef}>
254
+ <div class={containerClass_} role="listbox" aria-labelledby="select-label">
255
+ {#if searchable}
256
+ <div class={searchInputClass_}>
257
+ <svg
258
+ xmlns="http://www.w3.org/2000/svg"
259
+ width="12"
260
+ height="12"
261
+ viewBox="0 0 12 12"
262
+ class="size-4"
263
+ >
264
+ <path
265
+ fill="currentColor"
266
+ d="M5 1a4 4 0 1 0 2.452 7.16l2.694 2.693a.5.5 0 1 0 .707-.707L8.16 7.453A4 4 0 0 0 5 1M2 5a3 3 0 1 1 6 0a3 3 0 0 1-6 0"
267
+ />
268
+ </svg>
269
+ <input
270
+ bind:this={searchInputRef}
271
+ bind:value={searchQuery}
272
+ type="text"
273
+ class="ring-0 outline-0"
274
+ placeholder="Search..."
275
+ aria-label="Search select options"
276
+ />
277
+ </div>
278
+ {/if}
279
+
280
+ {#if filteredItems.length === 0}
281
+ <div class={emptyMessageClass}>No items found</div>
282
+ {:else}
283
+ <ul class={listClass_}>
284
+ {#each filteredItems as item, index (item.value)}
285
+ <li>
286
+ <button
287
+ type="button"
288
+ onclick={(event) => {
289
+ handleSelect(item);
290
+ event.preventDefault();
291
+ }}
292
+ disabled={item.disabled}
293
+ class={itemClass_}
294
+ role="option"
295
+ aria-selected={valueArray.includes(item.value)}
296
+ data-selected={valueArray.includes(item.value)}
297
+ data-highlighted={index === highlightedIndex}
298
+ data-index={index}
299
+ >
300
+ <span class="flex w-full items-center justify-between">
301
+ <span class="flex items-center gap-2 overflow-hidden">
302
+ {#if item.icon}
303
+ {@const Icon = item.icon}
304
+ <Icon class="h-4 w-4 flex-shrink-0" />
305
+ {/if}
306
+ <span class="truncate">{item.label}</span>
307
+ </span>
308
+
309
+ {#if valueArray.includes(item.value)}
310
+ <svg
311
+ xmlns="http://www.w3.org/2000/svg"
312
+ width="16"
313
+ height="16"
314
+ viewBox="0 0 24 24"
315
+ fill="none"
316
+ stroke="currentColor"
317
+ stroke-width="2"
318
+ stroke-linecap="round"
319
+ stroke-linejoin="round"
320
+ class="text-info-500"
321
+ >
322
+ <polyline points="20 6 9 17 4 12" />
323
+ </svg>
324
+ {/if}
325
+ </span>
326
+ </button>
327
+ </li>
328
+ {/each}
329
+ </ul>
330
+ {/if}
331
+ </div>
332
+ </Portal>
333
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { SelectProps } from '../../index.js';
2
+ declare const Select: import("svelte").Component<SelectProps, {}, "value">;
3
+ type Select = ReturnType<typeof Select>;
4
+ export default Select;