reui
Phone Input
Phone Input — кастомный компонент, портированный из ReUI (keenthemes/reui, MIT).
Загрузка превью…
src/reui/phone-input/PhoneInput.vue
<script setup lang="ts">
/**
* Порт ReUI PhoneInput (registry-reui/bases/radix/reui/phone-input.tsx, MIT).
*
* Замена библиотеки: у `react-phone-number-input` нет Vue-порта. Вместо
* неё — `libphonenumber-js` (форматирование через `AsYouType`, разбор
* через `parsePhoneNumberFromString`) и собственная логика ввода/выбора
* страны. Это меняет форму API: оригинал берёт `onChange`/`value` и всю
* логику форматирования, разбора E.164 и списка стран из
* `react-phone-number-input`; здесь то же самое собрано вручную поверх
* `libphonenumber-js`, поведение по духу совпадает (значение наружу —
* E.164 или пустая строка), но кобрёнок реализации не идентичен байт в
* байт апстриму (там она вообще не видна — это внутренности библиотеки).
*
* `smartCaret={false}` в оригинале явно отключает работу с положением
* курсора при переформатировании — здесь курсор тоже не сохраняется
* между ре-форматированиями (одинаковый компромисс, не хуже оригинала).
*/
import type { HTMLAttributes } from "vue"
import { computed, provide, reactive, ref, watch } from "vue"
import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js"
import { cn } from "@/lib/utils"
import { PhoneInputContextKey, type PhoneInputSize } from "./context"
import PhoneInputCountrySelect from "./PhoneInputCountrySelect.vue"
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
variant?: PhoneInputSize
popupClassName?: string
scrollAreaClassName?: string
defaultCountry?: string
placeholder?: string
disabled?: boolean
id?: string
name?: string
ariaInvalid?: boolean
}>(),
{
variant: "default",
}
)
const value = defineModel<string>()
const selectedCountry = ref<string | undefined>(props.defaultCountry)
// Хранит то, что реально набрано: либо международный номер с ведущим "+"
// (тогда форматируем международно, без привязки к выбранной стране — так
// же ведёт себя `react-phone-number-input`, когда значение приходит
// контролируемым `value`), либо цифры национального формата для текущей
// выбранной страны.
const rawDigits = ref("")
function formatDigits(digits: string, country?: string): string {
if (!digits) return ""
// Строка с ведущим "+" форматируется международно (AsYouType сам
// определяет страну по калling-коду); без "+" — национально, по
// выбранной в комбобоксе стране.
return digits.startsWith("+")
? new AsYouType().input(digits)
: new AsYouType(country as never).input(digits)
}
function syncFromValue(next: string | undefined) {
if (!next) {
rawDigits.value = ""
return
}
const parsed = parsePhoneNumberFromString(next)
if (parsed) {
selectedCountry.value = parsed.country ?? selectedCountry.value
}
// Как и `react-phone-number-input` при контролируемом `value`: значение
// отображается в международном формате (с "+" и кодом страны).
rawDigits.value = next
}
watch(() => value.value, syncFromValue, { immediate: true })
function emitFromDigits() {
if (!rawDigits.value) {
value.value = ""
return
}
const formatted = formatDigits(rawDigits.value, selectedCountry.value)
const parsed = parsePhoneNumberFromString(formatted, selectedCountry.value as never)
value.value = parsed?.number ?? ""
}
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
rawDigits.value = target.value.replace(/[^\d+]/g, "")
emitFromDigits()
}
watch(selectedCountry, () => emitFromDigits())
provide(
PhoneInputContextKey,
reactive({
variant: computed(() => props.variant ?? "default"),
popupClassName: computed(() => props.popupClassName),
scrollAreaClassName: computed(() => props.scrollAreaClassName),
})
)
</script>
<template>
<div
:class="
cn(
'flex',
props.ariaInvalid &&
'[&_*[data-slot=combobox-trigger]]:border-destructive [&_*[data-slot=combobox-trigger]]:ring-destructive/50',
props.class
)
"
>
<PhoneInputCountrySelect
v-model="selectedCountry"
:disabled="props.disabled"
/>
<input
data-slot="input"
:id="props.id"
:name="props.name"
type="tel"
:disabled="props.disabled"
:aria-invalid="props.ariaInvalid"
:placeholder="props.placeholder"
:value="formatDigits(rawDigits, selectedCountry)"
:class="
cn(
'cn-input w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
'rounded-s-none focus:z-1',
props.variant === 'sm' && 'h-7',
props.variant === 'lg' && 'h-9'
)
"
@input="handleInput"
/>
</div>
</template>
src/reui/phone-input/PhoneInputCountrySelect.vue
<script setup lang="ts">
/**
* Порт `CountrySelect` из ReUI phone-input. В оригинале это Base UI
* `Combobox` (registry/bases/radix/ui/combobox.tsx) с триггером,
* подменённым на `Button` через `render`-проп (Base UI-паттерн: при
* `render` собственная разметка `ComboboxTrigger` — chevron, свой
* `data-slot` — не рендерится, вся разметка приходит из `render`-элемента,
* поверх неё накладывается только поведение примитива). Итоговый DOM
* триггера — это именно `Button`, без `cn-combobox-trigger`/шеврона.
*
* Примитив: у reka-ui нет `Group`-подобной обвязки Base UI Combobox
* (Portal/Positioner/Popup разделены иначе), но набор частей
* (Root/Trigger/Anchor/Content/Input/Item/Empty/Separator) соответствует
* по смыслу. `ComboboxList` и `ScrollArea` из shadcn-обвязки здесь не
* заведены отдельными компонентами (вне области порта: это общие ui-
* примитивы, а не reui/phone-input), поэтому их разметка и классы
* (`cn-combobox-list`, `cn-scroll-area*`, уже есть в вендоренных
* per-style CSS) воспроизведены инлайново.
*/
import { computed, inject } from "vue"
import {
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxRoot,
ComboboxSeparator,
ComboboxTrigger,
} from "reka-ui"
import { getCountryCallingCode } from "libphonenumber-js"
import { cn } from "@/lib/utils"
import { PhoneInputContextKey } from "./context"
import { countries } from "./countries-data"
import PhoneInputFlag from "./PhoneInputFlag.vue"
const props = defineProps<{
disabled?: boolean
}>()
const country = defineModel<string>()
const context = inject(PhoneInputContextKey)!
const sizeClass = computed(() => {
switch (context.variant) {
case "sm":
return "cn-button-size-sm"
case "lg":
return "cn-button-size-lg"
default:
return "cn-button-size-default"
}
})
</script>
<template>
<ComboboxRoot
:model-value="country"
@update:model-value="(v) => (country = (v as string) || undefined)"
>
<ComboboxAnchor as-child>
<ComboboxTrigger
data-slot="button"
data-variant="outline"
:data-size="context.variant"
:disabled="props.disabled"
:class="
cn(
'cn-button group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
'cn-button-variant-outline',
sizeClass,
'style-vega:rounded-s-md style-nova:rounded-s-lg style-maia:rounded-s-4xl style-lyra:rounded-s-none style-mira:rounded-s-md style-luma:rounded-s-3xl style-rhea:rounded-s-2xl style-sera:rounded-s-none flex gap-1 rounded-e-none border-e-0 px-2.5 py-0 leading-none hover:bg-transparent focus:z-10 data-pressed:bg-transparent',
props.disabled && 'opacity-50'
)
"
>
<span class="sr-only">{{ country }}</span>
<PhoneInputFlag :country="country" :country-name="country" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent
position="popper"
side="bottom"
:side-offset="6"
align="start"
:align-offset="0"
data-slot="combobox-content"
:class="
cn(
'cn-combobox-content cn-combobox-content-logical cn-menu-target cn-menu-translucent group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin)',
'w-xs *:data-[slot=input-group]:bg-transparent',
context.popupClassName
)
"
>
<ComboboxInput
data-slot="combobox-chip-input"
placeholder="e.g. United States"
class="cn-combobox-chip-input min-w-16 flex-1 outline-none border-input focus-visible:border-border rounded-none border-0 px-0 py-2.5 shadow-none ring-0! outline-none! focus-visible:ring-0 focus-visible:ring-offset-0"
/>
<ComboboxSeparator
data-slot="combobox-separator"
class="cn-combobox-separator"
/>
<ComboboxEmpty
data-slot="combobox-empty"
class="cn-combobox-empty px-4 py-2.5 text-sm"
>
No country found.
</ComboboxEmpty>
<div data-slot="combobox-list" class="cn-combobox-list overflow-y-auto overscroll-contain">
<div class="relative flex max-h-full">
<div class="flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain">
<div
data-slot="scroll-area"
:class="
cn(
'cn-scroll-area relative size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain',
context.scrollAreaClassName
)
"
>
<div
data-slot="scroll-area-viewport"
class="cn-scroll-area-viewport size-full rounded-[inherit] transition-[color,box-shadow] outline-none overflow-auto"
>
<ComboboxItem
v-for="item in countries"
:key="item.value"
:value="item.value"
:text-value="item.label"
data-slot="combobox-item"
class="cn-combobox-item relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 flex items-center gap-2"
>
<PhoneInputFlag :country="item.value" :country-name="item.label" />
<span class="flex-1 text-sm">{{ item.label }}</span>
<span class="text-foreground/50 text-sm">{{ `+${getCountryCallingCode(item.value as never)}` }}</span>
</ComboboxItem>
</div>
</div>
</div>
</div>
</div>
</ComboboxContent>
</ComboboxRoot>
</template>
src/reui/phone-input/PhoneInputFlag.vue
<script setup lang="ts">
/**
* Порт `FlagComponent` из ReUI phone-input. В оригинале флаг берётся из
* `react-phone-number-input/flags` (реэкспорт `country-flag-icons/react/3x2`).
* Здесь — те же самые SVG (см. flags-data.ts), извлечённые из
* `country-flag-icons/3x2/*.svg` (тот же пакет, MIT), но только для
* ограниченного набора стран — см. комментарий в flags-data.ts. Для
* остального (и для неизвестных кодов, как в оригинале) — запасная
* иконка-глобус вместо `IconPlaceholder` (dev-инструмент сайта ReUI, не
* компилируется вне апстрима, см. общий комментарий в rating/Rating.vue).
*/
import { computed } from "vue"
import { flags } from "./flags-data"
const props = defineProps<{
country?: string
countryName?: string
}>()
const flag = computed(() => (props.country ? flags[props.country] : undefined))
</script>
<template>
<span
class="flex h-4 w-4 items-center justify-center [&_svg:not([class*='size-'])]:size-full! [&_svg:not([class*='size-'])]:rounded-[5px]"
>
<svg
v-if="flag"
:viewBox="flag.viewBox"
:aria-label="countryName"
role="img"
xmlns="http://www.w3.org/2000/svg"
v-html="flag.inner"
/>
<svg
v-else
class="size-4 opacity-60"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>
</span>
</template>
src/reui/phone-input/context.ts
import type { InjectionKey } from "vue"
export type PhoneInputSize = "sm" | "default" | "lg"
export interface PhoneInputContextValue {
variant: PhoneInputSize
popupClassName?: string
scrollAreaClassName?: string
}
export const PhoneInputContextKey: InjectionKey<PhoneInputContextValue> =
Symbol("PhoneInputContext")
src/reui/phone-input/countries-data.ts
/**
* Список стран для CountrySelect: ISO-коды из `libphonenumber-js` (`getCountries()`,
* те же коды, что использует `react-phone-number-input`), названия — из
* английской локали `react-phone-number-input/locale/en.json` (MIT), взятой
* как источник данных на этапе портирования (рантайм-зависимости на
* react-phone-number-input здесь нет). Сортировка — по названию, как и в
* оригинальном компоненте (countryList приходит от библиотеки уже
* отсортированным по локализованному названию).
*/
export interface CountryEntry {
value: string
label: string
}
export const countries: CountryEntry[] = [
{ value: "AF", label: "Afghanistan" },
{ value: "AX", label: "Åland Islands" },
{ value: "AL", label: "Albania" },
{ value: "DZ", label: "Algeria" },
{ value: "AS", label: "American Samoa" },
{ value: "AD", label: "Andorra" },
{ value: "AO", label: "Angola" },
{ value: "AI", label: "Anguilla" },
{ value: "AG", label: "Antigua and Barbuda" },
{ value: "AR", label: "Argentina" },
{ value: "AM", label: "Armenia" },
{ value: "AW", label: "Aruba" },
{ value: "AC", label: "Ascension Island" },
{ value: "AU", label: "Australia" },
{ value: "AT", label: "Austria" },
{ value: "AZ", label: "Azerbaijan" },
{ value: "BS", label: "Bahamas" },
{ value: "BH", label: "Bahrain" },
{ value: "BD", label: "Bangladesh" },
{ value: "BB", label: "Barbados" },
{ value: "BY", label: "Belarus" },
{ value: "BE", label: "Belgium" },
{ value: "BZ", label: "Belize" },
{ value: "BJ", label: "Benin" },
{ value: "BM", label: "Bermuda" },
{ value: "BT", label: "Bhutan" },
{ value: "BO", label: "Bolivia" },
{ value: "BQ", label: "Bonaire, Sint Eustatius and Saba" },
{ value: "BA", label: "Bosnia and Herzegovina" },
{ value: "BW", label: "Botswana" },
{ value: "BR", label: "Brazil" },
{ value: "IO", label: "British Indian Ocean Territory" },
{ value: "BN", label: "Brunei Darussalam" },
{ value: "BG", label: "Bulgaria" },
{ value: "BF", label: "Burkina Faso" },
{ value: "BI", label: "Burundi" },
{ value: "KH", label: "Cambodia" },
{ value: "CM", label: "Cameroon" },
{ value: "CA", label: "Canada" },
{ value: "CV", label: "Cape Verde" },
{ value: "KY", label: "Cayman Islands" },
{ value: "CF", label: "Central African Republic" },
{ value: "TD", label: "Chad" },
{ value: "CL", label: "Chile" },
{ value: "CN", label: "China" },
{ value: "CX", label: "Christmas Island" },
{ value: "CC", label: "Cocos (Keeling) Islands" },
{ value: "CO", label: "Colombia" },
{ value: "KM", label: "Comoros" },
{ value: "CG", label: "Congo" },
{ value: "CD", label: "Congo, Democratic Republic of the" },
{ value: "CK", label: "Cook Islands" },
{ value: "CR", label: "Costa Rica" },
{ value: "CI", label: "Cote d'Ivoire" },
{ value: "HR", label: "Croatia" },
{ value: "CU", label: "Cuba" },
{ value: "CW", label: "Curaçao" },
{ value: "CY", label: "Cyprus" },
{ value: "CZ", label: "Czech Republic" },
{ value: "DK", label: "Denmark" },
{ value: "DJ", label: "Djibouti" },
{ value: "DM", label: "Dominica" },
{ value: "DO", label: "Dominican Republic" },
{ value: "EC", label: "Ecuador" },
{ value: "EG", label: "Egypt" },
{ value: "SV", label: "El Salvador" },
{ value: "GQ", label: "Equatorial Guinea" },
{ value: "ER", label: "Eritrea" },
{ value: "EE", label: "Estonia" },
{ value: "ET", label: "Ethiopia" },
{ value: "FK", label: "Falkland Islands" },
{ value: "FO", label: "Faroe Islands" },
{ value: "FM", label: "Federated States of Micronesia" },
{ value: "FJ", label: "Fiji" },
{ value: "FI", label: "Finland" },
{ value: "FR", label: "France" },
{ value: "GF", label: "French Guiana" },
{ value: "PF", label: "French Polynesia" },
{ value: "GA", label: "Gabon" },
{ value: "GM", label: "Gambia" },
{ value: "GE", label: "Georgia" },
{ value: "DE", label: "Germany" },
{ value: "GH", label: "Ghana" },
{ value: "GI", label: "Gibraltar" },
{ value: "GR", label: "Greece" },
{ value: "GL", label: "Greenland" },
{ value: "GD", label: "Grenada" },
{ value: "GP", label: "Guadeloupe" },
{ value: "GU", label: "Guam" },
{ value: "GT", label: "Guatemala" },
{ value: "GG", label: "Guernsey" },
{ value: "GN", label: "Guinea" },
{ value: "GW", label: "Guinea-Bissau" },
{ value: "GY", label: "Guyana" },
{ value: "HT", label: "Haiti" },
{ value: "VA", label: "Holy See (Vatican City State)" },
{ value: "HN", label: "Honduras" },
{ value: "HK", label: "Hong Kong" },
{ value: "HU", label: "Hungary" },
{ value: "IS", label: "Iceland" },
{ value: "IN", label: "India" },
{ value: "ID", label: "Indonesia" },
{ value: "IR", label: "Iran" },
{ value: "IQ", label: "Iraq" },
{ value: "IE", label: "Ireland" },
{ value: "IM", label: "Isle of Man" },
{ value: "IL", label: "Israel" },
{ value: "IT", label: "Italy" },
{ value: "JM", label: "Jamaica" },
{ value: "JP", label: "Japan" },
{ value: "JE", label: "Jersey" },
{ value: "JO", label: "Jordan" },
{ value: "KZ", label: "Kazakhstan" },
{ value: "KE", label: "Kenya" },
{ value: "KI", label: "Kiribati" },
{ value: "XK", label: "Kosovo" },
{ value: "KW", label: "Kuwait" },
{ value: "KG", label: "Kyrgyzstan" },
{ value: "LA", label: "Laos" },
{ value: "LV", label: "Latvia" },
{ value: "LB", label: "Lebanon" },
{ value: "LS", label: "Lesotho" },
{ value: "LR", label: "Liberia" },
{ value: "LY", label: "Libya" },
{ value: "LI", label: "Liechtenstein" },
{ value: "LT", label: "Lithuania" },
{ value: "LU", label: "Luxembourg" },
{ value: "MO", label: "Macao" },
{ value: "MG", label: "Madagascar" },
{ value: "MW", label: "Malawi" },
{ value: "MY", label: "Malaysia" },
{ value: "MV", label: "Maldives" },
{ value: "ML", label: "Mali" },
{ value: "MT", label: "Malta" },
{ value: "MH", label: "Marshall Islands" },
{ value: "MQ", label: "Martinique" },
{ value: "MR", label: "Mauritania" },
{ value: "MU", label: "Mauritius" },
{ value: "YT", label: "Mayotte" },
{ value: "MX", label: "Mexico" },
{ value: "MD", label: "Moldova" },
{ value: "MC", label: "Monaco" },
{ value: "MN", label: "Mongolia" },
{ value: "ME", label: "Montenegro" },
{ value: "MS", label: "Montserrat" },
{ value: "MA", label: "Morocco" },
{ value: "MZ", label: "Mozambique" },
{ value: "MM", label: "Myanmar" },
{ value: "NA", label: "Namibia" },
{ value: "NR", label: "Nauru" },
{ value: "NP", label: "Nepal" },
{ value: "NL", label: "Netherlands" },
{ value: "NC", label: "New Caledonia" },
{ value: "NZ", label: "New Zealand" },
{ value: "NI", label: "Nicaragua" },
{ value: "NE", label: "Niger" },
{ value: "NG", label: "Nigeria" },
{ value: "NU", label: "Niue" },
{ value: "NF", label: "Norfolk Island" },
{ value: "KP", label: "North Korea" },
{ value: "MK", label: "North Macedonia" },
{ value: "MP", label: "Northern Mariana Islands" },
{ value: "NO", label: "Norway" },
{ value: "OM", label: "Oman" },
{ value: "PK", label: "Pakistan" },
{ value: "PW", label: "Palau" },
{ value: "PS", label: "Palestine" },
{ value: "PA", label: "Panama" },
{ value: "PG", label: "Papua New Guinea" },
{ value: "PY", label: "Paraguay" },
{ value: "PE", label: "Peru" },
{ value: "PH", label: "Philippines" },
{ value: "PL", label: "Poland" },
{ value: "PT", label: "Portugal" },
{ value: "PR", label: "Puerto Rico" },
{ value: "QA", label: "Qatar" },
{ value: "RE", label: "Reunion" },
{ value: "RO", label: "Romania" },
{ value: "RU", label: "Russia" },
{ value: "RW", label: "Rwanda" },
{ value: "BL", label: "Saint Barthélemy" },
{ value: "SH", label: "Saint Helena" },
{ value: "KN", label: "Saint Kitts and Nevis" },
{ value: "LC", label: "Saint Lucia" },
{ value: "MF", label: "Saint Martin (French Part)" },
{ value: "PM", label: "Saint Pierre and Miquelon" },
{ value: "VC", label: "Saint Vincent and the Grenadines" },
{ value: "WS", label: "Samoa" },
{ value: "SM", label: "San Marino" },
{ value: "ST", label: "Sao Tome and Principe" },
{ value: "SA", label: "Saudi Arabia" },
{ value: "SN", label: "Senegal" },
{ value: "RS", label: "Serbia" },
{ value: "SC", label: "Seychelles" },
{ value: "SL", label: "Sierra Leone" },
{ value: "SG", label: "Singapore" },
{ value: "SX", label: "Sint Maarten" },
{ value: "SK", label: "Slovakia" },
{ value: "SI", label: "Slovenia" },
{ value: "SB", label: "Solomon Islands" },
{ value: "SO", label: "Somalia" },
{ value: "ZA", label: "South Africa" },
{ value: "KR", label: "South Korea" },
{ value: "SS", label: "South Sudan" },
{ value: "ES", label: "Spain" },
{ value: "LK", label: "Sri Lanka" },
{ value: "SD", label: "Sudan" },
{ value: "SR", label: "Suriname" },
{ value: "SJ", label: "Svalbard and Jan Mayen" },
{ value: "SZ", label: "Swaziland" },
{ value: "SE", label: "Sweden" },
{ value: "CH", label: "Switzerland" },
{ value: "SY", label: "Syria" },
{ value: "TW", label: "Taiwan" },
{ value: "TJ", label: "Tajikistan" },
{ value: "TZ", label: "Tanzania" },
{ value: "TH", label: "Thailand" },
{ value: "TL", label: "Timor-Leste" },
{ value: "TG", label: "Togo" },
{ value: "TK", label: "Tokelau" },
{ value: "TO", label: "Tonga" },
{ value: "TT", label: "Trinidad and Tobago" },
{ value: "TA", label: "Tristan da Cunha" },
{ value: "TN", label: "Tunisia" },
{ value: "TR", label: "Turkey" },
{ value: "TM", label: "Turkmenistan" },
{ value: "TC", label: "Turks and Caicos Islands" },
{ value: "TV", label: "Tuvalu" },
{ value: "UG", label: "Uganda" },
{ value: "UA", label: "Ukraine" },
{ value: "AE", label: "United Arab Emirates" },
{ value: "GB", label: "United Kingdom" },
{ value: "US", label: "United States" },
{ value: "UY", label: "Uruguay" },
{ value: "UZ", label: "Uzbekistan" },
{ value: "VU", label: "Vanuatu" },
{ value: "VE", label: "Venezuela" },
{ value: "VN", label: "Vietnam" },
{ value: "VG", label: "Virgin Islands, British" },
{ value: "VI", label: "Virgin Islands, U.S." },
{ value: "WF", label: "Wallis and Futuna" },
{ value: "EH", label: "Western Sahara" },
{ value: "YE", label: "Yemen" },
{ value: "ZM", label: "Zambia" },
{ value: "ZW", label: "Zimbabwe" },
]
src/reui/phone-input/flags-data.ts
/**
* Флаги стран (3x2), извлечены дословно из `country-flag-icons/3x2/*.svg`
* (MIT, тот же пакет, из которого `react-phone-number-input/flags`
* реэкспортирует свои React-компоненты флагов: `export { default as default }
* from "country-flag-icons/react/3x2"`). Значит, при использовании
* реальной библиотеки на React-стороне кейса и этих же самых SVG на
* Vue-стороне флаги должны совпадать попиксельно.
*
* ВАЖНО — сознательное сокращение объёма: в оригинале доступны флаги для
* всех ~250 кодов стран ITU. Здесь заведено 28 самых
* распространённых (использованы в кейсе демонстрации). Для остальных
* кодов `PhoneInputFlag` отрисовывает запасной globe-icon — как и
* оригинал делает для НЕизвестных кодов, только здесь список "известных"
* короче. Расширить до полного набора — механическая работа (см. скрипт
* генерации в истории портирования), просто не уместилась в объём
* текущего порта.
*/
export interface FlagSvgData {
viewBox: string
inner: string
}
export const flags: Record<string, FlagSvgData> = {
US: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><g fill="#D80027"><path d="M0 0h513v26.3H0zM0 52.6h513v26.3H0zM0 105.2h513v26.3H0zM0 157.8h513v26.3H0zM0 210.5h513v26.3H0zM0 263.1h513v26.3H0zM0 315.7h513V342H0z"/></g><path fill="#2E52B2" d="M0 0h256.5v184.1H0z"/><g fill="#FFF"><path d="m47.8 138.9-4-12.8-4.4 12.8H26.2l10.7 7.7-4 12.8 10.9-7.9 10.6 7.9-4.1-12.8 10.9-7.7zM104.1 138.9l-4.1-12.8-4.2 12.8H82.6l10.7 7.7-4 12.8 10.7-7.9 10.8 7.9-4-12.8 10.7-7.7zM160.6 138.9l-4.3-12.8-4 12.8h-13.5l11 7.7-4.2 12.8 10.7-7.9 11 7.9-4.2-12.8 10.7-7.7zM216.8 138.9l-4-12.8-4.2 12.8h-13.3l10.8 7.7-4 12.8 10.7-7.9 10.8 7.9-4.3-12.8 11-7.7zM100 75.3l-4.2 12.8H82.6L93.3 96l-4 12.6 10.7-7.8 10.8 7.8-4-12.6 10.7-7.9h-13.4zM43.8 75.3l-4.4 12.8H26.2L36.9 96l-4 12.6 10.9-7.8 10.6 7.8L50.3 96l10.9-7.9H47.8zM156.3 75.3l-4 12.8h-13.5l11 7.9-4.2 12.6 10.7-7.8 11 7.8-4.2-12.6 10.7-7.9h-13.2zM212.8 75.3l-4.2 12.8h-13.3l10.8 7.9-4 12.6 10.7-7.8 10.8 7.8-4.3-12.6 11-7.9h-13.5zM43.8 24.7l-4.4 12.6H26.2l10.7 7.9-4 12.7L43.8 50l10.6 7.9-4.1-12.7 10.9-7.9H47.8zM100 24.7l-4.2 12.6H82.6l10.7 7.9-4 12.7L100 50l10.8 7.9-4-12.7 10.7-7.9h-13.4zM156.3 24.7l-4 12.6h-13.5l11 7.9-4.2 12.7 10.7-7.9 11 7.9-4.2-12.7 10.7-7.9h-13.2zM212.8 24.7l-4.2 12.6h-13.3l10.8 7.9-4 12.7 10.7-7.9 10.8 7.9-4.3-12.7 11-7.9h-13.5z"/></g>` },
GB: { viewBox: "0 0 513 342", inner: `<g fill="#FFF"><path d="M0 0h513v341.3H0V0z"/><path d="M311.7 230 513 341.3v-31.5L369.3 230h-57.6zM200.3 111.3 0 0v31.5l143.7 79.8h56.6z"/></g><g fill="#012169"><path d="M393.8 230 513 295.7V230H393.8zm-82.1 0L513 341.3v-31.5L369.3 230h-57.6zm146.9 111.3-147-81.7v81.7h147zM90.3 230 0 280.2V230h90.3zm110 14.2v97.2H25.5l174.8-97.2zM118.2 111.3 0 45.6v65.7h118.2zm82.1 0L0 0v31.5l143.7 79.8h56.6zM53.4 0l147 81.7V0h-147zM421.7 111.3 513 61.1v50.2h-91.3zm-110-14.2V0h174.9L311.7 97.1z"/></g><g fill="#c8102e"><path d="M288 0h-64v138.7H0v64h224v138.7h64V202.7h224v-64H288V0z"/><path d="M311.7 230 513 341.3v-31.5L369.3 230h-57.6zM143.7 230 0 309.9v31.5L200.3 230h-56.6zM200.3 111.3 0 0v31.5l143.7 79.8h56.6zM368.3 111.3 513 31.5V0L311.7 111.3h56.6z"/></g>` },
DE: { viewBox: "0 0 513 342", inner: `<path fill="#D80027" d="M0 0h513v342H0z"/><path d="M0 0h513v114H0z"/><path fill="#FFDA44" d="M0 228h513v114H0z"/>` },
FR: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#00318A" d="M0 0h171v342H0z"/><path fill="#D80027" d="M342 0h171v342H342z"/>` },
ES: { viewBox: "0 0 22.5 15", inner: `<path fill="#FFF" d="M0 0h22.5v15H0V0z"/><path fill="#D03433" d="M0 0h22.5v4H0V0zm0 11h22.5v4H0v-4z"/><path fill="#FBCA46" d="M0 4h22.5v7H0V4z"/><path fill="#FFF" d="M7.8 7h1v.5h-1V7z"/><path fill="#A41517" d="M7.2 8.5c0 .3.3.5.6.5s.6-.2.6-.5L8.5 7H7.1l.1 1.5zM6.6 7c0-.3.2-.5.4-.5h1.5c.3 0 .5.2.5.4V7l-.1 1.5c-.1.6-.5 1-1.1 1-.6 0-1-.4-1.1-1L6.6 7z"/><path fill="#A41517" d="M6.8 7.5h2V8h-.5l-.5 1-.5-1h-.5v-.5zM5.3 6h1v3.5h-1V6zm4 0h1v3.5h-1V6zm-2.5-.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v.2c0 .2-.1.3-.3.3H7c-.1 0-.2-.1-.2-.2v-.3z"/>` },
IT: { viewBox: "0 0 513 342", inner: `<path fill="#F4F5F0" d="M342 0H0v341.3h512V0z"/><path fill="#008C45" d="M0 0h171v342H0z"/><path fill="#CD212A" d="M342 0h171v342H342z"/>` },
LV: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><g fill="#A2001D"><path class="st1" d="M0 0h513v127.6H0zM0 214.4h513V342H0z"/></g>` },
LT: { viewBox: "0 0 513 342", inner: `<path fill="#6DA544" d="M0 0h513v342H0z"/><path fill="#FFDA44" d="M0 0h513v114H0z"/><path fill="#D80027" d="M0 228h513v114H0z"/>` },
EE: { viewBox: "0 0 513 342", inner: `<path fill="#111" d="M0 0h513v342H0z"/><path fill="#368FD8" d="M0 0h513v114H0z"/><path fill="#FFF" d="M0 228h513v114H0z"/>` },
RU: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v114H0z"/><path fill="#0039a6" d="M0 114h513v114H0z"/><path fill="#d52b1e" d="M0 228h513v114H0z"/>` },
UA: { viewBox: "0 85.333 512 341.333", inner: `<path fill="#FFDA44" d="M0 85.337h512v341.326H0z"/><path fill="#338AF3" d="M0 85.337h512V256H0z"/>` },
PL: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#dc143c" d="M0 171h513v171H0z"/>` },
NL: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 114h513v114H0z"/><path fill="#cd1f2a" d="M0 0h513v114H0z"/><path fill="#1d4185" d="M0 228h513v114H0z"/>` },
BE: { viewBox: "0 0 513 342", inner: `<path fill="#fdda25" d="M0 0h513v342H0z"/><path d="M0 0h171v342H0z"/><path fill="#ef3340" d="M342 0h171v342H342z"/>` },
CH: { viewBox: "0 0 768 512", inner: `<path fill="red" d="M0 0h768v512H0V0z"/><path fill="#FFF" d="M336 96h96v112h112v96H432v112h-96V304H224v-96h112V96z"/>` },
AT: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 114h513v114H0z"/><path fill="#D80027" d="M0 0h513v114H0zM0 228h513v114H0z"/>` },
SE: { viewBox: "0 0 513 342", inner: `<path fill="#004F8E" d="M0 0h513v342H0z"/><path fill="#F6C500" d="M192.4.3h-64.2v138.8H0v64.1h128.2V342h64.2V203.2H513v-64.1H192.4z"/>` },
NO: { viewBox: "0 0 513 342", inner: `<path fill="#ba0c2f" d="M0 0h513v342H0z"/><path fill="#FFF" d="M513 211H202.6v131h-79.9V211H0v-80h122.7V0h79.9v131H513v61.4z"/><path fill="#00205b" d="M513 149.6v42.8H184V342h-42.7V192.4H0v-42.8h141.3V0H184v149.6z"/>` },
DK: { viewBox: "0 0 513 342", inner: `<path fill="#c60c30" d="M0 0h513v342H0z"/><path fill="#FFF" d="M190 0h-60v140H0v60h130v142h60V200h323v-60H190z"/>` },
FI: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#2E52B2" d="M513 129.3V212H203.7v130H121V212H0v-82.7h121V0h82.7v129.3z"/>` },
JP: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h512v342H0z"/><circle fill="#D80027" cx="256.5" cy="171" r="96"/>` },
CN: { viewBox: "0 0 513 342", inner: `<path fill="#D80027" d="M0 0h513v342H0z"/><g fill="#FFDA44"><path d="m226.8 239.2-9.7-15.6-17.9 4.4 11.9-14.1-9.7-15.6 17.1 6.9 11.8-14.1-1.3 18.4 17.1 6.9-17.9 4.4zM290.6 82l-10.1 15.4 11.6 14.3-17.7-4.8-10.1 15.5-1-18.4-17.7-4.8 17.2-6.6-1-18.4 11.6 14.3zM236.2 25.4l-2 18.3 16.8 7.6-18 3.8-2 18.3-9.2-16-17.9 3.8 12.3-13.7-9.2-15.9 16.8 7.5zM292.8 161.8l-14.9 10.9 5.8 17.5-14.9-10.8-14.9 11 5.6-17.6-14.9-10.7 18.4-.1 5.6-17.6 5.8 17.5zM115 46.3l17.3 53.5h56.2l-45.4 32.9 17.3 53.5-45.4-33-45.5 33 17.4-53.5-45.5-32.9h56.3z"/></g>` },
IN: { viewBox: "0 0 513 342", inner: `<path fill="#ff6820" d="M0 0h513v114H0V0z"/><path fill="#FFF" d="M0 114h513v114H0V114z"/><path fill="#046a38" d="M0 228h513v114H0V228z"/><path fill="none" stroke="#07038d" stroke-width="4" d="M256.5 136.8c18.9 0 34.2 15.3 34.2 34.2s-15.3 34.2-34.2 34.2-34.2-15.3-34.2-34.2 15.3-34.2 34.2-34.2z"/><g stroke="#07038d" stroke-width="2"><path d="m265.3 138.2-17.6 65.7m17.6-65.7-17.6 65.7M273.5 141.6l-34 58.9M280.5 147l-48 48M285.9 154 227 188M289.3 162.2l-65.7 17.6M290.5 171h-68M289.3 179.8l-65.7-17.6M285.9 188 227 154M280.5 195l-48-48M273.5 200.4l-34-58.9M265.3 203.8l-17.6-65.7M256.5 205v-68M247.7 203.8l17.6-65.7M239.5 200.4l34-58.9M232.5 195l48.1-48.1M227.1 188l58.9-34M223.7 179.8l65.7-17.6M222.5 171h68M223.7 162.2l65.7 17.6M227.1 154l58.9 34M232.5 147l48.1 48.1M239.5 141.6l34 58.9M247.7 138.2l17.6 65.7"/></g>` },
BR: { viewBox: "0 0 513 342", inner: `<path fill="#009b3a" d="M0 0h513v342H0z"/><path fill="#fedf00" d="m256.5 19.3 204.9 151.4L256.5 322 50.6 170.7z"/><circle fill="#FFF" cx="256.5" cy="171" r="80.4"/><path fill="#002776" d="M215.9 165.7c-13.9 0-27.4 2.1-40.1 6 .6 43.9 36.3 79.3 80.3 79.3 27.2 0 51.3-13.6 65.8-34.3-24.9-31-63.2-51-106-51zM334.9 186c.9-5 1.5-10.1 1.5-15.4 0-44.4-36-80.4-80.4-80.4-33.1 0-61.5 20.1-73.9 48.6 10.9-2.2 22.1-3.4 33.6-3.4 46.8.1 89 19.5 119.2 50.6z"/>` },
CA: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><g fill="red"><path d="M0 0h142v342H0zM371 0h142v342H371zM306.5 206l50.4-25.2-25.2-12.6V143l-50.4 25.2 25.2-50.4h-25.2L256.1 80l-25.2 37.8h-25.2l25.2 50.4-50.4-25.2v25.2l-25.2 12.6 50.4 25.2-12.6 25.2h50.4V269h25.2v-37.8h50.4z"/></g>` },
AU: { viewBox: "0 0 513 342", inner: `<path fill="#00008b" d="M0 0h513v342H0z"/><g fill="#FFF"><path d="m188 212.6 11 22.9 24.7-5.7-11 22.8 19.9 15.8-24.8 5.6.1 25.4-19.9-15.9-19.8 15.9.1-25.4-24.8-5.6 19.9-15.8-11.1-22.8 24.8 5.7zM385.9 241.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6v12.2l-9.4-7.6-9.5 7.6.1-12.2-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM337.3 125.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.9-2.7 9.5-7.5-5.3-10.9L332 136zM385.9 58.9l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.8-2.7 9.5-7.5-5.3-10.9 11.8 2.7zM428.4 108.6l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6V150l-9.4-7.6-9.5 7.6v-12.2l-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM398 166.5l4.1 12.7h13.3l-10.8 7.8 4.2 12.7-10.8-7.9-10.8 7.9 4.1-12.7-10.7-7.8h13.3z"/></g><path fill="#00008b" d="M0 0h256.5v171H0z"/><g fill="#FFF"><path d="M256.5 0v30.6l-45.3 25.2h45.3v59.4h-59.2l59.2 32.9V171h-26.7l-73.7-40.9V171h-55.7v-48.7L12.8 171H0v-30.6l45.3-25.2H0V55.8h59.2L0 22.9V0h26.7l73.7 40.9V0h55.7v48.7L243.7 0z"/><path d="M156.1 115.2 256.5 171v-15.8l-72-40zM100.4 55.8 0 0v15.8l72 40z"/></g><g fill="red"><path d="M144.3 0h-32.1v69.5H0v32h112.2V171h32.1v-69.5h112.2v-32H144.3z"/><path d="M156.1 115.2 256.5 171v-15.8l-72-40zM72 115.2l-72 40V171l100.4-55.8zM100.4 55.8 0 0v15.8l72 40zM184.5 55.8l72-40V0L156.1 55.8z"/></g>` },
MX: { viewBox: "0 0 513 342", inner: `<path fill="#dc2339" d="M342 0H0v341.3h513V0z"/><path fill="#11865d" d="M0 0h171v342H0z"/><path fill="#FFF" d="M171 0h171v342H171z"/><path fill="#8C9157" d="M195.8 171.2c0 21.6 11.5 41.7 30.3 52.5 5.8 3.4 13.2 1.4 16.6-4.4 3.4-5.8 1.4-13.2-4.4-16.6-11.3-6.5-18.2-18.5-18.2-31.5 0-6.7-5.4-12.1-12.1-12.1-6.7 0-12.2 5.4-12.2 12.1zm93.4 51.1c17.5-11.1 28-30.4 28-51.1 0-6.7-5.4-12.1-12.1-12.1s-12.1 5.4-12.1 12.1c0 12.4-6.3 24-16.8 30.7-5.7 3.5-7.5 10.9-4.1 16.7s10.9 7.5 16.7 4.1c0-.2.2-.3.4-.4z"/><ellipse fill="#C59262" cx="256.5" cy="159.1" rx="24.3" ry="36.4"/>` },
KR: { viewBox: "0 0 513 342", inner: `<path fill="#FFF" d="M0 0h513v342H0z"/><g stroke="#000" stroke-width="14.25"><path d="M84.6 107.8 132 36.6m-29.7 83 47.4-71.1m-29.6 83 47.4-71.1M345.4 281.7l47.4-71.1m-29.6 82.9 47.4-71.1m-29.6 83 47.4-71.1M132 305.4l-47.4-71.1m65.2 59.2-47.4-71.1m65.2 59.3-47.4-71.1M392.8 131.5l-47.4-71.1m65.2 59.2-47.4-71.1m65.2 59.3L381 36.6"/></g><g stroke="#FFF" stroke-width="12.5"><path d="m357.3 238.2 59.3 39.5M117.2 263.9 135 252m222.3-148.2L378 90m17.8-11.9 17.8-11.9"/></g><circle fill="#CA163A" cx="256.5" cy="171" r="85.5"/><path fill="#0E4896" d="M185.3 123.6c-13.1 19.6-7.8 46.2 11.9 59.3s46.2 7.8 59.3-11.9 39.6-24.9 59.3-11.9c19.6 13.1 24.9 39.6 11.9 59.3-26.2 39.3-79.3 49.9-118.6 23.7s-49.9-79.3-23.8-118.5z"/>` },
}
src/reui/phone-input/index.ts
export { default as PhoneInput } from "./PhoneInput.vue"
export { default as PhoneInputCountrySelect } from "./PhoneInputCountrySelect.vue"
export { default as PhoneInputFlag } from "./PhoneInputFlag.vue"
export { countries, type CountryEntry } from "./countries-data"
export { flags, type FlagSvgData } from "./flags-data"
export {
PhoneInputContextKey,
type PhoneInputContextValue,
type PhoneInputSize,
} from "./context"
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/phone-input.jsonЗависимости реестра
npm-зависимости
- libphonenumber-js
- reka-ui
Источник: порт из ReUI (Keenthemes, MIT)