@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.
- package/dist/ai/ai-types.d.ts +7 -0
- package/dist/charts/Chart.svelte +18 -37
- package/dist/charts/chart-types.d.ts +7 -0
- package/dist/drawer/Drawer.svelte +29 -36
- package/dist/drawer/drawer.js +2 -2
- package/dist/elements/combobox/ComboBox.svelte +6 -6
- package/dist/elements/dropdown/Select.svelte +6 -8
- package/dist/elements/popover/Popover.svelte +1 -1
- package/dist/elements/progress/Progress.svelte +18 -5
- package/dist/elements/progress/progress-types.d.ts +8 -0
- package/dist/file-browser/FileBrowser.svelte +6 -3
- package/dist/filters/CompactFilters.svelte +78 -68
- package/dist/filters/filter-types.d.ts +11 -0
- package/dist/forms/DateRange.svelte +31 -20
- package/dist/forms/NumberInput.svelte +6 -2
- package/dist/forms/Tags.svelte +9 -4
- package/dist/forms/Toggle.svelte +8 -6
- package/dist/forms/calendar/Calendar.svelte +8 -2
- package/dist/forms/calendar/calendar-types.d.ts +9 -0
- package/dist/forms/date-picker/DatePicker.svelte +8 -1
- package/dist/forms/form-types.d.ts +23 -0
- package/dist/forms/month-picker/MonthPicker.svelte +12 -3
- package/dist/forms/month-picker/month-picker-types.d.ts +7 -0
- package/dist/header/Breadcrumbs.svelte +19 -5
- package/dist/header/header-types.d.ts +7 -0
- package/dist/layout/activity-list/ActivityList.svelte +29 -7
- package/dist/layout/activity-list/activity-list-types.d.ts +8 -0
- package/dist/layout/card/MetricCard.svelte +34 -8
- package/dist/layout/card/RankedCard.svelte +34 -10
- package/dist/layout/card/card-types.d.ts +8 -0
- package/dist/layout/card/ranked-card.d.ts +10 -0
- package/dist/layout/table/Table.svelte +5 -0
- package/dist/layout/table/table-types.d.ts +8 -0
- package/dist/modal/modal.js +4 -4
- package/dist/pipeline/Pipeline.svelte +7 -3
- package/dist/pipeline/pipeline-types.d.ts +6 -0
- package/package.json +1 -1
package/dist/ai/ai-types.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/charts/Chart.svelte
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
{
|
|
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
|
|
package/dist/drawer/drawer.js
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
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
|
-
|
|
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(
|
|
225
|
-
(portalContent && portalContent.contains(
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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}
|
|
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
|
-
//
|
|
294
|
-
const
|
|
295
|
-
const
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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={
|
|
238
|
-
class="
|
|
239
|
-
data-filters-clear-all=""
|
|
196
|
+
onclick={toggleExpanded}
|
|
197
|
+
class="flex flex-1 cursor-pointer items-center gap-2"
|
|
240
198
|
>
|
|
241
|
-
{
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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;
|