@alikhalilll/a-tel-input 1.0.1

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/index.cjs +5846 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +791 -0
  6. package/dist/index.d.ts +791 -0
  7. package/dist/index.js +5804 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/nuxt/index.cjs +30 -0
  10. package/dist/nuxt/index.cjs.map +1 -0
  11. package/dist/nuxt/index.d.cts +15 -0
  12. package/dist/nuxt/index.d.ts +15 -0
  13. package/dist/nuxt/index.js +30 -0
  14. package/dist/nuxt/index.js.map +1 -0
  15. package/dist/resolver/index.cjs +25 -0
  16. package/dist/resolver/index.cjs.map +1 -0
  17. package/dist/resolver/index.d.cts +14 -0
  18. package/dist/resolver/index.d.ts +14 -0
  19. package/dist/resolver/index.js +25 -0
  20. package/dist/resolver/index.js.map +1 -0
  21. package/dist/styles.css +520 -0
  22. package/package.json +123 -0
  23. package/src/components/ACountryFlag.vue +78 -0
  24. package/src/components/ACountrySelect.vue +674 -0
  25. package/src/components/ATelInput.vue +742 -0
  26. package/src/composables/useCountryDetection.ts +247 -0
  27. package/src/composables/useCountryMatching.ts +213 -0
  28. package/src/composables/usePhoneValidation.ts +573 -0
  29. package/src/composables/useTelInputValidation.ts +136 -0
  30. package/src/composables/useTypingPhase.ts +88 -0
  31. package/src/icons/AlertCircleIcon.vue +17 -0
  32. package/src/icons/CheckCircleIcon.vue +16 -0
  33. package/src/icons/CheckIcon.vue +15 -0
  34. package/src/icons/ChevronDownIcon.vue +15 -0
  35. package/src/icons/SearchIcon.vue +16 -0
  36. package/src/icons/SpinnerIcon.vue +28 -0
  37. package/src/icons/index.ts +6 -0
  38. package/src/index.ts +36 -0
  39. package/src/nuxt/index.ts +37 -0
  40. package/src/resolver/index.ts +29 -0
  41. package/src/types.ts +389 -0
  42. package/src/utils/digits.ts +42 -0
  43. package/src/utils/flag-url.ts +10 -0
  44. package/web-types.json +526 -0
