@urbicon-ui/table 6.1.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/README.md +153 -0
- package/dist/cells/ActionButtons.svelte +224 -0
- package/dist/cells/ActionButtons.svelte.d.ts +74 -0
- package/dist/cells/CopyButton.svelte +89 -0
- package/dist/cells/CopyButton.svelte.d.ts +33 -0
- package/dist/cells/CustomCell.svelte +136 -0
- package/dist/cells/CustomCell.svelte.d.ts +44 -0
- package/dist/cells/DateCell.svelte +194 -0
- package/dist/cells/DateCell.svelte.d.ts +39 -0
- package/dist/cells/LinkCell.svelte +240 -0
- package/dist/cells/LinkCell.svelte.d.ts +42 -0
- package/dist/cells/NumberCell.svelte +225 -0
- package/dist/cells/NumberCell.svelte.d.ts +47 -0
- package/dist/cells/StatusBadge.svelte +121 -0
- package/dist/cells/StatusBadge.svelte.d.ts +44 -0
- package/dist/cells/UserAvatar.svelte +71 -0
- package/dist/cells/UserAvatar.svelte.d.ts +37 -0
- package/dist/cells/index.d.ts +8 -0
- package/dist/cells/index.js +9 -0
- package/dist/core/EmptyState.svelte +161 -0
- package/dist/core/EmptyState.svelte.d.ts +16 -0
- package/dist/core/ErrorState.svelte +158 -0
- package/dist/core/ErrorState.svelte.d.ts +15 -0
- package/dist/core/GroupedRow.svelte +239 -0
- package/dist/core/GroupedRow.svelte.d.ts +18 -0
- package/dist/core/LoadingState.svelte +75 -0
- package/dist/core/LoadingState.svelte.d.ts +14 -0
- package/dist/core/MobileCard.svelte +151 -0
- package/dist/core/MobileCard.svelte.d.ts +15 -0
- package/dist/core/TableCell.svelte +105 -0
- package/dist/core/TableCell.svelte.d.ts +14 -0
- package/dist/core/TableDesktop.svelte +480 -0
- package/dist/core/TableDesktop.svelte.d.ts +26 -0
- package/dist/core/TableHead.svelte +314 -0
- package/dist/core/TableHead.svelte.d.ts +7 -0
- package/dist/core/TableMobile.svelte +112 -0
- package/dist/core/TableMobile.svelte.d.ts +13 -0
- package/dist/core/TableProvider.svelte +271 -0
- package/dist/core/TableProvider.svelte.d.ts +40 -0
- package/dist/core/TableRow.svelte +171 -0
- package/dist/core/TableRow.svelte.d.ts +16 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +14 -0
- package/dist/core/sticky-context.svelte.d.ts +48 -0
- package/dist/core/sticky-context.svelte.js +88 -0
- package/dist/core/table/Table.svelte +304 -0
- package/dist/core/table/Table.svelte.d.ts +26 -0
- package/dist/core/table/index.d.ts +448 -0
- package/dist/core/table/index.js +1 -0
- package/dist/core/table-style-context.d.ts +66 -0
- package/dist/core/table-style-context.js +26 -0
- package/dist/factories/ColumnValidation.d.ts +49 -0
- package/dist/factories/ColumnValidation.js +188 -0
- package/dist/factories/TableColumns.d.ts +97 -0
- package/dist/factories/TableColumns.js +262 -0
- package/dist/factories/TypedColumnBuilder.d.ts +41 -0
- package/dist/factories/TypedColumnBuilder.js +72 -0
- package/dist/factories/index.d.ts +12 -0
- package/dist/factories/index.js +13 -0
- package/dist/features/HeaderMenu.svelte +236 -0
- package/dist/features/HeaderMenu.svelte.d.ts +8 -0
- package/dist/features/LiveUpdateBanner.svelte +66 -0
- package/dist/features/LiveUpdateBanner.svelte.d.ts +6 -0
- package/dist/features/SearchHighlight.svelte +21 -0
- package/dist/features/SearchHighlight.svelte.d.ts +8 -0
- package/dist/features/SmartFilterBar/ChipsField.svelte +104 -0
- package/dist/features/SmartFilterBar/ChipsField.svelte.d.ts +5 -0
- package/dist/features/SmartFilterBar/ColumnVisibilityMenu.svelte +84 -0
- package/dist/features/SmartFilterBar/ColumnVisibilityMenu.svelte.d.ts +3 -0
- package/dist/features/SmartFilterBar/FilterMenu.svelte +367 -0
- package/dist/features/SmartFilterBar/FilterMenu.svelte.d.ts +3 -0
- package/dist/features/SmartFilterBar/GroupingMenu.svelte +82 -0
- package/dist/features/SmartFilterBar/GroupingMenu.svelte.d.ts +3 -0
- package/dist/features/SmartFilterBar/SmartFilterBar.svelte +109 -0
- package/dist/features/SmartFilterBar/SmartFilterBar.svelte.d.ts +11 -0
- package/dist/features/SmartFilterBar/SummaryMenu.svelte +118 -0
- package/dist/features/SmartFilterBar/SummaryMenu.svelte.d.ts +3 -0
- package/dist/features/SummaryRow.svelte +97 -0
- package/dist/features/SummaryRow.svelte.d.ts +8 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.js +4 -0
- package/dist/i18n/index.d.ts +366 -0
- package/dist/i18n/index.js +21 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +41 -0
- package/dist/stores/TableStore.svelte.d.ts +192 -0
- package/dist/stores/TableStore.svelte.js +362 -0
- package/dist/stores/concerns/index.d.ts +15 -0
- package/dist/stores/concerns/index.js +14 -0
- package/dist/stores/concerns/types.d.ts +31 -0
- package/dist/stores/concerns/types.js +1 -0
- package/dist/stores/concerns/useColumnOrder.svelte.d.ts +16 -0
- package/dist/stores/concerns/useColumnOrder.svelte.js +81 -0
- package/dist/stores/concerns/useColumnVisibility.svelte.d.ts +16 -0
- package/dist/stores/concerns/useColumnVisibility.svelte.js +58 -0
- package/dist/stores/concerns/useExpansion.svelte.d.ts +9 -0
- package/dist/stores/concerns/useExpansion.svelte.js +32 -0
- package/dist/stores/concerns/useFiltering.svelte.d.ts +20 -0
- package/dist/stores/concerns/useFiltering.svelte.js +109 -0
- package/dist/stores/concerns/useFocusManagement.svelte.d.ts +15 -0
- package/dist/stores/concerns/useFocusManagement.svelte.js +52 -0
- package/dist/stores/concerns/useGrouping.svelte.d.ts +15 -0
- package/dist/stores/concerns/useGrouping.svelte.js +86 -0
- package/dist/stores/concerns/useLiveUpdates.svelte.d.ts +45 -0
- package/dist/stores/concerns/useLiveUpdates.svelte.js +175 -0
- package/dist/stores/concerns/usePagination.svelte.d.ts +18 -0
- package/dist/stores/concerns/usePagination.svelte.js +54 -0
- package/dist/stores/concerns/usePersistence.svelte.d.ts +36 -0
- package/dist/stores/concerns/usePersistence.svelte.js +167 -0
- package/dist/stores/concerns/useRemoteData.svelte.d.ts +21 -0
- package/dist/stores/concerns/useRemoteData.svelte.js +64 -0
- package/dist/stores/concerns/useSearch.svelte.d.ts +8 -0
- package/dist/stores/concerns/useSearch.svelte.js +16 -0
- package/dist/stores/concerns/useSelection.svelte.d.ts +21 -0
- package/dist/stores/concerns/useSelection.svelte.js +110 -0
- package/dist/stores/concerns/useSorting.svelte.d.ts +11 -0
- package/dist/stores/concerns/useSorting.svelte.js +70 -0
- package/dist/stores/concerns/useSummary.svelte.d.ts +18 -0
- package/dist/stores/concerns/useSummary.svelte.js +96 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/style/index.css +137 -0
- package/dist/style/index.d.ts +2 -0
- package/dist/style/index.js +2 -0
- package/dist/style/table-theme.css +131 -0
- package/dist/style/themes/comfortable.css +20 -0
- package/dist/style/themes/compact.css +20 -0
- package/dist/translations/de.d.ts +177 -0
- package/dist/translations/de.js +176 -0
- package/dist/translations/en.d.ts +177 -0
- package/dist/translations/en.js +176 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/tableTypes.d.ts +262 -0
- package/dist/types/tableTypes.js +1 -0
- package/dist/utils/index.d.ts +165 -0
- package/dist/utils/index.js +330 -0
- package/dist/utils/sticky-measure.d.ts +54 -0
- package/dist/utils/sticky-measure.js +107 -0
- package/dist/utils/virtualizer.d.ts +43 -0
- package/dist/utils/virtualizer.js +43 -0
- package/dist/variants/index.d.ts +11 -0
- package/dist/variants/index.js +15 -0
- package/dist/variants/table-cells.variants.d.ts +827 -0
- package/dist/variants/table-cells.variants.js +627 -0
- package/dist/variants/table-features.variants.d.ts +547 -0
- package/dist/variants/table-features.variants.js +412 -0
- package/dist/variants/table-states.variants.d.ts +594 -0
- package/dist/variants/table-states.variants.js +394 -0
- package/dist/variants/table.system.d.ts +301 -0
- package/dist/variants/table.system.js +314 -0
- package/dist/variants/table.variants.d.ts +428 -0
- package/dist/variants/table.variants.js +360 -0
- package/package.json +93 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useTableI18n } from '../..';
|
|
3
|
+
import { getTableContext } from '../../stores/TableStore.svelte';
|
|
4
|
+
import type { FilterOperator } from '../../types/tableTypes';
|
|
5
|
+
import { resolveColumnId, resolveColumnLabel, resolveValueById } from '../../utils';
|
|
6
|
+
import { filterMenuVariants } from '../../variants';
|
|
7
|
+
import {
|
|
8
|
+
Badge,
|
|
9
|
+
Button,
|
|
10
|
+
Select,
|
|
11
|
+
Input,
|
|
12
|
+
Popover,
|
|
13
|
+
Tooltip,
|
|
14
|
+
useBlocksI18n,
|
|
15
|
+
resolveIcon,
|
|
16
|
+
CheckIcon as CheckIconDefault,
|
|
17
|
+
FunnelIcon as FunnelIconDefault,
|
|
18
|
+
CloseIcon as CloseIconDefault
|
|
19
|
+
} from '@urbicon-ui/blocks';
|
|
20
|
+
|
|
21
|
+
const tt = useTableI18n();
|
|
22
|
+
const bt = useBlocksI18n();
|
|
23
|
+
|
|
24
|
+
const CheckIcon = resolveIcon('check', CheckIconDefault);
|
|
25
|
+
const FunnelIcon = resolveIcon('funnel', FunnelIconDefault);
|
|
26
|
+
const CloseIcon = resolveIcon('close', CloseIconDefault);
|
|
27
|
+
|
|
28
|
+
// Get table context
|
|
29
|
+
const tableContext = getTableContext();
|
|
30
|
+
const {
|
|
31
|
+
state: tableState,
|
|
32
|
+
addFilter,
|
|
33
|
+
removeFiltersByColumn,
|
|
34
|
+
clearAllFilters,
|
|
35
|
+
hasFilterForColumn
|
|
36
|
+
} = tableContext;
|
|
37
|
+
|
|
38
|
+
// Internal state
|
|
39
|
+
let isOpen = $state(false);
|
|
40
|
+
let triggerButtonRef = $state<HTMLButtonElement | undefined>();
|
|
41
|
+
|
|
42
|
+
// Reactive data
|
|
43
|
+
const OPERATORS_BY_TYPE = {
|
|
44
|
+
number: [
|
|
45
|
+
{ value: 'equals' as const, label: () => tt('filter.operators.equals') },
|
|
46
|
+
{ value: 'greaterThan' as const, label: () => tt('filter.operators.greaterThan') },
|
|
47
|
+
{ value: 'lessThan' as const, label: () => tt('filter.operators.lessThan') }
|
|
48
|
+
],
|
|
49
|
+
date: [
|
|
50
|
+
{ value: 'equals' as const, label: () => tt('filter.operators.onDate') },
|
|
51
|
+
{ value: 'greaterThan' as const, label: () => tt('filter.operators.after') },
|
|
52
|
+
{ value: 'lessThan' as const, label: () => tt('filter.operators.before') }
|
|
53
|
+
],
|
|
54
|
+
text: [
|
|
55
|
+
{ value: 'contains' as const, label: () => tt('filter.operators.contains') },
|
|
56
|
+
{ value: 'equals' as const, label: () => tt('filter.operators.equals') },
|
|
57
|
+
{ value: 'startsWith' as const, label: () => tt('filter.operators.startsWith') },
|
|
58
|
+
{ value: 'endsWith' as const, label: () => tt('filter.operators.endsWith') }
|
|
59
|
+
]
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
const getOperatorsForType = (dataType: string) => {
|
|
63
|
+
const operators =
|
|
64
|
+
OPERATORS_BY_TYPE[dataType as keyof typeof OPERATORS_BY_TYPE] || OPERATORS_BY_TYPE.text;
|
|
65
|
+
return operators.map((op) => ({ value: op.value, label: op.label() }));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const activeFilters = $derived(tableState.activeFilters);
|
|
69
|
+
const filterOptions = $derived.by(() => {
|
|
70
|
+
return tableState.columns
|
|
71
|
+
.filter((col) => col.accessor !== undefined && col.searchable !== false)
|
|
72
|
+
.map((col) => {
|
|
73
|
+
const dataType = 'dataType' in col ? col.dataType || 'text' : 'text';
|
|
74
|
+
return {
|
|
75
|
+
key: resolveColumnId(col),
|
|
76
|
+
label: resolveColumnLabel(col),
|
|
77
|
+
dataType,
|
|
78
|
+
operators: getOperatorsForType(dataType)
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let filterStates = $state<
|
|
84
|
+
Record<
|
|
85
|
+
string,
|
|
86
|
+
{
|
|
87
|
+
selectedOperator: FilterOperator;
|
|
88
|
+
inputValue: string;
|
|
89
|
+
quickValueSearch: string;
|
|
90
|
+
showQuickValues: boolean;
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
>({});
|
|
94
|
+
|
|
95
|
+
$effect.pre(() => {
|
|
96
|
+
filterOptions.forEach((option) => {
|
|
97
|
+
if (!filterStates[option.key]) {
|
|
98
|
+
filterStates[option.key] = {
|
|
99
|
+
selectedOperator: 'contains',
|
|
100
|
+
inputValue: '',
|
|
101
|
+
quickValueSearch: '',
|
|
102
|
+
showQuickValues: false
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function getUniqueValues(columnKey: string): string[] {
|
|
109
|
+
// Local dedup accumulator — not reactive state.
|
|
110
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
111
|
+
const values = new Set<string>();
|
|
112
|
+
for (const item of tableState.items) {
|
|
113
|
+
const value = resolveValueById(tableState.columns, item, columnKey);
|
|
114
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
115
|
+
values.add(String(value));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Array.from(values).sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Simple function to get active filters for a column
|
|
122
|
+
function getActiveFiltersForColumn(columnKey: string) {
|
|
123
|
+
return activeFilters.filter((filter) => filter.column === columnKey);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Event handlers
|
|
127
|
+
function handleApplyFilter(optionKey: string, value?: string, shouldClose = false) {
|
|
128
|
+
const state = filterStates[optionKey];
|
|
129
|
+
if (!state) return;
|
|
130
|
+
|
|
131
|
+
const filterValue = value || state.inputValue;
|
|
132
|
+
|
|
133
|
+
// Add new filter if value is not empty
|
|
134
|
+
if (filterValue.trim()) {
|
|
135
|
+
addFilter({
|
|
136
|
+
column: optionKey,
|
|
137
|
+
operator: state.selectedOperator,
|
|
138
|
+
value: filterValue.trim()
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Clear input after applying
|
|
142
|
+
state.inputValue = '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (shouldClose) {
|
|
146
|
+
isOpen = false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleApplyAllFilters() {
|
|
151
|
+
// Apply all non-empty manual filter inputs
|
|
152
|
+
filterOptions.forEach((option) => {
|
|
153
|
+
const state = filterStates[option.key];
|
|
154
|
+
if (state?.inputValue.trim()) {
|
|
155
|
+
handleApplyFilter(option.key);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
isOpen = false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function handleCancel() {
|
|
162
|
+
isOpen = false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toggleQuickFilter(optionKey: string, value: string) {
|
|
166
|
+
const isActive = hasFilterForColumn(optionKey, 'contains', value);
|
|
167
|
+
if (isActive) {
|
|
168
|
+
removeFiltersByColumn(optionKey, 'contains', value);
|
|
169
|
+
} else {
|
|
170
|
+
handleApplyFilter(optionKey, value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleRemoveSpecificFilter = (column: string, operator: FilterOperator, value: string) => {
|
|
175
|
+
removeFiltersByColumn(column, operator, value);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const menuStyles = $derived(
|
|
179
|
+
filterMenuVariants({
|
|
180
|
+
size: 'md'
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
{#snippet triggerContent()}
|
|
186
|
+
<Tooltip label={tt('filter.button.add')}>
|
|
187
|
+
<Button
|
|
188
|
+
variant="ghost"
|
|
189
|
+
intent="neutral"
|
|
190
|
+
size="sm"
|
|
191
|
+
active={activeFilters.length > 0}
|
|
192
|
+
aria-expanded={isOpen}
|
|
193
|
+
aria-haspopup="true"
|
|
194
|
+
>
|
|
195
|
+
<FunnelIcon class="h-4 w-4" />
|
|
196
|
+
{#if activeFilters.length > 0}
|
|
197
|
+
<Badge variant="filled" size="xs" counter class="bg-filter text-text-on-primary ml-1">
|
|
198
|
+
{activeFilters.length}
|
|
199
|
+
</Badge>
|
|
200
|
+
{/if}
|
|
201
|
+
</Button>
|
|
202
|
+
</Tooltip>
|
|
203
|
+
{/snippet}
|
|
204
|
+
|
|
205
|
+
<Popover
|
|
206
|
+
bind:open={isOpen}
|
|
207
|
+
bind:triggerElement={triggerButtonRef}
|
|
208
|
+
placement="bottom-start"
|
|
209
|
+
offsetDistance={8}
|
|
210
|
+
onClickOutside={() => (isOpen = false)}
|
|
211
|
+
trigger={triggerContent}
|
|
212
|
+
role="dialog"
|
|
213
|
+
class={menuStyles.base()}
|
|
214
|
+
style="max-height: calc(100vh - 100px); overflow-y: auto;"
|
|
215
|
+
>
|
|
216
|
+
<!-- Header with tailwind-variants - ALLE Styles bleiben! -->
|
|
217
|
+
<div class={menuStyles.header()}>
|
|
218
|
+
<h3 class={menuStyles.title()}>
|
|
219
|
+
{tt('filter.menu.addFilter')}
|
|
220
|
+
</h3>
|
|
221
|
+
<div class="flex items-center gap-2">
|
|
222
|
+
{#if activeFilters.length > 0}
|
|
223
|
+
<Button
|
|
224
|
+
variant="ghost"
|
|
225
|
+
size="xs"
|
|
226
|
+
intent="danger"
|
|
227
|
+
onclick={() => {
|
|
228
|
+
clearAllFilters();
|
|
229
|
+
isOpen = false;
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{tt('filter.button.clearAll')}
|
|
233
|
+
</Button>
|
|
234
|
+
{/if}
|
|
235
|
+
<Button
|
|
236
|
+
variant="ghost"
|
|
237
|
+
size="xs"
|
|
238
|
+
onclick={() => (isOpen = false)}
|
|
239
|
+
aria-label={tt('button.close')}
|
|
240
|
+
>
|
|
241
|
+
<CloseIcon class="h-4 w-4" />
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div class="space-y-4">
|
|
247
|
+
{#each filterOptions as option (option.key)}
|
|
248
|
+
{@const state = filterStates[option.key]}
|
|
249
|
+
{@const uniqueValues = getUniqueValues(option.key)}
|
|
250
|
+
|
|
251
|
+
<div class={menuStyles.section()}>
|
|
252
|
+
<!-- Section Header -->
|
|
253
|
+
<div class="flex items-center justify-between">
|
|
254
|
+
<h4 class={menuStyles.sectionTitle()}>
|
|
255
|
+
{option.label}
|
|
256
|
+
</h4>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{#if state}
|
|
260
|
+
{@const columnFilters = getActiveFiltersForColumn(option.key)}
|
|
261
|
+
|
|
262
|
+
<div class="space-y-2">
|
|
263
|
+
<!-- Show existing filters for this column -->
|
|
264
|
+
{#if columnFilters.length > 0}
|
|
265
|
+
<div class="space-y-1">
|
|
266
|
+
{#each columnFilters as filter, i (`${filter.operator}:${filter.value}:${i}`)}
|
|
267
|
+
<div class={menuStyles.activeFilter()}>
|
|
268
|
+
<span class="text-text-primary flex-1 truncate text-sm">
|
|
269
|
+
{tt(`filter.operators.${filter.operator}`)}: {filter.value}
|
|
270
|
+
</span>
|
|
271
|
+
<Button
|
|
272
|
+
variant="ghost"
|
|
273
|
+
size="xs"
|
|
274
|
+
intent="danger"
|
|
275
|
+
onclick={() =>
|
|
276
|
+
handleRemoveSpecificFilter(filter.column, filter.operator, filter.value)}
|
|
277
|
+
class="ml-2 flex-shrink-0"
|
|
278
|
+
aria-label={tt('filter.button.remove')}
|
|
279
|
+
>
|
|
280
|
+
<CloseIcon class="h-3 w-3" />
|
|
281
|
+
</Button>
|
|
282
|
+
</div>
|
|
283
|
+
{/each}
|
|
284
|
+
</div>
|
|
285
|
+
{/if}
|
|
286
|
+
|
|
287
|
+
<!-- Operator Select and Input in one row -->
|
|
288
|
+
<div class={menuStyles.filterRow()}>
|
|
289
|
+
<div class={menuStyles.operatorSelect()}>
|
|
290
|
+
<Select
|
|
291
|
+
options={option.operators}
|
|
292
|
+
bind:value={state.selectedOperator}
|
|
293
|
+
usePortal={false}
|
|
294
|
+
size="sm"
|
|
295
|
+
variant="outlined"
|
|
296
|
+
class="w-full"
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div class={menuStyles.valueInput()}>
|
|
301
|
+
<Input
|
|
302
|
+
type={option.dataType === 'date' ? 'date' : 'text'}
|
|
303
|
+
placeholder={tt('filter.input.enterValue')}
|
|
304
|
+
bind:value={state.inputValue}
|
|
305
|
+
size="sm"
|
|
306
|
+
variant="outlined"
|
|
307
|
+
class="w-full"
|
|
308
|
+
onkeydown={(e) => {
|
|
309
|
+
if (e.key === 'Enter') {
|
|
310
|
+
handleApplyFilter(option.key);
|
|
311
|
+
}
|
|
312
|
+
}}
|
|
313
|
+
clearable={true}
|
|
314
|
+
onClear={() => (state.inputValue = '')}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- Quick Values with TV styling -->
|
|
320
|
+
{#if option.dataType === 'text' && uniqueValues.length > 0}
|
|
321
|
+
<div class="space-y-2">
|
|
322
|
+
<Button
|
|
323
|
+
variant="ghost"
|
|
324
|
+
size="xs"
|
|
325
|
+
onclick={() => (state.showQuickValues = !state.showQuickValues)}
|
|
326
|
+
>
|
|
327
|
+
{tt('filter.quickValues.title')} ({uniqueValues.length})
|
|
328
|
+
</Button>
|
|
329
|
+
|
|
330
|
+
{#if state.showQuickValues}
|
|
331
|
+
<div class={menuStyles.quickValues()}>
|
|
332
|
+
{#each uniqueValues.slice(0, 20) as value (value)}
|
|
333
|
+
{@const isActive = hasFilterForColumn(option.key, 'contains', value)}
|
|
334
|
+
<Button
|
|
335
|
+
variant={isActive ? 'filled' : 'outlined'}
|
|
336
|
+
size="xs"
|
|
337
|
+
intent={isActive ? 'primary' : 'neutral'}
|
|
338
|
+
onclick={() => toggleQuickFilter(option.key, value)}
|
|
339
|
+
class="min-w-0 justify-start truncate text-left"
|
|
340
|
+
title={value}
|
|
341
|
+
>
|
|
342
|
+
{#if isActive}
|
|
343
|
+
<CheckIcon class="mr-1 h-3 w-3 flex-shrink-0" />
|
|
344
|
+
{/if}
|
|
345
|
+
<span class="truncate">{value}</span>
|
|
346
|
+
</Button>
|
|
347
|
+
{/each}
|
|
348
|
+
</div>
|
|
349
|
+
{/if}
|
|
350
|
+
</div>
|
|
351
|
+
{/if}
|
|
352
|
+
</div>
|
|
353
|
+
{/if}
|
|
354
|
+
</div>
|
|
355
|
+
{/each}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<!-- Footer with TV styling -->
|
|
359
|
+
<div class={menuStyles.footer()}>
|
|
360
|
+
<Button variant="outlined" size="sm" intent="neutral" onclick={handleCancel}>
|
|
361
|
+
{bt('button.cancel')}
|
|
362
|
+
</Button>
|
|
363
|
+
<Button variant="filled" size="sm" intent="primary" onclick={handleApplyAllFilters}>
|
|
364
|
+
{bt('button.apply')}
|
|
365
|
+
</Button>
|
|
366
|
+
</div>
|
|
367
|
+
</Popover>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTableContext, useTableI18n } from '../..';
|
|
3
|
+
import { resolveColumnId, resolveColumnLabel } from '../../utils';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Select,
|
|
7
|
+
Tooltip,
|
|
8
|
+
resolveIcon,
|
|
9
|
+
LayersIcon as LayersIconDefault,
|
|
10
|
+
CheckIcon as CheckIconDefault
|
|
11
|
+
} from '@urbicon-ui/blocks';
|
|
12
|
+
|
|
13
|
+
const tt = useTableI18n();
|
|
14
|
+
|
|
15
|
+
const LayersIcon = resolveIcon('layers', LayersIconDefault);
|
|
16
|
+
const CheckIcon = resolveIcon('check', CheckIconDefault);
|
|
17
|
+
|
|
18
|
+
const tableContext = getTableContext();
|
|
19
|
+
const { state: tableState, setGroupByKey } = tableContext;
|
|
20
|
+
|
|
21
|
+
const currentValue = $derived(tableState.groupByKey || '');
|
|
22
|
+
const isActive = $derived(!!currentValue);
|
|
23
|
+
|
|
24
|
+
const groupableColumns = $derived.by(() => {
|
|
25
|
+
return tableState.columns.filter((col) => {
|
|
26
|
+
// Synthetic columns have no accessor and structurally lack the
|
|
27
|
+
// derivable flags — exclude before reading them.
|
|
28
|
+
if (col.accessor === undefined) return false;
|
|
29
|
+
if (col.groupable !== undefined) return col.groupable === true;
|
|
30
|
+
const id = resolveColumnId(col);
|
|
31
|
+
return col.sortable === true && id !== 'actions' && !id.includes('action');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const groupingOptions = $derived.by(() => {
|
|
36
|
+
const options = [{ label: tt('grouping.none'), value: '' }];
|
|
37
|
+
|
|
38
|
+
groupableColumns.forEach((column) => {
|
|
39
|
+
options.push({
|
|
40
|
+
label: resolveColumnLabel(column),
|
|
41
|
+
value: resolveColumnId(column)
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return options;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let menuOpen = $state(false);
|
|
49
|
+
|
|
50
|
+
function handleValueChange(value: string) {
|
|
51
|
+
setGroupByKey(value === '' ? null : value);
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#snippet customTrigger(_selected: unknown[], _open: boolean, _clear: () => void)}
|
|
56
|
+
<Tooltip label={tt('grouping.button')}>
|
|
57
|
+
<Button
|
|
58
|
+
variant="ghost"
|
|
59
|
+
intent="neutral"
|
|
60
|
+
size="sm"
|
|
61
|
+
active={isActive}
|
|
62
|
+
aria-expanded={menuOpen}
|
|
63
|
+
aria-haspopup="listbox"
|
|
64
|
+
onclick={() => (menuOpen = !menuOpen)}
|
|
65
|
+
>
|
|
66
|
+
<LayersIcon class="h-4 w-4" />
|
|
67
|
+
{#if isActive}
|
|
68
|
+
<CheckIcon class="h-3 w-3" />
|
|
69
|
+
{/if}
|
|
70
|
+
</Button>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
{/snippet}
|
|
73
|
+
|
|
74
|
+
<Select
|
|
75
|
+
options={groupingOptions}
|
|
76
|
+
value={currentValue}
|
|
77
|
+
bind:open={menuOpen}
|
|
78
|
+
onValueChange={(v: string | null) => handleValueChange(v ?? '')}
|
|
79
|
+
size="sm"
|
|
80
|
+
syncWidth={false}
|
|
81
|
+
{customTrigger}
|
|
82
|
+
/>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { getTableContext, useTableI18n } from '../..';
|
|
4
|
+
import { Input, Toolbar, resolveIcon, SearchIcon as SearchIconDefault } from '@urbicon-ui/blocks';
|
|
5
|
+
|
|
6
|
+
const SearchIcon = resolveIcon('search', SearchIconDefault);
|
|
7
|
+
import { smartFilterBarVariants, type SmartFilterBarVariantProps } from '../../variants';
|
|
8
|
+
import ChipsField from './ChipsField.svelte';
|
|
9
|
+
import ColumnVisibilityMenu from './ColumnVisibilityMenu.svelte';
|
|
10
|
+
import FilterMenu from './FilterMenu.svelte';
|
|
11
|
+
import GroupingMenu from './GroupingMenu.svelte';
|
|
12
|
+
import SummaryMenu from './SummaryMenu.svelte';
|
|
13
|
+
import { getTableStyleConfig, resolveSlotClass } from '../../core/table-style-context';
|
|
14
|
+
|
|
15
|
+
const tt = useTableI18n();
|
|
16
|
+
|
|
17
|
+
// Store-Kontext abrufen
|
|
18
|
+
const tableContext = getTableContext();
|
|
19
|
+
const { state: tableState, setSearchTerm } = tableContext;
|
|
20
|
+
const styleConfig = getTableStyleConfig();
|
|
21
|
+
|
|
22
|
+
// Props
|
|
23
|
+
let {
|
|
24
|
+
placeholder = tt('search.placeholder'),
|
|
25
|
+
debounceMs = 300,
|
|
26
|
+
size = 'md' as SmartFilterBarVariantProps['size'],
|
|
27
|
+
layout = 'responsive' as SmartFilterBarVariantProps['layout'],
|
|
28
|
+
responsive = false,
|
|
29
|
+
class: className = ''
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
// Local state — decouples UI input from the store so debouncing is not bypassed
|
|
33
|
+
let localSearch = $state(tableState.searchTerm);
|
|
34
|
+
let debounceTimer = $state<number | null>(null);
|
|
35
|
+
|
|
36
|
+
// Sync store → local (e.g. when the store is changed programmatically)
|
|
37
|
+
$effect(() => {
|
|
38
|
+
const storeValue = tableState.searchTerm;
|
|
39
|
+
if (storeValue !== untrack(() => localSearch)) {
|
|
40
|
+
localSearch = storeValue;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function handleSearchInput(event: Event) {
|
|
45
|
+
const target = event.target as HTMLInputElement;
|
|
46
|
+
localSearch = target.value;
|
|
47
|
+
|
|
48
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
49
|
+
debounceTimer = setTimeout(() => {
|
|
50
|
+
setSearchTerm(localSearch);
|
|
51
|
+
}, debounceMs) as unknown as number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
55
|
+
if (event.key === 'Escape' && localSearch) {
|
|
56
|
+
localSearch = '';
|
|
57
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
58
|
+
setSearchTerm('');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Tailwind-Variants Styling
|
|
63
|
+
const filterBarStyles = $derived(
|
|
64
|
+
smartFilterBarVariants({
|
|
65
|
+
size,
|
|
66
|
+
layout,
|
|
67
|
+
elevated: responsive,
|
|
68
|
+
appearance: styleConfig.appearance
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div
|
|
74
|
+
class={resolveSlotClass(
|
|
75
|
+
filterBarStyles.container(),
|
|
76
|
+
styleConfig.slotClasses.filterBar,
|
|
77
|
+
styleConfig.unstyled,
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<Toolbar aria-label={tt('aria.filterBar')} variant="ghost" gap="xs" padding="xs" class="w-full">
|
|
82
|
+
<div class={filterBarStyles.searchSection()}>
|
|
83
|
+
{#snippet searchIcon()}
|
|
84
|
+
<SearchIcon class="h-4 w-4" />
|
|
85
|
+
{/snippet}
|
|
86
|
+
<Input
|
|
87
|
+
type="search"
|
|
88
|
+
{placeholder}
|
|
89
|
+
value={localSearch}
|
|
90
|
+
oninput={handleSearchInput}
|
|
91
|
+
onkeydown={handleKeydown}
|
|
92
|
+
leftIcon={searchIcon}
|
|
93
|
+
class="w-full"
|
|
94
|
+
aria-label={tt('aria.searchData')}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="{filterBarStyles.actionsSection()} ml-auto gap-1">
|
|
99
|
+
<FilterMenu />
|
|
100
|
+
<GroupingMenu />
|
|
101
|
+
<SummaryMenu />
|
|
102
|
+
<ColumnVisibilityMenu />
|
|
103
|
+
</div>
|
|
104
|
+
</Toolbar>
|
|
105
|
+
|
|
106
|
+
<div class={filterBarStyles.chipsSection()}>
|
|
107
|
+
<ChipsField />
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SmartFilterBarVariantProps } from '../../variants';
|
|
2
|
+
declare const SmartFilterBar: import("svelte").Component<{
|
|
3
|
+
placeholder?: any;
|
|
4
|
+
debounceMs?: number;
|
|
5
|
+
size?: SmartFilterBarVariantProps["size"];
|
|
6
|
+
layout?: SmartFilterBarVariantProps["layout"];
|
|
7
|
+
responsive?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
}, {}, "">;
|
|
10
|
+
type SmartFilterBar = ReturnType<typeof SmartFilterBar>;
|
|
11
|
+
export default SmartFilterBar;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTableContext, useTableI18n } from '../..';
|
|
3
|
+
import { resolveColumnId, resolveColumnLabel } from '../../utils';
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
Select,
|
|
8
|
+
Tooltip,
|
|
9
|
+
resolveIcon,
|
|
10
|
+
SquareSigmaIcon as SquareSigmaIconDefault
|
|
11
|
+
} from '@urbicon-ui/blocks';
|
|
12
|
+
|
|
13
|
+
const tt = useTableI18n();
|
|
14
|
+
|
|
15
|
+
const SquareSigmaIcon = resolveIcon('squareSigma', SquareSigmaIconDefault);
|
|
16
|
+
|
|
17
|
+
interface SummaryConfig {
|
|
18
|
+
column: string;
|
|
19
|
+
type: 'sum' | 'avg' | 'count' | 'min' | 'max';
|
|
20
|
+
formatter?: (value: unknown) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tableContext = getTableContext();
|
|
24
|
+
const { state: tableState, addSummaryConfig } = tableContext;
|
|
25
|
+
|
|
26
|
+
const summaryConfigs = $derived(tableState.summaryConfigs);
|
|
27
|
+
const isActive = $derived(summaryConfigs.length > 0);
|
|
28
|
+
|
|
29
|
+
let selectedValue = $state('');
|
|
30
|
+
let menuOpen = $state(false);
|
|
31
|
+
|
|
32
|
+
const summableColumns = $derived.by(() => {
|
|
33
|
+
return tableState.columns.filter((col) => {
|
|
34
|
+
// Synthetic columns cannot be summed.
|
|
35
|
+
if (col.accessor === undefined) return false;
|
|
36
|
+
if (col.summable !== undefined) return col.summable === true;
|
|
37
|
+
return (
|
|
38
|
+
col.dataType === 'number' ||
|
|
39
|
+
/^(age|salary|price|amount|count|number|projectsCompleted|rating|score)$/i.test(
|
|
40
|
+
resolveColumnId(col)
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const summaryTypes = [
|
|
47
|
+
{ value: 'sum', label: tt('summary.types.sum'), icon: '∑' },
|
|
48
|
+
{ value: 'avg', label: tt('summary.types.average'), icon: '⌀' },
|
|
49
|
+
{ value: 'count', label: tt('summary.types.count'), icon: '#' },
|
|
50
|
+
{ value: 'min', label: tt('summary.types.minimum'), icon: '↓' },
|
|
51
|
+
{ value: 'max', label: tt('summary.types.maximum'), icon: '↑' }
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
const menuGroups = $derived.by(() => {
|
|
55
|
+
if (summableColumns.length === 0) return [];
|
|
56
|
+
|
|
57
|
+
return summableColumns.map((column) => {
|
|
58
|
+
const columnId = resolveColumnId(column);
|
|
59
|
+
return {
|
|
60
|
+
label: resolveColumnLabel(column),
|
|
61
|
+
options: summaryTypes.map((type) => ({
|
|
62
|
+
label: `${type.icon} ${type.label}`,
|
|
63
|
+
value: `${columnId}:${type.value}`,
|
|
64
|
+
disabled: summaryConfigs.some(
|
|
65
|
+
(config) => config.column === columnId && config.type === type.value
|
|
66
|
+
)
|
|
67
|
+
}))
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function handleValueChange(value: string) {
|
|
73
|
+
if (!value) return;
|
|
74
|
+
|
|
75
|
+
const [columnKey, type] = value.split(':');
|
|
76
|
+
if (columnKey && type) {
|
|
77
|
+
const summaryConfig = {
|
|
78
|
+
column: columnKey,
|
|
79
|
+
type: type as SummaryConfig['type']
|
|
80
|
+
};
|
|
81
|
+
addSummaryConfig(summaryConfig);
|
|
82
|
+
selectedValue = '';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
{#snippet customTrigger(_selected: unknown[], _open: boolean, _clear: () => void)}
|
|
88
|
+
<Tooltip label={tt('summary.button.title')}>
|
|
89
|
+
<Button
|
|
90
|
+
variant="ghost"
|
|
91
|
+
intent="neutral"
|
|
92
|
+
size="sm"
|
|
93
|
+
active={isActive}
|
|
94
|
+
aria-expanded={menuOpen}
|
|
95
|
+
aria-haspopup="listbox"
|
|
96
|
+
disabled={summableColumns.length === 0}
|
|
97
|
+
onclick={() => (menuOpen = !menuOpen)}
|
|
98
|
+
>
|
|
99
|
+
<SquareSigmaIcon class="h-4 w-4" />
|
|
100
|
+
{#if isActive}
|
|
101
|
+
<Badge variant="filled" size="xs" counter class="bg-summary text-text-on-primary ml-1">
|
|
102
|
+
{summaryConfigs.length}
|
|
103
|
+
</Badge>
|
|
104
|
+
{/if}
|
|
105
|
+
</Button>
|
|
106
|
+
</Tooltip>
|
|
107
|
+
{/snippet}
|
|
108
|
+
|
|
109
|
+
<Select
|
|
110
|
+
groups={menuGroups}
|
|
111
|
+
bind:value={selectedValue}
|
|
112
|
+
bind:open={menuOpen}
|
|
113
|
+
onValueChange={(v: string | null) => handleValueChange(v ?? '')}
|
|
114
|
+
disabled={summableColumns.length === 0}
|
|
115
|
+
size="sm"
|
|
116
|
+
syncWidth={false}
|
|
117
|
+
{customTrigger}
|
|
118
|
+
/>
|