@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 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
- - 12 filter operators: equals, contains, starts with, greater than, etc.
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;
@@ -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.accessorKey || col.id;
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.accessorKey || col.id));
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: $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<Record<string, any>, unknown>;
26
+ cell: import("@tanstack/table-core").Cell<T, unknown>;
27
27
  column: string;
28
28
  };
29
29
  };
30
30
  }
31
- export type TableKitProps_<T> = ReturnType<__sveltets_Render<T>['props']>;
32
- export type TableKitEvents<T> = ReturnType<__sveltets_Render<T>['events']>;
33
- export type TableKitSlots<T> = ReturnType<__sveltets_Render<T>['slots']>;
34
- export default class TableKit<T> extends SvelteComponent<TableKitProps_<T>, TableKitEvents<T>, TableKitSlots<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
- const col = columns.find((c) => getColumnId(c) === condition.field);
137
- return col ? getColumnLabel(col) : condition.field;
138
+ return selectedColumn ? getColumnLabel(selectedColumn) : condition.field;
138
139
  })();
139
- const operatorOptions = [
140
- { value: "equals", label: "equals" },
141
- { value: "not_equals", label: "does not equal" },
142
- { value: "contains", label: "contains" },
143
- { value: "not_contains", label: "does not contain" },
144
- { value: "starts_with", label: "starts with" },
145
- { value: "ends_with", label: "ends with" },
146
- { value: "is_empty", label: "is empty" },
147
- { value: "is_not_empty", label: "is not empty" },
148
- { value: "greater_than", label: ">" },
149
- { value: "less_than", label: "<" },
150
- { value: "greater_or_equal", label: ">=" },
151
- { value: "less_or_equal", label: "<=" }
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
- <input
343
- bind:this={valueInputRef}
344
- type="text"
345
- class="value-input"
346
- value={condition.value || ''}
347
- on:input={handleValueChange}
348
- on:focus={handleValueFocus}
349
- on:keydown={handleValueKeydown}
350
- disabled={valueDisabled}
351
- placeholder={valueDisabled ? 'N/A' : numericRange ? `${numericRange.min} - ${numericRange.max}` : 'Enter value...'}
352
- autocomplete="off"
353
- />
354
-
355
- {#if numericRange && !valueDisabled}
356
- <div class="numeric-range-hint">
357
- Range: {numericRange.min} - {numericRange.max}
358
- </div>
359
- {/if}
360
-
361
- {#if showValueSuggestions && filteredValueSuggestions.length > 0 && !numericRange}
362
- <div class="value-suggestions">
363
- {#each filteredValueSuggestions as { value, matchedIndices }, i}
364
- <button
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
- {#if columnValues.length > 50}
385
- <div class="suggestions-overflow">
386
- and {columnValues.length - 50} more...
387
- </div>
388
- {/if}
389
- </div>
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.accessorKey || col.id;
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.accessorKey || column.id}
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;
@@ -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, any>>(data: T[], conditions: FilterCondition[], logic?: FilterLogic): T[];
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
  */
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotleybuilder/svelte-table-kit",
3
- "version": "0.10.1",
3
+ "version": "0.13.0",
4
4
  "description": "A comprehensive, AI-configurable data table component for Svelte and SvelteKit, built on TanStack Table v8",
5
5
  "author": "Sertantai",
6
6
  "license": "MIT",