@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.
Files changed (154) hide show
  1. package/README.md +153 -0
  2. package/dist/cells/ActionButtons.svelte +224 -0
  3. package/dist/cells/ActionButtons.svelte.d.ts +74 -0
  4. package/dist/cells/CopyButton.svelte +89 -0
  5. package/dist/cells/CopyButton.svelte.d.ts +33 -0
  6. package/dist/cells/CustomCell.svelte +136 -0
  7. package/dist/cells/CustomCell.svelte.d.ts +44 -0
  8. package/dist/cells/DateCell.svelte +194 -0
  9. package/dist/cells/DateCell.svelte.d.ts +39 -0
  10. package/dist/cells/LinkCell.svelte +240 -0
  11. package/dist/cells/LinkCell.svelte.d.ts +42 -0
  12. package/dist/cells/NumberCell.svelte +225 -0
  13. package/dist/cells/NumberCell.svelte.d.ts +47 -0
  14. package/dist/cells/StatusBadge.svelte +121 -0
  15. package/dist/cells/StatusBadge.svelte.d.ts +44 -0
  16. package/dist/cells/UserAvatar.svelte +71 -0
  17. package/dist/cells/UserAvatar.svelte.d.ts +37 -0
  18. package/dist/cells/index.d.ts +8 -0
  19. package/dist/cells/index.js +9 -0
  20. package/dist/core/EmptyState.svelte +161 -0
  21. package/dist/core/EmptyState.svelte.d.ts +16 -0
  22. package/dist/core/ErrorState.svelte +158 -0
  23. package/dist/core/ErrorState.svelte.d.ts +15 -0
  24. package/dist/core/GroupedRow.svelte +239 -0
  25. package/dist/core/GroupedRow.svelte.d.ts +18 -0
  26. package/dist/core/LoadingState.svelte +75 -0
  27. package/dist/core/LoadingState.svelte.d.ts +14 -0
  28. package/dist/core/MobileCard.svelte +151 -0
  29. package/dist/core/MobileCard.svelte.d.ts +15 -0
  30. package/dist/core/TableCell.svelte +105 -0
  31. package/dist/core/TableCell.svelte.d.ts +14 -0
  32. package/dist/core/TableDesktop.svelte +480 -0
  33. package/dist/core/TableDesktop.svelte.d.ts +26 -0
  34. package/dist/core/TableHead.svelte +314 -0
  35. package/dist/core/TableHead.svelte.d.ts +7 -0
  36. package/dist/core/TableMobile.svelte +112 -0
  37. package/dist/core/TableMobile.svelte.d.ts +13 -0
  38. package/dist/core/TableProvider.svelte +271 -0
  39. package/dist/core/TableProvider.svelte.d.ts +40 -0
  40. package/dist/core/TableRow.svelte +171 -0
  41. package/dist/core/TableRow.svelte.d.ts +16 -0
  42. package/dist/core/index.d.ts +17 -0
  43. package/dist/core/index.js +14 -0
  44. package/dist/core/sticky-context.svelte.d.ts +48 -0
  45. package/dist/core/sticky-context.svelte.js +88 -0
  46. package/dist/core/table/Table.svelte +304 -0
  47. package/dist/core/table/Table.svelte.d.ts +26 -0
  48. package/dist/core/table/index.d.ts +448 -0
  49. package/dist/core/table/index.js +1 -0
  50. package/dist/core/table-style-context.d.ts +66 -0
  51. package/dist/core/table-style-context.js +26 -0
  52. package/dist/factories/ColumnValidation.d.ts +49 -0
  53. package/dist/factories/ColumnValidation.js +188 -0
  54. package/dist/factories/TableColumns.d.ts +97 -0
  55. package/dist/factories/TableColumns.js +262 -0
  56. package/dist/factories/TypedColumnBuilder.d.ts +41 -0
  57. package/dist/factories/TypedColumnBuilder.js +72 -0
  58. package/dist/factories/index.d.ts +12 -0
  59. package/dist/factories/index.js +13 -0
  60. package/dist/features/HeaderMenu.svelte +236 -0
  61. package/dist/features/HeaderMenu.svelte.d.ts +8 -0
  62. package/dist/features/LiveUpdateBanner.svelte +66 -0
  63. package/dist/features/LiveUpdateBanner.svelte.d.ts +6 -0
  64. package/dist/features/SearchHighlight.svelte +21 -0
  65. package/dist/features/SearchHighlight.svelte.d.ts +8 -0
  66. package/dist/features/SmartFilterBar/ChipsField.svelte +104 -0
  67. package/dist/features/SmartFilterBar/ChipsField.svelte.d.ts +5 -0
  68. package/dist/features/SmartFilterBar/ColumnVisibilityMenu.svelte +84 -0
  69. package/dist/features/SmartFilterBar/ColumnVisibilityMenu.svelte.d.ts +3 -0
  70. package/dist/features/SmartFilterBar/FilterMenu.svelte +367 -0
  71. package/dist/features/SmartFilterBar/FilterMenu.svelte.d.ts +3 -0
  72. package/dist/features/SmartFilterBar/GroupingMenu.svelte +82 -0
  73. package/dist/features/SmartFilterBar/GroupingMenu.svelte.d.ts +3 -0
  74. package/dist/features/SmartFilterBar/SmartFilterBar.svelte +109 -0
  75. package/dist/features/SmartFilterBar/SmartFilterBar.svelte.d.ts +11 -0
  76. package/dist/features/SmartFilterBar/SummaryMenu.svelte +118 -0
  77. package/dist/features/SmartFilterBar/SummaryMenu.svelte.d.ts +3 -0
  78. package/dist/features/SummaryRow.svelte +97 -0
  79. package/dist/features/SummaryRow.svelte.d.ts +8 -0
  80. package/dist/features/index.d.ts +4 -0
  81. package/dist/features/index.js +4 -0
  82. package/dist/i18n/index.d.ts +366 -0
  83. package/dist/i18n/index.js +21 -0
  84. package/dist/index.d.ts +28 -0
  85. package/dist/index.js +41 -0
  86. package/dist/stores/TableStore.svelte.d.ts +192 -0
  87. package/dist/stores/TableStore.svelte.js +362 -0
  88. package/dist/stores/concerns/index.d.ts +15 -0
  89. package/dist/stores/concerns/index.js +14 -0
  90. package/dist/stores/concerns/types.d.ts +31 -0
  91. package/dist/stores/concerns/types.js +1 -0
  92. package/dist/stores/concerns/useColumnOrder.svelte.d.ts +16 -0
  93. package/dist/stores/concerns/useColumnOrder.svelte.js +81 -0
  94. package/dist/stores/concerns/useColumnVisibility.svelte.d.ts +16 -0
  95. package/dist/stores/concerns/useColumnVisibility.svelte.js +58 -0
  96. package/dist/stores/concerns/useExpansion.svelte.d.ts +9 -0
  97. package/dist/stores/concerns/useExpansion.svelte.js +32 -0
  98. package/dist/stores/concerns/useFiltering.svelte.d.ts +20 -0
  99. package/dist/stores/concerns/useFiltering.svelte.js +109 -0
  100. package/dist/stores/concerns/useFocusManagement.svelte.d.ts +15 -0
  101. package/dist/stores/concerns/useFocusManagement.svelte.js +52 -0
  102. package/dist/stores/concerns/useGrouping.svelte.d.ts +15 -0
  103. package/dist/stores/concerns/useGrouping.svelte.js +86 -0
  104. package/dist/stores/concerns/useLiveUpdates.svelte.d.ts +45 -0
  105. package/dist/stores/concerns/useLiveUpdates.svelte.js +175 -0
  106. package/dist/stores/concerns/usePagination.svelte.d.ts +18 -0
  107. package/dist/stores/concerns/usePagination.svelte.js +54 -0
  108. package/dist/stores/concerns/usePersistence.svelte.d.ts +36 -0
  109. package/dist/stores/concerns/usePersistence.svelte.js +167 -0
  110. package/dist/stores/concerns/useRemoteData.svelte.d.ts +21 -0
  111. package/dist/stores/concerns/useRemoteData.svelte.js +64 -0
  112. package/dist/stores/concerns/useSearch.svelte.d.ts +8 -0
  113. package/dist/stores/concerns/useSearch.svelte.js +16 -0
  114. package/dist/stores/concerns/useSelection.svelte.d.ts +21 -0
  115. package/dist/stores/concerns/useSelection.svelte.js +110 -0
  116. package/dist/stores/concerns/useSorting.svelte.d.ts +11 -0
  117. package/dist/stores/concerns/useSorting.svelte.js +70 -0
  118. package/dist/stores/concerns/useSummary.svelte.d.ts +18 -0
  119. package/dist/stores/concerns/useSummary.svelte.js +96 -0
  120. package/dist/stores/index.d.ts +1 -0
  121. package/dist/stores/index.js +1 -0
  122. package/dist/style/index.css +137 -0
  123. package/dist/style/index.d.ts +2 -0
  124. package/dist/style/index.js +2 -0
  125. package/dist/style/table-theme.css +131 -0
  126. package/dist/style/themes/comfortable.css +20 -0
  127. package/dist/style/themes/compact.css +20 -0
  128. package/dist/translations/de.d.ts +177 -0
  129. package/dist/translations/de.js +176 -0
  130. package/dist/translations/en.d.ts +177 -0
  131. package/dist/translations/en.js +176 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/index.js +1 -0
  134. package/dist/types/tableTypes.d.ts +262 -0
  135. package/dist/types/tableTypes.js +1 -0
  136. package/dist/utils/index.d.ts +165 -0
  137. package/dist/utils/index.js +330 -0
  138. package/dist/utils/sticky-measure.d.ts +54 -0
  139. package/dist/utils/sticky-measure.js +107 -0
  140. package/dist/utils/virtualizer.d.ts +43 -0
  141. package/dist/utils/virtualizer.js +43 -0
  142. package/dist/variants/index.d.ts +11 -0
  143. package/dist/variants/index.js +15 -0
  144. package/dist/variants/table-cells.variants.d.ts +827 -0
  145. package/dist/variants/table-cells.variants.js +627 -0
  146. package/dist/variants/table-features.variants.d.ts +547 -0
  147. package/dist/variants/table-features.variants.js +412 -0
  148. package/dist/variants/table-states.variants.d.ts +594 -0
  149. package/dist/variants/table-states.variants.js +394 -0
  150. package/dist/variants/table.system.d.ts +301 -0
  151. package/dist/variants/table.system.js +314 -0
  152. package/dist/variants/table.variants.d.ts +428 -0
  153. package/dist/variants/table.variants.js +360 -0
  154. 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,3 @@
1
+ declare const FilterMenu: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type FilterMenu = ReturnType<typeof FilterMenu>;
3
+ export default FilterMenu;
@@ -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,3 @@
1
+ declare const GroupingMenu: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type GroupingMenu = ReturnType<typeof GroupingMenu>;
3
+ export default GroupingMenu;
@@ -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
+ />