@marianmeres/stuic 3.23.3 → 3.24.0
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/dist/components/Avatar/Avatar.svelte +22 -14
- package/dist/components/Avatar/Avatar.svelte.d.ts +1 -0
- package/dist/components/Checkout/CheckoutAddressForm.svelte +12 -6
- package/dist/components/Checkout/CheckoutAddressForm.svelte.d.ts +3 -0
- package/dist/components/Checkout/CheckoutGuestForm.svelte +8 -2
- package/dist/components/Checkout/CheckoutGuestForm.svelte.d.ts +3 -0
- package/dist/components/DropdownMenu/index.css +2 -2
- package/dist/components/Input/FieldPhoneNumber.svelte +338 -0
- package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +62 -0
- package/dist/components/Input/_internal/PhonePrefixPicker.svelte +118 -0
- package/dist/components/Input/_internal/PhonePrefixPicker.svelte.d.ts +15 -0
- package/dist/components/Input/_internal/countries.d.ts +20 -0
- package/dist/components/Input/_internal/countries.js +249 -0
- package/dist/components/Input/index.css +42 -0
- package/dist/components/Input/index.d.ts +3 -0
- package/dist/components/Input/index.js +3 -0
- package/dist/components/Input/phone-validation.d.ts +10 -0
- package/dist/components/Input/phone-validation.js +22 -0
- package/package.json +9 -4
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
autoColor?: boolean;
|
|
34
34
|
/** CSS class override */
|
|
35
35
|
class?: string;
|
|
36
|
+
classInner?: string;
|
|
36
37
|
/** Bindable element reference */
|
|
37
38
|
el?: HTMLDivElement | HTMLButtonElement;
|
|
38
39
|
}
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
textColor,
|
|
58
59
|
autoColor = false,
|
|
59
60
|
class: classProp,
|
|
61
|
+
classInner,
|
|
60
62
|
el = $bindable(),
|
|
61
63
|
}: Props = $props();
|
|
62
64
|
|
|
@@ -187,6 +189,8 @@
|
|
|
187
189
|
)
|
|
188
190
|
);
|
|
189
191
|
|
|
192
|
+
let _classInner = $derived(twMerge("inline-block", classInner));
|
|
193
|
+
|
|
190
194
|
function handleImageError() {
|
|
191
195
|
imageError = true;
|
|
192
196
|
}
|
|
@@ -202,13 +206,15 @@
|
|
|
202
206
|
data-size={isPresetSize(size) ? size : undefined}
|
|
203
207
|
data-interactive="true"
|
|
204
208
|
>
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
|
|
209
|
+
<span class={_classInner}>
|
|
210
|
+
{#if renderMode === "photo"}
|
|
211
|
+
<img {src} {alt} class="size-full object-cover" onerror={handleImageError} />
|
|
212
|
+
{:else if renderMode === "initials"}
|
|
213
|
+
{fallbackInitials}
|
|
214
|
+
{:else}
|
|
215
|
+
{@html iconToRender({ size: iconSize })}
|
|
216
|
+
{/if}
|
|
217
|
+
</span>
|
|
212
218
|
</button>
|
|
213
219
|
{:else}
|
|
214
220
|
<div
|
|
@@ -217,12 +223,14 @@
|
|
|
217
223
|
{style}
|
|
218
224
|
data-size={isPresetSize(size) ? size : undefined}
|
|
219
225
|
>
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
226
|
+
<span class={_classInner}>
|
|
227
|
+
{#if renderMode === "photo"}
|
|
228
|
+
<img {src} {alt} class="size-full object-cover" onerror={handleImageError} />
|
|
229
|
+
{:else if renderMode === "initials"}
|
|
230
|
+
{fallbackInitials}
|
|
231
|
+
{:else}
|
|
232
|
+
{@html iconToRender({ size: iconSize })}
|
|
233
|
+
{/if}
|
|
234
|
+
</span>
|
|
227
235
|
</div>
|
|
228
236
|
{/if}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
CheckoutAddressData,
|
|
7
7
|
CheckoutValidationError,
|
|
8
8
|
} from "./_internal/checkout-types.js";
|
|
9
|
+
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
9
10
|
|
|
10
11
|
export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
|
|
11
12
|
/** Bindable address data. Default: createEmptyAddress() */
|
|
@@ -59,6 +60,9 @@
|
|
|
59
60
|
]
|
|
60
61
|
>;
|
|
61
62
|
|
|
63
|
+
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
64
|
+
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
65
|
+
|
|
62
66
|
t?: TranslateFn;
|
|
63
67
|
unstyled?: boolean;
|
|
64
68
|
class?: string;
|
|
@@ -71,6 +75,8 @@
|
|
|
71
75
|
import { t_default } from "./_internal/checkout-i18n-defaults.js";
|
|
72
76
|
import { createEmptyAddress } from "./_internal/checkout-utils.js";
|
|
73
77
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
78
|
+
import FieldPhoneNumber from "../Input/FieldPhoneNumber.svelte";
|
|
79
|
+
import { validatePhoneNumber } from "../Input/phone-validation.js";
|
|
74
80
|
|
|
75
81
|
const DEFAULT_REQUIRED = ["name", "street", "city", "postal_code", "country"];
|
|
76
82
|
|
|
@@ -81,6 +87,7 @@
|
|
|
81
87
|
fields,
|
|
82
88
|
requiredFields = DEFAULT_REQUIRED,
|
|
83
89
|
countryField,
|
|
90
|
+
phoneFieldProps,
|
|
84
91
|
t: tProp,
|
|
85
92
|
unstyled = false,
|
|
86
93
|
class: classProp,
|
|
@@ -229,21 +236,20 @@
|
|
|
229
236
|
|
|
230
237
|
<!-- Phone (full width, block label) -->
|
|
231
238
|
{#if fields?.phone !== false}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
<FieldPhoneNumber
|
|
240
|
+
value={address.phone ?? ""}
|
|
241
|
+
onChange={(v) => { address.phone = v; }}
|
|
235
242
|
label={t("checkout.address.phone_label")}
|
|
236
|
-
type="tel"
|
|
237
243
|
placeholder={t("checkout.address.phone_placeholder")}
|
|
238
244
|
required={isRequired("phone")}
|
|
239
245
|
name="{label}-phone"
|
|
240
|
-
id="{label}-phone"
|
|
241
246
|
labelLeftBreakpoint={0}
|
|
242
247
|
validate={{
|
|
243
248
|
customValidator(val) {
|
|
244
|
-
return fieldError("phone") || "";
|
|
249
|
+
return fieldError("phone") || validatePhoneNumber(val) || "";
|
|
245
250
|
},
|
|
246
251
|
}}
|
|
252
|
+
{...phoneFieldProps}
|
|
247
253
|
/>
|
|
248
254
|
{/if}
|
|
249
255
|
</fieldset>
|
|
@@ -2,6 +2,7 @@ import type { Snippet } from "svelte";
|
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
4
|
import type { CheckoutAddressData, CheckoutValidationError } from "./_internal/checkout-types.js";
|
|
5
|
+
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
5
6
|
export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
|
|
6
7
|
/** Bindable address data. Default: createEmptyAddress() */
|
|
7
8
|
address?: CheckoutAddressData;
|
|
@@ -46,6 +47,8 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
|
|
|
46
47
|
id: string;
|
|
47
48
|
}
|
|
48
49
|
]>;
|
|
50
|
+
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
51
|
+
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
49
52
|
t?: TranslateFn;
|
|
50
53
|
unstyled?: boolean;
|
|
51
54
|
class?: string;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
CheckoutValidationError,
|
|
8
8
|
} from "./_internal/checkout-types.js";
|
|
9
9
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
10
|
+
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
10
11
|
|
|
11
12
|
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
12
13
|
/** Bindable form data. Default: createEmptyCustomerFormData() */
|
|
@@ -40,6 +41,9 @@
|
|
|
40
41
|
vat_number?: boolean;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
45
|
+
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
46
|
+
|
|
43
47
|
/** Override the CTA button label. Takes precedence over i18n. */
|
|
44
48
|
submitLabel?: string;
|
|
45
49
|
|
|
@@ -74,6 +78,7 @@
|
|
|
74
78
|
} from "./_internal/checkout-utils.js";
|
|
75
79
|
import Button from "../Button/Button.svelte";
|
|
76
80
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
81
|
+
import FieldPhoneNumber from "../Input/FieldPhoneNumber.svelte";
|
|
77
82
|
|
|
78
83
|
let {
|
|
79
84
|
formData = $bindable(createEmptyCustomerFormData()),
|
|
@@ -84,6 +89,7 @@
|
|
|
84
89
|
showB2bFields = true,
|
|
85
90
|
b2bExpanded = false,
|
|
86
91
|
fields,
|
|
92
|
+
phoneFieldProps,
|
|
87
93
|
submitLabel,
|
|
88
94
|
submittingLabel,
|
|
89
95
|
submitButton,
|
|
@@ -195,13 +201,13 @@
|
|
|
195
201
|
<!-- Phone -->
|
|
196
202
|
{#if fields?.phone !== false}
|
|
197
203
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
198
|
-
<
|
|
204
|
+
<FieldPhoneNumber
|
|
199
205
|
bind:value={formData.phone}
|
|
200
206
|
label={t("checkout.guest.phone_label")}
|
|
201
|
-
type="tel"
|
|
202
207
|
placeholder={t("checkout.guest.phone_placeholder")}
|
|
203
208
|
name="checkout-guest-phone"
|
|
204
209
|
labelLeftBreakpoint={0}
|
|
210
|
+
{...phoneFieldProps}
|
|
205
211
|
/>
|
|
206
212
|
{/if}
|
|
207
213
|
|
|
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from "svelte/elements";
|
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
4
|
import type { CheckoutCustomerFormData, CheckoutValidationError } from "./_internal/checkout-types.js";
|
|
5
5
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
6
|
+
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
6
7
|
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
7
8
|
/** Bindable form data. Default: createEmptyCustomerFormData() */
|
|
8
9
|
formData?: CheckoutCustomerFormData;
|
|
@@ -28,6 +29,8 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
28
29
|
tax_id?: boolean;
|
|
29
30
|
vat_number?: boolean;
|
|
30
31
|
};
|
|
32
|
+
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
33
|
+
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
31
34
|
/** Override the CTA button label. Takes precedence over i18n. */
|
|
32
35
|
submitLabel?: string;
|
|
33
36
|
/** Override the CTA button label while submitting. */
|
|
@@ -254,12 +254,12 @@
|
|
|
254
254
|
|
|
255
255
|
.stuic-dropdown-menu-search {
|
|
256
256
|
position: sticky;
|
|
257
|
-
top:
|
|
257
|
+
top: calc(var(--stuic-dropdown-menu-padding) * -1);
|
|
258
258
|
z-index: 10;
|
|
259
259
|
display: flex;
|
|
260
260
|
align-items: center;
|
|
261
261
|
gap: calc(var(--spacing) * 1);
|
|
262
|
-
padding: calc(var(--spacing) *
|
|
262
|
+
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
|
|
263
263
|
background: var(--stuic-dropdown-menu-search-bg);
|
|
264
264
|
border-bottom: 1px solid var(--stuic-dropdown-menu-search-border);
|
|
265
265
|
margin: calc(var(--stuic-dropdown-menu-padding) * -1);
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
4
|
+
import type { TranslateFn } from "../../types.js";
|
|
5
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
6
|
+
|
|
7
|
+
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
8
|
+
|
|
9
|
+
export interface Props extends Record<string, any> {
|
|
10
|
+
input?: HTMLInputElement;
|
|
11
|
+
/** Full phone number string, e.g. "+421905123456". Bindable. */
|
|
12
|
+
value?: string;
|
|
13
|
+
/** Selected country ISO code, e.g. "SK". Bindable. */
|
|
14
|
+
country?: string;
|
|
15
|
+
/** The dial code part with leading +, e.g. "+421". Bindable (read-derived). */
|
|
16
|
+
dialCode?: string;
|
|
17
|
+
/** The local number part, e.g. "905123456". Bindable. */
|
|
18
|
+
localNumber?: string;
|
|
19
|
+
/** ISO code for initial country selection when value is empty. */
|
|
20
|
+
defaultCountry?: string;
|
|
21
|
+
/** Show country flag emoji. Default: true. */
|
|
22
|
+
flags?: boolean;
|
|
23
|
+
/** Filtered list of countries to show (ISO codes). If undefined, show all. */
|
|
24
|
+
countries?: string[];
|
|
25
|
+
/** ISO codes of countries to pin at the top of the dropdown list. */
|
|
26
|
+
preferredCountries?: string[];
|
|
27
|
+
/** Placeholder for the tel input. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Name attribute for hidden input (form submission). */
|
|
30
|
+
name?: string;
|
|
31
|
+
//
|
|
32
|
+
label?: SnippetWithId | THC;
|
|
33
|
+
description?: SnippetWithId | THC;
|
|
34
|
+
class?: string;
|
|
35
|
+
id?: string;
|
|
36
|
+
tabindex?: number;
|
|
37
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
38
|
+
required?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
41
|
+
labelAfter?: SnippetWithId | THC;
|
|
42
|
+
inputAfter?: SnippetWithId | THC;
|
|
43
|
+
inputBelow?: SnippetWithId | THC;
|
|
44
|
+
below?: SnippetWithId | THC;
|
|
45
|
+
labelLeft?: boolean;
|
|
46
|
+
labelLeftWidth?: "normal" | "wide";
|
|
47
|
+
labelLeftBreakpoint?: number;
|
|
48
|
+
//
|
|
49
|
+
classInput?: string;
|
|
50
|
+
classLabel?: string;
|
|
51
|
+
classLabelBox?: string;
|
|
52
|
+
classInputBox?: string;
|
|
53
|
+
classInputBoxWrap?: string;
|
|
54
|
+
classInputBoxWrapInvalid?: string;
|
|
55
|
+
classDescBox?: string;
|
|
56
|
+
classBelowBox?: string;
|
|
57
|
+
classPrefixTrigger?: string;
|
|
58
|
+
classPrefixDropdown?: string;
|
|
59
|
+
style?: string;
|
|
60
|
+
//
|
|
61
|
+
t?: TranslateFn;
|
|
62
|
+
onChange?: (value: string) => void;
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<script lang="ts">
|
|
67
|
+
import { tick } from "svelte";
|
|
68
|
+
import {
|
|
69
|
+
validate as validateAction,
|
|
70
|
+
type ValidationResult,
|
|
71
|
+
} from "../../actions/validate.svelte.js";
|
|
72
|
+
import { getId } from "../../utils/get-id.js";
|
|
73
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
74
|
+
import InputWrap from "./_internal/InputWrap.svelte";
|
|
75
|
+
import PhonePrefixPicker from "./_internal/PhonePrefixPicker.svelte";
|
|
76
|
+
import {
|
|
77
|
+
COUNTRIES,
|
|
78
|
+
ISO_MAP,
|
|
79
|
+
DIAL_CODES_DESC,
|
|
80
|
+
DIAL_CODE_MAP,
|
|
81
|
+
type Country,
|
|
82
|
+
} from "./_internal/countries.js";
|
|
83
|
+
import { validatePhoneNumber } from "./phone-validation.js";
|
|
84
|
+
|
|
85
|
+
let {
|
|
86
|
+
input = $bindable(),
|
|
87
|
+
value = $bindable(""),
|
|
88
|
+
country = $bindable(""),
|
|
89
|
+
dialCode = $bindable(""),
|
|
90
|
+
localNumber = $bindable(""),
|
|
91
|
+
defaultCountry,
|
|
92
|
+
flags = true,
|
|
93
|
+
countries: allowedCountries,
|
|
94
|
+
preferredCountries,
|
|
95
|
+
placeholder,
|
|
96
|
+
name,
|
|
97
|
+
//
|
|
98
|
+
label,
|
|
99
|
+
description,
|
|
100
|
+
class: classProp,
|
|
101
|
+
id = getId(),
|
|
102
|
+
tabindex = 0,
|
|
103
|
+
renderSize = "md",
|
|
104
|
+
required = false,
|
|
105
|
+
disabled = false,
|
|
106
|
+
validate,
|
|
107
|
+
//
|
|
108
|
+
labelAfter,
|
|
109
|
+
inputAfter,
|
|
110
|
+
inputBelow,
|
|
111
|
+
below,
|
|
112
|
+
//
|
|
113
|
+
labelLeft = false,
|
|
114
|
+
labelLeftWidth = "normal",
|
|
115
|
+
labelLeftBreakpoint = 480,
|
|
116
|
+
//
|
|
117
|
+
classInput,
|
|
118
|
+
classLabel,
|
|
119
|
+
classLabelBox,
|
|
120
|
+
classInputBox,
|
|
121
|
+
classInputBoxWrap,
|
|
122
|
+
classInputBoxWrapInvalid,
|
|
123
|
+
classDescBox,
|
|
124
|
+
classBelowBox,
|
|
125
|
+
classPrefixTrigger,
|
|
126
|
+
classPrefixDropdown,
|
|
127
|
+
style,
|
|
128
|
+
//
|
|
129
|
+
t: tProp,
|
|
130
|
+
onChange,
|
|
131
|
+
...rest
|
|
132
|
+
}: Props = $props();
|
|
133
|
+
|
|
134
|
+
let hiddenInputEl: HTMLInputElement | undefined = $state();
|
|
135
|
+
|
|
136
|
+
// Validation state
|
|
137
|
+
let validation: ValidationResult | undefined = $state();
|
|
138
|
+
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
139
|
+
|
|
140
|
+
// Filtered country list
|
|
141
|
+
let countryList = $derived.by(() => {
|
|
142
|
+
if (!allowedCountries) return COUNTRIES;
|
|
143
|
+
const set = new Set(allowedCountries.map((c) => c.toUpperCase()));
|
|
144
|
+
return COUNTRIES.filter((c) => set.has(c.iso));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Selected country object (initialize once from defaultCountry prop)
|
|
148
|
+
const _initCountry = defaultCountry;
|
|
149
|
+
let selectedCountry: Country | undefined = $state(
|
|
150
|
+
_initCountry ? ISO_MAP.get(_initCountry.toUpperCase()) : undefined
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Internal local number (for controlled input)
|
|
154
|
+
let _localNumber = $state("");
|
|
155
|
+
|
|
156
|
+
// Flag to prevent circular sync
|
|
157
|
+
let _syncing = false;
|
|
158
|
+
|
|
159
|
+
// Parse value into parts when value changes externally
|
|
160
|
+
$effect(() => {
|
|
161
|
+
const v = value;
|
|
162
|
+
const defCountry = defaultCountry;
|
|
163
|
+
if (_syncing) return;
|
|
164
|
+
|
|
165
|
+
if (!v) {
|
|
166
|
+
if (!selectedCountry && defCountry) {
|
|
167
|
+
selectedCountry = ISO_MAP.get(defCountry.toUpperCase());
|
|
168
|
+
}
|
|
169
|
+
_localNumber = "";
|
|
170
|
+
syncDerived();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (v.startsWith("+")) {
|
|
175
|
+
const digits = v.slice(1);
|
|
176
|
+
for (const code of DIAL_CODES_DESC) {
|
|
177
|
+
if (digits.startsWith(code)) {
|
|
178
|
+
const matched = DIAL_CODE_MAP.get(code);
|
|
179
|
+
if (matched?.length) {
|
|
180
|
+
// Preserve current selection if it shares the same dial code
|
|
181
|
+
if (!selectedCountry || selectedCountry.dialCode !== code) {
|
|
182
|
+
selectedCountry = matched[0];
|
|
183
|
+
}
|
|
184
|
+
_localNumber = digits.slice(code.length);
|
|
185
|
+
syncDerived();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fallback: treat entire value as local number
|
|
193
|
+
_localNumber = v.replace(/^\+/, "");
|
|
194
|
+
syncDerived();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Sync convenience bindable props
|
|
198
|
+
function syncDerived() {
|
|
199
|
+
dialCode = selectedCountry ? `+${selectedCountry.dialCode}` : "";
|
|
200
|
+
country = selectedCountry?.iso ?? "";
|
|
201
|
+
localNumber = _localNumber;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Compose full value from parts
|
|
205
|
+
function composeValue(): string {
|
|
206
|
+
const digits = _localNumber.replace(/\D/g, "");
|
|
207
|
+
if (!digits) return "";
|
|
208
|
+
if (selectedCountry) return `+${selectedCountry.dialCode}${digits}`;
|
|
209
|
+
return digits;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Sync internal -> external value
|
|
213
|
+
function syncToValue() {
|
|
214
|
+
_syncing = true;
|
|
215
|
+
value = composeValue();
|
|
216
|
+
syncDerived();
|
|
217
|
+
tick().then(() => {
|
|
218
|
+
hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
219
|
+
_syncing = false;
|
|
220
|
+
});
|
|
221
|
+
onChange?.(value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Handle country selection from picker
|
|
225
|
+
function onCountrySelect(c: Country) {
|
|
226
|
+
selectedCountry = c;
|
|
227
|
+
syncToValue();
|
|
228
|
+
input?.focus();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle paste: detect international prefix
|
|
232
|
+
function handlePaste(e: ClipboardEvent) {
|
|
233
|
+
const pasted = e.clipboardData?.getData("text") ?? "";
|
|
234
|
+
if (pasted.startsWith("+") || pasted.startsWith("00")) {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
const normalized = pasted.startsWith("00") ? "+" + pasted.slice(2) : pasted;
|
|
237
|
+
const digits = normalized.slice(1).replace(/\D/g, "");
|
|
238
|
+
for (const code of DIAL_CODES_DESC) {
|
|
239
|
+
if (digits.startsWith(code)) {
|
|
240
|
+
const matched = DIAL_CODE_MAP.get(code);
|
|
241
|
+
if (matched?.length) {
|
|
242
|
+
// Preserve existing selection if matching dial code
|
|
243
|
+
if (!selectedCountry || selectedCountry.dialCode !== code) {
|
|
244
|
+
selectedCountry = matched[0];
|
|
245
|
+
}
|
|
246
|
+
_localNumber = digits.slice(code.length);
|
|
247
|
+
syncToValue();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// No recognized prefix
|
|
253
|
+
_localNumber = digits;
|
|
254
|
+
syncToValue();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Handle local number input change
|
|
259
|
+
function handleInput(e: Event) {
|
|
260
|
+
_localNumber = (e.currentTarget as HTMLInputElement).value;
|
|
261
|
+
syncToValue();
|
|
262
|
+
}
|
|
263
|
+
</script>
|
|
264
|
+
|
|
265
|
+
<InputWrap
|
|
266
|
+
{id}
|
|
267
|
+
{label}
|
|
268
|
+
{description}
|
|
269
|
+
{labelAfter}
|
|
270
|
+
{inputAfter}
|
|
271
|
+
{inputBelow}
|
|
272
|
+
{below}
|
|
273
|
+
{required}
|
|
274
|
+
{disabled}
|
|
275
|
+
size={renderSize}
|
|
276
|
+
class={classProp}
|
|
277
|
+
{labelLeft}
|
|
278
|
+
{labelLeftWidth}
|
|
279
|
+
{labelLeftBreakpoint}
|
|
280
|
+
{classLabel}
|
|
281
|
+
{classLabelBox}
|
|
282
|
+
{classInputBox}
|
|
283
|
+
{classInputBoxWrap}
|
|
284
|
+
{classInputBoxWrapInvalid}
|
|
285
|
+
{classDescBox}
|
|
286
|
+
{classBelowBox}
|
|
287
|
+
{validation}
|
|
288
|
+
{style}
|
|
289
|
+
>
|
|
290
|
+
{#snippet inputBefore()}
|
|
291
|
+
<PhonePrefixPicker
|
|
292
|
+
{selectedCountry}
|
|
293
|
+
{countryList}
|
|
294
|
+
{preferredCountries}
|
|
295
|
+
{flags}
|
|
296
|
+
{disabled}
|
|
297
|
+
classTrigger={classPrefixTrigger}
|
|
298
|
+
classDropdown={classPrefixDropdown}
|
|
299
|
+
onSelect={onCountrySelect}
|
|
300
|
+
t={tProp}
|
|
301
|
+
/>
|
|
302
|
+
{/snippet}
|
|
303
|
+
|
|
304
|
+
<input
|
|
305
|
+
bind:this={input}
|
|
306
|
+
type="tel"
|
|
307
|
+
inputmode="tel"
|
|
308
|
+
value={_localNumber}
|
|
309
|
+
oninput={handleInput}
|
|
310
|
+
onpaste={handlePaste}
|
|
311
|
+
{id}
|
|
312
|
+
{tabindex}
|
|
313
|
+
{required}
|
|
314
|
+
{disabled}
|
|
315
|
+
{placeholder}
|
|
316
|
+
class={twMerge(classInput)}
|
|
317
|
+
{...rest}
|
|
318
|
+
/>
|
|
319
|
+
</InputWrap>
|
|
320
|
+
|
|
321
|
+
<!-- Hidden input for form submission and validation -->
|
|
322
|
+
{#if name}
|
|
323
|
+
<input
|
|
324
|
+
type="hidden"
|
|
325
|
+
{name}
|
|
326
|
+
value={value ?? ""}
|
|
327
|
+
bind:this={hiddenInputEl}
|
|
328
|
+
use:validateAction={() => {
|
|
329
|
+
const customOpts = typeof validate === "object" && validate ? validate : {};
|
|
330
|
+
return {
|
|
331
|
+
enabled: validate !== false,
|
|
332
|
+
...customOpts,
|
|
333
|
+
customValidator: customOpts.customValidator ?? validatePhoneNumber,
|
|
334
|
+
setValidationResult,
|
|
335
|
+
};
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
{/if}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
type SnippetWithId = Snippet<[{
|
|
6
|
+
id: string;
|
|
7
|
+
}]>;
|
|
8
|
+
export interface Props extends Record<string, any> {
|
|
9
|
+
input?: HTMLInputElement;
|
|
10
|
+
/** Full phone number string, e.g. "+421905123456". Bindable. */
|
|
11
|
+
value?: string;
|
|
12
|
+
/** Selected country ISO code, e.g. "SK". Bindable. */
|
|
13
|
+
country?: string;
|
|
14
|
+
/** The dial code part with leading +, e.g. "+421". Bindable (read-derived). */
|
|
15
|
+
dialCode?: string;
|
|
16
|
+
/** The local number part, e.g. "905123456". Bindable. */
|
|
17
|
+
localNumber?: string;
|
|
18
|
+
/** ISO code for initial country selection when value is empty. */
|
|
19
|
+
defaultCountry?: string;
|
|
20
|
+
/** Show country flag emoji. Default: true. */
|
|
21
|
+
flags?: boolean;
|
|
22
|
+
/** Filtered list of countries to show (ISO codes). If undefined, show all. */
|
|
23
|
+
countries?: string[];
|
|
24
|
+
/** ISO codes of countries to pin at the top of the dropdown list. */
|
|
25
|
+
preferredCountries?: string[];
|
|
26
|
+
/** Placeholder for the tel input. */
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
/** Name attribute for hidden input (form submission). */
|
|
29
|
+
name?: string;
|
|
30
|
+
label?: SnippetWithId | THC;
|
|
31
|
+
description?: SnippetWithId | THC;
|
|
32
|
+
class?: string;
|
|
33
|
+
id?: string;
|
|
34
|
+
tabindex?: number;
|
|
35
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
36
|
+
required?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
39
|
+
labelAfter?: SnippetWithId | THC;
|
|
40
|
+
inputAfter?: SnippetWithId | THC;
|
|
41
|
+
inputBelow?: SnippetWithId | THC;
|
|
42
|
+
below?: SnippetWithId | THC;
|
|
43
|
+
labelLeft?: boolean;
|
|
44
|
+
labelLeftWidth?: "normal" | "wide";
|
|
45
|
+
labelLeftBreakpoint?: number;
|
|
46
|
+
classInput?: string;
|
|
47
|
+
classLabel?: string;
|
|
48
|
+
classLabelBox?: string;
|
|
49
|
+
classInputBox?: string;
|
|
50
|
+
classInputBoxWrap?: string;
|
|
51
|
+
classInputBoxWrapInvalid?: string;
|
|
52
|
+
classDescBox?: string;
|
|
53
|
+
classBelowBox?: string;
|
|
54
|
+
classPrefixTrigger?: string;
|
|
55
|
+
classPrefixDropdown?: string;
|
|
56
|
+
style?: string;
|
|
57
|
+
t?: TranslateFn;
|
|
58
|
+
onChange?: (value: string) => void;
|
|
59
|
+
}
|
|
60
|
+
declare const FieldPhoneNumber: import("svelte").Component<Props, {}, "value" | "input" | "country" | "dialCode" | "localNumber">;
|
|
61
|
+
type FieldPhoneNumber = ReturnType<typeof FieldPhoneNumber>;
|
|
62
|
+
export default FieldPhoneNumber;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { twMerge } from "../../../utils/tw-merge.js";
|
|
3
|
+
import { iconChevronDown } from "../../../icons/index.js";
|
|
4
|
+
import DropdownMenu, {
|
|
5
|
+
type DropdownMenuActionItem,
|
|
6
|
+
type DropdownMenuItem,
|
|
7
|
+
type DropdownMenuSearchConfig,
|
|
8
|
+
} from "../../DropdownMenu/DropdownMenu.svelte";
|
|
9
|
+
import { type Country, ISO_MAP } from "./countries.js";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
selectedCountry?: Country;
|
|
13
|
+
countryList: Country[];
|
|
14
|
+
preferredCountries?: string[];
|
|
15
|
+
flags?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
classTrigger?: string;
|
|
18
|
+
classDropdown?: string;
|
|
19
|
+
onSelect: (country: Country) => void;
|
|
20
|
+
t?: (k: string, v?: any, f?: any) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
selectedCountry,
|
|
25
|
+
countryList,
|
|
26
|
+
preferredCountries,
|
|
27
|
+
flags = true,
|
|
28
|
+
disabled = false,
|
|
29
|
+
classTrigger,
|
|
30
|
+
classDropdown,
|
|
31
|
+
onSelect,
|
|
32
|
+
t,
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
let isOpen = $state(false);
|
|
36
|
+
|
|
37
|
+
function countryToItem(c: Country): DropdownMenuActionItem {
|
|
38
|
+
const prefix = flags ? `${c.flag} ` : "";
|
|
39
|
+
return {
|
|
40
|
+
type: "action",
|
|
41
|
+
id: c.iso,
|
|
42
|
+
label: `${prefix}${c.name} (+${c.dialCode})`,
|
|
43
|
+
onSelect: () => {
|
|
44
|
+
onSelect(c);
|
|
45
|
+
isOpen = false;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let items: DropdownMenuItem[] = $derived.by(() => {
|
|
51
|
+
const result: DropdownMenuItem[] = [];
|
|
52
|
+
const preferredSet = new Set(preferredCountries?.map((c) => c.toUpperCase()) ?? []);
|
|
53
|
+
|
|
54
|
+
if (preferredSet.size > 0) {
|
|
55
|
+
const preferred = countryList.filter((c) => preferredSet.has(c.iso));
|
|
56
|
+
preferred.forEach((c) => result.push(countryToItem(c)));
|
|
57
|
+
if (preferred.length > 0) {
|
|
58
|
+
result.push({ type: "divider" });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rest =
|
|
63
|
+
preferredSet.size > 0
|
|
64
|
+
? countryList.filter((c) => !preferredSet.has(c.iso))
|
|
65
|
+
: countryList;
|
|
66
|
+
rest.forEach((c) => result.push(countryToItem(c)));
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let searchConfig: DropdownMenuSearchConfig = $derived({
|
|
72
|
+
placeholder: t?.("phone_search_country") || "Search country...",
|
|
73
|
+
strategy: "prefix",
|
|
74
|
+
getContent: (item) => {
|
|
75
|
+
const c = ISO_MAP.get(String(item.id));
|
|
76
|
+
if (!c) return String(item.id);
|
|
77
|
+
return `${c.name} ${c.iso} +${c.dialCode} ${c.dialCode}`;
|
|
78
|
+
},
|
|
79
|
+
autoFocus: true,
|
|
80
|
+
noResultsMessage: t?.("phone_no_country_found") || "No country found",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let triggerText = $derived.by(() => {
|
|
84
|
+
if (!selectedCountry) return "+?";
|
|
85
|
+
const prefix = flags ? `${selectedCountry.flag} ` : "";
|
|
86
|
+
return `${prefix}+${selectedCountry.dialCode}`;
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<DropdownMenu
|
|
91
|
+
{items}
|
|
92
|
+
bind:isOpen
|
|
93
|
+
position="bottom-span-right"
|
|
94
|
+
search={searchConfig}
|
|
95
|
+
maxHeight="300px"
|
|
96
|
+
closeOnSelect
|
|
97
|
+
class="stuic-phone-prefix-picker"
|
|
98
|
+
classDropdown={twMerge("min-w-64", classDropdown)}
|
|
99
|
+
>
|
|
100
|
+
{#snippet trigger({ toggle, triggerProps })}
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class={twMerge(
|
|
104
|
+
"stuic-phone-prefix-trigger",
|
|
105
|
+
"flex items-center gap-1 shrink-0 px-2 whitespace-nowrap cursor-pointer",
|
|
106
|
+
classTrigger
|
|
107
|
+
)}
|
|
108
|
+
onclick={toggle}
|
|
109
|
+
{disabled}
|
|
110
|
+
{...triggerProps}
|
|
111
|
+
>
|
|
112
|
+
<span>{triggerText}</span>
|
|
113
|
+
<span class={twMerge("transition-transform duration-150", isOpen && "rotate-180")}>
|
|
114
|
+
{@html iconChevronDown({ size: 14 })}
|
|
115
|
+
</span>
|
|
116
|
+
</button>
|
|
117
|
+
{/snippet}
|
|
118
|
+
</DropdownMenu>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Country } from "./countries.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
selectedCountry?: Country;
|
|
4
|
+
countryList: Country[];
|
|
5
|
+
preferredCountries?: string[];
|
|
6
|
+
flags?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
classTrigger?: string;
|
|
9
|
+
classDropdown?: string;
|
|
10
|
+
onSelect: (country: Country) => void;
|
|
11
|
+
t?: (k: string, v?: any, f?: any) => string;
|
|
12
|
+
}
|
|
13
|
+
declare const PhonePrefixPicker: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type PhonePrefixPicker = ReturnType<typeof PhonePrefixPicker>;
|
|
15
|
+
export default PhonePrefixPicker;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Country {
|
|
2
|
+
/** ISO 3166-1 alpha-2 code, e.g. "SK" */
|
|
3
|
+
iso: string;
|
|
4
|
+
/** Country name in English, e.g. "Slovakia" */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Dial code without leading +, e.g. "421" */
|
|
7
|
+
dialCode: string;
|
|
8
|
+
/** Country flag emoji, e.g. "🇸🇰" */
|
|
9
|
+
flag: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const COUNTRIES: Country[];
|
|
12
|
+
/** Lookup map: ISO code -> Country */
|
|
13
|
+
export declare const ISO_MAP: Map<string, Country>;
|
|
14
|
+
/** Lookup map: dial code -> Country[]. Multiple countries can share a code (e.g. +1). */
|
|
15
|
+
export declare const DIAL_CODE_MAP: Map<string, Country[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Sorted list of unique dial codes (longest first) for prefix detection during paste.
|
|
18
|
+
* Longest-first ensures we match "+1684" (American Samoa) before "+1" (US).
|
|
19
|
+
*/
|
|
20
|
+
export declare const DIAL_CODES_DESC: string[];
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// prettier-ignore
|
|
2
|
+
export const COUNTRIES = [
|
|
3
|
+
{ iso: "AF", name: "Afghanistan", dialCode: "93", flag: "🇦🇫" },
|
|
4
|
+
{ iso: "AL", name: "Albania", dialCode: "355", flag: "🇦🇱" },
|
|
5
|
+
{ iso: "DZ", name: "Algeria", dialCode: "213", flag: "🇩🇿" },
|
|
6
|
+
{ iso: "AS", name: "American Samoa", dialCode: "1684", flag: "🇦🇸" },
|
|
7
|
+
{ iso: "AD", name: "Andorra", dialCode: "376", flag: "🇦🇩" },
|
|
8
|
+
{ iso: "AO", name: "Angola", dialCode: "244", flag: "🇦🇴" },
|
|
9
|
+
{ iso: "AI", name: "Anguilla", dialCode: "1264", flag: "🇦🇮" },
|
|
10
|
+
{ iso: "AG", name: "Antigua and Barbuda", dialCode: "1268", flag: "🇦🇬" },
|
|
11
|
+
{ iso: "AR", name: "Argentina", dialCode: "54", flag: "🇦🇷" },
|
|
12
|
+
{ iso: "AM", name: "Armenia", dialCode: "374", flag: "🇦🇲" },
|
|
13
|
+
{ iso: "AW", name: "Aruba", dialCode: "297", flag: "🇦🇼" },
|
|
14
|
+
{ iso: "AU", name: "Australia", dialCode: "61", flag: "🇦🇺" },
|
|
15
|
+
{ iso: "AT", name: "Austria", dialCode: "43", flag: "🇦🇹" },
|
|
16
|
+
{ iso: "AZ", name: "Azerbaijan", dialCode: "994", flag: "🇦🇿" },
|
|
17
|
+
{ iso: "BS", name: "Bahamas", dialCode: "1242", flag: "🇧🇸" },
|
|
18
|
+
{ iso: "BH", name: "Bahrain", dialCode: "973", flag: "🇧🇭" },
|
|
19
|
+
{ iso: "BD", name: "Bangladesh", dialCode: "880", flag: "🇧🇩" },
|
|
20
|
+
{ iso: "BB", name: "Barbados", dialCode: "1246", flag: "🇧🇧" },
|
|
21
|
+
{ iso: "BY", name: "Belarus", dialCode: "375", flag: "🇧🇾" },
|
|
22
|
+
{ iso: "BE", name: "Belgium", dialCode: "32", flag: "🇧🇪" },
|
|
23
|
+
{ iso: "BZ", name: "Belize", dialCode: "501", flag: "🇧🇿" },
|
|
24
|
+
{ iso: "BJ", name: "Benin", dialCode: "229", flag: "🇧🇯" },
|
|
25
|
+
{ iso: "BM", name: "Bermuda", dialCode: "1441", flag: "🇧🇲" },
|
|
26
|
+
{ iso: "BT", name: "Bhutan", dialCode: "975", flag: "🇧🇹" },
|
|
27
|
+
{ iso: "BO", name: "Bolivia", dialCode: "591", flag: "🇧🇴" },
|
|
28
|
+
{ iso: "BA", name: "Bosnia and Herzegovina", dialCode: "387", flag: "🇧🇦" },
|
|
29
|
+
{ iso: "BW", name: "Botswana", dialCode: "267", flag: "🇧🇼" },
|
|
30
|
+
{ iso: "BR", name: "Brazil", dialCode: "55", flag: "🇧🇷" },
|
|
31
|
+
{ iso: "IO", name: "British Indian Ocean Territory", dialCode: "246", flag: "🇮🇴" },
|
|
32
|
+
{ iso: "VG", name: "British Virgin Islands", dialCode: "1284", flag: "🇻🇬" },
|
|
33
|
+
{ iso: "BN", name: "Brunei", dialCode: "673", flag: "🇧🇳" },
|
|
34
|
+
{ iso: "BG", name: "Bulgaria", dialCode: "359", flag: "🇧🇬" },
|
|
35
|
+
{ iso: "BF", name: "Burkina Faso", dialCode: "226", flag: "🇧🇫" },
|
|
36
|
+
{ iso: "BI", name: "Burundi", dialCode: "257", flag: "🇧🇮" },
|
|
37
|
+
{ iso: "KH", name: "Cambodia", dialCode: "855", flag: "🇰🇭" },
|
|
38
|
+
{ iso: "CM", name: "Cameroon", dialCode: "237", flag: "🇨🇲" },
|
|
39
|
+
{ iso: "CA", name: "Canada", dialCode: "1", flag: "🇨🇦" },
|
|
40
|
+
{ iso: "CV", name: "Cape Verde", dialCode: "238", flag: "🇨🇻" },
|
|
41
|
+
{ iso: "KY", name: "Cayman Islands", dialCode: "1345", flag: "🇰🇾" },
|
|
42
|
+
{ iso: "CF", name: "Central African Republic", dialCode: "236", flag: "🇨🇫" },
|
|
43
|
+
{ iso: "TD", name: "Chad", dialCode: "235", flag: "🇹🇩" },
|
|
44
|
+
{ iso: "CL", name: "Chile", dialCode: "56", flag: "🇨🇱" },
|
|
45
|
+
{ iso: "CN", name: "China", dialCode: "86", flag: "🇨🇳" },
|
|
46
|
+
{ iso: "CO", name: "Colombia", dialCode: "57", flag: "🇨🇴" },
|
|
47
|
+
{ iso: "KM", name: "Comoros", dialCode: "269", flag: "🇰🇲" },
|
|
48
|
+
{ iso: "CK", name: "Cook Islands", dialCode: "682", flag: "🇨🇰" },
|
|
49
|
+
{ iso: "CR", name: "Costa Rica", dialCode: "506", flag: "🇨🇷" },
|
|
50
|
+
{ iso: "HR", name: "Croatia", dialCode: "385", flag: "🇭🇷" },
|
|
51
|
+
{ iso: "CU", name: "Cuba", dialCode: "53", flag: "🇨🇺" },
|
|
52
|
+
{ iso: "CW", name: "Curaçao", dialCode: "599", flag: "🇨🇼" },
|
|
53
|
+
{ iso: "CY", name: "Cyprus", dialCode: "357", flag: "🇨🇾" },
|
|
54
|
+
{ iso: "CZ", name: "Czech Republic", dialCode: "420", flag: "🇨🇿" },
|
|
55
|
+
{ iso: "CD", name: "Democratic Republic of the Congo", dialCode: "243", flag: "🇨🇩" },
|
|
56
|
+
{ iso: "DK", name: "Denmark", dialCode: "45", flag: "🇩🇰" },
|
|
57
|
+
{ iso: "DJ", name: "Djibouti", dialCode: "253", flag: "🇩🇯" },
|
|
58
|
+
{ iso: "DM", name: "Dominica", dialCode: "1767", flag: "🇩🇲" },
|
|
59
|
+
{ iso: "DO", name: "Dominican Republic", dialCode: "1809", flag: "🇩🇴" },
|
|
60
|
+
{ iso: "EC", name: "Ecuador", dialCode: "593", flag: "🇪🇨" },
|
|
61
|
+
{ iso: "EG", name: "Egypt", dialCode: "20", flag: "🇪🇬" },
|
|
62
|
+
{ iso: "SV", name: "El Salvador", dialCode: "503", flag: "🇸🇻" },
|
|
63
|
+
{ iso: "GQ", name: "Equatorial Guinea", dialCode: "240", flag: "🇬🇶" },
|
|
64
|
+
{ iso: "ER", name: "Eritrea", dialCode: "291", flag: "🇪🇷" },
|
|
65
|
+
{ iso: "EE", name: "Estonia", dialCode: "372", flag: "🇪🇪" },
|
|
66
|
+
{ iso: "SZ", name: "Eswatini", dialCode: "268", flag: "🇸🇿" },
|
|
67
|
+
{ iso: "ET", name: "Ethiopia", dialCode: "251", flag: "🇪🇹" },
|
|
68
|
+
{ iso: "FK", name: "Falkland Islands", dialCode: "500", flag: "🇫🇰" },
|
|
69
|
+
{ iso: "FO", name: "Faroe Islands", dialCode: "298", flag: "🇫🇴" },
|
|
70
|
+
{ iso: "FJ", name: "Fiji", dialCode: "679", flag: "🇫🇯" },
|
|
71
|
+
{ iso: "FI", name: "Finland", dialCode: "358", flag: "🇫🇮" },
|
|
72
|
+
{ iso: "FR", name: "France", dialCode: "33", flag: "🇫🇷" },
|
|
73
|
+
{ iso: "GF", name: "French Guiana", dialCode: "594", flag: "🇬🇫" },
|
|
74
|
+
{ iso: "PF", name: "French Polynesia", dialCode: "689", flag: "🇵🇫" },
|
|
75
|
+
{ iso: "GA", name: "Gabon", dialCode: "241", flag: "🇬🇦" },
|
|
76
|
+
{ iso: "GM", name: "Gambia", dialCode: "220", flag: "🇬🇲" },
|
|
77
|
+
{ iso: "GE", name: "Georgia", dialCode: "995", flag: "🇬🇪" },
|
|
78
|
+
{ iso: "DE", name: "Germany", dialCode: "49", flag: "🇩🇪" },
|
|
79
|
+
{ iso: "GH", name: "Ghana", dialCode: "233", flag: "🇬🇭" },
|
|
80
|
+
{ iso: "GI", name: "Gibraltar", dialCode: "350", flag: "🇬🇮" },
|
|
81
|
+
{ iso: "GR", name: "Greece", dialCode: "30", flag: "🇬🇷" },
|
|
82
|
+
{ iso: "GL", name: "Greenland", dialCode: "299", flag: "🇬🇱" },
|
|
83
|
+
{ iso: "GD", name: "Grenada", dialCode: "1473", flag: "🇬🇩" },
|
|
84
|
+
{ iso: "GP", name: "Guadeloupe", dialCode: "590", flag: "🇬🇵" },
|
|
85
|
+
{ iso: "GU", name: "Guam", dialCode: "1671", flag: "🇬🇺" },
|
|
86
|
+
{ iso: "GT", name: "Guatemala", dialCode: "502", flag: "🇬🇹" },
|
|
87
|
+
{ iso: "GN", name: "Guinea", dialCode: "224", flag: "🇬🇳" },
|
|
88
|
+
{ iso: "GW", name: "Guinea-Bissau", dialCode: "245", flag: "🇬🇼" },
|
|
89
|
+
{ iso: "GY", name: "Guyana", dialCode: "592", flag: "🇬🇾" },
|
|
90
|
+
{ iso: "HT", name: "Haiti", dialCode: "509", flag: "🇭🇹" },
|
|
91
|
+
{ iso: "HN", name: "Honduras", dialCode: "504", flag: "🇭🇳" },
|
|
92
|
+
{ iso: "HK", name: "Hong Kong", dialCode: "852", flag: "🇭🇰" },
|
|
93
|
+
{ iso: "HU", name: "Hungary", dialCode: "36", flag: "🇭🇺" },
|
|
94
|
+
{ iso: "IS", name: "Iceland", dialCode: "354", flag: "🇮🇸" },
|
|
95
|
+
{ iso: "IN", name: "India", dialCode: "91", flag: "🇮🇳" },
|
|
96
|
+
{ iso: "ID", name: "Indonesia", dialCode: "62", flag: "🇮🇩" },
|
|
97
|
+
{ iso: "IR", name: "Iran", dialCode: "98", flag: "🇮🇷" },
|
|
98
|
+
{ iso: "IQ", name: "Iraq", dialCode: "964", flag: "🇮🇶" },
|
|
99
|
+
{ iso: "IE", name: "Ireland", dialCode: "353", flag: "🇮🇪" },
|
|
100
|
+
{ iso: "IL", name: "Israel", dialCode: "972", flag: "🇮🇱" },
|
|
101
|
+
{ iso: "IT", name: "Italy", dialCode: "39", flag: "🇮🇹" },
|
|
102
|
+
{ iso: "CI", name: "Ivory Coast", dialCode: "225", flag: "🇨🇮" },
|
|
103
|
+
{ iso: "JM", name: "Jamaica", dialCode: "1876", flag: "🇯🇲" },
|
|
104
|
+
{ iso: "JP", name: "Japan", dialCode: "81", flag: "🇯🇵" },
|
|
105
|
+
{ iso: "JO", name: "Jordan", dialCode: "962", flag: "🇯🇴" },
|
|
106
|
+
{ iso: "KZ", name: "Kazakhstan", dialCode: "7", flag: "🇰🇿" },
|
|
107
|
+
{ iso: "KE", name: "Kenya", dialCode: "254", flag: "🇰🇪" },
|
|
108
|
+
{ iso: "KI", name: "Kiribati", dialCode: "686", flag: "🇰🇮" },
|
|
109
|
+
{ iso: "XK", name: "Kosovo", dialCode: "383", flag: "🇽🇰" },
|
|
110
|
+
{ iso: "KW", name: "Kuwait", dialCode: "965", flag: "🇰🇼" },
|
|
111
|
+
{ iso: "KG", name: "Kyrgyzstan", dialCode: "996", flag: "🇰🇬" },
|
|
112
|
+
{ iso: "LA", name: "Laos", dialCode: "856", flag: "🇱🇦" },
|
|
113
|
+
{ iso: "LV", name: "Latvia", dialCode: "371", flag: "🇱🇻" },
|
|
114
|
+
{ iso: "LB", name: "Lebanon", dialCode: "961", flag: "🇱🇧" },
|
|
115
|
+
{ iso: "LS", name: "Lesotho", dialCode: "266", flag: "🇱🇸" },
|
|
116
|
+
{ iso: "LR", name: "Liberia", dialCode: "231", flag: "🇱🇷" },
|
|
117
|
+
{ iso: "LY", name: "Libya", dialCode: "218", flag: "🇱🇾" },
|
|
118
|
+
{ iso: "LI", name: "Liechtenstein", dialCode: "423", flag: "🇱🇮" },
|
|
119
|
+
{ iso: "LT", name: "Lithuania", dialCode: "370", flag: "🇱🇹" },
|
|
120
|
+
{ iso: "LU", name: "Luxembourg", dialCode: "352", flag: "🇱🇺" },
|
|
121
|
+
{ iso: "MO", name: "Macau", dialCode: "853", flag: "🇲🇴" },
|
|
122
|
+
{ iso: "MG", name: "Madagascar", dialCode: "261", flag: "🇲🇬" },
|
|
123
|
+
{ iso: "MW", name: "Malawi", dialCode: "265", flag: "🇲🇼" },
|
|
124
|
+
{ iso: "MY", name: "Malaysia", dialCode: "60", flag: "🇲🇾" },
|
|
125
|
+
{ iso: "MV", name: "Maldives", dialCode: "960", flag: "🇲🇻" },
|
|
126
|
+
{ iso: "ML", name: "Mali", dialCode: "223", flag: "🇲🇱" },
|
|
127
|
+
{ iso: "MT", name: "Malta", dialCode: "356", flag: "🇲🇹" },
|
|
128
|
+
{ iso: "MH", name: "Marshall Islands", dialCode: "692", flag: "🇲🇭" },
|
|
129
|
+
{ iso: "MQ", name: "Martinique", dialCode: "596", flag: "🇲🇶" },
|
|
130
|
+
{ iso: "MR", name: "Mauritania", dialCode: "222", flag: "🇲🇷" },
|
|
131
|
+
{ iso: "MU", name: "Mauritius", dialCode: "230", flag: "🇲🇺" },
|
|
132
|
+
{ iso: "YT", name: "Mayotte", dialCode: "262", flag: "🇾🇹" },
|
|
133
|
+
{ iso: "MX", name: "Mexico", dialCode: "52", flag: "🇲🇽" },
|
|
134
|
+
{ iso: "FM", name: "Micronesia", dialCode: "691", flag: "🇫🇲" },
|
|
135
|
+
{ iso: "MD", name: "Moldova", dialCode: "373", flag: "🇲🇩" },
|
|
136
|
+
{ iso: "MC", name: "Monaco", dialCode: "377", flag: "🇲🇨" },
|
|
137
|
+
{ iso: "MN", name: "Mongolia", dialCode: "976", flag: "🇲🇳" },
|
|
138
|
+
{ iso: "ME", name: "Montenegro", dialCode: "382", flag: "🇲🇪" },
|
|
139
|
+
{ iso: "MS", name: "Montserrat", dialCode: "1664", flag: "🇲🇸" },
|
|
140
|
+
{ iso: "MA", name: "Morocco", dialCode: "212", flag: "🇲🇦" },
|
|
141
|
+
{ iso: "MZ", name: "Mozambique", dialCode: "258", flag: "🇲🇿" },
|
|
142
|
+
{ iso: "MM", name: "Myanmar", dialCode: "95", flag: "🇲🇲" },
|
|
143
|
+
{ iso: "NA", name: "Namibia", dialCode: "264", flag: "🇳🇦" },
|
|
144
|
+
{ iso: "NR", name: "Nauru", dialCode: "674", flag: "🇳🇷" },
|
|
145
|
+
{ iso: "NP", name: "Nepal", dialCode: "977", flag: "🇳🇵" },
|
|
146
|
+
{ iso: "NL", name: "Netherlands", dialCode: "31", flag: "🇳🇱" },
|
|
147
|
+
{ iso: "NC", name: "New Caledonia", dialCode: "687", flag: "🇳🇨" },
|
|
148
|
+
{ iso: "NZ", name: "New Zealand", dialCode: "64", flag: "🇳🇿" },
|
|
149
|
+
{ iso: "NI", name: "Nicaragua", dialCode: "505", flag: "🇳🇮" },
|
|
150
|
+
{ iso: "NE", name: "Niger", dialCode: "227", flag: "🇳🇪" },
|
|
151
|
+
{ iso: "NG", name: "Nigeria", dialCode: "234", flag: "🇳🇬" },
|
|
152
|
+
{ iso: "NU", name: "Niue", dialCode: "683", flag: "🇳🇺" },
|
|
153
|
+
{ iso: "NF", name: "Norfolk Island", dialCode: "672", flag: "🇳🇫" },
|
|
154
|
+
{ iso: "KP", name: "North Korea", dialCode: "850", flag: "🇰🇵" },
|
|
155
|
+
{ iso: "MK", name: "North Macedonia", dialCode: "389", flag: "🇲🇰" },
|
|
156
|
+
{ iso: "MP", name: "Northern Mariana Islands", dialCode: "1670", flag: "🇲🇵" },
|
|
157
|
+
{ iso: "NO", name: "Norway", dialCode: "47", flag: "🇳🇴" },
|
|
158
|
+
{ iso: "OM", name: "Oman", dialCode: "968", flag: "🇴🇲" },
|
|
159
|
+
{ iso: "PK", name: "Pakistan", dialCode: "92", flag: "🇵🇰" },
|
|
160
|
+
{ iso: "PW", name: "Palau", dialCode: "680", flag: "🇵🇼" },
|
|
161
|
+
{ iso: "PS", name: "Palestine", dialCode: "970", flag: "🇵🇸" },
|
|
162
|
+
{ iso: "PA", name: "Panama", dialCode: "507", flag: "🇵🇦" },
|
|
163
|
+
{ iso: "PG", name: "Papua New Guinea", dialCode: "675", flag: "🇵🇬" },
|
|
164
|
+
{ iso: "PY", name: "Paraguay", dialCode: "595", flag: "🇵🇾" },
|
|
165
|
+
{ iso: "PE", name: "Peru", dialCode: "51", flag: "🇵🇪" },
|
|
166
|
+
{ iso: "PH", name: "Philippines", dialCode: "63", flag: "🇵🇭" },
|
|
167
|
+
{ iso: "PL", name: "Poland", dialCode: "48", flag: "🇵🇱" },
|
|
168
|
+
{ iso: "PT", name: "Portugal", dialCode: "351", flag: "🇵🇹" },
|
|
169
|
+
{ iso: "PR", name: "Puerto Rico", dialCode: "1787", flag: "🇵🇷" },
|
|
170
|
+
{ iso: "QA", name: "Qatar", dialCode: "974", flag: "🇶🇦" },
|
|
171
|
+
{ iso: "CG", name: "Republic of the Congo", dialCode: "242", flag: "🇨🇬" },
|
|
172
|
+
{ iso: "RE", name: "Réunion", dialCode: "262", flag: "🇷🇪" },
|
|
173
|
+
{ iso: "RO", name: "Romania", dialCode: "40", flag: "🇷🇴" },
|
|
174
|
+
{ iso: "RU", name: "Russia", dialCode: "7", flag: "🇷🇺" },
|
|
175
|
+
{ iso: "RW", name: "Rwanda", dialCode: "250", flag: "🇷🇼" },
|
|
176
|
+
{ iso: "KN", name: "Saint Kitts and Nevis", dialCode: "1869", flag: "🇰🇳" },
|
|
177
|
+
{ iso: "LC", name: "Saint Lucia", dialCode: "1758", flag: "🇱🇨" },
|
|
178
|
+
{ iso: "PM", name: "Saint Pierre and Miquelon", dialCode: "508", flag: "🇵🇲" },
|
|
179
|
+
{ iso: "VC", name: "Saint Vincent and the Grenadines", dialCode: "1784", flag: "🇻🇨" },
|
|
180
|
+
{ iso: "WS", name: "Samoa", dialCode: "685", flag: "🇼🇸" },
|
|
181
|
+
{ iso: "SM", name: "San Marino", dialCode: "378", flag: "🇸🇲" },
|
|
182
|
+
{ iso: "ST", name: "São Tomé and Príncipe", dialCode: "239", flag: "🇸🇹" },
|
|
183
|
+
{ iso: "SA", name: "Saudi Arabia", dialCode: "966", flag: "🇸🇦" },
|
|
184
|
+
{ iso: "SN", name: "Senegal", dialCode: "221", flag: "🇸🇳" },
|
|
185
|
+
{ iso: "RS", name: "Serbia", dialCode: "381", flag: "🇷🇸" },
|
|
186
|
+
{ iso: "SC", name: "Seychelles", dialCode: "248", flag: "🇸🇨" },
|
|
187
|
+
{ iso: "SL", name: "Sierra Leone", dialCode: "232", flag: "🇸🇱" },
|
|
188
|
+
{ iso: "SG", name: "Singapore", dialCode: "65", flag: "🇸🇬" },
|
|
189
|
+
{ iso: "SX", name: "Sint Maarten", dialCode: "1721", flag: "🇸🇽" },
|
|
190
|
+
{ iso: "SK", name: "Slovakia", dialCode: "421", flag: "🇸🇰" },
|
|
191
|
+
{ iso: "SI", name: "Slovenia", dialCode: "386", flag: "🇸🇮" },
|
|
192
|
+
{ iso: "SB", name: "Solomon Islands", dialCode: "677", flag: "🇸🇧" },
|
|
193
|
+
{ iso: "SO", name: "Somalia", dialCode: "252", flag: "🇸🇴" },
|
|
194
|
+
{ iso: "ZA", name: "South Africa", dialCode: "27", flag: "🇿🇦" },
|
|
195
|
+
{ iso: "KR", name: "South Korea", dialCode: "82", flag: "🇰🇷" },
|
|
196
|
+
{ iso: "SS", name: "South Sudan", dialCode: "211", flag: "🇸🇸" },
|
|
197
|
+
{ iso: "ES", name: "Spain", dialCode: "34", flag: "🇪🇸" },
|
|
198
|
+
{ iso: "LK", name: "Sri Lanka", dialCode: "94", flag: "🇱🇰" },
|
|
199
|
+
{ iso: "SD", name: "Sudan", dialCode: "249", flag: "🇸🇩" },
|
|
200
|
+
{ iso: "SR", name: "Suriname", dialCode: "597", flag: "🇸🇷" },
|
|
201
|
+
{ iso: "SE", name: "Sweden", dialCode: "46", flag: "🇸🇪" },
|
|
202
|
+
{ iso: "CH", name: "Switzerland", dialCode: "41", flag: "🇨🇭" },
|
|
203
|
+
{ iso: "SY", name: "Syria", dialCode: "963", flag: "🇸🇾" },
|
|
204
|
+
{ iso: "TW", name: "Taiwan", dialCode: "886", flag: "🇹🇼" },
|
|
205
|
+
{ iso: "TJ", name: "Tajikistan", dialCode: "992", flag: "🇹🇯" },
|
|
206
|
+
{ iso: "TZ", name: "Tanzania", dialCode: "255", flag: "🇹🇿" },
|
|
207
|
+
{ iso: "TH", name: "Thailand", dialCode: "66", flag: "🇹🇭" },
|
|
208
|
+
{ iso: "TL", name: "Timor-Leste", dialCode: "670", flag: "🇹🇱" },
|
|
209
|
+
{ iso: "TG", name: "Togo", dialCode: "228", flag: "🇹🇬" },
|
|
210
|
+
{ iso: "TK", name: "Tokelau", dialCode: "690", flag: "🇹🇰" },
|
|
211
|
+
{ iso: "TO", name: "Tonga", dialCode: "676", flag: "🇹🇴" },
|
|
212
|
+
{ iso: "TT", name: "Trinidad and Tobago", dialCode: "1868", flag: "🇹🇹" },
|
|
213
|
+
{ iso: "TN", name: "Tunisia", dialCode: "216", flag: "🇹🇳" },
|
|
214
|
+
{ iso: "TR", name: "Turkey", dialCode: "90", flag: "🇹🇷" },
|
|
215
|
+
{ iso: "TM", name: "Turkmenistan", dialCode: "993", flag: "🇹🇲" },
|
|
216
|
+
{ iso: "TC", name: "Turks and Caicos Islands", dialCode: "1649", flag: "🇹🇨" },
|
|
217
|
+
{ iso: "TV", name: "Tuvalu", dialCode: "688", flag: "🇹🇻" },
|
|
218
|
+
{ iso: "VI", name: "U.S. Virgin Islands", dialCode: "1340", flag: "🇻🇮" },
|
|
219
|
+
{ iso: "UG", name: "Uganda", dialCode: "256", flag: "🇺🇬" },
|
|
220
|
+
{ iso: "UA", name: "Ukraine", dialCode: "380", flag: "🇺🇦" },
|
|
221
|
+
{ iso: "AE", name: "United Arab Emirates", dialCode: "971", flag: "🇦🇪" },
|
|
222
|
+
{ iso: "GB", name: "United Kingdom", dialCode: "44", flag: "🇬🇧" },
|
|
223
|
+
{ iso: "US", name: "United States", dialCode: "1", flag: "🇺🇸" },
|
|
224
|
+
{ iso: "UY", name: "Uruguay", dialCode: "598", flag: "🇺🇾" },
|
|
225
|
+
{ iso: "UZ", name: "Uzbekistan", dialCode: "998", flag: "🇺🇿" },
|
|
226
|
+
{ iso: "VU", name: "Vanuatu", dialCode: "678", flag: "🇻🇺" },
|
|
227
|
+
{ iso: "VA", name: "Vatican City", dialCode: "379", flag: "🇻🇦" },
|
|
228
|
+
{ iso: "VE", name: "Venezuela", dialCode: "58", flag: "🇻🇪" },
|
|
229
|
+
{ iso: "VN", name: "Vietnam", dialCode: "84", flag: "🇻🇳" },
|
|
230
|
+
{ iso: "WF", name: "Wallis and Futuna", dialCode: "681", flag: "🇼🇫" },
|
|
231
|
+
{ iso: "YE", name: "Yemen", dialCode: "967", flag: "🇾🇪" },
|
|
232
|
+
{ iso: "ZM", name: "Zambia", dialCode: "260", flag: "🇿🇲" },
|
|
233
|
+
{ iso: "ZW", name: "Zimbabwe", dialCode: "263", flag: "🇿🇼" },
|
|
234
|
+
];
|
|
235
|
+
/** Lookup map: ISO code -> Country */
|
|
236
|
+
export const ISO_MAP = new Map();
|
|
237
|
+
COUNTRIES.forEach((c) => ISO_MAP.set(c.iso, c));
|
|
238
|
+
/** Lookup map: dial code -> Country[]. Multiple countries can share a code (e.g. +1). */
|
|
239
|
+
export const DIAL_CODE_MAP = new Map();
|
|
240
|
+
COUNTRIES.forEach((c) => {
|
|
241
|
+
const arr = DIAL_CODE_MAP.get(c.dialCode) || [];
|
|
242
|
+
arr.push(c);
|
|
243
|
+
DIAL_CODE_MAP.set(c.dialCode, arr);
|
|
244
|
+
});
|
|
245
|
+
/**
|
|
246
|
+
* Sorted list of unique dial codes (longest first) for prefix detection during paste.
|
|
247
|
+
* Longest-first ensures we match "+1684" (American Samoa) before "+1" (US).
|
|
248
|
+
*/
|
|
249
|
+
export const DIAL_CODES_DESC = [...new Set(COUNTRIES.map((c) => c.dialCode))].sort((a, b) => b.length - a.length);
|
|
@@ -66,6 +66,13 @@
|
|
|
66
66
|
--stuic-input-localized-toggle-hover-bg: var(--stuic-color-muted);
|
|
67
67
|
--stuic-input-localized-label-text: var(--stuic-color-muted-foreground);
|
|
68
68
|
|
|
69
|
+
/* FieldPhoneNumber specific */
|
|
70
|
+
--stuic-field-phone-trigger-text: var(--stuic-color-foreground);
|
|
71
|
+
--stuic-field-phone-trigger-text-hover: var(--stuic-color-foreground);
|
|
72
|
+
--stuic-field-phone-trigger-bg: transparent;
|
|
73
|
+
--stuic-field-phone-trigger-bg-hover: var(--stuic-color-muted);
|
|
74
|
+
--stuic-field-phone-trigger-border: var(--stuic-color-border);
|
|
75
|
+
|
|
69
76
|
/* FieldOptions specific */
|
|
70
77
|
--stuic-field-options-divider: var(--stuic-color-border);
|
|
71
78
|
--stuic-field-options-control-text: var(--stuic-color-muted-foreground);
|
|
@@ -585,4 +592,39 @@
|
|
|
585
592
|
border-right: 1px solid var(--stuic-input-localized-divider);
|
|
586
593
|
/* border-radius: var(--stuic-input-radius); */
|
|
587
594
|
}
|
|
595
|
+
|
|
596
|
+
/* ============================================================================
|
|
597
|
+
FIELD PHONE NUMBER
|
|
598
|
+
============================================================================ */
|
|
599
|
+
|
|
600
|
+
.stuic-phone-prefix-trigger {
|
|
601
|
+
color: var(--stuic-field-phone-trigger-text);
|
|
602
|
+
background: var(--stuic-field-phone-trigger-bg);
|
|
603
|
+
border-right: 1px solid var(--stuic-field-phone-trigger-border);
|
|
604
|
+
border-radius: var(--stuic-input-radius) 0 0 var(--stuic-input-radius);
|
|
605
|
+
transition:
|
|
606
|
+
background var(--stuic-input-transition),
|
|
607
|
+
color var(--stuic-input-transition);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.stuic-phone-prefix-trigger:hover:not(:disabled) {
|
|
611
|
+
color: var(--stuic-field-phone-trigger-text-hover);
|
|
612
|
+
background: var(--stuic-field-phone-trigger-bg-hover);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.stuic-phone-prefix-trigger:disabled {
|
|
616
|
+
opacity: 0.5;
|
|
617
|
+
cursor: not-allowed;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.stuic-phone-prefix-picker {
|
|
621
|
+
display: flex;
|
|
622
|
+
align-items: stretch;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* Reset field input styles leaking into the dropdown's search */
|
|
626
|
+
.stuic-phone-prefix-picker .stuic-dropdown-menu-search {
|
|
627
|
+
padding-top: 0;
|
|
628
|
+
padding-bottom: 0;
|
|
629
|
+
}
|
|
588
630
|
}
|
|
@@ -13,3 +13,6 @@ export { default as FieldTextarea, type Props as FieldTextareaProps, } from "./F
|
|
|
13
13
|
export { default as FieldInputLocalized, type Props as FieldInputLocalizedProps, } from "./FieldInputLocalized.svelte";
|
|
14
14
|
export { default as FieldKeyValues, type Props as FieldKeyValuesProps, type KeyValueEntry, } from "./FieldKeyValues.svelte";
|
|
15
15
|
export { default as FieldObject, type Props as FieldObjectProps, } from "./FieldObject.svelte";
|
|
16
|
+
export { default as FieldPhoneNumber, type Props as FieldPhoneNumberProps, } from "./FieldPhoneNumber.svelte";
|
|
17
|
+
export { validatePhoneNumber } from "./phone-validation.js";
|
|
18
|
+
export { type Country } from "./_internal/countries.js";
|
|
@@ -13,3 +13,6 @@ export { default as FieldTextarea, } from "./FieldTextarea.svelte";
|
|
|
13
13
|
export { default as FieldInputLocalized, } from "./FieldInputLocalized.svelte";
|
|
14
14
|
export { default as FieldKeyValues, } from "./FieldKeyValues.svelte";
|
|
15
15
|
export { default as FieldObject, } from "./FieldObject.svelte";
|
|
16
|
+
export { default as FieldPhoneNumber, } from "./FieldPhoneNumber.svelte";
|
|
17
|
+
export { validatePhoneNumber } from "./phone-validation.js";
|
|
18
|
+
export {} from "./_internal/countries.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in phone number validator for FieldPhoneNumber.
|
|
3
|
+
* Validates E.164 format (e.g. "+421905123456").
|
|
4
|
+
*
|
|
5
|
+
* Returns error message if invalid, undefined if valid or empty.
|
|
6
|
+
* Empty values are not validated — use the `required` attribute for that.
|
|
7
|
+
*
|
|
8
|
+
* Compatible with the `customValidator` signature of the validate action.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validatePhoneNumber(value: unknown, _context?: Record<string, unknown>, _el?: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): string | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isValidPhoneNumber } from "libphonenumber-js/min";
|
|
2
|
+
/**
|
|
3
|
+
* Built-in phone number validator for FieldPhoneNumber.
|
|
4
|
+
* Validates E.164 format (e.g. "+421905123456").
|
|
5
|
+
*
|
|
6
|
+
* Returns error message if invalid, undefined if valid or empty.
|
|
7
|
+
* Empty values are not validated — use the `required` attribute for that.
|
|
8
|
+
*
|
|
9
|
+
* Compatible with the `customValidator` signature of the validate action.
|
|
10
|
+
*/
|
|
11
|
+
export function validatePhoneNumber(value, _context, _el) {
|
|
12
|
+
if (!value || typeof value !== "string")
|
|
13
|
+
return undefined;
|
|
14
|
+
try {
|
|
15
|
+
if (isValidPhoneNumber(value))
|
|
16
|
+
return undefined;
|
|
17
|
+
return "Invalid phone number";
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "Invalid phone number";
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"!dist/**/*.test.*",
|
|
@@ -29,7 +29,11 @@
|
|
|
29
29
|
"types": "./dist/themes/*.d.ts",
|
|
30
30
|
"default": "./dist/themes/*.js"
|
|
31
31
|
},
|
|
32
|
-
"./themes/css/*": "./dist/themes/css/*"
|
|
32
|
+
"./themes/css/*": "./dist/themes/css/*",
|
|
33
|
+
"./phone-validation": {
|
|
34
|
+
"types": "./dist/components/Input/phone-validation.d.ts",
|
|
35
|
+
"default": "./dist/components/Input/phone-validation.js"
|
|
36
|
+
}
|
|
33
37
|
},
|
|
34
38
|
"peerDependencies": {
|
|
35
39
|
"svelte": "^5.0.0"
|
|
@@ -53,7 +57,7 @@
|
|
|
53
57
|
"prettier": "^3.8.1",
|
|
54
58
|
"prettier-plugin-svelte": "^3.4.1",
|
|
55
59
|
"publint": "^0.3.17",
|
|
56
|
-
"svelte": "^5.51.
|
|
60
|
+
"svelte": "^5.51.2",
|
|
57
61
|
"svelte-check": "^4.4.0",
|
|
58
62
|
"tailwindcss": "^4.1.18",
|
|
59
63
|
"tsx": "^4.21.0",
|
|
@@ -69,8 +73,9 @@
|
|
|
69
73
|
"@marianmeres/parse-boolean": "^2.0.5",
|
|
70
74
|
"@marianmeres/ticker": "^1.16.5",
|
|
71
75
|
"esm-env": "^1.2.2",
|
|
76
|
+
"libphonenumber-js": "^1.12.36",
|
|
72
77
|
"runed": "^0.23.4",
|
|
73
|
-
"tailwind-merge": "^3.4.
|
|
78
|
+
"tailwind-merge": "^3.4.1"
|
|
74
79
|
},
|
|
75
80
|
"scripts": {
|
|
76
81
|
"dev": "vite dev",
|