@makolabs/ripple 3.0.2 → 3.0.4

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 (37) hide show
  1. package/dist/ai/ai-types.d.ts +7 -0
  2. package/dist/charts/Chart.svelte +18 -37
  3. package/dist/charts/chart-types.d.ts +7 -0
  4. package/dist/drawer/Drawer.svelte +29 -36
  5. package/dist/drawer/drawer.js +2 -2
  6. package/dist/elements/combobox/ComboBox.svelte +6 -6
  7. package/dist/elements/dropdown/Select.svelte +6 -8
  8. package/dist/elements/popover/Popover.svelte +1 -1
  9. package/dist/elements/progress/Progress.svelte +18 -5
  10. package/dist/elements/progress/progress-types.d.ts +8 -0
  11. package/dist/file-browser/FileBrowser.svelte +6 -3
  12. package/dist/filters/CompactFilters.svelte +78 -68
  13. package/dist/filters/filter-types.d.ts +11 -0
  14. package/dist/forms/DateRange.svelte +31 -20
  15. package/dist/forms/NumberInput.svelte +6 -2
  16. package/dist/forms/Tags.svelte +9 -4
  17. package/dist/forms/Toggle.svelte +8 -6
  18. package/dist/forms/calendar/Calendar.svelte +8 -2
  19. package/dist/forms/calendar/calendar-types.d.ts +9 -0
  20. package/dist/forms/date-picker/DatePicker.svelte +8 -1
  21. package/dist/forms/form-types.d.ts +23 -0
  22. package/dist/forms/month-picker/MonthPicker.svelte +12 -3
  23. package/dist/forms/month-picker/month-picker-types.d.ts +7 -0
  24. package/dist/header/Breadcrumbs.svelte +19 -5
  25. package/dist/header/header-types.d.ts +7 -0
  26. package/dist/layout/activity-list/ActivityList.svelte +29 -7
  27. package/dist/layout/activity-list/activity-list-types.d.ts +8 -0
  28. package/dist/layout/card/MetricCard.svelte +34 -8
  29. package/dist/layout/card/RankedCard.svelte +34 -10
  30. package/dist/layout/card/card-types.d.ts +8 -0
  31. package/dist/layout/card/ranked-card.d.ts +10 -0
  32. package/dist/layout/table/Table.svelte +5 -0
  33. package/dist/layout/table/table-types.d.ts +8 -0
  34. package/dist/modal/modal.js +4 -4
  35. package/dist/pipeline/Pipeline.svelte +7 -3
  36. package/dist/pipeline/pipeline-types.d.ts +6 -0
  37. package/package.json +1 -1
