@makolabs/ripple 2.5.9 → 3.0.1
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 +244 -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 -108
- package/dist/elements/dropdown/select.js +38 -47
- 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 +254 -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/Checkbox.svelte +24 -9
- package/dist/forms/DateRange.svelte +23 -6
- package/dist/forms/Input.svelte +19 -19
- package/dist/forms/MarketSelector.svelte +9 -4
- package/dist/forms/NumberInput.svelte +14 -18
- package/dist/forms/RadioGroup.svelte +127 -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 +44 -14
- package/dist/forms/Textarea.svelte +121 -0
- package/dist/forms/Textarea.svelte.d.ts +4 -0
- package/dist/forms/Toggle.svelte +30 -22
- package/dist/forms/calendar/Calendar.svelte +315 -0
- package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/forms/calendar/calendar-types.d.ts +54 -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 +141 -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-size.d.ts +37 -0
- package/dist/forms/form-size.js +67 -0
- package/dist/forms/form-types.d.ts +430 -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 +25 -13
- package/dist/forms/slider.d.ts +3 -3
- package/dist/forms/slider.js +37 -30
- 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,346 @@
|
|
|
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 DateRange from '../forms/DateRange.svelte';
|
|
6
|
+
import { defaultDatePresets, toIsoDate, fromIsoDate } from './date-presets.js';
|
|
7
|
+
import type {
|
|
8
|
+
FilterGroup,
|
|
9
|
+
FilterSelectionValue,
|
|
10
|
+
DateRangeValue,
|
|
11
|
+
CompactFiltersProps
|
|
12
|
+
} from '../index.js';
|
|
13
|
+
|
|
14
|
+
// Reuse CompactFiltersProps shape for consistency — same `filterGroups`,
|
|
15
|
+
// `selections`, `onfilterchange`, `defaults`, `showClearAll`, `clearAllLabel`.
|
|
16
|
+
let {
|
|
17
|
+
filterGroups = [],
|
|
18
|
+
selections = $bindable<Record<string, FilterSelectionValue>>({}),
|
|
19
|
+
onfilterchange,
|
|
20
|
+
defaults,
|
|
21
|
+
showClearAll = false,
|
|
22
|
+
clearAllLabel = 'Clear',
|
|
23
|
+
class: className,
|
|
24
|
+
testId
|
|
25
|
+
}: Pick<
|
|
26
|
+
CompactFiltersProps,
|
|
27
|
+
| 'filterGroups'
|
|
28
|
+
| 'selections'
|
|
29
|
+
| 'onfilterchange'
|
|
30
|
+
| 'defaults'
|
|
31
|
+
| 'showClearAll'
|
|
32
|
+
| 'clearAllLabel'
|
|
33
|
+
| 'class'
|
|
34
|
+
| 'testId'
|
|
35
|
+
> = $props();
|
|
36
|
+
|
|
37
|
+
let openKey = $state<string | null>(null);
|
|
38
|
+
let popoverRefs = $state<Record<string, HTMLDivElement | undefined>>({});
|
|
39
|
+
|
|
40
|
+
function isDateRangeGroup(group: FilterGroup): boolean {
|
|
41
|
+
return !!group.dateRange;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function dateRangeConfig(group: FilterGroup) {
|
|
45
|
+
const cfg = group.dateRange;
|
|
46
|
+
if (cfg && typeof cfg === 'object') {
|
|
47
|
+
return {
|
|
48
|
+
presets: cfg.presets ?? defaultDatePresets,
|
|
49
|
+
minDate: cfg.minDate,
|
|
50
|
+
maxDate: cfg.maxDate
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { presets: defaultDatePresets, minDate: undefined, maxDate: undefined };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asDateRange(v: FilterSelectionValue | undefined): DateRangeValue | null {
|
|
57
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) return v as DateRangeValue;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getSelected(group: FilterGroup): FilterSelectionValue {
|
|
62
|
+
const v = selections[group.key];
|
|
63
|
+
if (v !== undefined) return v;
|
|
64
|
+
if (isDateRangeGroup(group)) return null;
|
|
65
|
+
return group.multiple ? [] : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isSelected(group: FilterGroup, value: string): boolean {
|
|
69
|
+
const current = getSelected(group);
|
|
70
|
+
return Array.isArray(current) ? current.includes(value) : current === value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getSelectedLabel(group: FilterGroup, current: FilterSelectionValue): string {
|
|
74
|
+
if (isDateRangeGroup(group)) {
|
|
75
|
+
const dr = asDateRange(current);
|
|
76
|
+
if (!dr) return 'Any';
|
|
77
|
+
if (dr.preset) {
|
|
78
|
+
const cfg = dateRangeConfig(group);
|
|
79
|
+
const preset = cfg.presets.find((p) => p.value === dr.preset);
|
|
80
|
+
if (preset) return preset.label;
|
|
81
|
+
}
|
|
82
|
+
return `${dr.from} → ${dr.to}`;
|
|
83
|
+
}
|
|
84
|
+
const tabs = group.tabs ?? [];
|
|
85
|
+
if (Array.isArray(current)) {
|
|
86
|
+
if (current.length === 0) return 'Any';
|
|
87
|
+
if (current.length === 1) {
|
|
88
|
+
return tabs.find((t) => t.value === current[0])?.label ?? current[0];
|
|
89
|
+
}
|
|
90
|
+
return `${current.length} selected`;
|
|
91
|
+
}
|
|
92
|
+
if (!current || typeof current !== 'string') return 'Any';
|
|
93
|
+
return tabs.find((t) => t.value === current)?.label ?? current;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handlePresetPick(
|
|
97
|
+
group: FilterGroup,
|
|
98
|
+
preset: { value: string; range: () => { from: Date; to: Date } }
|
|
99
|
+
) {
|
|
100
|
+
const { from, to } = preset.range();
|
|
101
|
+
const next: DateRangeValue = {
|
|
102
|
+
from: toIsoDate(from),
|
|
103
|
+
to: toIsoDate(to),
|
|
104
|
+
preset: preset.value
|
|
105
|
+
};
|
|
106
|
+
selections = { ...selections, [group.key]: next };
|
|
107
|
+
onfilterchange?.(group.key, next);
|
|
108
|
+
openKey = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handleCustomRange(group: FilterGroup, from: Date | undefined, to: Date | undefined) {
|
|
112
|
+
if (!from || !to) return;
|
|
113
|
+
const next: DateRangeValue = { from: toIsoDate(from), to: toIsoDate(to) };
|
|
114
|
+
selections = { ...selections, [group.key]: next };
|
|
115
|
+
onfilterchange?.(group.key, next);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function clearDateRange(group: FilterGroup) {
|
|
119
|
+
selections = { ...selections, [group.key]: null };
|
|
120
|
+
onfilterchange?.(group.key, null);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handleSelect(group: FilterGroup, value: string) {
|
|
124
|
+
let next: FilterSelectionValue;
|
|
125
|
+
if (group.multiple) {
|
|
126
|
+
const current = getSelected(group);
|
|
127
|
+
const arr: string[] = Array.isArray(current)
|
|
128
|
+
? (current as string[])
|
|
129
|
+
: typeof current === 'string' && current
|
|
130
|
+
? [current]
|
|
131
|
+
: [];
|
|
132
|
+
next = arr.includes(value) ? arr.filter((v) => v !== value) : [...arr, value];
|
|
133
|
+
} else {
|
|
134
|
+
next = value;
|
|
135
|
+
}
|
|
136
|
+
selections = { ...selections, [group.key]: next };
|
|
137
|
+
onfilterchange?.(group.key, next);
|
|
138
|
+
if (!group.multiple) openKey = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toggleGroup(key: string) {
|
|
142
|
+
openKey = openKey === key ? null : key;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function closeAll() {
|
|
146
|
+
openKey = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function handleKey(e: KeyboardEvent) {
|
|
150
|
+
if (e.key === 'Escape') closeAll();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleClickOutside(e: MouseEvent) {
|
|
154
|
+
if (!openKey) return;
|
|
155
|
+
const ref = popoverRefs[openKey];
|
|
156
|
+
const target = e.target as Node;
|
|
157
|
+
if (ref && !ref.contains(target)) closeAll();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isDirty = $derived.by(() => {
|
|
161
|
+
if (!defaults) return Object.keys(selections).length > 0;
|
|
162
|
+
for (const key of new Set([...Object.keys(defaults), ...Object.keys(selections)])) {
|
|
163
|
+
if (JSON.stringify(selections[key] ?? null) !== JSON.stringify(defaults[key] ?? null))
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function clearAll() {
|
|
170
|
+
selections = defaults ? { ...defaults } : {};
|
|
171
|
+
for (const group of filterGroups) {
|
|
172
|
+
onfilterchange?.(group.key, getSelected(group));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasActiveSelection(group: FilterGroup): boolean {
|
|
177
|
+
const current = getSelected(group);
|
|
178
|
+
const def = defaults?.[group.key];
|
|
179
|
+
if (isDateRangeGroup(group)) return !!asDateRange(current);
|
|
180
|
+
if (Array.isArray(current)) return current.length > 0;
|
|
181
|
+
if (def !== undefined) return current !== def;
|
|
182
|
+
return !!current;
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<svelte:window onkeydown={handleKey} onmousedown={handleClickOutside} />
|
|
187
|
+
|
|
188
|
+
<div
|
|
189
|
+
class={cn('flex flex-wrap items-center gap-2', className)}
|
|
190
|
+
data-testid={testId ? `${testId}-filter-popover` : undefined}
|
|
191
|
+
>
|
|
192
|
+
{#each filterGroups as group (group.key)}
|
|
193
|
+
{@const selectedValue = getSelected(group)}
|
|
194
|
+
{@const active = hasActiveSelection(group)}
|
|
195
|
+
<div class="relative" bind:this={popoverRefs[group.key]}>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onclick={() => toggleGroup(group.key)}
|
|
199
|
+
class={cn(
|
|
200
|
+
'inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors',
|
|
201
|
+
active
|
|
202
|
+
? 'bg-primary-50 text-primary-700 border-primary-200 hover:bg-primary-100'
|
|
203
|
+
: 'border-default-200 text-default-700 hover:bg-default-50 bg-white',
|
|
204
|
+
openKey === group.key && 'ring-primary-300 ring-2'
|
|
205
|
+
)}
|
|
206
|
+
aria-expanded={openKey === group.key}
|
|
207
|
+
aria-haspopup="listbox"
|
|
208
|
+
>
|
|
209
|
+
<span class="text-default-500">{group.label}:</span>
|
|
210
|
+
<span>{getSelectedLabel(group, selectedValue)}</span>
|
|
211
|
+
<svg
|
|
212
|
+
class={cn('size-3 transition-transform', openKey === group.key && 'rotate-180')}
|
|
213
|
+
viewBox="0 0 20 20"
|
|
214
|
+
fill="currentColor"
|
|
215
|
+
aria-hidden="true"
|
|
216
|
+
>
|
|
217
|
+
<path
|
|
218
|
+
fill-rule="evenodd"
|
|
219
|
+
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"
|
|
220
|
+
clip-rule="evenodd"
|
|
221
|
+
/>
|
|
222
|
+
</svg>
|
|
223
|
+
</button>
|
|
224
|
+
|
|
225
|
+
{#if openKey === group.key && isDateRangeGroup(group)}
|
|
226
|
+
{@const drCfg = dateRangeConfig(group)}
|
|
227
|
+
{@const drValue = asDateRange(selectedValue)}
|
|
228
|
+
<div
|
|
229
|
+
role="dialog"
|
|
230
|
+
transition:fly={{ y: -4, duration: 180, easing: quintOut }}
|
|
231
|
+
class="border-default-200 absolute z-50 mt-1 min-w-[18rem] rounded-lg border bg-white p-3 shadow-lg"
|
|
232
|
+
>
|
|
233
|
+
<div class="mb-2 flex flex-col gap-1">
|
|
234
|
+
{#each drCfg.presets as preset (preset.value)}
|
|
235
|
+
{@const presetSelected = drValue?.preset === preset.value}
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
onclick={() => handlePresetPick(group, preset)}
|
|
239
|
+
class={cn(
|
|
240
|
+
'flex w-full cursor-pointer items-center rounded-md px-3 py-1.5 text-left text-xs',
|
|
241
|
+
presetSelected
|
|
242
|
+
? 'bg-primary-50 text-primary-700 font-medium'
|
|
243
|
+
: 'text-default-700 hover:bg-default-50'
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
{preset.label}
|
|
247
|
+
</button>
|
|
248
|
+
{/each}
|
|
249
|
+
</div>
|
|
250
|
+
<div class="border-default-100 mt-2 border-t pt-2">
|
|
251
|
+
<div class="text-default-500 mb-1 text-[10px] font-semibold uppercase">Custom</div>
|
|
252
|
+
<DateRange
|
|
253
|
+
startDate={drValue?.from ? fromIsoDate(drValue.from) : undefined}
|
|
254
|
+
endDate={drValue?.to ? fromIsoDate(drValue.to) : undefined}
|
|
255
|
+
onselect={({ startDate, endDate }) => handleCustomRange(group, startDate, endDate)}
|
|
256
|
+
minDate={drCfg.minDate}
|
|
257
|
+
maxDate={drCfg.maxDate}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
{#if drValue}
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onclick={() => clearDateRange(group)}
|
|
264
|
+
class="text-default-500 hover:text-default-700 mt-2 w-full cursor-pointer rounded-md text-center text-xs underline"
|
|
265
|
+
>
|
|
266
|
+
Clear range
|
|
267
|
+
</button>
|
|
268
|
+
{/if}
|
|
269
|
+
</div>
|
|
270
|
+
{:else if openKey === group.key}
|
|
271
|
+
<div
|
|
272
|
+
role="listbox"
|
|
273
|
+
transition:fly={{ y: -4, duration: 180, easing: quintOut }}
|
|
274
|
+
class="border-default-200 absolute z-50 mt-1 min-w-[10rem] rounded-lg border bg-white p-1 shadow-lg"
|
|
275
|
+
>
|
|
276
|
+
{#each group.tabs ?? [] as tab (tab.value)}
|
|
277
|
+
{@const tabSelected = isSelected(group, tab.value)}
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
role="option"
|
|
281
|
+
aria-selected={tabSelected}
|
|
282
|
+
onclick={() => handleSelect(group, tab.value)}
|
|
283
|
+
class={cn(
|
|
284
|
+
'flex w-full cursor-pointer items-center justify-between rounded-md px-3 py-1.5 text-xs whitespace-nowrap',
|
|
285
|
+
tabSelected
|
|
286
|
+
? 'bg-primary-50 text-primary-700 font-medium'
|
|
287
|
+
: 'text-default-700 hover:bg-default-50'
|
|
288
|
+
)}
|
|
289
|
+
>
|
|
290
|
+
<span class="flex items-center gap-2">
|
|
291
|
+
{#if group.multiple}
|
|
292
|
+
<span
|
|
293
|
+
class={cn(
|
|
294
|
+
'flex size-4 shrink-0 items-center justify-center rounded border',
|
|
295
|
+
tabSelected
|
|
296
|
+
? 'bg-primary-600 border-primary-600'
|
|
297
|
+
: 'border-default-300 bg-white'
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{#if tabSelected}
|
|
301
|
+
<svg
|
|
302
|
+
class="size-3 text-white"
|
|
303
|
+
viewBox="0 0 12 12"
|
|
304
|
+
fill="none"
|
|
305
|
+
aria-hidden="true"
|
|
306
|
+
>
|
|
307
|
+
<path
|
|
308
|
+
d="M2 6l3 3 5-6"
|
|
309
|
+
stroke="currentColor"
|
|
310
|
+
stroke-width="2"
|
|
311
|
+
stroke-linecap="round"
|
|
312
|
+
stroke-linejoin="round"
|
|
313
|
+
/>
|
|
314
|
+
</svg>
|
|
315
|
+
{/if}
|
|
316
|
+
</span>
|
|
317
|
+
{/if}
|
|
318
|
+
<span>{tab.label}</span>
|
|
319
|
+
</span>
|
|
320
|
+
{#if tab.count !== undefined}
|
|
321
|
+
<span
|
|
322
|
+
class={cn(
|
|
323
|
+
'ml-4 text-[10px]',
|
|
324
|
+
tabSelected ? 'text-primary-500' : 'text-default-400'
|
|
325
|
+
)}
|
|
326
|
+
>
|
|
327
|
+
{tab.count}
|
|
328
|
+
</span>
|
|
329
|
+
{/if}
|
|
330
|
+
</button>
|
|
331
|
+
{/each}
|
|
332
|
+
</div>
|
|
333
|
+
{/if}
|
|
334
|
+
</div>
|
|
335
|
+
{/each}
|
|
336
|
+
|
|
337
|
+
{#if showClearAll && isDirty}
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
onclick={clearAll}
|
|
341
|
+
class="text-default-500 hover:bg-default-100 hover:text-default-700 cursor-pointer rounded-md px-2 py-1 text-xs font-medium"
|
|
342
|
+
>
|
|
343
|
+
{clearAllLabel}
|
|
344
|
+
</button>
|
|
345
|
+
{/if}
|
|
346
|
+
</div>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CompactFiltersProps } from '../index.js';
|
|
2
|
+
declare const FilterPopover: import("svelte").Component<Pick<CompactFiltersProps, "class" | "testId" | "filterGroups" | "selections" | "onfilterchange" | "defaults" | "showClearAll" | "clearAllLabel">, {}, "selections">;
|
|
3
|
+
type FilterPopover = ReturnType<typeof FilterPopover>;
|
|
4
|
+
export default FilterPopover;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DatePreset } from './filter-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Curated list of common date-range shortcuts. Used by default when a
|
|
4
|
+
* FilterGroup has `dateRange: true` without a custom preset list.
|
|
5
|
+
*/
|
|
6
|
+
export declare const defaultDatePresets: DatePreset[];
|
|
7
|
+
/** Format a `Date` as an ISO date string (YYYY-MM-DD) in local time. */
|
|
8
|
+
export declare function toIsoDate(d: Date): string;
|
|
9
|
+
/**
|
|
10
|
+
* Parse an ISO date string (YYYY-MM-DD) back to a local-time `Date`.
|
|
11
|
+
* Throws if the input doesn't match `YYYY-MM-DD` exactly or if the
|
|
12
|
+
* resulting date is invalid — silent normalization of bad input hides
|
|
13
|
+
* bugs in URL/filter-state handling.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fromIsoDate(s: string): Date;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start-of-day / end-of-day helpers so presets produce deterministic ranges.
|
|
3
|
+
* Kept as a tiny utility rather than pulling in a date lib.
|
|
4
|
+
*/
|
|
5
|
+
function startOfDay(d) {
|
|
6
|
+
const x = new Date(d);
|
|
7
|
+
x.setHours(0, 0, 0, 0);
|
|
8
|
+
return x;
|
|
9
|
+
}
|
|
10
|
+
function endOfDay(d) {
|
|
11
|
+
const x = new Date(d);
|
|
12
|
+
x.setHours(23, 59, 59, 999);
|
|
13
|
+
return x;
|
|
14
|
+
}
|
|
15
|
+
function addDays(d, days) {
|
|
16
|
+
const x = new Date(d);
|
|
17
|
+
x.setDate(x.getDate() + days);
|
|
18
|
+
return x;
|
|
19
|
+
}
|
|
20
|
+
function startOfMonth(d) {
|
|
21
|
+
const x = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
22
|
+
return startOfDay(x);
|
|
23
|
+
}
|
|
24
|
+
function endOfMonth(d) {
|
|
25
|
+
const x = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
|
26
|
+
return endOfDay(x);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Curated list of common date-range shortcuts. Used by default when a
|
|
30
|
+
* FilterGroup has `dateRange: true` without a custom preset list.
|
|
31
|
+
*/
|
|
32
|
+
export const defaultDatePresets = [
|
|
33
|
+
{
|
|
34
|
+
value: 'today',
|
|
35
|
+
label: 'Today',
|
|
36
|
+
range: () => {
|
|
37
|
+
const now = new Date();
|
|
38
|
+
return { from: startOfDay(now), to: endOfDay(now) };
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: 'yesterday',
|
|
43
|
+
label: 'Yesterday',
|
|
44
|
+
range: () => {
|
|
45
|
+
const y = addDays(new Date(), -1);
|
|
46
|
+
return { from: startOfDay(y), to: endOfDay(y) };
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'last-7',
|
|
51
|
+
label: 'Last 7 days',
|
|
52
|
+
range: () => {
|
|
53
|
+
const now = new Date();
|
|
54
|
+
return { from: startOfDay(addDays(now, -6)), to: endOfDay(now) };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: 'last-30',
|
|
59
|
+
label: 'Last 30 days',
|
|
60
|
+
range: () => {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
return { from: startOfDay(addDays(now, -29)), to: endOfDay(now) };
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: 'this-month',
|
|
67
|
+
label: 'This month',
|
|
68
|
+
range: () => {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
return { from: startOfMonth(now), to: endOfDay(now) };
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
value: 'last-month',
|
|
75
|
+
label: 'Last month',
|
|
76
|
+
range: () => {
|
|
77
|
+
const now = new Date();
|
|
78
|
+
const lastMonthAnchor = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
79
|
+
return { from: startOfMonth(lastMonthAnchor), to: endOfMonth(lastMonthAnchor) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
/** Format a `Date` as an ISO date string (YYYY-MM-DD) in local time. */
|
|
84
|
+
export function toIsoDate(d) {
|
|
85
|
+
const y = d.getFullYear();
|
|
86
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
87
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
88
|
+
return `${y}-${m}-${day}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse an ISO date string (YYYY-MM-DD) back to a local-time `Date`.
|
|
92
|
+
* Throws if the input doesn't match `YYYY-MM-DD` exactly or if the
|
|
93
|
+
* resulting date is invalid — silent normalization of bad input hides
|
|
94
|
+
* bugs in URL/filter-state handling.
|
|
95
|
+
*/
|
|
96
|
+
export function fromIsoDate(s) {
|
|
97
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
|
98
|
+
throw new Error(`fromIsoDate: expected "YYYY-MM-DD", got "${s}"`);
|
|
99
|
+
}
|
|
100
|
+
const [y, m, d] = s.split('-').map(Number);
|
|
101
|
+
const date = new Date(y, m - 1, d);
|
|
102
|
+
// Guard against e.g. "2026-02-30" — `new Date` silently rolls over.
|
|
103
|
+
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
|
|
104
|
+
throw new Error(`fromIsoDate: "${s}" is not a real calendar date`);
|
|
105
|
+
}
|
|
106
|
+
return date;
|
|
107
|
+
}
|
|
@@ -3,17 +3,83 @@ import type { Component } from 'svelte';
|
|
|
3
3
|
export type FilterTab = {
|
|
4
4
|
value: string;
|
|
5
5
|
label: string;
|
|
6
|
+
/** Optional count badge rendered next to the label, e.g. "Active (23)". */
|
|
7
|
+
count?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A named date-range shortcut, e.g. "Last 7 days". The `range` function is
|
|
11
|
+
* called when the user picks the preset to produce concrete from/to dates.
|
|
12
|
+
*/
|
|
13
|
+
export type DatePreset = {
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
range: () => {
|
|
17
|
+
from: Date;
|
|
18
|
+
to: Date;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Config for a date-range filter group. Pass `true` for sensible defaults
|
|
23
|
+
* (curated preset list + no min/max), or an object for customization.
|
|
24
|
+
*/
|
|
25
|
+
export type DateRangeConfig = {
|
|
26
|
+
/** Preset buttons shown above the calendar. Defaults to `defaultDatePresets`. */
|
|
27
|
+
presets?: DatePreset[];
|
|
28
|
+
/** Earliest selectable date in the calendar. */
|
|
29
|
+
minDate?: Date;
|
|
30
|
+
/** Latest selectable date in the calendar. */
|
|
31
|
+
maxDate?: Date;
|
|
6
32
|
};
|
|
7
33
|
export type FilterGroup = {
|
|
8
34
|
key: string;
|
|
9
35
|
label: string;
|
|
10
|
-
tabs: FilterTab[];
|
|
11
|
-
selectedValue: string;
|
|
12
|
-
onChange: (value: string) => void;
|
|
13
36
|
minWidth?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Tabs for a tab-based filter group. Mutually exclusive with `dateRange`.
|
|
39
|
+
* One of `tabs` or `dateRange` is required.
|
|
40
|
+
*/
|
|
41
|
+
tabs?: FilterTab[];
|
|
42
|
+
/**
|
|
43
|
+
* Opt into a date-range filter group. `true` uses default presets; pass a
|
|
44
|
+
* `DateRangeConfig` for custom presets / min / max. Mutually exclusive
|
|
45
|
+
* with `tabs`.
|
|
46
|
+
*/
|
|
47
|
+
dateRange?: boolean | DateRangeConfig;
|
|
48
|
+
/**
|
|
49
|
+
* For tab groups: allow multiple simultaneous selections. `selections[key]`
|
|
50
|
+
* becomes `string[]` instead of `string`. Ignored for date-range groups.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
multiple?: boolean;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Date-range selection value. ISO-8601 date strings (YYYY-MM-DD) for easy
|
|
57
|
+
* URL serialization. `preset` is the preset `value` when a preset was picked,
|
|
58
|
+
* so consumers can still render "Last 7 days" instead of the date range.
|
|
59
|
+
*/
|
|
60
|
+
export type DateRangeValue = {
|
|
61
|
+
from: string;
|
|
62
|
+
to: string;
|
|
63
|
+
preset?: string;
|
|
14
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Per-group selection value.
|
|
67
|
+
* - `string` — single-tab selection
|
|
68
|
+
* - `string[]` — multi-tab selection
|
|
69
|
+
* - `DateRangeValue` — date-range group selection
|
|
70
|
+
* - `null` / `undefined` — unset
|
|
71
|
+
*/
|
|
72
|
+
export type FilterSelectionValue = string | string[] | DateRangeValue | null;
|
|
15
73
|
export type CompactFiltersProps = {
|
|
16
74
|
filterGroups: FilterGroup[];
|
|
75
|
+
selections?: Record<string, FilterSelectionValue>;
|
|
76
|
+
onfilterchange?: (key: string, value: FilterSelectionValue) => void;
|
|
77
|
+
defaults?: Record<string, FilterSelectionValue>;
|
|
78
|
+
showClearAll?: boolean;
|
|
79
|
+
clearAllLabel?: string;
|
|
80
|
+
searchQuery?: string;
|
|
81
|
+
searchPlaceholder?: string;
|
|
82
|
+
chipSummary?: boolean;
|
|
17
83
|
isExpanded?: boolean;
|
|
18
84
|
title?: string;
|
|
19
85
|
class?: ClassValue;
|
package/dist/filters/index.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
1
|
export { default as CompactFilters } from './CompactFilters.svelte';
|
|
2
|
+
export { default as FilterPopover } from './FilterPopover.svelte';
|
|
3
|
+
export { default as FilterBar } from './FilterBar.svelte';
|
|
4
|
+
export { syncFiltersToUrl } from './sync-filters-to-url.svelte.js';
|
|
5
|
+
export { defaultDatePresets, toIsoDate, fromIsoDate } from './date-presets.js';
|
|
6
|
+
export type { FilterTab, FilterGroup, FilterSelectionValue, DatePreset, DateRangeConfig, DateRangeValue, CompactFiltersProps } from './filter-types.js';
|
package/dist/filters/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export { default as CompactFilters } from './CompactFilters.svelte';
|
|
2
|
+
export { default as FilterPopover } from './FilterPopover.svelte';
|
|
3
|
+
export { default as FilterBar } from './FilterBar.svelte';
|
|
4
|
+
export { syncFiltersToUrl } from './sync-filters-to-url.svelte.js';
|
|
5
|
+
export { defaultDatePresets, toIsoDate, fromIsoDate } from './date-presets.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FilterSelectionValue } from './filter-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sync a reactive `selections` object to/from URL query params.
|
|
4
|
+
*
|
|
5
|
+
* - On first call: reads current URL params into the setter.
|
|
6
|
+
* - On every subsequent change to `getter()`: writes back to the URL.
|
|
7
|
+
*
|
|
8
|
+
* Designed for use inside a Svelte 5 component with `$effect` semantics — call
|
|
9
|
+
* it at the top level of `<script>` and it will register its own `$effect`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script>
|
|
14
|
+
* import { CompactFilters, syncFiltersToUrl } from '@makolabs/ripple';
|
|
15
|
+
* let selections = $state({ status: 'all' });
|
|
16
|
+
* syncFiltersToUrl(() => selections, (v) => (selections = v));
|
|
17
|
+
* </script>
|
|
18
|
+
*
|
|
19
|
+
* <CompactFilters {filterGroups} bind:selections />
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Environment:
|
|
23
|
+
* - Uses `window.location` and `history.replaceState` — no-ops on the server.
|
|
24
|
+
* - Multi-select values (`string[]`) are serialized as comma-joined strings.
|
|
25
|
+
* - Removes a key from the URL when its value is empty / empty array.
|
|
26
|
+
* - Leaves unrelated URL params untouched.
|
|
27
|
+
*/
|
|
28
|
+
export declare function syncFiltersToUrl(getter: () => Record<string, FilterSelectionValue>, setter: (next: Record<string, FilterSelectionValue>) => void, options?: {
|
|
29
|
+
/** Group keys to manage. If omitted, all keys returned by `getter` are managed. */
|
|
30
|
+
keys?: string[];
|
|
31
|
+
/** Debounce writes by this many ms. @default 150 */
|
|
32
|
+
debounceMs?: number;
|
|
33
|
+
/** Keys whose values should be parsed as arrays (multi-select). */
|
|
34
|
+
arrayKeys?: string[];
|
|
35
|
+
/** Keys whose values are date ranges. Serialized as `<key>_from`, `<key>_to`, `<key>_preset`. */
|
|
36
|
+
dateRangeKeys?: string[];
|
|
37
|
+
}): void;
|