@scripso-homepad/ui 0.3.6 → 0.3.7

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.
@@ -0,0 +1,117 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { View, StyleSheet } from "react-native";
4
+ import { EyeIcon } from "../icons/EyeIcon";
5
+ import { KeyIcon } from "../icons/KeyIcon";
6
+ import { Input } from "./Input";
7
+
8
+ const meta = {
9
+ title: "Components/Input",
10
+ component: Input,
11
+ args: {
12
+ label: "Label",
13
+ placeholder: "placeholder",
14
+ },
15
+ } satisfies Meta<typeof Input>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {};
21
+
22
+ export const WithIcons: Story = {
23
+ args: {
24
+ leftIcon: <KeyIcon />,
25
+ rightIcon: <EyeIcon />,
26
+ placeholder: "placeholder",
27
+ },
28
+ };
29
+
30
+ export const WithError: Story = {
31
+ args: {
32
+ leftIcon: <KeyIcon />,
33
+ rightIcon: <EyeIcon />,
34
+ value: "invalid",
35
+ error: "Invalid value",
36
+ },
37
+ };
38
+
39
+ export const WithHint: Story = {
40
+ args: {
41
+ leftIcon: <KeyIcon />,
42
+ rightIcon: <EyeIcon />,
43
+ hint: "Use at least 8 characters",
44
+ },
45
+ };
46
+
47
+ export const Disabled: Story = {
48
+ args: {
49
+ leftIcon: <KeyIcon />,
50
+ rightIcon: <EyeIcon />,
51
+ value: "placeholder",
52
+ editable: false,
53
+ },
54
+ };
55
+
56
+ /** Figma state row — default, focus, error, disabled */
57
+ export const FigmaStates: Story = {
58
+ render: () => (
59
+ <View style={styles.column}>
60
+ <Input
61
+ label="Label"
62
+ leftIcon={<KeyIcon />}
63
+ rightIcon={<EyeIcon />}
64
+ placeholder="placeholder"
65
+ />
66
+ <Input
67
+ label="Label"
68
+ leftIcon={<KeyIcon />}
69
+ rightIcon={<EyeIcon />}
70
+ placeholder="placeholder"
71
+ autoFocus
72
+ />
73
+ <Input
74
+ label="Label"
75
+ leftIcon={<KeyIcon />}
76
+ rightIcon={<EyeIcon />}
77
+ placeholder="placeholder"
78
+ error="Error message"
79
+ />
80
+ <Input
81
+ label="Label"
82
+ leftIcon={<KeyIcon />}
83
+ rightIcon={<EyeIcon />}
84
+ placeholder="placeholder"
85
+ editable={false}
86
+ />
87
+ </View>
88
+ ),
89
+ };
90
+
91
+ export const Controlled: Story = {
92
+ render: function ControlledInputStory() {
93
+ const [value, setValue] = useState("");
94
+ return (
95
+ <View style={styles.wrapper}>
96
+ <Input
97
+ label="Label"
98
+ leftIcon={<KeyIcon />}
99
+ rightIcon={<EyeIcon />}
100
+ placeholder="placeholder"
101
+ value={value}
102
+ onChangeText={setValue}
103
+ />
104
+ </View>
105
+ );
106
+ },
107
+ };
108
+
109
+ const styles = StyleSheet.create({
110
+ wrapper: {
111
+ width: 360,
112
+ },
113
+ column: {
114
+ width: 360,
115
+ gap: 16,
116
+ },
117
+ });
@@ -0,0 +1,177 @@
1
+ import {
2
+ cloneElement,
3
+ isValidElement,
4
+ useRef,
5
+ useState,
6
+ type ComponentRef,
7
+ type ReactNode,
8
+ } from "react";
9
+ import {
10
+ StyleSheet,
11
+ Text,
12
+ TextInput,
13
+ View,
14
+ type NativeSyntheticEvent,
15
+ type StyleProp,
16
+ type TextInputFocusEventData,
17
+ type TextInputProps,
18
+ type TextStyle,
19
+ type ViewStyle,
20
+ } from "react-native";
21
+ import {
22
+ getInputFieldStyles,
23
+ inputFieldMetrics,
24
+ INPUT_ICON_SIZE,
25
+ resolveInputVisualState,
26
+ } from "../theme/input";
27
+ import { colors, spacing } from "../theme/tokens";
28
+ import { useApplyWebClassName } from "../utils/useApplyWebClassName";
29
+ import { Label } from "./Label";
30
+
31
+ type InputIconProps = {
32
+ size?: number;
33
+ color?: string;
34
+ };
35
+
36
+ function renderInputIcon(icon: ReactNode, color: string) {
37
+ if (!icon) return null;
38
+
39
+ if (isValidElement<InputIconProps>(icon)) {
40
+ return cloneElement(icon, {
41
+ size: icon.props.size ?? INPUT_ICON_SIZE,
42
+ color: icon.props.color ?? color,
43
+ });
44
+ }
45
+
46
+ return icon;
47
+ }
48
+
49
+ export interface InputProps extends TextInputProps {
50
+ /** Optional field label rendered above the input. */
51
+ label?: string;
52
+ leftIcon?: ReactNode;
53
+ rightIcon?: ReactNode;
54
+ /** Validation or helper error message. Shown instead of `hint` when set. */
55
+ error?: string;
56
+ /** Helper text shown below the input when there is no error. */
57
+ hint?: string;
58
+ containerStyle?: StyleProp<ViewStyle>;
59
+ style?: StyleProp<TextStyle>;
60
+ className?: string;
61
+ labelClassName?: string;
62
+ inputClassName?: string;
63
+ errorClassName?: string;
64
+ hintClassName?: string;
65
+ }
66
+
67
+ export function Input({
68
+ label,
69
+ leftIcon,
70
+ rightIcon,
71
+ error,
72
+ hint,
73
+ containerStyle,
74
+ style,
75
+ className,
76
+ labelClassName,
77
+ inputClassName,
78
+ errorClassName,
79
+ hintClassName,
80
+ editable = true,
81
+ onFocus,
82
+ onBlur,
83
+ ...props
84
+ }: InputProps) {
85
+ const wrapperRef = useRef<ComponentRef<typeof View>>(null);
86
+ const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
87
+ const helperRef = useRef<ComponentRef<typeof Text>>(null);
88
+ const [focused, setFocused] = useState(false);
89
+
90
+ useApplyWebClassName(wrapperRef, className);
91
+ useApplyWebClassName(inputRef, inputClassName);
92
+ useApplyWebClassName(helperRef, error ? errorClassName : hintClassName);
93
+
94
+ const isDisabled = editable === false;
95
+ const visualState = resolveInputVisualState({
96
+ focused,
97
+ error: Boolean(error),
98
+ disabled: isDisabled,
99
+ });
100
+ const fieldStyles = getInputFieldStyles(visualState);
101
+ const iconColor = fieldStyles.icon;
102
+
103
+ function handleFocus(event: NativeSyntheticEvent<TextInputFocusEventData>) {
104
+ setFocused(true);
105
+ onFocus?.(event);
106
+ }
107
+
108
+ function handleBlur(event: NativeSyntheticEvent<TextInputFocusEventData>) {
109
+ setFocused(false);
110
+ onBlur?.(event);
111
+ }
112
+
113
+ const helperMessage = error ?? hint;
114
+
115
+ return (
116
+ <View ref={wrapperRef} style={[styles.wrapper, containerStyle]}>
117
+ {label ? (
118
+ <Label disabled={isDisabled} className={labelClassName}>
119
+ {label}
120
+ </Label>
121
+ ) : null}
122
+ <View style={fieldStyles.outline}>
123
+ <View style={[inputFieldMetrics.container, fieldStyles.container]}>
124
+ {leftIcon ? (
125
+ <View style={styles.iconSlot}>{renderInputIcon(leftIcon, iconColor)}</View>
126
+ ) : null}
127
+ <TextInput
128
+ ref={inputRef}
129
+ style={[
130
+ inputFieldMetrics.input,
131
+ fieldStyles.text,
132
+ style,
133
+ ]}
134
+ placeholderTextColor={fieldStyles.placeholder}
135
+ editable={editable}
136
+ onFocus={handleFocus}
137
+ onBlur={handleBlur}
138
+ accessibilityState={{ disabled: isDisabled }}
139
+ {...props}
140
+ />
141
+ {rightIcon ? (
142
+ <View style={styles.iconSlot}>{renderInputIcon(rightIcon, iconColor)}</View>
143
+ ) : null}
144
+ </View>
145
+ </View>
146
+ {helperMessage ? (
147
+ <Text ref={helperRef} style={error ? styles.error : styles.hint}>
148
+ {helperMessage}
149
+ </Text>
150
+ ) : null}
151
+ </View>
152
+ );
153
+ }
154
+
155
+ const styles = StyleSheet.create({
156
+ wrapper: {
157
+ width: "100%",
158
+ gap: spacing.sm,
159
+ },
160
+ iconSlot: {
161
+ width: INPUT_ICON_SIZE,
162
+ height: INPUT_ICON_SIZE,
163
+ alignItems: "center",
164
+ justifyContent: "center",
165
+ flexShrink: 0,
166
+ },
167
+ error: {
168
+ fontSize: 12,
169
+ lineHeight: 12,
170
+ color: colors.inputError,
171
+ },
172
+ hint: {
173
+ fontSize: 12,
174
+ lineHeight: 12,
175
+ color: colors.stormGray300,
176
+ },
177
+ });
@@ -0,0 +1,56 @@
1
+ import { useRef, type ComponentRef, type ReactNode } from "react";
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ type StyleProp,
6
+ type TextProps,
7
+ type TextStyle,
8
+ } from "react-native";
9
+ import { colors, labelTypography } from "../theme/tokens";
10
+ import { useApplyWebClassName } from "../utils/useApplyWebClassName";
11
+
12
+ export interface LabelProps extends TextProps {
13
+ children: ReactNode;
14
+ /** Render as a required field indicator. */
15
+ required?: boolean;
16
+ disabled?: boolean;
17
+ style?: StyleProp<TextStyle>;
18
+ className?: string;
19
+ }
20
+
21
+ export function Label({
22
+ children,
23
+ required = false,
24
+ disabled = false,
25
+ style,
26
+ className,
27
+ ...props
28
+ }: LabelProps) {
29
+ const ref = useRef<ComponentRef<typeof Text>>(null);
30
+ useApplyWebClassName(ref, className);
31
+
32
+ return (
33
+ <Text
34
+ ref={ref}
35
+ style={[styles.label, disabled && styles.labelDisabled, style]}
36
+ accessibilityRole="text"
37
+ {...props}
38
+ >
39
+ {children}
40
+ {required ? <Text style={styles.required}> *</Text> : null}
41
+ </Text>
42
+ );
43
+ }
44
+
45
+ const styles = StyleSheet.create({
46
+ label: {
47
+ ...labelTypography,
48
+ color: colors.slateBlue,
49
+ },
50
+ labelDisabled: {
51
+ color: colors.stormGray400,
52
+ },
53
+ required: {
54
+ color: colors.inputError,
55
+ },
56
+ });
@@ -0,0 +1,85 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { View, StyleSheet } from "react-native";
4
+ import { PhoneInput } from "./PhoneInput";
5
+
6
+ const meta = {
7
+ title: "Components/PhoneInput",
8
+ component: PhoneInput,
9
+ args: {
10
+ label: "Label",
11
+ placeholder: "placeholder",
12
+ },
13
+ } satisfies Meta<typeof PhoneInput>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {};
19
+
20
+ export const WithError: Story = {
21
+ args: {
22
+ error: "Invalid phone number",
23
+ },
24
+ };
25
+
26
+ export const WithHint: Story = {
27
+ args: {
28
+ hint: "Include your area code",
29
+ },
30
+ };
31
+
32
+ export const Disabled: Story = {
33
+ args: {
34
+ value: "placeholder",
35
+ editable: false,
36
+ },
37
+ };
38
+
39
+ export const FigmaStates: Story = {
40
+ render: () => (
41
+ <View style={styles.column}>
42
+ <PhoneInput label="Label" placeholder="placeholder" />
43
+ <PhoneInput label="Label" placeholder="placeholder" autoFocus />
44
+ <PhoneInput
45
+ label="Label"
46
+ placeholder="placeholder"
47
+ error="Error message"
48
+ />
49
+ <PhoneInput
50
+ label="Label"
51
+ placeholder="placeholder"
52
+ editable={false}
53
+ />
54
+ </View>
55
+ ),
56
+ };
57
+
58
+ export const Controlled: Story = {
59
+ render: function ControlledPhoneStory() {
60
+ const [countryCode, setCountryCode] = useState("AM");
61
+ const [phone, setPhone] = useState("");
62
+ return (
63
+ <View style={styles.wrapper}>
64
+ <PhoneInput
65
+ label="Label"
66
+ placeholder="placeholder"
67
+ countryCode={countryCode}
68
+ onCountryCodeChange={setCountryCode}
69
+ value={phone}
70
+ onChangeText={setPhone}
71
+ />
72
+ </View>
73
+ );
74
+ },
75
+ };
76
+
77
+ const styles = StyleSheet.create({
78
+ wrapper: {
79
+ width: 360,
80
+ },
81
+ column: {
82
+ width: 360,
83
+ gap: 16,
84
+ },
85
+ });
@@ -0,0 +1,172 @@
1
+ import { useRef, useState, type ComponentRef } from "react";
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ TextInput,
6
+ View,
7
+ type NativeSyntheticEvent,
8
+ type StyleProp,
9
+ type TextInputFocusEventData,
10
+ type TextInputProps,
11
+ type TextStyle,
12
+ type ViewStyle,
13
+ } from "react-native";
14
+ import { CountryCodeSelector } from "./CountryCodeSelector";
15
+ import { Label } from "./Label";
16
+ import {
17
+ getInputFieldStyles,
18
+ inputFieldMetrics,
19
+ INPUT_ICON_GAP,
20
+ resolveInputVisualState,
21
+ } from "../theme/input";
22
+ import { colors, spacing } from "../theme/tokens";
23
+ import { useApplyWebClassName } from "../utils/useApplyWebClassName";
24
+
25
+ export interface PhoneInputProps extends Omit<TextInputProps, "style"> {
26
+ label?: string;
27
+ countryCode?: string;
28
+ onCountryCodeChange?: (countryCode: string) => void;
29
+ error?: string;
30
+ hint?: string;
31
+ containerStyle?: StyleProp<ViewStyle>;
32
+ style?: StyleProp<TextStyle>;
33
+ className?: string;
34
+ labelClassName?: string;
35
+ inputClassName?: string;
36
+ errorClassName?: string;
37
+ hintClassName?: string;
38
+ }
39
+
40
+ export function PhoneInput({
41
+ label,
42
+ countryCode,
43
+ onCountryCodeChange,
44
+ error,
45
+ hint,
46
+ containerStyle,
47
+ style,
48
+ className,
49
+ labelClassName,
50
+ inputClassName,
51
+ errorClassName,
52
+ hintClassName,
53
+ editable = true,
54
+ onFocus,
55
+ onBlur,
56
+ ...props
57
+ }: PhoneInputProps) {
58
+ const wrapperRef = useRef<ComponentRef<typeof View>>(null);
59
+ const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
60
+ const helperRef = useRef<ComponentRef<typeof Text>>(null);
61
+ const [focused, setFocused] = useState(false);
62
+
63
+ useApplyWebClassName(wrapperRef, className);
64
+ useApplyWebClassName(inputRef, inputClassName);
65
+ useApplyWebClassName(helperRef, error ? errorClassName : hintClassName);
66
+
67
+ const isDisabled = editable === false;
68
+ const phoneVisualState = resolveInputVisualState({
69
+ focused,
70
+ error: Boolean(error),
71
+ disabled: isDisabled,
72
+ });
73
+ const phoneFieldStyles = getInputFieldStyles(phoneVisualState);
74
+
75
+ function handleFocus(event: NativeSyntheticEvent<TextInputFocusEventData>) {
76
+ setFocused(true);
77
+ onFocus?.(event);
78
+ }
79
+
80
+ function handleBlur(event: NativeSyntheticEvent<TextInputFocusEventData>) {
81
+ setFocused(false);
82
+ onBlur?.(event);
83
+ }
84
+
85
+ const helperMessage = error ?? hint;
86
+
87
+ return (
88
+ <View ref={wrapperRef} style={[styles.wrapper, containerStyle]}>
89
+ {label ? (
90
+ <Label disabled={isDisabled} className={labelClassName}>
91
+ {label}
92
+ </Label>
93
+ ) : null}
94
+ <View style={styles.row}>
95
+ <CountryCodeSelector
96
+ value={countryCode}
97
+ onValueChange={onCountryCodeChange}
98
+ disabled={isDisabled}
99
+ style={styles.countrySelector}
100
+ />
101
+ <View style={styles.phoneColumn}>
102
+ <View style={[phoneFieldStyles.outline, styles.phoneOutline]}>
103
+ <View
104
+ style={[
105
+ inputFieldMetrics.container,
106
+ phoneFieldStyles.container,
107
+ styles.phoneField,
108
+ ]}
109
+ >
110
+ <TextInput
111
+ ref={inputRef}
112
+ style={[
113
+ inputFieldMetrics.input,
114
+ phoneFieldStyles.text,
115
+ style,
116
+ ]}
117
+ keyboardType="phone-pad"
118
+ placeholderTextColor={phoneFieldStyles.placeholder}
119
+ editable={editable}
120
+ onFocus={handleFocus}
121
+ onBlur={handleBlur}
122
+ accessibilityState={{ disabled: isDisabled }}
123
+ {...props}
124
+ />
125
+ </View>
126
+ </View>
127
+ {helperMessage ? (
128
+ <Text ref={helperRef} style={error ? styles.error : styles.hint}>
129
+ {helperMessage}
130
+ </Text>
131
+ ) : null}
132
+ </View>
133
+ </View>
134
+ </View>
135
+ );
136
+ }
137
+
138
+ const styles = StyleSheet.create({
139
+ wrapper: {
140
+ width: "100%",
141
+ gap: spacing.sm,
142
+ },
143
+ row: {
144
+ flexDirection: "row",
145
+ alignItems: "flex-start",
146
+ gap: INPUT_ICON_GAP,
147
+ },
148
+ countrySelector: {
149
+ flexShrink: 0,
150
+ alignSelf: "flex-start",
151
+ },
152
+ phoneColumn: {
153
+ flex: 1,
154
+ gap: spacing.sm,
155
+ },
156
+ phoneOutline: {
157
+ width: "100%",
158
+ },
159
+ phoneField: {
160
+ flex: 1,
161
+ },
162
+ error: {
163
+ fontSize: 12,
164
+ lineHeight: 12,
165
+ color: colors.inputError,
166
+ },
167
+ hint: {
168
+ fontSize: 12,
169
+ lineHeight: 12,
170
+ color: colors.stormGray300,
171
+ },
172
+ });
@@ -0,0 +1,98 @@
1
+ export type Country = {
2
+ code: string;
3
+ name: string;
4
+ dialCode: string;
5
+ };
6
+
7
+ export const defaultCountry: Country = {
8
+ code: "AM",
9
+ name: "Armenia",
10
+ dialCode: "+374",
11
+ };
12
+
13
+ export const countries: Country[] = [
14
+ defaultCountry,
15
+ { code: "AF", name: "Afghanistan", dialCode: "+93" },
16
+ { code: "AL", name: "Albania", dialCode: "+355" },
17
+ { code: "DZ", name: "Algeria", dialCode: "+213" },
18
+ { code: "AR", name: "Argentina", dialCode: "+54" },
19
+ { code: "AU", name: "Australia", dialCode: "+61" },
20
+ { code: "AT", name: "Austria", dialCode: "+43" },
21
+ { code: "AZ", name: "Azerbaijan", dialCode: "+994" },
22
+ { code: "BH", name: "Bahrain", dialCode: "+973" },
23
+ { code: "BY", name: "Belarus", dialCode: "+375" },
24
+ { code: "BE", name: "Belgium", dialCode: "+32" },
25
+ { code: "BR", name: "Brazil", dialCode: "+55" },
26
+ { code: "BG", name: "Bulgaria", dialCode: "+359" },
27
+ { code: "CA", name: "Canada", dialCode: "+1" },
28
+ { code: "CL", name: "Chile", dialCode: "+56" },
29
+ { code: "CN", name: "China", dialCode: "+86" },
30
+ { code: "CO", name: "Colombia", dialCode: "+57" },
31
+ { code: "HR", name: "Croatia", dialCode: "+385" },
32
+ { code: "CY", name: "Cyprus", dialCode: "+357" },
33
+ { code: "CZ", name: "Czech Republic", dialCode: "+420" },
34
+ { code: "DK", name: "Denmark", dialCode: "+45" },
35
+ { code: "EG", name: "Egypt", dialCode: "+20" },
36
+ { code: "EE", name: "Estonia", dialCode: "+372" },
37
+ { code: "FI", name: "Finland", dialCode: "+358" },
38
+ { code: "FR", name: "France", dialCode: "+33" },
39
+ { code: "GE", name: "Georgia", dialCode: "+995" },
40
+ { code: "DE", name: "Germany", dialCode: "+49" },
41
+ { code: "GR", name: "Greece", dialCode: "+30" },
42
+ { code: "HK", name: "Hong Kong", dialCode: "+852" },
43
+ { code: "HU", name: "Hungary", dialCode: "+36" },
44
+ { code: "IN", name: "India", dialCode: "+91" },
45
+ { code: "ID", name: "Indonesia", dialCode: "+62" },
46
+ { code: "IR", name: "Iran", dialCode: "+98" },
47
+ { code: "IQ", name: "Iraq", dialCode: "+964" },
48
+ { code: "IE", name: "Ireland", dialCode: "+353" },
49
+ { code: "IL", name: "Israel", dialCode: "+972" },
50
+ { code: "IT", name: "Italy", dialCode: "+39" },
51
+ { code: "JP", name: "Japan", dialCode: "+81" },
52
+ { code: "JO", name: "Jordan", dialCode: "+962" },
53
+ { code: "KZ", name: "Kazakhstan", dialCode: "+7" },
54
+ { code: "KW", name: "Kuwait", dialCode: "+965" },
55
+ { code: "LV", name: "Latvia", dialCode: "+371" },
56
+ { code: "LB", name: "Lebanon", dialCode: "+961" },
57
+ { code: "LT", name: "Lithuania", dialCode: "+370" },
58
+ { code: "LU", name: "Luxembourg", dialCode: "+352" },
59
+ { code: "MY", name: "Malaysia", dialCode: "+60" },
60
+ { code: "MX", name: "Mexico", dialCode: "+52" },
61
+ { code: "MD", name: "Moldova", dialCode: "+373" },
62
+ { code: "NL", name: "Netherlands", dialCode: "+31" },
63
+ { code: "NZ", name: "New Zealand", dialCode: "+64" },
64
+ { code: "NG", name: "Nigeria", dialCode: "+234" },
65
+ { code: "NO", name: "Norway", dialCode: "+47" },
66
+ { code: "PK", name: "Pakistan", dialCode: "+92" },
67
+ { code: "PS", name: "Palestine", dialCode: "+970" },
68
+ { code: "PH", name: "Philippines", dialCode: "+63" },
69
+ { code: "PL", name: "Poland", dialCode: "+48" },
70
+ { code: "PT", name: "Portugal", dialCode: "+351" },
71
+ { code: "QA", name: "Qatar", dialCode: "+974" },
72
+ { code: "RO", name: "Romania", dialCode: "+40" },
73
+ { code: "RU", name: "Russia", dialCode: "+7" },
74
+ { code: "SA", name: "Saudi Arabia", dialCode: "+966" },
75
+ { code: "RS", name: "Serbia", dialCode: "+381" },
76
+ { code: "SG", name: "Singapore", dialCode: "+65" },
77
+ { code: "SK", name: "Slovakia", dialCode: "+421" },
78
+ { code: "SI", name: "Slovenia", dialCode: "+386" },
79
+ { code: "ZA", name: "South Africa", dialCode: "+27" },
80
+ { code: "KR", name: "South Korea", dialCode: "+82" },
81
+ { code: "ES", name: "Spain", dialCode: "+34" },
82
+ { code: "SE", name: "Sweden", dialCode: "+46" },
83
+ { code: "CH", name: "Switzerland", dialCode: "+41" },
84
+ { code: "SY", name: "Syria", dialCode: "+963" },
85
+ { code: "TW", name: "Taiwan", dialCode: "+886" },
86
+ { code: "TH", name: "Thailand", dialCode: "+66" },
87
+ { code: "TR", name: "Turkey", dialCode: "+90" },
88
+ { code: "UA", name: "Ukraine", dialCode: "+380" },
89
+ { code: "AE", name: "United Arab Emirates", dialCode: "+971" },
90
+ { code: "GB", name: "United Kingdom", dialCode: "+44" },
91
+ { code: "US", name: "United States", dialCode: "+1" },
92
+ { code: "UZ", name: "Uzbekistan", dialCode: "+998" },
93
+ { code: "VN", name: "Vietnam", dialCode: "+84" },
94
+ ];
95
+
96
+ export function findCountry(code: string): Country {
97
+ return countries.find((country) => country.code === code) ?? defaultCountry;
98
+ }