@@ -107,6 +107,13 @@ export interface FileBrowserProps {
107
107
  selectAllScope?: 'page' | 'all';
108
108
  /** Tailwind height class for the browser container. @default 'h-[500px]' */
109
109
  height?: string;
110
+ /**
111
+ * Allow clicking into subdirectories. When `false`, folder rows are
112
+ * still shown but clicking them does nothing — useful for flat file
113
+ * lists or when the adapter doesn't support folder traversal.
114
+ * @default true
115
+ */
116
+ allowFolderNavigation?: boolean;
110
117
  /** Additional CSS classes for the outer container. */
111
118
  class?: ClassValue;
112
119
  /**
@@ -42,6 +42,8 @@
42
42
  GraphicComponent
43
43
  ]);
44
44
 
45
+ import { buildTestId } from '../helper/testid.js';
46
+
45
47
  let {
46
48
  data = [],
47
49
  config,
@@ -50,7 +52,8 @@
50
52
  class: className = '',
51
53
  colors = {},
52
54
  onpointclick,
53
- onchartrender
55
+ onchartrender,
56
+ testId
54
57
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
58
  }: ChartProps<any> = $props();
56
59
 
@@ -143,38 +146,6 @@
143
146
  return rgbToHex(r * (1 - amount), g * (1 - amount), b * (1 - amount));
144
147
  }
145
148
 
146
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
- function barGradient(baseHex: string, stacked: boolean): any {
148
- if (stacked) {
149
- // Horizontal sheen — same light direction for all segments
150
- return {
151
- type: 'linear',
152
- x: 0,
153
- y: 0,
154
- x2: 1,
155
- y2: 0,
156
- colorStops: [
157
- { offset: 0, color: baseHex },
158
- { offset: 0.5, color: lighten(baseHex, 0.1) },
159
- { offset: 1, color: baseHex }
160
- ]
161
- };
162
- }
163
- // Standalone bars — top-to-bottom depth
164
- return {
165
- type: 'linear',
166
- x: 0,
167
- y: 0,
168
- x2: 0,
169
- y2: 1,
170
- colorStops: [
171
- { offset: 0, color: lighten(baseHex, 0.2) },
172
- { offset: 0.6, color: baseHex },
173
- { offset: 1, color: darken(baseHex, 0.15) }
174
- ]
175
- };
176
- }
177
-
178
149
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
150
  function lineGradient(baseHex: string): any {
180
151
  return {
@@ -435,9 +406,10 @@
435
406
  color: getColor(seriesConfig.color),
436
407
  emphasis: seriesConfig.emphasis || {
437
408
  focus: 'series',
438
- blurScope: 'global'
409
+ blurScope: 'global',
410
+ itemStyle: { borderRadius: 0 }
439
411
  },
440
- blur: { itemStyle: { opacity: 0.25 }, lineStyle: { opacity: 0.25 } },
412
+ blur: { itemStyle: { opacity: 0.25, borderRadius: 0 }, lineStyle: { opacity: 0.25 } },
441
413
  animation: true,
442
414
  animationDuration: AnimationDuration,
443
415
  ...(axisMarkLine && { markLine: axisMarkLine }),
@@ -475,8 +447,11 @@
475
447
  barGap: '10%',
476
448
  color: getColor(seriesConfig.color),
477
449
  itemStyle: {
478
- color: barGradient(getColor(seriesConfig.color) ?? '#6366f1', !!seriesConfig.stack),
479
- opacity: seriesConfig.opacity ?? 1
450
+ color: getColor(seriesConfig.color) ?? '#6366f1',
451
+ opacity: seriesConfig.opacity ?? 1,
452
+ borderColor: darken(getColor(seriesConfig.color) ?? '#6366f1', 0.15),
453
+ borderWidth: 1,
454
+ borderRadius: [0, 0, 0, 0]
480
455
  }
481
456
  }),
482
457
 
@@ -781,4 +756,10 @@
781
756
  class={cn('advanced-chart', className)}
782
757
  style="height: {height}; width: {width}"
783
758
  bind:this={chartContainer}
759
+ data-testid={buildTestId('chart', undefined, testId)}
784
760
  ></div>
761
+ {#if testId}
762
+ <div data-testid={buildTestId('chart', 'data', testId)} style="display:none" aria-hidden="true">
763
+ {JSON.stringify({ data, series: config.series, xAxis: config.xAxis, yAxis: config.yAxis })}
764
+ </div>
765
+ {/if}
@@ -222,5 +222,12 @@ export interface ChartProps<T> {
222
222
  * access (custom plugins, imperative animation triggers, etc.).
223
223
  */
224
224
  onchartrender?: (event: ChartRenderType<T>) => void;
225
+ /**
226
+ * Test ID prefix. When set, the component emits these selectors:
227
+ * - `{testId}-chart` — root wrapper (around the canvas)
228
+ * - `{testId}-chart-data` — hidden element containing the chart's
229
+ * source data as JSON, so test automation can read values without
230
+ * parsing the canvas. The JSON shape is `{ data, series, xAxis, yAxis }`.
231
+ */
225
232
  testId?: string;
226
233
  }
@@ -37,8 +37,7 @@
37
37
  header: headerVClass,
38
38
  body,
39
39
  footer: footerVClass,
