@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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/index.cjs +5846 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +791 -0
- package/dist/index.d.ts +791 -0
- package/dist/index.js +5804 -0
- package/dist/index.js.map +1 -0
- package/dist/nuxt/index.cjs +30 -0
- package/dist/nuxt/index.cjs.map +1 -0
- package/dist/nuxt/index.d.cts +15 -0
- package/dist/nuxt/index.d.ts +15 -0
- package/dist/nuxt/index.js +30 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/resolver/index.cjs +25 -0
- package/dist/resolver/index.cjs.map +1 -0
- package/dist/resolver/index.d.cts +14 -0
- package/dist/resolver/index.d.ts +14 -0
- package/dist/resolver/index.js +25 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/styles.css +520 -0
- package/package.json +123 -0
- package/src/components/ACountryFlag.vue +78 -0
- package/src/components/ACountrySelect.vue +674 -0
- package/src/components/ATelInput.vue +742 -0
- package/src/composables/useCountryDetection.ts +247 -0
- package/src/composables/useCountryMatching.ts +213 -0
- package/src/composables/usePhoneValidation.ts +573 -0
- package/src/composables/useTelInputValidation.ts +136 -0
- package/src/composables/useTypingPhase.ts +88 -0
- package/src/icons/AlertCircleIcon.vue +17 -0
- package/src/icons/CheckCircleIcon.vue +16 -0
- package/src/icons/CheckIcon.vue +15 -0
- package/src/icons/ChevronDownIcon.vue +15 -0
- package/src/icons/SearchIcon.vue +16 -0
- package/src/icons/SpinnerIcon.vue +28 -0
- package/src/icons/index.ts +6 -0
- package/src/index.ts +36 -0
- package/src/nuxt/index.ts +37 -0
- package/src/resolver/index.ts +29 -0
- package/src/types.ts +389 -0
- package/src/utils/digits.ts +42 -0
- package/src/utils/flag-url.ts +10 -0
- 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>
|