@makolabs/ripple 0.0.1-dev.7 → 0.0.1-dev.71

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 (158) hide show
  1. package/README.md +394 -54
  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 +5 -3
  11. package/dist/button/Button.svelte.d.ts +1 -1
  12. package/dist/button/button.d.ts +40 -63
  13. package/dist/button/button.js +15 -14
  14. package/dist/charts/Chart.svelte +545 -0
  15. package/dist/charts/Chart.svelte.d.ts +4 -0
  16. package/dist/drawer/Drawer.svelte +13 -2
  17. package/dist/drawer/Drawer.svelte.d.ts +1 -1
  18. package/dist/drawer/drawer.d.ts +0 -17
  19. package/dist/drawer/drawer.js +3 -3
  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 +13 -5
  27. package/dist/elements/badge/Badge.svelte.d.ts +1 -1
  28. package/dist/elements/badge/badge.d.ts +0 -12
  29. package/dist/elements/dropdown/Dropdown.svelte +100 -138
  30. package/dist/elements/dropdown/Dropdown.svelte.d.ts +1 -1
  31. package/dist/elements/dropdown/Select.svelte +169 -66
  32. package/dist/elements/dropdown/Select.svelte.d.ts +1 -1
  33. package/dist/elements/dropdown/dropdown.d.ts +34 -57
  34. package/dist/elements/dropdown/dropdown.js +11 -5
  35. package/dist/elements/dropdown/select.d.ts +34 -54
  36. package/dist/elements/dropdown/select.js +29 -21
  37. package/dist/elements/file-upload/FileUpload.svelte +135 -0
  38. package/dist/elements/file-upload/FileUpload.svelte.d.ts +4 -0
  39. package/dist/elements/file-upload/FilesPreview.svelte +93 -0
  40. package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
  41. package/dist/elements/progress/Progress.svelte +145 -0
  42. package/dist/elements/progress/Progress.svelte.d.ts +4 -0
  43. package/dist/elements/timeline/Timeline.svelte +92 -0
  44. package/dist/elements/timeline/Timeline.svelte.d.ts +7 -0
  45. package/dist/file-browser/FileBrowser.svelte +823 -0
  46. package/dist/file-browser/FileBrowser.svelte.d.ts +13 -0
  47. package/dist/file-browser/index.d.ts +1 -0
  48. package/dist/file-browser/index.js +1 -0
  49. package/dist/filters/CompactFilters.svelte +157 -0
  50. package/dist/filters/CompactFilters.svelte.d.ts +5 -0
  51. package/dist/filters/index.d.ts +1 -0
  52. package/dist/filters/index.js +1 -0
  53. package/dist/forms/Checkbox.svelte +54 -0
  54. package/dist/forms/Checkbox.svelte.d.ts +4 -0
  55. package/dist/forms/DateRange.svelte +493 -0
  56. package/dist/forms/DateRange.svelte.d.ts +4 -0
  57. package/dist/forms/Form.svelte +39 -0
  58. package/dist/forms/Form.svelte.d.ts +4 -0
  59. package/dist/forms/Input.svelte +86 -0
  60. package/dist/forms/Input.svelte.d.ts +4 -0
  61. package/dist/forms/NumberInput.svelte +159 -0
  62. package/dist/forms/NumberInput.svelte.d.ts +4 -0
  63. package/dist/forms/RadioInputs.svelte +64 -0
  64. package/dist/forms/RadioInputs.svelte.d.ts +4 -0
  65. package/dist/forms/RadioPill.svelte +66 -0
  66. package/dist/forms/RadioPill.svelte.d.ts +4 -0
  67. package/dist/forms/Slider.svelte +342 -0
  68. package/dist/forms/Slider.svelte.d.ts +4 -0
  69. package/dist/forms/Tags.svelte +181 -0
  70. package/dist/forms/Tags.svelte.d.ts +4 -0
  71. package/dist/forms/Toggle.svelte +132 -0
  72. package/dist/forms/Toggle.svelte.d.ts +4 -0
  73. package/dist/forms/slider.d.ts +143 -0
  74. package/dist/forms/slider.js +62 -0
  75. package/dist/header/Breadcrumbs.svelte +2 -1
  76. package/dist/header/Breadcrumbs.svelte.d.ts +1 -1
  77. package/dist/header/PageHeader.svelte +2 -2
  78. package/dist/header/PageHeader.svelte.d.ts +1 -1
  79. package/dist/header/breadcrumbs.d.ts +20 -14
  80. package/dist/header/breadcrumbs.js +6 -0
  81. package/dist/helper/date.d.ts +7 -0
  82. package/dist/helper/date.js +15 -0
  83. package/dist/index.d.ts +846 -9
  84. package/dist/index.js +76 -16
  85. package/dist/layout/card/Card.svelte +5 -8
  86. package/dist/layout/card/Card.svelte.d.ts +1 -1
  87. package/dist/layout/card/MetricCard.svelte +59 -0
  88. package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
  89. package/dist/layout/card/StatsCard.svelte +119 -89
  90. package/dist/layout/card/StatsCard.svelte.d.ts +1 -1
  91. package/dist/layout/card/card.d.ts +22 -33
  92. package/dist/layout/card/card.js +9 -8
  93. package/dist/layout/card/metric-card.d.ts +49 -0
  94. package/dist/layout/card/metric-card.js +10 -0
  95. package/dist/layout/card/stats-card.d.ts +22 -39
  96. package/dist/layout/card/stats-card.js +14 -14
  97. package/dist/layout/navbar/navbar.d.ts +0 -23
  98. package/dist/layout/sidebar/NavGroup.svelte +25 -50
  99. package/dist/layout/sidebar/NavGroup.svelte.d.ts +1 -1
  100. package/dist/layout/sidebar/NavItem.svelte +3 -3
  101. package/dist/layout/sidebar/NavItem.svelte.d.ts +1 -1
  102. package/dist/layout/sidebar/Sidebar.svelte +101 -72
  103. package/dist/layout/sidebar/Sidebar.svelte.d.ts +1 -1
  104. package/dist/layout/table/Table.svelte +464 -87
  105. package/dist/layout/table/Table.svelte.d.ts +1 -1
  106. package/dist/layout/table/table.d.ts +0 -47
  107. package/dist/layout/table/table.js +0 -8
  108. package/dist/layout/tabs/Tab.svelte +9 -6
  109. package/dist/layout/tabs/Tab.svelte.d.ts +1 -1
  110. package/dist/layout/tabs/TabContent.svelte +1 -2
  111. package/dist/layout/tabs/TabContent.svelte.d.ts +1 -1
  112. package/dist/layout/tabs/TabGroup.svelte +10 -5
  113. package/dist/layout/tabs/TabGroup.svelte.d.ts +2 -2
  114. package/dist/layout/tabs/tabs.d.ts +61 -76
  115. package/dist/layout/tabs/tabs.js +170 -28
  116. package/dist/modal/Modal.svelte +2 -1
  117. package/dist/modal/Modal.svelte.d.ts +1 -1
  118. package/dist/modal/modal.d.ts +0 -23
  119. package/dist/modal/modal.js +3 -3
  120. package/dist/sonner/sonner.svelte +13 -0
  121. package/dist/sonner/sonner.svelte.d.ts +4 -0
  122. package/dist/types/variants.d.ts +1 -21
  123. package/dist/types/variants.js +1 -19
  124. package/dist/utils/Portal.svelte +108 -0
  125. package/dist/utils/Portal.svelte.d.ts +8 -0
  126. package/dist/utils/dateUtils.d.ts +7 -0
  127. package/dist/utils/dateUtils.js +26 -0
  128. package/dist/variants.d.ts +30 -0
  129. package/dist/variants.js +36 -0
  130. package/package.json +7 -3
  131. package/dist/button/index.d.ts +0 -1
  132. package/dist/button/index.js +0 -1
  133. package/dist/drawer/index.d.ts +0 -2
  134. package/dist/drawer/index.js +0 -1
  135. package/dist/elements/badge/index.d.ts +0 -2
  136. package/dist/elements/badge/index.js +0 -2
  137. package/dist/elements/dropdown/index.d.ts +0 -3
  138. package/dist/elements/dropdown/index.js +0 -2
  139. package/dist/header/index.d.ts +0 -4
  140. package/dist/header/index.js +0 -2
  141. package/dist/header/pageheaders.d.ts +0 -10
  142. package/dist/header/pageheaders.js +0 -1
  143. package/dist/layout/card/index.d.ts +0 -4
  144. package/dist/layout/card/index.js +0 -2
  145. package/dist/layout/index.d.ts +0 -5
  146. package/dist/layout/index.js +0 -5
  147. package/dist/layout/navbar/index.d.ts +0 -2
  148. package/dist/layout/navbar/index.js +0 -2
  149. package/dist/layout/sidebar/index.d.ts +0 -2
  150. package/dist/layout/sidebar/index.js +0 -1
  151. package/dist/layout/sidebar/sidebar.d.ts +0 -46
  152. package/dist/layout/sidebar/sidebar.js +0 -1
  153. package/dist/layout/table/index.d.ts +0 -3
  154. package/dist/layout/table/index.js +0 -2
  155. package/dist/layout/tabs/index.d.ts +0 -3
  156. package/dist/layout/tabs/index.js +0 -3
  157. package/dist/modal/index.d.ts +0 -1
  158. package/dist/modal/index.js +0 -1
