notionsoft-ui 1.0.21 → 1.0.22

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 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
- chalk.green(`✓ Installed ${component} component as ${destFile}`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notionsoft-ui",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "A React UI component installer (shadcn-style). Installs components directly into your project.",
5
5
  "bin": {
6
6
  "notionsoft-ui": "./cli/index.cjs"
@@ -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 PhoneCountryPickerSize = "sm" | "md" | "lg";
71
+
72
+ interface PhoneCountryPickerProps
73
+ extends React.InputHTMLAttributes<HTMLInputElement> {
74
+ requiredHint?: string;
75
+ label?: string;
76
+ errorMessage?: string;
77
+ classNames?: {
78
+ rootDivClassName?: string;
79
+ iconClassName?: string;
80
+ };
81
+ measurement?: PhoneCountryPickerSize;
82
+ ROW_HEIGHT?: number;
83
+ VISIBLE_ROWS?: number;
84
+ BUFFER?: number;
85
+ }
86
+
87
+ export const PhoneCountryPicker: React.FC<PhoneCountryPickerProps> = ({
88
+ measurement = "sm",
89
+ errorMessage,
90
+ label,
91
+ readOnly,
92
+ className,
93
+ classNames,
94
+ requiredHint,
95
+ value,
96
+ onChange,
97
+ ROW_HEIGHT = 32,
98
+ VISIBLE_ROWS = 10,
99
+ BUFFER = 5,
100
+ ...rest
101
+ }) => {
102
+ const { rootDivClassName, iconClassName = "size-4" } = classNames || {};
103
+ const [open, setOpen] = useState(false);
104
+ const [country, setCountry] = useState<ParsedCountry>(defaultCountries[0]);
105
+ const [highlightedIndex, setHighlightedIndex] = useState<number>(0);
106
+ const [phone, setPhone] = useState<string>(
107
+ typeof value == "string" ? value : ""
108
+ );
109
+ const containerRef = useRef<HTMLDivElement>(null);
110
+ const dropdownRef = useRef<HTMLDivElement>(null);
111
+ const inputRef = useRef<HTMLInputElement>(null);
112
+ const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
113
+
114
+ const [dropDirection, setDropDirection] = useState<"down" | "up">("down");
115
+
116
+ const hasError = !!errorMessage;
117
+
118
+ // Choose country
119
+ const chooseCountry = (c: ParsedCountry) => {
120
+ setCountry(c);
121
+ setOpen(false);
122
+ inputRef.current?.focus();
123
+ };
124
+
125
+ // Keyboard navigation
126
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
127
+ if (!open) {
128
+ if (e.key === "ArrowDown" || e.key === "Enter") {
129
+ setOpen(true);
130
+ e.preventDefault();
131
+ }
132
+ return;
133
+ }
134
+
135
+ if (e.key === "ArrowDown") {
136
+ setHighlightedIndex((prev) =>
137
+ Math.min(prev + 1, defaultCountries.length - 1)
138
+ );
139
+ e.preventDefault();
140
+ } else if (e.key === "ArrowUp") {
141
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
142
+ e.preventDefault();
143
+ } else if (e.key === "Enter") {
144
+ chooseCountry(defaultCountries[highlightedIndex]);
145
+ e.preventDefault();
146
+ } else if (e.key === "Escape") {
147
+ setOpen(false);
148
+ e.preventDefault();
149
+ }
150
+ };
151
+
152
+ // Scroll highlighted item into view
153
+ useEffect(() => {
154
+ if (!open || !dropdownRef.current) return;
155
+
156
+ // Get the scrollable container inside the virtual list
157
+ const scrollableContainer =
158
+ dropdownRef.current.querySelector(".overflow-y-auto");
159
+ if (!scrollableContainer) return;
160
+
161
+ const rowTop = highlightedIndex * ROW_HEIGHT;
162
+ const rowBottom = rowTop + ROW_HEIGHT;
163
+ const scrollContainer = scrollableContainer as HTMLDivElement;
164
+
165
+ if (rowTop < scrollContainer.scrollTop) {
166
+ scrollContainer.scrollTop = rowTop;
167
+ } else if (
168
+ rowBottom >
169
+ scrollContainer.scrollTop + ROW_HEIGHT * VISIBLE_ROWS
170
+ ) {
171
+ scrollContainer.scrollTop = rowBottom - ROW_HEIGHT * VISIBLE_ROWS;
172
+ }
173
+ }, [highlightedIndex, open]);
174
+
175
+ // Close on outside click
176
+ useEffect(() => {
177
+ const handleClickOutside = (e: MouseEvent) => {
178
+ if (
179
+ !containerRef.current?.contains(e.target as Node) &&
180
+ !dropdownRef.current?.contains(e.target as Node)
181
+ ) {
182
+ setOpen(false);
183
+ }
184
+ };
185
+ document.addEventListener("mousedown", handleClickOutside);
186
+ return () => document.removeEventListener("mousedown", handleClickOutside);
187
+ }, []);
188
+
189
+ // Update dropdown position
190
+ const updateDropdownPosition = () => {
191
+ const inputEl = containerRef.current;
192
+ const dropdownEl = dropdownRef.current;
193
+ if (!inputEl || !dropdownEl) return;
194
+
195
+ const rect = inputEl.getBoundingClientRect();
196
+ const viewportHeight = window.innerHeight;
197
+ const gap = 4;
198
+
199
+ const dropdownHeight = Math.min(dropdownEl.offsetHeight || 0, 260);
200
+
201
+ const spaceBelow = viewportHeight - rect.bottom;
202
+ const spaceAbove = rect.top;
203
+
204
+ if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
205
+ setDropDirection("up");
206
+ setPosition({
207
+ top: rect.top + window.scrollY - dropdownHeight - gap,
208
+ left: rect.left + window.scrollX,
209
+ width: rect.width,
210
+ });
211
+ } else {
212
+ setDropDirection("down");
213
+ setPosition({
214
+ top: rect.bottom + window.scrollY + gap,
215
+ left: rect.left + window.scrollX,
216
+ width: rect.width,
217
+ });
218
+ }
219
+ };
220
+
221
+ useLayoutEffect(() => updateDropdownPosition(), [open]);
222
+ useEffect(() => {
223
+ if (!open) return;
224
+ window.addEventListener("resize", updateDropdownPosition);
225
+ window.addEventListener("scroll", updateDropdownPosition, true);
226
+ return () => {
227
+ window.removeEventListener("resize", updateDropdownPosition);
228
+ window.removeEventListener("scroll", updateDropdownPosition, true);
229
+ };
230
+ }, [open]);
231
+
232
+ // Reset highlighted index when opening
233
+ useEffect(() => {
234
+ if (open) {
235
+ const currentIndex = defaultCountries.findIndex(
236
+ (c) => c.iso2 === country.iso2
237
+ );
238
+ if (currentIndex >= 0) {
239
+ setHighlightedIndex(currentIndex);
240
+ }
241
+ }
242
+ }, [open, country]);
243
+ const heightStyle = useMemo(
244
+ () =>
245
+ measurement == "lg"
246
+ ? {
247
+ height: "50px",
248
+ endContent: label
249
+ ? "ltr:top-[48px] rtl:top-[54px]-translate-y-1/2"
250
+ : "top-[26px] -translate-y-1/2",
251
+ startContent: label
252
+ ? "ltr:top-[48px] rtl:top-[54px] -translate-y-1/2"
253
+ : "top-[26px] -translate-y-1/2",
254
+ required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
255
+ }
256
+ : measurement == "md"
257
+ ? {
258
+ height: "44px",
259
+ endContent: label
260
+ ? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
261
+ : "top-[22px] -translate-y-1/2",
262
+ startContent: label
263
+ ? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
264
+ : "top-[22px] -translate-y-1/2",
265
+ required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
266
+ }
267
+ : {
268
+ height: "40px",
269
+ endContent: label
270
+ ? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
271
+ : "top-[20px] -translate-y-1/2",
272
+ startContent: label
273
+ ? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
274
+ : "top-[20px] -translate-y-1/2",
275
+ required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
276
+ },
277
+ [measurement, label]
278
+ );
279
+ const readOnlyStyle = readOnly && "opacity-40";
280
+
281
+ return (
282
+ <div
283
+ className={cn(
284
+ rootDivClassName,
285
+ "relative flex flex-col w-full",
286
+ readOnlyStyle
287
+ )}
288
+ ref={containerRef}
289
+ onKeyDown={handleKeyDown}
290
+ >
291
+ {/* Required Hint */}
292
+ {requiredHint && (
293
+ <span
294
+ className={cn(
295
+ "absolute font-semibold text-red-600 rtl:text-[13px] ltr:text-[11px] ltr:right-2.5 rtl:left-2.5",
296
+ heightStyle.required
297
+ )}
298
+ >
299
+ {requiredHint}
300
+ </span>
301
+ )}
302
+
303
+ {/* Label */}
304
+ {label && (
305
+ <label
306
+ htmlFor={label}
307
+ className={cn(
308
+ "font-semibold ltr:text-[13px] rtl:text-[18px] text-start inline-block pb-1"
309
+ )}
310
+ >
311
+ {label}
312
+ </label>
313
+ )}
314
+ <div className="flex gap-2">
315
+ <button
316
+ type="button"
317
+ style={{
318
+ height: heightStyle.height,
319
+ }}
320
+ 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"
321
+ onClick={() => setOpen(!open)}
322
+ aria-haspopup="listbox"
323
+ aria-expanded={open}
324
+ >
325
+ <LazyFlag iso2={country.iso2} className={iconClassName} />
326
+ <span className="text-primary ltr:text-sm rtl:text-sm rtl:font-semibold">
327
+ +{country.dialCode}
328
+ </span>
329
+ </button>
330
+ <input
331
+ ref={inputRef}
332
+ type="tel"
333
+ value={phone}
334
+ onChange={(e) => {
335
+ if (onChange) onChange(e);
336
+ setPhone(e.target.value);
337
+ }}
338
+ placeholder="Phone number"
339
+ style={{
340
+ height: heightStyle.height,
341
+ }}
342
+ className={cn(
343
+ "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",
344
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
345
+ "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",
346
+ "focus-visible:border-tertiary/60",
347
+ "[&::-webkit-outer-spin-button]:appearance-none",
348
+ "[&::-webkit-inner-spin-button]:appearance-none",
349
+ "[-moz-appearance:textfield] ",
350
+ hasError && "border-red-400",
351
+ className
352
+ )}
353
+ {...rest}
354
+ disabled={readOnly}
355
+ />
356
+ </div>
357
+
358
+ {open &&
359
+ createPortal(
360
+ <div
361
+ ref={dropdownRef}
362
+ className={cn(
363
+ "absolute z-50 border bg-card shadow-lg",
364
+ dropDirection === "down" ? "rounded-b" : "rounded-t"
365
+ )}
366
+ style={{
367
+ top: position.top,
368
+ left: position.left,
369
+ width: position.width,
370
+ maxHeight: ROW_HEIGHT * VISIBLE_ROWS, // Set maxHeight here instead
371
+ }}
372
+ role="listbox"
373
+ >
374
+ <VirtualList
375
+ ROW_HEIGHT={ROW_HEIGHT}
376
+ BUFFER={BUFFER}
377
+ items={defaultCountries}
378
+ height={ROW_HEIGHT * VISIBLE_ROWS}
379
+ renderRow={(c, i) => (
380
+ <div
381
+ onClick={() => chooseCountry(c)}
382
+ onMouseEnter={() => setHighlightedIndex(i)}
383
+ className={`flex ltr:text-sm rtl:text-sm rtl:font-semibold items-center gap-2 px-2 py-1 cursor-pointer ${
384
+ i == highlightedIndex ? "bg-primary/5" : ""
385
+ }`}
386
+ role="option"
387
+ aria-selected={i === highlightedIndex}
388
+ >
389
+ <LazyFlag iso2={c.iso2} className={iconClassName} />
390
+ <span className="flex-1 truncate">{c.name}</span>
391
+ <span>+{c.dialCode}</span>
392
+ </div>
393
+ )}
394
+ />
395
+ </div>,
396
+ document.body
397
+ )}
398
+ </div>
399
+ );
400
+ };
@@ -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
+ };