@shotleybuilder/svelte-table-kit 0.6.0 → 0.12.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 +13 -1
- package/dist/TableKit.svelte +76 -9
- package/dist/TableKit.svelte.d.ts +8 -8
- package/dist/components/CellContextMenu.svelte +208 -0
- package/dist/components/CellContextMenu.svelte.d.ts +41 -0
- package/dist/components/ColumnMenu.svelte +3 -3
- package/dist/components/FilterBar.svelte +76 -0
- package/dist/components/FilterBar.svelte.d.ts +3 -0
- package/dist/components/FilterCondition.svelte +639 -42
- package/dist/components/FilterCondition.svelte.d.ts +7 -0
- package/dist/components/GroupBar.svelte +5 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -2
- package/dist/stores/persistence.d.ts +10 -1
- package/dist/stores/persistence.js +14 -1
- package/dist/types.d.ts +20 -0
- package/dist/utils/filters.d.ts +13 -2
- package/dist/utils/filters.js +75 -0
- package/dist/utils/fuzzy.d.ts +47 -0
- package/dist/utils/fuzzy.js +142 -0
- package/package.json +76 -76
|
@@ -1,24 +1,209 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script>import { loadFilterColumnOrderMode, saveFilterColumnOrderMode } from "../stores/persistence";
|
|
2
|
+
import { fuzzyMatch, highlightMatches } from "../utils/fuzzy";
|
|
3
|
+
import { getOperatorsForType } from "../utils/filters";
|
|
4
|
+
import { onMount, tick } from "svelte";
|
|
5
|
+
export let condition;
|
|
2
6
|
export let columns;
|
|
7
|
+
export let columnOrder = [];
|
|
8
|
+
export let storageKey = "table-kit";
|
|
9
|
+
export let columnValues = [];
|
|
10
|
+
export let numericRange = null;
|
|
3
11
|
export let onUpdate;
|
|
4
12
|
export let onRemove;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
let orderMode = "definition";
|
|
14
|
+
let searchTerm = "";
|
|
15
|
+
let showDropdown = false;
|
|
16
|
+
let highlightedIndex = 0;
|
|
17
|
+
let searchInputRef;
|
|
18
|
+
let dropdownRef;
|
|
19
|
+
let showValueSuggestions = false;
|
|
20
|
+
let valueSuggestionIndex = 0;
|
|
21
|
+
let valueInputRef;
|
|
22
|
+
let valueSuggestionsRef;
|
|
23
|
+
onMount(() => {
|
|
24
|
+
orderMode = loadFilterColumnOrderMode(storageKey);
|
|
25
|
+
function handleClickOutside(event) {
|
|
26
|
+
if (dropdownRef && !dropdownRef.contains(event.target)) {
|
|
27
|
+
showDropdown = false;
|
|
28
|
+
}
|
|
29
|
+
if (valueSuggestionsRef && !valueSuggestionsRef.contains(event.target)) {
|
|
30
|
+
showValueSuggestions = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
document.addEventListener("click", handleClickOutside);
|
|
34
|
+
return () => document.removeEventListener("click", handleClickOutside);
|
|
35
|
+
});
|
|
36
|
+
$: filteredValueSuggestions = (() => {
|
|
37
|
+
if (!columnValues || columnValues.length === 0) return [];
|
|
38
|
+
const currentValue = condition.value || "";
|
|
39
|
+
if (!currentValue.trim()) {
|
|
40
|
+
return columnValues.slice(0, 50).map((val) => ({
|
|
41
|
+
value: val,
|
|
42
|
+
matchedIndices: [],
|
|
43
|
+
score: 0
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const val of columnValues) {
|
|
48
|
+
const match = fuzzyMatch(currentValue, val);
|
|
49
|
+
if (match) {
|
|
50
|
+
results.push({
|
|
51
|
+
value: val,
|
|
52
|
+
matchedIndices: match.matchedIndices,
|
|
53
|
+
score: match.score
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results.sort((a, b) => b.score - a.score).slice(0, 50);
|
|
58
|
+
})();
|
|
59
|
+
$: if (filteredValueSuggestions) {
|
|
60
|
+
valueSuggestionIndex = 0;
|
|
61
|
+
}
|
|
62
|
+
$: canShowSuggestions = columnValues.length > 0 && !valueDisabled;
|
|
63
|
+
function cycleOrderMode() {
|
|
64
|
+
const modes = ["definition", "ui", "alphabetical"];
|
|
65
|
+
const currentIndex = modes.indexOf(orderMode);
|
|
66
|
+
orderMode = modes[(currentIndex + 1) % modes.length];
|
|
67
|
+
saveFilterColumnOrderMode(storageKey, orderMode);
|
|
68
|
+
}
|
|
69
|
+
function getOrderModeLabel(mode) {
|
|
70
|
+
switch (mode) {
|
|
71
|
+
case "definition":
|
|
72
|
+
return "Default";
|
|
73
|
+
case "ui":
|
|
74
|
+
return "Table";
|
|
75
|
+
case "alphabetical":
|
|
76
|
+
return "A-Z";
|
|
77
|
+
default:
|
|
78
|
+
return "Default";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function getColumnId(col) {
|
|
82
|
+
return col.accessorKey || col.id || "";
|
|
83
|
+
}
|
|
84
|
+
function getColumnLabel(col) {
|
|
85
|
+
return String(col.header || getColumnId(col));
|
|
86
|
+
}
|
|
87
|
+
$: orderedColumns = (() => {
|
|
88
|
+
switch (orderMode) {
|
|
89
|
+
case "alphabetical":
|
|
90
|
+
return [...columns].sort((a, b) => {
|
|
91
|
+
const labelA = getColumnLabel(a).toLowerCase();
|
|
92
|
+
const labelB = getColumnLabel(b).toLowerCase();
|
|
93
|
+
return labelA.localeCompare(labelB);
|
|
94
|
+
});
|
|
95
|
+
case "ui":
|
|
96
|
+
return [...columns].sort((a, b) => {
|
|
97
|
+
const idA = getColumnId(a);
|
|
98
|
+
const idB = getColumnId(b);
|
|
99
|
+
const indexA = columnOrder.indexOf(idA);
|
|
100
|
+
const indexB = columnOrder.indexOf(idB);
|
|
101
|
+
const posA = indexA === -1 ? 999 : indexA;
|
|
102
|
+
const posB = indexB === -1 ? 999 : indexB;
|
|
103
|
+
return posA - posB;
|
|
104
|
+
});
|
|
105
|
+
case "definition":
|
|
106
|
+
default:
|
|
107
|
+
return columns;
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
$: filteredColumns = (() => {
|
|
111
|
+
if (!searchTerm.trim()) {
|
|
112
|
+
return orderedColumns.map((col) => ({
|
|
113
|
+
column: col,
|
|
114
|
+
matchedIndices: [],
|
|
115
|
+
score: 0
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const col of orderedColumns) {
|
|
120
|
+
const label = getColumnLabel(col);
|
|
121
|
+
const match = fuzzyMatch(searchTerm, label);
|
|
122
|
+
if (match) {
|
|
123
|
+
results.push({
|
|
124
|
+
column: col,
|
|
125
|
+
matchedIndices: match.matchedIndices,
|
|
126
|
+
score: match.score
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return results.sort((a, b) => b.score - a.score);
|
|
131
|
+
})();
|
|
132
|
+
$: if (filteredColumns) {
|
|
133
|
+
highlightedIndex = 0;
|
|
134
|
+
}
|
|
135
|
+
$: selectedColumn = condition.field ? columns.find((c) => getColumnId(c) === condition.field) : null;
|
|
136
|
+
$: selectedColumnLabel = (() => {
|
|
137
|
+
if (!condition.field) return "";
|
|
138
|
+
return selectedColumn ? getColumnLabel(selectedColumn) : condition.field;
|
|
139
|
+
})();
|
|
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
|
+
}
|
|
159
|
+
function selectColumn(col) {
|
|
160
|
+
const field = getColumnId(col);
|
|
21
161
|
onUpdate({ ...condition, field });
|
|
162
|
+
showDropdown = false;
|
|
163
|
+
searchTerm = "";
|
|
164
|
+
}
|
|
165
|
+
function handleSearchKeydown(event) {
|
|
166
|
+
switch (event.key) {
|
|
167
|
+
case "ArrowDown":
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
highlightedIndex = Math.min(highlightedIndex + 1, filteredColumns.length - 1);
|
|
170
|
+
scrollToHighlighted();
|
|
171
|
+
break;
|
|
172
|
+
case "ArrowUp":
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
175
|
+
scrollToHighlighted();
|
|
176
|
+
break;
|
|
177
|
+
case "Enter":
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
if (filteredColumns[highlightedIndex]) {
|
|
180
|
+
selectColumn(filteredColumns[highlightedIndex].column);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
case "Escape":
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
showDropdown = false;
|
|
186
|
+
searchTerm = "";
|
|
187
|
+
break;
|
|
188
|
+
case "Tab":
|
|
189
|
+
showDropdown = false;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function scrollToHighlighted() {
|
|
194
|
+
await tick();
|
|
195
|
+
const highlighted = dropdownRef?.querySelector(".field-option.highlighted");
|
|
196
|
+
if (highlighted) {
|
|
197
|
+
highlighted.scrollIntoView({ block: "nearest" });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function openDropdown() {
|
|
201
|
+
showDropdown = true;
|
|
202
|
+
searchTerm = "";
|
|
203
|
+
highlightedIndex = 0;
|
|
204
|
+
tick().then(() => {
|
|
205
|
+
searchInputRef?.focus();
|
|
206
|
+
});
|
|
22
207
|
}
|
|
23
208
|
function handleOperatorChange(event) {
|
|
24
209
|
const operator = event.target.value;
|
|
@@ -27,22 +212,131 @@ function handleOperatorChange(event) {
|
|
|
27
212
|
function handleValueChange(event) {
|
|
28
213
|
const value = event.target.value;
|
|
29
214
|
onUpdate({ ...condition, value });
|
|
215
|
+
if (canShowSuggestions && value.length >= 0) {
|
|
216
|
+
showValueSuggestions = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function handleValueFocus() {
|
|
220
|
+
if (canShowSuggestions) {
|
|
221
|
+
showValueSuggestions = true;
|
|
222
|
+
valueSuggestionIndex = 0;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function selectValueSuggestion(value) {
|
|
226
|
+
onUpdate({ ...condition, value });
|
|
227
|
+
showValueSuggestions = false;
|
|
228
|
+
}
|
|
229
|
+
function handleValueKeydown(event) {
|
|
230
|
+
if (!showValueSuggestions || filteredValueSuggestions.length === 0) return;
|
|
231
|
+
switch (event.key) {
|
|
232
|
+
case "ArrowDown":
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
valueSuggestionIndex = Math.min(
|
|
235
|
+
valueSuggestionIndex + 1,
|
|
236
|
+
filteredValueSuggestions.length - 1
|
|
237
|
+
);
|
|
238
|
+
scrollToHighlightedSuggestion();
|
|
239
|
+
break;
|
|
240
|
+
case "ArrowUp":
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
valueSuggestionIndex = Math.max(valueSuggestionIndex - 1, 0);
|
|
243
|
+
scrollToHighlightedSuggestion();
|
|
244
|
+
break;
|
|
245
|
+
case "Enter":
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
if (filteredValueSuggestions[valueSuggestionIndex]) {
|
|
248
|
+
selectValueSuggestion(filteredValueSuggestions[valueSuggestionIndex].value);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
case "Escape":
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
showValueSuggestions = false;
|
|
254
|
+
break;
|
|
255
|
+
case "Tab":
|
|
256
|
+
showValueSuggestions = false;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function scrollToHighlightedSuggestion() {
|
|
261
|
+
await tick();
|
|
262
|
+
const highlighted = valueSuggestionsRef?.querySelector(".suggestion-option.highlighted");
|
|
263
|
+
if (highlighted) {
|
|
264
|
+
highlighted.scrollIntoView({ block: "nearest" });
|
|
265
|
+
}
|
|
30
266
|
}
|
|
31
267
|
$: valueDisabled = condition.operator === "is_empty" || condition.operator === "is_not_empty";
|
|
32
268
|
</script>
|
|
33
269
|
|
|
34
270
|
<div class="filter-condition">
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
271
|
+
<div class="field-picker-wrapper" bind:this={dropdownRef}>
|
|
272
|
+
<button
|
|
273
|
+
class="field-picker-trigger"
|
|
274
|
+
class:has-value={condition.field}
|
|
275
|
+
on:click={openDropdown}
|
|
276
|
+
type="button"
|
|
277
|
+
>
|
|
278
|
+
<span class="field-picker-text">
|
|
279
|
+
{selectedColumnLabel || 'Select field...'}
|
|
280
|
+
</span>
|
|
281
|
+
<svg class="chevron-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
282
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
283
|
+
</svg>
|
|
284
|
+
</button>
|
|
285
|
+
|
|
286
|
+
{#if showDropdown}
|
|
287
|
+
<div class="field-dropdown">
|
|
288
|
+
<div class="field-search-wrapper">
|
|
289
|
+
<input
|
|
290
|
+
bind:this={searchInputRef}
|
|
291
|
+
type="text"
|
|
292
|
+
class="field-search"
|
|
293
|
+
placeholder="Search fields..."
|
|
294
|
+
bind:value={searchTerm}
|
|
295
|
+
on:keydown={handleSearchKeydown}
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
class="order-mode-btn"
|
|
299
|
+
on:click|stopPropagation={cycleOrderMode}
|
|
300
|
+
title="Change column order: {getOrderModeLabel(orderMode)}"
|
|
301
|
+
type="button"
|
|
302
|
+
>
|
|
303
|
+
{getOrderModeLabel(orderMode)}
|
|
304
|
+
</button>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="field-options">
|
|
308
|
+
{#each filteredColumns as { column, matchedIndices }, i}
|
|
309
|
+
{@const columnId = getColumnId(column)}
|
|
310
|
+
{@const columnLabel = getColumnLabel(column)}
|
|
311
|
+
{#if columnId}
|
|
312
|
+
<button
|
|
313
|
+
class="field-option"
|
|
314
|
+
class:highlighted={i === highlightedIndex}
|
|
315
|
+
class:selected={columnId === condition.field}
|
|
316
|
+
on:click={() => selectColumn(column)}
|
|
317
|
+
on:mouseenter={() => (highlightedIndex = i)}
|
|
318
|
+
type="button"
|
|
319
|
+
>
|
|
320
|
+
{#if matchedIndices.length > 0}
|
|
321
|
+
{#each highlightMatches(columnLabel, matchedIndices) as segment}
|
|
322
|
+
{#if segment.isMatch}
|
|
323
|
+
<mark class="match-highlight">{segment.text}</mark>
|
|
324
|
+
{:else}
|
|
325
|
+
{segment.text}
|
|
326
|
+
{/if}
|
|
327
|
+
{/each}
|
|
328
|
+
{:else}
|
|
329
|
+
{columnLabel}
|
|
330
|
+
{/if}
|
|
331
|
+
</button>
|
|
332
|
+
{/if}
|
|
333
|
+
{:else}
|
|
334
|
+
<div class="no-results">No matching fields</div>
|
|
335
|
+
{/each}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
{/if}
|
|
339
|
+
</div>
|
|
46
340
|
|
|
47
341
|
<select class="operator-select" value={condition.operator} on:change={handleOperatorChange}>
|
|
48
342
|
{#each operatorOptions as option}
|
|
@@ -50,16 +344,107 @@ $: valueDisabled = condition.operator === "is_empty" || condition.operator === "
|
|
|
50
344
|
{/each}
|
|
51
345
|
</select>
|
|
52
346
|
|
|
53
|
-
<input
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
347
|
+
<div class="value-input-wrapper" bind:this={valueSuggestionsRef}>
|
|
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>
|
|
371
|
+
{/each}
|
|
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}
|
|
444
|
+
{/if}
|
|
445
|
+
</div>
|
|
61
446
|
|
|
62
|
-
<button class="remove-btn" on:click={onRemove} title="Remove condition">
|
|
447
|
+
<button class="remove-btn" on:click={onRemove} title="Remove condition" type="button">
|
|
63
448
|
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
64
449
|
<path
|
|
65
450
|
stroke-linecap="round"
|
|
@@ -81,19 +466,165 @@ $: valueDisabled = condition.operator === "is_empty" || condition.operator === "
|
|
|
81
466
|
border-radius: 0.375rem;
|
|
82
467
|
}
|
|
83
468
|
|
|
84
|
-
.field-
|
|
85
|
-
|
|
86
|
-
|
|
469
|
+
.field-picker-wrapper {
|
|
470
|
+
position: relative;
|
|
471
|
+
flex: 1;
|
|
472
|
+
min-width: 150px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.field-picker-trigger {
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
justify-content: space-between;
|
|
479
|
+
width: 100%;
|
|
87
480
|
padding: 0.375rem 0.75rem;
|
|
88
481
|
font-size: 0.875rem;
|
|
482
|
+
text-align: left;
|
|
89
483
|
border: 1px solid #d1d5db;
|
|
90
484
|
border-radius: 0.375rem;
|
|
91
485
|
background: white;
|
|
486
|
+
cursor: pointer;
|
|
487
|
+
transition: border-color 0.15s;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.field-picker-trigger:hover {
|
|
491
|
+
border-color: #9ca3af;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.field-picker-trigger:focus {
|
|
495
|
+
outline: none;
|
|
496
|
+
border-color: #3b82f6;
|
|
497
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.field-picker-trigger:not(.has-value) .field-picker-text {
|
|
501
|
+
color: #9ca3af;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.field-picker-text {
|
|
505
|
+
flex: 1;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
text-overflow: ellipsis;
|
|
508
|
+
white-space: nowrap;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.chevron-icon {
|
|
512
|
+
width: 1rem;
|
|
513
|
+
height: 1rem;
|
|
514
|
+
color: #6b7280;
|
|
515
|
+
flex-shrink: 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.field-dropdown {
|
|
519
|
+
position: absolute;
|
|
520
|
+
top: calc(100% + 4px);
|
|
521
|
+
left: 0;
|
|
522
|
+
width: 100%;
|
|
523
|
+
min-width: 200px;
|
|
524
|
+
background: white;
|
|
525
|
+
border: 1px solid #d1d5db;
|
|
526
|
+
border-radius: 0.375rem;
|
|
527
|
+
box-shadow:
|
|
528
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
529
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
530
|
+
z-index: 50;
|
|
531
|
+
overflow: hidden;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.field-search-wrapper {
|
|
535
|
+
display: flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
gap: 0.25rem;
|
|
538
|
+
padding: 0.5rem;
|
|
539
|
+
border-bottom: 1px solid #e5e7eb;
|
|
92
540
|
}
|
|
93
541
|
|
|
94
|
-
.field-
|
|
542
|
+
.field-search {
|
|
95
543
|
flex: 1;
|
|
96
|
-
|
|
544
|
+
padding: 0.375rem 0.5rem;
|
|
545
|
+
font-size: 0.875rem;
|
|
546
|
+
border: 1px solid #d1d5db;
|
|
547
|
+
border-radius: 0.25rem;
|
|
548
|
+
outline: none;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.field-search:focus {
|
|
552
|
+
border-color: #3b82f6;
|
|
553
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.order-mode-btn {
|
|
557
|
+
flex-shrink: 0;
|
|
558
|
+
padding: 0.25rem 0.5rem;
|
|
559
|
+
font-size: 0.625rem;
|
|
560
|
+
font-weight: 600;
|
|
561
|
+
text-transform: uppercase;
|
|
562
|
+
letter-spacing: 0.025em;
|
|
563
|
+
color: #6b7280;
|
|
564
|
+
background: #e5e7eb;
|
|
565
|
+
border: none;
|
|
566
|
+
border-radius: 0.25rem;
|
|
567
|
+
cursor: pointer;
|
|
568
|
+
transition: all 0.15s;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.order-mode-btn:hover {
|
|
572
|
+
background: #d1d5db;
|
|
573
|
+
color: #374151;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.field-options {
|
|
577
|
+
max-height: 200px;
|
|
578
|
+
overflow-y: auto;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.field-option {
|
|
582
|
+
display: block;
|
|
583
|
+
width: 100%;
|
|
584
|
+
padding: 0.5rem 0.75rem;
|
|
585
|
+
font-size: 0.875rem;
|
|
586
|
+
text-align: left;
|
|
587
|
+
background: none;
|
|
588
|
+
border: none;
|
|
589
|
+
cursor: pointer;
|
|
590
|
+
transition: background-color 0.1s;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.field-option:hover,
|
|
594
|
+
.field-option.highlighted {
|
|
595
|
+
background: #f3f4f6;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.field-option.selected {
|
|
599
|
+
background: #eff6ff;
|
|
600
|
+
color: #1d4ed8;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.field-option.highlighted.selected {
|
|
604
|
+
background: #dbeafe;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.match-highlight {
|
|
608
|
+
background: #fef08a;
|
|
609
|
+
color: inherit;
|
|
610
|
+
padding: 0;
|
|
611
|
+
border-radius: 1px;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.no-results {
|
|
615
|
+
padding: 0.75rem;
|
|
616
|
+
text-align: center;
|
|
617
|
+
color: #6b7280;
|
|
618
|
+
font-size: 0.875rem;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.operator-select,
|
|
622
|
+
.value-input {
|
|
623
|
+
padding: 0.375rem 0.75rem;
|
|
624
|
+
font-size: 0.875rem;
|
|
625
|
+
border: 1px solid #d1d5db;
|
|
626
|
+
border-radius: 0.375rem;
|
|
627
|
+
background: white;
|
|
97
628
|
}
|
|
98
629
|
|
|
99
630
|
.operator-select {
|
|
@@ -101,6 +632,16 @@ $: valueDisabled = condition.operator === "is_empty" || condition.operator === "
|
|
|
101
632
|
min-width: 100px;
|
|
102
633
|
}
|
|
103
634
|
|
|
635
|
+
.value-input-wrapper {
|
|
636
|
+
position: relative;
|
|
637
|
+
flex: 1;
|
|
638
|
+
min-width: 120px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.value-input-wrapper .value-input {
|
|
642
|
+
width: 100%;
|
|
643
|
+
}
|
|
644
|
+
|
|
104
645
|
.value-input {
|
|
105
646
|
flex: 1;
|
|
106
647
|
min-width: 120px;
|
|
@@ -112,6 +653,62 @@ $: valueDisabled = condition.operator === "is_empty" || condition.operator === "
|
|
|
112
653
|
cursor: not-allowed;
|
|
113
654
|
}
|
|
114
655
|
|
|
656
|
+
.value-suggestions {
|
|
657
|
+
position: absolute;
|
|
658
|
+
top: calc(100% + 4px);
|
|
659
|
+
left: 0;
|
|
660
|
+
right: 0;
|
|
661
|
+
max-height: 200px;
|
|
662
|
+
overflow-y: auto;
|
|
663
|
+
background: white;
|
|
664
|
+
border: 1px solid #d1d5db;
|
|
665
|
+
border-radius: 0.375rem;
|
|
666
|
+
box-shadow:
|
|
667
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
668
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
669
|
+
z-index: 50;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.suggestion-option {
|
|
673
|
+
display: block;
|
|
674
|
+
width: 100%;
|
|
675
|
+
padding: 0.5rem 0.75rem;
|
|
676
|
+
font-size: 0.875rem;
|
|
677
|
+
text-align: left;
|
|
678
|
+
background: none;
|
|
679
|
+
border: none;
|
|
680
|
+
cursor: pointer;
|
|
681
|
+
transition: background-color 0.1s;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.suggestion-option:hover,
|
|
685
|
+
.suggestion-option.highlighted {
|
|
686
|
+
background: #f3f4f6;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.suggestions-overflow {
|
|
690
|
+
padding: 0.5rem 0.75rem;
|
|
691
|
+
font-size: 0.75rem;
|
|
692
|
+
color: #6b7280;
|
|
693
|
+
text-align: center;
|
|
694
|
+
border-top: 1px solid #e5e7eb;
|
|
695
|
+
background: #f9fafb;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.numeric-range-hint {
|
|
699
|
+
position: absolute;
|
|
700
|
+
top: calc(100% + 4px);
|
|
701
|
+
left: 0;
|
|
702
|
+
right: 0;
|
|
703
|
+
padding: 0.375rem 0.75rem;
|
|
704
|
+
font-size: 0.75rem;
|
|
705
|
+
color: #6b7280;
|
|
706
|
+
background: #f9fafb;
|
|
707
|
+
border: 1px solid #e5e7eb;
|
|
708
|
+
border-radius: 0.375rem;
|
|
709
|
+
text-align: center;
|
|
710
|
+
}
|
|
711
|
+
|
|
115
712
|
.remove-btn {
|
|
116
713
|
flex-shrink: 0;
|
|
117
714
|
padding: 0.375rem;
|