@makolabs/ripple 2.5.9 → 3.0.0
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 +403 -497
- package/dist/adapters/storage/S3Adapter.d.ts +49 -1
- package/dist/adapters/storage/S3Adapter.js +38 -1
- package/dist/adapters/storage/types.d.ts +20 -0
- package/dist/ai/AIChatInterface.svelte +2 -1
- package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
- package/dist/ai/CodeRenderer.svelte +7 -2
- package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
- package/dist/ai/ComposeDropdown.svelte +1 -1
- package/dist/ai/MessageBox.svelte +3 -3
- package/dist/ai/MessageBox.svelte.d.ts +3 -2
- package/dist/ai/ThinkingDisplay.svelte +4 -3
- package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
- package/dist/ai/ai-types.d.ts +55 -1
- package/dist/button/Button.svelte +5 -5
- package/dist/button/button-types.d.ts +49 -4
- package/dist/button/button.d.ts +9 -9
- package/dist/button/button.js +6 -6
- package/dist/charts/Chart.svelte +8 -16
- package/dist/charts/chart-types.d.ts +78 -1
- package/dist/drawer/Drawer.svelte +6 -26
- package/dist/drawer/drawer-types.d.ts +33 -12
- package/dist/drawer/drawer.d.ts +3 -3
- package/dist/drawer/drawer.js +1 -1
- package/dist/elements/accordion/Accordion.svelte +6 -17
- package/dist/elements/accordion/accordion-types.d.ts +53 -6
- package/dist/elements/alert/Alert.svelte +3 -0
- package/dist/elements/badge/Badge.svelte +1 -1
- package/dist/elements/badge/badge-types.d.ts +22 -0
- package/dist/elements/badge/badge.d.ts +3 -3
- package/dist/elements/badge/badge.js +1 -1
- package/dist/elements/combobox/ComboBox.svelte +247 -0
- package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
- package/dist/elements/combobox/combobox-types.d.ts +41 -0
- package/dist/elements/combobox/combobox-types.js +1 -0
- package/dist/elements/context-menu/ContextMenu.svelte +137 -0
- package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
- package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
- package/dist/elements/context-menu/context-menu-types.js +1 -0
- package/dist/elements/dropdown/Dropdown.svelte +1 -1
- package/dist/elements/dropdown/Select.svelte +4 -1
- package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
- package/dist/elements/dropdown/dropdown.d.ts +3 -3
- package/dist/elements/dropdown/dropdown.js +2 -2
- package/dist/elements/dropdown/select.d.ts +3 -3
- package/dist/elements/dropdown/select.js +2 -2
- package/dist/elements/empty-state/EmptyState.svelte +1 -1
- package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
- package/dist/elements/empty-state/empty-state.d.ts +3 -3
- package/dist/elements/empty-state/empty-state.js +2 -2
- package/dist/elements/file-upload/FileUpload.svelte +5 -0
- package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
- package/dist/elements/pagination/Pagination.svelte +53 -21
- package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
- package/dist/elements/popover/Popover.svelte +234 -0
- package/dist/elements/popover/Popover.svelte.d.ts +4 -0
- package/dist/elements/popover/index.d.ts +2 -0
- package/dist/elements/popover/index.js +1 -0
- package/dist/elements/popover/popover-types.d.ts +60 -0
- package/dist/elements/popover/popover-types.js +1 -0
- package/dist/elements/progress/Progress.svelte +32 -7
- package/dist/elements/progress/progress-types.d.ts +48 -1
- package/dist/elements/skeleton/Skeleton.svelte +56 -0
- package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
- package/dist/elements/skeleton/index.d.ts +2 -0
- package/dist/elements/skeleton/index.js +1 -0
- package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
- package/dist/elements/skeleton/skeleton-types.js +1 -0
- package/dist/elements/spinner/Spinner.svelte +1 -1
- package/dist/elements/spinner/spinner-types.d.ts +20 -0
- package/dist/elements/spinner/spinner.d.ts +3 -3
- package/dist/elements/spinner/spinner.js +2 -2
- package/dist/elements/tooltip/Tooltip.svelte +108 -11
- package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
- package/dist/file-browser/FileBrowser.svelte +21 -12
- package/dist/filters/CompactFilters.svelte +221 -33
- package/dist/filters/CompactFilters.svelte.d.ts +1 -1
- package/dist/filters/FilterBar.svelte +184 -0
- package/dist/filters/FilterBar.svelte.d.ts +4 -0
- package/dist/filters/FilterPopover.svelte +346 -0
- package/dist/filters/FilterPopover.svelte.d.ts +4 -0
- package/dist/filters/date-presets.d.ts +15 -0
- package/dist/filters/date-presets.js +107 -0
- package/dist/filters/filter-types.d.ts +69 -3
- package/dist/filters/index.d.ts +5 -0
- package/dist/filters/index.js +4 -0
- package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
- package/dist/filters/sync-filters-to-url.svelte.js +114 -0
- package/dist/forms/DateRange.svelte +4 -2
- package/dist/forms/Input.svelte +2 -2
- package/dist/forms/MarketSelector.svelte +8 -3
- package/dist/forms/NumberInput.svelte +4 -4
- package/dist/forms/RadioGroup.svelte +123 -0
- package/dist/forms/RadioGroup.svelte.d.ts +4 -0
- package/dist/forms/SegmentedControl.svelte +11 -4
- package/dist/forms/Slider.svelte +72 -3
- package/dist/forms/Tags.svelte +14 -5
- package/dist/forms/Textarea.svelte +126 -0
- package/dist/forms/Textarea.svelte.d.ts +4 -0
- package/dist/forms/Toggle.svelte +8 -8
- package/dist/forms/calendar/Calendar.svelte +218 -0
- package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/forms/calendar/calendar-types.d.ts +46 -0
- package/dist/forms/calendar/calendar-types.js +1 -0
- package/dist/forms/calendar/index.d.ts +2 -0
- package/dist/forms/calendar/index.js +1 -0
- package/dist/forms/date-picker/DatePicker.svelte +144 -0
- package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
- package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
- package/dist/forms/date-picker/date-picker-types.js +1 -0
- package/dist/forms/form-types.d.ts +425 -6
- package/dist/forms/market/market-selector-types.d.ts +52 -1
- package/dist/forms/segmented-control.d.ts +5 -2
- package/dist/forms/segmented-control.js +16 -5
- package/dist/forms/slider.d.ts +3 -3
- package/dist/forms/slider.js +2 -2
- package/dist/funcs/user-management.remote.js +1 -1
- package/dist/header/Breadcrumbs.svelte +4 -20
- package/dist/header/PageHeader.svelte +6 -14
- package/dist/header/breadcrumbs.d.ts +3 -11
- package/dist/header/breadcrumbs.js +10 -5
- package/dist/header/header-types.d.ts +62 -11
- package/dist/index.d.ts +35 -9
- package/dist/index.js +24 -4
- package/dist/layout/activity-list/ActivityList.svelte +13 -7
- package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
- package/dist/layout/card/Card.svelte +12 -15
- package/dist/layout/card/MetricCard.svelte +50 -32
- package/dist/layout/card/card-types.d.ts +114 -4
- package/dist/layout/navbar/navbar-types.d.ts +48 -0
- package/dist/layout/navbar/navbar.d.ts +3 -3
- package/dist/layout/navbar/navbar.js +2 -2
- package/dist/layout/sidebar/Sidebar.svelte +87 -11
- package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
- package/dist/layout/stepper/Stepper.svelte +288 -0
- package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
- package/dist/layout/stepper/stepper-types.d.ts +80 -0
- package/dist/layout/stepper/stepper-types.js +1 -0
- package/dist/layout/table/Table.svelte +91 -85
- package/dist/layout/table/table-types.d.ts +148 -24
- package/dist/layout/table/table.d.ts +3 -3
- package/dist/layout/table/table.js +2 -2
- package/dist/layout/tabs/Tab.svelte +6 -2
- package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
- package/dist/layout/tabs/TabGroup.svelte +9 -2
- package/dist/layout/tabs/tabs-types.d.ts +63 -0
- package/dist/layout/tabs/tabs.d.ts +3 -3
- package/dist/layout/tabs/tabs.js +12 -6
- package/dist/modal/ConfirmDialog.svelte +65 -0
- package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
- package/dist/modal/Modal.svelte +6 -26
- package/dist/modal/confirm-dialog-types.d.ts +39 -0
- package/dist/modal/confirm-dialog-types.js +1 -0
- package/dist/modal/modal-types.d.ts +51 -12
- package/dist/modal/modal.d.ts +3 -3
- package/dist/modal/modal.js +3 -3
- package/dist/pipeline/Pipeline.svelte +8 -3
- package/dist/pipeline/pipeline-types.d.ts +55 -3
- package/dist/pipeline/pipeline.d.ts +18 -3
- package/dist/pipeline/pipeline.js +7 -2
- package/dist/server/s3.d.ts +35 -3
- package/dist/sonner/Toaster.svelte +29 -0
- package/dist/sonner/Toaster.svelte.d.ts +4 -0
- package/dist/sonner/index.d.ts +21 -0
- package/dist/sonner/index.js +20 -0
- package/dist/user-management/UserManagement.svelte +22 -16
- package/dist/user-management/UserModal.svelte +10 -7
- package/dist/user-management/UserTable.svelte +16 -17
- package/dist/user-management/UserViewModal.svelte +11 -11
- package/dist/user-management/user-management-types.d.ts +118 -31
- package/dist/variants.d.ts +1 -1
- package/dist/variants.js +1 -1
- package/package.json +7 -4
- package/dist/config/ai.d.ts +0 -13
- package/dist/config/ai.js +0 -44
- package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
- package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
- package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
- package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
- package/dist/helper/deprecation.d.ts +0 -14
- package/dist/helper/deprecation.js +0 -24
- package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
- package/dist/modal/ModalFooterTestWrapper.svelte.d.ts +0 -8
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import { Size } from '../../variants.js';
|
|
5
|
+
import Popover from '../popover/Popover.svelte';
|
|
6
|
+
import type { ComboBoxProps, ComboBoxItem } from './combobox-types.js';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
name,
|
|
10
|
+
id = name,
|
|
11
|
+
label,
|
|
12
|
+
placeholder = 'Start typing…',
|
|
13
|
+
value = $bindable<string | null>(null),
|
|
14
|
+
items,
|
|
15
|
+
filter,
|
|
16
|
+
onsearch,
|
|
17
|
+
emptyLabel = 'No results',
|
|
18
|
+
clearable = true,
|
|
19
|
+
disabled = false,
|
|
20
|
+
required = false,
|
|
21
|
+
size = Size.MD,
|
|
22
|
+
errors = [],
|
|
23
|
+
class: className = '',
|
|
24
|
+
onchange,
|
|
25
|
+
testId
|
|
26
|
+
}: ComboBoxProps = $props();
|
|
27
|
+
|
|
28
|
+
let open = $state(false);
|
|
29
|
+
let query = $state('');
|
|
30
|
+
let highlightedIndex = $state(0);
|
|
31
|
+
let inputEl = $state<HTMLInputElement | undefined>();
|
|
32
|
+
|
|
33
|
+
const selectedItem = $derived(items.find((i) => i.value === value) ?? null);
|
|
34
|
+
const hasErrors = $derived(errors.length > 0);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* When the field isn't open for editing, the text reflects the current
|
|
38
|
+
* selection. When open, the user's query takes over so they can narrow
|
|
39
|
+
* the list. Blur without selection reverts to the saved selection.
|
|
40
|
+
*/
|
|
41
|
+
const displayValue = $derived(open ? query : (selectedItem?.label ?? ''));
|
|
42
|
+
|
|
43
|
+
const defaultFilter = (item: ComboBoxItem, q: string) => {
|
|
44
|
+
if (!q) return true;
|
|
45
|
+
return item.label.toLowerCase().includes(q.toLowerCase());
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const filteredItems = $derived(
|
|
49
|
+
onsearch
|
|
50
|
+
? items // when server-side, parent controls the filtered list
|
|
51
|
+
: items.filter((item) => (filter ?? defaultFilter)(item, query))
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Reset highlight when list shrinks
|
|
55
|
+
$effect(() => {
|
|
56
|
+
void filteredItems;
|
|
57
|
+
if (highlightedIndex >= filteredItems.length) highlightedIndex = 0;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const sizeClass = $derived(
|
|
61
|
+
{
|
|
62
|
+
[Size.XS]: 'h-7 text-xs',
|
|
63
|
+
[Size.SM]: 'h-8 text-sm',
|
|
64
|
+
[Size.MD]: 'h-10 text-sm',
|
|
65
|
+
[Size.LG]: 'h-12 text-base',
|
|
66
|
+
[Size.XL]: 'h-14 text-lg',
|
|
67
|
+
[Size.XXL]: 'h-16 text-lg'
|
|
68
|
+
}[size]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
function openMenu() {
|
|
72
|
+
if (disabled) return;
|
|
73
|
+
open = true;
|
|
74
|
+
query = '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function closeMenu() {
|
|
78
|
+
open = false;
|
|
79
|
+
query = '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pick(item: ComboBoxItem) {
|
|
83
|
+
if (item.disabled) return;
|
|
84
|
+
value = item.value;
|
|
85
|
+
onchange?.(item.value);
|
|
86
|
+
closeMenu();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clear(e: MouseEvent) {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
value = null;
|
|
92
|
+
onchange?.(null);
|
|
93
|
+
inputEl?.focus();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleInput(e: Event) {
|
|
97
|
+
const v = (e.currentTarget as HTMLInputElement).value;
|
|
98
|
+
query = v;
|
|
99
|
+
open = true;
|
|
100
|
+
highlightedIndex = 0;
|
|
101
|
+
onsearch?.(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleKey(e: KeyboardEvent) {
|
|
105
|
+
if (disabled) return;
|
|
106
|
+
if (e.key === 'ArrowDown') {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
if (!open) {
|
|
109
|
+
openMenu();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
highlightedIndex = Math.min(highlightedIndex + 1, filteredItems.length - 1);
|
|
113
|
+
} else if (e.key === 'ArrowUp') {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
116
|
+
} else if (e.key === 'Enter') {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
const item = filteredItems[highlightedIndex];
|
|
119
|
+
if (item) pick(item);
|
|
120
|
+
} else if (e.key === 'Escape') {
|
|
121
|
+
if (open) {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
closeMenu();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<div class={cn('w-full', className)} data-testid={buildTestId('combobox', 'wrapper', testId)}>
|
|
130
|
+
{#if label}
|
|
131
|
+
<label for={id} class="text-default-700 mb-1 block text-sm font-medium">
|
|
132
|
+
{label}
|
|
133
|
+
{#if required}<span class="text-danger-500" aria-hidden="true">*</span>{/if}
|
|
134
|
+
</label>
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
<input type="hidden" {name} value={value ?? ''} {required} />
|
|
138
|
+
|
|
139
|
+
<Popover trigger="manual" bind:open panelClass="w-[var(--cbx-w,20rem)] p-0">
|
|
140
|
+
<div
|
|
141
|
+
class={cn(
|
|
142
|
+
'flex w-full items-center gap-2 rounded-lg border bg-white px-3 transition-colors',
|
|
143
|
+
'focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none',
|
|
144
|
+
sizeClass,
|
|
145
|
+
hasErrors
|
|
146
|
+
? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
|
|
147
|
+
: 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
|
|
148
|
+
disabled && 'cursor-not-allowed opacity-50'
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
{id}
|
|
153
|
+
bind:this={inputEl}
|
|
154
|
+
type="text"
|
|
155
|
+
role="combobox"
|
|
156
|
+
aria-expanded={open}
|
|
157
|
+
aria-controls="{id}-listbox"
|
|
158
|
+
aria-autocomplete="list"
|
|
159
|
+
aria-invalid={hasErrors}
|
|
160
|
+
autocomplete="off"
|
|
161
|
+
{placeholder}
|
|
162
|
+
{disabled}
|
|
163
|
+
value={displayValue}
|
|
164
|
+
oninput={handleInput}
|
|
165
|
+
onfocus={openMenu}
|
|
166
|
+
onkeydown={handleKey}
|
|
167
|
+
class="flex-1 bg-transparent outline-none"
|
|
168
|
+
data-testid={buildTestId('combobox', undefined, testId)}
|
|
169
|
+
/>
|
|
170
|
+
{#if clearable && value && !disabled}
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onclick={clear}
|
|
174
|
+
aria-label="Clear selection"
|
|
175
|
+
class="text-default-400 hover:text-default-700 flex size-5 items-center justify-center rounded"
|
|
176
|
+
>
|
|
177
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
178
|
+
<path
|
|
179
|
+
d="M3 3l6 6M9 3l-6 6"
|
|
180
|
+
stroke="currentColor"
|
|
181
|
+
stroke-width="1.5"
|
|
182
|
+
stroke-linecap="round"
|
|
183
|
+
/>
|
|
184
|
+
</svg>
|
|
185
|
+
</button>
|
|
186
|
+
{/if}
|
|
187
|
+
<svg
|
|
188
|
+
class={cn('text-default-400 size-4 transition-transform', open && 'rotate-180')}
|
|
189
|
+
viewBox="0 0 20 20"
|
|
190
|
+
fill="currentColor"
|
|
191
|
+
aria-hidden="true"
|
|
192
|
+
>
|
|
193
|
+
<path
|
|
194
|
+
fill-rule="evenodd"
|
|
195
|
+
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06z"
|
|
196
|
+
clip-rule="evenodd"
|
|
197
|
+
/>
|
|
198
|
+
</svg>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{#snippet content()}
|
|
202
|
+
<ul
|
|
203
|
+
id="{id}-listbox"
|
|
204
|
+
role="listbox"
|
|
205
|
+
class="max-h-64 overflow-auto py-1"
|
|
206
|
+
data-testid={buildTestId('combobox', 'listbox', testId)}
|
|
207
|
+
>
|
|
208
|
+
{#each filteredItems as item, i (item.value)}
|
|
209
|
+
{@const selected = item.value === value}
|
|
210
|
+
{@const highlighted = i === highlightedIndex}
|
|
211
|
+
<li>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
role="option"
|
|
215
|
+
aria-selected={selected}
|
|
216
|
+
disabled={item.disabled}
|
|
217
|
+
onmouseenter={() => (highlightedIndex = i)}
|
|
218
|
+
onclick={() => pick(item)}
|
|
219
|
+
class={cn(
|
|
220
|
+
'flex w-full flex-col items-start gap-0.5 px-3 py-1.5 text-left text-sm',
|
|
221
|
+
highlighted && 'bg-primary-50 text-primary-700',
|
|
222
|
+
selected && !highlighted && 'text-primary-600 font-medium',
|
|
223
|
+
item.disabled && 'text-default-300 cursor-not-allowed',
|
|
224
|
+
!item.disabled && !highlighted && 'text-default-700 hover:bg-default-50'
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
<span>{item.label}</span>
|
|
228
|
+
{#if item.description}
|
|
229
|
+
<span class="text-default-500 text-xs">{item.description}</span>
|
|
230
|
+
{/if}
|
|
231
|
+
</button>
|
|
232
|
+
</li>
|
|
233
|
+
{:else}
|
|
234
|
+
<li class="text-default-400 px-3 py-2 text-sm">{emptyLabel}</li>
|
|
235
|
+
{/each}
|
|
236
|
+
</ul>
|
|
237
|
+
{/snippet}
|
|
238
|
+
</Popover>
|
|
239
|
+
|
|
240
|
+
{#if hasErrors}
|
|
241
|
+
<ul class="mt-1 space-y-0.5" role="alert">
|
|
242
|
+
{#each errors as error (error)}
|
|
243
|
+
<li class="text-danger-600 text-xs">{error}</li>
|
|
244
|
+
{/each}
|
|
245
|
+
</ul>
|
|
246
|
+
{/if}
|
|
247
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ClassValue } from 'tailwind-variants';
|
|
2
|
+
import type { VariantSizes } from '../../index.js';
|
|
3
|
+
export type ComboBoxItem = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Optional helper text shown below the label in the dropdown. */
|
|
7
|
+
description?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ComboBoxProps = {
|
|
11
|
+
name: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** Bindable selected value. `null` when unset. */
|
|
16
|
+
value?: string | null;
|
|
17
|
+
/** Full list of options. Filtered locally by the typed query. */
|
|
18
|
+
items: ComboBoxItem[];
|
|
19
|
+
/**
|
|
20
|
+
* Custom filter. If provided, replaces the default case-insensitive
|
|
21
|
+
* label substring match. Return true to include the item in the list.
|
|
22
|
+
*/
|
|
23
|
+
filter?: (item: ComboBoxItem, query: string) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Called on every keystroke with the current query. Use this to fetch
|
|
26
|
+
* items from a server (set `items` from the parent based on the query).
|
|
27
|
+
* When present, the local `filter` is bypassed for the fetched set.
|
|
28
|
+
*/
|
|
29
|
+
onsearch?: (query: string) => void;
|
|
30
|
+
/** Message shown when filtering leaves no matches. @default 'No results' */
|
|
31
|
+
emptyLabel?: string;
|
|
32
|
+
/** Show a clear × when something is selected. @default true */
|
|
33
|
+
clearable?: boolean;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
size?: VariantSizes;
|
|
37
|
+
errors?: string[];
|
|
38
|
+
class?: ClassValue;
|
|
39
|
+
onchange?: (value: string | null) => void;
|
|
40
|
+
testId?: string;
|
|
41
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import { fly } from 'svelte/transition';
|
|
5
|
+
import { quintOut } from 'svelte/easing';
|
|
6
|
+
import { Color } from '../../variants.js';
|
|
7
|
+
import type { ContextMenuProps, ContextMenuItem } from './context-menu-types.js';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
items,
|
|
11
|
+
children,
|
|
12
|
+
trigger = 'rightclick',
|
|
13
|
+
class: className = '',
|
|
14
|
+
menuClass = '',
|
|
15
|
+
testId
|
|
16
|
+
}: ContextMenuProps = $props();
|
|
17
|
+
|
|
18
|
+
let open = $state(false);
|
|
19
|
+
let x = $state(0);
|
|
20
|
+
let y = $state(0);
|
|
21
|
+
let menuRef = $state<HTMLDivElement | undefined>();
|
|
22
|
+
let longPressTimer: ReturnType<typeof setTimeout> | undefined;
|
|
23
|
+
|
|
24
|
+
function openAt(clientX: number, clientY: number) {
|
|
25
|
+
x = clientX;
|
|
26
|
+
y = clientY;
|
|
27
|
+
open = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function close() {
|
|
31
|
+
open = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleContextMenu(e: MouseEvent) {
|
|
35
|
+
if (trigger !== 'rightclick') return;
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
openAt(e.clientX, e.clientY);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleTouchStart(e: TouchEvent) {
|
|
41
|
+
if (trigger !== 'longpress') return;
|
|
42
|
+
const t = e.touches[0];
|
|
43
|
+
longPressTimer = setTimeout(() => openAt(t.clientX, t.clientY), 600);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleTouchEnd() {
|
|
47
|
+
clearTimeout(longPressTimer);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleWindowClick(e: MouseEvent) {
|
|
51
|
+
if (!open) return;
|
|
52
|
+
if (menuRef && !menuRef.contains(e.target as Node)) close();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleKey(e: KeyboardEvent) {
|
|
56
|
+
if (open && e.key === 'Escape') close();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleItem(item: Exclude<ContextMenuItem, 'separator'>) {
|
|
60
|
+
if (item.disabled) return;
|
|
61
|
+
item.onclick?.();
|
|
62
|
+
close();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function itemColorClass(color?: string): string {
|
|
66
|
+
if (color === Color.DANGER) return 'text-danger-600 hover:bg-danger-50';
|
|
67
|
+
return 'text-default-700 hover:bg-default-50';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clamp x/y into viewport after mount so the menu doesn't overflow
|
|
71
|
+
$effect(() => {
|
|
72
|
+
if (!open || !menuRef) return;
|
|
73
|
+
const rect = menuRef.getBoundingClientRect();
|
|
74
|
+
const maxX = window.innerWidth - rect.width - 4;
|
|
75
|
+
const maxY = window.innerHeight - rect.height - 4;
|
|
76
|
+
if (x > maxX) x = Math.max(4, maxX);
|
|
77
|
+
if (y > maxY) y = Math.max(4, maxY);
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<svelte:window onmousedown={handleWindowClick} onkeydown={handleKey} />
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
class={cn('inline-block', className)}
|
|
85
|
+
oncontextmenu={handleContextMenu}
|
|
86
|
+
ontouchstart={handleTouchStart}
|
|
87
|
+
ontouchend={handleTouchEnd}
|
|
88
|
+
ontouchcancel={handleTouchEnd}
|
|
89
|
+
role="presentation"
|
|
90
|
+
data-testid={buildTestId('context-menu', undefined, testId)}
|
|
91
|
+
>
|
|
92
|
+
{@render children()}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{#if open}
|
|
96
|
+
<div
|
|
97
|
+
bind:this={menuRef}
|
|
98
|
+
role="menu"
|
|
99
|
+
transition:fly={{ y: -4, duration: 140, easing: quintOut }}
|
|
100
|
+
class={cn(
|
|
101
|
+
'border-default-200 fixed z-50 min-w-[11rem] rounded-lg border bg-white py-1 shadow-lg',
|
|
102
|
+
menuClass
|
|
103
|
+
)}
|
|
104
|
+
style="left: {x}px; top: {y}px;"
|
|
105
|
+
data-testid={buildTestId('context-menu', 'menu', testId)}
|
|
106
|
+
>
|
|
107
|
+
{#each items as item, i (i)}
|
|
108
|
+
{#if item === 'separator'}
|
|
109
|
+
<div class="bg-default-100 my-1 h-px" aria-hidden="true"></div>
|
|
110
|
+
{:else}
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
role="menuitem"
|
|
114
|
+
disabled={item.disabled}
|
|
115
|
+
onclick={() => handleItem(item)}
|
|
116
|
+
class={cn(
|
|
117
|
+
'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
|
|
118
|
+
item.disabled
|
|
119
|
+
? 'text-default-300 cursor-not-allowed'
|
|
120
|
+
: `cursor-pointer ${itemColorClass(item.color)}`
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{#if item.icon}
|
|
124
|
+
{@const Icon = item.icon}
|
|
125
|
+
<span class="flex size-4 shrink-0 items-center justify-center">
|
|
126
|
+
<Icon />
|
|
127
|
+
</span>
|
|
128
|
+
{/if}
|
|
129
|
+
<span class="flex-1">{item.label}</span>
|
|
130
|
+
{#if item.shortcut}
|
|
131
|
+
<span class="text-default-400 ml-4 text-xs">{item.shortcut}</span>
|
|
132
|
+
{/if}
|
|
133
|
+
</button>
|
|
134
|
+
{/if}
|
|
135
|
+
{/each}
|
|
136
|
+
</div>
|
|
137
|
+
{/if}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ClassValue } from 'tailwind-variants';
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
import type { VariantColors } from '../../index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Single entry in a ContextMenu. A `'separator'` value renders a
|
|
6
|
+
* horizontal divider.
|
|
7
|
+
*/
|
|
8
|
+
export type ContextMenuItem = {
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: Component;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Semantic color for the item. `'danger'` is the conventional choice
|
|
14
|
+
* for destructive actions (Delete, Remove) and tints both the icon
|
|
15
|
+
* and the label.
|
|
16
|
+
*/
|
|
17
|
+
color?: VariantColors;
|
|
18
|
+
/** Short text shown right-aligned (e.g. keyboard shortcut). */
|
|
19
|
+
shortcut?: string;
|
|
20
|
+
onclick?: () => void;
|
|
21
|
+
} | 'separator';
|
|
22
|
+
export type ContextMenuProps = {
|
|
23
|
+
/** Ordered list of items or `'separator'` strings. */
|
|
24
|
+
items: ContextMenuItem[];
|
|
25
|
+
/**
|
|
26
|
+
* Wrap this trigger in a right-click handler. Any child markup works —
|
|
27
|
+
* the consumer's interactive elements inside still receive their own
|
|
28
|
+
* events.
|
|
29
|
+
*/
|
|
30
|
+
children: Snippet;
|
|
31
|
+
/**
|
|
32
|
+
* Alternative trigger gesture. `'rightclick'` is the web-standard
|
|
33
|
+
* `contextmenu` event. `'longpress'` is touch-friendly (600ms).
|
|
34
|
+
* @default 'rightclick'
|
|
35
|
+
*/
|
|
36
|
+
trigger?: 'rightclick' | 'longpress';
|
|
37
|
+
class?: ClassValue;
|
|
38
|
+
menuClass?: ClassValue;
|
|
39
|
+
testId?: string;
|
|
40
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { cn } from '../../helper/cls.js';
|
|
4
4
|
import { buildTestId } from '../../helper/testid.js';
|
|
5
5
|
import { selectTV } from './select.js';
|
|
6
|
+
import { fly } from 'svelte/transition';
|
|
7
|
+
import { quintOut } from 'svelte/easing';
|
|
6
8
|
import type { SelectItem, SelectProps } from '../../index.js';
|
|
7
9
|
import Badge from '../badge/Badge.svelte';
|
|
8
10
|
import { Size } from '../../variants.js';
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
placeholder = 'Select an option',
|
|
16
18
|
searchable = false,
|
|
17
19
|
disabled = false,
|
|
18
|
-
size = Size.
|
|
20
|
+
size = Size.MD,
|
|
19
21
|
class: className = '',
|
|
20
22
|
containerClass = '',
|
|
21
23
|
listClass = '',
|
|
@@ -407,6 +409,7 @@
|
|
|
407
409
|
role="listbox"
|
|
408
410
|
aria-labelledby="{selectId}-label"
|
|
409
411
|
data-testid={buildTestId('select', 'list', testId)}
|
|
412
|
+
transition:fly={{ y: -8, duration: 300, easing: quintOut }}
|
|
410
413
|
>
|
|
411
414
|
{#if isSearchable}
|
|
412
415
|
<div class={searchInputClass_}>
|