@shotleybuilder/svelte-table-kit 0.2.0 → 0.4.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
@@ -19,10 +19,11 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
19
19
  - ↔️ **Column spacing control** - 3 sizes: narrow, normal, wide
20
20
  - 🔍 **Advanced filtering** - 12 operators with AND/OR logic
21
21
  - 📊 **Multi-level grouping** - Up to 3 nested levels (like Airtable)
22
- - ⬆️ Multi-column sorting with visual indicators
22
+ - ⬆️ **Flexible sorting** - Column header or Airtable-style sort control
23
23
  - 📄 Pagination with customizable page sizes
24
24
  - 💾 LocalStorage persistence for all user preferences
25
25
  - ✂️ Text truncation with ellipsis for long content
26
+ - 📋 **Column context menu** - Quick access to sort, filter, group, and hide actions
26
27
 
27
28
  **Advanced Filtering:**
28
29
  - 12 filter operators: equals, contains, starts with, greater than, etc.
@@ -31,6 +32,13 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
31
32
  - Active filter count badge
32
33
  - Real-time filtering as you type
33
34
 
35
+ **Sorting Options:**
36
+ - **Column header mode** (default) - Click headers to sort with ↑↓↕ indicators
37
+ - **Airtable-style control** - Dedicated sort dropdown with multi-level sorting
38
+ - Choose column and direction (A → Z or Z → A)
39
+ - Multiple sort levels applied top to bottom
40
+ - Collapsible SortBar UI
41
+
34
42
  **Grouping & Hierarchy:**
35
43
  - Group by up to 3 columns simultaneously
36
44
  - Expand/collapse groups with chevron buttons
@@ -38,6 +46,15 @@ Svelte Table Kit brings Airtable-like functionality to your Svelte applications
38
46
  - Item count per group
39
47
  - Collapsible GroupBar UI
40
48
 
49
+ **Column Context Menu:**
50
+ - Hover over column headers to reveal menu trigger (chevron icon)
51
+ - **Sort A → Z / Sort Z → A** - Quick sort with active state indication
52
+ - **Filter by this field** - Creates pre-filled filter condition
53
+ - **Group by this field** - Adds column to grouping configuration
54
+ - **Hide field** - Remove column from view
55
+ - Actions conditionally shown based on feature flags
56
+ - Seamlessly integrates with existing controls
57
+
41
58
  **Developer Experience:**
42
59
  - 🎨 Headless design - style it your way
43
60
  - 📦 Built on TanStack Table v8 (battle-tested, powerful)
@@ -107,19 +124,21 @@ The simplest way to use TableKit:
107
124
 
108
125
  ### With Configuration
109
126
 
110
- Use AI-generated or predefined configurations:
127
+ Customize initial table state programmatically:
111
128
 
112
129
  ```svelte
113
130
  <script>
114
- import { TableKit, presets } from '@shotleybuilder/svelte-table-kit';
115
-
116
- const config = presets.dashboard; // or generate with AI
131
+ import { TableKit } from '@shotleybuilder/svelte-table-kit';
117
132
  </script>
118
133
 
119
134
  <TableKit
120
135
  {data}
121
136
  {columns}
122
- {config}
137
+ config={{
138
+ defaultColumnOrder: ['name', 'role', 'age', 'id'], // Set initial column order
139
+ defaultColumnSizing: { name: 200, role: 150 }, // Set initial column widths
140
+ defaultVisibleColumns: ['name', 'role', 'age'] // Hide 'id' column initially
141
+ }}
123
142
  features={{
124
143
  columnVisibility: true,
125
144
  filtering: true,
@@ -143,6 +162,7 @@ Control which features are enabled:
143
162
  columnReordering: true,
144
163
  filtering: true,
145
164
  sorting: true,
165
+ sortingMode: 'control', // 'header' (default) or 'control' (Airtable-style)
146
166
  pagination: true,
147
167
  rowSelection: false,
148
168
  grouping: false
@@ -150,6 +170,10 @@ Control which features are enabled:
150
170
  />
151
171
  ```
152
172
 
173
+ **Sorting Modes:**
174
+ - `sortingMode: 'header'` - Click column headers to sort (default behavior)
175
+ - `sortingMode: 'control'` - Use Airtable-style sort dropdown with multi-level support
176
+
153
177
  ### Event Handlers
154
178
 
155
179
  Listen to table events:
@@ -23,8 +23,11 @@ import {
23
23
  import { applyFilters } from "./utils/filters";
24
24
  import FilterBar from "./components/FilterBar.svelte";
25
25
  import GroupBar from "./components/GroupBar.svelte";
26
+ import SortBar from "./components/SortBar.svelte";
27
+ import ColumnMenu from "./components/ColumnMenu.svelte";
26
28
  export let data = [];
27
29
  export let columns = [];
30
+ export let config = void 0;
28
31
  export let storageKey = "table-kit";
29
32
  export let persistState = true;
30
33
  export let align = "left";
@@ -36,6 +39,8 @@ export let features = {
36
39
  columnReordering: true,
37
40
  filtering: true,
38
41
  sorting: true,
42
+ sortingMode: "header",
43
+ // 'header' or 'control'
39
44
  pagination: true
40
45
  };
41
46
  export let onRowClick = void 0;
@@ -52,12 +57,14 @@ let columnFilters = writable(
52
57
  persistState && storageKey ? loadColumnFilters(storageKey) : []
53
58
  );
54
59
  let columnOrder = writable(
55
- persistState && storageKey ? loadColumnOrder(storageKey) : []
60
+ persistState && storageKey ? loadColumnOrder(storageKey) : config?.defaultColumnOrder || []
56
61
  );
57
62
  let filterConditions = writable([]);
58
63
  let filterLogic = writable("and");
64
+ let filterBarExpanded = false;
59
65
  let grouping = writable([]);
60
66
  let expanded = writable(true);
67
+ let groupBarExpanded = false;
61
68
  $: horizontalPadding = columnSpacing === "narrow" ? 0.5 : columnSpacing === "wide" ? 2 : 1;
62
69
  $: verticalPadding = rowHeight === "short" ? 0.375 : rowHeight === "tall" ? 1 : rowHeight === "extra_tall" ? 1.5 : 0.75;
63
70
  $: filteredData = applyFilters(data, $filterConditions, $filterLogic);
@@ -68,6 +75,48 @@ $: if (persistState && storageKey && isBrowser) {
68
75
  saveColumnOrder(storageKey, $columnOrder);
69
76
  }
70
77
  let showColumnPicker = false;
78
+ let columnPickerButton = null;
79
+ let columnPickerPosition = { top: 0, left: 0 };
80
+ let openColumnMenuId = null;
81
+ function generateFilterId() {
82
+ return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
83
+ }
84
+ function addFilterForColumn(columnId) {
85
+ const newCondition = {
86
+ id: generateFilterId(),
87
+ field: columnId,
88
+ operator: "equals",
89
+ value: ""
90
+ };
91
+ filterConditions.update((conditions) => [...conditions, newCondition]);
92
+ filterBarExpanded = true;
93
+ }
94
+ function addGroupForColumn(columnId) {
95
+ grouping.update((groups) => {
96
+ if (groups.includes(columnId)) {
97
+ return groups;
98
+ }
99
+ if (groups.length >= 3) {
100
+ return groups;
101
+ }
102
+ return [...groups, columnId];
103
+ });
104
+ groupBarExpanded = true;
105
+ }
106
+ function updateColumnPickerPosition() {
107
+ if (columnPickerButton && showColumnPicker) {
108
+ const rect = columnPickerButton.getBoundingClientRect();
109
+ columnPickerPosition = {
110
+ top: rect.bottom + 8,
111
+ // 8px margin (0.5rem)
112
+ left: rect.right - 224
113
+ // 224px = 14rem dropdown width
114
+ };
115
+ }
116
+ }
117
+ $: if (showColumnPicker) {
118
+ updateColumnPickerPosition();
119
+ }
71
120
  let showRowHeightMenu = false;
72
121
  let showColumnSpacingMenu = false;
73
122
  let draggedColumnId = null;
@@ -194,6 +243,7 @@ $: if ($columnOrder.length === 0 && columns.length > 0) {
194
243
  columnOrder.set(columns.map((col) => col.accessorKey || col.id));
195
244
  }
196
245
  $: hasActiveFilters = $filterConditions.length > 0;
246
+ $: totalTableWidth = $table.getVisibleLeafColumns().reduce((sum, col) => sum + col.getSize(), 0);
197
247
  $: if (onStateChange) {
198
248
  onStateChange({
199
249
  columnVisibility: $columnVisibility,
@@ -208,7 +258,7 @@ $: if (onStateChange) {
208
258
 
209
259
  <div class="table-kit-container align-{align}">
210
260
  <!-- Filters and Controls -->
211
- {#if features.filtering !== false || features.grouping !== false || features.columnVisibility !== false}
261
+ {#if features.filtering !== false || features.grouping !== false || features.columnVisibility !== false || (features.sorting !== false && features.sortingMode === 'control')}
212
262
  <div class="table-kit-toolbar">
213
263
  <!-- Filter Controls -->
214
264
  {#if features.filtering !== false}
@@ -219,6 +269,19 @@ $: if (onStateChange) {
219
269
  onConditionsChange={(newConditions) => filterConditions.set(newConditions)}
220
270
  logic={$filterLogic}
221
271
  onLogicChange={(newLogic) => filterLogic.set(newLogic)}
272
+ isExpanded={filterBarExpanded}
273
+ onExpandedChange={(expanded) => (filterBarExpanded = expanded)}
274
+ />
275
+ </div>
276
+ {/if}
277
+
278
+ <!-- Sort Controls (when sortingMode is 'control') -->
279
+ {#if features.sorting !== false && features.sortingMode === 'control'}
280
+ <div class="table-kit-sorts">
281
+ <SortBar
282
+ {columns}
283
+ sorting={$sorting}
284
+ onSortingChange={(newSorting) => sorting.set(newSorting)}
222
285
  />
223
286
  </div>
224
287
  {/if}
@@ -230,6 +293,8 @@ $: if (onStateChange) {
230
293
  {columns}
231
294
  grouping={$grouping}
232
295
  onGroupingChange={(newGrouping) => grouping.set(newGrouping)}
296
+ isExpanded={groupBarExpanded}
297
+ onExpandedChange={(expanded) => (groupBarExpanded = expanded)}
233
298
  />
234
299
  </div>
235
300
  {/if}
@@ -405,6 +470,7 @@ $: if (onStateChange) {
405
470
  <div class="table-kit-column-picker">
406
471
  <div class="relative">
407
472
  <button
473
+ bind:this={columnPickerButton}
408
474
  on:click={() => (showColumnPicker = !showColumnPicker)}
409
475
  class="column-picker-btn"
410
476
  >
@@ -423,7 +489,10 @@ $: if (onStateChange) {
423
489
  <!-- svelte-ignore a11y-click-events-have-key-events -->
424
490
  <!-- svelte-ignore a11y-no-static-element-interactions -->
425
491
  <div class="backdrop" on:click={() => (showColumnPicker = false)} />
426
- <div class="column-picker-dropdown">
492
+ <div
493
+ class="column-picker-dropdown"
494
+ style="top: {columnPickerPosition.top}px; left: {columnPickerPosition.left}px;"
495
+ >
427
496
  <div class="dropdown-header">
428
497
  <span>Toggle Columns</span>
429
498
  <div class="header-actions">
@@ -465,7 +534,7 @@ $: if (onStateChange) {
465
534
  </div>
466
535
  {:else}
467
536
  <div class="table-kit-scroll">
468
- <table class="table-kit-table">
537
+ <table class="table-kit-table" style="width: {totalTableWidth}px;">
469
538
  <thead>
470
539
  {#each $table.getHeaderGroups() as headerGroup}
471
540
  <tr>
@@ -477,34 +546,97 @@ $: if (onStateChange) {
477
546
  style="width: {header.getSize()}px;"
478
547
  >
479
548
  {#if !header.isPlaceholder}
480
- <div
481
- class="th-content"
482
- style="padding: {verticalPadding}rem {horizontalPadding}rem; cursor: {features.columnReordering !==
483
- false
484
- ? 'grab'
485
- : 'default'};"
486
- draggable={features.columnReordering !== false}
487
- on:dragstart={() => handleDragStart(header.column.id)}
488
- >
489
- <button
490
- class="sort-btn"
491
- class:sortable={header.column.getCanSort()}
492
- on:click={header.column.getToggleSortingHandler()}
549
+ <div class="th-wrapper">
550
+ <div
551
+ class="th-content"
552
+ style="padding: {verticalPadding}rem {horizontalPadding}rem; cursor: {features.columnReordering !==
553
+ false
554
+ ? 'grab'
555
+ : 'default'};"
556
+ draggable={features.columnReordering !== false}
557
+ on:dragstart={() => handleDragStart(header.column.id)}
493
558
  >
494
- <span class="header-text">
495
- <svelte:component
496
- this={flexRender(header.column.columnDef.header, header.getContext())}
497
- />
498
- </span>
499
- {#if features.sorting !== false && header.column.getCanSort()}
500
- <span class="sort-icon">
501
- {{
502
- asc: '↑',
503
- desc: '↓'
504
- }[header.column.getIsSorted()] ?? '↕'}
559
+ {#if features.sorting !== false && features.sortingMode !== 'control' && header.column.getCanSort()}
560
+ <button
561
+ class="sort-btn"
562
+ class:sortable={header.column.getCanSort()}
563
+ on:click={header.column.getToggleSortingHandler()}
564
+ >
565
+ <span class="header-text">
566
+ <svelte:component
567
+ this={flexRender(header.column.columnDef.header, header.getContext())}
568
+ />
569
+ </span>
570
+ <span class="sort-icon">
571
+ {{
572
+ asc: '↑',
573
+ desc: '↓'
574
+ }[header.column.getIsSorted()] ?? '↕'}
575
+ </span>
576
+ </button>
577
+ {:else}
578
+ <span class="header-text">
579
+ <svelte:component
580
+ this={flexRender(header.column.columnDef.header, header.getContext())}
581
+ />
505
582
  </span>
506
583
  {/if}
507
- </button>
584
+
585
+ <!-- Column Menu Trigger -->
586
+ <button
587
+ class="column-menu-trigger"
588
+ on:click|stopPropagation={() => {
589
+ openColumnMenuId =
590
+ openColumnMenuId === header.column.id ? null : header.column.id;
591
+ }}
592
+ aria-label="Column options"
593
+ >
594
+ <svg
595
+ width="12"
596
+ height="12"
597
+ viewBox="0 0 12 12"
598
+ fill="none"
599
+ xmlns="http://www.w3.org/2000/svg"
600
+ >
601
+ <path
602
+ d="M3 5L6 8L9 5"
603
+ stroke="currentColor"
604
+ stroke-width="1.5"
605
+ stroke-linecap="round"
606
+ stroke-linejoin="round"
607
+ />
608
+ </svg>
609
+ </button>
610
+ </div>
611
+
612
+ <!-- Column Menu -->
613
+ <ColumnMenu
614
+ column={header.column}
615
+ isOpen={openColumnMenuId === header.column.id}
616
+ canSort={features.sorting !== false}
617
+ canFilter={features.filtering !== false}
618
+ canGroup={features.grouping !== false}
619
+ on:sort={(e) => {
620
+ const direction = e.detail.direction;
621
+ header.column.toggleSorting(direction === 'desc');
622
+ openColumnMenuId = null;
623
+ }}
624
+ on:filter={() => {
625
+ addFilterForColumn(header.column.id);
626
+ openColumnMenuId = null;
627
+ }}
628
+ on:group={() => {
629
+ addGroupForColumn(header.column.id);
630
+ openColumnMenuId = null;
631
+ }}
632
+ on:hide={() => {
633
+ header.column.toggleVisibility(false);
634
+ openColumnMenuId = null;
635
+ }}
636
+ on:close={() => {
637
+ openColumnMenuId = null;
638
+ }}
639
+ />
508
640
  </div>
509
641
  <!-- Resize Handle -->
510
642
  {#if features.columnResizing !== false && header.column.getCanResize()}
@@ -822,9 +954,9 @@ $: if (onStateChange) {
822
954
  }
823
955
 
824
956
  .column-picker-dropdown {
825
- position: absolute;
826
- right: 0;
827
- z-index: 20;
957
+ position: fixed; /* Use fixed to break out of container constraints */
958
+ right: auto;
959
+ z-index: 30;
828
960
  margin-top: 0.5rem;
829
961
  width: 14rem;
830
962
  border-radius: 0.375rem;
@@ -871,7 +1003,8 @@ $: if (onStateChange) {
871
1003
  }
872
1004
 
873
1005
  .column-list {
874
- max-height: 16rem;
1006
+ min-height: 12rem; /* Ensure picker stays usable even when all columns hidden */
1007
+ max-height: 20rem;
875
1008
  overflow-y: auto;
876
1009
  }
877
1010
 
@@ -910,9 +1043,9 @@ $: if (onStateChange) {
910
1043
  }
911
1044
 
912
1045
  .table-kit-table {
913
- width: auto;
914
1046
  border-collapse: collapse;
915
1047
  table-layout: fixed;
1048
+ min-width: 100%; /* Ensure table is at least full container width */
916
1049
  }
917
1050
 
918
1051
  thead {
@@ -1006,6 +1139,39 @@ $: if (onStateChange) {
1006
1139
  color: #9ca3af;
1007
1140
  }
1008
1141
 
1142
+ .th-wrapper {
1143
+ position: relative;
1144
+ display: flex;
1145
+ align-items: center;
1146
+ width: 100%;
1147
+ }
1148
+
1149
+ .column-menu-trigger {
1150
+ flex-shrink: 0;
1151
+ display: flex;
1152
+ align-items: center;
1153
+ justify-content: center;
1154
+ padding: 0.25rem;
1155
+ margin-left: 0.25rem;
1156
+ background: transparent;
1157
+ border: none;
1158
+ border-radius: 0.25rem;
1159
+ cursor: pointer;
1160
+ color: #9ca3af;
1161
+ transition: all 0.15s;
1162
+ opacity: 0;
1163
+ }
1164
+
1165
+ .th-wrapper:hover .column-menu-trigger,
1166
+ .column-menu-trigger:focus {
1167
+ opacity: 1;
1168
+ }
1169
+
1170
+ .column-menu-trigger:hover {
1171
+ background-color: #f3f4f6;
1172
+ color: #374151;
1173
+ }
1174
+
1009
1175
  .resize-handle {
1010
1176
  position: absolute;
1011
1177
  top: 0;
@@ -5,6 +5,7 @@ declare class __sveltets_Render<T> {
5
5
  props(): {
6
6
  data?: T[] | undefined;
7
7
  columns?: ColumnDef<T>[] | undefined;
8
+ config?: TableKitProps<T_1>["config"];
8
9
  storageKey?: TableKitProps<T_1>["storageKey"];
9
10
  persistState?: TableKitProps<T_1>["persistState"];
10
11
  align?: TableKitProps<T_1>["align"];
@@ -0,0 +1,271 @@
1
+ <script>import { createEventDispatcher, onDestroy, onMount } from "svelte";
2
+ import { browser } from "$app/environment";
3
+ export let column;
4
+ export let isOpen = false;
5
+ export let canSort = true;
6
+ export let canFilter = true;
7
+ export let canGroup = true;
8
+ const dispatch = createEventDispatcher();
9
+ let menuElement;
10
+ $: currentSort = column.getIsSorted();
11
+ $: canSortColumn = canSort && column.getCanSort();
12
+ $: canFilterColumn = canFilter;
13
+ $: canGroupColumn = canGroup;
14
+ function handleSortAsc() {
15
+ dispatch("sort", { direction: "asc" });
16
+ dispatch("close");
17
+ }
18
+ function handleSortDesc() {
19
+ dispatch("sort", { direction: "desc" });
20
+ dispatch("close");
21
+ }
22
+ function handleFilter() {
23
+ dispatch("filter");
24
+ dispatch("close");
25
+ }
26
+ function handleGroup() {
27
+ dispatch("group");
28
+ dispatch("close");
29
+ }
30
+ function handleHideColumn() {
31
+ dispatch("hide");
32
+ dispatch("close");
33
+ }
34
+ function handleClickOutside(event) {
35
+ if (menuElement && !menuElement.contains(event.target)) {
36
+ dispatch("close");
37
+ }
38
+ }
39
+ function handleKeydown(event) {
40
+ if (event.key === "Escape") {
41
+ dispatch("close");
42
+ }
43
+ }
44
+ $: {
45
+ if (browser) {
46
+ if (isOpen) {
47
+ setTimeout(() => {
48
+ document.addEventListener("mousedown", handleClickOutside);
49
+ document.addEventListener("keydown", handleKeydown);
50
+ }, 0);
51
+ } else {
52
+ document.removeEventListener("mousedown", handleClickOutside);
53
+ document.removeEventListener("keydown", handleKeydown);
54
+ }
55
+ }
56
+ }
57
+ onDestroy(() => {
58
+ if (browser) {
59
+ document.removeEventListener("mousedown", handleClickOutside);
60
+ document.removeEventListener("keydown", handleKeydown);
61
+ }
62
+ });
63
+ </script>
64
+
65
+ {#if isOpen}
66
+ <div class="column-menu" bind:this={menuElement}>
67
+ <!-- Sort Actions -->
68
+ {#if canSortColumn}
69
+ <button
70
+ class="menu-item"
71
+ class:active={currentSort === 'asc'}
72
+ on:click={handleSortAsc}
73
+ >
74
+ <svg
75
+ class="menu-icon"
76
+ width="16"
77
+ height="16"
78
+ viewBox="0 0 16 16"
79
+ fill="none"
80
+ xmlns="http://www.w3.org/2000/svg"
81
+ >
82
+ <path
83
+ d="M8 12V4M8 4L5 7M8 4L11 7"
84
+ stroke="currentColor"
85
+ stroke-width="1.5"
86
+ stroke-linecap="round"
87
+ stroke-linejoin="round"
88
+ />
89
+ </svg>
90
+ <span>Sort A → Z</span>
91
+ {#if currentSort === 'asc'}
92
+ <span class="check-icon">✓</span>
93
+ {/if}
94
+ </button>
95
+
96
+ <button
97
+ class="menu-item"
98
+ class:active={currentSort === 'desc'}
99
+ on:click={handleSortDesc}
100
+ >
101
+ <svg
102
+ class="menu-icon"
103
+ width="16"
104
+ height="16"
105
+ viewBox="0 0 16 16"
106
+ fill="none"
107
+ xmlns="http://www.w3.org/2000/svg"
108
+ >
109
+ <path
110
+ d="M8 4V12M8 12L11 9M8 12L5 9"
111
+ stroke="currentColor"
112
+ stroke-width="1.5"
113
+ stroke-linecap="round"
114
+ stroke-linejoin="round"
115
+ />
116
+ </svg>
117
+ <span>Sort Z → A</span>
118
+ {#if currentSort === 'desc'}
119
+ <span class="check-icon">✓</span>
120
+ {/if}
121
+ </button>
122
+
123
+ <div class="menu-divider"></div>
124
+ {/if}
125
+
126
+ <!-- Filter by this field -->
127
+ {#if canFilterColumn}
128
+ <button class="menu-item" on:click={handleFilter}>
129
+ <svg
130
+ class="menu-icon"
131
+ width="16"
132
+ height="16"
133
+ viewBox="0 0 16 16"
134
+ fill="none"
135
+ xmlns="http://www.w3.org/2000/svg"
136
+ >
137
+ <path
138
+ d="M2 3h12M4 6h8M6 9h4M7 12h2"
139
+ stroke="currentColor"
140
+ stroke-width="1.5"
141
+ stroke-linecap="round"
142
+ stroke-linejoin="round"
143
+ />
144
+ </svg>
145
+ <span>Filter by this field</span>
146
+ </button>
147
+
148
+ <div class="menu-divider"></div>
149
+ {/if}
150
+
151
+ <!-- Group by this field -->
152
+ {#if canGroupColumn}
153
+ <button class="menu-item" on:click={handleGroup}>
154
+ <svg
155
+ class="menu-icon"
156
+ width="16"
157
+ height="16"
158
+ viewBox="0 0 16 16"
159
+ fill="none"
160
+ xmlns="http://www.w3.org/2000/svg"
161
+ >
162
+ <path
163
+ d="M2 4h12M4 8h8M6 12h4"
164
+ stroke="currentColor"
165
+ stroke-width="1.5"
166
+ stroke-linecap="round"
167
+ stroke-linejoin="round"
168
+ />
169
+ </svg>
170
+ <span>Group by this field</span>
171
+ </button>
172
+
173
+ <div class="menu-divider"></div>
174
+ {/if}
175
+
176
+ <!-- Hide Field -->
177
+ <button class="menu-item" on:click={handleHideColumn}>
178
+ <svg
179
+ class="menu-icon"
180
+ width="16"
181
+ height="16"
182
+ viewBox="0 0 16 16"
183
+ fill="none"
184
+ xmlns="http://www.w3.org/2000/svg"
185
+ >
186
+ <path
187
+ d="M2 8C2 8 4.5 3 8 3C11.5 3 14 8 14 8C14 8 11.5 13 8 13C4.5 13 2 8 2 8Z"
188
+ stroke="currentColor"
189
+ stroke-width="1.5"
190
+ stroke-linecap="round"
191
+ stroke-linejoin="round"
192
+ />
193
+ <circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.5" />
194
+ <line x1="2" y1="2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" />
195
+ </svg>
196
+ <span>Hide field</span>
197
+ </button>
198
+ </div>
199
+ {/if}
200
+
201
+ <style>
202
+ .column-menu {
203
+ position: absolute;
204
+ top: 100%;
205
+ right: 0;
206
+ margin-top: 0.25rem;
207
+ min-width: 12rem;
208
+ background: white;
209
+ border: 1px solid rgba(0, 0, 0, 0.1);
210
+ border-radius: 0.375rem;
211
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
212
+ padding: 0.25rem;
213
+ z-index: 50;
214
+ }
215
+
216
+ .menu-item {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 0.75rem;
220
+ width: 100%;
221
+ padding: 0.5rem 0.75rem;
222
+ border: none;
223
+ background: transparent;
224
+ text-align: left;
225
+ font-size: 0.875rem;
226
+ cursor: pointer;
227
+ border-radius: 0.25rem;
228
+ transition: background-color 0.15s;
229
+ color: #374151;
230
+ position: relative;
231
+ }
232
+
233
+ .menu-item:hover {
234
+ background-color: #f3f4f6;
235
+ }
236
+
237
+ .menu-item.active {
238
+ background-color: #eff6ff;
239
+ color: #1e40af;
240
+ }
241
+
242
+ .menu-item.active:hover {
243
+ background-color: #dbeafe;
244
+ }
245
+
246
+ .menu-icon {
247
+ flex-shrink: 0;
248
+ color: #6b7280;
249
+ }
250
+
251
+ .menu-item:hover .menu-icon {
252
+ color: #374151;
253
+ }
254
+
255
+ .menu-item.active .menu-icon {
256
+ color: #3b82f6;
257
+ }
258
+
259
+ .check-icon {
260
+ margin-left: auto;
261
+ color: #3b82f6;
262
+ font-weight: bold;
263
+ font-size: 1rem;
264
+ }
265
+
266
+ .menu-divider {
267
+ height: 1px;
268
+ background-color: #e5e7eb;
269
+ margin: 0.25rem 0;
270
+ }
271
+ </style>
@@ -0,0 +1,29 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { Column } from '@tanstack/svelte-table';
3
+ declare const __propDef: {
4
+ props: {
5
+ column: Column<any>;
6
+ isOpen?: boolean;
7
+ canSort?: boolean;
8
+ canFilter?: boolean;
9
+ canGroup?: boolean;
10
+ };
11
+ events: {
12
+ sort: CustomEvent<any>;
13
+ close: CustomEvent<any>;
14
+ filter: CustomEvent<any>;
15
+ group: CustomEvent<any>;
16
+ hide: CustomEvent<any>;
17
+ } & {
18
+ [evt: string]: CustomEvent<any>;
19
+ };
20
+ slots: {};
21
+ exports?: {} | undefined;
22
+ bindings?: string | undefined;
23
+ };
24
+ export type ColumnMenuProps = typeof __propDef.props;
25
+ export type ColumnMenuEvents = typeof __propDef.events;
26
+ export type ColumnMenuSlots = typeof __propDef.slots;
27
+ export default class ColumnMenu extends SvelteComponent<ColumnMenuProps, ColumnMenuEvents, ColumnMenuSlots> {
28
+ }
29
+ export {};
@@ -4,7 +4,8 @@ export let conditions = [];
4
4
  export let onConditionsChange;
5
5
  export let logic = "and";
6
6
  export let onLogicChange;
7
- let isExpanded = false;
7
+ export let isExpanded = false;
8
+ export let onExpandedChange = void 0;
8
9
  function generateId() {
9
10
  return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
10
11
  }
@@ -16,7 +17,13 @@ function addCondition() {
16
17
  value: ""
17
18
  };
18
19
  onConditionsChange([...conditions, newCondition]);
19
- isExpanded = true;
20
+ setExpanded(true);
21
+ }
22
+ function setExpanded(value) {
23
+ isExpanded = value;
24
+ if (onExpandedChange) {
25
+ onExpandedChange(value);
26
+ }
20
27
  }
21
28
  function updateCondition(index, updated) {
22
29
  const newConditions = [...conditions];
@@ -27,12 +34,12 @@ function removeCondition(index) {
27
34
  const newConditions = conditions.filter((_, i) => i !== index);
28
35
  onConditionsChange(newConditions);
29
36
  if (newConditions.length === 0) {
30
- isExpanded = false;
37
+ setExpanded(false);
31
38
  }
32
39
  }
33
40
  function clearAllConditions() {
34
41
  onConditionsChange([]);
35
- isExpanded = false;
42
+ setExpanded(false);
36
43
  }
37
44
  $: hasConditions = conditions.length > 0;
38
45
  $: filterCount = conditions.filter(
@@ -42,7 +49,7 @@ $: filterCount = conditions.filter(
42
49
 
43
50
  <div class="filter-bar">
44
51
  <!-- Compact Filter Button -->
45
- <button class="filter-toggle-btn" on:click={() => (isExpanded = !isExpanded)}>
52
+ <button class="filter-toggle-btn" on:click={() => setExpanded(!isExpanded)}>
46
53
  <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
54
  <path
48
55
  stroke-linecap="round"
@@ -8,6 +8,8 @@ declare const __propDef: {
8
8
  onConditionsChange: (conditions: FilterCondition[]) => void;
9
9
  logic?: FilterLogic;
10
10
  onLogicChange: (logic: FilterLogic) => void;
11
+ isExpanded?: boolean;
12
+ onExpandedChange?: ((expanded: boolean) => void) | undefined;
11
13
  };
12
14
  events: {
13
15
  [evt: string]: CustomEvent<any>;
@@ -1,12 +1,19 @@
1
1
  <script>export let columns;
2
2
  export let grouping = [];
3
3
  export let onGroupingChange;
4
+ export let isExpanded = false;
5
+ export let onExpandedChange = void 0;
4
6
  const MAX_LEVELS = 3;
5
- let isExpanded = false;
7
+ function setExpanded(value) {
8
+ isExpanded = value;
9
+ if (onExpandedChange) {
10
+ onExpandedChange(value);
11
+ }
12
+ }
6
13
  function addGroup() {
7
14
  if (grouping.length >= MAX_LEVELS) return;
8
15
  onGroupingChange([...grouping, ""]);
9
- isExpanded = true;
16
+ setExpanded(true);
10
17
  }
11
18
  function updateGroup(index, columnId) {
12
19
  const newGrouping = [...grouping];
@@ -17,12 +24,12 @@ function removeGroup(index) {
17
24
  const newGrouping = grouping.filter((_, i) => i !== index);
18
25
  onGroupingChange(newGrouping);
19
26
  if (newGrouping.length === 0) {
20
- isExpanded = false;
27
+ setExpanded(false);
21
28
  }
22
29
  }
23
30
  function clearAllGroups() {
24
31
  onGroupingChange([]);
25
- isExpanded = false;
32
+ setExpanded(false);
26
33
  }
27
34
  $: availableColumns = columns.filter((col) => {
28
35
  const columnId = col.accessorKey || col.id;
@@ -35,7 +42,7 @@ $: canAddMore = grouping.length < MAX_LEVELS;
35
42
 
36
43
  <div class="group-bar">
37
44
  <!-- Compact Group Button -->
38
- <button class="group-toggle-btn" on:click={() => (isExpanded = !isExpanded)}>
45
+ <button class="group-toggle-btn" on:click={() => setExpanded(!isExpanded)}>
39
46
  <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40
47
  <path
41
48
  stroke-linecap="round"
@@ -5,6 +5,8 @@ declare const __propDef: {
5
5
  columns: ColumnDef<any>[];
6
6
  grouping?: string[];
7
7
  onGroupingChange: (grouping: string[]) => void;
8
+ isExpanded?: boolean;
9
+ onExpandedChange?: ((expanded: boolean) => void) | undefined;
8
10
  };
9
11
  events: {
10
12
  [evt: string]: CustomEvent<any>;
@@ -0,0 +1,239 @@
1
+ <script>import SortConditionComponent from "./SortCondition.svelte";
2
+ export let columns;
3
+ export let sorting = [];
4
+ export let onSortingChange;
5
+ let isExpanded = false;
6
+ function addSort() {
7
+ const newSort = { id: "", desc: false };
8
+ onSortingChange([...sorting, newSort]);
9
+ isExpanded = true;
10
+ }
11
+ function updateSort(index, columnId, desc) {
12
+ const newSorting = [...sorting];
13
+ newSorting[index] = { id: columnId, desc };
14
+ onSortingChange(newSorting);
15
+ }
16
+ function removeSort(index) {
17
+ const newSorting = sorting.filter((_, i) => i !== index);
18
+ onSortingChange(newSorting);
19
+ if (newSorting.length === 0) {
20
+ isExpanded = false;
21
+ }
22
+ }
23
+ function clearAllSorts() {
24
+ onSortingChange([]);
25
+ isExpanded = false;
26
+ }
27
+ function createUpdateHandler(index) {
28
+ return (columnId, desc) => updateSort(index, columnId, desc);
29
+ }
30
+ function createRemoveHandler(index) {
31
+ return () => removeSort(index);
32
+ }
33
+ $: availableColumns = columns.filter((col) => {
34
+ const columnId = col.accessorKey || col.id;
35
+ return columnId && col.enableSorting !== false;
36
+ });
37
+ $: availableColumnsForNew = availableColumns.filter((col) => {
38
+ const columnId = col.accessorKey || col.id;
39
+ return !sorting.some((s) => s.id === columnId);
40
+ });
41
+ $: hasSorts = sorting.length > 0;
42
+ $: validSortCount = sorting.filter((s) => s.id !== "").length;
43
+ $: canAddMore = availableColumnsForNew.length > 0;
44
+ </script>
45
+
46
+ <div class="sort-bar">
47
+ <!-- Compact Sort Button -->
48
+ <button class="sort-toggle-btn" on:click={() => (isExpanded = !isExpanded)}>
49
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path
51
+ stroke-linecap="round"
52
+ stroke-linejoin="round"
53
+ stroke-width="2"
54
+ d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"
55
+ />
56
+ </svg>
57
+ Sort
58
+ {#if validSortCount > 0}
59
+ <span class="sort-badge">{validSortCount}</span>
60
+ {/if}
61
+ <svg
62
+ class="chevron"
63
+ class:expanded={isExpanded}
64
+ fill="none"
65
+ stroke="currentColor"
66
+ viewBox="0 0 24 24"
67
+ >
68
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
69
+ </svg>
70
+ </button>
71
+
72
+ <!-- Expandable Sort Panel -->
73
+ {#if isExpanded}
74
+ <div class="sort-panel">
75
+ {#if hasSorts}
76
+ <div class="sort-header">
77
+ <span class="sort-label">Sort by</span>
78
+ <button class="clear-all-btn" on:click={clearAllSorts}> Clear all </button>
79
+ </div>
80
+
81
+ <div class="sort-levels">
82
+ {#each sorting as sort, index (index)}
83
+ <SortConditionComponent
84
+ columnId={sort.id}
85
+ desc={sort.desc}
86
+ {columns}
87
+ {sorting}
88
+ onUpdate={createUpdateHandler(index)}
89
+ onRemove={createRemoveHandler(index)}
90
+ />
91
+ {/each}
92
+ </div>
93
+ {/if}
94
+
95
+ <button class="add-sort-btn" on:click={addSort} disabled={!canAddMore}>
96
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
97
+ <path
98
+ stroke-linecap="round"
99
+ stroke-linejoin="round"
100
+ stroke-width="2"
101
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
102
+ />
103
+ </svg>
104
+ {hasSorts ? 'Add another sort' : 'Add a sort'}
105
+ </button>
106
+ </div>
107
+ {/if}
108
+ </div>
109
+
110
+ <style>
111
+ .sort-bar {
112
+ position: relative;
113
+ }
114
+
115
+ /* Compact Sort Toggle Button */
116
+ .sort-toggle-btn {
117
+ display: inline-flex;
118
+ align-items: center;
119
+ gap: 0.5rem;
120
+ padding: 0.5rem 1rem;
121
+ font-size: 0.875rem;
122
+ font-weight: 500;
123
+ color: #374151;
124
+ background: white;
125
+ border: 1px solid #d1d5db;
126
+ border-radius: 0.375rem;
127
+ cursor: pointer;
128
+ transition: all 0.2s;
129
+ }
130
+
131
+ .sort-toggle-btn:hover {
132
+ background: #f9fafb;
133
+ border-color: #9ca3af;
134
+ }
135
+
136
+ .sort-badge {
137
+ display: inline-flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ min-width: 1.25rem;
141
+ height: 1.25rem;
142
+ padding: 0 0.375rem;
143
+ font-size: 0.75rem;
144
+ font-weight: 600;
145
+ color: white;
146
+ background: #f59e0b;
147
+ border-radius: 0.75rem;
148
+ }
149
+
150
+ .chevron {
151
+ width: 1rem;
152
+ height: 1rem;
153
+ transition: transform 0.2s;
154
+ }
155
+
156
+ .chevron.expanded {
157
+ transform: rotate(180deg);
158
+ }
159
+
160
+ /* Expandable Sort Panel */
161
+ .sort-panel {
162
+ position: absolute;
163
+ top: calc(100% + 0.5rem);
164
+ left: 0;
165
+ z-index: 20;
166
+ min-width: 400px;
167
+ padding: 1rem;
168
+ background: white;
169
+ border: 1px solid #e5e7eb;
170
+ border-radius: 0.5rem;
171
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
172
+ }
173
+
174
+ .sort-header {
175
+ display: flex;
176
+ justify-content: space-between;
177
+ align-items: center;
178
+ margin-bottom: 0.75rem;
179
+ }
180
+
181
+ .sort-label {
182
+ font-size: 0.875rem;
183
+ font-weight: 600;
184
+ color: #374151;
185
+ }
186
+
187
+ .clear-all-btn {
188
+ font-size: 0.75rem;
189
+ color: #6b7280;
190
+ background: none;
191
+ border: none;
192
+ cursor: pointer;
193
+ padding: 0.25rem 0.5rem;
194
+ border-radius: 0.25rem;
195
+ transition: all 0.2s;
196
+ }
197
+
198
+ .clear-all-btn:hover {
199
+ color: #dc2626;
200
+ background: #fee2e2;
201
+ }
202
+
203
+ .sort-levels {
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: 0.5rem;
207
+ margin-bottom: 0.75rem;
208
+ }
209
+
210
+ .add-sort-btn {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ gap: 0.5rem;
214
+ padding: 0.5rem 0.75rem;
215
+ font-size: 0.875rem;
216
+ font-weight: 500;
217
+ color: #f59e0b;
218
+ background: white;
219
+ border: 1px dashed #f59e0b;
220
+ border-radius: 0.375rem;
221
+ cursor: pointer;
222
+ transition: all 0.2s;
223
+ }
224
+
225
+ .add-sort-btn:hover:not(:disabled) {
226
+ background: #fef3c7;
227
+ border-style: solid;
228
+ }
229
+
230
+ .add-sort-btn:disabled {
231
+ opacity: 0.5;
232
+ cursor: not-allowed;
233
+ }
234
+
235
+ .icon {
236
+ width: 1rem;
237
+ height: 1rem;
238
+ }
239
+ </style>
@@ -0,0 +1,22 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { ColumnDef } from '@tanstack/svelte-table';
3
+ import type { SortingState } from '@tanstack/svelte-table';
4
+ declare const __propDef: {
5
+ props: {
6
+ columns: ColumnDef<any>[];
7
+ sorting?: SortingState;
8
+ onSortingChange: (sorting: SortingState) => void;
9
+ };
10
+ events: {
11
+ [evt: string]: CustomEvent<any>;
12
+ };
13
+ slots: {};
14
+ exports?: {} | undefined;
15
+ bindings?: string | undefined;
16
+ };
17
+ export type SortBarProps = typeof __propDef.props;
18
+ export type SortBarEvents = typeof __propDef.events;
19
+ export type SortBarSlots = typeof __propDef.slots;
20
+ export default class SortBar extends SvelteComponent<SortBarProps, SortBarEvents, SortBarSlots> {
21
+ }
22
+ export {};
@@ -0,0 +1,131 @@
1
+ <script>export let columnId;
2
+ export let desc;
3
+ export let columns;
4
+ export let sorting;
5
+ export let onUpdate;
6
+ export let onRemove;
7
+ const directionOptions = [
8
+ { value: "asc", label: "A \u2192 Z", icon: "\u2191" },
9
+ { value: "desc", label: "Z \u2192 A", icon: "\u2193" }
10
+ ];
11
+ function handleColumnChange(event) {
12
+ const newColumnId = event.target.value;
13
+ onUpdate(newColumnId, desc);
14
+ }
15
+ function handleDirectionChange(event) {
16
+ const direction = event.target.value;
17
+ onUpdate(columnId, direction === "desc");
18
+ }
19
+ function getColumnId(col) {
20
+ return col.accessorKey || col.id;
21
+ }
22
+ $: availableColumns = columns.filter((col) => {
23
+ const colId = getColumnId(col);
24
+ if (!colId || col.enableSorting === false) return false;
25
+ return colId === columnId || !sorting.some((s) => s.id === colId);
26
+ });
27
+ $: columnOptions = availableColumns.map((col) => ({
28
+ id: getColumnId(col) || "",
29
+ label: col.header || getColumnId(col) || ""
30
+ }));
31
+ $: currentDirection = desc ? "desc" : "asc";
32
+ </script>
33
+
34
+ <div class="sort-condition">
35
+ <select class="field-select" value={columnId} on:change={handleColumnChange}>
36
+ <option value="">Select field...</option>
37
+ {#each columnOptions as option}
38
+ <option value={option.id}>
39
+ {option.label}
40
+ </option>
41
+ {/each}
42
+ </select>
43
+
44
+ <select
45
+ class="direction-select"
46
+ value={currentDirection}
47
+ on:change={handleDirectionChange}
48
+ disabled={!columnId}
49
+ >
50
+ {#each directionOptions as option}
51
+ <option value={option.value}>
52
+ {option.icon} {option.label}
53
+ </option>
54
+ {/each}
55
+ </select>
56
+
57
+ <button class="remove-btn" on:click={onRemove} title="Remove sort">
58
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
59
+ <path
60
+ stroke-linecap="round"
61
+ stroke-linejoin="round"
62
+ stroke-width="2"
63
+ d="M6 18L18 6M6 6l12 12"
64
+ />
65
+ </svg>
66
+ </button>
67
+ </div>
68
+
69
+ <style>
70
+ .sort-condition {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 0.5rem;
74
+ padding: 0.5rem;
75
+ background: #f9fafb;
76
+ border-radius: 0.375rem;
77
+ }
78
+
79
+ .field-select,
80
+ .direction-select {
81
+ padding: 0.375rem 0.75rem;
82
+ font-size: 0.875rem;
83
+ border: 1px solid #d1d5db;
84
+ border-radius: 0.375rem;
85
+ background: white;
86
+ }
87
+
88
+ .field-select {
89
+ flex: 1;
90
+ min-width: 150px;
91
+ }
92
+
93
+ .direction-select {
94
+ flex: 0.7;
95
+ min-width: 120px;
96
+ }
97
+
98
+ .direction-select:disabled {
99
+ background: #f3f4f6;
100
+ color: #9ca3af;
101
+ cursor: not-allowed;
102
+ }
103
+
104
+ .field-select:focus,
105
+ .direction-select:focus {
106
+ outline: none;
107
+ border-color: #f59e0b;
108
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1);
109
+ }
110
+
111
+ .remove-btn {
112
+ flex-shrink: 0;
113
+ padding: 0.375rem;
114
+ background: none;
115
+ border: none;
116
+ color: #6b7280;
117
+ cursor: pointer;
118
+ border-radius: 0.25rem;
119
+ transition: all 0.2s;
120
+ }
121
+
122
+ .remove-btn:hover {
123
+ background: #fee2e2;
124
+ color: #dc2626;
125
+ }
126
+
127
+ .icon {
128
+ width: 1rem;
129
+ height: 1rem;
130
+ }
131
+ </style>
@@ -0,0 +1,25 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { ColumnDef } from '@tanstack/svelte-table';
3
+ import type { SortingState } from '@tanstack/svelte-table';
4
+ declare const __propDef: {
5
+ props: {
6
+ columnId: string;
7
+ desc: boolean;
8
+ columns: ColumnDef<any>[];
9
+ sorting: SortingState;
10
+ onUpdate: (columnId: string, desc: boolean) => void;
11
+ onRemove: () => void;
12
+ };
13
+ events: {
14
+ [evt: string]: CustomEvent<any>;
15
+ };
16
+ slots: {};
17
+ exports?: {} | undefined;
18
+ bindings?: string | undefined;
19
+ };
20
+ export type SortConditionProps = typeof __propDef.props;
21
+ export type SortConditionEvents = typeof __propDef.events;
22
+ export type SortConditionSlots = typeof __propDef.slots;
23
+ export default class SortCondition extends SvelteComponent<SortConditionProps, SortConditionEvents, SortConditionSlots> {
24
+ }
25
+ export {};
package/dist/types.d.ts CHANGED
@@ -21,6 +21,7 @@ export interface TableFeatures {
21
21
  columnReordering?: boolean;
22
22
  filtering?: boolean;
23
23
  sorting?: boolean;
24
+ sortingMode?: 'header' | 'control';
24
25
  pagination?: boolean;
25
26
  rowSelection?: boolean;
26
27
  grouping?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotleybuilder/svelte-table-kit",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",