2025-08-07 19:20:29 +02:00
|
|
|
<template>
|
2025-08-08 12:59:27 +02:00
|
|
|
<div class="phone-input-wrapper" :class="{ 'phone-input-wrapper--mobile': isMobile }">
|
2025-08-08 12:54:05 +02:00
|
|
|
<v-text-field
|
|
|
|
|
v-model="localNumber"
|
|
|
|
|
:label="label"
|
|
|
|
|
:placeholder="placeholder"
|
|
|
|
|
:error="error"
|
|
|
|
|
:error-messages="errorMessage"
|
|
|
|
|
:hint="helpText"
|
|
|
|
|
:persistent-hint="!!helpText"
|
|
|
|
|
:required="required"
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
variant="outlined"
|
2025-08-08 12:59:27 +02:00
|
|
|
:density="isMobile ? 'default' : 'comfortable'"
|
|
|
|
|
class="phone-text-field"
|
2025-08-08 12:54:05 +02:00
|
|
|
@input="handleInput"
|
|
|
|
|
@blur="handleBlur"
|
|
|
|
|
>
|
|
|
|
|
<template #prepend-inner>
|
|
|
|
|
<!-- Country Selector -->
|
|
|
|
|
<v-menu
|
|
|
|
|
v-model="dropdownOpen"
|
|
|
|
|
:close-on-content-click="false"
|
2025-08-08 22:51:14 +02:00
|
|
|
:location="isMobile ? undefined : 'bottom start'"
|
|
|
|
|
:offset="isMobile ? 0 : 4"
|
|
|
|
|
:min-width="isMobile ? undefined : '280'"
|
|
|
|
|
:transition="isMobile ? 'fade-transition' : 'fade-transition'"
|
|
|
|
|
:no-click-animation="isMobile"
|
|
|
|
|
:attach="isMobile"
|
|
|
|
|
:strategy="isMobile ? 'fixed' : 'absolute'"
|
2025-08-08 12:54:05 +02:00
|
|
|
>
|
|
|
|
|
<template #activator="{ props: menuProps }">
|
|
|
|
|
<div
|
|
|
|
|
v-bind="menuProps"
|
|
|
|
|
class="country-selector"
|
2025-08-08 12:59:27 +02:00
|
|
|
:class="{
|
|
|
|
|
'country-selector--open': dropdownOpen,
|
|
|
|
|
'country-selector--mobile': isMobile
|
|
|
|
|
}"
|
2025-08-08 12:54:05 +02:00
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
:src="flagUrl"
|
|
|
|
|
:alt="`${selectedCountry.name} flag`"
|
|
|
|
|
class="country-flag"
|
|
|
|
|
@error="handleFlagError"
|
|
|
|
|
/>
|
|
|
|
|
<span class="country-code">{{ selectedCountry.dialCode }}</span>
|
|
|
|
|
<v-icon
|
2025-08-08 12:59:27 +02:00
|
|
|
:size="isMobile ? 18 : 16"
|
2025-08-08 12:54:05 +02:00
|
|
|
class="dropdown-icon"
|
|
|
|
|
:class="{ 'dropdown-icon--rotated': dropdownOpen }"
|
|
|
|
|
>
|
|
|
|
|
mdi-chevron-down
|
|
|
|
|
</v-icon>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
2025-08-08 12:59:27 +02:00
|
|
|
<!-- Mobile Full-Screen Overlay -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="isMobile && dropdownOpen"
|
|
|
|
|
class="mobile-overlay"
|
|
|
|
|
@click="closeDropdown"
|
|
|
|
|
@touchstart="handleOverlayTouch"
|
|
|
|
|
/>
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
<!-- Dropdown Content -->
|
2025-08-08 12:59:27 +02:00
|
|
|
<v-card
|
|
|
|
|
class="country-dropdown"
|
|
|
|
|
:class="{ 'country-dropdown--mobile': isMobile }"
|
|
|
|
|
elevation="8"
|
|
|
|
|
>
|
|
|
|
|
<!-- Mobile Header -->
|
|
|
|
|
<div v-if="isMobile" class="mobile-header">
|
|
|
|
|
<h3 class="mobile-title">Select Country</h3>
|
|
|
|
|
<v-btn
|
|
|
|
|
icon="mdi-close"
|
|
|
|
|
variant="text"
|
|
|
|
|
size="small"
|
|
|
|
|
@click="closeDropdown"
|
|
|
|
|
class="close-btn"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
<!-- Search Bar -->
|
|
|
|
|
<div class="search-container">
|
|
|
|
|
<v-text-field
|
|
|
|
|
v-model="searchQuery"
|
|
|
|
|
placeholder="Search countries..."
|
|
|
|
|
variant="outlined"
|
2025-08-08 12:59:27 +02:00
|
|
|
:density="isMobile ? 'default' : 'compact'"
|
2025-08-08 12:54:05 +02:00
|
|
|
prepend-inner-icon="mdi-magnify"
|
|
|
|
|
hide-details
|
|
|
|
|
class="search-input"
|
2025-08-08 12:59:27 +02:00
|
|
|
:autofocus="!isMobile"
|
|
|
|
|
clearable
|
2025-08-08 12:54:05 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Country List -->
|
2025-08-08 12:59:27 +02:00
|
|
|
<v-list
|
|
|
|
|
class="country-list"
|
|
|
|
|
:class="{ 'country-list--mobile': isMobile }"
|
|
|
|
|
:density="isMobile ? 'default' : 'compact'"
|
|
|
|
|
>
|
2025-08-08 12:54:05 +02:00
|
|
|
<v-list-item
|
|
|
|
|
v-for="country in filteredCountries"
|
|
|
|
|
:key="country.iso2"
|
|
|
|
|
:class="{
|
|
|
|
|
'country-item': true,
|
|
|
|
|
'country-item--selected': country.iso2 === selectedCountry.iso2,
|
2025-08-08 12:59:27 +02:00
|
|
|
'country-item--preferred': isPreferredCountry(country.iso2),
|
|
|
|
|
'country-item--mobile': isMobile
|
2025-08-08 12:54:05 +02:00
|
|
|
}"
|
|
|
|
|
@click="selectCountry(country)"
|
2025-08-08 12:59:27 +02:00
|
|
|
@touchstart="handleCountryTouch"
|
|
|
|
|
:ripple="isMobile"
|
2025-08-08 12:54:05 +02:00
|
|
|
>
|
|
|
|
|
<template #prepend>
|
|
|
|
|
<img
|
|
|
|
|
:src="getCountryFlagUrl(country.iso2)"
|
|
|
|
|
:alt="`${country.name} flag`"
|
|
|
|
|
class="list-flag"
|
2025-08-08 12:59:27 +02:00
|
|
|
:class="{ 'list-flag--mobile': isMobile }"
|
2025-08-08 12:54:05 +02:00
|
|
|
@error="handleFlagError"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
|
2025-08-08 12:59:27 +02:00
|
|
|
<v-list-item-title
|
|
|
|
|
class="country-name"
|
|
|
|
|
:class="{ 'country-name--mobile': isMobile }"
|
|
|
|
|
>
|
2025-08-08 12:54:05 +02:00
|
|
|
{{ country.name }}
|
|
|
|
|
</v-list-item-title>
|
|
|
|
|
|
|
|
|
|
<template #append>
|
2025-08-08 12:59:27 +02:00
|
|
|
<span
|
|
|
|
|
class="dial-code"
|
|
|
|
|
:class="{ 'dial-code--mobile': isMobile }"
|
|
|
|
|
>
|
|
|
|
|
{{ country.dialCode }}
|
|
|
|
|
</span>
|
2025-08-08 12:54:05 +02:00
|
|
|
</template>
|
|
|
|
|
</v-list-item>
|
|
|
|
|
</v-list>
|
2025-08-08 12:59:27 +02:00
|
|
|
|
|
|
|
|
<!-- Mobile Footer -->
|
|
|
|
|
<div v-if="isMobile" class="mobile-footer">
|
|
|
|
|
<v-btn
|
|
|
|
|
block
|
|
|
|
|
variant="text"
|
|
|
|
|
@click="closeDropdown"
|
|
|
|
|
class="cancel-btn"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</v-btn>
|
|
|
|
|
</div>
|
2025-08-08 12:54:05 +02:00
|
|
|
</v-card>
|
|
|
|
|
</v-menu>
|
|
|
|
|
</template>
|
|
|
|
|
</v-text-field>
|
2025-08-07 19:20:29 +02:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-08-08 12:54:05 +02:00
|
|
|
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
2025-08-08 18:32:46 +02:00
|
|
|
import { getPhoneCountriesWithPreferred, searchPhoneCountries, getPhoneCountryByCode, type PhoneCountry } from '~/utils/phone-countries';
|
2025-08-07 19:20:29 +02:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
modelValue?: string;
|
|
|
|
|
label?: string;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
error?: boolean;
|
|
|
|
|
errorMessage?: string;
|
2025-08-08 00:19:16 +02:00
|
|
|
helpText?: string;
|
2025-08-07 19:20:29 +02:00
|
|
|
required?: boolean;
|
|
|
|
|
disabled?: boolean;
|
2025-08-08 00:35:57 +02:00
|
|
|
defaultCountry?: string;
|
2025-08-08 12:54:05 +02:00
|
|
|
preferredCountries?: string[];
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Emits {
|
|
|
|
|
(e: 'update:modelValue', value: string): void;
|
2025-08-08 18:32:46 +02:00
|
|
|
(e: 'country-changed', country: PhoneCountry): void;
|
|
|
|
|
(e: 'phone-data', data: { number: string; isValid: boolean; country: PhoneCountry }): void;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
modelValue: '',
|
|
|
|
|
placeholder: 'Phone number',
|
|
|
|
|
error: false,
|
|
|
|
|
required: false,
|
2025-08-08 00:35:57 +02:00
|
|
|
disabled: false,
|
2025-08-08 12:54:05 +02:00
|
|
|
defaultCountry: 'MC',
|
|
|
|
|
preferredCountries: () => ['MC', 'FR', 'US', 'IT', 'CH']
|
2025-08-07 19:20:29 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
|
|
2025-08-08 18:32:46 +02:00
|
|
|
// Get comprehensive countries list
|
|
|
|
|
const countries = getPhoneCountriesWithPreferred(props.preferredCountries);
|
2025-08-08 12:54:05 +02:00
|
|
|
|
|
|
|
|
// Reactive state
|
|
|
|
|
const dropdownOpen = ref(false);
|
|
|
|
|
const searchQuery = ref('');
|
|
|
|
|
const localNumber = ref('');
|
2025-08-08 18:32:46 +02:00
|
|
|
const selectedCountry = ref<PhoneCountry>(
|
|
|
|
|
getPhoneCountryByCode(props.defaultCountry) || countries[0]
|
2025-08-08 12:54:05 +02:00
|
|
|
);
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-08 12:59:27 +02:00
|
|
|
// Mobile detection
|
|
|
|
|
const isMobile = ref(false);
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
// Computed
|
|
|
|
|
const flagUrl = computed(() => getCountryFlagUrl(selectedCountry.value.iso2));
|
|
|
|
|
|
2025-08-08 12:59:27 +02:00
|
|
|
// Mobile detection on mount
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
const checkMobile = () => {
|
|
|
|
|
isMobile.value = window.innerWidth <= 768 || 'ontouchstart' in window;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
checkMobile();
|
|
|
|
|
window.addEventListener('resize', checkMobile);
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', checkMobile);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
const filteredCountries = computed(() => {
|
2025-08-08 18:32:46 +02:00
|
|
|
return searchPhoneCountries(searchQuery.value, props.preferredCountries);
|
2025-08-08 12:54:05 +02:00
|
|
|
});
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
// Methods
|
|
|
|
|
const getCountryFlagUrl = (iso2: string) => {
|
|
|
|
|
return `https://flagcdn.com/24x18/${iso2.toLowerCase()}.png`;
|
2025-08-08 00:35:57 +02:00
|
|
|
};
|
2025-08-07 19:20:29 +02:00
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
const isPreferredCountry = (iso2: string) => {
|
|
|
|
|
return props.preferredCountries.includes(iso2);
|
|
|
|
|
};
|
2025-08-08 00:19:16 +02:00
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
const toggleDropdown = () => {
|
|
|
|
|
if (!props.disabled) {
|
|
|
|
|
dropdownOpen.value = !dropdownOpen.value;
|
|
|
|
|
if (dropdownOpen.value) {
|
|
|
|
|
searchQuery.value = '';
|
|
|
|
|
}
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
2025-08-08 12:54:05 +02:00
|
|
|
};
|
2025-08-08 00:19:16 +02:00
|
|
|
|
2025-08-08 18:32:46 +02:00
|
|
|
const selectCountry = (country: PhoneCountry) => {
|
2025-08-08 12:54:05 +02:00
|
|
|
selectedCountry.value = country;
|
|
|
|
|
dropdownOpen.value = false;
|
|
|
|
|
emit('country-changed', country);
|
|
|
|
|
|
|
|
|
|
// Reformat existing number with new country
|
|
|
|
|
if (localNumber.value) {
|
|
|
|
|
handleInput();
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-08-08 00:19:16 +02:00
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
const handleInput = () => {
|
|
|
|
|
const rawInput = localNumber.value;
|
|
|
|
|
|
|
|
|
|
// Create full international number
|
|
|
|
|
const fullNumber = selectedCountry.value.dialCode + rawInput.replace(/\D/g, '');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Parse and validate
|
|
|
|
|
const phoneNumber = parsePhoneNumber(fullNumber);
|
|
|
|
|
const isValid = phoneNumber?.isValid() || false;
|
|
|
|
|
|
|
|
|
|
// Format for display (national format)
|
|
|
|
|
if (phoneNumber && isValid) {
|
|
|
|
|
const formatter = new AsYouType(selectedCountry.value.iso2 as any);
|
|
|
|
|
const formatted = formatter.input(rawInput);
|
|
|
|
|
localNumber.value = formatted;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Emit data
|
|
|
|
|
emit('update:modelValue', fullNumber);
|
|
|
|
|
emit('phone-data', {
|
|
|
|
|
number: fullNumber,
|
|
|
|
|
isValid,
|
|
|
|
|
country: selectedCountry.value
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Handle invalid numbers gracefully
|
|
|
|
|
emit('update:modelValue', fullNumber);
|
|
|
|
|
emit('phone-data', {
|
|
|
|
|
number: fullNumber,
|
|
|
|
|
isValid: false,
|
|
|
|
|
country: selectedCountry.value
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-08 00:19:16 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
const handleBlur = () => {
|
|
|
|
|
// Additional formatting on blur if needed
|
2025-08-08 00:19:16 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
const handleFlagError = (event: Event) => {
|
|
|
|
|
// Fallback to a default flag or hide image
|
|
|
|
|
const img = event.target as HTMLImageElement;
|
|
|
|
|
img.style.display = 'none';
|
2025-08-08 00:19:16 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-08 12:59:27 +02:00
|
|
|
// Mobile-specific handlers
|
|
|
|
|
const closeDropdown = () => {
|
|
|
|
|
dropdownOpen.value = false;
|
|
|
|
|
searchQuery.value = '';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTouchStart = (event: TouchEvent) => {
|
2025-08-08 20:41:39 +02:00
|
|
|
// Allow natural touch behavior, just ensure dropdown works
|
|
|
|
|
if (!dropdownOpen.value && !props.disabled) {
|
|
|
|
|
dropdownOpen.value = true;
|
|
|
|
|
searchQuery.value = '';
|
2025-08-08 12:59:27 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOverlayTouch = (event: TouchEvent) => {
|
|
|
|
|
// Close dropdown when touching overlay
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
closeDropdown();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCountryTouch = (event: TouchEvent) => {
|
2025-08-08 20:41:39 +02:00
|
|
|
// Allow touch to work naturally for country selection
|
|
|
|
|
// Don't prevent default as it interferes with click events
|
2025-08-08 12:59:27 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
// Initialize from modelValue
|
|
|
|
|
watch(() => props.modelValue, (newValue) => {
|
|
|
|
|
if (newValue && newValue !== selectedCountry.value.dialCode + localNumber.value.replace(/\D/g, '')) {
|
|
|
|
|
try {
|
|
|
|
|
const phoneNumber = parsePhoneNumber(newValue);
|
|
|
|
|
if (phoneNumber) {
|
|
|
|
|
// Find matching country
|
|
|
|
|
const matchingCountry = countries.find(c =>
|
|
|
|
|
c.dialCode === '+' + phoneNumber.countryCallingCode
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (matchingCountry) {
|
|
|
|
|
selectedCountry.value = matchingCountry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set local number (national format)
|
|
|
|
|
localNumber.value = phoneNumber.formatNational().replace(phoneNumber.countryCallingCode, '').trim();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Handle invalid initial value
|
|
|
|
|
localNumber.value = newValue;
|
|
|
|
|
}
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
2025-08-08 12:54:05 +02:00
|
|
|
}, { immediate: true });
|
2025-08-07 19:20:29 +02:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-08-08 12:54:05 +02:00
|
|
|
.phone-input-wrapper {
|
2025-08-07 19:20:29 +02:00
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-selector {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
2025-08-08 00:35:57 +02:00
|
|
|
transition: all 0.2s ease;
|
2025-08-08 12:54:05 +02:00
|
|
|
background: rgba(var(--v-theme-surface), 1);
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
margin-right: 8px;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-selector:hover {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.08);
|
|
|
|
|
border-color: rgba(var(--v-theme-primary), 0.24);
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-selector--open {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.12);
|
|
|
|
|
border-color: rgba(var(--v-theme-primary), 0.48);
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-flag {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 18px;
|
2025-08-08 00:35:57 +02:00
|
|
|
border-radius: 2px;
|
2025-08-08 00:19:16 +02:00
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
2025-08-08 12:54:05 +02:00
|
|
|
object-fit: cover;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-code {
|
2025-08-08 00:19:16 +02:00
|
|
|
font-size: 0.875rem;
|
2025-08-08 12:54:05 +02:00
|
|
|
font-weight: 600;
|
2025-08-08 00:19:16 +02:00
|
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
2025-08-08 12:54:05 +02:00
|
|
|
min-width: 32px;
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.dropdown-icon {
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.dropdown-icon--rotated {
|
|
|
|
|
transform: rotate(180deg);
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
/* Dropdown Styling */
|
|
|
|
|
.country-dropdown {
|
|
|
|
|
min-width: 280px;
|
|
|
|
|
max-width: 320px;
|
2025-08-08 18:32:46 +02:00
|
|
|
max-height: 400px;
|
2025-08-08 00:35:57 +02:00
|
|
|
border-radius: 8px;
|
2025-08-08 00:19:16 +02:00
|
|
|
overflow: hidden;
|
2025-08-08 18:32:46 +02:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.search-container {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.12);
|
|
|
|
|
background: rgba(var(--v-theme-surface), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input :deep(.v-field) {
|
|
|
|
|
background: rgba(var(--v-theme-surface), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Country List */
|
|
|
|
|
.country-list {
|
2025-08-08 18:32:46 +02:00
|
|
|
flex: 1;
|
|
|
|
|
max-height: 300px;
|
2025-08-08 12:54:05 +02:00
|
|
|
overflow-y: auto;
|
|
|
|
|
background: rgba(var(--v-theme-surface), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-list::-webkit-scrollbar {
|
2025-08-08 00:19:16 +02:00
|
|
|
width: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-list::-webkit-scrollbar-track {
|
2025-08-08 00:19:16 +02:00
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-list::-webkit-scrollbar-thumb {
|
2025-08-08 00:19:16 +02:00
|
|
|
background: rgba(var(--v-theme-primary), 0.3);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-item {
|
2025-08-07 19:20:29 +02:00
|
|
|
cursor: pointer;
|
2025-08-08 00:19:16 +02:00
|
|
|
transition: all 0.15s ease;
|
2025-08-08 12:54:05 +02:00
|
|
|
border-left: 3px solid transparent;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-item:hover {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.08) !important;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-item--selected {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.12) !important;
|
|
|
|
|
border-left-color: rgb(var(--v-theme-primary));
|
2025-08-08 00:35:57 +02:00
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-item--preferred {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.04);
|
|
|
|
|
font-weight: 500;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.list-flag {
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 15px;
|
2025-08-08 00:35:57 +02:00
|
|
|
border-radius: 2px;
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
2025-08-08 12:54:05 +02:00
|
|
|
object-fit: cover;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-name {
|
|
|
|
|
font-size: 0.875rem;
|
2025-08-07 19:20:29 +02:00
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.dial-code {
|
2025-08-08 00:35:57 +02:00
|
|
|
font-size: 0.8125rem;
|
2025-08-08 12:54:05 +02:00
|
|
|
font-weight: 600;
|
2025-08-08 00:19:16 +02:00
|
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
|
|
|
font-family: 'Roboto Mono', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:59:27 +02:00
|
|
|
/* Mobile Overlay */
|
|
|
|
|
.mobile-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
2025-08-08 22:51:14 +02:00
|
|
|
z-index: 1999;
|
2025-08-08 12:59:27 +02:00
|
|
|
backdrop-filter: blur(4px);
|
2025-08-08 22:51:14 +02:00
|
|
|
-webkit-backdrop-filter: blur(4px);
|
2025-08-08 12:59:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mobile Header */
|
|
|
|
|
.mobile-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.12);
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.04);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-title {
|
|
|
|
|
font-size: 1.125rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mobile Footer */
|
|
|
|
|
.mobile-footer {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-top: 1px solid rgba(var(--v-theme-outline), 0.12);
|
|
|
|
|
background: rgba(var(--v-theme-surface), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cancel-btn {
|
|
|
|
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mobile-specific styling */
|
|
|
|
|
.phone-input-wrapper--mobile {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-selector--mobile {
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
min-height: 44px; /* Touch-friendly size */
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-selector--mobile:hover {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
position: fixed !important;
|
2025-08-08 22:51:14 +02:00
|
|
|
top: 10% !important;
|
|
|
|
|
left: 5% !important;
|
|
|
|
|
right: 5% !important;
|
2025-08-08 12:59:27 +02:00
|
|
|
width: 90vw !important;
|
|
|
|
|
max-width: 400px !important;
|
|
|
|
|
max-height: 80vh !important;
|
2025-08-08 22:51:14 +02:00
|
|
|
margin: 0 auto !important;
|
2025-08-08 12:59:27 +02:00
|
|
|
border-radius: 16px !important;
|
2025-08-08 22:51:14 +02:00
|
|
|
z-index: 2000 !important;
|
|
|
|
|
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3) !important;
|
|
|
|
|
overflow: hidden !important;
|
2025-08-08 12:59:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-list--mobile {
|
|
|
|
|
max-height: calc(60vh - 120px) !important;
|
|
|
|
|
overflow-y: auto !important;
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-list--mobile::-webkit-scrollbar {
|
|
|
|
|
width: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-list--mobile::-webkit-scrollbar-thumb {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.4);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-item--mobile {
|
|
|
|
|
min-height: 56px !important;
|
|
|
|
|
padding: 12px 20px !important;
|
|
|
|
|
border-left-width: 4px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-item--mobile:active {
|
|
|
|
|
background: rgba(var(--v-theme-primary), 0.16) !important;
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-flag--mobile {
|
|
|
|
|
width: 24px !important;
|
|
|
|
|
height: 18px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-name--mobile {
|
|
|
|
|
font-size: 1rem !important;
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dial-code--mobile {
|
|
|
|
|
font-size: 0.9375rem !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Touch-friendly input field */
|
|
|
|
|
.phone-input-wrapper--mobile .phone-text-field :deep(.v-field) {
|
|
|
|
|
min-height: 56px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.phone-input-wrapper--mobile .phone-text-field :deep(.v-field__input) {
|
|
|
|
|
font-size: 16px !important; /* Prevent zoom on iOS */
|
|
|
|
|
padding: 16px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Responsive Breakpoints */
|
2025-08-08 00:19:16 +02:00
|
|
|
@media (max-width: 768px) {
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-dropdown {
|
|
|
|
|
min-width: 260px;
|
|
|
|
|
max-width: 300px;
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-list {
|
|
|
|
|
max-height: 200px;
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
2025-08-08 12:59:27 +02:00
|
|
|
|
|
|
|
|
.country-selector {
|
|
|
|
|
min-height: 48px;
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-flag {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-code {
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
min-width: 36px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-container {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input :deep(.v-field) {
|
|
|
|
|
font-size: 16px !important; /* Prevent zoom */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
width: 95vw !important;
|
|
|
|
|
max-height: 85vh !important;
|
|
|
|
|
border-radius: 12px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-list--mobile {
|
|
|
|
|
max-height: calc(65vh - 140px) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-item--mobile {
|
|
|
|
|
min-height: 52px !important;
|
|
|
|
|
padding: 10px 16px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-header,
|
|
|
|
|
.mobile-footer,
|
|
|
|
|
.search-container {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-title {
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Landscape orientation adjustments */
|
|
|
|
|
@media (max-height: 500px) and (orientation: landscape) {
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
max-height: 90vh !important;
|
|
|
|
|
width: 60vw !important;
|
|
|
|
|
max-width: 500px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-list--mobile {
|
|
|
|
|
max-height: calc(75vh - 120px) !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* High DPI displays */
|
|
|
|
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
|
|
|
|
.country-flag,
|
|
|
|
|
.list-flag {
|
|
|
|
|
image-rendering: -webkit-optimize-contrast;
|
|
|
|
|
image-rendering: crisp-edges;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* iOS specific fixes */
|
|
|
|
|
@supports (-webkit-touch-callout: none) {
|
|
|
|
|
.phone-input-wrapper--mobile .phone-text-field :deep(.v-field__input) {
|
|
|
|
|
font-size: 16px !important; /* Prevent zoom on focus */
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
-webkit-backdrop-filter: blur(8px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-overlay {
|
|
|
|
|
-webkit-backdrop-filter: blur(4px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Android specific fixes */
|
|
|
|
|
@media (pointer: coarse) {
|
|
|
|
|
.country-item--mobile {
|
|
|
|
|
min-height: 56px !important; /* Material Design touch target */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-selector--mobile {
|
|
|
|
|
min-height: 48px !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Accessibility improvements for mobile */
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
.country-item--mobile,
|
|
|
|
|
.country-selector--mobile {
|
|
|
|
|
transition: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-overlay {
|
|
|
|
|
backdrop-filter: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
backdrop-filter: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* High contrast mode for mobile */
|
|
|
|
|
@media (prefers-contrast: high) {
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
border: 3px solid rgb(var(--v-theme-outline));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.country-item--mobile {
|
|
|
|
|
border-bottom: 1px solid rgb(var(--v-theme-outline));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-header {
|
|
|
|
|
border-bottom: 2px solid rgb(var(--v-theme-outline));
|
|
|
|
|
}
|
2025-08-07 19:20:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-08 12:54:05 +02:00
|
|
|
/* Dark theme support */
|
2025-08-08 00:35:57 +02:00
|
|
|
@media (prefers-color-scheme: dark) {
|
2025-08-08 12:54:05 +02:00
|
|
|
.country-dropdown {
|
2025-08-08 00:35:57 +02:00
|
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
2025-08-08 12:59:27 +02:00
|
|
|
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.6) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mobile-overlay {
|
|
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Safe area handling for notched devices */
|
|
|
|
|
@supports (padding: max(0px)) {
|
|
|
|
|
.country-dropdown--mobile {
|
|
|
|
|
padding-top: max(16px, env(safe-area-inset-top));
|
|
|
|
|
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
|
|
|
|
padding-left: max(16px, env(safe-area-inset-left));
|
|
|
|
|
padding-right: max(16px, env(safe-area-inset-right));
|
|
|
|
|
}
|
2025-08-08 00:19:16 +02:00
|
|
|
}
|
2025-08-07 19:20:29 +02:00
|
|
|
</style>
|