@r2digisolutions/ui 0.27.3 → 0.28.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.
- package/dist/components/container/DataTableShell/DataTableShell.svelte +631 -0
- package/dist/components/container/DataTableShell/DataTableShell.svelte.d.ts +48 -0
- package/dist/components/container/DataTableShell/components/AdvancedFiltersBuilder.svelte +311 -0
- package/dist/components/container/DataTableShell/components/AdvancedFiltersBuilder.svelte.d.ts +7 -0
- package/dist/components/container/DataTableShell/components/ColumnVisibilityMenu.svelte +112 -0
- package/dist/components/container/DataTableShell/components/ColumnVisibilityMenu.svelte.d.ts +8 -0
- package/dist/components/container/DataTableShell/components/ContextMenu.svelte +70 -0
- package/dist/components/container/DataTableShell/components/ContextMenu.svelte.d.ts +30 -0
- package/dist/components/container/DataTableShell/components/DataTableFiltersSidebar.svelte +0 -0
- package/dist/components/container/DataTableShell/components/DataTableFiltersSidebar.svelte.d.ts +26 -0
- package/dist/components/container/DataTableShell/components/DataTableFooter.svelte +36 -0
- package/dist/components/container/DataTableShell/components/DataTableFooter.svelte.d.ts +18 -0
- package/dist/components/container/DataTableShell/components/DataTableToolbar.svelte +822 -0
- package/dist/components/container/DataTableShell/components/DataTableToolbar.svelte.d.ts +30 -0
- package/dist/components/container/DataTableShell/components/Pagination.svelte +117 -0
- package/dist/components/container/DataTableShell/components/Pagination.svelte.d.ts +28 -0
- package/dist/components/container/DataTableShell/components/Submenu.svelte +109 -0
- package/dist/components/container/DataTableShell/components/Submenu.svelte.d.ts +30 -0
- package/dist/components/container/DataTableShell/components/Toolbar.svelte +0 -0
- package/dist/components/container/DataTableShell/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/container/DataTableShell/core/DataTableController.svelte.d.ts +54 -0
- package/dist/components/container/DataTableShell/core/DataTableController.svelte.js +148 -0
- package/dist/components/container/DataTableShell/core/DataTableEngine.svelte.d.ts +68 -0
- package/dist/components/container/DataTableShell/core/DataTableEngine.svelte.js +319 -0
- package/dist/components/container/DataTableShell/core/DataTableInternal.svelte.d.ts +68 -0
- package/dist/components/container/DataTableShell/core/DataTableInternal.svelte.js +396 -0
- package/dist/components/container/DataTableShell/core/context.d.ts +3 -0
- package/dist/components/container/DataTableShell/core/context.js +12 -0
- package/dist/components/container/DataTableShell/core/filters-types.d.ts +14 -0
- package/dist/components/container/DataTableShell/core/filters-types.js +1 -0
- package/dist/components/container/DataTableShell/core/types.d.ts +60 -0
- package/dist/components/container/DataTableShell/core/types.js +1 -0
- package/dist/components/container/index.d.ts +3 -1
- package/dist/components/container/index.js +3 -1
- package/package.json +13 -13
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import {
|
|
3
|
+
Search,
|
|
4
|
+
SlidersHorizontal,
|
|
5
|
+
LayoutList,
|
|
6
|
+
LayoutGrid,
|
|
7
|
+
X,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronRight
|
|
10
|
+
} from 'lucide-svelte';
|
|
11
|
+
import { useTable } from '../core/context.js';
|
|
12
|
+
import type { DataTableController } from '../core/DataTableController.svelte';
|
|
13
|
+
import type {
|
|
14
|
+
QueryStructure,
|
|
15
|
+
FilterOperator,
|
|
16
|
+
Operator,
|
|
17
|
+
Filter,
|
|
18
|
+
QueryGroup,
|
|
19
|
+
TQueryFilter
|
|
20
|
+
} from '../core/filters-types.js';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
density: 'comfortable' | 'compact';
|
|
24
|
+
viewMode: 'list' | 'grid';
|
|
25
|
+
onDensityChange?: (d: 'comfortable' | 'compact') => void;
|
|
26
|
+
onViewModeChange?: (m: 'list' | 'grid') => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { density, viewMode, onDensityChange, onViewModeChange }: Props = $props();
|
|
30
|
+
const id = $props.id();
|
|
31
|
+
|
|
32
|
+
const controller = useTable<T>() as DataTableController<T>;
|
|
33
|
+
|
|
34
|
+
let search = $state('');
|
|
35
|
+
let filtersOpen = $state(false);
|
|
36
|
+
let currentQuery = $state<QueryStructure | null>(null);
|
|
37
|
+
let filtersDialog: HTMLDivElement | null = $state(null);
|
|
38
|
+
|
|
39
|
+
type LocalCondition = {
|
|
40
|
+
id: number;
|
|
41
|
+
kind: 'condition';
|
|
42
|
+
field: string;
|
|
43
|
+
operator: FilterOperator;
|
|
44
|
+
value: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type LocalGroup = {
|
|
48
|
+
id: number;
|
|
49
|
+
kind: 'group';
|
|
50
|
+
joinOperation: Operator;
|
|
51
|
+
children: LocalFilterNode[];
|
|
52
|
+
collapsed?: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type LocalFilterNode = LocalCondition | LocalGroup;
|
|
56
|
+
|
|
57
|
+
let rootGroup = $state<LocalGroup>({
|
|
58
|
+
id: 0,
|
|
59
|
+
kind: 'group',
|
|
60
|
+
joinOperation: 'AND',
|
|
61
|
+
children: [],
|
|
62
|
+
collapsed: false
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let nextNodeId = 1;
|
|
66
|
+
|
|
67
|
+
const operators: FilterOperator[] = [
|
|
68
|
+
'equals',
|
|
69
|
+
'contains',
|
|
70
|
+
'not_contains',
|
|
71
|
+
'greater_than',
|
|
72
|
+
'less_than',
|
|
73
|
+
'startsWith',
|
|
74
|
+
'endsWith',
|
|
75
|
+
'not_equals',
|
|
76
|
+
'is_empty',
|
|
77
|
+
'is_not_empty',
|
|
78
|
+
'in',
|
|
79
|
+
'not_in'
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const columns = $derived(
|
|
83
|
+
(((controller as any).allColumns ?? []) as { id: string; label: string }[]) || []
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
function isConditionUsable(c: LocalCondition) {
|
|
87
|
+
if (!c.field || !c.operator) return false;
|
|
88
|
+
if (c.operator === 'is_empty' || c.operator === 'is_not_empty') return true;
|
|
89
|
+
return c.value != null && String(c.value).trim() !== '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function countUsableInNode(node: LocalFilterNode): number {
|
|
93
|
+
if (node.kind === 'condition') {
|
|
94
|
+
return isConditionUsable(node) ? 1 : 0;
|
|
95
|
+
}
|
|
96
|
+
return node.children.reduce((acc, child) => acc + countUsableInNode(child), 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const activeFilterCount = $derived(countUsableInNode(rootGroup));
|
|
100
|
+
|
|
101
|
+
function describeOperator(op: FilterOperator) {
|
|
102
|
+
switch (op) {
|
|
103
|
+
case 'equals':
|
|
104
|
+
return '=';
|
|
105
|
+
case 'not_equals':
|
|
106
|
+
return '≠';
|
|
107
|
+
case 'contains':
|
|
108
|
+
return 'contiene';
|
|
109
|
+
case 'not_contains':
|
|
110
|
+
return 'no contiene';
|
|
111
|
+
case 'greater_than':
|
|
112
|
+
return '>';
|
|
113
|
+
case 'less_than':
|
|
114
|
+
return '<';
|
|
115
|
+
case 'startsWith':
|
|
116
|
+
return 'empieza por';
|
|
117
|
+
case 'endsWith':
|
|
118
|
+
return 'termina en';
|
|
119
|
+
case 'is_empty':
|
|
120
|
+
return 'vacío';
|
|
121
|
+
case 'is_not_empty':
|
|
122
|
+
return 'no vacío';
|
|
123
|
+
case 'in':
|
|
124
|
+
return 'en lista';
|
|
125
|
+
case 'not_in':
|
|
126
|
+
return 'no en lista';
|
|
127
|
+
default:
|
|
128
|
+
return op;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function describeConditionChip(c: LocalCondition) {
|
|
133
|
+
const col = columns.find((col) => col.id === c.field);
|
|
134
|
+
const label = (col?.label ?? c.field) || 'sin columna';
|
|
135
|
+
const op = describeOperator(c.operator);
|
|
136
|
+
if (c.operator === 'is_empty' || c.operator === 'is_not_empty') {
|
|
137
|
+
return `${label} · ${op}`;
|
|
138
|
+
}
|
|
139
|
+
const value =
|
|
140
|
+
c.operator === 'in' || c.operator === 'not_in'
|
|
141
|
+
? c.value
|
|
142
|
+
.split(',')
|
|
143
|
+
.map((v) => v.trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.slice(0, 3)
|
|
146
|
+
.join(', ')
|
|
147
|
+
: c.value;
|
|
148
|
+
return `${label} · ${op} ${value}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type FilterChip = {
|
|
152
|
+
id: number;
|
|
153
|
+
label: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function collectChipsFromNode(node: LocalFilterNode, acc: FilterChip[]) {
|
|
157
|
+
if (node.kind === 'condition') {
|
|
158
|
+
if (isConditionUsable(node)) {
|
|
159
|
+
acc.push({
|
|
160
|
+
id: node.id,
|
|
161
|
+
label: describeConditionChip(node)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
for (const child of node.children) {
|
|
167
|
+
collectChipsFromNode(child, acc);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const filterChips = $derived.by(() => {
|
|
172
|
+
const acc: FilterChip[] = [];
|
|
173
|
+
collectChipsFromNode(rootGroup, acc);
|
|
174
|
+
return acc;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
$effect(() => {
|
|
178
|
+
controller.setSearch(search.trim());
|
|
179
|
+
controller.setPage(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
function openFilters() {
|
|
183
|
+
syncFromQuery();
|
|
184
|
+
filtersOpen = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function closeFilters() {
|
|
188
|
+
filtersOpen = false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mapFromFilters(filters: TQueryFilter): LocalFilterNode[] {
|
|
192
|
+
const result: LocalFilterNode[] = [];
|
|
193
|
+
for (const f of filters) {
|
|
194
|
+
if (Array.isArray(f)) {
|
|
195
|
+
const [field, operator, value] = f as Filter;
|
|
196
|
+
result.push({
|
|
197
|
+
id: nextNodeId++,
|
|
198
|
+
kind: 'condition',
|
|
199
|
+
field: String(field),
|
|
200
|
+
operator: operator as FilterOperator,
|
|
201
|
+
value:
|
|
202
|
+
value == null
|
|
203
|
+
? ''
|
|
204
|
+
: Array.isArray(value)
|
|
205
|
+
? (value as (string | number | boolean)[]).join(', ')
|
|
206
|
+
: String(value)
|
|
207
|
+
});
|
|
208
|
+
} else if (f && (f as QueryGroup).type === 'group') {
|
|
209
|
+
const g = f as QueryGroup;
|
|
210
|
+
const group: LocalGroup = {
|
|
211
|
+
id: nextNodeId++,
|
|
212
|
+
kind: 'group',
|
|
213
|
+
joinOperation: g.joinOperation,
|
|
214
|
+
children: mapFromFilters(g.filters),
|
|
215
|
+
collapsed: false
|
|
216
|
+
};
|
|
217
|
+
result.push(group);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function syncFromQuery() {
|
|
224
|
+
const q =
|
|
225
|
+
((controller as any).query as QueryStructure | undefined | null) ?? currentQuery ?? null;
|
|
226
|
+
|
|
227
|
+
nextNodeId = 1;
|
|
228
|
+
|
|
229
|
+
if (!q || !q.useQuery || !q.filters?.length) {
|
|
230
|
+
rootGroup = {
|
|
231
|
+
id: nextNodeId++,
|
|
232
|
+
kind: 'group',
|
|
233
|
+
joinOperation: 'AND',
|
|
234
|
+
children: [],
|
|
235
|
+
collapsed: false
|
|
236
|
+
};
|
|
237
|
+
currentQuery = null;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
rootGroup = {
|
|
242
|
+
id: nextNodeId++,
|
|
243
|
+
kind: 'group',
|
|
244
|
+
joinOperation: q.joinOperation,
|
|
245
|
+
children: mapFromFilters(q.filters),
|
|
246
|
+
collapsed: false
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
currentQuery = q;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function updateGroup(
|
|
253
|
+
node: LocalGroup,
|
|
254
|
+
targetId: number,
|
|
255
|
+
fn: (g: LocalGroup) => LocalGroup
|
|
256
|
+
): LocalGroup {
|
|
257
|
+
if (node.id === targetId) {
|
|
258
|
+
return fn(node);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...node,
|
|
262
|
+
children: node.children.map((child) => {
|
|
263
|
+
if (child.kind === 'group') {
|
|
264
|
+
return updateGroup(child, targetId, fn);
|
|
265
|
+
}
|
|
266
|
+
return child;
|
|
267
|
+
})
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function removeNodeFromGroup(node: LocalGroup, targetId: number): LocalGroup {
|
|
272
|
+
return {
|
|
273
|
+
...node,
|
|
274
|
+
children: node.children
|
|
275
|
+
.filter((child) => child.id !== targetId)
|
|
276
|
+
.map((child) => (child.kind === 'group' ? removeNodeFromGroup(child, targetId) : child))
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function addConditionToGroup(groupId: number) {
|
|
281
|
+
rootGroup = updateGroup(rootGroup, groupId, (g) => ({
|
|
282
|
+
...g,
|
|
283
|
+
children: [
|
|
284
|
+
...g.children,
|
|
285
|
+
{
|
|
286
|
+
id: nextNodeId++,
|
|
287
|
+
kind: 'condition',
|
|
288
|
+
field: '',
|
|
289
|
+
operator: 'equals',
|
|
290
|
+
value: ''
|
|
291
|
+
} as LocalCondition
|
|
292
|
+
]
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function addGroupToGroup(groupId: number) {
|
|
297
|
+
rootGroup = updateGroup(rootGroup, groupId, (g) => ({
|
|
298
|
+
...g,
|
|
299
|
+
children: [
|
|
300
|
+
...g.children,
|
|
301
|
+
{
|
|
302
|
+
id: nextNodeId++,
|
|
303
|
+
kind: 'group',
|
|
304
|
+
joinOperation: 'AND',
|
|
305
|
+
children: [],
|
|
306
|
+
collapsed: false
|
|
307
|
+
} as LocalGroup
|
|
308
|
+
]
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function removeFilterNode(id: number) {
|
|
313
|
+
if (id === rootGroup.id) return;
|
|
314
|
+
rootGroup = removeNodeFromGroup(rootGroup, id);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toggleGroupCollapse(id: number) {
|
|
318
|
+
rootGroup = updateGroup(rootGroup, id, (g) => ({
|
|
319
|
+
...g,
|
|
320
|
+
collapsed: !g.collapsed
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function updateGroupJoinOperation(id: number, op: Operator) {
|
|
325
|
+
rootGroup = updateGroup(rootGroup, id, (g) => ({
|
|
326
|
+
...g,
|
|
327
|
+
joinOperation: op
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function toFilterNode(node: LocalFilterNode): Filter | QueryGroup | null {
|
|
332
|
+
if (node.kind === 'condition') {
|
|
333
|
+
if (!isConditionUsable(node)) return null;
|
|
334
|
+
let value: string | number | boolean | string[] = node.value;
|
|
335
|
+
if (node.operator === 'in' || node.operator === 'not_in') {
|
|
336
|
+
value = node.value
|
|
337
|
+
.split(',')
|
|
338
|
+
.map((v) => v.trim())
|
|
339
|
+
.filter(Boolean);
|
|
340
|
+
}
|
|
341
|
+
return [node.field, node.operator, value];
|
|
342
|
+
}
|
|
343
|
+
const children = node.children
|
|
344
|
+
.map(toFilterNode)
|
|
345
|
+
.filter((c): c is Filter | QueryGroup => c != null);
|
|
346
|
+
if (!children.length) return null;
|
|
347
|
+
return {
|
|
348
|
+
type: 'group',
|
|
349
|
+
joinOperation: node.joinOperation,
|
|
350
|
+
filters: children
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function clearFilters() {
|
|
355
|
+
rootGroup = {
|
|
356
|
+
id: rootGroup.id || 0,
|
|
357
|
+
kind: 'group',
|
|
358
|
+
joinOperation: 'AND',
|
|
359
|
+
children: [],
|
|
360
|
+
collapsed: false
|
|
361
|
+
};
|
|
362
|
+
currentQuery = null;
|
|
363
|
+
controller.setQuery(null as any);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function applyFilters() {
|
|
367
|
+
const filters = rootGroup.children
|
|
368
|
+
.map(toFilterNode)
|
|
369
|
+
.filter((c): c is Filter | QueryGroup => c != null);
|
|
370
|
+
|
|
371
|
+
if (!filters.length) {
|
|
372
|
+
controller.setQuery(null as any);
|
|
373
|
+
currentQuery = null;
|
|
374
|
+
filtersOpen = false;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const q: QueryStructure = {
|
|
379
|
+
useQuery: true,
|
|
380
|
+
joinOperation: rootGroup.joinOperation,
|
|
381
|
+
filters
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
controller.setQuery(q);
|
|
385
|
+
currentQuery = q;
|
|
386
|
+
filtersOpen = false;
|
|
387
|
+
controller.setPage(1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function handleDensityClick(d: 'comfortable' | 'compact') {
|
|
391
|
+
if (onDensityChange) onDensityChange(d);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function handleViewModeClick(m: 'list' | 'grid') {
|
|
395
|
+
if (onViewModeChange) onViewModeChange(m);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function clearSearch() {
|
|
399
|
+
if (!search) return;
|
|
400
|
+
search = '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function removeChip(id: number) {
|
|
404
|
+
removeFilterNode(id);
|
|
405
|
+
applyFilters();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const styles_sidebar = $derived.by(() => {
|
|
409
|
+
let styles = `
|
|
410
|
+
inset: auto;
|
|
411
|
+
position-anchor: --filters-dialog-${id};
|
|
412
|
+
position-area: right bottom;
|
|
413
|
+
position-try-fallbacks: flip-block;
|
|
414
|
+
transform: translateX(-100%);
|
|
415
|
+
margin-block-start: 0.5rem;
|
|
416
|
+
`;
|
|
417
|
+
|
|
418
|
+
return styles;
|
|
419
|
+
});
|
|
420
|
+
</script>
|
|
421
|
+
|
|
422
|
+
{#snippet FilterCondition({ condition }: { condition: LocalCondition })}
|
|
423
|
+
<div
|
|
424
|
+
class="rounded-2xl border border-neutral-200/80 bg-white/90 px-3 py-2 shadow-sm dark:border-neutral-800/80 dark:bg-neutral-900/90"
|
|
425
|
+
>
|
|
426
|
+
<div class="mb-1 flex items-center justify-between gap-2">
|
|
427
|
+
<span
|
|
428
|
+
class="text-[10px] font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400"
|
|
429
|
+
>
|
|
430
|
+
Condición
|
|
431
|
+
</span>
|
|
432
|
+
<button
|
|
433
|
+
type="button"
|
|
434
|
+
class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] text-neutral-500 hover:bg-red-50 hover:text-red-500 dark:text-neutral-400 dark:hover:bg-red-500/10 dark:hover:text-red-300"
|
|
435
|
+
onclick={() => removeFilterNode(condition.id)}
|
|
436
|
+
>
|
|
437
|
+
Quitar
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div class="grid grid-cols-1 gap-1.5">
|
|
442
|
+
<select
|
|
443
|
+
class="w-full rounded-xl border border-neutral-200/80 bg-white/95 px-2 py-1.5 text-[11px] text-neutral-900 outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500/70 dark:border-neutral-700/80 dark:bg-neutral-900/90 dark:text-neutral-50"
|
|
444
|
+
bind:value={condition.field}
|
|
445
|
+
>
|
|
446
|
+
<option value="">Columna…</option>
|
|
447
|
+
{#each columns as col}
|
|
448
|
+
<option value={col.id}>{col.label}</option>
|
|
449
|
+
{/each}
|
|
450
|
+
</select>
|
|
451
|
+
|
|
452
|
+
<select
|
|
453
|
+
class="w-full rounded-xl border border-neutral-200/80 bg-white/95 px-2 py-1.5 text-[11px] text-neutral-900 outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500/70 dark:border-neutral-700/80 dark:bg-neutral-900/90 dark:text-neutral-50"
|
|
454
|
+
bind:value={condition.operator}
|
|
455
|
+
>
|
|
456
|
+
{#each operators as op}
|
|
457
|
+
<option value={op}>{op}</option>
|
|
458
|
+
{/each}
|
|
459
|
+
</select>
|
|
460
|
+
|
|
461
|
+
{#if condition.operator !== 'is_empty' && condition.operator !== 'is_not_empty'}
|
|
462
|
+
<input
|
|
463
|
+
type="text"
|
|
464
|
+
placeholder={condition.operator === 'in' || condition.operator === 'not_in'
|
|
465
|
+
? 'Valores separados por coma…'
|
|
466
|
+
: 'Valor…'}
|
|
467
|
+
bind:value={condition.value}
|
|
468
|
+
class="w-full rounded-xl border border-neutral-200/80 bg-white/95 px-2 py-1.5 text-[11px] text-neutral-900 outline-none placeholder:text-neutral-400 focus:border-purple-500 focus:ring-1 focus:ring-purple-500/70 dark:border-neutral-700/80 dark:bg-neutral-900/90 dark:text-neutral-50 dark:placeholder:text-neutral-500"
|
|
469
|
+
/>
|
|
470
|
+
{:else}
|
|
471
|
+
<div
|
|
472
|
+
class="rounded-xl border border-dashed border-neutral-200/80 bg-neutral-50/90 px-2 py-1.5 text-[10px] text-neutral-500 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-400"
|
|
473
|
+
>
|
|
474
|
+
Este operador no necesita valor.
|
|
475
|
+
</div>
|
|
476
|
+
{/if}
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
{/snippet}
|
|
480
|
+
|
|
481
|
+
{#snippet FilterGroup({ group, isRoot }: { group: LocalGroup; isRoot: boolean })}
|
|
482
|
+
<div
|
|
483
|
+
class="rounded-2xl border border-neutral-200/80 bg-gradient-to-b from-white/98 to-neutral-50/95 px-3 py-2.5 shadow-md dark:border-neutral-800/80 dark:from-neutral-950/98 dark:to-neutral-900/95"
|
|
484
|
+
>
|
|
485
|
+
<div class="mb-2 flex items-center justify-between gap-2">
|
|
486
|
+
<div class="flex items-center gap-1.5">
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-neutral-100 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-800 dark:bg-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
|
|
490
|
+
onclick={() => toggleGroupCollapse(group.id)}
|
|
491
|
+
>
|
|
492
|
+
{#if group.collapsed}
|
|
493
|
+
<ChevronRight class="h-3.5 w-3.5" />
|
|
494
|
+
{:else}
|
|
495
|
+
<ChevronDown class="h-3.5 w-3.5" />
|
|
496
|
+
{/if}
|
|
497
|
+
</button>
|
|
498
|
+
<div class="flex flex-col">
|
|
499
|
+
<span class="text-[11px] font-semibold text-neutral-900 dark:text-neutral-50">
|
|
500
|
+
{isRoot ? 'Grupo raíz' : 'Grupo'}
|
|
501
|
+
</span>
|
|
502
|
+
<span class="text-[10px] text-neutral-500 dark:text-neutral-400">
|
|
503
|
+
Condiciones unidas con
|
|
504
|
+
<span class="font-semibold"> {group.joinOperation}</span>
|
|
505
|
+
</span>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<div class="flex items-center gap-1">
|
|
510
|
+
<div
|
|
511
|
+
class="inline-flex items-center rounded-full border border-neutral-200/80 bg-neutral-100/80 p-0.5 text-[10px] text-neutral-700 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-200"
|
|
512
|
+
>
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
class={`inline-flex items-center rounded-full px-2 py-0.5 ${
|
|
516
|
+
group.joinOperation === 'AND'
|
|
517
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
518
|
+
: 'hover:text-neutral-900 dark:hover:text-neutral-50'
|
|
519
|
+
}`}
|
|
520
|
+
onclick={() => updateGroupJoinOperation(group.id, 'AND')}
|
|
521
|
+
>
|
|
522
|
+
AND
|
|
523
|
+
</button>
|
|
524
|
+
<button
|
|
525
|
+
type="button"
|
|
526
|
+
class={`inline-flex items-center rounded-full px-2 py-0.5 ${
|
|
527
|
+
group.joinOperation === 'OR'
|
|
528
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
529
|
+
: 'hover:text-neutral-900 dark:hover:text-neutral-50'
|
|
530
|
+
}`}
|
|
531
|
+
onclick={() => updateGroupJoinOperation(group.id, 'OR')}
|
|
532
|
+
>
|
|
533
|
+
OR
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
{#if !isRoot}
|
|
538
|
+
<button
|
|
539
|
+
type="button"
|
|
540
|
+
class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] text-neutral-500 hover:bg-red-50 hover:text-red-500 dark:text-neutral-400 dark:hover:bg-red-500/10 dark:hover:text-red-300"
|
|
541
|
+
onclick={() => removeFilterNode(group.id)}
|
|
542
|
+
>
|
|
543
|
+
Quitar
|
|
544
|
+
</button>
|
|
545
|
+
{/if}
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
{#if !group.collapsed}
|
|
550
|
+
<div
|
|
551
|
+
class="space-y-2 border-l border-dashed border-neutral-200/80 pl-3 dark:border-neutral-700/80"
|
|
552
|
+
>
|
|
553
|
+
{#if !group.children.length}
|
|
554
|
+
<div
|
|
555
|
+
class="rounded-xl border border-dashed border-neutral-200/80 bg-neutral-50/80 px-2.5 py-2 text-[10px] text-neutral-500 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-400"
|
|
556
|
+
>
|
|
557
|
+
Este grupo no tiene condiciones. Añade una condición o un subgrupo.
|
|
558
|
+
</div>
|
|
559
|
+
{:else}
|
|
560
|
+
<div class="space-y-1.5">
|
|
561
|
+
{#each group.children as child (child.id)}
|
|
562
|
+
{#if child.kind === 'group'}
|
|
563
|
+
{@render FilterGroup({ group: child, isRoot: false })}
|
|
564
|
+
{:else}
|
|
565
|
+
{@render FilterCondition({ condition: child })}
|
|
566
|
+
{/if}
|
|
567
|
+
{/each}
|
|
568
|
+
</div>
|
|
569
|
+
{/if}
|
|
570
|
+
|
|
571
|
+
<div class="mt-2 flex flex-wrap items-center gap-1.5">
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
class="inline-flex items-center justify-center rounded-xl border border-dashed border-neutral-200/80 bg-neutral-50/80 px-2.5 py-1 text-[10px] text-neutral-700 hover:border-purple-500 hover:text-purple-600 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-200 dark:hover:border-purple-500 dark:hover:text-purple-200"
|
|
575
|
+
onclick={() => addConditionToGroup(group.id)}
|
|
576
|
+
>
|
|
577
|
+
+ Condición
|
|
578
|
+
</button>
|
|
579
|
+
<button
|
|
580
|
+
type="button"
|
|
581
|
+
class="inline-flex items-center justify-center rounded-xl border border-dashed border-neutral-200/80 bg-neutral-50/80 px-2.5 py-1 text-[10px] text-neutral-700 hover:border-purple-500 hover:text-purple-600 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-200 dark:hover:border-purple-500 dark:hover:text-purple-200"
|
|
582
|
+
onclick={() => addGroupToGroup(group.id)}
|
|
583
|
+
>
|
|
584
|
+
+ Subgrupo
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
{/if}
|
|
589
|
+
</div>
|
|
590
|
+
{/snippet}
|
|
591
|
+
|
|
592
|
+
<div
|
|
593
|
+
class="flex flex-col gap-1 border-b border-neutral-200/80 bg-gradient-to-r from-neutral-50/95 via-white/95 to-neutral-50/95 px-3 py-2 text-[11px] text-neutral-600 shadow-[0_8px_24px_rgba(15,23,42,0.04)] backdrop-blur-xl dark:border-neutral-800/80 dark:from-neutral-950/90 dark:via-neutral-950/85 dark:to-neutral-900/85 dark:text-neutral-300"
|
|
594
|
+
>
|
|
595
|
+
<div class="flex items-center justify-between gap-3">
|
|
596
|
+
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
597
|
+
<div class="relative max-w-xs flex-1">
|
|
598
|
+
<input
|
|
599
|
+
type="search"
|
|
600
|
+
placeholder="Buscar…"
|
|
601
|
+
bind:value={search}
|
|
602
|
+
class="w-full rounded-xl border border-neutral-200/80 bg-white/80 py-1.5 pr-7 pl-7 text-[11px] text-neutral-800 outline-none placeholder:text-neutral-400 focus:border-purple-400 focus:ring-2 focus:ring-purple-400/60 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-50 dark:placeholder:text-neutral-500"
|
|
603
|
+
/>
|
|
604
|
+
<div class="pointer-events-none absolute inset-y-0 left-2 flex items-center">
|
|
605
|
+
<Search class="h-3.5 w-3.5 text-neutral-400 dark:text-neutral-500" />
|
|
606
|
+
</div>
|
|
607
|
+
{#if search}
|
|
608
|
+
<button
|
|
609
|
+
type="button"
|
|
610
|
+
class="absolute inset-y-0 right-1 flex items-center justify-center rounded-full p-1 text-neutral-400 hover:bg-neutral-100 hover:text-neutral-700 dark:text-neutral-500 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
|
|
611
|
+
onclick={clearSearch}
|
|
612
|
+
>
|
|
613
|
+
<X class="h-3 w-3" />
|
|
614
|
+
</button>
|
|
615
|
+
{/if}
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<div class="flex items-center gap-2">
|
|
620
|
+
<div
|
|
621
|
+
class="hidden items-center gap-0.5 rounded-full border border-neutral-200/80 bg-white/80 p-0.5 text-[10px] text-neutral-500 shadow-sm sm:inline-flex dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-300"
|
|
622
|
+
>
|
|
623
|
+
<button
|
|
624
|
+
type="button"
|
|
625
|
+
class={`inline-flex items-center gap-1 rounded-full px-2 py-1 ${
|
|
626
|
+
density === 'comfortable'
|
|
627
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
628
|
+
: 'hover:text-neutral-800 dark:hover:text-neutral-100'
|
|
629
|
+
}`}
|
|
630
|
+
onclick={() => handleDensityClick('comfortable')}
|
|
631
|
+
>
|
|
632
|
+
<span class="hidden md:inline">Cómodo</span>
|
|
633
|
+
<span class="md:hidden">C</span>
|
|
634
|
+
</button>
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
class={`inline-flex items-center gap-1 rounded-full px-2 py-1 ${
|
|
638
|
+
density === 'compact'
|
|
639
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
640
|
+
: 'hover:text-neutral-800 dark:hover:text-neutral-100'
|
|
641
|
+
}`}
|
|
642
|
+
onclick={() => handleDensityClick('compact')}
|
|
643
|
+
>
|
|
644
|
+
<span class="hidden md:inline">Compacto</span>
|
|
645
|
+
<span class="md:hidden">X</span>
|
|
646
|
+
</button>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<div
|
|
650
|
+
class="inline-flex items-center gap-0.5 rounded-full border border-neutral-200/80 bg-white/80 p-0.5 text-[10px] text-neutral-500 shadow-sm dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-300"
|
|
651
|
+
>
|
|
652
|
+
<button
|
|
653
|
+
type="button"
|
|
654
|
+
class={`inline-flex items-center gap-1 rounded-full px-2 py-1 ${
|
|
655
|
+
viewMode === 'list'
|
|
656
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
657
|
+
: 'hover:text-neutral-800 dark:hover:text-neutral-100'
|
|
658
|
+
}`}
|
|
659
|
+
onclick={() => handleViewModeClick('list')}
|
|
660
|
+
>
|
|
661
|
+
<LayoutList class="h-3 w-3" />
|
|
662
|
+
<span class="hidden sm:inline">Lista</span>
|
|
663
|
+
</button>
|
|
664
|
+
<button
|
|
665
|
+
type="button"
|
|
666
|
+
class={`inline-flex items-center gap-1 rounded-full px-2 py-1 ${
|
|
667
|
+
viewMode === 'grid'
|
|
668
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
669
|
+
: 'hover:text-neutral-800 dark:hover:text-neutral-100'
|
|
670
|
+
}`}
|
|
671
|
+
onclick={() => handleViewModeClick('grid')}
|
|
672
|
+
>
|
|
673
|
+
<LayoutGrid class="h-3 w-3" />
|
|
674
|
+
<span class="hidden sm:inline">Grid</span>
|
|
675
|
+
</button>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<button
|
|
679
|
+
type="button"
|
|
680
|
+
class="inline-flex cursor-pointer items-center gap-1 rounded-full border border-neutral-200/80 bg-white/80 px-2.5 py-1.5 text-[10px] font-medium text-neutral-600 shadow-sm transition-colors hover:border-purple-300 hover:text-purple-600 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-300 dark:hover:border-purple-500/60 dark:hover:text-purple-100"
|
|
681
|
+
popovertarget="filters-dialog-{id}"
|
|
682
|
+
style="anchor-name: --filters-dialog-{id};"
|
|
683
|
+
>
|
|
684
|
+
<SlidersHorizontal class="h-3.5 w-3.5" />
|
|
685
|
+
<span class="hidden sm:inline">Filtros</span>
|
|
686
|
+
{#if activeFilterCount}
|
|
687
|
+
<span
|
|
688
|
+
class="ml-1 rounded-full bg-purple-600 px-1.5 py-[1px] text-[9px] font-semibold text-white shadow-sm dark:bg-purple-500"
|
|
689
|
+
>
|
|
690
|
+
{activeFilterCount}
|
|
691
|
+
</span>
|
|
692
|
+
{/if}
|
|
693
|
+
</button>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
{#if filterChips.length}
|
|
698
|
+
<div class="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
699
|
+
{#each filterChips as chip}
|
|
700
|
+
<button
|
|
701
|
+
type="button"
|
|
702
|
+
class="group inline-flex max-w-xs items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-[10px] text-neutral-700 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-200 dark:hover:bg-neutral-800"
|
|
703
|
+
onclick={() => removeChip(chip.id)}
|
|
704
|
+
>
|
|
705
|
+
<span class="truncate">
|
|
706
|
+
{chip.label}
|
|
707
|
+
</span>
|
|
708
|
+
<span
|
|
709
|
+
class="flex h-4 w-4 items-center justify-center rounded-full bg-neutral-300/70 text-[9px] text-neutral-700 group-hover:bg-neutral-400 dark:bg-neutral-700/70 dark:text-neutral-100 dark:group-hover:bg-neutral-600"
|
|
710
|
+
>
|
|
711
|
+
<X class="h-2.5 w-2.5" />
|
|
712
|
+
</span>
|
|
713
|
+
</button>
|
|
714
|
+
{/each}
|
|
715
|
+
</div>
|
|
716
|
+
{/if}
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<div
|
|
720
|
+
popover
|
|
721
|
+
id="filters-dialog-{id}"
|
|
722
|
+
bind:this={filtersDialog}
|
|
723
|
+
style={styles_sidebar}
|
|
724
|
+
class="overflow-visible rounded-2xl border border-neutral-200/80 bg-gradient-to-b from-neutral-50/98 via-neutral-50/95 to-neutral-100/95 text-[11px] text-neutral-900 shadow-sm dark:border-neutral-800/80 dark:from-neutral-950/98 dark:via-neutral-950/95 dark:to-neutral-900/95 dark:text-neutral-50"
|
|
725
|
+
>
|
|
726
|
+
<div class="flex max-h-[80vh] w-[min(360px,100vw-16px)] flex-col p-3">
|
|
727
|
+
<header class="mb-2 flex items-center justify-between gap-2">
|
|
728
|
+
<div class="flex flex-col gap-0.5">
|
|
729
|
+
<h3 class="text-[12px] font-semibold text-neutral-900 dark:text-neutral-50">
|
|
730
|
+
Filtros avanzados
|
|
731
|
+
</h3>
|
|
732
|
+
<p class="text-[10px] text-neutral-500 dark:text-neutral-400">
|
|
733
|
+
Combina grupos AND/OR para refinar los resultados.
|
|
734
|
+
</p>
|
|
735
|
+
</div>
|
|
736
|
+
</header>
|
|
737
|
+
|
|
738
|
+
<div
|
|
739
|
+
class="mb-2 flex items-center justify-between gap-2 rounded-2xl border border-neutral-200/80 bg-white/80 px-3 py-2 text-[10px] shadow-sm dark:border-neutral-800/80 dark:bg-neutral-900/80"
|
|
740
|
+
>
|
|
741
|
+
<div class="flex flex-col">
|
|
742
|
+
<span class="font-medium text-neutral-800 dark:text-neutral-100"> Operador raíz </span>
|
|
743
|
+
<span class="text-neutral-500 dark:text-neutral-400">
|
|
744
|
+
Une los grupos superiores con
|
|
745
|
+
<span class="font-semibold"> {rootGroup.joinOperation}</span>
|
|
746
|
+
</span>
|
|
747
|
+
</div>
|
|
748
|
+
<div
|
|
749
|
+
class="inline-flex items-center rounded-full border border-neutral-200/80 bg-neutral-100/80 p-0.5 text-[10px] text-neutral-700 dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-200"
|
|
750
|
+
>
|
|
751
|
+
<button
|
|
752
|
+
type="button"
|
|
753
|
+
class={`inline-flex items-center rounded-full px-2 py-0.5 ${
|
|
754
|
+
rootGroup.joinOperation === 'AND'
|
|
755
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
756
|
+
: 'hover:text-neutral-900 dark:hover:text-neutral-50'
|
|
757
|
+
}`}
|
|
758
|
+
onclick={() => updateGroupJoinOperation(rootGroup.id, 'AND')}
|
|
759
|
+
>
|
|
760
|
+
AND
|
|
761
|
+
</button>
|
|
762
|
+
<button
|
|
763
|
+
type="button"
|
|
764
|
+
class={`inline-flex items-center rounded-full px-2 py-0.5 ${
|
|
765
|
+
rootGroup.joinOperation === 'OR'
|
|
766
|
+
? 'bg-neutral-900 text-neutral-50 dark:bg-neutral-100 dark:text-neutral-900'
|
|
767
|
+
: 'hover:text-neutral-900 dark:hover:text-neutral-50'
|
|
768
|
+
}`}
|
|
769
|
+
onclick={() => updateGroupJoinOperation(rootGroup.id, 'OR')}
|
|
770
|
+
>
|
|
771
|
+
OR
|
|
772
|
+
</button>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
<div class="flex-1 space-y-2 overflow-auto pr-1">
|
|
777
|
+
{#if !columns.length}
|
|
778
|
+
<div
|
|
779
|
+
class="rounded-xl border border-dashed border-neutral-300/80 bg-white/90 px-3 py-2 text-[10px] text-neutral-500 shadow-inner dark:border-neutral-700/80 dark:bg-neutral-900/80 dark:text-neutral-400"
|
|
780
|
+
>
|
|
781
|
+
No hay columnas configuradas para filtrar.
|
|
782
|
+
</div>
|
|
783
|
+
{/if}
|
|
784
|
+
|
|
785
|
+
{@render FilterGroup({ group: rootGroup, isRoot: true })}
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<footer
|
|
789
|
+
class="mt-3 flex items-center justify-between gap-2 border-t border-neutral-200/80 pt-2 dark:border-neutral-800/80"
|
|
790
|
+
>
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
class="rounded-xl px-2.5 py-1.5 text-[10px] text-neutral-500 hover:bg-neutral-200/80 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-900 dark:hover:text-neutral-50"
|
|
794
|
+
onclick={clearFilters}
|
|
795
|
+
>
|
|
796
|
+
Limpiar filtros
|
|
797
|
+
</button>
|
|
798
|
+
<div class="flex items-center gap-2">
|
|
799
|
+
{#if activeFilterCount}
|
|
800
|
+
<div
|
|
801
|
+
class="hidden items-center gap-1 rounded-full bg-neutral-900/90 px-2 py-1 text-[9px] text-neutral-100 shadow-sm md:inline-flex dark:bg-neutral-800/90"
|
|
802
|
+
>
|
|
803
|
+
<span>{activeFilterCount} condición{activeFilterCount === 1 ? '' : 'es'}</span>
|
|
804
|
+
</div>
|
|
805
|
+
{/if}
|
|
806
|
+
<button
|
|
807
|
+
type="button"
|
|
808
|
+
class="inline-flex items-center gap-2 rounded-xl bg-purple-600 px-3 py-1.5 text-[11px] font-medium text-white shadow-sm hover:bg-purple-500 disabled:opacity-40"
|
|
809
|
+
onclick={applyFilters}
|
|
810
|
+
disabled={!activeFilterCount}
|
|
811
|
+
>
|
|
812
|
+
<span>Aplicar</span>
|
|
813
|
+
{#if activeFilterCount}
|
|
814
|
+
<span class="rounded-full bg-white/15 px-1.5 py-[1px] text-[9px]">
|
|
815
|
+
{activeFilterCount}
|
|
816
|
+
</span>
|
|
817
|
+
{/if}
|
|
818
|
+
</button>
|
|
819
|
+
</div>
|
|
820
|
+
</footer>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|