@joewinke/jatui 0.1.26 → 0.1.27
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.
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
displayValue,
|
|
27
27
|
colorFn,
|
|
28
28
|
footer,
|
|
29
|
+
variant = 'default',
|
|
30
|
+
size = 'md',
|
|
31
|
+
dropup = false,
|
|
32
|
+
appendToBody = false,
|
|
33
|
+
clearable = false,
|
|
34
|
+
triggerAttrs,
|
|
35
|
+
embedded = false,
|
|
29
36
|
onChange,
|
|
30
37
|
}: {
|
|
31
38
|
value: string;
|
|
@@ -35,13 +42,31 @@
|
|
|
35
42
|
displayValue?: string;
|
|
36
43
|
colorFn?: (value: string) => string | undefined;
|
|
37
44
|
footer?: Snippet;
|
|
45
|
+
/** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
|
|
46
|
+
variant?: 'default' | 'chip';
|
|
47
|
+
/** 'sm' renders a smaller trigger matching text-[11px] compact UIs */
|
|
48
|
+
size?: 'sm' | 'md';
|
|
49
|
+
/** Open the panel above the trigger instead of below */
|
|
50
|
+
dropup?: boolean;
|
|
51
|
+
/** Render panel with position:fixed using viewport coordinates — escapes table/overflow stacking contexts */
|
|
52
|
+
appendToBody?: boolean;
|
|
53
|
+
/** Show an × button on the trigger when a value is selected */
|
|
54
|
+
clearable?: boolean;
|
|
55
|
+
/** Extra attributes forwarded to the trigger button (e.g. data-rail-id) */
|
|
56
|
+
triggerAttrs?: Record<string, string>;
|
|
57
|
+
/** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
|
|
58
|
+
embedded?: boolean;
|
|
38
59
|
onChange: (value: string) => void;
|
|
39
60
|
} = $props();
|
|
40
61
|
|
|
41
62
|
let open = $state(false);
|
|
42
63
|
let searchQuery = $state('');
|
|
43
|
-
let
|
|
64
|
+
let highlightedIndex = $state(-1);
|
|
65
|
+
let panelStyle = $state('');
|
|
66
|
+
let searchInput: HTMLInputElement | undefined;
|
|
67
|
+
let optionListRef: HTMLUListElement | undefined;
|
|
44
68
|
let containerRef: HTMLDivElement | undefined;
|
|
69
|
+
let triggerRef: HTMLButtonElement | undefined;
|
|
45
70
|
|
|
46
71
|
// Find the currently selected option across all groups
|
|
47
72
|
const selectedOption = $derived.by(() => {
|
|
@@ -55,6 +80,13 @@
|
|
|
55
80
|
const triggerLabel = $derived(displayValue || selectedOption?.label || value || placeholder);
|
|
56
81
|
const triggerIcon = $derived(selectedOption?.icon || '');
|
|
57
82
|
const triggerImage = $derived(selectedOption?.image || '');
|
|
83
|
+
|
|
84
|
+
// Generate a deterministic hue from a string for initials avatars
|
|
85
|
+
function nameHue(s: string): number {
|
|
86
|
+
let h = 0;
|
|
87
|
+
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xffff;
|
|
88
|
+
return h % 360;
|
|
89
|
+
}
|
|
58
90
|
const activeColor = $derived(colorFn ? colorFn(value) : undefined);
|
|
59
91
|
|
|
60
92
|
const filteredGroups = $derived.by(() => {
|
|
@@ -70,12 +102,22 @@
|
|
|
70
102
|
return result;
|
|
71
103
|
});
|
|
72
104
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
for (
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
// Flat ordered list of visible options for keyboard navigation
|
|
106
|
+
const flatOptions = $derived.by(() => {
|
|
107
|
+
const opts: SearchDropdownOption[] = [];
|
|
108
|
+
for (const group of filteredGroups) {
|
|
109
|
+
for (const opt of group.options) opts.push(opt);
|
|
110
|
+
}
|
|
111
|
+
return opts;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// When search changes, reset highlight to first option (or to current value's position)
|
|
115
|
+
$effect(() => {
|
|
116
|
+
void filteredGroups;
|
|
117
|
+
if (!open && !embedded) return;
|
|
118
|
+
const idx = flatOptions.findIndex(o => o.value === value);
|
|
119
|
+
highlightedIndex = idx >= 0 ? idx : 0;
|
|
120
|
+
});
|
|
79
121
|
|
|
80
122
|
function select(optionValue: string) {
|
|
81
123
|
open = false;
|
|
@@ -83,6 +125,35 @@
|
|
|
83
125
|
onChange(optionValue);
|
|
84
126
|
}
|
|
85
127
|
|
|
128
|
+
function scrollHighlightedIntoView() {
|
|
129
|
+
tick().then(() => {
|
|
130
|
+
const el = optionListRef?.querySelector<HTMLElement>('[data-highlighted="true"]');
|
|
131
|
+
el?.scrollIntoView({ block: 'center' });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleSearchKeydown(e: KeyboardEvent) {
|
|
136
|
+
if (e.key === 'Escape') {
|
|
137
|
+
e.stopPropagation();
|
|
138
|
+
open = false;
|
|
139
|
+
searchQuery = '';
|
|
140
|
+
} else if (e.key === 'ArrowDown') {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
highlightedIndex = Math.min(highlightedIndex + 1, flatOptions.length - 1);
|
|
143
|
+
scrollHighlightedIntoView();
|
|
144
|
+
} else if (e.key === 'ArrowUp') {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
|
147
|
+
scrollHighlightedIntoView();
|
|
148
|
+
} else if (e.key === 'Enter') {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
if (highlightedIndex >= 0 && highlightedIndex < flatOptions.length) {
|
|
152
|
+
select(flatOptions[highlightedIndex].value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
86
157
|
function handleClickOutside(e: MouseEvent) {
|
|
87
158
|
if (containerRef && !containerRef.contains(e.target as Node)) {
|
|
88
159
|
open = false;
|
|
@@ -91,142 +162,194 @@
|
|
|
91
162
|
}
|
|
92
163
|
|
|
93
164
|
$effect(() => {
|
|
165
|
+
if (embedded) {
|
|
166
|
+
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
94
169
|
if (open) {
|
|
170
|
+
if (appendToBody && triggerRef) {
|
|
171
|
+
const rect = triggerRef.getBoundingClientRect();
|
|
172
|
+
const width = Math.max(rect.width, 192);
|
|
173
|
+
// Viewport-aware flip: if there isn't room below the trigger but there
|
|
174
|
+
// is above, anchor the panel to the trigger's top and grow upward. This
|
|
175
|
+
// keeps the panel on-screen for rows near the bottom of a scroll area
|
|
176
|
+
// (e.g. suggested-task rows in the completed-session overlay).
|
|
177
|
+
const estPanelHeight = 300;
|
|
178
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
179
|
+
const spaceAbove = rect.top;
|
|
180
|
+
const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
|
|
181
|
+
// Clamp left so a right-edge trigger doesn't push the panel off-screen.
|
|
182
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
|
|
183
|
+
const vertical = flipUp
|
|
184
|
+
? `bottom: ${window.innerHeight - rect.top + 4}px;`
|
|
185
|
+
: `top: ${rect.bottom + 4}px;`;
|
|
186
|
+
panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; margin-top: 0;`;
|
|
187
|
+
}
|
|
95
188
|
document.addEventListener('mousedown', handleClickOutside);
|
|
96
|
-
tick().then(() => searchInput?.focus());
|
|
189
|
+
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
97
190
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
98
191
|
}
|
|
99
192
|
});
|
|
193
|
+
|
|
194
|
+
export function openDropdown() {
|
|
195
|
+
open = true;
|
|
196
|
+
}
|
|
100
197
|
</script>
|
|
101
198
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class="sd-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
199
|
+
{#snippet panelBody()}
|
|
200
|
+
<!-- Search input -->
|
|
201
|
+
<div class="sd-search">
|
|
202
|
+
<div class="sd-search-inner">
|
|
203
|
+
<svg class="sd-search-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
204
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
205
|
+
</svg>
|
|
206
|
+
<input
|
|
207
|
+
bind:this={searchInput}
|
|
208
|
+
bind:value={searchQuery}
|
|
209
|
+
onkeydown={handleSearchKeydown}
|
|
210
|
+
type="text"
|
|
211
|
+
{placeholder}
|
|
212
|
+
class="sd-search-input"
|
|
213
|
+
autocomplete="off"
|
|
214
|
+
/>
|
|
215
|
+
{#if searchQuery}
|
|
216
|
+
<button type="button" aria-label="Clear search" onclick={() => { searchQuery = ''; searchInput?.focus(); }} class="sd-search-clear">
|
|
217
|
+
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
218
|
+
</button>
|
|
120
219
|
{/if}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Options list -->
|
|
224
|
+
<ul class="sd-list" bind:this={optionListRef}>
|
|
225
|
+
{#if filteredGroups.length > 0}
|
|
226
|
+
{#each filteredGroups as group, _gi}
|
|
227
|
+
<li class="sd-group-label">
|
|
228
|
+
<span>{group.label}</span>
|
|
229
|
+
</li>
|
|
230
|
+
{#each group.options as option}
|
|
231
|
+
{@const flatIdx = flatOptions.indexOf(option)}
|
|
232
|
+
<li>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
onclick={() => select(option.value)}
|
|
236
|
+
class="sd-option"
|
|
237
|
+
class:sd-option-selected={value === option.value}
|
|
238
|
+
class:sd-option-highlighted={flatIdx === highlightedIndex}
|
|
239
|
+
data-highlighted={flatIdx === highlightedIndex ? 'true' : undefined}
|
|
240
|
+
style={colorFn && colorFn(option.value)
|
|
241
|
+
? variant === 'chip'
|
|
242
|
+
? `--sd-color: ${colorFn(option.value)};`
|
|
243
|
+
: `border-left-color: ${colorFn(option.value)}; color: ${colorFn(option.value)};`
|
|
244
|
+
: ''}
|
|
245
|
+
>
|
|
246
|
+
{#if option.image}
|
|
247
|
+
<img src={option.image} alt="" class="sd-avatar" />
|
|
248
|
+
{:else if option.icon}
|
|
249
|
+
<span class="sd-option-icon">{option.icon}</span>
|
|
250
|
+
{:else if variant !== 'chip' && option.value}
|
|
251
|
+
<span class="sd-avatar sd-avatar-initials" style="background:oklch(0.55 0.15 {nameHue(option.label)})">
|
|
252
|
+
{option.label.charAt(0).toUpperCase()}
|
|
253
|
+
</span>
|
|
254
|
+
{/if}
|
|
255
|
+
{#if variant === 'chip' && colorFn && colorFn(option.value)}
|
|
256
|
+
<span class="sd-option-chip">{option.label}</span>
|
|
257
|
+
{:else}
|
|
258
|
+
<span class="truncate">{option.label}</span>
|
|
259
|
+
{/if}
|
|
260
|
+
{#if value === option.value}
|
|
261
|
+
<svg class="sd-check" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
|
|
262
|
+
{/if}
|
|
153
263
|
</button>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
264
|
+
</li>
|
|
265
|
+
{/each}
|
|
266
|
+
{/each}
|
|
267
|
+
{:else}
|
|
268
|
+
<li class="sd-empty">No matches for "{searchQuery}"</li>
|
|
269
|
+
{/if}
|
|
270
|
+
</ul>
|
|
271
|
+
{#if footer}
|
|
272
|
+
<div class="sd-footer">
|
|
273
|
+
{@render footer()}
|
|
274
|
+
</div>
|
|
275
|
+
{/if}
|
|
276
|
+
{/snippet}
|
|
157
277
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{/each}
|
|
190
|
-
{/each}
|
|
191
|
-
{:else}
|
|
192
|
-
<li class="sd-empty">
|
|
193
|
-
<svg class="sd-empty-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
|
194
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803 7.5 7.5 0 0015.803 15.803z" />
|
|
195
|
-
</svg>
|
|
196
|
-
No matches for "{searchQuery}"
|
|
197
|
-
</li>
|
|
278
|
+
{#if embedded}
|
|
279
|
+
<div class="sd-embedded" class:sd-panel-sm={size === 'sm'}>
|
|
280
|
+
{@render panelBody()}
|
|
281
|
+
</div>
|
|
282
|
+
{:else}
|
|
283
|
+
<div class="search-dropdown" class:sd-chip-wrap={variant === 'chip'} bind:this={containerRef}>
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
class="sd-trigger"
|
|
287
|
+
class:sd-disabled={disabled}
|
|
288
|
+
class:sd-chip={variant === 'chip'}
|
|
289
|
+
class:sd-sm={size === 'sm'}
|
|
290
|
+
style={activeColor
|
|
291
|
+
? variant === 'chip'
|
|
292
|
+
? `--sd-color: ${activeColor};`
|
|
293
|
+
: `border-left-color: ${activeColor}; border-left-width: 3px; color: ${activeColor};`
|
|
294
|
+
: ''}
|
|
295
|
+
bind:this={triggerRef}
|
|
296
|
+
onclick={() => { if (!disabled) open = !open; }}
|
|
297
|
+
{disabled}
|
|
298
|
+
{...triggerAttrs}
|
|
299
|
+
>
|
|
300
|
+
<span class="sd-trigger-label">
|
|
301
|
+
{#if variant !== 'chip' && triggerImage}
|
|
302
|
+
<img src={triggerImage} alt="" class="sd-avatar" />
|
|
303
|
+
{:else if triggerIcon}
|
|
304
|
+
<span class="sd-trigger-icon">{triggerIcon}</span>
|
|
305
|
+
{:else if variant !== 'chip' && selectedOption && selectedOption.value}
|
|
306
|
+
<span class="sd-avatar sd-avatar-initials" style="background:oklch(0.55 0.15 {nameHue(selectedOption.label)})">
|
|
307
|
+
{selectedOption.label.charAt(0).toUpperCase()}
|
|
308
|
+
</span>
|
|
198
309
|
{/if}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
310
|
+
<span class="truncate">{triggerLabel}</span>
|
|
311
|
+
</span>
|
|
312
|
+
{#if clearable && value !== ''}
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
class="sd-clear"
|
|
316
|
+
aria-label="Clear filter"
|
|
317
|
+
onclick={(e) => { e.stopPropagation(); onChange(''); }}
|
|
318
|
+
>
|
|
319
|
+
<svg class="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
320
|
+
</button>
|
|
204
321
|
{/if}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
</
|
|
322
|
+
<svg class="sd-chevron" class:sd-chevron-open={open} fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
323
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
324
|
+
</svg>
|
|
325
|
+
</button>
|
|
326
|
+
|
|
327
|
+
{#if open}
|
|
328
|
+
<div
|
|
329
|
+
class="sd-panel"
|
|
330
|
+
class:sd-panel-sm={size === 'sm'}
|
|
331
|
+
class:sd-panel-dropup={dropup && !appendToBody}
|
|
332
|
+
style={appendToBody ? panelStyle : undefined}
|
|
333
|
+
transition:slide={{ duration: 120 }}
|
|
334
|
+
>
|
|
335
|
+
{@render panelBody()}
|
|
336
|
+
</div>
|
|
337
|
+
{/if}
|
|
338
|
+
</div>
|
|
339
|
+
{/if}
|
|
208
340
|
|
|
209
341
|
<style>
|
|
210
342
|
.search-dropdown {
|
|
211
343
|
position: relative;
|
|
212
|
-
--sd-bg: var(--sd-color-bg, var(--color-base-100, Canvas));
|
|
213
|
-
--sd-bg-hover: var(--sd-color-bg-hover, var(--color-base-200, color-mix(in oklch, Canvas 90%, CanvasText)));
|
|
214
|
-
--sd-bg-selected: var(--sd-color-bg-selected, var(--color-base-300, color-mix(in oklch, Canvas 80%, CanvasText)));
|
|
215
|
-
--sd-border: var(--sd-color-border, color-mix(in oklch, var(--color-base-content, CanvasText) 20%, transparent));
|
|
216
|
-
--sd-text: var(--sd-color-text, var(--color-base-content, CanvasText));
|
|
217
|
-
--sd-text-muted: var(--sd-color-text-muted, color-mix(in oklch, var(--color-base-content, CanvasText) 50%, transparent));
|
|
218
|
-
--sd-text-label: var(--sd-color-text-label, color-mix(in oklch, var(--color-base-content, CanvasText) 45%, transparent));
|
|
219
|
-
--sd-accent: var(--sd-color-accent, var(--color-primary, LinkText));
|
|
220
|
-
--sd-success: var(--sd-color-success, var(--color-success, green));
|
|
221
344
|
}
|
|
222
345
|
|
|
223
346
|
/* Trigger button */
|
|
224
347
|
.sd-trigger {
|
|
225
348
|
width: 100%;
|
|
226
349
|
padding: 0.25rem 0.5rem;
|
|
227
|
-
border-radius:
|
|
228
|
-
font-family:
|
|
229
|
-
font-size:
|
|
350
|
+
border-radius: 0.5rem;
|
|
351
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
352
|
+
font-size: 0.8125rem;
|
|
230
353
|
text-align: left;
|
|
231
354
|
display: flex;
|
|
232
355
|
align-items: center;
|
|
@@ -235,28 +358,88 @@
|
|
|
235
358
|
transition: background 0.15s, border-color 0.15s;
|
|
236
359
|
min-height: 2rem;
|
|
237
360
|
cursor: pointer;
|
|
238
|
-
background:
|
|
239
|
-
border: 1px solid
|
|
240
|
-
color:
|
|
361
|
+
background: oklch(0.16 0.01 250);
|
|
362
|
+
border: 1px solid oklch(0.25 0.02 250);
|
|
363
|
+
color: oklch(0.85 0.02 250);
|
|
241
364
|
}
|
|
242
365
|
.sd-trigger:hover:not(:disabled) {
|
|
243
|
-
background:
|
|
366
|
+
background: oklch(0.18 0.01 250);
|
|
367
|
+
border-color: oklch(0.30 0.02 250);
|
|
244
368
|
}
|
|
245
369
|
.sd-trigger:disabled, .sd-disabled {
|
|
246
370
|
opacity: 0.5;
|
|
247
371
|
cursor: not-allowed;
|
|
248
372
|
}
|
|
249
373
|
|
|
374
|
+
/* Small size variant — matches DaisyUI select-sm / input-sm */
|
|
375
|
+
.sd-sm {
|
|
376
|
+
font-size: 0.75rem;
|
|
377
|
+
min-height: 2rem;
|
|
378
|
+
padding: 0 0.75rem;
|
|
379
|
+
border-radius: var(--rounded-btn, 0.5rem);
|
|
380
|
+
background: var(--b2, oklch(0.16 0.01 250));
|
|
381
|
+
border: 1px solid oklch(from var(--bc, oklch(0.85 0.02 250)) l c h / 0.3);
|
|
382
|
+
color: var(--bc, oklch(0.85 0.02 250));
|
|
383
|
+
}
|
|
384
|
+
.sd-sm .sd-chevron {
|
|
385
|
+
width: 0.625rem;
|
|
386
|
+
height: 0.625rem;
|
|
387
|
+
}
|
|
388
|
+
.sd-panel-sm {
|
|
389
|
+
font-size: 0.75rem;
|
|
390
|
+
}
|
|
391
|
+
.sd-panel-sm .sd-search {
|
|
392
|
+
padding: 0.25rem 0.375rem;
|
|
393
|
+
}
|
|
394
|
+
.sd-panel-sm .sd-search-input {
|
|
395
|
+
font-size: 0.6875rem;
|
|
396
|
+
}
|
|
397
|
+
.sd-panel-sm .sd-option {
|
|
398
|
+
padding: 0.375rem 0.625rem;
|
|
399
|
+
font-size: 0.75rem;
|
|
400
|
+
}
|
|
401
|
+
.sd-panel-sm .sd-group-label {
|
|
402
|
+
font-size: 0.625rem;
|
|
403
|
+
padding: 0.25rem 0.625rem 0.125rem;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Chip variant — TopBar fav-chip style: full border, color-tinted background */
|
|
407
|
+
.sd-chip-wrap {
|
|
408
|
+
display: inline-flex;
|
|
409
|
+
}
|
|
410
|
+
/* Right-align the panel for chip variant so it doesn't spill past the container edge */
|
|
411
|
+
.sd-chip-wrap .sd-panel {
|
|
412
|
+
right: 0;
|
|
413
|
+
left: auto;
|
|
414
|
+
}
|
|
415
|
+
.sd-chip {
|
|
416
|
+
width: auto;
|
|
417
|
+
max-width: 10rem;
|
|
418
|
+
min-height: 1.5rem;
|
|
419
|
+
padding: 0.15rem 0.4rem;
|
|
420
|
+
border-radius: 0.375rem;
|
|
421
|
+
font-size: 0.6875rem;
|
|
422
|
+
font-weight: 600;
|
|
423
|
+
text-transform: uppercase;
|
|
424
|
+
letter-spacing: 0.025em;
|
|
425
|
+
background: transparent;
|
|
426
|
+
border: 1px solid color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 35%, transparent);
|
|
427
|
+
color: color-mix(in oklch, var(--sd-color, oklch(0.7 0 0)) 85%, oklch(0.80 0 0));
|
|
428
|
+
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s, color 0.15s;
|
|
429
|
+
}
|
|
430
|
+
.sd-chip:hover:not(:disabled) {
|
|
431
|
+
background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
|
|
432
|
+
border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
|
|
433
|
+
color: var(--sd-color, oklch(0.80 0 0));
|
|
434
|
+
box-shadow: 0 0 6px color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
|
|
435
|
+
}
|
|
436
|
+
|
|
250
437
|
.sd-trigger-label {
|
|
251
438
|
display: flex;
|
|
252
439
|
align-items: center;
|
|
253
440
|
gap: 0.375rem;
|
|
254
441
|
min-width: 0;
|
|
255
442
|
}
|
|
256
|
-
.sd-trigger-icon {
|
|
257
|
-
flex-shrink: 0;
|
|
258
|
-
font-size: 0.75rem;
|
|
259
|
-
}
|
|
260
443
|
.sd-avatar {
|
|
261
444
|
width: 1.25rem;
|
|
262
445
|
height: 1.25rem;
|
|
@@ -273,62 +456,110 @@
|
|
|
273
456
|
color: oklch(1 0 0);
|
|
274
457
|
line-height: 1;
|
|
275
458
|
}
|
|
459
|
+
.sd-trigger-icon {
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
font-size: 0.75rem;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.sd-clear {
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
justify-content: center;
|
|
468
|
+
flex-shrink: 0;
|
|
469
|
+
padding: 0.1rem;
|
|
470
|
+
border-radius: 0.25rem;
|
|
471
|
+
background: none;
|
|
472
|
+
border: none;
|
|
473
|
+
cursor: pointer;
|
|
474
|
+
color: oklch(0.50 0.02 250);
|
|
475
|
+
transition: color 0.1s, background 0.1s;
|
|
476
|
+
}
|
|
477
|
+
.sd-clear:hover {
|
|
478
|
+
color: oklch(0.75 0.15 30);
|
|
479
|
+
background: oklch(0.75 0.15 30 / 0.12);
|
|
480
|
+
}
|
|
276
481
|
|
|
277
482
|
.sd-chevron {
|
|
278
483
|
width: 0.75rem;
|
|
279
484
|
height: 0.75rem;
|
|
280
485
|
flex-shrink: 0;
|
|
281
486
|
transition: transform 0.15s;
|
|
282
|
-
color:
|
|
487
|
+
color: oklch(0.50 0.02 250);
|
|
283
488
|
}
|
|
284
489
|
.sd-chevron-open {
|
|
285
490
|
transform: rotate(180deg);
|
|
286
491
|
}
|
|
287
492
|
|
|
493
|
+
/* Embedded (inline) mode — panel content with no trigger or positioning */
|
|
494
|
+
.sd-embedded {
|
|
495
|
+
width: 100%;
|
|
496
|
+
}
|
|
497
|
+
.sd-embedded .sd-search {
|
|
498
|
+
background: transparent;
|
|
499
|
+
border-bottom: 1px solid oklch(0.22 0.02 250);
|
|
500
|
+
}
|
|
501
|
+
.sd-embedded .sd-list {
|
|
502
|
+
max-height: none;
|
|
503
|
+
}
|
|
504
|
+
|
|
288
505
|
/* Dropdown panel */
|
|
289
506
|
.sd-panel {
|
|
290
507
|
position: absolute;
|
|
291
|
-
z-index:
|
|
508
|
+
z-index: 9999;
|
|
509
|
+
top: 100%;
|
|
292
510
|
margin-top: 0.25rem;
|
|
293
|
-
width:
|
|
294
|
-
min-width:
|
|
295
|
-
border-radius:
|
|
511
|
+
width: max-content;
|
|
512
|
+
min-width: max(100%, 14rem);
|
|
513
|
+
border-radius: 0.5rem;
|
|
296
514
|
overflow: hidden;
|
|
297
|
-
box-shadow: 0 4px 24px oklch(0 0 0 / 0.
|
|
298
|
-
background:
|
|
299
|
-
border: 1px solid
|
|
515
|
+
box-shadow: 0 4px 24px oklch(0 0 0 / 0.4);
|
|
516
|
+
background: oklch(0.16 0.01 250);
|
|
517
|
+
border: 1px solid oklch(0.25 0.02 250);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.sd-panel-dropup {
|
|
521
|
+
top: auto;
|
|
522
|
+
bottom: 100%;
|
|
523
|
+
margin-top: 0;
|
|
524
|
+
margin-bottom: 0.25rem;
|
|
525
|
+
box-shadow: 0 -4px 24px oklch(0 0 0 / 0.4);
|
|
300
526
|
}
|
|
301
527
|
|
|
302
528
|
/* Search section */
|
|
303
529
|
.sd-search {
|
|
304
|
-
padding: 0.375rem 0.
|
|
305
|
-
border-bottom: 1px solid
|
|
530
|
+
padding: 0.375rem 0.5rem;
|
|
531
|
+
border-bottom: 1px solid oklch(0.25 0.02 250);
|
|
532
|
+
background: oklch(0.13 0.01 250);
|
|
306
533
|
}
|
|
307
534
|
.sd-search-inner {
|
|
308
535
|
display: flex;
|
|
309
536
|
align-items: center;
|
|
310
537
|
gap: 0.375rem;
|
|
538
|
+
background: oklch(0.19 0.01 250);
|
|
539
|
+
border: 1px solid oklch(0.28 0.02 250);
|
|
540
|
+
border-radius: 0.25rem;
|
|
541
|
+
padding: 0.2rem 0.4rem;
|
|
311
542
|
}
|
|
312
543
|
.sd-search-icon {
|
|
313
544
|
width: 0.75rem;
|
|
314
545
|
height: 0.75rem;
|
|
315
546
|
flex-shrink: 0;
|
|
316
|
-
color:
|
|
547
|
+
color: oklch(0.60 0.04 250);
|
|
317
548
|
}
|
|
318
549
|
.sd-search-input {
|
|
319
550
|
width: 100%;
|
|
320
551
|
background: transparent;
|
|
321
|
-
font-size:
|
|
322
|
-
font-family:
|
|
323
|
-
color:
|
|
552
|
+
font-size: 0.6875rem;
|
|
553
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
554
|
+
color: oklch(0.85 0.02 250);
|
|
324
555
|
border: none;
|
|
325
556
|
outline: none;
|
|
326
557
|
}
|
|
327
558
|
.sd-search-input::placeholder {
|
|
328
|
-
color:
|
|
559
|
+
color: oklch(0.52 0.03 250);
|
|
329
560
|
}
|
|
330
561
|
.sd-search-clear {
|
|
331
|
-
color:
|
|
562
|
+
color: oklch(0.50 0.02 250);
|
|
332
563
|
background: none;
|
|
333
564
|
border: none;
|
|
334
565
|
cursor: pointer;
|
|
@@ -336,7 +567,7 @@
|
|
|
336
567
|
display: flex;
|
|
337
568
|
}
|
|
338
569
|
.sd-search-clear:hover {
|
|
339
|
-
|
|
570
|
+
color: oklch(0.75 0.15 30);
|
|
340
571
|
}
|
|
341
572
|
|
|
342
573
|
/* Options list */
|
|
@@ -352,11 +583,12 @@
|
|
|
352
583
|
padding: 0.375rem 0.75rem 0.125rem;
|
|
353
584
|
}
|
|
354
585
|
.sd-group-label span {
|
|
355
|
-
font-size: 0.
|
|
356
|
-
font-family:
|
|
357
|
-
font-weight:
|
|
358
|
-
|
|
359
|
-
|
|
586
|
+
font-size: 0.5625rem;
|
|
587
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
588
|
+
font-weight: 600;
|
|
589
|
+
text-transform: uppercase;
|
|
590
|
+
letter-spacing: 0.05em;
|
|
591
|
+
color: oklch(0.50 0.10 250);
|
|
360
592
|
}
|
|
361
593
|
|
|
362
594
|
.sd-option {
|
|
@@ -366,20 +598,63 @@
|
|
|
366
598
|
align-items: center;
|
|
367
599
|
gap: 0.5rem;
|
|
368
600
|
text-align: left;
|
|
369
|
-
font-size:
|
|
370
|
-
font-family:
|
|
601
|
+
font-size: 0.6875rem;
|
|
602
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
371
603
|
transition: background 0.1s;
|
|
372
604
|
cursor: pointer;
|
|
373
605
|
background: transparent;
|
|
374
606
|
border: none;
|
|
375
|
-
|
|
607
|
+
border-left: 2px solid transparent;
|
|
608
|
+
color: oklch(0.80 0.02 250);
|
|
376
609
|
}
|
|
377
610
|
.sd-option:hover {
|
|
378
|
-
background:
|
|
611
|
+
background: oklch(0.19 0.01 250);
|
|
379
612
|
}
|
|
380
613
|
.sd-option-selected {
|
|
381
|
-
background:
|
|
614
|
+
background: oklch(0.20 0.02 250);
|
|
615
|
+
border-left-color: oklch(0.65 0.15 250);
|
|
616
|
+
}
|
|
617
|
+
.sd-option-highlighted {
|
|
618
|
+
background: oklch(0.22 0.02 250);
|
|
619
|
+
outline: 1px solid oklch(0.45 0.10 250 / 0.5);
|
|
620
|
+
outline-offset: -1px;
|
|
621
|
+
}
|
|
622
|
+
.sd-option-highlighted.sd-option-selected {
|
|
623
|
+
background: oklch(0.24 0.03 250);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* Chip variant — options render as inline chip pills */
|
|
627
|
+
.sd-chip-wrap .sd-option {
|
|
628
|
+
border-left: none;
|
|
629
|
+
padding: 0.25rem 0.625rem;
|
|
630
|
+
}
|
|
631
|
+
.sd-chip-wrap .sd-option-selected {
|
|
632
|
+
background: oklch(0.19 0.01 250);
|
|
633
|
+
border-left: none;
|
|
634
|
+
}
|
|
635
|
+
.sd-option-chip {
|
|
636
|
+
display: inline-flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
padding: 0.1rem 0.45rem;
|
|
639
|
+
border-radius: 0.3rem;
|
|
640
|
+
font-size: 0.6875rem;
|
|
382
641
|
font-weight: 600;
|
|
642
|
+
text-transform: uppercase;
|
|
643
|
+
letter-spacing: 0.025em;
|
|
644
|
+
border: 1px solid color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 35%, transparent);
|
|
645
|
+
color: color-mix(in oklch, var(--sd-color, oklch(0.7 0 0)) 85%, oklch(0.80 0 0));
|
|
646
|
+
background: transparent;
|
|
647
|
+
transition: background 0.1s, border-color 0.1s, color 0.1s;
|
|
648
|
+
}
|
|
649
|
+
.sd-chip-wrap .sd-option:hover .sd-option-chip {
|
|
650
|
+
background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 12%, transparent);
|
|
651
|
+
border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
|
|
652
|
+
color: var(--sd-color, oklch(0.80 0 0));
|
|
653
|
+
}
|
|
654
|
+
.sd-chip-wrap .sd-option-selected .sd-option-chip {
|
|
655
|
+
background: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 15%, transparent);
|
|
656
|
+
border-color: color-mix(in oklch, var(--sd-color, oklch(0.5 0 0)) 50%, transparent);
|
|
657
|
+
color: var(--sd-color, oklch(0.80 0 0));
|
|
383
658
|
}
|
|
384
659
|
|
|
385
660
|
.sd-option-icon {
|
|
@@ -394,28 +669,18 @@
|
|
|
394
669
|
height: 0.75rem;
|
|
395
670
|
flex-shrink: 0;
|
|
396
671
|
margin-left: auto;
|
|
397
|
-
color:
|
|
672
|
+
color: oklch(0.70 0.15 145);
|
|
398
673
|
}
|
|
399
674
|
|
|
400
675
|
.sd-footer {
|
|
401
|
-
border-top: 1px solid
|
|
676
|
+
border-top: 1px solid oklch(0.22 0.02 250);
|
|
402
677
|
}
|
|
403
678
|
|
|
404
679
|
.sd-empty {
|
|
405
|
-
padding:
|
|
680
|
+
padding: 0.75rem;
|
|
406
681
|
text-align: center;
|
|
407
|
-
font-size:
|
|
408
|
-
font-family:
|
|
409
|
-
color:
|
|
410
|
-
display: flex;
|
|
411
|
-
flex-direction: column;
|
|
412
|
-
align-items: center;
|
|
413
|
-
gap: 0.375rem;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.sd-empty-icon {
|
|
417
|
-
width: 1.25rem;
|
|
418
|
-
height: 1.25rem;
|
|
419
|
-
opacity: 0.35;
|
|
682
|
+
font-size: 0.625rem;
|
|
683
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
684
|
+
color: oklch(0.45 0.02 250);
|
|
420
685
|
}
|
|
421
686
|
</style>
|
|
@@ -17,8 +17,24 @@ type $$ComponentProps = {
|
|
|
17
17
|
displayValue?: string;
|
|
18
18
|
colorFn?: (value: string) => string | undefined;
|
|
19
19
|
footer?: Snippet;
|
|
20
|
+
/** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
|
|
21
|
+
variant?: 'default' | 'chip';
|
|
22
|
+
/** 'sm' renders a smaller trigger matching text-[11px] compact UIs */
|
|
23
|
+
size?: 'sm' | 'md';
|
|
24
|
+
/** Open the panel above the trigger instead of below */
|
|
25
|
+
dropup?: boolean;
|
|
26
|
+
/** Render panel with position:fixed using viewport coordinates — escapes table/overflow stacking contexts */
|
|
27
|
+
appendToBody?: boolean;
|
|
28
|
+
/** Show an × button on the trigger when a value is selected */
|
|
29
|
+
clearable?: boolean;
|
|
30
|
+
/** Extra attributes forwarded to the trigger button (e.g. data-rail-id) */
|
|
31
|
+
triggerAttrs?: Record<string, string>;
|
|
32
|
+
/** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
|
|
33
|
+
embedded?: boolean;
|
|
20
34
|
onChange: (value: string) => void;
|
|
21
35
|
};
|
|
22
|
-
declare const SearchDropdown: import("svelte").Component<$$ComponentProps, {
|
|
36
|
+
declare const SearchDropdown: import("svelte").Component<$$ComponentProps, {
|
|
37
|
+
openDropdown: () => void;
|
|
38
|
+
}, "">;
|
|
23
39
|
type SearchDropdown = ReturnType<typeof SearchDropdown>;
|
|
24
40
|
export default SearchDropdown;
|