@@ -1,17 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
3
  import { fly } from 'svelte/transition';
4
- import { dropdownMenu, type DropdownMenuProps, type DropdownItem } from './dropdown.js';
4
+ import type { DropdownMenuProps, DropdownItem } from '../../index.js';
5
+ import { dropdownMenu } from '../../index.js';
5
6
  import { onMount, onDestroy } from 'svelte';
6
- import { Size } from '../../types/variants.js';
7
+ import { Size } from '../../variants.js';
8
+ import Portal from '../../utils/Portal.svelte';
7
9
 
8
10
  let {
9
11
  sections = [],
10
12
  open: isOpen = $bindable(false),
11
- label = 'Options',
13
+ label = '',
12
14
  icon: Icon,
13
- triggerContent,
14
- triggerClass = '',
15
15
  containerClass = '',
16
16
  itemClass = '',
17
17
  class: className = '',
@@ -22,9 +22,11 @@
22
22
  header
23
23
  }: DropdownMenuProps = $props();
24
24
 
25
+ // Determine if we're in icon-only mode
26
+ const iconOnly = $derived(!label && !!Icon);
27
+
25
28
  let dropdownRef = $state<HTMLDivElement | undefined>();
26
29
  let triggerRef = $state<HTMLDivElement | undefined>();
