@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.
- package/README.md +394 -54
- package/dist/adapters/storage/BaseAdapter.d.ts +20 -0
- package/dist/adapters/storage/BaseAdapter.js +171 -0
- package/dist/adapters/storage/S3Adapter.d.ts +21 -0
- package/dist/adapters/storage/S3Adapter.js +194 -0
- package/dist/adapters/storage/index.d.ts +3 -0
- package/dist/adapters/storage/index.js +3 -0
- package/dist/adapters/storage/types.d.ts +102 -0
- package/dist/adapters/storage/types.js +4 -0
- package/dist/button/Button.svelte +5 -3
- package/dist/button/Button.svelte.d.ts +1 -1
- package/dist/button/button.d.ts +40 -63
- package/dist/button/button.js +15 -14
- package/dist/charts/Chart.svelte +545 -0
- package/dist/charts/Chart.svelte.d.ts +4 -0
- package/dist/drawer/Drawer.svelte +13 -2
- package/dist/drawer/Drawer.svelte.d.ts +1 -1
- package/dist/drawer/drawer.d.ts +0 -17
- package/dist/drawer/drawer.js +3 -3
- package/dist/elements/accordion/Accordion.svelte +98 -0
- package/dist/elements/accordion/Accordion.svelte.d.ts +4 -0
- package/dist/elements/accordion/accordion.d.ts +227 -0
- package/dist/elements/accordion/accordion.js +138 -0
- package/dist/elements/alert/Alert.svelte +57 -0
- package/dist/elements/alert/Alert.svelte.d.ts +4 -0
- package/dist/elements/badge/Badge.svelte +13 -5
- package/dist/elements/badge/Badge.svelte.d.ts +1 -1
- package/dist/elements/badge/badge.d.ts +0 -12
- package/dist/elements/dropdown/Dropdown.svelte +100 -138
- package/dist/elements/dropdown/Dropdown.svelte.d.ts +1 -1
- package/dist/elements/dropdown/Select.svelte +169 -66
- package/dist/elements/dropdown/Select.svelte.d.ts +1 -1
- package/dist/elements/dropdown/dropdown.d.ts +34 -57
- package/dist/elements/dropdown/dropdown.js +11 -5
- package/dist/elements/dropdown/select.d.ts +34 -54
- package/dist/elements/dropdown/select.js +29 -21
- package/dist/elements/file-upload/FileUpload.svelte +135 -0
- package/dist/elements/file-upload/FileUpload.svelte.d.ts +4 -0
- package/dist/elements/file-upload/FilesPreview.svelte +93 -0
- package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
- package/dist/elements/progress/Progress.svelte +145 -0
- package/dist/elements/progress/Progress.svelte.d.ts +4 -0
- package/dist/elements/timeline/Timeline.svelte +92 -0
- package/dist/elements/timeline/Timeline.svelte.d.ts +7 -0
- package/dist/file-browser/FileBrowser.svelte +823 -0
- package/dist/file-browser/FileBrowser.svelte.d.ts +13 -0
- package/dist/file-browser/index.d.ts +1 -0
- package/dist/file-browser/index.js +1 -0
- package/dist/filters/CompactFilters.svelte +157 -0
- package/dist/filters/CompactFilters.svelte.d.ts +5 -0
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/forms/Checkbox.svelte +54 -0
- package/dist/forms/Checkbox.svelte.d.ts +4 -0
- package/dist/forms/DateRange.svelte +493 -0
- package/dist/forms/DateRange.svelte.d.ts +4 -0
- package/dist/forms/Form.svelte +39 -0
- package/dist/forms/Form.svelte.d.ts +4 -0
- package/dist/forms/Input.svelte +86 -0
- package/dist/forms/Input.svelte.d.ts +4 -0
- package/dist/forms/NumberInput.svelte +159 -0
- package/dist/forms/NumberInput.svelte.d.ts +4 -0
- package/dist/forms/RadioInputs.svelte +64 -0
- package/dist/forms/RadioInputs.svelte.d.ts +4 -0
- package/dist/forms/RadioPill.svelte +66 -0
- package/dist/forms/RadioPill.svelte.d.ts +4 -0
- package/dist/forms/Slider.svelte +342 -0
- package/dist/forms/Slider.svelte.d.ts +4 -0
- package/dist/forms/Tags.svelte +181 -0
- package/dist/forms/Tags.svelte.d.ts +4 -0
- package/dist/forms/Toggle.svelte +132 -0
- package/dist/forms/Toggle.svelte.d.ts +4 -0
- package/dist/forms/slider.d.ts +143 -0
- package/dist/forms/slider.js +62 -0
- package/dist/header/Breadcrumbs.svelte +2 -1
- package/dist/header/Breadcrumbs.svelte.d.ts +1 -1
- package/dist/header/PageHeader.svelte +2 -2
- package/dist/header/PageHeader.svelte.d.ts +1 -1
- package/dist/header/breadcrumbs.d.ts +20 -14
- package/dist/header/breadcrumbs.js +6 -0
- package/dist/helper/date.d.ts +7 -0
- package/dist/helper/date.js +15 -0
- package/dist/index.d.ts +846 -9
- package/dist/index.js +76 -16
- package/dist/layout/card/Card.svelte +5 -8
- package/dist/layout/card/Card.svelte.d.ts +1 -1
- package/dist/layout/card/MetricCard.svelte +59 -0
- package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
- package/dist/layout/card/StatsCard.svelte +119 -89
- package/dist/layout/card/StatsCard.svelte.d.ts +1 -1
- package/dist/layout/card/card.d.ts +22 -33
- package/dist/layout/card/card.js +9 -8
- package/dist/layout/card/metric-card.d.ts +49 -0
- package/dist/layout/card/metric-card.js +10 -0
- package/dist/layout/card/stats-card.d.ts +22 -39
- package/dist/layout/card/stats-card.js +14 -14
- package/dist/layout/navbar/navbar.d.ts +0 -23
- package/dist/layout/sidebar/NavGroup.svelte +25 -50
- package/dist/layout/sidebar/NavGroup.svelte.d.ts +1 -1
- package/dist/layout/sidebar/NavItem.svelte +3 -3
- package/dist/layout/sidebar/NavItem.svelte.d.ts +1 -1
- package/dist/layout/sidebar/Sidebar.svelte +101 -72
- package/dist/layout/sidebar/Sidebar.svelte.d.ts +1 -1
- package/dist/layout/table/Table.svelte +464 -87
- package/dist/layout/table/Table.svelte.d.ts +1 -1
- package/dist/layout/table/table.d.ts +0 -47
- package/dist/layout/table/table.js +0 -8
- package/dist/layout/tabs/Tab.svelte +9 -6
- package/dist/layout/tabs/Tab.svelte.d.ts +1 -1
- package/dist/layout/tabs/TabContent.svelte +1 -2
- package/dist/layout/tabs/TabContent.svelte.d.ts +1 -1
- package/dist/layout/tabs/TabGroup.svelte +10 -5
- package/dist/layout/tabs/TabGroup.svelte.d.ts +2 -2
- package/dist/layout/tabs/tabs.d.ts +61 -76
- package/dist/layout/tabs/tabs.js +170 -28
- package/dist/modal/Modal.svelte +2 -1
- package/dist/modal/Modal.svelte.d.ts +1 -1
- package/dist/modal/modal.d.ts +0 -23
- package/dist/modal/modal.js +3 -3
- package/dist/sonner/sonner.svelte +13 -0
- package/dist/sonner/sonner.svelte.d.ts +4 -0
- package/dist/types/variants.d.ts +1 -21
- package/dist/types/variants.js +1 -19
- package/dist/utils/Portal.svelte +108 -0
- package/dist/utils/Portal.svelte.d.ts +8 -0
- package/dist/utils/dateUtils.d.ts +7 -0
- package/dist/utils/dateUtils.js +26 -0
- package/dist/variants.d.ts +30 -0
- package/dist/variants.js +36 -0
- package/package.json +7 -3
- package/dist/button/index.d.ts +0 -1
- package/dist/button/index.js +0 -1
- package/dist/drawer/index.d.ts +0 -2
- package/dist/drawer/index.js +0 -1
- package/dist/elements/badge/index.d.ts +0 -2
- package/dist/elements/badge/index.js +0 -2
- package/dist/elements/dropdown/index.d.ts +0 -3
- package/dist/elements/dropdown/index.js +0 -2
- package/dist/header/index.d.ts +0 -4
- package/dist/header/index.js +0 -2
- package/dist/header/pageheaders.d.ts +0 -10
- package/dist/header/pageheaders.js +0 -1
- package/dist/layout/card/index.d.ts +0 -4
- package/dist/layout/card/index.js +0 -2
- package/dist/layout/index.d.ts +0 -5
- package/dist/layout/index.js +0 -5
- package/dist/layout/navbar/index.d.ts +0 -2
- package/dist/layout/navbar/index.js +0 -2
- package/dist/layout/sidebar/index.d.ts +0 -2
- package/dist/layout/sidebar/index.js +0 -1
- package/dist/layout/sidebar/sidebar.d.ts +0 -46
- package/dist/layout/sidebar/sidebar.js +0 -1
- package/dist/layout/table/index.d.ts +0 -3
- package/dist/layout/table/index.js +0 -2
- package/dist/layout/tabs/index.d.ts +0 -3
- package/dist/layout/tabs/index.js +0 -3
- package/dist/modal/index.d.ts +0 -1
- 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 {
|
|
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 '../../
|
|
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 = '
|
|
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(),
|
|
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 +
|
|
91
|
-
if (centeredLeft +
|
|
93
|
+
const centeredLeft = left + triggerWidth / 2;
|
|
94
|
+
if (centeredLeft + dropdownWidthPx / 2 > viewportWidth - 20) {
|
|
92
95
|
posStyles += `right: 20px; left: auto;`;
|
|
93
|
-
} else if (centeredLeft -
|
|
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 (
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
{
|
|
227
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
{
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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 (!
|
|
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
|
-
<
|
|
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
|
-
|
|
207
|
+
aria-label="Toggle dropdown"
|
|
149
208
|
{disabled}
|
|
150
|
-
class={triggerClass_}
|
|
151
|
-
aria-haspopup="listbox"
|
|
152
209
|
aria-expanded={open}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
259
|
+
width="12"
|
|
260
|
+
height="12"
|
|
261
|
+
viewBox="0 0 12 12"
|
|
262
|
+
class="size-4"
|
|
171
263
|
>
|
|
172
264
|
<path
|
|
173
|
-
fill
|
|
174
|
-
d="M5
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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={() =>
|
|
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={
|
|
210
|
-
data-selected={
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
230
|
-
|
|
332
|
+
</Portal>
|
|
333
|
+
{/if}
|