@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.
Files changed (183) hide show
  1. package/README.md +403 -497
  2. package/dist/adapters/storage/S3Adapter.d.ts +49 -1
  3. package/dist/adapters/storage/S3Adapter.js +38 -1
  4. package/dist/adapters/storage/types.d.ts +20 -0
  5. package/dist/ai/AIChatInterface.svelte +2 -1
  6. package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
  7. package/dist/ai/CodeRenderer.svelte +7 -2
  8. package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
  9. package/dist/ai/ComposeDropdown.svelte +1 -1
  10. package/dist/ai/MessageBox.svelte +3 -3
  11. package/dist/ai/MessageBox.svelte.d.ts +3 -2
  12. package/dist/ai/ThinkingDisplay.svelte +4 -3
  13. package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
  14. package/dist/ai/ai-types.d.ts +55 -1
  15. package/dist/button/Button.svelte +5 -5
  16. package/dist/button/button-types.d.ts +49 -4
  17. package/dist/button/button.d.ts +9 -9
  18. package/dist/button/button.js +6 -6
  19. package/dist/charts/Chart.svelte +8 -16
  20. package/dist/charts/chart-types.d.ts +78 -1
  21. package/dist/drawer/Drawer.svelte +6 -26
  22. package/dist/drawer/drawer-types.d.ts +33 -12
  23. package/dist/drawer/drawer.d.ts +3 -3
  24. package/dist/drawer/drawer.js +1 -1
  25. package/dist/elements/accordion/Accordion.svelte +6 -17
  26. package/dist/elements/accordion/accordion-types.d.ts +53 -6
  27. package/dist/elements/alert/Alert.svelte +3 -0
  28. package/dist/elements/badge/Badge.svelte +1 -1
  29. package/dist/elements/badge/badge-types.d.ts +22 -0
  30. package/dist/elements/badge/badge.d.ts +3 -3
  31. package/dist/elements/badge/badge.js +1 -1
  32. package/dist/elements/combobox/ComboBox.svelte +247 -0
  33. package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
  34. package/dist/elements/combobox/combobox-types.d.ts +41 -0
  35. package/dist/elements/combobox/combobox-types.js +1 -0
  36. package/dist/elements/context-menu/ContextMenu.svelte +137 -0
  37. package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
  38. package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
  39. package/dist/elements/context-menu/context-menu-types.js +1 -0
  40. package/dist/elements/dropdown/Dropdown.svelte +1 -1
  41. package/dist/elements/dropdown/Select.svelte +4 -1
  42. package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
  43. package/dist/elements/dropdown/dropdown.d.ts +3 -3
  44. package/dist/elements/dropdown/dropdown.js +2 -2
  45. package/dist/elements/dropdown/select.d.ts +3 -3
  46. package/dist/elements/dropdown/select.js +2 -2
  47. package/dist/elements/empty-state/EmptyState.svelte +1 -1
  48. package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
  49. package/dist/elements/empty-state/empty-state.d.ts +3 -3
  50. package/dist/elements/empty-state/empty-state.js +2 -2
  51. package/dist/elements/file-upload/FileUpload.svelte +5 -0
  52. package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
  53. package/dist/elements/pagination/Pagination.svelte +53 -21
  54. package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
  55. package/dist/elements/popover/Popover.svelte +234 -0
  56. package/dist/elements/popover/Popover.svelte.d.ts +4 -0
  57. package/dist/elements/popover/index.d.ts +2 -0
  58. package/dist/elements/popover/index.js +1 -0
  59. package/dist/elements/popover/popover-types.d.ts +60 -0
  60. package/dist/elements/popover/popover-types.js +1 -0
  61. package/dist/elements/progress/Progress.svelte +32 -7
  62. package/dist/elements/progress/progress-types.d.ts +48 -1
  63. package/dist/elements/skeleton/Skeleton.svelte +56 -0
  64. package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
  65. package/dist/elements/skeleton/index.d.ts +2 -0
  66. package/dist/elements/skeleton/index.js +1 -0
  67. package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
  68. package/dist/elements/skeleton/skeleton-types.js +1 -0
  69. package/dist/elements/spinner/Spinner.svelte +1 -1
  70. package/dist/elements/spinner/spinner-types.d.ts +20 -0
  71. package/dist/elements/spinner/spinner.d.ts +3 -3
  72. package/dist/elements/spinner/spinner.js +2 -2
  73. package/dist/elements/tooltip/Tooltip.svelte +108 -11
  74. package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
  75. package/dist/file-browser/FileBrowser.svelte +21 -12
  76. package/dist/filters/CompactFilters.svelte +221 -33
  77. package/dist/filters/CompactFilters.svelte.d.ts +1 -1
  78. package/dist/filters/FilterBar.svelte +184 -0
  79. package/dist/filters/FilterBar.svelte.d.ts +4 -0
  80. package/dist/filters/FilterPopover.svelte +346 -0
  81. package/dist/filters/FilterPopover.svelte.d.ts +4 -0
  82. package/dist/filters/date-presets.d.ts +15 -0
  83. package/dist/filters/date-presets.js +107 -0
  84. package/dist/filters/filter-types.d.ts +69 -3
  85. package/dist/filters/index.d.ts +5 -0
  86. package/dist/filters/index.js +4 -0
  87. package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
  88. package/dist/filters/sync-filters-to-url.svelte.js +114 -0
  89. package/dist/forms/DateRange.svelte +4 -2
  90. package/dist/forms/Input.svelte +2 -2
  91. package/dist/forms/MarketSelector.svelte +8 -3
  92. package/dist/forms/NumberInput.svelte +4 -4
  93. package/dist/forms/RadioGroup.svelte +123 -0
  94. package/dist/forms/RadioGroup.svelte.d.ts +4 -0
  95. package/dist/forms/SegmentedControl.svelte +11 -4
  96. package/dist/forms/Slider.svelte +72 -3
  97. package/dist/forms/Tags.svelte +14 -5
  98. package/dist/forms/Textarea.svelte +126 -0
  99. package/dist/forms/Textarea.svelte.d.ts +4 -0
  100. package/dist/forms/Toggle.svelte +8 -8
  101. package/dist/forms/calendar/Calendar.svelte +218 -0
  102. package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
  103. package/dist/forms/calendar/calendar-types.d.ts +46 -0
  104. package/dist/forms/calendar/calendar-types.js +1 -0
  105. package/dist/forms/calendar/index.d.ts +2 -0
  106. package/dist/forms/calendar/index.js +1 -0
  107. package/dist/forms/date-picker/DatePicker.svelte +144 -0
  108. package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
  109. package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
  110. package/dist/forms/date-picker/date-picker-types.js +1 -0
  111. package/dist/forms/form-types.d.ts +425 -6
  112. package/dist/forms/market/market-selector-types.d.ts +52 -1
  113. package/dist/forms/segmented-control.d.ts +5 -2
  114. package/dist/forms/segmented-control.js +16 -5
  115. package/dist/forms/slider.d.ts +3 -3
  116. package/dist/forms/slider.js +2 -2
  117. package/dist/funcs/user-management.remote.js +1 -1
  118. package/dist/header/Breadcrumbs.svelte +4 -20
  119. package/dist/header/PageHeader.svelte +6 -14
  120. package/dist/header/breadcrumbs.d.ts +3 -11
  121. package/dist/header/breadcrumbs.js +10 -5
  122. package/dist/header/header-types.d.ts +62 -11
  123. package/dist/index.d.ts +35 -9
  124. package/dist/index.js +24 -4
  125. package/dist/layout/activity-list/ActivityList.svelte +13 -7
  126. package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
  127. package/dist/layout/card/Card.svelte +12 -15
  128. package/dist/layout/card/MetricCard.svelte +50 -32
  129. package/dist/layout/card/card-types.d.ts +114 -4
  130. package/dist/layout/navbar/navbar-types.d.ts +48 -0
  131. package/dist/layout/navbar/navbar.d.ts +3 -3
  132. package/dist/layout/navbar/navbar.js +2 -2
  133. package/dist/layout/sidebar/Sidebar.svelte +87 -11
  134. package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
  135. package/dist/layout/stepper/Stepper.svelte +288 -0
  136. package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
  137. package/dist/layout/stepper/stepper-types.d.ts +80 -0
  138. package/dist/layout/stepper/stepper-types.js +1 -0
  139. package/dist/layout/table/Table.svelte +91 -85
  140. package/dist/layout/table/table-types.d.ts +148 -24
  141. package/dist/layout/table/table.d.ts +3 -3
  142. package/dist/layout/table/table.js +2 -2
  143. package/dist/layout/tabs/Tab.svelte +6 -2
  144. package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
  145. package/dist/layout/tabs/TabGroup.svelte +9 -2
  146. package/dist/layout/tabs/tabs-types.d.ts +63 -0
  147. package/dist/layout/tabs/tabs.d.ts +3 -3
  148. package/dist/layout/tabs/tabs.js +12 -6
  149. package/dist/modal/ConfirmDialog.svelte +65 -0
  150. package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
  151. package/dist/modal/Modal.svelte +6 -26
  152. package/dist/modal/confirm-dialog-types.d.ts +39 -0
  153. package/dist/modal/confirm-dialog-types.js +1 -0
  154. package/dist/modal/modal-types.d.ts +51 -12
  155. package/dist/modal/modal.d.ts +3 -3
  156. package/dist/modal/modal.js +3 -3
  157. package/dist/pipeline/Pipeline.svelte +8 -3
  158. package/dist/pipeline/pipeline-types.d.ts +55 -3
  159. package/dist/pipeline/pipeline.d.ts +18 -3
  160. package/dist/pipeline/pipeline.js +7 -2
  161. package/dist/server/s3.d.ts +35 -3
  162. package/dist/sonner/Toaster.svelte +29 -0
  163. package/dist/sonner/Toaster.svelte.d.ts +4 -0
  164. package/dist/sonner/index.d.ts +21 -0
  165. package/dist/sonner/index.js +20 -0
  166. package/dist/user-management/UserManagement.svelte +22 -16
  167. package/dist/user-management/UserModal.svelte +10 -7
  168. package/dist/user-management/UserTable.svelte +16 -17
  169. package/dist/user-management/UserViewModal.svelte +11 -11
  170. package/dist/user-management/user-management-types.d.ts +118 -31
  171. package/dist/variants.d.ts +1 -1
  172. package/dist/variants.js +1 -1
  173. package/package.json +7 -4
  174. package/dist/config/ai.d.ts +0 -13
  175. package/dist/config/ai.js +0 -44
  176. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
  177. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
  178. package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
  179. package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
  180. package/dist/helper/deprecation.d.ts +0 -14
  181. package/dist/helper/deprecation.js +0 -24
  182. package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
  183. 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;
@@ -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';
@@ -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;