@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.
- package/README.md +575 -8
- 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 +48 -0
- package/dist/button/Button.svelte.d.ts +4 -0
- package/dist/button/button.d.ts +113 -0
- package/dist/button/button.js +168 -0
- package/dist/charts/Chart.svelte +545 -0
- package/dist/charts/Chart.svelte.d.ts +4 -0
- package/dist/drawer/Drawer.svelte +224 -0
- package/dist/drawer/Drawer.svelte.d.ts +4 -0
- package/dist/drawer/drawer.d.ts +160 -0
- package/dist/drawer/drawer.js +80 -0
- 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 +43 -0
- package/dist/elements/badge/Badge.svelte.d.ts +4 -0
- package/dist/elements/badge/badge.d.ts +181 -0
- package/dist/elements/badge/badge.js +65 -0
- package/dist/elements/dropdown/Dropdown.svelte +234 -0
- package/dist/elements/dropdown/Dropdown.svelte.d.ts +4 -0
- package/dist/elements/dropdown/Select.svelte +333 -0
- package/dist/elements/dropdown/Select.svelte.d.ts +4 -0
- package/dist/elements/dropdown/dropdown.d.ts +251 -0
- package/dist/elements/dropdown/dropdown.js +95 -0
- package/dist/elements/dropdown/select.d.ts +200 -0
- package/dist/elements/dropdown/select.js +82 -0
- 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 +877 -0
- package/dist/file-browser/FileBrowser.svelte.d.ts +14 -0
- package/dist/file-browser/index.d.ts +1 -0
- package/dist/file-browser/index.js +1 -0
- package/dist/filters/CompactFilters.svelte +147 -0
- package/dist/filters/CompactFilters.svelte.d.ts +4 -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 +73 -0
- package/dist/header/Breadcrumbs.svelte.d.ts +4 -0
- package/dist/header/PageHeader.svelte +68 -0
- package/dist/header/PageHeader.svelte.d.ts +4 -0
- package/dist/header/breadcrumbs.d.ts +226 -0
- package/dist/header/breadcrumbs.js +87 -0
- package/dist/helper/cls.d.ts +1 -0
- package/dist/helper/cls.js +4 -0
- package/dist/helper/date.d.ts +7 -0
- package/dist/helper/date.js +15 -0
- package/dist/helper/nav.svelte.d.ts +6 -0
- package/dist/helper/nav.svelte.js +23 -0
- package/dist/index.d.ts +856 -1
- package/dist/index.js +78 -1
- package/dist/layout/card/Card.svelte +41 -0
- package/dist/layout/card/Card.svelte.d.ts +4 -0
- package/dist/layout/card/MetricCard.svelte +64 -0
- package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
- package/dist/layout/card/StatsCard.svelte +266 -0
- package/dist/layout/card/StatsCard.svelte.d.ts +4 -0
- package/dist/layout/card/card.d.ts +128 -0
- package/dist/layout/card/card.js +51 -0
- 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 +191 -0
- package/dist/layout/card/stats-card.js +73 -0
- package/dist/layout/navbar/Navbar.svelte +206 -0
- package/dist/layout/navbar/Navbar.svelte.d.ts +4 -0
- package/dist/layout/navbar/navbar.d.ts +205 -0
- package/dist/layout/navbar/navbar.js +98 -0
- package/dist/layout/sidebar/NavGroup.svelte +91 -0
- package/dist/layout/sidebar/NavGroup.svelte.d.ts +4 -0
- package/dist/layout/sidebar/NavItem.svelte +29 -0
- package/dist/layout/sidebar/NavItem.svelte.d.ts +4 -0
- package/dist/layout/sidebar/Sidebar.svelte +193 -0
- package/dist/layout/sidebar/Sidebar.svelte.d.ts +4 -0
- package/dist/layout/table/Cells.svelte +111 -0
- package/dist/layout/table/Cells.svelte.d.ts +27 -0
- package/dist/layout/table/Table.svelte +790 -0
- package/dist/layout/table/Table.svelte.d.ts +4 -0
- package/dist/layout/table/table.d.ts +256 -0
- package/dist/layout/table/table.js +141 -0
- package/dist/layout/tabs/Tab.svelte +60 -0
- package/dist/layout/tabs/Tab.svelte.d.ts +4 -0
- package/dist/layout/tabs/TabContent.svelte +30 -0
- package/dist/layout/tabs/TabContent.svelte.d.ts +4 -0
- package/dist/layout/tabs/TabGroup.svelte +62 -0
- package/dist/layout/tabs/TabGroup.svelte.d.ts +4 -0
- package/dist/layout/tabs/tabs.d.ts +140 -0
- package/dist/layout/tabs/tabs.js +298 -0
- package/dist/modal/Modal.svelte +207 -0
- package/dist/modal/Modal.svelte.d.ts +4 -0
- package/dist/modal/modal.d.ts +211 -0
- package/dist/modal/modal.js +81 -0
- package/dist/sonner/sonner.svelte +13 -0
- package/dist/sonner/sonner.svelte.d.ts +4 -0
- package/dist/types/variants.d.ts +1 -0
- package/dist/types/variants.js +1 -0
- 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 +39 -6
- package/dist/layout/Card.svelte +0 -179
- package/dist/layout/Card.svelte.d.ts +0 -208
- package/dist/layout/index.d.ts +0 -1
- 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,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}
|