notionsoft-ui 1.0.21 → 1.0.23
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/cli/index.cjs +89 -7
- package/package.json +1 -1
- package/src/notion-ui/date-picker/date-picker.tsx +0 -2
- package/src/notion-ui/phone-input/country-data.ts +420 -0
- package/src/notion-ui/phone-input/index.ts +3 -0
- package/src/notion-ui/phone-input/lazy-flag.tsx +83 -0
- package/src/notion-ui/phone-input/phone-input.tsx +400 -0
- package/src/notion-ui/phone-input/type.ts +227 -0
- package/src/notion-ui/phone-input/utils.ts +23 -0
package/cli/index.cjs
CHANGED
|
@@ -137,13 +137,95 @@ program
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Copy as a flat file into user's project
|
|
140
|
-
const destFile = path.join(config.componentDir, component + ".tsx");
|
|
141
|
-
fs.ensureDirSync(config.componentDir);
|
|
142
|
-
fs.copyFileSync(templateFile, destFile);
|
|
143
|
-
|
|
144
|
-
console.log(
|
|
145
|
-
|
|
146
|
-
);
|
|
140
|
+
// const destFile = path.join(config.componentDir, component + ".tsx");
|
|
141
|
+
// fs.ensureDirSync(config.componentDir);
|
|
142
|
+
// fs.copyFileSync(templateFile, destFile);
|
|
143
|
+
|
|
144
|
+
// console.log(
|
|
145
|
+
// chalk.green(`✓ Installed ${component} component as ${destFile}`)
|
|
146
|
+
// );
|
|
147
|
+
try {
|
|
148
|
+
const templateDir = path.join(__dirname, "../src/notion-ui", component);
|
|
149
|
+
const destDir = path.join(config.componentDir, component);
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(templateDir)) {
|
|
152
|
+
console.log(chalk.red(`❌ Component '${component}' does not exist.`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Step 1: Merge to utils ---
|
|
157
|
+
const utilsDir = path.join(cwd, "src/utils");
|
|
158
|
+
fs.ensureDirSync(utilsDir);
|
|
159
|
+
|
|
160
|
+
let mergeSuccess = true;
|
|
161
|
+
|
|
162
|
+
fs.readdirSync(templateDir).forEach((file) => {
|
|
163
|
+
const filePath = path.join(templateDir, file);
|
|
164
|
+
const content = fs.readFileSync(filePath, "utf-8").trim();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
let targetPath;
|
|
168
|
+
if (file.endsWith("-data.ts"))
|
|
169
|
+
targetPath = path.join(utilsDir, "dt.ts");
|
|
170
|
+
else if (file === "type.ts")
|
|
171
|
+
targetPath = path.join(utilsDir, "type.ts");
|
|
172
|
+
else if (file.startsWith("use-") && file.endsWith(".ts"))
|
|
173
|
+
targetPath = path.join(utilsDir, "hook.ts");
|
|
174
|
+
else return; // skip other files
|
|
175
|
+
|
|
176
|
+
let targetContent = "";
|
|
177
|
+
if (fs.existsSync(targetPath))
|
|
178
|
+
targetContent = fs.readFileSync(targetPath, "utf-8");
|
|
179
|
+
|
|
180
|
+
// Append only if content not already in the target file
|
|
181
|
+
if (!targetContent.includes(content)) {
|
|
182
|
+
if (targetContent.length > 0) targetContent += "\n\n";
|
|
183
|
+
targetContent += content;
|
|
184
|
+
fs.writeFileSync(targetPath, targetContent);
|
|
185
|
+
console.log(
|
|
186
|
+
chalk.green(
|
|
187
|
+
`✓ Merged ${file} → ${path.relative(cwd, targetPath)}`
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
console.log(
|
|
192
|
+
chalk.yellow(
|
|
193
|
+
`⚠ ${file} already exists in ${path.relative(
|
|
194
|
+
cwd,
|
|
195
|
+
targetPath
|
|
196
|
+
)}, skipping`
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
mergeSuccess = false;
|
|
202
|
+
console.log(chalk.red(`❌ Failed merging ${file}: ${err.message}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!mergeSuccess) {
|
|
207
|
+
console.log(
|
|
208
|
+
chalk.red(
|
|
209
|
+
"❌ Failed to merge required files. Component installation aborted."
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Step 2: Copy component files ---
|
|
216
|
+
fs.copySync(templateDir, destDir, {
|
|
217
|
+
filter: (src) => {
|
|
218
|
+
const filename = path.basename(src);
|
|
219
|
+
if (filename === "index.ts") return false;
|
|
220
|
+
if (filename.endsWith(".stories.tsx")) return false;
|
|
221
|
+
return true;
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
console.log(chalk.green(`✓ Installed ${component} to ${destDir}`));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.log(chalk.red(`❌ Installation failed: ${err.message}`));
|
|
228
|
+
}
|
|
147
229
|
});
|
|
148
230
|
|
|
149
231
|
/* ------------------------------
|
package/package.json
CHANGED
|
@@ -25,7 +25,6 @@ export interface DatePickerProps {
|
|
|
25
25
|
};
|
|
26
26
|
placeholder: string;
|
|
27
27
|
place?: string;
|
|
28
|
-
required?: boolean;
|
|
29
28
|
format?: string;
|
|
30
29
|
requiredHint?: string;
|
|
31
30
|
hintColor?: string;
|
|
@@ -42,7 +41,6 @@ export default function DatePicker(props: DatePickerProps) {
|
|
|
42
41
|
className,
|
|
43
42
|
classNames,
|
|
44
43
|
placeholder,
|
|
45
|
-
required,
|
|
46
44
|
requiredHint,
|
|
47
45
|
measurement,
|
|
48
46
|
label,
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { ParsedCountry } from "./type";
|
|
2
|
+
|
|
3
|
+
export const defaultCountries: ParsedCountry[] = [
|
|
4
|
+
{ name: "Afghanistan", iso2: "af", dialCode: "93" },
|
|
5
|
+
{ name: "Albania", iso2: "al", dialCode: "355" },
|
|
6
|
+
{ name: "Algeria", iso2: "dz", dialCode: "213" },
|
|
7
|
+
{ name: "Andorra", iso2: "ad", dialCode: "376" },
|
|
8
|
+
{ name: "Angola", iso2: "ao", dialCode: "244" },
|
|
9
|
+
{ name: "Antigua and Barbuda", iso2: "ag", dialCode: "1268" },
|
|
10
|
+
{
|
|
11
|
+
name: "Argentina",
|
|
12
|
+
iso2: "ar",
|
|
13
|
+
dialCode: "54",
|
|
14
|
+
format: {
|
|
15
|
+
default: "(..) .... ....",
|
|
16
|
+
"/^11/": "(..) .... ....",
|
|
17
|
+
"/^15/": "(..) ... ....",
|
|
18
|
+
"/^(2|3|4|5)/": "(.) .... ....",
|
|
19
|
+
"/^9/": "(.) .... .....",
|
|
20
|
+
},
|
|
21
|
+
priority: 0,
|
|
22
|
+
},
|
|
23
|
+
{ name: "Armenia", iso2: "am", dialCode: "374", format: ".. ......" },
|
|
24
|
+
{ name: "Aruba", iso2: "aw", dialCode: "297" },
|
|
25
|
+
{
|
|
26
|
+
name: "Australia",
|
|
27
|
+
iso2: "au",
|
|
28
|
+
dialCode: "61",
|
|
29
|
+
format: {
|
|
30
|
+
default: ". .... ....",
|
|
31
|
+
"/^4/": "... ... ...",
|
|
32
|
+
"/^5(?!50)/": "... ... ...",
|
|
33
|
+
"/^1(3|8)00/": ".... ... ...",
|
|
34
|
+
"/^13/": ".. .. ..",
|
|
35
|
+
"/^180/": "... ....",
|
|
36
|
+
},
|
|
37
|
+
priority: 0,
|
|
38
|
+
areaCodes: [],
|
|
39
|
+
},
|
|
40
|
+
{ name: "Austria", iso2: "at", dialCode: "43" },
|
|
41
|
+
{ name: "Azerbaijan", iso2: "az", dialCode: "994", format: "(..) ... .. .." },
|
|
42
|
+
{ name: "Bahamas", iso2: "bs", dialCode: "1242" },
|
|
43
|
+
{ name: "Bahrain", iso2: "bh", dialCode: "973" },
|
|
44
|
+
{ name: "Bangladesh", iso2: "bd", dialCode: "880" },
|
|
45
|
+
{ name: "Barbados", iso2: "bb", dialCode: "1246" },
|
|
46
|
+
{ name: "Belarus", iso2: "by", dialCode: "375", format: "(..) ... .. .." },
|
|
47
|
+
{ name: "Belgium", iso2: "be", dialCode: "32", format: "... .. .. .." },
|
|
48
|
+
{ name: "Belize", iso2: "bz", dialCode: "501" },
|
|
49
|
+
{ name: "Benin", iso2: "bj", dialCode: "229" },
|
|
50
|
+
{ name: "Bhutan", iso2: "bt", dialCode: "975" },
|
|
51
|
+
{ name: "Bolivia", iso2: "bo", dialCode: "591" },
|
|
52
|
+
{ name: "Bosnia and Herzegovina", iso2: "ba", dialCode: "387" },
|
|
53
|
+
{ name: "Botswana", iso2: "bw", dialCode: "267" },
|
|
54
|
+
{ name: "Brazil", iso2: "br", dialCode: "55", format: "(..) .....-...." },
|
|
55
|
+
{ name: "British Indian Ocean Territory", iso2: "io", dialCode: "246" },
|
|
56
|
+
{ name: "Brunei", iso2: "bn", dialCode: "673" },
|
|
57
|
+
{ name: "Bulgaria", iso2: "bg", dialCode: "359" },
|
|
58
|
+
{ name: "Burkina Faso", iso2: "bf", dialCode: "226" },
|
|
59
|
+
{ name: "Burundi", iso2: "bi", dialCode: "257" },
|
|
60
|
+
{ name: "Cambodia", iso2: "kh", dialCode: "855" },
|
|
61
|
+
{ name: "Cameroon", iso2: "cm", dialCode: "237" },
|
|
62
|
+
{
|
|
63
|
+
name: "Canada",
|
|
64
|
+
iso2: "ca",
|
|
65
|
+
dialCode: "1",
|
|
66
|
+
format: "(...) ...-....",
|
|
67
|
+
priority: 1,
|
|
68
|
+
areaCodes: [
|
|
69
|
+
"204",
|
|
70
|
+
"226",
|
|
71
|
+
"236",
|
|
72
|
+
"249",
|
|
73
|
+
"250",
|
|
74
|
+
"289",
|
|
75
|
+
"306",
|
|
76
|
+
"343",
|
|
77
|
+
"365",
|
|
78
|
+
"387",
|
|
79
|
+
"403",
|
|
80
|
+
"416",
|
|
81
|
+
"418",
|
|
82
|
+
"431",
|
|
83
|
+
"437",
|
|
84
|
+
"438",
|
|
85
|
+
"450",
|
|
86
|
+
"506",
|
|
87
|
+
"514",
|
|
88
|
+
"519",
|
|
89
|
+
"548",
|
|
90
|
+
"579",
|
|
91
|
+
"581",
|
|
92
|
+
"587",
|
|
93
|
+
"604",
|
|
94
|
+
"613",
|
|
95
|
+
"639",
|
|
96
|
+
"647",
|
|
97
|
+
"672",
|
|
98
|
+
"705",
|
|
99
|
+
"709",
|
|
100
|
+
"742",
|
|
101
|
+
"778",
|
|
102
|
+
"780",
|
|
103
|
+
"782",
|
|
104
|
+
"807",
|
|
105
|
+
"819",
|
|
106
|
+
"825",
|
|
107
|
+
"867",
|
|
108
|
+
"873",
|
|
109
|
+
"902",
|
|
110
|
+
"905",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{ name: "Cape Verde", iso2: "cv", dialCode: "238" },
|
|
114
|
+
{ name: "Caribbean Netherlands", iso2: "bq", dialCode: "599", priority: 1 },
|
|
115
|
+
{
|
|
116
|
+
name: "Cayman Islands",
|
|
117
|
+
iso2: "ky",
|
|
118
|
+
dialCode: "1",
|
|
119
|
+
format: "... ... ....",
|
|
120
|
+
priority: 4,
|
|
121
|
+
areaCodes: ["345"],
|
|
122
|
+
},
|
|
123
|
+
{ name: "Central African Republic", iso2: "cf", dialCode: "236" },
|
|
124
|
+
{ name: "Chad", iso2: "td", dialCode: "235" },
|
|
125
|
+
{ name: "Chile", iso2: "cl", dialCode: "56" },
|
|
126
|
+
{ name: "China", iso2: "cn", dialCode: "86", format: "... .... ...." },
|
|
127
|
+
{ name: "Colombia", iso2: "co", dialCode: "57", format: "... ... ...." },
|
|
128
|
+
{ name: "Comoros", iso2: "km", dialCode: "269" },
|
|
129
|
+
{ name: "Congo", iso2: "cd", dialCode: "243" },
|
|
130
|
+
{ name: "Congo", iso2: "cg", dialCode: "242" },
|
|
131
|
+
{ name: "Costa Rica", iso2: "cr", dialCode: "506", format: "....-...." },
|
|
132
|
+
{
|
|
133
|
+
name: "Côte d'Ivoire",
|
|
134
|
+
iso2: "ci",
|
|
135
|
+
dialCode: "225",
|
|
136
|
+
format: ".. .. .. .. ..",
|
|
137
|
+
},
|
|
138
|
+
{ name: "Croatia", iso2: "hr", dialCode: "385" },
|
|
139
|
+
{ name: "Cuba", iso2: "cu", dialCode: "53" },
|
|
140
|
+
{ name: "Curaçao", iso2: "cw", dialCode: "599", priority: 0 },
|
|
141
|
+
{ name: "Cyprus", iso2: "cy", dialCode: "357", format: ".. ......" },
|
|
142
|
+
{
|
|
143
|
+
name: "Czech Republic",
|
|
144
|
+
iso2: "cz",
|
|
145
|
+
dialCode: "420",
|
|
146
|
+
format: "... ... ...",
|
|
147
|
+
},
|
|
148
|
+
{ name: "Denmark", iso2: "dk", dialCode: "45", format: ".. .. .. .." },
|
|
149
|
+
{ name: "Djibouti", iso2: "dj", dialCode: "253", format: ".. .. ...." },
|
|
150
|
+
{ name: "Dominica", iso2: "dm", dialCode: "1767" },
|
|
151
|
+
{
|
|
152
|
+
name: "Dominican Republic",
|
|
153
|
+
iso2: "do",
|
|
154
|
+
dialCode: "1",
|
|
155
|
+
format: "(...) ...-....",
|
|
156
|
+
priority: 2,
|
|
157
|
+
areaCodes: ["809", "829", "849"],
|
|
158
|
+
},
|
|
159
|
+
{ name: "Ecuador", iso2: "ec", dialCode: "593" },
|
|
160
|
+
{ name: "Egypt", iso2: "eg", dialCode: "20" },
|
|
161
|
+
{ name: "El Salvador", iso2: "sv", dialCode: "503", format: "....-...." },
|
|
162
|
+
{ name: "Equatorial Guinea", iso2: "gq", dialCode: "240" },
|
|
163
|
+
{ name: "Eritrea", iso2: "er", dialCode: "291" },
|
|
164
|
+
{ name: "Estonia", iso2: "ee", dialCode: "372", format: ".... ......" },
|
|
165
|
+
{ name: "Ethiopia", iso2: "et", dialCode: "251", format: ".. ... ...." },
|
|
166
|
+
{ name: "Faroe Islands", iso2: "fo", dialCode: "298", format: ".. .. .." },
|
|
167
|
+
{ name: "Fiji", iso2: "fj", dialCode: "679" },
|
|
168
|
+
{ name: "Finland", iso2: "fi", dialCode: "358", format: ".. ... .. .." },
|
|
169
|
+
{ name: "France", iso2: "fr", dialCode: "33", format: ". .. .. .. .." },
|
|
170
|
+
{
|
|
171
|
+
name: "French Guiana",
|
|
172
|
+
iso2: "gf",
|
|
173
|
+
dialCode: "594",
|
|
174
|
+
format: "... .. .. ..",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "French Polynesia",
|
|
178
|
+
iso2: "pf",
|
|
179
|
+
dialCode: "689",
|
|
180
|
+
format: {
|
|
181
|
+
"/^44/": ".. .. ..",
|
|
182
|
+
"/^80[0-5]/": "... .. .. ..",
|
|
183
|
+
default: ".. .. .. ..",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{ name: "Gabon", iso2: "ga", dialCode: "241" },
|
|
187
|
+
{ name: "Gambia", iso2: "gm", dialCode: "220" },
|
|
188
|
+
{ name: "Georgia", iso2: "ge", dialCode: "995" },
|
|
189
|
+
{ name: "Germany", iso2: "de", dialCode: "49", format: "... ........." },
|
|
190
|
+
{ name: "Ghana", iso2: "gh", dialCode: "233" },
|
|
191
|
+
{ name: "Greece", iso2: "gr", dialCode: "30" },
|
|
192
|
+
{ name: "Greenland", iso2: "gl", dialCode: "299", format: ".. .. .." },
|
|
193
|
+
{ name: "Grenada", iso2: "gd", dialCode: "1473" },
|
|
194
|
+
{
|
|
195
|
+
name: "Guadeloupe",
|
|
196
|
+
iso2: "gp",
|
|
197
|
+
dialCode: "590",
|
|
198
|
+
format: "... .. .. ..",
|
|
199
|
+
priority: 0,
|
|
200
|
+
},
|
|
201
|
+
{ name: "Guam", iso2: "gu", dialCode: "1671" },
|
|
202
|
+
{ name: "Guatemala", iso2: "gt", dialCode: "502", format: "....-...." },
|
|
203
|
+
{ name: "Guinea", iso2: "gn", dialCode: "224" },
|
|
204
|
+
{ name: "Guinea-Bissau", iso2: "gw", dialCode: "245" },
|
|
205
|
+
{ name: "Guyana", iso2: "gy", dialCode: "592" },
|
|
206
|
+
{ name: "Haiti", iso2: "ht", dialCode: "509", format: "....-...." },
|
|
207
|
+
{ name: "Honduras", iso2: "hn", dialCode: "504" },
|
|
208
|
+
{ name: "Hong Kong", iso2: "hk", dialCode: "852", format: ".... ...." },
|
|
209
|
+
{ name: "Hungary", iso2: "hu", dialCode: "36" },
|
|
210
|
+
{ name: "Iceland", iso2: "is", dialCode: "354", format: "... ...." },
|
|
211
|
+
{ name: "India", iso2: "in", dialCode: "91", format: ".....-....." },
|
|
212
|
+
{ name: "Indonesia", iso2: "id", dialCode: "62" },
|
|
213
|
+
{ name: "Iran", iso2: "ir", dialCode: "98", format: "... ... ...." },
|
|
214
|
+
{ name: "Iraq", iso2: "iq", dialCode: "964" },
|
|
215
|
+
{ name: "Ireland", iso2: "ie", dialCode: "353", format: ".. ......." },
|
|
216
|
+
{ name: "Israel", iso2: "il", dialCode: "972", format: "... ... ...." },
|
|
217
|
+
{
|
|
218
|
+
name: "Italy",
|
|
219
|
+
iso2: "it",
|
|
220
|
+
dialCode: "39",
|
|
221
|
+
format: "... .......",
|
|
222
|
+
priority: 0,
|
|
223
|
+
},
|
|
224
|
+
{ name: "Jamaica", iso2: "jm", dialCode: "1876" },
|
|
225
|
+
{ name: "Japan", iso2: "jp", dialCode: "81", format: ".. .... ...." },
|
|
226
|
+
{ name: "Jordan", iso2: "jo", dialCode: "962" },
|
|
227
|
+
{
|
|
228
|
+
name: "Kazakhstan",
|
|
229
|
+
iso2: "kz",
|
|
230
|
+
dialCode: "7",
|
|
231
|
+
format: "... ...-..-..",
|
|
232
|
+
priority: 0,
|
|
233
|
+
},
|
|
234
|
+
{ name: "Kenya", iso2: "ke", dialCode: "254" },
|
|
235
|
+
{ name: "Kiribati", iso2: "ki", dialCode: "686" },
|
|
236
|
+
{ name: "Kosovo", iso2: "xk", dialCode: "383" },
|
|
237
|
+
{ name: "Kuwait", iso2: "kw", dialCode: "965" },
|
|
238
|
+
{ name: "Kyrgyzstan", iso2: "kg", dialCode: "996", format: "... ... ..." },
|
|
239
|
+
{ name: "Laos", iso2: "la", dialCode: "856" },
|
|
240
|
+
{ name: "Latvia", iso2: "lv", dialCode: "371", format: ".. ... ..." },
|
|
241
|
+
{ name: "Lebanon", iso2: "lb", dialCode: "961" },
|
|
242
|
+
{ name: "Lesotho", iso2: "ls", dialCode: "266" },
|
|
243
|
+
{ name: "Liberia", iso2: "lr", dialCode: "231" },
|
|
244
|
+
{ name: "Libya", iso2: "ly", dialCode: "218" },
|
|
245
|
+
{ name: "Liechtenstein", iso2: "li", dialCode: "423" },
|
|
246
|
+
{ name: "Lithuania", iso2: "lt", dialCode: "370" },
|
|
247
|
+
{ name: "Luxembourg", iso2: "lu", dialCode: "352" },
|
|
248
|
+
{ name: "Macau", iso2: "mo", dialCode: "853" },
|
|
249
|
+
{ name: "Macedonia", iso2: "mk", dialCode: "389" },
|
|
250
|
+
{ name: "Madagascar", iso2: "mg", dialCode: "261" },
|
|
251
|
+
{ name: "Malawi", iso2: "mw", dialCode: "265" },
|
|
252
|
+
{ name: "Malaysia", iso2: "my", dialCode: "60", format: "..-....-...." },
|
|
253
|
+
{ name: "Maldives", iso2: "mv", dialCode: "960" },
|
|
254
|
+
{ name: "Mali", iso2: "ml", dialCode: "223" },
|
|
255
|
+
{ name: "Malta", iso2: "mt", dialCode: "356" },
|
|
256
|
+
{ name: "Marshall Islands", iso2: "mh", dialCode: "692" },
|
|
257
|
+
{ name: "Martinique", iso2: "mq", dialCode: "596", format: "... .. .. .." },
|
|
258
|
+
{ name: "Mauritania", iso2: "mr", dialCode: "222" },
|
|
259
|
+
{ name: "Mauritius", iso2: "mu", dialCode: "230" },
|
|
260
|
+
{
|
|
261
|
+
name: "Mayotte",
|
|
262
|
+
iso2: "yt",
|
|
263
|
+
dialCode: "262",
|
|
264
|
+
format: "... .. .. ..",
|
|
265
|
+
priority: 1,
|
|
266
|
+
areaCodes: ["269", "639"],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "Mexico",
|
|
270
|
+
iso2: "mx",
|
|
271
|
+
dialCode: "52",
|
|
272
|
+
format: "... ... ....",
|
|
273
|
+
priority: 0,
|
|
274
|
+
},
|
|
275
|
+
{ name: "Micronesia", iso2: "fm", dialCode: "691" },
|
|
276
|
+
{ name: "Moldova", iso2: "md", dialCode: "373", format: "(..) ..-..-.." },
|
|
277
|
+
{ name: "Monaco", iso2: "mc", dialCode: "377" },
|
|
278
|
+
{ name: "Mongolia", iso2: "mn", dialCode: "976" },
|
|
279
|
+
{ name: "Montenegro", iso2: "me", dialCode: "382" },
|
|
280
|
+
{ name: "Morocco", iso2: "ma", dialCode: "212" },
|
|
281
|
+
{ name: "Mozambique", iso2: "mz", dialCode: "258" },
|
|
282
|
+
{ name: "Myanmar", iso2: "mm", dialCode: "95" },
|
|
283
|
+
{ name: "Namibia", iso2: "na", dialCode: "264" },
|
|
284
|
+
{ name: "Nauru", iso2: "nr", dialCode: "674" },
|
|
285
|
+
{ name: "Nepal", iso2: "np", dialCode: "977" },
|
|
286
|
+
{
|
|
287
|
+
name: "Netherlands",
|
|
288
|
+
iso2: "nl",
|
|
289
|
+
dialCode: "31",
|
|
290
|
+
format: {
|
|
291
|
+
"/^06/": "(.). .........",
|
|
292
|
+
"/^6/": ". .........",
|
|
293
|
+
"/^0(10|13|14|15|20|23|24|26|30|33|35|36|38|40|43|44|45|46|50|53|55|58|70|71|72|73|74|75|76|77|78|79|82|84|85|87|88|91)/":
|
|
294
|
+
"(.).. ........",
|
|
295
|
+
"/^(10|13|14|15|20|23|24|26|30|33|35|36|38|40|43|44|45|46|50|53|55|58|70|71|72|73|74|75|76|77|78|79|82|84|85|87|88|91)/":
|
|
296
|
+
".. ........",
|
|
297
|
+
"/^0/": "(.)... .......",
|
|
298
|
+
default: "... .......",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{ name: "New Caledonia", iso2: "nc", dialCode: "687" },
|
|
302
|
+
{ name: "New Zealand", iso2: "nz", dialCode: "64", format: "...-...-...." },
|
|
303
|
+
{ name: "Nicaragua", iso2: "ni", dialCode: "505" },
|
|
304
|
+
{ name: "Niger", iso2: "ne", dialCode: "227" },
|
|
305
|
+
{ name: "Nigeria", iso2: "ng", dialCode: "234" },
|
|
306
|
+
{ name: "North Korea", iso2: "kp", dialCode: "850" },
|
|
307
|
+
{ name: "Norway", iso2: "no", dialCode: "47", format: "... .. ..." },
|
|
308
|
+
{ name: "Oman", iso2: "om", dialCode: "968" },
|
|
309
|
+
{ name: "Pakistan", iso2: "pk", dialCode: "92", format: "...-......." },
|
|
310
|
+
{ name: "Palau", iso2: "pw", dialCode: "680" },
|
|
311
|
+
{ name: "Palestine", iso2: "ps", dialCode: "970" },
|
|
312
|
+
{ name: "Panama", iso2: "pa", dialCode: "507" },
|
|
313
|
+
{ name: "Papua New Guinea", iso2: "pg", dialCode: "675" },
|
|
314
|
+
{ name: "Paraguay", iso2: "py", dialCode: "595" },
|
|
315
|
+
{ name: "Peru", iso2: "pe", dialCode: "51" },
|
|
316
|
+
{ name: "Philippines", iso2: "ph", dialCode: "63", format: "... ... ...." },
|
|
317
|
+
{ name: "Poland", iso2: "pl", dialCode: "48", format: "...-...-..." },
|
|
318
|
+
{ name: "Portugal", iso2: "pt", dialCode: "351" },
|
|
319
|
+
{
|
|
320
|
+
name: "Puerto Rico",
|
|
321
|
+
iso2: "pr",
|
|
322
|
+
dialCode: "1",
|
|
323
|
+
format: "(...) ...-....",
|
|
324
|
+
priority: 3,
|
|
325
|
+
areaCodes: ["787", "939"],
|
|
326
|
+
},
|
|
327
|
+
{ name: "Qatar", iso2: "qa", dialCode: "974" },
|
|
328
|
+
{
|
|
329
|
+
name: "Réunion",
|
|
330
|
+
iso2: "re",
|
|
331
|
+
dialCode: "262",
|
|
332
|
+
format: "... .. .. ..",
|
|
333
|
+
priority: 0,
|
|
334
|
+
},
|
|
335
|
+
{ name: "Romania", iso2: "ro", dialCode: "40" },
|
|
336
|
+
{
|
|
337
|
+
name: "Russia",
|
|
338
|
+
iso2: "ru",
|
|
339
|
+
dialCode: "7",
|
|
340
|
+
format: "(...) ...-..-..",
|
|
341
|
+
priority: 1,
|
|
342
|
+
},
|
|
343
|
+
{ name: "Rwanda", iso2: "rw", dialCode: "250" },
|
|
344
|
+
{ name: "Saint Kitts and Nevis", iso2: "kn", dialCode: "1869" },
|
|
345
|
+
{ name: "Saint Lucia", iso2: "lc", dialCode: "1758" },
|
|
346
|
+
{
|
|
347
|
+
name: "Saint Pierre & Miquelon",
|
|
348
|
+
iso2: "pm",
|
|
349
|
+
dialCode: "508",
|
|
350
|
+
format: {
|
|
351
|
+
"/^708/": "... ... ...",
|
|
352
|
+
"/^8/": "... .. .. ..",
|
|
353
|
+
default: ".. .. ..",
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{ name: "Saint Vincent and the Grenadines", iso2: "vc", dialCode: "1784" },
|
|
357
|
+
{ name: "Samoa", iso2: "ws", dialCode: "685" },
|
|
358
|
+
{ name: "San Marino", iso2: "sm", dialCode: "378" },
|
|
359
|
+
{ name: "São Tomé and Príncipe", iso2: "st", dialCode: "239" },
|
|
360
|
+
{ name: "Saudi Arabia", iso2: "sa", dialCode: "966" },
|
|
361
|
+
{ name: "Senegal", iso2: "sn", dialCode: "221" },
|
|
362
|
+
{ name: "Serbia", iso2: "rs", dialCode: "381" },
|
|
363
|
+
{ name: "Seychelles", iso2: "sc", dialCode: "248" },
|
|
364
|
+
{ name: "Sierra Leone", iso2: "sl", dialCode: "232" },
|
|
365
|
+
{ name: "Singapore", iso2: "sg", dialCode: "65", format: "....-...." },
|
|
366
|
+
{ name: "Slovakia", iso2: "sk", dialCode: "421" },
|
|
367
|
+
{ name: "Slovenia", iso2: "si", dialCode: "386" },
|
|
368
|
+
{ name: "Solomon Islands", iso2: "sb", dialCode: "677" },
|
|
369
|
+
{ name: "Somalia", iso2: "so", dialCode: "252" },
|
|
370
|
+
{ name: "South Africa", iso2: "za", dialCode: "27" },
|
|
371
|
+
{ name: "South Korea", iso2: "kr", dialCode: "82", format: "... .... ...." },
|
|
372
|
+
{ name: "South Sudan", iso2: "ss", dialCode: "211" },
|
|
373
|
+
{ name: "Spain", iso2: "es", dialCode: "34", format: "... ... ..." },
|
|
374
|
+
{ name: "Sri Lanka", iso2: "lk", dialCode: "94" },
|
|
375
|
+
{ name: "Sudan", iso2: "sd", dialCode: "249" },
|
|
376
|
+
{ name: "Suriname", iso2: "sr", dialCode: "597" },
|
|
377
|
+
{ name: "Swaziland", iso2: "sz", dialCode: "268" },
|
|
378
|
+
{ name: "Sweden", iso2: "se", dialCode: "46", format: "... ... ..." },
|
|
379
|
+
{ name: "Switzerland", iso2: "ch", dialCode: "41", format: ".. ... .. .." },
|
|
380
|
+
{ name: "Syria", iso2: "sy", dialCode: "963" },
|
|
381
|
+
{ name: "Taiwan", iso2: "tw", dialCode: "886" },
|
|
382
|
+
{ name: "Tajikistan", iso2: "tj", dialCode: "992" },
|
|
383
|
+
{ name: "Tanzania", iso2: "tz", dialCode: "255" },
|
|
384
|
+
{ name: "Thailand", iso2: "th", dialCode: "66" },
|
|
385
|
+
{ name: "Timor-Leste", iso2: "tl", dialCode: "670" },
|
|
386
|
+
{ name: "Togo", iso2: "tg", dialCode: "228" },
|
|
387
|
+
{ name: "Tonga", iso2: "to", dialCode: "676" },
|
|
388
|
+
{ name: "Trinidad and Tobago", iso2: "tt", dialCode: "1868" },
|
|
389
|
+
{ name: "Tunisia", iso2: "tn", dialCode: "216" },
|
|
390
|
+
{ name: "Turkey", iso2: "tr", dialCode: "90", format: "... ... .. .." },
|
|
391
|
+
{ name: "Turkmenistan", iso2: "tm", dialCode: "993" },
|
|
392
|
+
{ name: "Tuvalu", iso2: "tv", dialCode: "688" },
|
|
393
|
+
{ name: "Uganda", iso2: "ug", dialCode: "256" },
|
|
394
|
+
{ name: "Ukraine", iso2: "ua", dialCode: "380", format: "(..) ... .. .." },
|
|
395
|
+
{ name: "United Arab Emirates", iso2: "ae", dialCode: "971" },
|
|
396
|
+
{ name: "United Kingdom", iso2: "gb", dialCode: "44", format: ".... ......" },
|
|
397
|
+
{
|
|
398
|
+
name: "United States",
|
|
399
|
+
iso2: "us",
|
|
400
|
+
dialCode: "1",
|
|
401
|
+
format: "(...) ...-....",
|
|
402
|
+
priority: 0,
|
|
403
|
+
},
|
|
404
|
+
{ name: "Uruguay", iso2: "uy", dialCode: "598" },
|
|
405
|
+
{ name: "Uzbekistan", iso2: "uz", dialCode: "998", format: ".. ... .. .." },
|
|
406
|
+
{ name: "Vanuatu", iso2: "vu", dialCode: "678" },
|
|
407
|
+
{
|
|
408
|
+
name: "Vatican City",
|
|
409
|
+
iso2: "va",
|
|
410
|
+
dialCode: "39",
|
|
411
|
+
format: ".. .... ....",
|
|
412
|
+
priority: 1,
|
|
413
|
+
},
|
|
414
|
+
{ name: "Venezuela", iso2: "ve", dialCode: "58" },
|
|
415
|
+
{ name: "Vietnam", iso2: "vn", dialCode: "84" },
|
|
416
|
+
{ name: "Wallis & Futuna", iso2: "wf", dialCode: "681", format: ".. .. .." },
|
|
417
|
+
{ name: "Yemen", iso2: "ye", dialCode: "967" },
|
|
418
|
+
{ name: "Zambia", iso2: "zm", dialCode: "260" },
|
|
419
|
+
{ name: "Zimbabwe", iso2: "zw", dialCode: "263" },
|
|
420
|
+
];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getFlagCodepoint } from "./utils";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export const LazyFlag: React.FC<{
|
|
5
|
+
iso2: string;
|
|
6
|
+
className?: string | string;
|
|
7
|
+
}> = ({ iso2, className }) => {
|
|
8
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
9
|
+
const [svgContent, setSvgContent] = useState<string | undefined>(undefined);
|
|
10
|
+
const codepoint = getFlagCodepoint(iso2);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (!el) return;
|
|
15
|
+
|
|
16
|
+
const io = new IntersectionObserver(
|
|
17
|
+
(entries) => {
|
|
18
|
+
if (entries[0].isIntersecting) {
|
|
19
|
+
const fullUrl = `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${codepoint}.svg`;
|
|
20
|
+
|
|
21
|
+
const fetchAndCacheSvg = async () => {
|
|
22
|
+
try {
|
|
23
|
+
let response = await fetch(fullUrl);
|
|
24
|
+
if (response.ok) {
|
|
25
|
+
const responseClone = response.clone(); // clone BEFORE consuming body
|
|
26
|
+
const svgText = await response.text();
|
|
27
|
+
|
|
28
|
+
setSvgContent(svgText);
|
|
29
|
+
|
|
30
|
+
if ("caches" in window) {
|
|
31
|
+
const cache = await caches.open("flags-cache");
|
|
32
|
+
await cache.put(codepoint, responseClone);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
console.error("Failed to fetch SVG", response.statusText);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error fetching SVG:", error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const checkCache = async () => {
|
|
42
|
+
if (!("caches" in window)) {
|
|
43
|
+
fetchAndCacheSvg();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cache = await caches.open("flags-cache");
|
|
48
|
+
const cachedResponse = await cache.match(codepoint);
|
|
49
|
+
|
|
50
|
+
if (cachedResponse) {
|
|
51
|
+
const svgText = await cachedResponse.text();
|
|
52
|
+
|
|
53
|
+
setSvgContent(svgText);
|
|
54
|
+
} else {
|
|
55
|
+
fetchAndCacheSvg();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
checkCache();
|
|
59
|
+
|
|
60
|
+
io.disconnect();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ rootMargin: "550px" }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
io.observe(el);
|
|
67
|
+
return () => io.disconnect();
|
|
68
|
+
}, [codepoint]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div ref={ref}>
|
|
72
|
+
{!svgContent ? (
|
|
73
|
+
<div className={`bg-primary rounded-full size-4 animate-pulse`} />
|
|
74
|
+
) : (
|
|
75
|
+
<div
|
|
76
|
+
ref={ref}
|
|
77
|
+
className={className}
|
|
78
|
+
dangerouslySetInnerHTML={{ __html: svgContent }}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { defaultCountries } from "./country-data";
|
|
2
|
+
import { LazyFlag } from "./lazy-flag";
|
|
3
|
+
import type { ParsedCountry } from "./type";
|
|
4
|
+
import { cn } from "@/utils/cn";
|
|
5
|
+
import React, {
|
|
6
|
+
useState,
|
|
7
|
+
useRef,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { createPortal } from "react-dom";
|
|
13
|
+
|
|
14
|
+
interface VirtualListProps {
|
|
15
|
+
items: ParsedCountry[];
|
|
16
|
+
renderRow: (item: ParsedCountry, index: number) => React.ReactNode;
|
|
17
|
+
height: number;
|
|
18
|
+
ROW_HEIGHT: number;
|
|
19
|
+
BUFFER: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VirtualList: React.FC<VirtualListProps> = ({
|
|
23
|
+
items,
|
|
24
|
+
renderRow,
|
|
25
|
+
height,
|
|
26
|
+
ROW_HEIGHT,
|
|
27
|
+
BUFFER,
|
|
28
|
+
}) => {
|
|
29
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
30
|
+
|
|
31
|
+
const totalHeight = items.length * ROW_HEIGHT;
|
|
32
|
+
|
|
33
|
+
// calculate visible indices
|
|
34
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER);
|
|
35
|
+
const endIndex = Math.min(
|
|
36
|
+
items.length,
|
|
37
|
+
Math.ceil((scrollTop + height) / ROW_HEIGHT) + BUFFER
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Slice items to render
|
|
41
|
+
const visibleItems = items.slice(startIndex, endIndex);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className="overflow-y-auto max-h-60"
|
|
46
|
+
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
|
|
47
|
+
>
|
|
48
|
+
<div style={{ height: totalHeight, position: "relative" }}>
|
|
49
|
+
{visibleItems.map((item, i) => {
|
|
50
|
+
const index = startIndex + i; // absolute index
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
key={`${item.iso2}-${index}`} // absolute key ensures React updates
|
|
54
|
+
style={{
|
|
55
|
+
position: "absolute",
|
|
56
|
+
top: index * ROW_HEIGHT,
|
|
57
|
+
left: 0,
|
|
58
|
+
right: 0,
|
|
59
|
+
height: ROW_HEIGHT,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{renderRow(item, index)} {/* pass absolute index */}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
export type PhoneInputSize = "sm" | "md" | "lg";
|
|
71
|
+
|
|
72
|
+
interface PhoneInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
73
|
+
requiredHint?: string;
|
|
74
|
+
label?: string;
|
|
75
|
+
errorMessage?: string;
|
|
76
|
+
classNames?: {
|
|
77
|
+
rootDivClassName?: string;
|
|
78
|
+
iconClassName?: string;
|
|
79
|
+
};
|
|
80
|
+
measurement?: PhoneInputSize;
|
|
81
|
+
ROW_HEIGHT?: number;
|
|
82
|
+
VISIBLE_ROWS?: number;
|
|
83
|
+
BUFFER?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
87
|
+
measurement = "sm",
|
|
88
|
+
errorMessage,
|
|
89
|
+
label,
|
|
90
|
+
readOnly,
|
|
91
|
+
className,
|
|
92
|
+
classNames,
|
|
93
|
+
requiredHint,
|
|
94
|
+
value,
|
|
95
|
+
onChange,
|
|
96
|
+
ROW_HEIGHT = 32,
|
|
97
|
+
VISIBLE_ROWS = 10,
|
|
98
|
+
BUFFER = 5,
|
|
99
|
+
...rest
|
|
100
|
+
}) => {
|
|
101
|
+
const { rootDivClassName, iconClassName = "size-4" } = classNames || {};
|
|
102
|
+
const [open, setOpen] = useState(false);
|
|
103
|
+
const [country, setCountry] = useState<ParsedCountry>(defaultCountries[0]);
|
|
104
|
+
const [highlightedIndex, setHighlightedIndex] = useState<number>(0);
|
|
105
|
+
const [phone, setPhone] = useState<string>(
|
|
106
|
+
typeof value == "string" ? value : ""
|
|
107
|
+
);
|
|
108
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
109
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
110
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
111
|
+
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
112
|
+
|
|
113
|
+
const [dropDirection, setDropDirection] = useState<"down" | "up">("down");
|
|
114
|
+
|
|
115
|
+
const hasError = !!errorMessage;
|
|
116
|
+
|
|
117
|
+
// Choose country
|
|
118
|
+
const chooseCountry = (c: ParsedCountry) => {
|
|
119
|
+
setCountry(c);
|
|
120
|
+
setOpen(false);
|
|
121
|
+
inputRef.current?.focus();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Keyboard navigation
|
|
125
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
126
|
+
if (!open) {
|
|
127
|
+
if (e.key === "ArrowDown" || e.key === "Enter") {
|
|
128
|
+
setOpen(true);
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (e.key === "ArrowDown") {
|
|
135
|
+
setHighlightedIndex((prev) =>
|
|
136
|
+
Math.min(prev + 1, defaultCountries.length - 1)
|
|
137
|
+
);
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
} else if (e.key === "ArrowUp") {
|
|
140
|
+
setHighlightedIndex((prev) => Math.max(prev - 1, 0));
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
} else if (e.key === "Enter") {
|
|
143
|
+
chooseCountry(defaultCountries[highlightedIndex]);
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
} else if (e.key === "Escape") {
|
|
146
|
+
setOpen(false);
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Scroll highlighted item into view
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!open || !dropdownRef.current) return;
|
|
154
|
+
|
|
155
|
+
// Get the scrollable container inside the virtual list
|
|
156
|
+
const scrollableContainer =
|
|
157
|
+
dropdownRef.current.querySelector(".overflow-y-auto");
|
|
158
|
+
if (!scrollableContainer) return;
|
|
159
|
+
|
|
160
|
+
const rowTop = highlightedIndex * ROW_HEIGHT;
|
|
161
|
+
const rowBottom = rowTop + ROW_HEIGHT;
|
|
162
|
+
const scrollContainer = scrollableContainer as HTMLDivElement;
|
|
163
|
+
|
|
164
|
+
if (rowTop < scrollContainer.scrollTop) {
|
|
165
|
+
scrollContainer.scrollTop = rowTop;
|
|
166
|
+
} else if (
|
|
167
|
+
rowBottom >
|
|
168
|
+
scrollContainer.scrollTop + ROW_HEIGHT * VISIBLE_ROWS
|
|
169
|
+
) {
|
|
170
|
+
scrollContainer.scrollTop = rowBottom - ROW_HEIGHT * VISIBLE_ROWS;
|
|
171
|
+
}
|
|
172
|
+
}, [highlightedIndex, open]);
|
|
173
|
+
|
|
174
|
+
// Close on outside click
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
177
|
+
if (
|
|
178
|
+
!containerRef.current?.contains(e.target as Node) &&
|
|
179
|
+
!dropdownRef.current?.contains(e.target as Node)
|
|
180
|
+
) {
|
|
181
|
+
setOpen(false);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
185
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
// Update dropdown position
|
|
189
|
+
const updateDropdownPosition = () => {
|
|
190
|
+
const inputEl = containerRef.current;
|
|
191
|
+
const dropdownEl = dropdownRef.current;
|
|
192
|
+
if (!inputEl || !dropdownEl) return;
|
|
193
|
+
|
|
194
|
+
const rect = inputEl.getBoundingClientRect();
|
|
195
|
+
const viewportHeight = window.innerHeight;
|
|
196
|
+
const gap = 4;
|
|
197
|
+
|
|
198
|
+
const dropdownHeight = Math.min(dropdownEl.offsetHeight || 0, 260);
|
|
199
|
+
|
|
200
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
201
|
+
const spaceAbove = rect.top;
|
|
202
|
+
|
|
203
|
+
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
|
204
|
+
setDropDirection("up");
|
|
205
|
+
setPosition({
|
|
206
|
+
top: rect.top + window.scrollY - dropdownHeight - gap,
|
|
207
|
+
left: rect.left + window.scrollX,
|
|
208
|
+
width: rect.width,
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
setDropDirection("down");
|
|
212
|
+
setPosition({
|
|
213
|
+
top: rect.bottom + window.scrollY + gap,
|
|
214
|
+
left: rect.left + window.scrollX,
|
|
215
|
+
width: rect.width,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
useLayoutEffect(() => updateDropdownPosition(), [open]);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!open) return;
|
|
223
|
+
window.addEventListener("resize", updateDropdownPosition);
|
|
224
|
+
window.addEventListener("scroll", updateDropdownPosition, true);
|
|
225
|
+
return () => {
|
|
226
|
+
window.removeEventListener("resize", updateDropdownPosition);
|
|
227
|
+
window.removeEventListener("scroll", updateDropdownPosition, true);
|
|
228
|
+
};
|
|
229
|
+
}, [open]);
|
|
230
|
+
|
|
231
|
+
// Reset highlighted index when opening
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (open) {
|
|
234
|
+
const currentIndex = defaultCountries.findIndex(
|
|
235
|
+
(c) => c.iso2 === country.iso2
|
|
236
|
+
);
|
|
237
|
+
if (currentIndex >= 0) {
|
|
238
|
+
setHighlightedIndex(currentIndex);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}, [open, country]);
|
|
242
|
+
const heightStyle = useMemo(
|
|
243
|
+
() =>
|
|
244
|
+
measurement == "lg"
|
|
245
|
+
? {
|
|
246
|
+
height: "50px",
|
|
247
|
+
endContent: label
|
|
248
|
+
? "ltr:top-[48px] rtl:top-[54px]-translate-y-1/2"
|
|
249
|
+
: "top-[26px] -translate-y-1/2",
|
|
250
|
+
startContent: label
|
|
251
|
+
? "ltr:top-[48px] rtl:top-[54px] -translate-y-1/2"
|
|
252
|
+
: "top-[26px] -translate-y-1/2",
|
|
253
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
254
|
+
}
|
|
255
|
+
: measurement == "md"
|
|
256
|
+
? {
|
|
257
|
+
height: "44px",
|
|
258
|
+
endContent: label
|
|
259
|
+
? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
|
|
260
|
+
: "top-[22px] -translate-y-1/2",
|
|
261
|
+
startContent: label
|
|
262
|
+
? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
|
|
263
|
+
: "top-[22px] -translate-y-1/2",
|
|
264
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
265
|
+
}
|
|
266
|
+
: {
|
|
267
|
+
height: "40px",
|
|
268
|
+
endContent: label
|
|
269
|
+
? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
|
|
270
|
+
: "top-[20px] -translate-y-1/2",
|
|
271
|
+
startContent: label
|
|
272
|
+
? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
|
|
273
|
+
: "top-[20px] -translate-y-1/2",
|
|
274
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
275
|
+
},
|
|
276
|
+
[measurement, label]
|
|
277
|
+
);
|
|
278
|
+
const readOnlyStyle = readOnly && "opacity-40";
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
className={cn(
|
|
283
|
+
rootDivClassName,
|
|
284
|
+
"relative flex flex-col w-full",
|
|
285
|
+
readOnlyStyle
|
|
286
|
+
)}
|
|
287
|
+
ref={containerRef}
|
|
288
|
+
onKeyDown={handleKeyDown}
|
|
289
|
+
>
|
|
290
|
+
{/* Required Hint */}
|
|
291
|
+
{requiredHint && (
|
|
292
|
+
<span
|
|
293
|
+
className={cn(
|
|
294
|
+
"absolute font-semibold text-red-600 rtl:text-[13px] ltr:text-[11px] ltr:right-2.5 rtl:left-2.5",
|
|
295
|
+
heightStyle.required
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{requiredHint}
|
|
299
|
+
</span>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Label */}
|
|
303
|
+
{label && (
|
|
304
|
+
<label
|
|
305
|
+
htmlFor={label}
|
|
306
|
+
className={cn(
|
|
307
|
+
"font-semibold ltr:text-[13px] rtl:text-[18px] text-start inline-block pb-1"
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
{label}
|
|
311
|
+
</label>
|
|
312
|
+
)}
|
|
313
|
+
<div className="flex gap-2">
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
style={{
|
|
317
|
+
height: heightStyle.height,
|
|
318
|
+
}}
|
|
319
|
+
className="flex items-center dark:bg-input/30 gap-2 px-2 border rounded-sm bg-card hover:bg-primary/5 focus:outline-none focus:ring-1 focus:ring-tertiary/60"
|
|
320
|
+
onClick={() => setOpen(!open)}
|
|
321
|
+
aria-haspopup="listbox"
|
|
322
|
+
aria-expanded={open}
|
|
323
|
+
>
|
|
324
|
+
<LazyFlag iso2={country.iso2} className={iconClassName} />
|
|
325
|
+
<span className="text-primary ltr:text-sm rtl:text-sm rtl:font-semibold">
|
|
326
|
+
+{country.dialCode}
|
|
327
|
+
</span>
|
|
328
|
+
</button>
|
|
329
|
+
<input
|
|
330
|
+
ref={inputRef}
|
|
331
|
+
type="tel"
|
|
332
|
+
value={phone}
|
|
333
|
+
onChange={(e) => {
|
|
334
|
+
if (onChange) onChange(e);
|
|
335
|
+
setPhone(e.target.value);
|
|
336
|
+
}}
|
|
337
|
+
placeholder="Phone number"
|
|
338
|
+
style={{
|
|
339
|
+
height: heightStyle.height,
|
|
340
|
+
}}
|
|
341
|
+
className={cn(
|
|
342
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 flex w-full min-w-0 rounded-sm border px-3 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70",
|
|
343
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
344
|
+
"appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0 focus-visible:shadow-sm focus-visible:ring-offset-0 transition-[border] bg-card",
|
|
345
|
+
"focus-visible:border-tertiary/60",
|
|
346
|
+
"[&::-webkit-outer-spin-button]:appearance-none",
|
|
347
|
+
"[&::-webkit-inner-spin-button]:appearance-none",
|
|
348
|
+
"[-moz-appearance:textfield] ",
|
|
349
|
+
hasError && "border-red-400",
|
|
350
|
+
className
|
|
351
|
+
)}
|
|
352
|
+
{...rest}
|
|
353
|
+
disabled={readOnly}
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
{open &&
|
|
358
|
+
createPortal(
|
|
359
|
+
<div
|
|
360
|
+
ref={dropdownRef}
|
|
361
|
+
className={cn(
|
|
362
|
+
"absolute z-50 border bg-card shadow-lg",
|
|
363
|
+
dropDirection === "down" ? "rounded-b" : "rounded-t"
|
|
364
|
+
)}
|
|
365
|
+
style={{
|
|
366
|
+
top: position.top,
|
|
367
|
+
left: position.left,
|
|
368
|
+
width: position.width,
|
|
369
|
+
maxHeight: ROW_HEIGHT * VISIBLE_ROWS, // Set maxHeight here instead
|
|
370
|
+
}}
|
|
371
|
+
role="listbox"
|
|
372
|
+
>
|
|
373
|
+
<VirtualList
|
|
374
|
+
ROW_HEIGHT={ROW_HEIGHT}
|
|
375
|
+
BUFFER={BUFFER}
|
|
376
|
+
items={defaultCountries}
|
|
377
|
+
height={ROW_HEIGHT * VISIBLE_ROWS}
|
|
378
|
+
renderRow={(c, i) => (
|
|
379
|
+
<div
|
|
380
|
+
onClick={() => chooseCountry(c)}
|
|
381
|
+
onMouseEnter={() => setHighlightedIndex(i)}
|
|
382
|
+
className={`flex ltr:text-sm rtl:text-sm rtl:font-semibold items-center gap-2 px-2 py-1 cursor-pointer ${
|
|
383
|
+
i == highlightedIndex ? "bg-primary/5" : ""
|
|
384
|
+
}`}
|
|
385
|
+
role="option"
|
|
386
|
+
aria-selected={i === highlightedIndex}
|
|
387
|
+
>
|
|
388
|
+
<LazyFlag iso2={c.iso2} className={iconClassName} />
|
|
389
|
+
<span className="flex-1 truncate">{c.name}</span>
|
|
390
|
+
<span>+{c.dialCode}</span>
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
/>
|
|
394
|
+
</div>,
|
|
395
|
+
document.body
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
export default PhoneInput;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export type CountryIso2 =
|
|
2
|
+
| (string & {}) // allow any string but add autocompletion
|
|
3
|
+
| "af"
|
|
4
|
+
| "al"
|
|
5
|
+
| "dz"
|
|
6
|
+
| "ad"
|
|
7
|
+
| "ao"
|
|
8
|
+
| "ag"
|
|
9
|
+
| "ar"
|
|
10
|
+
| "am"
|
|
11
|
+
| "aw"
|
|
12
|
+
| "au"
|
|
13
|
+
| "at"
|
|
14
|
+
| "az"
|
|
15
|
+
| "bs"
|
|
16
|
+
| "bh"
|
|
17
|
+
| "bd"
|
|
18
|
+
| "bb"
|
|
19
|
+
| "by"
|
|
20
|
+
| "be"
|
|
21
|
+
| "bz"
|
|
22
|
+
| "bj"
|
|
23
|
+
| "bt"
|
|
24
|
+
| "bo"
|
|
25
|
+
| "ba"
|
|
26
|
+
| "bw"
|
|
27
|
+
| "br"
|
|
28
|
+
| "io"
|
|
29
|
+
| "bn"
|
|
30
|
+
| "bg"
|
|
31
|
+
| "bf"
|
|
32
|
+
| "bi"
|
|
33
|
+
| "kh"
|
|
34
|
+
| "cm"
|
|
35
|
+
| "ca"
|
|
36
|
+
| "cv"
|
|
37
|
+
| "bq"
|
|
38
|
+
| "cf"
|
|
39
|
+
| "td"
|
|
40
|
+
| "cl"
|
|
41
|
+
| "cn"
|
|
42
|
+
| "co"
|
|
43
|
+
| "km"
|
|
44
|
+
| "cd"
|
|
45
|
+
| "cg"
|
|
46
|
+
| "cr"
|
|
47
|
+
| "ci"
|
|
48
|
+
| "hr"
|
|
49
|
+
| "cu"
|
|
50
|
+
| "cw"
|
|
51
|
+
| "cy"
|
|
52
|
+
| "cz"
|
|
53
|
+
| "dk"
|
|
54
|
+
| "dj"
|
|
55
|
+
| "dm"
|
|
56
|
+
| "do"
|
|
57
|
+
| "ec"
|
|
58
|
+
| "eg"
|
|
59
|
+
| "sv"
|
|
60
|
+
| "gq"
|
|
61
|
+
| "er"
|
|
62
|
+
| "ee"
|
|
63
|
+
| "et"
|
|
64
|
+
| "fj"
|
|
65
|
+
| "fo"
|
|
66
|
+
| "fi"
|
|
67
|
+
| "fr"
|
|
68
|
+
| "gf"
|
|
69
|
+
| "pf"
|
|
70
|
+
| "ga"
|
|
71
|
+
| "gm"
|
|
72
|
+
| "ge"
|
|
73
|
+
| "de"
|
|
74
|
+
| "gh"
|
|
75
|
+
| "gr"
|
|
76
|
+
| "gd"
|
|
77
|
+
| "gp"
|
|
78
|
+
| "gu"
|
|
79
|
+
| "gt"
|
|
80
|
+
| "gn"
|
|
81
|
+
| "gw"
|
|
82
|
+
| "gy"
|
|
83
|
+
| "ht"
|
|
84
|
+
| "hn"
|
|
85
|
+
| "hk"
|
|
86
|
+
| "hu"
|
|
87
|
+
| "is"
|
|
88
|
+
| "in"
|
|
89
|
+
| "id"
|
|
90
|
+
| "ir"
|
|
91
|
+
| "iq"
|
|
92
|
+
| "ie"
|
|
93
|
+
| "il"
|
|
94
|
+
| "it"
|
|
95
|
+
| "jm"
|
|
96
|
+
| "jp"
|
|
97
|
+
| "jo"
|
|
98
|
+
| "kz"
|
|
99
|
+
| "ke"
|
|
100
|
+
| "ki"
|
|
101
|
+
| "xk"
|
|
102
|
+
| "kw"
|
|
103
|
+
| "kg"
|
|
104
|
+
| "la"
|
|
105
|
+
| "lv"
|
|
106
|
+
| "lb"
|
|
107
|
+
| "ls"
|
|
108
|
+
| "lr"
|
|
109
|
+
| "ly"
|
|
110
|
+
| "li"
|
|
111
|
+
| "lt"
|
|
112
|
+
| "lu"
|
|
113
|
+
| "mo"
|
|
114
|
+
| "mk"
|
|
115
|
+
| "mg"
|
|
116
|
+
| "mw"
|
|
117
|
+
| "my"
|
|
118
|
+
| "mv"
|
|
119
|
+
| "ml"
|
|
120
|
+
| "mt"
|
|
121
|
+
| "mh"
|
|
122
|
+
| "mq"
|
|
123
|
+
| "mr"
|
|
124
|
+
| "mu"
|
|
125
|
+
| "mx"
|
|
126
|
+
| "fm"
|
|
127
|
+
| "md"
|
|
128
|
+
| "mc"
|
|
129
|
+
| "mn"
|
|
130
|
+
| "me"
|
|
131
|
+
| "ma"
|
|
132
|
+
| "mz"
|
|
133
|
+
| "mm"
|
|
134
|
+
| "na"
|
|
135
|
+
| "nr"
|
|
136
|
+
| "np"
|
|
137
|
+
| "nl"
|
|
138
|
+
| "nc"
|
|
139
|
+
| "nz"
|
|
140
|
+
| "ni"
|
|
141
|
+
| "ne"
|
|
142
|
+
| "ng"
|
|
143
|
+
| "kp"
|
|
144
|
+
| "no"
|
|
145
|
+
| "om"
|
|
146
|
+
| "pk"
|
|
147
|
+
| "pw"
|
|
148
|
+
| "ps"
|
|
149
|
+
| "pa"
|
|
150
|
+
| "pg"
|
|
151
|
+
| "py"
|
|
152
|
+
| "pe"
|
|
153
|
+
| "ph"
|
|
154
|
+
| "pl"
|
|
155
|
+
| "pm"
|
|
156
|
+
| "pt"
|
|
157
|
+
| "pr"
|
|
158
|
+
| "qa"
|
|
159
|
+
| "re"
|
|
160
|
+
| "ro"
|
|
161
|
+
| "ru"
|
|
162
|
+
| "rw"
|
|
163
|
+
| "kn"
|
|
164
|
+
| "lc"
|
|
165
|
+
| "vc"
|
|
166
|
+
| "ws"
|
|
167
|
+
| "sm"
|
|
168
|
+
| "st"
|
|
169
|
+
| "sa"
|
|
170
|
+
| "sn"
|
|
171
|
+
| "rs"
|
|
172
|
+
| "sc"
|
|
173
|
+
| "sl"
|
|
174
|
+
| "sg"
|
|
175
|
+
| "sk"
|
|
176
|
+
| "si"
|
|
177
|
+
| "sb"
|
|
178
|
+
| "so"
|
|
179
|
+
| "za"
|
|
180
|
+
| "kr"
|
|
181
|
+
| "ss"
|
|
182
|
+
| "es"
|
|
183
|
+
| "lk"
|
|
184
|
+
| "sd"
|
|
185
|
+
| "sr"
|
|
186
|
+
| "sz"
|
|
187
|
+
| "se"
|
|
188
|
+
| "ch"
|
|
189
|
+
| "sy"
|
|
190
|
+
| "tw"
|
|
191
|
+
| "tj"
|
|
192
|
+
| "tz"
|
|
193
|
+
| "th"
|
|
194
|
+
| "tl"
|
|
195
|
+
| "tg"
|
|
196
|
+
| "to"
|
|
197
|
+
| "tt"
|
|
198
|
+
| "tn"
|
|
199
|
+
| "tr"
|
|
200
|
+
| "tm"
|
|
201
|
+
| "tv"
|
|
202
|
+
| "ug"
|
|
203
|
+
| "ua"
|
|
204
|
+
| "ae"
|
|
205
|
+
| "gb"
|
|
206
|
+
| "us"
|
|
207
|
+
| "uy"
|
|
208
|
+
| "uz"
|
|
209
|
+
| "vu"
|
|
210
|
+
| "va"
|
|
211
|
+
| "ve"
|
|
212
|
+
| "vn"
|
|
213
|
+
| "wf"
|
|
214
|
+
| "ye"
|
|
215
|
+
| "yt"
|
|
216
|
+
| "zm"
|
|
217
|
+
| "zw";
|
|
218
|
+
type FormatConfig = Record<string, string> & { default: string };
|
|
219
|
+
|
|
220
|
+
export interface ParsedCountry {
|
|
221
|
+
name: string;
|
|
222
|
+
iso2: CountryIso2;
|
|
223
|
+
dialCode: string;
|
|
224
|
+
format?: string | FormatConfig; // make sure this line exists
|
|
225
|
+
priority?: number;
|
|
226
|
+
areaCodes?: string[];
|
|
227
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
2
|
+
const A_LETTER_CODEPOINT = "1f1e6";
|
|
3
|
+
|
|
4
|
+
const incrementCodepoint = (codePoint: string, incrementBy: number): string => {
|
|
5
|
+
const decimal = parseInt(codePoint, 16);
|
|
6
|
+
return (decimal + incrementBy).toString(16);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const codepoints: Record<string, string> = alphabet.split("").reduce(
|
|
10
|
+
(obj, currentLetter, index) => ({
|
|
11
|
+
...obj,
|
|
12
|
+
[currentLetter]: incrementCodepoint(A_LETTER_CODEPOINT, index),
|
|
13
|
+
}),
|
|
14
|
+
{}
|
|
15
|
+
);
|
|
16
|
+
export const getFlagCodepoint = (iso2: string) => {
|
|
17
|
+
if (!iso2 || iso2.length !== 2) return "";
|
|
18
|
+
const lower = iso2.toLowerCase();
|
|
19
|
+
const first = codepoints[lower[0]];
|
|
20
|
+
const second = codepoints[lower[1]];
|
|
21
|
+
if (!first || !second) return "";
|
|
22
|
+
return `${first}-${second}`;
|
|
23
|
+
};
|