@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 +30 -6
- package/dist/TableKit.svelte +200 -34
- package/dist/TableKit.svelte.d.ts +1 -0
- package/dist/components/ColumnMenu.svelte +271 -0
- package/dist/components/ColumnMenu.svelte.d.ts +29 -0
- package/dist/components/FilterBar.svelte +12 -5
- package/dist/components/FilterBar.svelte.d.ts +2 -0
- package/dist/components/GroupBar.svelte +12 -5
- package/dist/components/GroupBar.svelte.d.ts +2 -0
- package/dist/components/SortBar.svelte +239 -0
- package/dist/components/SortBar.svelte.d.ts +22 -0
- package/dist/components/SortCondition.svelte +131 -0
- package/dist/components/SortCondition.svelte.d.ts +25 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
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
|
-
- ⬆️
|
|
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
|
-
|
|
127
|
+
Customize initial table state programmatically:
|
|
111
128
|
|
|
112
129
|
```svelte
|
|
113
130
|
<script>
|
|
114
|
-
import { TableKit
|
|
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
|
-
{
|
|
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:
|
package/dist/TableKit.svelte
CHANGED
|
@@ -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
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
495
|
-
<
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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:
|
|
826
|
-
right:
|
|
827
|
-
z-index:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
setExpanded(false);
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
function clearAllConditions() {
|
|
34
41
|
onConditionsChange([]);
|
|
35
|
-
|
|
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={() => (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
setExpanded(false);
|
|
21
28
|
}
|
|
22
29
|
}
|
|
23
30
|
function clearAllGroups() {
|
|
24
31
|
onGroupingChange([]);
|
|
25
|
-
|
|
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={() => (
|
|
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
package/package.json
CHANGED