@@ -0,0 +1,674 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref, watch } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+ import {
5
+ AResponsivePopover,
6
+ AResponsivePopoverContent,
7
+ AResponsivePopoverTrigger,
8
+ } from '@alikhalilll/a-responsive-popover';
9
+ import {
10
+ usePhoneValidation,
11
+ localizeCountries,
12
+ type CountryOption,
13
+ } from '../composables/usePhoneValidation';
14
+ import { DEFAULT_SIZE } from '@alikhalilll/a-ui-base';
15
+ import type { ACountrySelectProps, ACountrySelectSlots } from '../types';
16
+ import ACountryFlag from './ACountryFlag.vue';
17
+ import { ChevronDownIcon, SearchIcon, CheckIcon } from '../icons';
18
+
19
+ const props = withDefaults(defineProps<ACountrySelectProps>(), {
20
+ searchPlaceholder: 'Search country or +code…',
21
+ emptyText: 'No countries found.',
22
+ loadingText: 'Loading countries…',
23
+ suggestedLabel: 'Suggested',
24
+ allCountriesLabel: 'All countries',
25
+ countryLabel: 'Country',
26
+ selectCountryLabel: 'Select country',
27
+ size: DEFAULT_SIZE,
28
+ suggestedLimit: 4,
29
+ maxResults: 80,
30
+ kbdOpen: '⌘K',
31
+ kbdClose: 'Esc',
32
+ });
33
+
34
+ defineSlots<ACountrySelectSlots>();
35
+
36
+ const selected = defineModel<string>('selected', { default: '' });
37
+
38
+ const {
39
+ countries: internalCountries,
40
+ isCountriesLoading,
41
+ getCountries,
42
+ searchCountries: defaultSearch,
43
+ getCountryByValue: lookupInternal,
44
+ } = usePhoneValidation();
45
+
46
+ const open = ref(false);
47
+ const search = ref('');
48
+
49
+ void getCountries();
50
+
51
+ /* ---------------------------------------------------------------
52
+ * Country source — either the user-supplied list (props.countries)
53
+ * or the internal REST Countries + localStorage cache. A `locale`
54
+ * localizes the internal list's display names via `Intl.DisplayNames`;
55
+ * a caller-supplied `countries` list is used verbatim (caller owns names).
56
+ * ------------------------------------------------------------- */
57
+ const effectiveCountries = computed<CountryOption[]>(() =>
58
+ props.countries && props.countries.length
59
+ ? props.countries
60
+ : localizeCountries(internalCountries.value, props.locale)
61
+ );
62
+
63
+ const effectiveByValue = computed<Map<string, CountryOption>>(
64
+ () => new Map(effectiveCountries.value.map((c) => [c.value, c]))
65
+ );
66
+
67
+ function lookup(iso2: string): CountryOption | null {
68
+ if (!iso2) return null;
69
+ return effectiveByValue.value.get(iso2) ?? lookupInternal(iso2);
70
+ }
71
+
72
+ /* ---------------------------------------------------------------
73
+ * Recent picks — persisted so subsequent visits surface the user's
74
+ * actual countries above the long alphabetical list.
75
+ * ------------------------------------------------------------- */
76
+ const RECENTS_KEY = 'ali_ui_country_recents_v1';
77
+ const recents = ref<string[]>([]);
78
+
79
+ function loadRecents() {
80
+ if (typeof window === 'undefined') return;
81
+ try {
82
+ const raw = localStorage.getItem(RECENTS_KEY);
83
+ if (!raw) return;
84
+ const parsed = JSON.parse(raw);
85
+ if (!Array.isArray(parsed)) return;
86
+ recents.value = parsed.filter((v): v is string => typeof v === 'string').slice(0, 8);
87
+ } catch {
88
+ /* ignore corrupt cache */
89
+ }
90
+ }
91
+
92
+ function pushRecent(iso2: string) {
93
+ if (typeof window === 'undefined' || !iso2) return;
94
+ const next = [iso2, ...recents.value.filter((x) => x !== iso2)].slice(0, 8);
95
+ recents.value = next;
96
+ try {
97
+ localStorage.setItem(RECENTS_KEY, JSON.stringify(next));
98
+ } catch {
99
+ /* quota or storage disabled */
100
+ }
101
+ }
102
+
103
+ onMounted(loadRecents);
104
+
105
+ /* ---------------------------------------------------------------
106
+ * Section state
107
+ * ------------------------------------------------------------- */
108
+ const isSearching = computed(() => search.value.trim().length > 0);
109
+
110
+ function defaultSearcher(q: string, c: CountryOption): boolean {
111
+ return c.search_key.includes(q.toLowerCase());
112
+ }
113
+
114
+ const filtered = computed<CountryOption[]>(() => {
115
+ if (!isSearching.value) return [];
116
+ // When the caller didn't override the country source, the internal `searchCountries`
117
+ // is already optimal (uses the precomputed search_key + early break). Fall back to a
118
+ // manual filter when we need to honor a custom `searcher`/`countries` source, or a
119
+ // `locale` (whose localized `search_key` lives only on `effectiveCountries`).
120
+ if (!props.countries && !props.searcher && !props.locale) {
121
+ return defaultSearch(search.value, props.maxResults);
122
+ }
123
+ const q = search.value.trim();
124
+ const matcher = props.searcher ?? defaultSearcher;
125
+ const out: CountryOption[] = [];
126
+ for (const c of effectiveCountries.value) {
127
+ if (matcher(q, c)) {
128
+ out.push(c);
129
+ if (out.length >= props.maxResults) break;
130
+ }
131
+ }
132
+ return out;
133
+ });
134
+
135
+ const suggested = computed<CountryOption[]>(() => {
136
+ if (isSearching.value) return [];
137
+ const seen = new Set<string>();
138
+ const out: CountryOption[] = [];
139
+ const candidate = (iso: string) => {
140
+ if (!iso || seen.has(iso)) return;
141
+ const c = lookup(iso);
142
+ if (!c) return;
143
+ seen.add(iso);
144
+ out.push(c);
145
+ };
146
+ candidate(selected.value);
147
+
148
+ const allowed = props.allowedDialCodes;
149
+ const hasAllowed = Array.isArray(allowed) && allowed.length > 0;
150
+ if (hasAllowed) {
151
+ // Surface every whitelisted country in the Suggested group — they're the only
152
+ // selectable options, so they belong at the top and the recents/limit logic is
153
+ // irrelevant here.
154
+ for (const c of effectiveCountries.value) {
155
+ if (allowed.includes(c.raw_data.dial_digits)) candidate(c.value);
156
+ }
157
+ return out;
158
+ }
159
+
160
+ for (const r of recents.value) {
161
+ candidate(r);
162
+ if (out.length >= props.suggestedLimit) break;
163
+ }
164
+ return out.slice(0, props.suggestedLimit);
165
+ });
166
+
167
+ const allCountries = computed<CountryOption[]>(() => {
168
+ if (isSearching.value) return [];
169
+ return effectiveCountries.value;
170
+ });
171
+
172
+ const selectedCountry = computed<CountryOption | null>(() => lookup(selected.value));
173
+
174
+ function isAllowed(option: CountryOption) {
175
+ const allowed = props.allowedDialCodes;
176
+ if (!allowed || allowed.length === 0) return true;
177
+ return allowed.includes(option.raw_data.dial_digits);
178
+ }
179
+
180
+ function selectCountry(option: CountryOption) {
181
+ if (!isAllowed(option)) return;
182
+ selected.value = option.value;
183
+ pushRecent(option.value);
184
+ open.value = false;
185
+ }
186
+
187
+ watch(open, (isOpen) => {
188
+ if (!isOpen) search.value = '';
189
+ });
190
+
191
+ /** Trigger size — class is consumed by the scoped `<style>` block via `data-size`. The
192
+ * legacy `sizeClasses` slot prop is preserved for backwards compat but it's now an empty
193
+ * string (consumers should rely on `size` directly when overriding the trigger). */
194
+ const triggerSizeClasses = computed(() => '');
195
+
196
+ defineExpose({
197
+ open,
198
+ setOpen: (v: boolean) => (open.value = v),
199
+ search,
200
+ setSearch: (v: string) => (search.value = v),
201
+ selectedCountry,
202
+ selectCountry,
203
+ countries: effectiveCountries,
204
+ recents,
205
+ });
206
+ </script>
207
+
208
+ <template>
209
+ <AResponsivePopover v-model:open="open" :scroll-lock="props.scrollLock">
210
+ <AResponsivePopoverTrigger as-child>
211
+ <slot
212
+ name="trigger"
213
+ :selected-country="selectedCountry"
214
+ :open="open"
215
+ :size-classes="triggerSizeClasses"
216
+ >
217
+ <button
218
+ type="button"
219
+ :disabled="props.disabled"
220
+ data-slot="country-select-trigger"
221
+ :data-state="open ? 'open' : 'closed'"
222
+ :data-size="props.size"
223
+ :class="cn('a-country-select__trigger', props.triggerClass)"
224
+ :aria-label="
225
+ selectedCountry
226
+ ? `${props.countryLabel}: ${selectedCountry.raw_data.name}`
227
+ : props.selectCountryLabel
228
+ "
229
+ >
230
+ <slot v-if="selectedCountry" name="flag" :country="selectedCountry" context="trigger">
231
+ <ACountryFlag
232
+ :iso2="selectedCountry.raw_data.iso2"
233
+ :src="selectedCountry.raw_data.flag"
234
+ :flag-url="props.flagUrl"
235
+ />
236
+ </slot>
237
+ <slot name="chevron" :open="open">
238
+ <ChevronDownIcon class="a-country-select__chevron" :data-open="open ? '' : undefined" />
239
+ </slot>
240
+ </button>
241
+ </slot>
242
+ </AResponsivePopoverTrigger>
243
+
244
+ <AResponsivePopoverContent
245
+ align="end"
246
+ :side-offset="6"
247
+ :class="cn('a-country-select__content', props.contentClass)"
248
+ :popover-class="cn('a-country-select__popover', props.popoverClass)"
249
+ :drawer-class="cn('a-country-select__drawer', props.drawerClass)"
250
+ >
251
+ <!-- Search header -->
252
+ <slot
253
+ name="search"
254
+ :value="search"
255
+ :set-value="(v: string) => (search = v)"
256
+ :is-searching="isSearching"
257
+ >
258
+ <div class="a-country-select__search">
259
+ <div class="a-country-select__search-box">
260
+ <slot name="search-icon">
261
+ <SearchIcon class="a-country-select__search-icon" />
262
+ </slot>
263
+ <input
264
+ v-model="search"
265
+ type="text"
266
+ data-slot="country-select-search"
267
+ :placeholder="props.searchPlaceholder"
268
+ class="a-country-select__search-input"
269
+ />
270
+ <kbd
271
+ v-if="!isSearching && props.kbdOpen"
272
+ class="a-country-select__kbd a-country-select__kbd--open"
273
+ >
274
+ {{ props.kbdOpen }}
275
+ </kbd>
276
+ <kbd
277
+ v-else-if="isSearching && props.kbdClose"
278
+ class="a-country-select__kbd a-country-select__kbd--close"
279
+ >
280
+ {{ props.kbdClose }}
281
+ </kbd>
282
+ </div>
283
+ </div>
284
+ </slot>
285
+
286
+ <!-- List -->
287
+ <div class="a-country-select__list">
288
+ <slot v-if="isCountriesLoading && effectiveCountries.length === 0" name="loading">
289
+ <div class="a-country-select__loading">
290
+ {{ props.loadingText }}
291
+ </div>
292
+ </slot>
293
+
294
+ <slot v-else-if="isSearching && filtered.length === 0" name="empty" :query="search">
295
+ <div class="a-country-select__empty">
296
+ {{ props.emptyText }}
297
+ </div>
298
+ </slot>
299
+
300
+ <template v-else>
301
+ <!-- Suggested group -->
302
+ <section
303
+ v-if="suggested.length > 0"
304
+ data-slot="country-select-group"
305
+ data-group="suggested"
306
+ class="a-country-select__section"
307
+ >
308
+ <slot name="group-header" :label="props.suggestedLabel" group="suggested">
309
+ <header class="a-country-select__group-header">
310
+ {{ props.suggestedLabel }}
311
+ </header>
312
+ </slot>
313
+ <ul
314
+ role="listbox"
315
+ :aria-label="props.suggestedLabel"
316
+ class="a-country-select__group-list"
317
+ >
318
+ <li
319
+ v-for="option in suggested"
320
+ :key="`s-${option.value}`"
321
+ role="option"
322
+ :aria-selected="option.value === selected"
323
+ :aria-disabled="!isAllowed(option)"
324
+ >
325
+ <slot
326
+ name="item"
327
+ :country="option"
328
+ :selected="option.value === selected"
329
+ :disabled="!isAllowed(option)"
330
+ :select="() => selectCountry(option)"
331
+ >
332
+ <button
333
+ type="button"
334
+ :disabled="!isAllowed(option)"
335
+ data-slot="country-select-item"
336
+ :data-selected="option.value === selected ? '' : undefined"
337
+ class="a-country-select__item"
338
+ @click="selectCountry(option)"
339
+ >
340
+ <slot name="flag" :country="option" context="item">
341
+ <ACountryFlag
342
+ :iso2="option.raw_data.iso2"
343
+ :src="option.raw_data.flag"
344
+ :flag-url="props.flagUrl"
345
+ />
346
+ </slot>
347
+ <span class="a-country-select__item-name">{{ option.raw_data.name }}</span>
348
+ <span class="a-country-select__item-dial">{{ option.raw_data.dial_code }}</span>
349
+ <slot v-if="option.value === selected" name="item-check" :country="option">
350
+ <CheckIcon class="a-country-select__item-check" />
351
+ </slot>
352
+ </button>
353
+ </slot>
354
+ </li>
355
+ </ul>
356
+ </section>
357
+
358
+ <!-- All countries / search results -->
359
+ <section
360
+ data-slot="country-select-group"
361
+ data-group="all"
362
+ class="a-country-select__section"
363
+ >
364
+ <slot
365
+ v-if="!isSearching && allCountries.length > 0"
366
+ name="group-header"
367
+ :label="props.allCountriesLabel"
368
+ group="all"
369
+ >
370
+ <header class="a-country-select__group-header">
371
+ {{ props.allCountriesLabel }}
372
+ </header>
373
+ </slot>
374
+ <ul
375
+ role="listbox"
376
+ :aria-label="isSearching ? props.searchPlaceholder : props.allCountriesLabel"
377
+ class="a-country-select__group-list"
378
+ >
379
+ <li
380
+ v-for="option in isSearching ? filtered : allCountries"
381
+ :key="option.value"
382
+ role="option"
383
+ :aria-selected="option.value === selected"
384
+ :aria-disabled="!isAllowed(option)"
385
+ >
386
+ <slot
387
+ name="item"
388
+ :country="option"
389
+ :selected="option.value === selected"
390
+ :disabled="!isAllowed(option)"
391
+ :select="() => selectCountry(option)"
392
+ >
393
+ <button
394
+ type="button"
395
+ :disabled="!isAllowed(option)"
396
+ data-slot="country-select-item"
397
+ :data-selected="option.value === selected ? '' : undefined"
398
+ class="a-country-select__item"
399
+ @click="selectCountry(option)"
400
+ >
401
+ <slot name="flag" :country="option" context="item">
402
+ <ACountryFlag
403
+ :iso2="option.raw_data.iso2"
404
+ :src="option.raw_data.flag"
405
+ :flag-url="props.flagUrl"
406
+ />
407
+ </slot>
408
+ <span class="a-country-select__item-name">{{ option.raw_data.name }}</span>
409
+ <span class="a-country-select__item-dial">{{ option.raw_data.dial_code }}</span>
410
+ <slot v-if="option.value === selected" name="item-check" :country="option">
411
+ <CheckIcon class="a-country-select__item-check" />
412
+ </slot>
413
+ </button>
414
+ </slot>
415
+ </li>
416
+ </ul>
417
+ </section>
418
+ </template>
419
+ </div>
420
+ </AResponsivePopoverContent>
421
+ </AResponsivePopover>
422
+ </template>
423
+
424
+ <style scoped>
425
+ /* ------------------------------------------------------------
426
+ * In-tree (non-teleported) styles — only the trigger button.
427
+ * ---------------------------------------------------------- */
428
+ .a-country-select__trigger {
429
+ display: inline-flex;
430
+ height: 100%;
431
+ flex-shrink: 0;
432
+ align-items: center;
433
+ gap: 0.375rem;
434
+ background: transparent;
435
+ border: 0;
436
+ cursor: pointer;
437
+ transition: background-color 150ms;
438
+ outline: none;
439
+ color: inherit;
440
+ font: inherit;
441
+ }
442
+ .a-country-select__trigger:hover,
443
+ .a-country-select__trigger:focus-visible,
444
+ .a-country-select__trigger[data-state='open'] {
445
+ background: hsl(var(--ak-ui-muted));
446
+ }
447
+ .a-country-select__trigger:focus-visible {
448
+ box-shadow: inset 0 0 0 1px hsl(var(--ak-ui-ring));
449
+ }
450
+ .a-country-select__trigger:disabled {
451
+ cursor: not-allowed;
452
+ opacity: 0.5;
453
+ }
454
+ .a-country-select__trigger[data-size='xs'] {
455
+ padding: 0 0.5rem;
456
+ font-size: 0.75rem;
457
+ }
458
+ .a-country-select__trigger[data-size='sm'] {
459
+ padding: 0 0.625rem;
460
+ font-size: 0.875rem;
461
+ }
462
+ .a-country-select__trigger[data-size='md'] {
463
+ padding: 0 0.75rem;
464
+ font-size: 0.875rem;
465
+ }
466
+ .a-country-select__trigger[data-size='lg'] {
467
+ padding: 0 0.875rem;
468
+ font-size: 1rem;
469
+ }
470
+ .a-country-select__trigger[data-size='xl'] {
471
+ padding: 0 1rem;
472
+ font-size: 1rem;
473
+ }
474
+
475
+ .a-country-select__chevron {
476
+ width: 0.875rem;
477
+ height: 0.875rem;
478
+ flex-shrink: 0;
479
+ color: hsl(var(--ak-ui-muted-foreground));
480
+ transition: transform 200ms;
481
+ }
482
+ .a-country-select__chevron[data-open] {
483
+ transform: rotate(180deg);
484
+ }
485
+ </style>
486
+
487
+ <!--
488
+ The popover content is teleported to <body> by AResponsivePopoverContent (reka-ui Popover
489
+ or vaul-vue Drawer). Vue's `<style scoped>` data-attribute does NOT propagate to teleported
490
+ nodes, so the dropdown UI is styled in this unscoped block. Class names are uniquely
491
+ prefixed `a-country-select__*` to avoid collisions.
492
+ -->
493
+ <style>
494
+ .a-country-select__content {
495
+ display: flex;
496
+ flex-direction: column;
497
+ overflow: hidden;
498
+ padding: 0;
499
+ }
500
+ .a-country-select__popover {
501
+ width: min(20rem, calc(100vw - 2rem));
502
+ max-height: min(22rem, var(--reka-popover-content-available-height));
503
+ }
504
+ .a-country-select__drawer {
505
+ max-height: 80vh;
506
+ padding-bottom: 1rem;
507
+ }
508
+
509
+ .a-country-select__search {
510
+ border-bottom: 1px solid hsl(var(--ak-ui-border) / 0.7);
511
+ padding: 0.375rem;
512
+ }
513
+ .a-country-select__search-box {
514
+ position: relative;
515
+ display: flex;
516
+ align-items: center;
517
+ background: hsl(var(--ak-ui-muted) / 0.4);
518
+ border-radius: calc(var(--ak-ui-radius) - 2px);
519
+ box-shadow: 0 0 0 1px hsl(var(--ak-ui-border) / 0.7);
520
+ transition: box-shadow 150ms;
521
+ }
522
+ .a-country-select__search-box:focus-within {
523
+ box-shadow: 0 0 0 1px hsl(var(--ak-ui-ring) / 0.5);
524
+ }
525
+ .a-country-select__search-icon {
526
+ position: absolute;
527
+ top: 50%;
528
+ inset-inline-start: 0.625rem;
529
+ width: 0.875rem;
530
+ height: 0.875rem;
531
+ transform: translateY(-50%);
532
+ color: hsl(var(--ak-ui-muted-foreground));
533
+ pointer-events: none;
534
+ }
535
+ .a-country-select__search-input {
536
+ height: 2.5rem;
537
+ width: 100%;
538
+ background: transparent;
539
+ border: 0;
540
+ padding-inline-start: 2rem;
541
+ padding-inline-end: 3.5rem;
542
+ font-size: 0.875rem;
543
+ outline: none;
544
+ color: inherit;
545
+ font-family: inherit;
546
+ }
547
+ .a-country-select__search-input::placeholder {
548
+ color: hsl(var(--ak-ui-muted-foreground));
549
+ }
550
+ .a-country-select__kbd {
551
+ position: absolute;
552
+ top: 50%;
553
+ inset-inline-end: 0.5rem;
554
+ display: none;
555
+ align-items: center;
556
+ gap: 0.125rem;
557
+ background: hsl(var(--ak-ui-background));
558
+ color: hsl(var(--ak-ui-muted-foreground));
559
+ border: 1px solid hsl(var(--ak-ui-border));
560
+ border-radius: 0.25rem;
561
+ padding: 0.125rem 0.375rem;
562
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
563
+ font-size: 10px;
564
+ letter-spacing: -0.025em;
565
+ transform: translateY(-50%);
566
+ }
567
+ @media (min-width: 768px) {
568
+ .a-country-select__kbd--open {
569
+ display: inline-flex;
570
+ }
571
+ .a-country-select__kbd--close {
572
+ display: inline-block;
573
+ }
574
+ }
575
+
576
+ .a-country-select__list {
577
+ flex: 1;
578
+ overflow-y: auto;
579
+ /* Themed scrollbar — Firefox + WebKit/Blink. Resolves the browser-default
580
+ light-grey scrollbar that didn't match the popover surface in dark mode. */
581
+ scrollbar-width: thin;
582
+ scrollbar-color: hsl(var(--ak-ui-muted-foreground) / 0.4) transparent;
583
+ }
584
+ .a-country-select__list::-webkit-scrollbar {
585
+ width: 8px;
586
+ height: 8px;
587
+ }
588
+ .a-country-select__list::-webkit-scrollbar-track {
589
+ background: transparent;
590
+ }
591
+ .a-country-select__list::-webkit-scrollbar-thumb {
592
+ background-color: hsl(var(--ak-ui-muted-foreground) / 0.4);
593
+ border-radius: 4px;
594
+ }
595
+ .a-country-select__list::-webkit-scrollbar-thumb:hover {
596
+ background-color: hsl(var(--ak-ui-muted-foreground) / 0.6);
597
+ }
598
+ .a-country-select__loading,
599
+ .a-country-select__empty {
600
+ color: hsl(var(--ak-ui-muted-foreground));
601
+ padding: 1rem;
602
+ text-align: center;
603
+ font-size: 0.875rem;
604
+ }
605
+
606
+ .a-country-select__group-header {
607
+ position: sticky;
608
+ top: 0;
609
+ z-index: 10;
610
+ background: hsl(var(--ak-ui-popover));
611
+ color: hsl(var(--ak-ui-muted-foreground));
612
+ padding: 0.375rem 0.75rem;
613
+ font-size: 10px;
614
+ font-weight: 500;
615
+ letter-spacing: 0.05em;
616
+ text-transform: uppercase;
617
+ margin: 0;
618
+ }
619
+
620
+ .a-country-select__group-list {
621
+ list-style: none;
622
+ margin: 0;
623
+ padding: 0 0 0.25rem;
624
+ }
625
+
626
+ .a-country-select__item {
627
+ display: flex;
628
+ width: 100%;
629
+ align-items: center;
630
+ gap: 0.75rem;
631
+ padding: 0.5rem 0.75rem;
632
+ text-align: start;
633
+ font-size: 0.875rem;
634
+ background: transparent;
635
+ border: 0;
636
+ cursor: pointer;
637
+ color: inherit;
638
+ transition: background-color 150ms;
639
+ outline: none;
640
+ font-family: inherit;
641
+ }
642
+ .a-country-select__item:hover,
643
+ .a-country-select__item:focus-visible {
644
+ background: hsl(var(--ak-ui-muted) / 0.6);
645
+ }
646
+ .a-country-select__item[data-selected] {
647
+ background: hsl(var(--ak-ui-muted));
648
+ }
649
+ .a-country-select__item:disabled {
650
+ cursor: not-allowed;
651
+ opacity: 0.4;
652
+ }
653
+ .a-country-select__item:disabled:hover {
654
+ background: transparent;
655
+ }
656
+
657
+ .a-country-select__item-name {
658
+ flex: 1;
659
+ min-width: 0;
660
+ overflow: hidden;
661
+ text-overflow: ellipsis;
662
+ white-space: nowrap;
663
+ }
664
+ .a-country-select__item-dial {
665
+ color: hsl(var(--ak-ui-muted-foreground));
666
+ font-variant-numeric: tabular-nums;
667
+ }
668
+ .a-country-select__item-check {
669
+ width: 0.875rem;
670
+ height: 0.875rem;
671
+ flex-shrink: 0;
672
+ color: hsl(var(--ak-ui-foreground));
673
+ }
674
+ </style>