cynx-lib 1.3.50 → 1.3.52

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.
@@ -0,0 +1,755 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ ref,
4
+ computed,
5
+ watch,
6
+ onMounted,
7
+ onBeforeUnmount,
8
+ nextTick,
9
+ useSlots,
10
+ } from "vue";
11
+ import { ChevronDown, Check, X, Search } from "lucide-vue-next";
12
+
13
+ export interface SelectOption {
14
+ value: string | number;
15
+ label: string;
16
+ disabled?: boolean;
17
+ [key: string]: any;
18
+ }
19
+
20
+ const SIZES: Record<
21
+ string,
22
+ {
23
+ height: number;
24
+ fontSize: number;
25
+ paddingH: number;
26
+ searchHeight: number;
27
+ searchFont: number;
28
+ itemMinHeight: number;
29
+ itemPadding: string;
30
+ }
31
+ > = {
32
+ xs: {
33
+ height: 24,
34
+ fontSize: 11,
35
+ paddingH: 6,
36
+ searchHeight: 22,
37
+ searchFont: 10.5,
38
+ itemMinHeight: 20,
39
+ itemPadding: "3px 6px",
40
+ },
41
+ sm: {
42
+ height: 28,
43
+ fontSize: 11.5,
44
+ paddingH: 8,
45
+ searchHeight: 26,
46
+ searchFont: 11.5,
47
+ itemMinHeight: 24,
48
+ itemPadding: "4px 8px",
49
+ },
50
+ md: {
51
+ height: 32,
52
+ fontSize: 13,
53
+ paddingH: 10,
54
+ searchHeight: 30,
55
+ searchFont: 12.5,
56
+ itemMinHeight: 28,
57
+ itemPadding: "6px 10px",
58
+ },
59
+ lg: {
60
+ height: 36,
61
+ fontSize: 14,
62
+ paddingH: 14,
63
+ searchHeight: 34,
64
+ searchFont: 13,
65
+ itemMinHeight: 32,
66
+ itemPadding: "8px 12px",
67
+ },
68
+ };
69
+
70
+ const props = withDefaults(
71
+ defineProps<{
72
+ modelValue?: string | number | (string | number)[];
73
+ options?: SelectOption[];
74
+ placeholder?: string;
75
+ disabled?: boolean;
76
+ multi?: boolean;
77
+ searchable?: boolean;
78
+ clearable?: boolean;
79
+ selectAll?: boolean;
80
+ selectAllText?: string;
81
+ size?: "xs" | "sm" | "md" | "lg";
82
+ showCount?: boolean | ((count: number) => string);
83
+ rounded?: boolean | string | number;
84
+ searchMode?: "includes" | "starts_with";
85
+ className?: string;
86
+ style?: Record<string, any>;
87
+ }>(),
88
+ {
89
+ modelValue: undefined,
90
+ options: () => [],
91
+ placeholder: "Select...",
92
+ disabled: false,
93
+ multi: false,
94
+ searchable: false,
95
+ clearable: false,
96
+ selectAll: false,
97
+ selectAllText: "All Select",
98
+ size: "md",
99
+ showCount: false,
100
+ rounded: false,
101
+ searchMode: "includes",
102
+ className: "",
103
+ style: () => ({}),
104
+ }
105
+ );
106
+
107
+ const emit = defineEmits<{
108
+ (e: "update:modelValue", value: any): void;
109
+ (e: "change", value: any): void;
110
+ (e: "clear"): void;
111
+ (e: "visible-change", visible: boolean): void;
112
+ }>();
113
+
114
+ const slots = useSlots();
115
+ const open = ref(false);
116
+ const query = ref("");
117
+ const wrapRef = ref<HTMLElement | null>(null);
118
+ const dropRef = ref<HTMLElement | null>(null);
119
+ const searchInputRef = ref<HTMLInputElement | null>(null);
120
+
121
+ const pos = ref({
122
+ top: 0,
123
+ left: 0,
124
+ width: 0,
125
+ openUp: false,
126
+ maxHeight: 220,
127
+ });
128
+
129
+ const s = computed(() => SIZES[props.size] || SIZES.md);
130
+
131
+ const selectedValues = computed<(string | number)[]>(() => {
132
+ if (props.multi) {
133
+ if (Array.isArray(props.modelValue)) return props.modelValue;
134
+ if (props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== "") {
135
+ return [props.modelValue as string | number];
136
+ }
137
+ return [];
138
+ }
139
+ if (props.modelValue !== undefined && props.modelValue !== null && props.modelValue !== "") {
140
+ return [props.modelValue as string | number];
141
+ }
142
+ return [];
143
+ });
144
+
145
+ const selectedOptions = computed<SelectOption[]>(() => {
146
+ return selectedValues.value
147
+ .map((v) => props.options.find((o) => String(o.value) === String(v)))
148
+ .filter((o): o is SelectOption => Boolean(o));
149
+ });
150
+
151
+ const isAllSelected = computed(() => {
152
+ if (!props.options.length) return false;
153
+ const enabledOptions = props.options.filter((o) => !o.disabled);
154
+ if (!enabledOptions.length) return false;
155
+ return enabledOptions.every((o) =>
156
+ selectedValues.value.some((v) => String(v) === String(o.value))
157
+ );
158
+ });
159
+
160
+ const filteredOptions = computed<SelectOption[]>(() => {
161
+ if (!query.value) return props.options;
162
+ const q = query.value.toLowerCase().trim();
163
+ return props.options.filter((o) => {
164
+ const label = String(o.label ?? "").toLowerCase();
165
+ const val = String(o.value ?? "").toLowerCase();
166
+ if (props.searchMode === "starts_with") {
167
+ return label.startsWith(q) || val.startsWith(q);
168
+ }
169
+ return label.includes(q) || val.includes(q);
170
+ });
171
+ });
172
+
173
+ const calcPosition = () => {
174
+ if (!wrapRef.value) return;
175
+ const r = wrapRef.value.getBoundingClientRect();
176
+ const dropEl = dropRef.value;
177
+ const dropHeight = dropEl ? dropEl.offsetHeight : 220;
178
+ const spaceBelow = window.innerHeight - r.bottom;
179
+ const spaceAbove = r.top;
180
+
181
+ const openUp =
182
+ spaceBelow < Math.min(dropHeight, 220) + 12 && spaceAbove > spaceBelow;
183
+
184
+ const availableSpace = openUp ? spaceAbove - 16 : spaceBelow - 16;
185
+ const calculatedMaxHeight = Math.max(
186
+ 90,
187
+ Math.min(240, availableSpace - (props.searchable || props.selectAll ? 48 : 12))
188
+ );
189
+
190
+ let top: number;
191
+ if (openUp) {
192
+ top = Math.max(8, r.top - (dropEl ? dropEl.offsetHeight : dropHeight) - 4);
193
+ } else {
194
+ top = Math.min(window.innerHeight - 40, r.bottom + 4);
195
+ }
196
+
197
+ let left = r.left;
198
+ const width = r.width;
199
+ if (left + width > window.innerWidth - 8) {
200
+ left = Math.max(8, window.innerWidth - width - 8);
201
+ }
202
+
203
+ pos.value = {
204
+ top,
205
+ left,
206
+ width,
207
+ openUp,
208
+ maxHeight: calculatedMaxHeight,
209
+ };
210
+ };
211
+
212
+ const handleWindowClick = (e: MouseEvent) => {
213
+ if (!open.value) return;
214
+ const target = e.target as Node;
215
+ const inTrigger = wrapRef.value && wrapRef.value.contains(target);
216
+ const inDrop = dropRef.value && dropRef.value.contains(target);
217
+ if (!inTrigger && !inDrop) {
218
+ closeDropdown();
219
+ }
220
+ };
221
+
222
+ const openDropdown = () => {
223
+ if (props.disabled) return;
224
+ open.value = true;
225
+ calcPosition();
226
+ emit("visible-change", true);
227
+ nextTick(() => {
228
+ calcPosition();
229
+ if (props.searchable && searchInputRef.value) {
230
+ searchInputRef.value.focus();
231
+ }
232
+ });
233
+ };
234
+
235
+ const closeDropdown = () => {
236
+ open.value = false;
237
+ query.value = "";
238
+ emit("visible-change", false);
239
+ };
240
+
241
+ const toggleOpen = () => {
242
+ if (open.value) {
243
+ closeDropdown();
244
+ } else {
245
+ openDropdown();
246
+ }
247
+ };
248
+
249
+ const isOptionSelected = (optVal: string | number) => {
250
+ return selectedValues.value.some((v) => String(v) === String(optVal));
251
+ };
252
+
253
+ const toggleOption = (optVal: string | number) => {
254
+ if (props.multi) {
255
+ const current = [...selectedValues.value];
256
+ const index = current.findIndex((v) => String(v) === String(optVal));
257
+ let next: (string | number)[];
258
+ if (index >= 0) {
259
+ next = current.filter((_, i) => i !== index);
260
+ } else {
261
+ next = [...current, optVal];
262
+ }
263
+ emit("update:modelValue", next);
264
+ emit("change", next);
265
+ } else {
266
+ emit("update:modelValue", optVal);
267
+ emit("change", optVal);
268
+ closeDropdown();
269
+ }
270
+ };
271
+
272
+ const toggleSelectAll = () => {
273
+ if (!props.multi) return;
274
+ if (isAllSelected.value) {
275
+ emit("update:modelValue", []);
276
+ emit("change", []);
277
+ } else {
278
+ const allVals = props.options
279
+ .filter((o) => !o.disabled)
280
+ .map((o) => o.value);
281
+ emit("update:modelValue", allVals);
282
+ emit("change", allVals);
283
+ }
284
+ };
285
+
286
+ const removeTag = (e: MouseEvent, val: string | number) => {
287
+ e.stopPropagation();
288
+ const next = selectedValues.value.filter((v) => String(v) !== String(val));
289
+ emit("update:modelValue", next);
290
+ emit("change", next);
291
+ };
292
+
293
+ const handleClear = (e: MouseEvent) => {
294
+ e.stopPropagation();
295
+ const next = props.multi ? [] : "";
296
+ emit("update:modelValue", next);
297
+ emit("change", next);
298
+ emit("clear");
299
+ };
300
+
301
+ const getOptionLabel = (val: string | number) => {
302
+ const opt = props.options.find((o) => String(o.value) === String(val));
303
+ return opt ? opt.label : String(val);
304
+ };
305
+
306
+ const displaySummaryText = computed(() => {
307
+ if (!selectedValues.value.length) return "";
308
+ if (typeof props.showCount === "function") {
309
+ return props.showCount(selectedValues.value.length);
310
+ }
311
+ if (props.showCount) {
312
+ if (selectedValues.value.length === 1) {
313
+ return getOptionLabel(selectedValues.value[0]);
314
+ }
315
+ if (isAllSelected.value) {
316
+ return `All Selected (${selectedValues.value.length})`;
317
+ }
318
+ return `Selected (${selectedValues.value.length})`;
319
+ }
320
+ return "";
321
+ });
322
+
323
+ watch(open, (isOpen) => {
324
+ if (isOpen) {
325
+ window.addEventListener("scroll", calcPosition, true);
326
+ window.addEventListener("resize", calcPosition);
327
+ document.addEventListener("mousedown", handleWindowClick, true);
328
+ } else {
329
+ window.removeEventListener("scroll", calcPosition, true);
330
+ window.removeEventListener("resize", calcPosition);
331
+ document.removeEventListener("mousedown", handleWindowClick, true);
332
+ }
333
+ });
334
+
335
+ onBeforeUnmount(() => {
336
+ window.removeEventListener("scroll", calcPosition, true);
337
+ window.removeEventListener("resize", calcPosition);
338
+ document.removeEventListener("mousedown", handleWindowClick, true);
339
+ });
340
+
341
+ const radiusStyle = computed(() => {
342
+ if (props.rounded) {
343
+ if (typeof props.rounded === "string") {
344
+ return props.rounded.endsWith("px") ||
345
+ props.rounded.endsWith("rem") ||
346
+ props.rounded.startsWith("var")
347
+ ? props.rounded
348
+ : `${props.rounded}px`;
349
+ }
350
+ if (typeof props.rounded === "number") return `${props.rounded}px`;
351
+ return "6px";
352
+ }
353
+ return s.value.height <= 26
354
+ ? "4px"
355
+ : s.value.height <= 28
356
+ ? "6px"
357
+ : "var(--rads, 8px)";
358
+ });
359
+ </script>
360
+
361
+ <template>
362
+ <div
363
+ ref="wrapRef"
364
+ class="cynx-select-root"
365
+ :class="className"
366
+ :style="{
367
+ position: 'relative',
368
+ width: '100%',
369
+ boxSizing: 'border-box',
370
+ opacity: disabled ? 0.5 : 1,
371
+ pointerEvents: disabled ? 'none' : undefined,
372
+ ...style,
373
+ }"
374
+ >
375
+ <!-- Select Trigger -->
376
+ <div
377
+ class="cynx-select-trigger"
378
+ :style="{
379
+ width: '100%',
380
+ boxSizing: 'border-box',
381
+ display: 'flex',
382
+ alignItems: 'center',
383
+ justifyContent: 'space-between',
384
+ gap: '4px',
385
+ padding: `0 ${s.paddingH}px`,
386
+ height: `${s.height}px`,
387
+ background: 'var(--sf, var(--surface, #ffffff))',
388
+ border: `1px solid ${open ? 'var(--accent, #12675D)' : 'var(--border, #e2e8f0)'}`,
389
+ borderRadius: radiusStyle,
390
+ cursor: 'pointer',
391
+ fontSize: `${s.fontSize}px`,
392
+ lineHeight: 1.2,
393
+ color: 'var(--primary-text, #0f172a)',
394
+ outline: 'none',
395
+ transition: 'border-color 0.15s, box-shadow 0.15s',
396
+ boxShadow: open
397
+ ? '0 0 0 2px rgba(18, 103, 93, 0.15)'
398
+ : 'var(--shadow, 0 1px 2px rgba(0, 0, 0, 0.04))',
399
+ }"
400
+ @click="toggleOpen"
401
+ >
402
+ <!-- Prefix Slot -->
403
+ <slot name="prefix" />
404
+
405
+ <!-- Content / Label Area -->
406
+ <div
407
+ style="
408
+ flex: 1;
409
+ min-width: 0;
410
+ display: flex;
411
+ align-items: center;
412
+ overflow: hidden;
413
+ "
414
+ >
415
+ <!-- Custom Label Slot -->
416
+ <slot
417
+ v-if="slots.label && selectedValues.length > 0"
418
+ name="label"
419
+ :value="multi ? selectedValues : selectedValues[0]"
420
+ :selected-options="selectedOptions"
421
+ :selected-option="selectedOptions[0]"
422
+ />
423
+
424
+ <!-- Multi Select Tags / Summary -->
425
+ <template v-else-if="multi && selectedValues.length > 0">
426
+ <span
427
+ v-if="displaySummaryText"
428
+ style="
429
+ font-weight: 500;
430
+ white-space: nowrap;
431
+ overflow: hidden;
432
+ text-overflow: ellipsis;
433
+ color: var(--primary-text, #0f172a);
434
+ "
435
+ >
436
+ {{ displaySummaryText }}
437
+ </span>
438
+ <div
439
+ v-else
440
+ style="
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ gap: 4px;
444
+ flex: 1;
445
+ min-width: 0;
446
+ "
447
+ >
448
+ <span
449
+ v-for="v in selectedValues"
450
+ :key="v"
451
+ style="
452
+ display: inline-flex;
453
+ align-items: center;
454
+ gap: 4px;
455
+ padding: 1px 6px;
456
+ background: rgba(18, 103, 93, 0.1);
457
+ border: 1px solid var(--accent, #12675D);
458
+ border-radius: 4px;
459
+ font-size: 11px;
460
+ font-weight: 600;
461
+ color: var(--accent, #12675D);
462
+ white-space: nowrap;
463
+ max-width: 140px;
464
+ "
465
+ >
466
+ <span style="overflow: hidden; text-overflow: ellipsis">
467
+ {{ getOptionLabel(v) }}
468
+ </span>
469
+ <X
470
+ :size="11"
471
+ style="cursor: pointer; flex-shrink: 0; opacity: 0.6"
472
+ @mousedown="removeTag($event, v)"
473
+ />
474
+ </span>
475
+ </div>
476
+ </template>
477
+
478
+ <!-- Single Select Value -->
479
+ <span
480
+ v-else-if="!multi && selectedValues.length > 0"
481
+ style="
482
+ font-weight: 400;
483
+ white-space: nowrap;
484
+ overflow: hidden;
485
+ text-overflow: ellipsis;
486
+ color: var(--primary-text, #0f172a);
487
+ "
488
+ >
489
+ {{ getOptionLabel(selectedValues[0]) }}
490
+ </span>
491
+
492
+ <!-- Placeholder -->
493
+ <span
494
+ v-else
495
+ style="
496
+ color: var(--grey-3, #94a3b8);
497
+ font-weight: 400;
498
+ white-space: nowrap;
499
+ overflow: hidden;
500
+ text-overflow: ellipsis;
501
+ "
502
+ >
503
+ {{ placeholder }}
504
+ </span>
505
+ </div>
506
+
507
+ <!-- Suffix / Clear / Chevron -->
508
+ <div style="display: flex; align-items: center; gap: 4px; flex-shrink: 0">
509
+ <slot name="suffix" />
510
+
511
+ <span
512
+ v-if="clearable && selectedValues.length > 0 && !disabled"
513
+ style="
514
+ display: inline-flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ cursor: pointer;
518
+ color: var(--muted, #94a3b8);
519
+ padding: 0 2px;
520
+ transition: color 0.15s;
521
+ "
522
+ title="Clear selection"
523
+ @click="handleClear"
524
+ >
525
+ <X :size="12" />
526
+ </span>
527
+
528
+ <ChevronDown
529
+ :size="13"
530
+ :style="{
531
+ flexShrink: 0,
532
+ color: 'var(--grey-3, #94a3b8)',
533
+ transition: 'transform 0.2s ease',
534
+ transform: open ? 'rotate(180deg)' : undefined,
535
+ }"
536
+ />
537
+ </div>
538
+ </div>
539
+
540
+ <!-- Dropdown Menu Teleported to Body -->
541
+ <Teleport to="body">
542
+ <div
543
+ v-if="open"
544
+ ref="dropRef"
545
+ class="cynx-select-dropdown"
546
+ :style="{
547
+ position: 'fixed',
548
+ zIndex: 9999,
549
+ top: `${pos.top}px`,
550
+ left: `${pos.left}px`,
551
+ width: `${pos.width}px`,
552
+ boxSizing: 'border-box',
553
+ background: 'var(--sf, var(--surface, #ffffff))',
554
+ border: '1px solid var(--border, #e2e8f0)',
555
+ borderRadius: radiusStyle,
556
+ boxShadow: 'var(--shadow, 0 4px 12px rgba(0, 0, 0, 0.08))',
557
+ padding: '4px',
558
+ overflow: 'hidden',
559
+ fontFamily: 'inherit',
560
+ }"
561
+ >
562
+ <!-- Custom Header Slot -->
563
+ <slot name="header" />
564
+
565
+ <!-- Built-in Search & Select All Controls -->
566
+ <div
567
+ v-if="searchable || (multi && selectAll)"
568
+ style="
569
+ display: flex;
570
+ flex-direction: column;
571
+ gap: 4px;
572
+ padding: 2px 2px 6px 2px;
573
+ border-bottom: 1px solid var(--border, #f1f5f9);
574
+ margin-bottom: 4px;
575
+ "
576
+ >
577
+ <!-- Search Input -->
578
+ <div
579
+ v-if="searchable"
580
+ style="
581
+ position: relative;
582
+ display: flex;
583
+ align-items: center;
584
+ width: 100%;
585
+ "
586
+ >
587
+ <Search
588
+ :size="12"
589
+ style="
590
+ position: absolute;
591
+ left: 8px;
592
+ color: var(--grey-3, #94a3b8);
593
+ pointer-events: none;
594
+ "
595
+ />
596
+ <input
597
+ ref="searchInputRef"
598
+ v-model="query"
599
+ type="text"
600
+ placeholder="Search..."
601
+ style="
602
+ width: 100%;
603
+ box-sizing: border-box;
604
+ padding: 4px 8px 4px 26px;
605
+ height: 28px;
606
+ background: var(--grey-1, #f8fafc);
607
+ border: 1px solid var(--border, #e2e8f0);
608
+ border-radius: 6px;
609
+ font-size: 12px;
610
+ line-height: 1;
611
+ color: var(--primary-text, #0f172a);
612
+ outline: none;
613
+ transition: border-color 0.15s;
614
+ "
615
+ @keydown.stop
616
+ @mousedown.stop
617
+ />
618
+ </div>
619
+
620
+ <!-- Select All Toggle -->
621
+ <div
622
+ v-if="multi && selectAll && options.length > 0"
623
+ style="
624
+ display: flex;
625
+ align-items: center;
626
+ gap: 6px;
627
+ padding: 4px 6px;
628
+ border-radius: 4px;
629
+ cursor: pointer;
630
+ user-select: none;
631
+ font-size: 12px;
632
+ font-weight: 600;
633
+ color: var(--primary-text, #0f172a);
634
+ "
635
+ @mousedown.prevent.stop="toggleSelectAll"
636
+ >
637
+ <span
638
+ style="
639
+ width: 14px;
640
+ height: 14px;
641
+ border-radius: 3px;
642
+ display: flex;
643
+ align-items: center;
644
+ justify-content: center;
645
+ transition: all 0.15s;
646
+ "
647
+ :style="{
648
+ border: `1.5px solid ${isAllSelected ? 'var(--accent, #12675D)' : 'var(--border, #cbd5e1)'}`,
649
+ background: isAllSelected ? 'var(--accent, #12675D)' : 'transparent',
650
+ }"
651
+ >
652
+ <Check v-if="isAllSelected" :size="10" color="#ffffff" />
653
+ </span>
654
+ <span>{{ selectAllText }}</span>
655
+ </div>
656
+ </div>
657
+
658
+ <!-- Options List -->
659
+ <div
660
+ :style="{
661
+ maxHeight: `${pos.maxHeight}px`,
662
+ overflowY: 'auto',
663
+ }"
664
+ >
665
+ <div
666
+ v-if="filteredOptions.length === 0"
667
+ style="
668
+ padding: 10px 8px;
669
+ text-align: center;
670
+ color: var(--grey-3, #94a3b8);
671
+ font-size: 12px;
672
+ "
673
+ >
674
+ No items found
675
+ </div>
676
+
677
+ <div
678
+ v-for="opt in filteredOptions"
679
+ :key="opt.value"
680
+ :style="{
681
+ display: 'flex',
682
+ alignItems: 'center',
683
+ justifyContent: 'space-between',
684
+ gap: '6px',
685
+ minHeight: `${s.itemMinHeight}px`,
686
+ padding: s.itemPadding,
687
+ borderRadius: '4px',
688
+ cursor: opt.disabled ? 'not-allowed' : 'pointer',
689
+ color: isOptionSelected(opt.value)
690
+ ? 'var(--accent, #12675D)'
691
+ : 'var(--primary-text, #0f172a)',
692
+ background: isOptionSelected(opt.value)
693
+ ? 'rgba(18, 103, 93, 0.08)'
694
+ : 'transparent',
695
+ fontSize: `${s.searchFont}px`,
696
+ fontWeight: isOptionSelected(opt.value) ? 500 : 400,
697
+ transition: 'background 0.1s',
698
+ userSelect: 'none',
699
+ opacity: opt.disabled ? 0.4 : 1,
700
+ pointerEvents: opt.disabled ? 'none' : undefined,
701
+ }"
702
+ @mouseenter="
703
+ (e) => {
704
+ if (!opt.disabled && !isOptionSelected(opt.value)) {
705
+ (e.currentTarget as HTMLElement).style.background =
706
+ 'var(--grey-1, #f8fafc)';
707
+ }
708
+ }
709
+ "
710
+ @mouseleave="
711
+ (e) => {
712
+ if (!opt.disabled && !isOptionSelected(opt.value)) {
713
+ (e.currentTarget as HTMLElement).style.background =
714
+ 'transparent';
715
+ }
716
+ }
717
+ "
718
+ @mousedown.prevent.stop="
719
+ () => {
720
+ if (!opt.disabled) toggleOption(opt.value);
721
+ }
722
+ "
723
+ >
724
+ <!-- Option Content Slot or Default -->
725
+ <slot
726
+ name="option"
727
+ :option="opt"
728
+ :selected="isOptionSelected(opt.value)"
729
+ >
730
+ <span
731
+ style="
732
+ flex: 1;
733
+ white-space: nowrap;
734
+ overflow: hidden;
735
+ text-overflow: ellipsis;
736
+ "
737
+ >
738
+ {{ opt.label }}
739
+ </span>
740
+ </slot>
741
+
742
+ <Check
743
+ v-if="isOptionSelected(opt.value)"
744
+ :size="13"
745
+ style="flex-shrink: 0; color: var(--accent, #12675D)"
746
+ />
747
+ </div>
748
+ </div>
749
+
750
+ <!-- Custom Footer Slot -->
751
+ <slot name="footer" />
752
+ </div>
753
+ </Teleport>
754
+ </div>
755
+ </template>