40
- title: titleVClass,
41
- closeButton
40
+ title: titleVClass
42
41
  } = $derived(
43
42
  drawer({
44
43
  open,
@@ -54,7 +53,6 @@
54
53
  const headerClasses = $derived(cn(headerVClass(), headerClass));
55
54
  const bodyClasses = $derived(cn(body(), bodyClass));
56
55
  const titleClasses = $derived(cn(titleVClass(), titleClass));
57
- const closeButtonClasses = $derived(cn(closeButton(), ''));
58
56
  const footerClasses = $derived(cn(footerVClass(), 'mt-auto', footerClass));
59
57
 
60
58
  function handleBackdropClick(e: MouseEvent) {
@@ -181,41 +179,36 @@
181
179
  data-testid={buildTestId('drawer', 'dialog', testId)}
182
180
  >
183
181
  <div class={contentClasses}>
182
+ <!-- Always-visible close button -->
183
+ <button
184
+ type="button"
185
+ class="bg-default-100 text-default-600 hover:bg-default-200 hover:text-default-900 absolute top-2 right-2 z-10 flex size-8 cursor-pointer items-center justify-center rounded-full transition-colors"
186
+ onclick={handleCloseClick}
187
+ aria-label="Close drawer"
188
+ data-testid={buildTestId('drawer', 'close', testId)}
189
+ >
190
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 12 12">
191
+ <path
192
+ fill="currentColor"
193
+ d="m1.897 2.054l.073-.084a.75.75 0 0 1 .976-.073l.084.073L6 4.939l2.97-2.97a.75.75 0 1 1 1.06 1.061L7.061 6l2.97 2.97a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L6 7.061l-2.97 2.97A.75.75 0 1 1 1.97 8.97L4.939 6l-2.97-2.97a.75.75 0 0 1-.072-.976l.073-.084z"
194
+ />
195
+ </svg>
196
+ </button>
197
+
184
198
  <!-- Header -->
185
- {#if header || title}
199
+ {#if title}
200
+ <div class={headerClasses}>
201
+ <h3
202
+ id="drawer-title"
203
+ class={titleClasses}
204
+ data-testid={buildTestId('drawer', 'title', testId)}
205
+ >
206
+ {title}
207
+ </h3>
208
+ </div>
209
+ {:else if header}
186
210
  <div class={headerClasses}>
187
- {#if header}
188
- {@render header()}
189
- {:else}
190
- {#if title}
191
- <h3
192
- id="drawer-title"
193
- class={titleClasses}
194
- data-testid={buildTestId('drawer', 'title', testId)}
195
- >
196
- {title}
197
- </h3>
198
- {/if}
199
- <button
200
- type="button"
201
- class={closeButtonClasses}
202
- onclick={handleCloseClick}
203
- aria-label="Close drawer"
204
- data-testid={buildTestId('drawer', 'close', testId)}
205
- >
206
- <svg
207
- xmlns="http://www.w3.org/2000/svg"
208
- width="12"
209
- height="12"
210
- viewBox="0 0 12 12"
211
- >
212
- <path
213
- fill="currentColor"
214
- d="m1.897 2.054l.073-.084a.75.75 0 0 1 .976-.073l.084.073L6 4.939l2.97-2.97a.75.75 0 1 1 1.06 1.061L7.061 6l2.97 2.97a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L6 7.061l-2.97 2.97A.75.75 0 1 1 1.97 8.97L4.939 6l-2.97-2.97a.75.75 0 0 1-.072-.976l.073-.084z"
215
- />
216
- </svg>
217
- </button>
218
- {/if}
211
+ {@render header()}
219
212
  </div>
220
213
  {/if}
221
214
 
@@ -4,8 +4,8 @@ export const drawer = tv({
4
4
  slots: {
5
5
  base: 'fixed inset-0 z-50 flex overflow-hidden',
6
6
  backdrop: 'fixed inset-0 transition-opacity bg-black/50',
7
- contentWrapper: 'absolute flex flex-col transform transition-transform',
8
- content: 'flex flex-col h-full w-full overflow-y-auto bg-white',
7
+ contentWrapper: 'absolute flex flex-col transform transition-transform max-w-[100vw]',
8
+ content: 'relative flex flex-col h-full w-full overflow-y-auto bg-white',
9
9
  header: 'flex items-center justify-between px-4 py-3 border-b border-default-200',
10
10
  body: 'flex-1 overflow-y-auto p-4',
11
11
  footer: 'flex justify-end border-t border-default-200 p-4',
@@ -97,12 +97,15 @@
97
97
  const v = (e.currentTarget as HTMLInputElement).value;
98
98
  query = v;
99
99
  open = true;
100
+ // Fire onsearch immediately (documented "every keystroke" contract).
101
+ // Only debounce local filtering so rapid typing doesn't churn the
102
+ // DOM with intermediate filter results.
103
+ onsearch?.(v);
100
104
  clearTimeout(debounceTimer);
101
105
  debounceTimer = setTimeout(() => {
102
106
  debouncedQuery = v;
103
107
  highlightedIndex = 0;
104
- onsearch?.(v);
105
- }, 1000);
108
+ }, 300);
106
109
  }
107
110
 
108
111
  $effect(() => {
@@ -206,10 +209,7 @@
206
209
  else openMenu();
207
210
  }
208
211
  }}
209
- class={cn(
210
- 'text-default-400 hover:text-default-700 flex cursor-pointer items-center justify-center rounded',
211
- !disabled && 'hover:text-default-600'
212
- )}
212
+ class="text-default-400 hover:text-default-600 flex cursor-pointer items-center justify-center rounded"
213
213
  aria-label={open ? 'Close suggestions' : 'Open suggestions'}
214
214
  {disabled}
215
215
  >
@@ -216,19 +216,17 @@
216
216
  }
217
217
 
218
218
  function handleClickOutside(event: MouseEvent) {
219
- // Check if the click is inside the portal content
219
+ if (!open) return;
220
+ // On mobile the sheet handles its own close via backdrop.
221
+ if (isMobile) return;
220
222
  const portalContent = document.querySelector('.ripple-portal .portal-content');
221
-
222
- // If the click is inside either the label (trigger) or the portal content, don't close
223
+ const target = event.target as Node;
223
224
  if (
224
- (labelRef && labelRef.contains(event.target as Node)) ||
225
- (portalContent && portalContent.contains(event.target as Node)) ||
226
- !open
225
+ (labelRef && labelRef.contains(target)) ||
226
+ (portalContent && portalContent.contains(target))
227
227
  ) {
228
228
  return;
229
229
  }
230
-
231
- // Otherwise close the dropdown
232
230
  open = false;
233
231
  onclose();
234
232
  }
@@ -236,7 +236,7 @@
236
236
  type="button"
237
237
  class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
238
238
  aria-label="Close"
239
- onclick={close}
239
+ onclick={closeOnOutsideClick ? close : undefined}
240
240
  ></button>
241
241
  <div
242
242
  role="dialog"
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../../helper/cls.js';
3
+ import { buildTestId } from '../../helper/testid.js';
3
4
  import { Color, Size } from '../../variants.js';
4
5
  import { Tween } from 'svelte/motion';
5
6
  import { quintOut } from 'svelte/easing';
@@ -17,7 +18,8 @@
17
18
  showLabels = false,
18
19
  class: className = '',
19
20
  labelClass = '',
20
- barClass = ''
21
+ barClass = '',
22
+ testId
21
23
  }: ProgressProps = $props();
22
24
 
23
25
  // Function composition for better readability and maintainability
@@ -120,13 +122,14 @@
120
122
  );
121
123
  </script>
122
124
 
123
- <div class={containerClass}>
125
+ <div class={containerClass} data-testid={buildTestId('progress', undefined, testId)}>
124
126
  <div
125
127
  class={progressClass}
126
128
  role="progressbar"
127
129
  aria-valuenow={percentage}
128
130
  aria-valuemin="0"
129
131
  aria-valuemax="100"
132
+ data-testid={buildTestId('progress', 'track', testId)}
130
133
  >
131
134
  {#if segments}
132
135
  {#each segmentPercentages as segment, index (segment.label ?? `segment-${index}`)}
@@ -135,11 +138,16 @@
135
138
  class={cn(getColorClass(segment.color), barClass)}
136
139
  style="width: {segmentTweens[index]?.current ?? 0}%"
137
140
  title={segment.label || `${segment.value} (${segment.percentage}%)`}
141
+ data-testid={buildTestId('progress', 'segment', testId, index)}
138
142
  ></div>
139
143
  {/if}
140
144
  {/each}
141
145
  {:else}
142
- <div class={fillClass} style="width: {tween.current}%"></div>
146
+ <div
147
+ class={fillClass}
148
+ style="width: {tween.current}%"
149
+ data-testid={buildTestId('progress', 'fill', testId)}
150
+ ></div>
143
151
  {/if}
144
152
  </div>
145
153
 
@@ -147,7 +155,10 @@
147
155
  <div class="mt-1 flex justify-between">
148
156
  {#each segmentPercentages as segment, index (segment.label ?? `segment-${index}`)}
149
157
  {#if segment.percentage > 0}
150
- <div class={labelTextClass}>
158
+ <div
159
+ class={labelTextClass}
160
+ data-testid={buildTestId('progress', 'segment-label', testId, index)}
161
+ >
151
162
  {#if showLabels && segment.label}
152
163
  {segment.label}
153
164
  {/if}
@@ -162,6 +173,8 @@
162
173
  {/each}
163
174
  </div>
164
175
  {:else if showLabel}
165
- <span class={labelTextClass}>{percentage}%</span>
176
+ <span class={labelTextClass} data-testid={buildTestId('progress', 'value', testId)}
177
+ >{percentage}%</span
178
+ >
166
179
  {/if}
167
180
  </div>
@@ -65,5 +65,13 @@ export type ProgressProps = {
65
65
  labelClass?: ClassValue;
66
66
  /** Classes on the inner bar element(s). */
67
67
  barClass?: ClassValue;
68
+ /**
69
+ * Test ID prefix. When set, the component emits these selectors:
70
+ * - `{testId}-progress` — root wrapper
71
+ * - `{testId}-progress-track` — track bar
72
+ * - `{testId}-progress-fill` — fill bar (single mode)
73
+ * - `{testId}-progress-segment-{i}` — each segment
74
+ * - `{testId}-progress-value` — percentage display
75
+ */
68
76
  testId?: string;
69
77
  };
@@ -18,6 +18,7 @@
18
18
  actions = [],
19
19
  infoSection,
20
20
  selectAllScope = 'page',
21
+ allowFolderNavigation = true,
21
22
  height = 'h-[500px]',
22
23
  class: className = '',
23
24
  selectedItems = $bindable<FileItem[]>([]),
@@ -197,6 +198,7 @@
197
198
  }
198
199
 
199
200
  function navigateToFolder(folderPath: string) {
201
+ if (!allowFolderNavigation) return;
200
202
  // If we have the file info for this folder, cache its name in the adapter
201
203
  // before navigating (useful for Google Drive adapter)
202
204
  const folderFile = files.find((file) => file.key === folderPath);
@@ -290,9 +292,10 @@
290
292
 
291
293
  // Sort files based on current sort state
292
294
  function getSortedFiles(filesArray: FileItem[]): FileItem[] {
293
- // Separate folders and files
294
- const folders = filesArray.filter((file) => file.isFolder);
295
- const files = filesArray.filter((file) => !file.isFolder);
295
+ // When folder navigation is off, hide folders entirely.
296
+ const filtered = allowFolderNavigation ? filesArray : filesArray.filter((f) => !f.isFolder);
297
+ const folders = filtered.filter((file) => file.isFolder);
298
+ const files = filtered.filter((file) => !file.isFolder);
296
299
 
297
300
  // Handle default sort (folders alphabetically, files by date newest first)
298
301
  if (sortState.column === 'default' && sortState.direction === 'default') {
@@ -19,6 +19,8 @@
19
19
  chipSummary = false,
20
20
  isExpanded = $bindable(false),
21
21
  title = 'Filters',
22
+ card = true,
23
+ expandable = true,
22
24
  class: className,
23
25
  summaryClass,
24
26
  expandedClass,
@@ -28,7 +30,11 @@
28
30
  // Search input only renders when the consumer bound to searchQuery.
29
31
  const searchEnabled = $derived(searchQuery !== undefined);
30
32
 
33
+ // When not expandable, filters are always visible.
34
+ if (!expandable) isExpanded = true;
35
+
31
36
  function toggleExpanded() {
37
+ if (!expandable) return;
32
38
  isExpanded = !isExpanded;
33
39
  }
34
40
 
@@ -181,80 +187,84 @@
181
187
  </svg>
182
188
  {/snippet}
183
189
 
184
- <div class={cn('border-default-200 rounded-lg border bg-white p-3 shadow-sm', className)}>
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
- >
194
- {#if FilterIcon}
195
- <FilterIcon size={16} class="text-default-500" />
196
- {:else}
197
- <span class="text-default-500">{@render DefaultFilterIcon()}</span>
198
- {/if}
199
- <span class="text-sm font-medium">{title}</span>
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}
190
+ <div class={cn(card && 'border-default-200 rounded-lg border bg-white p-3 shadow-sm', className)}>
191
+ {#if card || expandable}
192
+ <!-- Header row: title | search | clear | chevron -->
193
+ <div class="mb-2 flex flex-wrap items-center gap-2">
235
194
  <button
236
195
  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=""
196
+ onclick={toggleExpanded}
197
+ class="flex flex-1 cursor-pointer items-center gap-2"
240
198
  >
241
- {clearAllLabel}
199
+ {#if FilterIcon}
200
+ <FilterIcon size={16} class="text-default-500" />
201
+ {:else}
202
+ <span class="text-default-500">{@render DefaultFilterIcon()}</span>
203
+ {/if}
204
+ <span class="text-sm font-medium">{title}</span>
242
205
  </button>
243
- {/if}
244
206
 
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"
249
- aria-label={isExpanded ? `Collapse ${title.toLowerCase()}` : `Expand ${title.toLowerCase()}`}
250
- >
251
- {#if isExpanded}
252
- {@render DefaultChevronUp()}
253
- {:else}
254
- {@render DefaultChevronDown()}
207
+ {#if searchEnabled}
208
+ <!-- `w-full sm:w-48` lets the search field fill the row on
209
+ narrow viewports (after wrapping) but stay compact on wide. -->
210
+ <div class="relative order-last w-full sm:order-none sm:w-auto">
211
+ <input
212
+ type="text"
213
+ 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"
214
+ placeholder={searchPlaceholder}
215
+ bind:value={searchQuery}
216
+ data-filters-search=""
217
+ />
218
+ {#if searchQuery}
219
+ <button
220
+ type="button"
221
+ onclick={() => (searchQuery = '')}
222
+ 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"
223
+ aria-label="Clear search"
224
+ data-filters-search-clear=""
225
+ >
226
+ <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
227
+ <path
228
+ d="M3 3l6 6M9 3l-6 6"
229
+ stroke="currentColor"
230
+ stroke-width="1.5"
231
+ stroke-linecap="round"
232
+ />
233
+ </svg>
234
+ </button>
235
+ {/if}
236
+ </div>
237
+ {/if}
238
+
239
+ {#if showClearAll && isDirty}
240
+ <button
241
+ type="button"
242
+ onclick={clearAll}
243
+ class="text-default-600 hover:bg-default-100 cursor-pointer rounded-md px-2 py-1 text-xs font-medium"
244
+ data-filters-clear-all=""
245
+ >
246
+ {clearAllLabel}
247
+ </button>
255
248
  {/if}
256
- </button>
257
- </div>
249
+
250
+ {#if expandable}
251
+ <button
252
+ type="button"
253
+ onclick={toggleExpanded}
254
+ class="text-default-500 hover:bg-default-100 hover:text-default-700 cursor-pointer rounded-md p-1"
255
+ aria-label={isExpanded
256
+ ? `Collapse ${title.toLowerCase()}`
257
+ : `Expand ${title.toLowerCase()}`}
258
+ >
259
+ {#if isExpanded}
260
+ {@render DefaultChevronUp()}
261
+ {:else}
262
+ {@render DefaultChevronDown()}
263
+ {/if}
264
+ </button>
265
+ {/if}
266
+ </div>
267
+ {/if}
258
268
 
259
269
  {#if !isExpanded}
260
270
  <div class={cn('flex flex-wrap gap-2', summaryClass)}>
@@ -82,6 +82,17 @@ export type CompactFiltersProps = {
82
82
  chipSummary?: boolean;
83
83
  isExpanded?: boolean;
84
84
  title?: string;
85
+ /**
86
+ * Render with a bordered card wrapper (border, shadow, padding,
87
+ * white background). Set to `false` to embed inside your own card
88
+ * or layout without double-bordering. @default true
89
+ */
90
+ card?: boolean;
91
+ /**
92
+ * Show the expand/collapse chevron toggle. When `false`, filters
93
+ * are always expanded (no collapsed chip summary). @default true
94
+ */
95
+ expandable?: boolean;
85
96
  class?: ClassValue;
86
97
  summaryClass?: ClassValue;
87
98
  expandedClass?: ClassValue;