@makolabs/ripple 2.5.8 → 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.d.ts +1 -1
- package/dist/funcs/user-management.remote.js +2 -2
- 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
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
FilterTab,
|
|
5
|
+
FilterGroup,
|
|
6
|
+
FilterSelectionValue,
|
|
7
|
+
CompactFiltersProps
|
|
8
|
+
} from '../index.js';
|
|
4
9
|
|
|
5
|
-
// Props definition
|
|
6
10
|
let {
|
|
7
11
|
filterGroups = [],
|
|
12
|
+
selections = $bindable<Record<string, FilterSelectionValue>>({}),
|
|
13
|
+
onfilterchange,
|
|
14
|
+
defaults,
|
|
15
|
+
showClearAll = false,
|
|
16
|
+
clearAllLabel = 'Clear',
|
|
17
|
+
searchQuery = $bindable<string | undefined>(undefined),
|
|
18
|
+
searchPlaceholder = 'Search…',
|
|
19
|
+
chipSummary = false,
|
|
8
20
|
isExpanded = $bindable(false),
|
|
9
21
|
title = 'Filters',
|
|
10
22
|
class: className,
|
|
@@ -13,16 +25,115 @@
|
|
|
13
25
|
FilterIcon
|
|
14
26
|
}: CompactFiltersProps = $props();
|
|
15
27
|
|
|
16
|
-
//
|
|
28
|
+
// Search input only renders when the consumer bound to searchQuery.
|
|
29
|
+
const searchEnabled = $derived(searchQuery !== undefined);
|
|
30
|
+
|
|
17
31
|
function toggleExpanded() {
|
|
18
32
|
isExpanded = !isExpanded;
|
|
19
33
|
}
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
function
|
|
23
|
-
const
|
|
35
|
+
/** Read the selected value(s) for a group from the `selections` map. */
|
|
36
|
+
function getSelected(group: FilterGroup): FilterSelectionValue {
|
|
37
|
+
const fromMap = selections[group.key];
|
|
38
|
+
if (fromMap !== undefined) return fromMap;
|
|
39
|
+
return group.multiple ? [] : '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isSelected(group: FilterGroup, value: string): boolean {
|
|
43
|
+
const current = getSelected(group);
|
|
44
|
+
if (Array.isArray(current)) return current.includes(value);
|
|
45
|
+
return current === value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Toggle a value in a multi-select group. */
|
|
49
|
+
function toggleMulti(current: string[], value: string): string[] {
|
|
50
|
+
return current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Write selected value(s). */
|
|
54
|
+
function handleSelect(group: FilterGroup, value: string) {
|
|
55
|
+
let next: FilterSelectionValue;
|
|
56
|
+
if (group.multiple) {
|
|
57
|
+
const current = getSelected(group);
|
|
58
|
+
const arr: string[] = Array.isArray(current)
|
|
59
|
+
? (current as string[])
|
|
60
|
+
: typeof current === 'string' && current
|
|
61
|
+
? [current]
|
|
62
|
+
: [];
|
|
63
|
+
next = toggleMulti(arr, value);
|
|
64
|
+
} else {
|
|
65
|
+
next = value;
|
|
66
|
+
}
|
|
67
|
+
selections = { ...selections, [group.key]: next };
|
|
68
|
+
onfilterchange?.(group.key, next);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Remove a single selection (used by chip summary ×). */
|
|
72
|
+
function removeSelection(key: string, value: string) {
|
|
73
|
+
const group = filterGroups.find((g) => g.key === key);
|
|
74
|
+
if (!group) return;
|
|
75
|
+
if (group.multiple) {
|
|
76
|
+
const current = getSelected(group);
|
|
77
|
+
const arr: string[] = Array.isArray(current) ? (current as string[]) : [];
|
|
78
|
+
const next = arr.filter((v) => v !== value);
|
|
79
|
+
selections = { ...selections, [key]: next };
|
|
80
|
+
onfilterchange?.(key, next);
|
|
81
|
+
} else {
|
|
82
|
+
const next = defaults?.[key] ?? '';
|
|
83
|
+
selections = { ...selections, [key]: next };
|
|
84
|
+
onfilterchange?.(key, next);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearAll() {
|
|
89
|
+
const next = defaults ?? {};
|
|
90
|
+
selections = { ...next };
|
|
91
|
+
for (const group of filterGroups) {
|
|
92
|
+
onfilterchange?.(group.key, getSelected(group));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Are current selections different from defaults? */
|
|
97
|
+
const isDirty = $derived.by(() => {
|
|
98
|
+
if (!defaults) return Object.keys(selections).length > 0;
|
|
99
|
+
for (const key of new Set([...Object.keys(defaults), ...Object.keys(selections)])) {
|
|
100
|
+
const a = JSON.stringify(selections[key] ?? null);
|
|
101
|
+
const b = JSON.stringify(defaults[key] ?? null);
|
|
102
|
+
if (a !== b) return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function getSelectedLabel(tabs: FilterTab[], selectedValue: FilterSelectionValue): string {
|
|
108
|
+
if (Array.isArray(selectedValue)) {
|
|
109
|
+
if (selectedValue.length === 0) return 'All';
|
|
110
|
+
const labels = selectedValue.map((v) => tabs.find((t) => t.value === v)?.label || v);
|
|
111
|
+
return labels.join(', ');
|
|
112
|
+
}
|
|
113
|
+
if (typeof selectedValue !== 'string' || !selectedValue) return 'All';
|
|
114
|
+
const tab = tabs.find((t) => t.value === selectedValue);
|
|
24
115
|
return tab ? tab.label : 'All';
|
|
25
116
|
}
|
|
117
|
+
|
|
118
|
+
/** Chips for the collapsed chip summary. One chip per selected non-default value. */
|
|
119
|
+
const chips = $derived.by(() => {
|
|
120
|
+
const out: { key: string; value: string; label: string; groupLabel: string }[] = [];
|
|
121
|
+
for (const group of filterGroups) {
|
|
122
|
+
const current = getSelected(group);
|
|
123
|
+
const defaultValue = defaults?.[group.key];
|
|
124
|
+
const tabs = group.tabs ?? [];
|
|
125
|
+
if (Array.isArray(current)) {
|
|
126
|
+
for (const v of current) {
|
|
127
|
+
const label = tabs.find((t) => t.value === v)?.label ?? v;
|
|
128
|
+
out.push({ key: group.key, value: v, label, groupLabel: group.label });
|
|
129
|
+
}
|
|
130
|
+
} else if (typeof current === 'string' && current && current !== defaultValue) {
|
|
131
|
+
const label = tabs.find((t) => t.value === current)?.label ?? current;
|
|
132
|
+
out.push({ key: group.key, value: current, label, groupLabel: group.label });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
});
|
|
26
137
|
</script>
|
|
27
138
|
|
|
28
139
|
{#snippet DefaultFilterIcon()}
|
|
@@ -71,50 +182,120 @@
|
|
|
71
182
|
{/snippet}
|
|
72
183
|
|
|
73
184
|
<div class={cn('border-default-200 rounded-lg border bg-white p-3 shadow-sm', className)}>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
>
|
|
78
|
-
<
|
|
185
|
+
<!-- Header row: title | search | clear | chevron.
|
|
186
|
+
`flex-wrap` lets the search + controls drop to a second row on
|
|
187
|
+
narrow viewports instead of overflowing off-screen. -->
|
|
188
|
+
<div class="mb-2 flex flex-wrap items-center gap-2">
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onclick={toggleExpanded}
|
|
192
|
+
class="flex flex-1 cursor-pointer items-center gap-2"
|
|
193
|
+
>
|
|
79
194
|
{#if FilterIcon}
|
|
80
195
|
<FilterIcon size={16} class="text-default-500" />
|
|
81
196
|
{:else}
|
|
82
|
-
<span class="text-default-500">
|
|
83
|
-
{@render DefaultFilterIcon()}
|
|
84
|
-
</span>
|
|
197
|
+
<span class="text-default-500">{@render DefaultFilterIcon()}</span>
|
|
85
198
|
{/if}
|
|
86
199
|
<span class="text-sm font-medium">{title}</span>
|
|
87
|
-
</
|
|
88
|
-
|
|
89
|
-
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
{#if searchEnabled}
|
|
203
|
+
<!-- `w-full sm:w-48` lets the search field fill the row on
|
|
204
|
+
narrow viewports (after wrapping) but stay compact on wide. -->
|
|
205
|
+
<div class="relative order-last w-full sm:order-none sm:w-auto">
|
|
206
|
+
<input
|
|
207
|
+
type="text"
|
|
208
|
+
class="border-default-200 focus:border-primary-400 h-7 w-full rounded-md border px-2 pr-6 text-xs focus:outline-none sm:w-48"
|
|
209
|
+
placeholder={searchPlaceholder}
|
|
210
|
+
bind:value={searchQuery}
|
|
211
|
+
data-filters-search=""
|
|
212
|
+
/>
|
|
213
|
+
{#if searchQuery}
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
onclick={() => (searchQuery = '')}
|
|
217
|
+
class="text-default-400 hover:text-default-700 absolute top-1/2 right-1 flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center rounded"
|
|
218
|
+
aria-label="Clear search"
|
|
219
|
+
data-filters-search-clear=""
|
|
220
|
+
>
|
|
221
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
222
|
+
<path
|
|
223
|
+
d="M3 3l6 6M9 3l-6 6"
|
|
224
|
+
stroke="currentColor"
|
|
225
|
+
stroke-width="1.5"
|
|
226
|
+
stroke-linecap="round"
|
|
227
|
+
/>
|
|
228
|
+
</svg>
|
|
229
|
+
</button>
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
{/if}
|
|
233
|
+
|
|
234
|
+
{#if showClearAll && isDirty}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onclick={clearAll}
|
|
238
|
+
class="text-default-600 hover:bg-default-100 cursor-pointer rounded-md px-2 py-1 text-xs font-medium"
|
|
239
|
+
data-filters-clear-all=""
|
|
240
|
+
>
|
|
241
|
+
{clearAllLabel}
|
|
242
|
+
</button>
|
|
243
|
+
{/if}
|
|
244
|
+
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onclick={toggleExpanded}
|
|
248
|
+
class="text-default-500 hover:bg-default-100 hover:text-default-700 cursor-pointer rounded-md p-1"
|
|
90
249
|
aria-label={isExpanded ? `Collapse ${title.toLowerCase()}` : `Expand ${title.toLowerCase()}`}
|
|
91
250
|
>
|
|
92
251
|
{#if isExpanded}
|
|
93
|
-
|
|
252
|
+
{@render DefaultChevronUp()}
|
|
94
253
|
{:else}
|
|
95
|
-
|
|
254
|
+
{@render DefaultChevronDown()}
|
|
96
255
|
{/if}
|
|
97
|
-
</
|
|
98
|
-
</
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
99
258
|
|
|
100
259
|
{#if !isExpanded}
|
|
101
|
-
<!-- Summary of selected filters when collapsed -->
|
|
102
260
|
<div class={cn('flex flex-wrap gap-2', summaryClass)}>
|
|
103
|
-
{#
|
|
104
|
-
{#
|
|
105
|
-
<
|
|
106
|
-
class="bg-primary-50 text-primary-700 border-primary-200 flex items-center gap-1 rounded-full border px-
|
|
261
|
+
{#if chipSummary}
|
|
262
|
+
{#each chips as chip (chip.key + '::' + chip.value)}
|
|
263
|
+
<span
|
|
264
|
+
class="bg-primary-50 text-primary-700 border-primary-200 flex items-center gap-1 rounded-full border px-2 py-1 text-xs"
|
|
265
|
+
data-filters-chip=""
|
|
107
266
|
>
|
|
108
|
-
<span class="font-medium">{
|
|
109
|
-
{
|
|
110
|
-
|
|
267
|
+
<span class="font-medium">{chip.groupLabel}:</span>
|
|
268
|
+
<span>{chip.label}</span>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onclick={() => removeSelection(chip.key, chip.value)}
|
|
272
|
+
aria-label={`Remove ${chip.groupLabel}: ${chip.label}`}
|
|
273
|
+
class="text-primary-600 hover:text-primary-900 ml-1 cursor-pointer"
|
|
274
|
+
>
|
|
275
|
+
×
|
|
276
|
+
</button>
|
|
277
|
+
</span>
|
|
278
|
+
{/each}
|
|
279
|
+
{#if chips.length === 0}
|
|
280
|
+
<span class="text-default-400 text-xs">No filters applied</span>
|
|
111
281
|
{/if}
|
|
112
|
-
{
|
|
282
|
+
{:else}
|
|
283
|
+
{#each filterGroups as group (group.key)}
|
|
284
|
+
{#if (group.tabs ?? []).length > 0}
|
|
285
|
+
<div
|
|
286
|
+
class="bg-primary-50 text-primary-700 border-primary-200 flex items-center gap-1 rounded-full border px-3 py-1 text-xs"
|
|
287
|
+
>
|
|
288
|
+
<span class="font-medium">{group.label}:</span>
|
|
289
|
+
{getSelectedLabel(group.tabs ?? [], getSelected(group))}
|
|
290
|
+
</div>
|
|
291
|
+
{/if}
|
|
292
|
+
{/each}
|
|
293
|
+
{/if}
|
|
113
294
|
</div>
|
|
114
295
|
{:else}
|
|
115
296
|
<div class={cn('flex flex-col gap-2', expandedClass)}>
|
|
116
297
|
{#each filterGroups as group, index (group.key)}
|
|
117
|
-
{#if group.tabs.length > 0}
|
|
298
|
+
{#if (group.tabs ?? []).length > 0}
|
|
118
299
|
<div
|
|
119
300
|
class={cn(
|
|
120
301
|
'flex items-center gap-2 pb-2',
|
|
@@ -128,16 +309,23 @@
|
|
|
128
309
|
</div>
|
|
129
310
|
<div class="flex flex-wrap gap-2">
|
|
130
311
|
{#each group.tabs as tab (tab.value)}
|
|
312
|
+
{@const active = isSelected(group, tab.value)}
|
|
131
313
|
<button
|
|
132
|
-
|
|
314
|
+
type="button"
|
|
315
|
+
onclick={() => handleSelect(group, tab.value)}
|
|
133
316
|
class={cn(
|
|
134
317
|
'rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap',
|
|
135
|
-
|
|
318
|
+
active
|
|
136
319
|
? 'bg-primary-50 text-primary-700 border-primary-200'
|
|
137
320
|
: 'border-default-200 text-default-700 hover:bg-default-50'
|
|
138
321
|
)}
|
|
139
322
|
>
|
|
140
323
|
{tab.label}
|
|
324
|
+
{#if tab.count !== undefined}
|
|
325
|
+
<span class={cn('ml-1', active ? 'text-primary-500' : 'text-default-400')}
|
|
326
|
+
>({tab.count})</span
|
|
327
|
+
>
|
|
328
|
+
{/if}
|
|
141
329
|
</button>
|
|
142
330
|
{/each}
|
|
143
331
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CompactFiltersProps } from '../index.js';
|
|
2
|
-
declare const CompactFilters: import("svelte").Component<CompactFiltersProps, {}, "isExpanded">;
|
|
2
|
+
declare const CompactFilters: import("svelte").Component<CompactFiltersProps, {}, "selections" | "searchQuery" | "isExpanded">;
|
|
3
3
|
type CompactFilters = ReturnType<typeof CompactFilters>;
|
|
4
4
|
export default CompactFilters;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { fly } from 'svelte/transition';
|
|
4
|
+
import { quintOut } from 'svelte/easing';
|
|
5
|
+
import type {
|
|
6
|
+
FilterGroup,
|
|
7
|
+
FilterSelectionValue,
|
|
8
|
+
DateRangeValue,
|
|
9
|
+
CompactFiltersProps
|
|
10
|
+
} from '../index.js';
|
|
11
|
+
import FilterPopover from './FilterPopover.svelte';
|
|
12
|
+
import { defaultDatePresets, toIsoDate } from './date-presets.js';
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
filterGroups = [],
|
|
16
|
+
selections = $bindable<Record<string, FilterSelectionValue>>({}),
|
|
17
|
+
onfilterchange,
|
|
18
|
+
defaults,
|
|
19
|
+
showClearAll = false,
|
|
20
|
+
clearAllLabel = 'Clear all',
|
|
21
|
+
class: className,
|
|
22
|
+
testId
|
|
23
|
+
}: Pick<
|
|
24
|
+
CompactFiltersProps,
|
|
25
|
+
| 'filterGroups'
|
|
26
|
+
| 'selections'
|
|
27
|
+
| 'onfilterchange'
|
|
28
|
+
| 'defaults'
|
|
29
|
+
| 'showClearAll'
|
|
30
|
+
| 'clearAllLabel'
|
|
31
|
+
| 'class'
|
|
32
|
+
| 'testId'
|
|
33
|
+
> = $props();
|
|
34
|
+
|
|
35
|
+
let addMenuOpen = $state(false);
|
|
36
|
+
let addMenuRef = $state<HTMLDivElement | undefined>();
|
|
37
|
+
|
|
38
|
+
function isGroupActive(group: FilterGroup): boolean {
|
|
39
|
+
const v = selections[group.key];
|
|
40
|
+
if (v === undefined || v === null) return false;
|
|
41
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
42
|
+
if (typeof v === 'object') return true; // DateRangeValue
|
|
43
|
+
const def = defaults?.[group.key];
|
|
44
|
+
if (def !== undefined) return v !== def;
|
|
45
|
+
return !!v;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Groups currently shown as pills (have a selection that differs from default). */
|
|
49
|
+
const activeGroups = $derived(filterGroups.filter(isGroupActive));
|
|
50
|
+
|
|
51
|
+
/** Groups not yet used — available in the "+ Add filter" menu. */
|
|
52
|
+
const availableGroups = $derived(filterGroups.filter((g) => !isGroupActive(g)));
|
|
53
|
+
|
|
54
|
+
function addGroup(group: FilterGroup) {
|
|
55
|
+
let next: FilterSelectionValue;
|
|
56
|
+
if (group.dateRange) {
|
|
57
|
+
// Seed a date-range group with the first preset so a pill appears.
|
|
58
|
+
const cfg = typeof group.dateRange === 'object' ? group.dateRange : {};
|
|
59
|
+
const presets = cfg.presets ?? defaultDatePresets;
|
|
60
|
+
const first = presets[0];
|
|
61
|
+
if (!first) return;
|
|
62
|
+
const { from, to } = first.range();
|
|
63
|
+
const value: DateRangeValue = {
|
|
64
|
+
from: toIsoDate(from),
|
|
65
|
+
to: toIsoDate(to),
|
|
66
|
+
preset: first.value
|
|
67
|
+
};
|
|
68
|
+
next = value;
|
|
69
|
+
} else {
|
|
70
|
+
const firstTab = group.tabs?.[0];
|
|
71
|
+
if (!firstTab) return;
|
|
72
|
+
next = group.multiple ? [firstTab.value] : firstTab.value;
|
|
73
|
+
}
|
|
74
|
+
selections = { ...selections, [group.key]: next };
|
|
75
|
+
onfilterchange?.(group.key, next);
|
|
76
|
+
addMenuOpen = false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeGroup(group: FilterGroup) {
|
|
80
|
+
const def = defaults?.[group.key];
|
|
81
|
+
const next: FilterSelectionValue =
|
|
82
|
+
def !== undefined ? def : group.dateRange ? null : group.multiple ? [] : '';
|
|
83
|
+
selections = { ...selections, [group.key]: next };
|
|
84
|
+
onfilterchange?.(group.key, next);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearAll() {
|
|
88
|
+
selections = defaults ? { ...defaults } : {};
|
|
89
|
+
for (const group of filterGroups) {
|
|
90
|
+
onfilterchange?.(group.key, selections[group.key] ?? (group.multiple ? [] : ''));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleKey(e: KeyboardEvent) {
|
|
95
|
+
if (e.key === 'Escape') addMenuOpen = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleClickOutside(e: MouseEvent) {
|
|
99
|
+
if (!addMenuOpen) return;
|
|
100
|
+
const t = e.target as Node;
|
|
101
|
+
if (addMenuRef && !addMenuRef.contains(t)) addMenuOpen = false;
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<svelte:window onkeydown={handleKey} onmousedown={handleClickOutside} />
|
|
106
|
+
|
|
107
|
+
<div class={cn('flex flex-wrap items-center gap-2', className)}>
|
|
108
|
+
{#each activeGroups as group (group.key)}
|
|
109
|
+
<div class="inline-flex items-center gap-1">
|
|
110
|
+
<FilterPopover
|
|
111
|
+
filterGroups={[group]}
|
|
112
|
+
bind:selections
|
|
113
|
+
{onfilterchange}
|
|
114
|
+
{defaults}
|
|
115
|
+
showClearAll={false}
|
|
116
|
+
{testId}
|
|
117
|
+
/>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onclick={() => removeGroup(group)}
|
|
121
|
+
class="text-default-400 hover:bg-default-100 hover:text-danger-500 flex size-5 shrink-0 cursor-pointer items-center justify-center rounded"
|
|
122
|
+
aria-label="Remove {group.label} filter"
|
|
123
|
+
title="Remove {group.label}"
|
|
124
|
+
>
|
|
125
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
126
|
+
<path
|
|
127
|
+
d="M3 3l6 6M9 3l-6 6"
|
|
128
|
+
stroke="currentColor"
|
|
129
|
+
stroke-width="1.5"
|
|
130
|
+
stroke-linecap="round"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
{/each}
|
|
136
|
+
|
|
137
|
+
{#if availableGroups.length > 0}
|
|
138
|
+
<div class="relative" bind:this={addMenuRef}>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onclick={() => (addMenuOpen = !addMenuOpen)}
|
|
142
|
+
class={cn(
|
|
143
|
+
'inline-flex cursor-pointer items-center gap-1 rounded-full border border-dashed px-3 py-1 text-xs font-medium',
|
|
144
|
+
'border-default-300 text-default-600 hover:border-primary-300 hover:text-primary-600 hover:bg-primary-50 transition-colors'
|
|
145
|
+
)}
|
|
146
|
+
aria-haspopup="menu"
|
|
147
|
+
aria-expanded={addMenuOpen}
|
|
148
|
+
>
|
|
149
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
150
|
+
<path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
151
|
+
</svg>
|
|
152
|
+
Add filter
|
|
153
|
+
</button>
|
|
154
|
+
{#if addMenuOpen}
|
|
155
|
+
<div
|
|
156
|
+
role="menu"
|
|
157
|
+
transition:fly={{ y: -4, duration: 180, easing: quintOut }}
|
|
158
|
+
class="border-default-200 absolute z-50 mt-1 min-w-[10rem] rounded-lg border bg-white p-1 shadow-lg"
|
|
159
|
+
>
|
|
160
|
+
{#each availableGroups as group (group.key)}
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
role="menuitem"
|
|
164
|
+
onclick={() => addGroup(group)}
|
|
165
|
+
class="text-default-700 hover:bg-default-50 flex w-full cursor-pointer items-center rounded-md px-3 py-1.5 text-xs"
|
|
166
|
+
>
|
|
167
|
+
{group.label}
|
|
168
|
+
</button>
|
|
169
|
+
{/each}
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
174
|
+
|
|
175
|
+
{#if showClearAll && activeGroups.length > 0}
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onclick={clearAll}
|
|
179
|
+
class="text-default-500 hover:bg-default-100 hover:text-default-700 ml-1 cursor-pointer rounded-md px-2 py-1 text-xs font-medium"
|
|
180
|
+
>
|
|
181
|
+
{clearAllLabel}
|
|
182
|
+
</button>
|
|
183
|
+
{/if}
|
|
184
|
+
</div>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompactFiltersProps } from '../index.js';
|
|
2
|
+
declare const FilterBar: import("svelte").Component<Pick<CompactFiltersProps, "class" | "testId" | "filterGroups" | "selections" | "onfilterchange" | "defaults" | "showClearAll" | "clearAllLabel">, {}, "selections">;
|
|
3
|
+
type FilterBar = ReturnType<typeof FilterBar>;
|
|
4
|
+
export default FilterBar;
|