27
- let portalEl = $state<HTMLDivElement | undefined>();
28
30
  let triggerRect = $state<DOMRect | null>(null);
29
31
 
30
32
  const {
@@ -41,12 +43,13 @@
41
43
  dropdownMenu({
42
44
  position,
43
45
  size,
44
- isOpen
46
+ isOpen,
47
+ iconOnly
45
48
  })
46
49
  );
47
50
 
48
51
  const baseClass = $derived(cn(base(), className));
49
- const triggerClass_ = $derived(cn(trigger(), triggerClass));
52
+ const triggerClass_ = $derived(cn(trigger(), ''));
50
53
  const containerClass_ = $derived(cn(container(), width, containerClass, 'shadow-lg'));
51
54
  const sectionClass = $derived(cn(section()));
52
55
  const itemClass_ = $derived(cn(item(), itemClass));
@@ -87,10 +90,10 @@
87
90
  posStyles += `left: ${right - triggerWidth}px;`;
88
91
  }
89
92
  } else {
90
- const centeredLeft = left + (triggerWidth / 2);
91
- if (centeredLeft + (dropdownWidthPx / 2) > viewportWidth - 20) {
93
+ const centeredLeft = left + triggerWidth / 2;
94
+ if (centeredLeft + dropdownWidthPx / 2 > viewportWidth - 20) {
92
95
  posStyles += `right: 20px; left: auto;`;
93
- } else if (centeredLeft - (dropdownWidthPx / 2) < 20) {
96
+ } else if (centeredLeft - dropdownWidthPx / 2 < 20) {
94
97
  posStyles += `left: 20px; right: auto;`;
95
98
  } else {
96
99
  posStyles += `left: ${centeredLeft}px; transform: translateX(-50%);`;
@@ -108,159 +111,118 @@
108
111
  function handleToggle() {
109
112
  if (disabled) return;
110
113
  isOpen = !isOpen;
111
-
112
- if (isOpen) {
113
- setTimeout(updatePosition, 0); // Use setTimeout to ensure DOM is updated
114
- }
115
114
  }
116
115
 
117
116
  function handleClickOutside(event: MouseEvent) {
118
- if (isOpen &&
117
+ if (
118
+ isOpen &&
119
119
  dropdownRef &&
120
120
  !dropdownRef.contains(event.target as Node) &&
121
121
  triggerRef &&
122
- !triggerRef.contains(event.target as Node)) {
122
+ !triggerRef.contains(event.target as Node)
123
+ ) {
123
124
  isOpen = false;
124
125
  }
125
126
  }
126
127
 
127
128
  function handleItemClick(item: DropdownItem) {
128
- if (item.disabled) return;
129
129
  if (item.onclick) item.onclick();
130
130
  isOpen = false;
131
131
  }
132
-
133
- function updatePosition() {
134
- if (triggerRef) {
135
- triggerRect = triggerRef.getBoundingClientRect();
136
- }
137
- }
138
-
139
- function renderPortalDropdown() {
140
- if (!portalEl || !document.body.contains(portalEl)) {
141
- portalEl = document.createElement('div');
142
- portalEl.id = 'dropdown-portal';
143
- portalEl.style.position = 'fixed';
144
- portalEl.style.top = '0';
145
- portalEl.style.left = '0';
146
- portalEl.style.width = '100%';
147
- portalEl.style.height = '100%';
148
- portalEl.style.pointerEvents = 'none';
149
- portalEl.style.zIndex = '9999';
150
- document.body.appendChild(portalEl);
151
- }
152
- }
153
-
154
- onMount(() => {
155
- renderPortalDropdown();
156
- window.addEventListener('scroll', updatePosition, true);
157
- window.addEventListener('resize', updatePosition);
158
- });
159
-
160
- onDestroy(() => {
161
- if (!triggerRef) return;
162
- window.removeEventListener('scroll', updatePosition, true);
163
- window.removeEventListener('resize', updatePosition);
164
- if (portalEl && document.body.contains(portalEl)) {
165
- document.body.removeChild(portalEl);
166
- }
167
- });
168
-
169
- $effect(() => {
170
- if (isOpen) {
171
- renderPortalDropdown();
172
- updatePosition();
173
- }
174
- });
175
132
  </script>
176
133
 
177
134
  <svelte:window onclick={handleClickOutside} />
178
135
 
179
136
  <div class={baseClass}>
180
137
  <div bind:this={triggerRef}>
181
- {#if triggerContent}
182
- {@render triggerContent()}
183
- {:else}
184
- <button
185
- type="button"
186
- id="menu-button"
187
- aria-expanded={isOpen}
188
- aria-haspopup="true"
189
- class={triggerClass_}
190
- onclick={handleToggle}
191
- {disabled}
192
- >
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}
193
148
  {label}
194
- {#if Icon}
195
- <Icon class="-mr-1 size-5 text-default-400" />
196
- {:else}
197
- <svg
198
- class="-mr-1 size-5 text-default-400"
199
- viewBox="0 0 16 16"
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
200
162
  fill="currentColor"
201
- aria-hidden="true"
202
- >
203
- <path
204
- d="M6.62 12.7c.31.27.77.27 1.08 0l5-4.5A.76.76 0 0 0 12.16 7c0-.2-.08-.38-.21-.52a.76.76 0 0 0-1.09.02L7 10l-3.85-3.5a.76.76 0 0 0-1.07-.02.74.74 0 0 0-.02 1.07l4.56 5.15Z"
205
- />
206
- </svg>
207
- {/if}
208
- </button>
209
- {/if}
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>
210
168
  </div>
211
169
  </div>
212
170
 
213
- {#if isOpen && portalEl}
214
- <div
215
- bind:this={dropdownRef}
216
- class={containerClass_}
217
- role="menu"
218
- aria-orientation="vertical"
219
- aria-labelledby="menu-button"
220
- style={dropdownStyles}
221
- transition:fly={{ duration: 150, y: 5, opacity: 0 }}
222
- >
223
- {#if header}
224
- <button class={headerClass_} onclick={header.onclick} aria-label="Header Actions">
225
- {#if header.content}
226
- {@render header.content()}
227
- {:else}
228
- {#if header.title}
229
- <span class={headerTitleClass}>{header.title}</span>
230
- {/if}
231
- {#if header.subtitle}
232
- <span class={headerSubtitleClass}>{header.subtitle}</span>
233
- {/if}
234
- {/if}
235
- </button>
236
- {/if}
237
-
238
- {#each sections as section_, sectionIndex (sectionIndex)}
239
- <div class={sectionClass}>
240
- {#each section_.items as menuItem, itemIndex (itemIndex)}
241
- {@const itemProps = {
242
- class: itemClass_,
243
- role: 'menuitem',
244
- tabindex: -1,
245
- id: `menu-item-${sectionIndex}-${itemIndex}`,
246
- 'data-active': menuItem.active
247
- }}
248
- {#if menuItem.href}
249
- <a href={menuItem.href} {...itemProps}>
250
- {@render DropItemContent(menuItem)}
251
- </a>
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()}
252
186
  {:else}
253
- <button
254
- type="button"
255
- onclick={() => handleItemClick(menuItem)}
256
- disabled={menuItem.disabled} {...itemProps}>
257
- {@render DropItemContent(menuItem)}
258
- </button>
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}
259
193
  {/if}
260
- {/each}
261
- </div>
262
- {/each}
263
- </div>
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>
264
226
  {/if}
265
227
 
266
228
  {#snippet DropItemContent(menuItem: DropdownItem)}
@@ -269,4 +231,4 @@
269
231
  <ItemIcon class={iconClass} />
270
232
  {/if}
271
233
  <span class="truncate">{menuItem.label}</span>
272
- {/snippet}
234
+ {/snippet}
@@ -1,4 +1,4 @@
1
- import { type DropdownMenuProps } from './dropdown.js';
1
+ import type { DropdownMenuProps } from '../../index.js';
2
2
  declare const Dropdown: import("svelte").Component<DropdownMenuProps, {}, "open">;
3
3
  type Dropdown = ReturnType<typeof Dropdown>;
4
4
  export default Dropdown;
@@ -1,24 +1,28 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
3
  import { cn } from '../../helper/cls.js';
4
- import { selectTV, type SelectItem, type SelectProps } from './select.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';
5
9
 
6
10
  let {
7
11
  items = [],
8
12
  value = $bindable(''),
13
+ multiple = false,
9
14
  placeholder = 'Select an option',
10
15
  searchable = false,
11
16
  disabled = false,
12
- size = 'sm',
17
+ size = Size.BASE,
13
18
  class: className = '',
14
- triggerClass = '',
15
19
  containerClass = '',
16
20
  listClass = '',
17
21
  itemClass = '',
18
22
  searchInputClass = '',
19
23
  icon: Icon,
20
24
  iconClass = '',
21
- triggerContent,
25
+ triggerClass = '', // recently, just now
22
26
  onselect = () => {},
23
27
  onopen = () => {},
24
28
  onclose = () => {}
@@ -26,19 +30,28 @@
26
30
 
27
31
  let open = $state(false);
28
32
  let searchQuery = $state('');
29
- let selectRef = $state<HTMLDivElement | null>(null);
33
+ let labelRef = $state<HTMLLabelElement | null>(null);
30
34
  let searchInputRef = $state<HTMLInputElement | null>(null);
31
35
  let highlightedIndex = $state(-1);
32
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
+
33
45
  const { base, trigger, triggerIcon, container, searchInput, list, item, emptyMessage } = $derived(
34
46
  selectTV({
35
47
  size,
36
- disabled
48
+ disabled,
49
+ multiple
37
50
  })
38
51
  );
39
52
 
40
53
  const baseClass = $derived(cn(base(), className));
41
- const triggerClass_ = $derived(cn(trigger(), triggerClass));
54
+ const triggerClass_ = $derived(cn(trigger(), triggerClass, baseClass));
42
55
  const triggerIconClass = $derived(cn(triggerIcon(), iconClass));
43
56
  const containerClass_ = $derived(cn(container(), containerClass));
44
57
  const searchInputClass_ = $derived(cn(searchInput(), searchInputClass));
@@ -47,6 +60,7 @@
47
60
  const emptyMessageClass = $derived(cn(emptyMessage()));
48
61
 
49
62
  const selectedItem = $derived(items.find((item) => item.value === value));
63
+ const selectedItems = $derived(items.filter((item) => valueArray.includes(item.value)));
50
64
 
51
65
  const filteredItems = $derived(
52
66
  searchable && searchQuery
@@ -59,7 +73,7 @@
59
73
  open = !open;
60
74
 
61
75
  if (open) {
62
- highlightedIndex = filteredItems.findIndex((item) => item.value === value);
76
+ highlightedIndex = !multiple ? filteredItems.findIndex((item) => item.value === value) : -1;
63
77
 
64
78
  onopen();
65
79
 
@@ -76,21 +90,59 @@
76
90
 
77
91
  function handleSelect(item: SelectItem) {
78
92
  if (item.disabled) return;
79
- value = item.value;
80
- onselect({ value: item.value });
81
- open = false;
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
+ }
82
123
  }
83
124
 
84
125
  function handleClickOutside(event: MouseEvent) {
85
- if (selectRef && !selectRef.contains(event.target as Node) && open) {
86
- open = false;
87
- onclose();
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;
88
136
  }
137
+
138
+ // Otherwise close the dropdown
139
+ open = false;
140
+ onclose();
89
141
  }
90
142
 
91
143
  function handleKeydown(event: KeyboardEvent) {
92
144
  // check if the event is fired from the select
93
- if (!selectRef || !selectRef.contains(event.target as Node)) return;
145
+ if (!labelRef || !labelRef.contains(event.target as Node)) return;
94
146
 
95
147
  if (!open) {
96
148
  if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
@@ -142,56 +194,87 @@
142
194
 
143
195
  <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
144
196
 
145
- <div bind:this={selectRef} class={baseClass} data-state={open ? 'open' : 'closed'}>
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
+ >
146
205
  <button
147
206
  type="button"
148
- onclick={handleToggle}
207
+ aria-label="Toggle dropdown"
149
208
  {disabled}
150
- class={triggerClass_}
151
- aria-haspopup="listbox"
152
209
  aria-expanded={open}
153
- aria-labelledby="select-label"
154
- >
155
- {#if triggerContent}
156
- {@render triggerContent({ selectedItem, open })}
157
- {:else}
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}
158
222
  <span id="select-label" class="flex-1 truncate text-left">
159
- {selectedItem?.label || placeholder}
223
+ {selectedItem.label}
224
+ </span>
225
+ {:else}
226
+ <span id="select-label" class="text-default-500 px-1">
227
+ {placeholder}
160
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>
161
251
 
162
- <span class="flex items-center gap-2">
163
- {#if Icon}
164
- <Icon class={triggerIconClass} />
165
- {:else}
252
+ {#if open}
253
+ <Portal target={labelRef}>
254
+ <div class={containerClass_} role="listbox" aria-labelledby="select-label">
255
+ {#if searchable}
256
+ <div class={searchInputClass_}>
166
257
  <svg
167
258
  xmlns="http://www.w3.org/2000/svg"
168
- viewBox="0 0 20 20"
169
- fill="currentColor"
170
- class={cn(triggerIconClass, open && 'rotate-180 transform')}
259
+ width="12"
260
+ height="12"
261
+ viewBox="0 0 12 12"
262
+ class="size-4"
171
263
  >
172
264
  <path
173
- fill-rule="evenodd"
174
- 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"
175
- clip-rule="evenodd"
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"
176
267
  />
177
268
  </svg>
178
- {/if}
179
- </span>
180
- {/if}
181
- </button>
182
-
183
- <!-- Select Menu -->
184
- {#if open}
185
- <div class={containerClass_} role="listbox" aria-labelledby="select-label">
186
- {#if searchable}
187
- <input
188
- bind:this={searchInputRef}
189
- bind:value={searchQuery}
190
- type="text"
191
- placeholder="Search..."
192
- class={searchInputClass_}
193
- aria-label="Search select options"
194
- />
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>
195
278
  {/if}
196
279
 
197
280
  {#if filteredItems.length === 0}
@@ -202,29 +285,49 @@
202
285
  <li>
203
286
  <button
204
287
  type="button"
205
- onclick={() => handleSelect(item)}
288
+ onclick={(event) => {
289
+ handleSelect(item);
290
+ event.preventDefault();
291
+ }}
206
292
  disabled={item.disabled}
207
293
  class={itemClass_}
208
294
  role="option"
209
- aria-selected={value === item.value}
210
- data-selected={value === item.value}
295
+ aria-selected={valueArray.includes(item.value)}
296
+ data-selected={valueArray.includes(item.value)}
211
297
  data-highlighted={index === highlightedIndex}
212
298
  data-index={index}
213
299
  >
214
- {#if item.icon}
215
- {@const Icon = item.icon}
216
- <div class="flex items-center gap-2">
217
- <Icon class="h-4 w-4 flex-shrink-0" />
218
- <span>{item.label}</span>
219
- </div>
220
- {:else}
221
- {item.label}
222
- {/if}
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>
223
326
  </button>
224
327
  </li>
225
328
  {/each}
226
329
  </ul>
227
330
  {/if}
228
331
  </div>
229
- {/if}
230
- </div>
332
+ </Portal>
333
+ {/if}
@@ -1,4 +1,4 @@
1
- import { type SelectProps } from './select.js';
1
+ import type { SelectProps } from '../../index.js';
2
2
  declare const Select: import("svelte").Component<SelectProps, {}, "value">;
3
3
  type Select = ReturnType<typeof Select>;
4
4
  export default Select;