@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.
@@ -1,24 +1,209 @@
1
- <script>export let condition;
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
- const operatorOptions = [
6
- { value: "equals", label: "equals" },
7
- { value: "not_equals", label: "does not equal" },
8
- { value: "contains", label: "contains" },
9
- { value: "not_contains", label: "does not contain" },
10
- { value: "starts_with", label: "starts with" },
11
- { value: "ends_with", label: "ends with" },
12
- { value: "is_empty", label: "is empty" },
13
- { value: "is_not_empty", label: "is not empty" },
14
- { value: "greater_than", label: ">" },
15
- { value: "less_than", label: "<" },
16
- { value: "greater_or_equal", label: ">=" },
17
- { value: "less_or_equal", label: "<=" }
18
- ];
19
- function handleFieldChange(event) {
20
- const field = event.target.value;
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
- <select class="field-select" value={condition.field} on:change={handleFieldChange}>
36
- <option value="">Select field...</option>
37
- {#each columns as column}
38
- {@const columnId = column.accessorKey || column.id}
39
- {#if columnId}
40
- <option value={columnId}>
41
- {column.header || columnId}
42
- </option>
43
- {/if}
44
- {/each}
45
- </select>
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
- type="text"
55
- class="value-input"
56
- value={condition.value || ''}
57
- on:input={handleValueChange}
58
- disabled={valueDisabled}
59
- placeholder={valueDisabled ? 'N/A' : 'Enter value...'}
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-select,
85
- .operator-select,
86
- .value-input {
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-select {
542
+ .field-search {
95
543
  flex: 1;
96
- min-width: 120px;
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;