@joewinke/jatui 0.6.2 → 0.6.3
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.
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
triggerAttrs,
|
|
36
36
|
embedded = false,
|
|
37
37
|
onChange,
|
|
38
|
+
onCreate,
|
|
39
|
+
createLabel = (q: string) => `Create "${q}"`,
|
|
38
40
|
}: {
|
|
39
41
|
value: string;
|
|
40
42
|
groups: SearchDropdownGroup[];
|
|
@@ -60,6 +62,16 @@
|
|
|
60
62
|
/** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
|
|
61
63
|
embedded?: boolean;
|
|
62
64
|
onChange: (value: string) => void;
|
|
65
|
+
/**
|
|
66
|
+
* When set, an always-present "create" row is rendered as the last option
|
|
67
|
+
* whenever the search box is non-empty — it survives a zero-match filter (so
|
|
68
|
+
* a brand-new value the list doesn't contain is still actionable) and is
|
|
69
|
+
* called with the trimmed query. Enables the "add something not in the list"
|
|
70
|
+
* pattern (e.g. create a reviewer by typed name).
|
|
71
|
+
*/
|
|
72
|
+
onCreate?: (query: string) => void;
|
|
73
|
+
/** Label for the create row; receives the trimmed query. */
|
|
74
|
+
createLabel?: (query: string) => string;
|
|
63
75
|
} = $props();
|
|
64
76
|
|
|
65
77
|
let open = $state(false);
|
|
@@ -114,6 +126,14 @@
|
|
|
114
126
|
return opts;
|
|
115
127
|
});
|
|
116
128
|
|
|
129
|
+
// The always-present "create" row shows whenever onCreate is wired and the
|
|
130
|
+
// search box has content — independent of whether anything matched, so a
|
|
131
|
+
// value the list doesn't contain is still actionable. It sits at keyboard
|
|
132
|
+
// index === flatOptions.length (one past the last real option).
|
|
133
|
+
const trimmedQuery = $derived(searchQuery.trim());
|
|
134
|
+
const canCreate = $derived(!!onCreate && trimmedQuery.length > 0);
|
|
135
|
+
const maxIndex = $derived(flatOptions.length - 1 + (canCreate ? 1 : 0));
|
|
136
|
+
|
|
117
137
|
// When search changes, reset highlight to first option (or to current value's position)
|
|
118
138
|
$effect(() => {
|
|
119
139
|
void filteredGroups;
|
|
@@ -128,6 +148,14 @@
|
|
|
128
148
|
onChange(optionValue);
|
|
129
149
|
}
|
|
130
150
|
|
|
151
|
+
function doCreate() {
|
|
152
|
+
const q = trimmedQuery;
|
|
153
|
+
if (!onCreate || !q) return;
|
|
154
|
+
open = false;
|
|
155
|
+
searchQuery = '';
|
|
156
|
+
onCreate(q);
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
function scrollHighlightedIntoView() {
|
|
132
160
|
tick().then(() => {
|
|
133
161
|
const el = optionListRef?.querySelector<HTMLElement>('[data-highlighted="true"]');
|
|
@@ -142,7 +170,7 @@
|
|
|
142
170
|
searchQuery = '';
|
|
143
171
|
} else if (e.key === 'ArrowDown') {
|
|
144
172
|
e.preventDefault();
|
|
145
|
-
highlightedIndex = Math.min(highlightedIndex + 1,
|
|
173
|
+
highlightedIndex = Math.min(highlightedIndex + 1, maxIndex);
|
|
146
174
|
scrollHighlightedIntoView();
|
|
147
175
|
} else if (e.key === 'ArrowUp') {
|
|
148
176
|
e.preventDefault();
|
|
@@ -153,6 +181,8 @@
|
|
|
153
181
|
e.stopPropagation();
|
|
154
182
|
if (highlightedIndex >= 0 && highlightedIndex < flatOptions.length) {
|
|
155
183
|
select(flatOptions[highlightedIndex].value);
|
|
184
|
+
} else if (canCreate && highlightedIndex === flatOptions.length) {
|
|
185
|
+
doCreate();
|
|
156
186
|
}
|
|
157
187
|
}
|
|
158
188
|
}
|
|
@@ -281,9 +311,24 @@
|
|
|
281
311
|
</li>
|
|
282
312
|
{/each}
|
|
283
313
|
{/each}
|
|
284
|
-
{:else}
|
|
314
|
+
{:else if !canCreate}
|
|
285
315
|
<li class="sd-empty">No matches for "{searchQuery}"</li>
|
|
286
316
|
{/if}
|
|
317
|
+
{#if canCreate}
|
|
318
|
+
{@const createIdx = flatOptions.length}
|
|
319
|
+
<li>
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
onclick={doCreate}
|
|
323
|
+
class="sd-option sd-option-create"
|
|
324
|
+
class:sd-option-highlighted={highlightedIndex === createIdx}
|
|
325
|
+
data-highlighted={highlightedIndex === createIdx ? 'true' : undefined}
|
|
326
|
+
>
|
|
327
|
+
<span class="sd-option-icon">+</span>
|
|
328
|
+
<span class="truncate">{createLabel(trimmedQuery)}</span>
|
|
329
|
+
</button>
|
|
330
|
+
</li>
|
|
331
|
+
{/if}
|
|
287
332
|
</ul>
|
|
288
333
|
{#if footer}
|
|
289
334
|
<div class="sd-footer">
|
|
@@ -691,6 +736,16 @@
|
|
|
691
736
|
color: var(--color-success, oklch(0.70 0.15 145));
|
|
692
737
|
}
|
|
693
738
|
|
|
739
|
+
/* Create row — the always-present "add something not in the list" action */
|
|
740
|
+
.sd-option-create {
|
|
741
|
+
border-top: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.22 0.02 250)) 20%, transparent);
|
|
742
|
+
color: color-mix(in oklch, var(--color-primary, oklch(0.65 0.15 250)) 90%, var(--color-base-content, oklch(0.80 0 0)));
|
|
743
|
+
font-weight: 600;
|
|
744
|
+
}
|
|
745
|
+
.sd-option-create .sd-option-icon {
|
|
746
|
+
color: var(--color-primary, oklch(0.65 0.15 250));
|
|
747
|
+
}
|
|
748
|
+
|
|
694
749
|
.sd-footer {
|
|
695
750
|
border-top: 1px solid color-mix(in oklch, var(--color-base-content, oklch(0.22 0.02 250)) 25%, transparent);
|
|
696
751
|
}
|
|
@@ -34,6 +34,16 @@ type $$ComponentProps = {
|
|
|
34
34
|
/** Render inline without a trigger button — panel content is always visible, no dropdown behavior */
|
|
35
35
|
embedded?: boolean;
|
|
36
36
|
onChange: (value: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* When set, an always-present "create" row is rendered as the last option
|
|
39
|
+
* whenever the search box is non-empty — it survives a zero-match filter (so
|
|
40
|
+
* a brand-new value the list doesn't contain is still actionable) and is
|
|
41
|
+
* called with the trimmed query. Enables the "add something not in the list"
|
|
42
|
+
* pattern (e.g. create a reviewer by typed name).
|
|
43
|
+
*/
|
|
44
|
+
onCreate?: (query: string) => void;
|
|
45
|
+
/** Label for the create row; receives the trimmed query. */
|
|
46
|
+
createLabel?: (query: string) => string;
|
|
37
47
|
};
|
|
38
48
|
declare const SearchDropdown: import("svelte").Component<$$ComponentProps, {
|
|
39
49
|
openDropdown: () => void;
|