@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.
Files changed (186) 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 +244 -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 -108
  46. package/dist/elements/dropdown/select.js +38 -47
  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 +254 -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/Checkbox.svelte +24 -9
  90. package/dist/forms/DateRange.svelte +23 -6
  91. package/dist/forms/Input.svelte +19 -19
  92. package/dist/forms/MarketSelector.svelte +9 -4
  93. package/dist/forms/NumberInput.svelte +14 -18
  94. package/dist/forms/RadioGroup.svelte +127 -0
  95. package/dist/forms/RadioGroup.svelte.d.ts +4 -0
  96. package/dist/forms/SegmentedControl.svelte +11 -4
  97. package/dist/forms/Slider.svelte +72 -3
  98. package/dist/forms/Tags.svelte +44 -14
  99. package/dist/forms/Textarea.svelte +121 -0
  100. package/dist/forms/Textarea.svelte.d.ts +4 -0
  101. package/dist/forms/Toggle.svelte +30 -22
  102. package/dist/forms/calendar/Calendar.svelte +315 -0
  103. package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
  104. package/dist/forms/calendar/calendar-types.d.ts +54 -0
  105. package/dist/forms/calendar/calendar-types.js +1 -0
  106. package/dist/forms/calendar/index.d.ts +2 -0
  107. package/dist/forms/calendar/index.js +1 -0
  108. package/dist/forms/date-picker/DatePicker.svelte +141 -0
  109. package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
  110. package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
  111. package/dist/forms/date-picker/date-picker-types.js +1 -0
  112. package/dist/forms/form-size.d.ts +37 -0
  113. package/dist/forms/form-size.js +67 -0
  114. package/dist/forms/form-types.d.ts +430 -6
  115. package/dist/forms/market/market-selector-types.d.ts +52 -1
  116. package/dist/forms/segmented-control.d.ts +5 -2
  117. package/dist/forms/segmented-control.js +25 -13
  118. package/dist/forms/slider.d.ts +3 -3
  119. package/dist/forms/slider.js +37 -30
  120. package/dist/funcs/user-management.remote.js +1 -1
  121. package/dist/header/Breadcrumbs.svelte +4 -20
  122. package/dist/header/PageHeader.svelte +6 -14
  123. package/dist/header/breadcrumbs.d.ts +3 -11
  124. package/dist/header/breadcrumbs.js +10 -5
  125. package/dist/header/header-types.d.ts +62 -11
  126. package/dist/index.d.ts +35 -9
  127. package/dist/index.js +24 -4
  128. package/dist/layout/activity-list/ActivityList.svelte +13 -7
  129. package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
  130. package/dist/layout/card/Card.svelte +12 -15
  131. package/dist/layout/card/MetricCard.svelte +50 -32
  132. package/dist/layout/card/card-types.d.ts +114 -4
  133. package/dist/layout/navbar/navbar-types.d.ts +48 -0
  134. package/dist/layout/navbar/navbar.d.ts +3 -3
  135. package/dist/layout/navbar/navbar.js +2 -2
  136. package/dist/layout/sidebar/Sidebar.svelte +87 -11
  137. package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
  138. package/dist/layout/stepper/Stepper.svelte +288 -0
  139. package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
  140. package/dist/layout/stepper/stepper-types.d.ts +80 -0
  141. package/dist/layout/stepper/stepper-types.js +1 -0
  142. package/dist/layout/table/Table.svelte +91 -85
  143. package/dist/layout/table/table-types.d.ts +148 -24
  144. package/dist/layout/table/table.d.ts +3 -3
  145. package/dist/layout/table/table.js +2 -2
  146. package/dist/layout/tabs/Tab.svelte +6 -2
  147. package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
  148. package/dist/layout/tabs/TabGroup.svelte +9 -2
  149. package/dist/layout/tabs/tabs-types.d.ts +63 -0
  150. package/dist/layout/tabs/tabs.d.ts +3 -3
  151. package/dist/layout/tabs/tabs.js +12 -6
  152. package/dist/modal/ConfirmDialog.svelte +65 -0
  153. package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
  154. package/dist/modal/Modal.svelte +6 -26
  155. package/dist/modal/confirm-dialog-types.d.ts +39 -0
  156. package/dist/modal/confirm-dialog-types.js +1 -0
  157. package/dist/modal/modal-types.d.ts +51 -12
  158. package/dist/modal/modal.d.ts +3 -3
  159. package/dist/modal/modal.js +3 -3
  160. package/dist/pipeline/Pipeline.svelte +8 -3
  161. package/dist/pipeline/pipeline-types.d.ts +55 -3
  162. package/dist/pipeline/pipeline.d.ts +18 -3
  163. package/dist/pipeline/pipeline.js +7 -2
  164. package/dist/server/s3.d.ts +35 -3
  165. package/dist/sonner/Toaster.svelte +29 -0
  166. package/dist/sonner/Toaster.svelte.d.ts +4 -0
  167. package/dist/sonner/index.d.ts +21 -0
  168. package/dist/sonner/index.js +20 -0
  169. package/dist/user-management/UserManagement.svelte +22 -16
  170. package/dist/user-management/UserModal.svelte +10 -7
  171. package/dist/user-management/UserTable.svelte +16 -17
  172. package/dist/user-management/UserViewModal.svelte +11 -11
  173. package/dist/user-management/user-management-types.d.ts +118 -31
  174. package/dist/variants.d.ts +1 -1
  175. package/dist/variants.js +1 -1
  176. package/package.json +7 -4
  177. package/dist/config/ai.d.ts +0 -13
  178. package/dist/config/ai.js +0 -44
  179. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
  180. package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
  181. package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
  182. package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
  183. package/dist/helper/deprecation.d.ts +0 -14
  184. package/dist/helper/deprecation.js +0 -24
  185. package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
  186. 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 { FilterTab, CompactFiltersProps } from '../index.js';
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
- // Toggle expanded state
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
- // Helper to get the label of the selected filter
22
- function getSelectedLabel(tabs: FilterTab[], selectedValue: string): string {
23
- const tab = tabs.find((tab) => tab.value === selectedValue);
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
- <button
75
- onclick={toggleExpanded}
76
- class="mb-2 flex min-w-full cursor-pointer items-center justify-between"
77
- >
78
- <div class="flex items-center gap-2">
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
- </div>
88
- <div
89
- class="text-default-500 hover:bg-default-100 hover:text-default-700 rounded-md p-1"
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
- <span>{@render DefaultChevronUp()}</span>
252
+ {@render DefaultChevronUp()}
94
253
  {:else}
95
- <span>{@render DefaultChevronDown()}</span>
254
+ {@render DefaultChevronDown()}
96
255
  {/if}
97
- </div>
98
- </button>
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
- {#each filterGroups as group (group.key)}
104
- {#if group.tabs.length > 0}
105
- <div
106
- class="bg-primary-50 text-primary-700 border-primary-200 flex items-center gap-1 rounded-full border px-3 py-1 text-xs"
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">{group.label}:</span>
109
- {getSelectedLabel(group.tabs, group.selectedValue)}
110
- </div>
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
- {/each}
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
- onclick={() => group.onChange(tab.value)}
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
- group.selectedValue === tab.value
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;