ketekny-ui-kit 1.0.19 → 1.0.21
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/package.json +1 -1
- package/src/ui/kSelect.vue +75 -22
package/package.json
CHANGED
package/src/ui/kSelect.vue
CHANGED
|
@@ -37,35 +37,39 @@
|
|
|
37
37
|
:side-offset="6"
|
|
38
38
|
class="select-content z-[9999] bg-white border border-gray-200 rounded-lg shadow-[0_10px_38px_-10px_rgba(22,23,24,0.35),0_10px_20px_-15px_rgba(22,23,24,0.2)] overflow-hidden"
|
|
39
39
|
>
|
|
40
|
-
<div v-if="searchable" class="sticky top-0 z-10 p-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
<div v-if="searchable" class="sticky top-0 z-10 p-2 border-b border-gray-100 bg-white">
|
|
41
|
+
<div class="relative">
|
|
42
|
+
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
|
|
43
|
+
<input
|
|
44
|
+
ref="searchInput"
|
|
45
|
+
type="text"
|
|
46
|
+
v-model="searchQuery"
|
|
47
|
+
placeholder="Αναζήτηση..."
|
|
48
|
+
class="w-full pl-9 pr-3 py-2 text-base border border-gray-200 rounded-lg focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-colors placeholder-gray-400"
|
|
49
|
+
role="searchbox"
|
|
50
|
+
aria-label="Search options"
|
|
51
|
+
@keydown="handleSearchKeydown"
|
|
52
|
+
@keyup.stop
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
52
55
|
</div>
|
|
53
56
|
|
|
54
57
|
<SelectViewport
|
|
55
58
|
ref="viewportRef"
|
|
56
|
-
class="p-1 overflow-y-
|
|
59
|
+
class="select-viewport p-1 overflow-y-scroll"
|
|
57
60
|
:style="{ maxHeight: dropdownHeight }"
|
|
61
|
+
@keydown="handleViewportKeydown"
|
|
58
62
|
>
|
|
59
63
|
<SelectItem
|
|
60
64
|
v-for="(option, index) in filteredOptions"
|
|
61
|
-
:key="
|
|
65
|
+
:key="option[optionValue]"
|
|
62
66
|
:value="option[optionValue]"
|
|
63
|
-
class="select-item relative flex items-center gap-1.5 rounded-[4px] py-
|
|
67
|
+
class="select-item relative flex items-center gap-1.5 rounded-[4px] py-2.5 pl-8 pr-3 text-base cursor-pointer outline-none select-none text-slate-700
|
|
64
68
|
data-[highlighted]:bg-primary data-[highlighted]:text-white
|
|
65
|
-
data-[state=checked]:text-primary
|
|
66
|
-
data-[state=checked]:data-[highlighted]:text-white"
|
|
69
|
+
data-[state=checked]:bg-primary/5 data-[state=checked]:text-primary
|
|
70
|
+
data-[state=checked]:data-[highlighted]:bg-primary data-[state=checked]:data-[highlighted]:text-white"
|
|
67
71
|
>
|
|
68
|
-
<span class="absolute left-
|
|
72
|
+
<span class="absolute left-2 flex items-center justify-center w-4 h-4 shrink-0">
|
|
69
73
|
<SelectItemIndicator>
|
|
70
74
|
<Check class="w-3.5 h-3.5" />
|
|
71
75
|
</SelectItemIndicator>
|
|
@@ -73,8 +77,9 @@
|
|
|
73
77
|
<SelectItemText class="truncate">{{ option[optionLabel] }}</SelectItemText>
|
|
74
78
|
</SelectItem>
|
|
75
79
|
|
|
76
|
-
<div v-if="filteredOptions.length === 0" class="px-3 py-
|
|
77
|
-
|
|
80
|
+
<div v-if="filteredOptions.length === 0" class="flex flex-col items-center gap-1.5 px-3 py-4 text-base text-gray-400 text-center">
|
|
81
|
+
<SearchX class="w-5 h-5 text-gray-300" />
|
|
82
|
+
<span>Δεν βρέθηκαν επιλογές</span>
|
|
78
83
|
</div>
|
|
79
84
|
</SelectViewport>
|
|
80
85
|
</SelectContent>
|
|
@@ -104,7 +109,7 @@ import {
|
|
|
104
109
|
SelectValue,
|
|
105
110
|
SelectViewport,
|
|
106
111
|
} from "reka-ui";
|
|
107
|
-
import { X, ChevronDown, Check } from "lucide-vue-next";
|
|
112
|
+
import { X, ChevronDown, Check, Search, SearchX } from "lucide-vue-next";
|
|
108
113
|
|
|
109
114
|
const props = defineProps({
|
|
110
115
|
options: { type: Array, required: true },
|
|
@@ -146,17 +151,29 @@ const internalValue = computed({
|
|
|
146
151
|
},
|
|
147
152
|
});
|
|
148
153
|
|
|
154
|
+
const normalizedOptions = computed(() =>
|
|
155
|
+
props.options.map((o) => ({
|
|
156
|
+
option: o,
|
|
157
|
+
normalized: String(o?.[props.optionLabel] ?? "").toLowerCase(),
|
|
158
|
+
}))
|
|
159
|
+
);
|
|
160
|
+
|
|
149
161
|
const filteredOptions = computed(() => {
|
|
150
162
|
if (!props.searchable) return props.options;
|
|
151
163
|
const q = searchQuery.value.trim().toLowerCase();
|
|
152
164
|
if (!q) return props.options;
|
|
153
|
-
return
|
|
165
|
+
return normalizedOptions.value
|
|
166
|
+
.filter(({ normalized }) => normalized.includes(q))
|
|
167
|
+
.map(({ option }) => option);
|
|
154
168
|
});
|
|
155
169
|
|
|
156
170
|
watch(open, (val) => {
|
|
157
171
|
if (val) {
|
|
158
172
|
nextTick(() => {
|
|
159
173
|
if (props.searchable) searchInput.value?.focus();
|
|
174
|
+
const el = viewportRef.value?.$el ?? viewportRef.value;
|
|
175
|
+
const checked = el?.querySelector('[data-state="checked"]');
|
|
176
|
+
checked?.scrollIntoView({ block: "nearest" });
|
|
160
177
|
});
|
|
161
178
|
return;
|
|
162
179
|
}
|
|
@@ -220,6 +237,15 @@ function handleSearchKeydown(event) {
|
|
|
220
237
|
if (focused) focused.click();
|
|
221
238
|
}
|
|
222
239
|
}
|
|
240
|
+
|
|
241
|
+
function handleViewportKeydown(event) {
|
|
242
|
+
if (!props.searchable || !searchInput.value || event.target === searchInput.value) return;
|
|
243
|
+
const isPrintable = event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
244
|
+
if (!isPrintable) return;
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
searchQuery.value += event.key;
|
|
247
|
+
nextTick(() => searchInput.value?.focus());
|
|
248
|
+
}
|
|
223
249
|
</script>
|
|
224
250
|
|
|
225
251
|
<style scoped>
|
|
@@ -261,4 +287,31 @@ function handleSearchKeydown(event) {
|
|
|
261
287
|
.select-item {
|
|
262
288
|
transition: background-color 80ms, color 80ms;
|
|
263
289
|
}
|
|
290
|
+
|
|
291
|
+
:deep([data-reka-select-viewport]) {
|
|
292
|
+
scrollbar-gutter: stable;
|
|
293
|
+
scrollbar-width: auto !important;
|
|
294
|
+
scrollbar-color: #475569 #cbd5e1 !important;
|
|
295
|
+
-ms-overflow-style: auto !important;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
:deep([data-reka-select-viewport]::-webkit-scrollbar) {
|
|
299
|
+
display: block !important;
|
|
300
|
+
width: 12px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
:deep([data-reka-select-viewport]::-webkit-scrollbar-track) {
|
|
304
|
+
background: #cbd5e1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
:deep([data-reka-select-viewport]::-webkit-scrollbar-thumb) {
|
|
308
|
+
background-color: #475569;
|
|
309
|
+
border-radius: 999px;
|
|
310
|
+
border: 2px solid #cbd5e1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
:deep([data-reka-select-viewport]::-webkit-scrollbar-thumb:hover) {
|
|
314
|
+
background-color: #334155;
|
|
315
|
+
}
|
|
316
|
+
|
|
264
317
|
</style>
|