notionsoft-ui 1.0.20 → 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 +89 -7
- package/package.json +2 -1
- package/src/notion-ui/button/button.tsx +1 -1
- package/src/notion-ui/date-picker/DatePicker.stories.tsx +99 -0
- package/src/notion-ui/date-picker/date-picker.tsx +271 -0
- package/src/notion-ui/date-picker/index.ts +3 -0
- package/src/notion-ui/input/input.tsx +19 -12
- package/src/notion-ui/multi-date-picker/MultiDatePicker.stories.tsx +94 -0
- package/src/notion-ui/multi-date-picker/index.ts +3 -0
- package/src/notion-ui/multi-date-picker/multi-date-picker.tsx +265 -0
- package/src/notion-ui/multi-select-input/multi-select-input.tsx +202 -54
- package/src/notion-ui/password-input/password-input.tsx +39 -34
- package/src/notion-ui/phone-input/country-data.ts +420 -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
|
@@ -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
|
+
};
|