@shotleybuilder/svelte-table-kit 0.10.1 → 0.13.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/README.md +6 -1
- package/dist/TableKit.svelte +24 -8
- package/dist/TableKit.svelte.d.ts +9 -9
- package/dist/components/FilterCondition.svelte +117 -63
- package/dist/components/GroupBar.svelte +5 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/stores/persistence.d.ts +17 -1
- package/dist/stores/persistence.js +26 -0
- package/dist/types.d.ts +21 -0
- package/dist/utils/filters.d.ts +13 -2
- package/dist/utils/filters.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
|
|
|
26
26
|
- 📋 **Column context menu** - Quick access to sort, filter, group, and hide actions
|
|
27
27
|
|
|
28
28
|
**Advanced Filtering:**
|
|
29
|
-
-
|
|
29
|
+
- 14 filter operators: equals, contains, starts with, greater than, is_before, is_after, etc.
|
|
30
30
|
- AND/OR logic between conditions
|
|
31
31
|
- Collapsible FilterBar UI (space-efficient)
|
|
32
32
|
- Active filter count badge
|
|
@@ -35,6 +35,7 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
|
|
|
35
35
|
- **Fuzzy search** - Type to quickly find columns in large tables with highlighted matches (v0.8.0+)
|
|
36
36
|
- **Value suggestions** - Autocomplete dropdown shows existing column values as you type (v0.10.0+)
|
|
37
37
|
- **Numeric range hints** - Numeric columns display min/max range in the value input (v0.10.0+)
|
|
38
|
+
- **Data type awareness** - Operators and value inputs adapt based on column data type (v0.11.0+)
|
|
38
39
|
|
|
39
40
|
**Sorting Options:**
|
|
40
41
|
- **Column header mode** (default) - Click headers to sort with ↑↓↕ indicators
|
|
@@ -159,6 +160,8 @@ Customize initial table state programmatically:
|
|
|
159
160
|
defaultSorting: [
|
|
160
161
|
{ columnId: 'name', direction: 'asc' }
|
|
161
162
|
],
|
|
163
|
+
defaultGrouping: ['role'],
|
|
164
|
+
defaultExpanded: true,
|
|
162
165
|
filterLogic: 'and'
|
|
163
166
|
}}
|
|
164
167
|
features={{
|
|
@@ -360,6 +363,8 @@ interface TableConfig {
|
|
|
360
363
|
defaultVisibleColumns?: string[]; // Visible column IDs (others hidden)
|
|
361
364
|
defaultFilters?: FilterCondition[]; // Initial filter conditions
|
|
362
365
|
defaultSorting?: SortConfig[]; // Initial sort configuration
|
|
366
|
+
defaultGrouping?: string[]; // Initial grouping column IDs (up to 3)
|
|
367
|
+
defaultExpanded?: boolean; // Initial expanded state for groups (default: true)
|
|
363
368
|
filterLogic?: 'and' | 'or'; // Filter combination logic
|
|
364
369
|
pagination?: {
|
|
365
370
|
pageSize: number;
|
package/dist/TableKit.svelte
CHANGED
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
saveColumnFilters,
|
|
19
19
|
loadColumnOrder,
|
|
20
20
|
saveColumnOrder,
|
|
21
|
+
loadGrouping,
|
|
22
|
+
saveGrouping,
|
|
23
|
+
loadExpanded,
|
|
24
|
+
saveExpanded,
|
|
21
25
|
isBrowser
|
|
22
26
|
} from "./stores/persistence";
|
|
23
27
|
import { applyFilters } from "./utils/filters";
|
|
@@ -47,6 +51,9 @@ export let features = {
|
|
|
47
51
|
export let onRowClick = void 0;
|
|
48
52
|
export let onRowSelect = void 0;
|
|
49
53
|
export let onStateChange = void 0;
|
|
54
|
+
function getColumnId(col) {
|
|
55
|
+
return col.accessorKey || col.id || "";
|
|
56
|
+
}
|
|
50
57
|
let sorting = writable([]);
|
|
51
58
|
let columnVisibility = writable({});
|
|
52
59
|
let columnSizing = writable({});
|
|
@@ -79,7 +86,7 @@ $: {
|
|
|
79
86
|
if (config.defaultVisibleColumns && columns.length > 0) {
|
|
80
87
|
const visibilityMap = {};
|
|
81
88
|
columns.forEach((col) => {
|
|
82
|
-
const colId = col
|
|
89
|
+
const colId = getColumnId(col);
|
|
83
90
|
if (config && config.defaultVisibleColumns) {
|
|
84
91
|
visibilityMap[colId] = config.defaultVisibleColumns.includes(colId);
|
|
85
92
|
}
|
|
@@ -102,6 +109,12 @@ $: {
|
|
|
102
109
|
if (config.defaultColumnSizing) {
|
|
103
110
|
columnSizing.set(config.defaultColumnSizing);
|
|
104
111
|
}
|
|
112
|
+
if (config.defaultGrouping) {
|
|
113
|
+
grouping.set(config.defaultGrouping);
|
|
114
|
+
}
|
|
115
|
+
if (config.defaultExpanded !== void 0) {
|
|
116
|
+
expanded.set(config.defaultExpanded);
|
|
117
|
+
}
|
|
105
118
|
previousConfigId = config.id;
|
|
106
119
|
configInitialized = true;
|
|
107
120
|
} else if (!hasConfig && persistState && storageKey && !configInitialized) {
|
|
@@ -109,6 +122,8 @@ $: {
|
|
|
109
122
|
columnVisibility.set(loadColumnVisibility(storageKey) || {});
|
|
110
123
|
columnSizing.set(loadColumnSizing(storageKey) || {});
|
|
111
124
|
columnFilters.set(loadColumnFilters(storageKey) || []);
|
|
125
|
+
grouping.set(loadGrouping(storageKey) || []);
|
|
126
|
+
expanded.set(loadExpanded(storageKey) ?? true);
|
|
112
127
|
configInitialized = true;
|
|
113
128
|
} else if (!hasConfig && !configInitialized) {
|
|
114
129
|
configInitialized = true;
|
|
@@ -125,6 +140,8 @@ $: {
|
|
|
125
140
|
saveColumnSizing(storageKey, $columnSizing);
|
|
126
141
|
saveColumnFilters(storageKey, $columnFilters);
|
|
127
142
|
saveColumnOrder(storageKey, $columnOrder);
|
|
143
|
+
saveGrouping(storageKey, $grouping);
|
|
144
|
+
saveExpanded(storageKey, $expanded);
|
|
128
145
|
}
|
|
129
146
|
}
|
|
130
147
|
let showColumnPicker = false;
|
|
@@ -293,7 +310,7 @@ function handleDrop(targetColumnId) {
|
|
|
293
310
|
draggedColumnId = null;
|
|
294
311
|
}
|
|
295
312
|
$: if ($columnOrder.length === 0 && columns.length > 0) {
|
|
296
|
-
columnOrder.set(columns.map((col) => col
|
|
313
|
+
columnOrder.set(columns.map((col) => getColumnId(col)));
|
|
297
314
|
}
|
|
298
315
|
function showCellContextMenu(event, cell) {
|
|
299
316
|
if (features.filtering === false) return;
|
|
@@ -333,8 +350,8 @@ $: if (onStateChange) {
|
|
|
333
350
|
columnVisibility: $columnVisibility,
|
|
334
351
|
columnOrder: $columnOrder,
|
|
335
352
|
columnSizing: $columnSizing,
|
|
336
|
-
columnFilters: $
|
|
337
|
-
sorting: $sorting,
|
|
353
|
+
columnFilters: $filterConditions,
|
|
354
|
+
sorting: $sorting.map((s) => ({ columnId: s.id, direction: s.desc ? "desc" : "asc" })),
|
|
338
355
|
pagination: $table.getState().pagination
|
|
339
356
|
});
|
|
340
357
|
}
|
|
@@ -639,8 +656,10 @@ $: if (onStateChange) {
|
|
|
639
656
|
>
|
|
640
657
|
{#if !header.isPlaceholder}
|
|
641
658
|
<div class="th-wrapper">
|
|
659
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
642
660
|
<div
|
|
643
661
|
class="th-content"
|
|
662
|
+
role={features.columnReordering !== false ? 'button' : undefined}
|
|
644
663
|
style="padding: {verticalPadding}rem {horizontalPadding}rem; cursor: {features.columnReordering !==
|
|
645
664
|
false
|
|
646
665
|
? 'grab'
|
|
@@ -660,10 +679,7 @@ $: if (onStateChange) {
|
|
|
660
679
|
/>
|
|
661
680
|
</span>
|
|
662
681
|
<span class="sort-icon">
|
|
663
|
-
{
|
|
664
|
-
asc: '↑',
|
|
665
|
-
desc: '↓'
|
|
666
|
-
}[header.column.getIsSorted()] ?? '↕'}
|
|
682
|
+
{header.column.getIsSorted() === 'asc' ? '↑' : header.column.getIsSorted() === 'desc' ? '↓' : '↕'}
|
|
667
683
|
</span>
|
|
668
684
|
</button>
|
|
669
685
|
{:else}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { SvelteComponent } from "svelte";
|
|
2
2
|
import { type ColumnDef } from '@tanstack/svelte-table';
|
|
3
3
|
import type { TableKitProps } from './types';
|
|
4
|
-
declare class __sveltets_Render<T
|
|
4
|
+
declare class __sveltets_Render<T extends Record<string, unknown>> {
|
|
5
5
|
props(): {
|
|
6
6
|
data?: T[] | undefined;
|
|
7
7
|
columns?: ColumnDef<T>[] | undefined;
|
|
8
8
|
config?: TableKitProps<T_1>["config"];
|
|
9
|
-
storageKey?: TableKitProps<T_1>["storageKey"]
|
|
9
|
+
storageKey?: NonNullable<TableKitProps<T_1>["storageKey"]>;
|
|
10
10
|
persistState?: TableKitProps<T_1>["persistState"];
|
|
11
11
|
align?: TableKitProps<T_1>["align"];
|
|
12
12
|
rowHeight?: TableKitProps<T_1>["rowHeight"];
|
|
13
13
|
columnSpacing?: TableKitProps<T_1>["columnSpacing"];
|
|
14
|
-
features?: TableKitProps<T_1>["features"]
|
|
14
|
+
features?: NonNullable<TableKitProps<T_1>["features"]>;
|
|
15
15
|
onRowClick?: ((row: T) => void) | undefined;
|
|
16
|
-
onRowSelect?: ((rows: T[]) => void) | undefined;
|
|
16
|
+
/** @deprecated Row selection not yet implemented - reserved for future use */ onRowSelect?: ((rows: T[]) => void) | undefined;
|
|
17
17
|
onStateChange?: TableKitProps<T_1>["onStateChange"];
|
|
18
18
|
};
|
|
19
19
|
events(): {} & {
|
|
@@ -23,14 +23,14 @@ declare class __sveltets_Render<T> {
|
|
|
23
23
|
'toolbar-left': {};
|
|
24
24
|
empty: {};
|
|
25
25
|
cell: {
|
|
26
|
-
cell: import("@tanstack/table-core").Cell<
|
|
26
|
+
cell: import("@tanstack/table-core").Cell<T, unknown>;
|
|
27
27
|
column: string;
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
export type TableKitProps_<T
|
|
32
|
-
export type TableKitEvents<T
|
|
33
|
-
export type TableKitSlots<T
|
|
34
|
-
export default class TableKit<T
|
|
31
|
+
export type TableKitProps_<T extends Record<string, unknown>> = ReturnType<__sveltets_Render<T>['props']>;
|
|
32
|
+
export type TableKitEvents<T extends Record<string, unknown>> = ReturnType<__sveltets_Render<T>['events']>;
|
|
33
|
+
export type TableKitSlots<T extends Record<string, unknown>> = ReturnType<__sveltets_Render<T>['slots']>;
|
|
34
|
+
export default class TableKit<T extends Record<string, unknown>> extends SvelteComponent<TableKitProps_<T>, TableKitEvents<T>, TableKitSlots<T>> {
|
|
35
35
|
}
|
|
36
36
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script>import { loadFilterColumnOrderMode, saveFilterColumnOrderMode } from "../stores/persistence";
|
|
2
2
|
import { fuzzyMatch, highlightMatches } from "../utils/fuzzy";
|
|
3
|
+
import { getOperatorsForType } from "../utils/filters";
|
|
3
4
|
import { onMount, tick } from "svelte";
|
|
4
5
|
export let condition;
|
|
5
6
|
export let columns;
|
|
@@ -131,25 +132,30 @@ $: filteredColumns = (() => {
|
|
|
131
132
|
$: if (filteredColumns) {
|
|
132
133
|
highlightedIndex = 0;
|
|
133
134
|
}
|
|
135
|
+
$: selectedColumn = condition.field ? columns.find((c) => getColumnId(c) === condition.field) : null;
|
|
134
136
|
$: selectedColumnLabel = (() => {
|
|
135
137
|
if (!condition.field) return "";
|
|
136
|
-
|
|
137
|
-
return col ? getColumnLabel(col) : condition.field;
|
|
138
|
+
return selectedColumn ? getColumnLabel(selectedColumn) : condition.field;
|
|
138
139
|
})();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
$: columnDataType = (() => {
|
|
141
|
+
if (!selectedColumn) return "text";
|
|
142
|
+
const meta = selectedColumn.meta;
|
|
143
|
+
return meta?.dataType || "text";
|
|
144
|
+
})();
|
|
145
|
+
$: selectOptions = (() => {
|
|
146
|
+
if (!selectedColumn) return [];
|
|
147
|
+
const meta = selectedColumn.meta;
|
|
148
|
+
return meta?.selectOptions || [];
|
|
149
|
+
})();
|
|
150
|
+
$: operatorOptions = getOperatorsForType(columnDataType);
|
|
151
|
+
$: {
|
|
152
|
+
if (condition.field && operatorOptions.length > 0) {
|
|
153
|
+
const currentOperatorValid = operatorOptions.some((op) => op.value === condition.operator);
|
|
154
|
+
if (!currentOperatorValid) {
|
|
155
|
+
onUpdate({ ...condition, operator: "equals" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
153
159
|
function selectColumn(col) {
|
|
154
160
|
const field = getColumnId(col);
|
|
155
161
|
onUpdate({ ...condition, field });
|
|
@@ -339,54 +345,102 @@ $: valueDisabled = condition.operator === "is_empty" || condition.operator === "
|
|
|
339
345
|
</select>
|
|
340
346
|
|
|
341
347
|
<div class="value-input-wrapper" bind:this={valueSuggestionsRef}>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
{#each
|
|
364
|
-
<
|
|
365
|
-
class="suggestion-option"
|
|
366
|
-
class:highlighted={i === valueSuggestionIndex}
|
|
367
|
-
on:click={() => selectValueSuggestion(value)}
|
|
368
|
-
on:mouseenter={() => (valueSuggestionIndex = i)}
|
|
369
|
-
type="button"
|
|
370
|
-
>
|
|
371
|
-
{#if matchedIndices.length > 0}
|
|
372
|
-
{#each highlightMatches(value, matchedIndices) as segment}
|
|
373
|
-
{#if segment.isMatch}
|
|
374
|
-
<mark class="match-highlight">{segment.text}</mark>
|
|
375
|
-
{:else}
|
|
376
|
-
{segment.text}
|
|
377
|
-
{/if}
|
|
378
|
-
{/each}
|
|
379
|
-
{:else}
|
|
380
|
-
{value}
|
|
381
|
-
{/if}
|
|
382
|
-
</button>
|
|
348
|
+
{#if columnDataType === 'boolean'}
|
|
349
|
+
<!-- Boolean: dropdown with true/false -->
|
|
350
|
+
<select
|
|
351
|
+
class="value-input"
|
|
352
|
+
value={condition.value || ''}
|
|
353
|
+
on:change={handleValueChange}
|
|
354
|
+
disabled={valueDisabled}
|
|
355
|
+
>
|
|
356
|
+
<option value="">Select...</option>
|
|
357
|
+
<option value="true">True</option>
|
|
358
|
+
<option value="false">False</option>
|
|
359
|
+
</select>
|
|
360
|
+
{:else if columnDataType === 'select' && selectOptions.length > 0}
|
|
361
|
+
<!-- Select: dropdown with options from meta -->
|
|
362
|
+
<select
|
|
363
|
+
class="value-input"
|
|
364
|
+
value={condition.value || ''}
|
|
365
|
+
on:change={handleValueChange}
|
|
366
|
+
disabled={valueDisabled}
|
|
367
|
+
>
|
|
368
|
+
<option value="">Select...</option>
|
|
369
|
+
{#each selectOptions as opt}
|
|
370
|
+
<option value={opt.value}>{opt.label}</option>
|
|
383
371
|
{/each}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
{
|
|
389
|
-
|
|
372
|
+
</select>
|
|
373
|
+
{:else if columnDataType === 'date'}
|
|
374
|
+
<!-- Date: date input -->
|
|
375
|
+
<input
|
|
376
|
+
bind:this={valueInputRef}
|
|
377
|
+
type="date"
|
|
378
|
+
class="value-input"
|
|
379
|
+
value={condition.value || ''}
|
|
380
|
+
on:input={handleValueChange}
|
|
381
|
+
disabled={valueDisabled}
|
|
382
|
+
/>
|
|
383
|
+
{:else if columnDataType === 'number'}
|
|
384
|
+
<!-- Number: number input with range hint -->
|
|
385
|
+
<input
|
|
386
|
+
bind:this={valueInputRef}
|
|
387
|
+
type="number"
|
|
388
|
+
class="value-input"
|
|
389
|
+
value={condition.value || ''}
|
|
390
|
+
on:input={handleValueChange}
|
|
391
|
+
disabled={valueDisabled}
|
|
392
|
+
placeholder={valueDisabled ? 'N/A' : numericRange ? `${numericRange.min} - ${numericRange.max}` : 'Enter number...'}
|
|
393
|
+
/>
|
|
394
|
+
{#if numericRange && !valueDisabled}
|
|
395
|
+
<div class="numeric-range-hint">
|
|
396
|
+
Range: {numericRange.min} - {numericRange.max}
|
|
397
|
+
</div>
|
|
398
|
+
{/if}
|
|
399
|
+
{:else}
|
|
400
|
+
<!-- Text: text input with autocomplete suggestions -->
|
|
401
|
+
<input
|
|
402
|
+
bind:this={valueInputRef}
|
|
403
|
+
type="text"
|
|
404
|
+
class="value-input"
|
|
405
|
+
value={condition.value || ''}
|
|
406
|
+
on:input={handleValueChange}
|
|
407
|
+
on:focus={handleValueFocus}
|
|
408
|
+
on:keydown={handleValueKeydown}
|
|
409
|
+
disabled={valueDisabled}
|
|
410
|
+
placeholder={valueDisabled ? 'N/A' : 'Enter value...'}
|
|
411
|
+
autocomplete="off"
|
|
412
|
+
/>
|
|
413
|
+
|
|
414
|
+
{#if showValueSuggestions && filteredValueSuggestions.length > 0}
|
|
415
|
+
<div class="value-suggestions">
|
|
416
|
+
{#each filteredValueSuggestions as { value, matchedIndices }, i}
|
|
417
|
+
<button
|
|
418
|
+
class="suggestion-option"
|
|
419
|
+
class:highlighted={i === valueSuggestionIndex}
|
|
420
|
+
on:click={() => selectValueSuggestion(value)}
|
|
421
|
+
on:mouseenter={() => (valueSuggestionIndex = i)}
|
|
422
|
+
type="button"
|
|
423
|
+
>
|
|
424
|
+
{#if matchedIndices.length > 0}
|
|
425
|
+
{#each highlightMatches(value, matchedIndices) as segment}
|
|
426
|
+
{#if segment.isMatch}
|
|
427
|
+
<mark class="match-highlight">{segment.text}</mark>
|
|
428
|
+
{:else}
|
|
429
|
+
{segment.text}
|
|
430
|
+
{/if}
|
|
431
|
+
{/each}
|
|
432
|
+
{:else}
|
|
433
|
+
{value}
|
|
434
|
+
{/if}
|
|
435
|
+
</button>
|
|
436
|
+
{/each}
|
|
437
|
+
{#if columnValues.length > 50}
|
|
438
|
+
<div class="suggestions-overflow">
|
|
439
|
+
and {columnValues.length - 50} more...
|
|
440
|
+
</div>
|
|
441
|
+
{/if}
|
|
442
|
+
</div>
|
|
443
|
+
{/if}
|
|
390
444
|
{/if}
|
|
391
445
|
</div>
|
|
392
446
|
|
|
@@ -31,8 +31,11 @@ function clearAllGroups() {
|
|
|
31
31
|
onGroupingChange([]);
|
|
32
32
|
setExpanded(false);
|
|
33
33
|
}
|
|
34
|
+
function getColumnId(col) {
|
|
35
|
+
return col.accessorKey || col.id || "";
|
|
36
|
+
}
|
|
34
37
|
$: availableColumns = columns.filter((col) => {
|
|
35
|
-
const columnId = col
|
|
38
|
+
const columnId = getColumnId(col);
|
|
36
39
|
return columnId && col.enableGrouping !== false;
|
|
37
40
|
});
|
|
38
41
|
$: hasGroups = grouping.length > 0;
|
|
@@ -85,7 +88,7 @@ $: canAddMore = grouping.length < MAX_LEVELS;
|
|
|
85
88
|
>
|
|
86
89
|
<option value="">Select field...</option>
|
|
87
90
|
{#each availableColumns as column}
|
|
88
|
-
{@const columnId = column
|
|
91
|
+
{@const columnId = getColumnId(column)}
|
|
89
92
|
<option value={columnId}>
|
|
90
93
|
{column.header || columnId}
|
|
91
94
|
</option>
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,11 @@ export { default as FilterBar } from './components/FilterBar.svelte';
|
|
|
3
3
|
export { default as FilterConditionEditor } from './components/FilterCondition.svelte';
|
|
4
4
|
export { default as GroupBar } from './components/GroupBar.svelte';
|
|
5
5
|
export { default as CellContextMenu } from './components/CellContextMenu.svelte';
|
|
6
|
-
export type { TableKitProps, TableConfig, ViewPreset, FilterCondition, FilterOperator, FilterLogic, ColumnOrderMode, SortConfig, ClassNameMap, TableFeatures, TableState } from './types';
|
|
6
|
+
export type { TableKitProps, TableConfig, ViewPreset, FilterCondition, FilterOperator, FilterLogic, ColumnOrderMode, ColumnDataType, ColumnMeta, SortConfig, ClassNameMap, TableFeatures, TableState } from './types';
|
|
7
7
|
export { presets } from './presets';
|
|
8
8
|
export { generateTableConfig, validateTableConfig, mergeConfigs } from './utils/config';
|
|
9
|
-
export { createTextFilter, createSelectFilter, createNumericFilter, evaluateCondition, applyFilters } from './utils/filters';
|
|
9
|
+
export { createTextFilter, createSelectFilter, createNumericFilter, evaluateCondition, applyFilters, getOperatorsForType } from './utils/filters';
|
|
10
|
+
export type { OperatorOption } from './utils/filters';
|
|
10
11
|
export { formatDate, formatCurrency, formatNumber, formatPercent } from './utils/formatters';
|
|
11
12
|
export { fuzzyMatch, fuzzySearch, highlightMatches } from './utils/fuzzy';
|
|
12
13
|
export type { FuzzyMatch } from './utils/fuzzy';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { presets } from './presets';
|
|
|
11
11
|
// Utilities for AI configuration
|
|
12
12
|
export { generateTableConfig, validateTableConfig, mergeConfigs } from './utils/config';
|
|
13
13
|
// Filter utilities
|
|
14
|
-
export { createTextFilter, createSelectFilter, createNumericFilter, evaluateCondition, applyFilters } from './utils/filters';
|
|
14
|
+
export { createTextFilter, createSelectFilter, createNumericFilter, evaluateCondition, applyFilters, getOperatorsForType } from './utils/filters';
|
|
15
15
|
// Formatters
|
|
16
16
|
export { formatDate, formatCurrency, formatNumber, formatPercent } from './utils/formatters';
|
|
17
17
|
// Fuzzy search utilities
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VisibilityState, ColumnSizingState, ColumnFiltersState, ColumnOrderState, SortingState, PaginationState } from '@tanstack/svelte-table';
|
|
1
|
+
import type { VisibilityState, ColumnSizingState, ColumnFiltersState, ColumnOrderState, SortingState, PaginationState, GroupingState, ExpandedState } from '@tanstack/svelte-table';
|
|
2
2
|
import type { ColumnOrderMode } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Check if we're in a browser environment
|
|
@@ -53,6 +53,22 @@ export declare function loadPagination(storageKey: string, defaultPageSize?: num
|
|
|
53
53
|
* Save pagination state to localStorage
|
|
54
54
|
*/
|
|
55
55
|
export declare function savePagination(storageKey: string, state: PaginationState): void;
|
|
56
|
+
/**
|
|
57
|
+
* Load grouping state from localStorage
|
|
58
|
+
*/
|
|
59
|
+
export declare function loadGrouping(storageKey: string): GroupingState;
|
|
60
|
+
/**
|
|
61
|
+
* Save grouping state to localStorage
|
|
62
|
+
*/
|
|
63
|
+
export declare function saveGrouping(storageKey: string, state: GroupingState): void;
|
|
64
|
+
/**
|
|
65
|
+
* Load expanded state from localStorage
|
|
66
|
+
*/
|
|
67
|
+
export declare function loadExpanded(storageKey: string): ExpandedState;
|
|
68
|
+
/**
|
|
69
|
+
* Save expanded state to localStorage
|
|
70
|
+
*/
|
|
71
|
+
export declare function saveExpanded(storageKey: string, state: ExpandedState): void;
|
|
56
72
|
/**
|
|
57
73
|
* Load filter column order mode from localStorage
|
|
58
74
|
*/
|
|
@@ -107,6 +107,30 @@ export function loadPagination(storageKey, defaultPageSize = 10) {
|
|
|
107
107
|
export function savePagination(storageKey, state) {
|
|
108
108
|
saveToStorage(`${storageKey}_pagination`, state);
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Load grouping state from localStorage
|
|
112
|
+
*/
|
|
113
|
+
export function loadGrouping(storageKey) {
|
|
114
|
+
return loadFromStorage(`${storageKey}_grouping`, []);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Save grouping state to localStorage
|
|
118
|
+
*/
|
|
119
|
+
export function saveGrouping(storageKey, state) {
|
|
120
|
+
saveToStorage(`${storageKey}_grouping`, state);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Load expanded state from localStorage
|
|
124
|
+
*/
|
|
125
|
+
export function loadExpanded(storageKey) {
|
|
126
|
+
return loadFromStorage(`${storageKey}_expanded`, true);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Save expanded state to localStorage
|
|
130
|
+
*/
|
|
131
|
+
export function saveExpanded(storageKey, state) {
|
|
132
|
+
saveToStorage(`${storageKey}_expanded`, state);
|
|
133
|
+
}
|
|
110
134
|
/**
|
|
111
135
|
* Load filter column order mode from localStorage
|
|
112
136
|
*/
|
|
@@ -132,6 +156,8 @@ export function clearTableState(storageKey) {
|
|
|
132
156
|
localStorage.removeItem(`${storageKey}_column_order`);
|
|
133
157
|
localStorage.removeItem(`${storageKey}_sorting`);
|
|
134
158
|
localStorage.removeItem(`${storageKey}_pagination`);
|
|
159
|
+
localStorage.removeItem(`${storageKey}_grouping`);
|
|
160
|
+
localStorage.removeItem(`${storageKey}_expanded`);
|
|
135
161
|
localStorage.removeItem(`${storageKey}_filter_column_order_mode`);
|
|
136
162
|
}
|
|
137
163
|
catch (error) {
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface TableConfig {
|
|
|
40
40
|
defaultFilters?: FilterCondition[];
|
|
41
41
|
filterLogic?: FilterLogic;
|
|
42
42
|
defaultSorting?: SortConfig[];
|
|
43
|
+
defaultGrouping?: string[];
|
|
44
|
+
defaultExpanded?: boolean;
|
|
43
45
|
pagination?: {
|
|
44
46
|
pageSize: number;
|
|
45
47
|
pageSizeOptions?: number[];
|
|
@@ -55,6 +57,25 @@ export interface ViewPreset {
|
|
|
55
57
|
export type FilterOperator = 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty' | 'greater_than' | 'less_than' | 'greater_or_equal' | 'less_or_equal' | 'is_before' | 'is_after';
|
|
56
58
|
export type FilterLogic = 'and' | 'or';
|
|
57
59
|
export type ColumnOrderMode = 'definition' | 'ui' | 'alphabetical';
|
|
60
|
+
/**
|
|
61
|
+
* Column data type for filter operator adaptation
|
|
62
|
+
*/
|
|
63
|
+
export type ColumnDataType = 'text' | 'number' | 'date' | 'boolean' | 'select';
|
|
64
|
+
/**
|
|
65
|
+
* Extended column meta for data type awareness
|
|
66
|
+
* Use in column definitions: { meta: { dataType: 'number', selectOptions: [...] } }
|
|
67
|
+
*/
|
|
68
|
+
export interface ColumnMeta {
|
|
69
|
+
/** Data type for filter operator adaptation */
|
|
70
|
+
dataType?: ColumnDataType;
|
|
71
|
+
/** Options for 'select' type columns */
|
|
72
|
+
selectOptions?: {
|
|
73
|
+
value: string;
|
|
74
|
+
label: string;
|
|
75
|
+
}[];
|
|
76
|
+
/** Column group for grouped picker (future feature) */
|
|
77
|
+
group?: string;
|
|
78
|
+
}
|
|
58
79
|
export interface FilterCondition {
|
|
59
80
|
id: string;
|
|
60
81
|
field: string;
|
package/dist/utils/filters.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type { FilterCondition, FilterLogic } from '../types';
|
|
1
|
+
import type { FilterCondition, FilterOperator, FilterLogic, ColumnDataType } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Operator option for UI display
|
|
4
|
+
*/
|
|
5
|
+
export interface OperatorOption {
|
|
6
|
+
value: FilterOperator;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get operators available for a specific data type
|
|
11
|
+
*/
|
|
12
|
+
export declare function getOperatorsForType(dataType?: ColumnDataType): OperatorOption[];
|
|
2
13
|
/**
|
|
3
14
|
* Evaluate a single filter condition against a row value
|
|
4
15
|
*/
|
|
@@ -6,7 +17,7 @@ export declare function evaluateCondition(condition: FilterCondition, rowValue:
|
|
|
6
17
|
/**
|
|
7
18
|
* Filter data array by multiple conditions with AND or OR logic
|
|
8
19
|
*/
|
|
9
|
-
export declare function applyFilters<T extends Record<string,
|
|
20
|
+
export declare function applyFilters<T extends Record<string, unknown>>(data: T[], conditions: FilterCondition[], logic?: FilterLogic): T[];
|
|
10
21
|
/**
|
|
11
22
|
* Create a text filter configuration
|
|
12
23
|
*/
|
package/dist/utils/filters.js
CHANGED
|
@@ -1,4 +1,69 @@
|
|
|
1
1
|
// Filter creation and evaluation utilities
|
|
2
|
+
/**
|
|
3
|
+
* All available operators with labels
|
|
4
|
+
*/
|
|
5
|
+
const ALL_OPERATORS = [
|
|
6
|
+
{ value: 'equals', label: 'equals' },
|
|
7
|
+
{ value: 'not_equals', label: 'does not equal' },
|
|
8
|
+
{ value: 'contains', label: 'contains' },
|
|
9
|
+
{ value: 'not_contains', label: 'does not contain' },
|
|
10
|
+
{ value: 'starts_with', label: 'starts with' },
|
|
11
|
+
{ value: 'ends_with', label: 'ends with' },
|
|
12
|
+
{ value: 'is_empty', label: 'is empty' },
|
|
13
|
+
{ value: 'is_not_empty', label: 'is not empty' },
|
|
14
|
+
{ value: 'greater_than', label: '>' },
|
|
15
|
+
{ value: 'less_than', label: '<' },
|
|
16
|
+
{ value: 'greater_or_equal', label: '>=' },
|
|
17
|
+
{ value: 'less_or_equal', label: '<=' },
|
|
18
|
+
{ value: 'is_before', label: 'is before' },
|
|
19
|
+
{ value: 'is_after', label: 'is after' }
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Get operators available for a specific data type
|
|
23
|
+
*/
|
|
24
|
+
export function getOperatorsForType(dataType = 'text') {
|
|
25
|
+
switch (dataType) {
|
|
26
|
+
case 'text':
|
|
27
|
+
return ALL_OPERATORS.filter((op) => [
|
|
28
|
+
'equals',
|
|
29
|
+
'not_equals',
|
|
30
|
+
'contains',
|
|
31
|
+
'not_contains',
|
|
32
|
+
'starts_with',
|
|
33
|
+
'ends_with',
|
|
34
|
+
'is_empty',
|
|
35
|
+
'is_not_empty'
|
|
36
|
+
].includes(op.value));
|
|
37
|
+
case 'number':
|
|
38
|
+
return ALL_OPERATORS.filter((op) => [
|
|
39
|
+
'equals',
|
|
40
|
+
'not_equals',
|
|
41
|
+
'greater_than',
|
|
42
|
+
'less_than',
|
|
43
|
+
'greater_or_equal',
|
|
44
|
+
'less_or_equal',
|
|
45
|
+
'is_empty',
|
|
46
|
+
'is_not_empty'
|
|
47
|
+
].includes(op.value));
|
|
48
|
+
case 'date':
|
|
49
|
+
return ALL_OPERATORS.filter((op) => ['equals', 'not_equals', 'is_before', 'is_after', 'is_empty', 'is_not_empty'].includes(op.value));
|
|
50
|
+
case 'boolean':
|
|
51
|
+
return ALL_OPERATORS.filter((op) => ['equals', 'is_empty', 'is_not_empty'].includes(op.value));
|
|
52
|
+
case 'select':
|
|
53
|
+
return ALL_OPERATORS.filter((op) => ['equals', 'not_equals', 'is_empty', 'is_not_empty'].includes(op.value));
|
|
54
|
+
default:
|
|
55
|
+
return ALL_OPERATORS.filter((op) => [
|
|
56
|
+
'equals',
|
|
57
|
+
'not_equals',
|
|
58
|
+
'contains',
|
|
59
|
+
'not_contains',
|
|
60
|
+
'starts_with',
|
|
61
|
+
'ends_with',
|
|
62
|
+
'is_empty',
|
|
63
|
+
'is_not_empty'
|
|
64
|
+
].includes(op.value));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
2
67
|
/**
|
|
3
68
|
* Evaluate a single filter condition against a row value
|
|
4
69
|
*/
|
|
@@ -32,6 +97,16 @@ export function evaluateCondition(condition, rowValue) {
|
|
|
32
97
|
return Number(rowValue) >= Number(value);
|
|
33
98
|
case 'less_or_equal':
|
|
34
99
|
return Number(rowValue) <= Number(value);
|
|
100
|
+
case 'is_before': {
|
|
101
|
+
const rowDate = new Date(rowValue);
|
|
102
|
+
const filterDate = new Date(value);
|
|
103
|
+
return !isNaN(rowDate.getTime()) && !isNaN(filterDate.getTime()) && rowDate < filterDate;
|
|
104
|
+
}
|
|
105
|
+
case 'is_after': {
|
|
106
|
+
const rowDate = new Date(rowValue);
|
|
107
|
+
const filterDate = new Date(value);
|
|
108
|
+
return !isNaN(rowDate.getTime()) && !isNaN(filterDate.getTime()) && rowDate > filterDate;
|
|
109
|
+
}
|
|
35
110
|
default:
|
|
36
111
|
return true;
|
|
37
112
|
}
|
package/package.json
CHANGED