@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +81 -2
- package/check.mjs +236 -0
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/eslint.js +53 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +76 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
|
+
import { Check, ChevronDown, Search, X } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./phone-input.module.css";
|
|
8
|
+
const country = (iso, name, dial, patterns, example, trunk = "0", areaCodes) => ({ iso, name, dial, patterns: Array.isArray(patterns) ? patterns : [patterns], example, trunk, areaCodes });
|
|
9
|
+
const CA_AREA_CODES = "204 226 236 249 250 263 289 306 343 354 365 367 368 382 387 403 416 418 428 431 437 438 450 468 474 506 514 519 548 579 581 584 587 604 613 639 647 672 683 705 709 742 753 778 780 782 807 819 825 867 873 879 902 905".split(" ");
|
|
10
|
+
export const PHONE_COUNTRIES = [
|
|
11
|
+
country("US", "United States", "1", "(###) ###-####", "2015550123", "1"),
|
|
12
|
+
country("CA", "Canada", "1", "(###) ###-####", "5065550123", "1", CA_AREA_CODES),
|
|
13
|
+
country("MX", "Mexico", "52", "## #### ####", "2221234567", ""),
|
|
14
|
+
country("BR", "Brazil", "55", ["(##) ####-####", "(##) #####-####"], "11961234567"),
|
|
15
|
+
country("AR", "Argentina", "54", "## ####-####", "1123456789"),
|
|
16
|
+
country("CO", "Colombia", "57", "### ### ####", "3211234567", ""),
|
|
17
|
+
country("CL", "Chile", "56", "# #### ####", "221234567", ""),
|
|
18
|
+
country("PE", "Peru", "51", "### ### ###", "912345678"),
|
|
19
|
+
country("GB", "United Kingdom", "44", "#### ######", "7400123456"),
|
|
20
|
+
country("IE", "Ireland", "353", "## ### ####", "850123456"),
|
|
21
|
+
country("FR", "France", "33", "# ## ## ## ##", "612345678"),
|
|
22
|
+
country("DE", "Germany", "49", ["### #######", "### ########"], "15123456789"),
|
|
23
|
+
country("ES", "Spain", "34", "### ## ## ##", "612345678", ""),
|
|
24
|
+
country("IT", "Italy", "39", ["### ### ###", "### ### ####"], "3123456789", ""),
|
|
25
|
+
country("PT", "Portugal", "351", "### ### ###", "912345678", ""),
|
|
26
|
+
country("NL", "Netherlands", "31", "# ########", "612345678"),
|
|
27
|
+
country("BE", "Belgium", "32", "### ## ## ##", "470123456"),
|
|
28
|
+
country("CH", "Switzerland", "41", "## ### ## ##", "781234567"),
|
|
29
|
+
country("AT", "Austria", "43", ["### #######", "### ########"], "6641234567"),
|
|
30
|
+
country("SE", "Sweden", "46", "## ### ## ##", "701234567"),
|
|
31
|
+
country("NO", "Norway", "47", "### ## ###", "40612345", ""),
|
|
32
|
+
country("DK", "Denmark", "45", "## ## ## ##", "32123456", ""),
|
|
33
|
+
country("FI", "Finland", "358", ["## ### ####", "## ### #####"], "412345678"),
|
|
34
|
+
country("PL", "Poland", "48", "### ### ###", "512345678", ""),
|
|
35
|
+
country("CZ", "Czechia", "420", "### ### ###", "601123456", ""),
|
|
36
|
+
country("GR", "Greece", "30", "### ### ####", "6912345678", ""),
|
|
37
|
+
country("UA", "Ukraine", "380", "## ### ## ##", "501234567"),
|
|
38
|
+
country("TR", "Turkey", "90", "### ### ## ##", "5012345678"),
|
|
39
|
+
country("IL", "Israel", "972", "##-###-####", "502345678"),
|
|
40
|
+
country("AE", "United Arab Emirates", "971", "## ### ####", "501234567"),
|
|
41
|
+
country("SA", "Saudi Arabia", "966", "## ### ####", "512345678"),
|
|
42
|
+
country("EG", "Egypt", "20", "### ### ####", "1001234567"),
|
|
43
|
+
country("NG", "Nigeria", "234", "### ### ####", "8021234567"),
|
|
44
|
+
country("KE", "Kenya", "254", "### ######", "712123456"),
|
|
45
|
+
country("ZA", "South Africa", "27", "## ### ####", "711234567"),
|
|
46
|
+
country("IN", "India", "91", "#####-#####", "8123456789"),
|
|
47
|
+
country("PK", "Pakistan", "92", "### #######", "3012345678"),
|
|
48
|
+
country("CN", "China", "86", "### #### ####", "13123456789"),
|
|
49
|
+
country("JP", "Japan", "81", "##-####-####", "9012345678"),
|
|
50
|
+
country("KR", "South Korea", "82", ["##-###-####", "##-####-####"], "1020000000"),
|
|
51
|
+
country("TW", "Taiwan", "886", "### ### ###", "912345678"),
|
|
52
|
+
country("HK", "Hong Kong", "852", "#### ####", "51234567", ""),
|
|
53
|
+
country("SG", "Singapore", "65", "#### ####", "81234567", ""),
|
|
54
|
+
country("PH", "Philippines", "63", "### ### ####", "9051234567"),
|
|
55
|
+
country("ID", "Indonesia", "62", ["###-###-####", "###-####-####", "###-####-#####"], "81234567890"),
|
|
56
|
+
country("TH", "Thailand", "66", "## ### ####", "812345678"),
|
|
57
|
+
country("VN", "Vietnam", "84", "## ### ## ##", "912345678"),
|
|
58
|
+
country("AU", "Australia", "61", "### ### ###", "412345678"),
|
|
59
|
+
country("NZ", "New Zealand", "64", ["# ### ####", "## ### ####", "## #### ####"], "211234567"),
|
|
60
|
+
];
|
|
61
|
+
const BY_ISO = new Map(PHONE_COUNTRIES.map(entry => [entry.iso, entry]));
|
|
62
|
+
const capacity = (pattern) => pattern.split("#").length - 1;
|
|
63
|
+
const lengthsOf = (entry) => entry.patterns.map(capacity);
|
|
64
|
+
/** A typed trunk prefix earns its own room; without one the longest valid length is the cap. */
|
|
65
|
+
const capDigits = (entry, digits) => digits.slice(0, splitTrunk(entry, digits)[0].length + Math.max(...lengthsOf(entry)));
|
|
66
|
+
const onlyDigits = (text) => text.replace(/\D/g, "");
|
|
67
|
+
/** Regional indicator letters. Platforms without flag glyphs show the two letters, which still read. */
|
|
68
|
+
export const flagOf = (iso) => String.fromCodePoint(...[...iso.toUpperCase()].map(char => 0x1f1e6 + char.charCodeAt(0) - 65));
|
|
69
|
+
function splitTrunk(entry, digits) {
|
|
70
|
+
return entry.trunk && digits.startsWith(entry.trunk) ? [entry.trunk, digits.slice(entry.trunk.length)] : ["", digits];
|
|
71
|
+
}
|
|
72
|
+
/** Separators only print while digits follow, so a half typed number never ends in a dangling ") " or "-". */
|
|
73
|
+
function applyPattern(digits, pattern) {
|
|
74
|
+
let out = "", index = 0;
|
|
75
|
+
for (const char of pattern) {
|
|
76
|
+
if (index >= digits.length)
|
|
77
|
+
break;
|
|
78
|
+
out += char === "#" ? digits[index++] : char;
|
|
79
|
+
}
|
|
80
|
+
return out + digits.slice(index);
|
|
81
|
+
}
|
|
82
|
+
function patternFor(entry, length) {
|
|
83
|
+
return entry.patterns.find(pattern => capacity(pattern) >= length) ?? entry.patterns[entry.patterns.length - 1];
|
|
84
|
+
}
|
|
85
|
+
/** Formats national digits as typed, keeping a typed trunk prefix: "07911123456" in the UK reads "07911 123456". */
|
|
86
|
+
export function formatNational(entry, digits) {
|
|
87
|
+
const [trunk, rest] = splitTrunk(entry, digits);
|
|
88
|
+
const pattern = patternFor(entry, rest.length);
|
|
89
|
+
const body = applyPattern(rest, pattern);
|
|
90
|
+
if (!trunk)
|
|
91
|
+
return body;
|
|
92
|
+
if (!rest)
|
|
93
|
+
return trunk;
|
|
94
|
+
return trunk + (/^#/.test(pattern) ? "" : " ") + body;
|
|
95
|
+
}
|
|
96
|
+
function statusOf(entry, digits) {
|
|
97
|
+
const length = splitTrunk(entry, digits)[1].length;
|
|
98
|
+
const lengths = lengthsOf(entry);
|
|
99
|
+
if (!length)
|
|
100
|
+
return "empty";
|
|
101
|
+
if (lengths.includes(length))
|
|
102
|
+
return "valid";
|
|
103
|
+
return length > Math.max(...lengths) ? "too-long" : "incomplete";
|
|
104
|
+
}
|
|
105
|
+
const toE164 = (entry, digits) => { const rest = splitTrunk(entry, digits)[1]; return rest ? `+${entry.dial}${rest}` : ""; };
|
|
106
|
+
/** Reads "+44 (0)7911 123456", "0044 7911…", or "+14165550123" into a country and national digits. Longest calling code wins. */
|
|
107
|
+
export function parsePhoneNumber(input, pool = PHONE_COUNTRIES) {
|
|
108
|
+
const trimmed = input.trim();
|
|
109
|
+
let digits = onlyDigits(trimmed);
|
|
110
|
+
if (!trimmed.startsWith("+")) {
|
|
111
|
+
if (!trimmed.startsWith("00"))
|
|
112
|
+
return null;
|
|
113
|
+
digits = digits.slice(2);
|
|
114
|
+
}
|
|
115
|
+
for (const size of [3, 2, 1]) {
|
|
116
|
+
const dial = digits.slice(0, size);
|
|
117
|
+
const matches = pool.filter(entry => entry.dial === dial);
|
|
118
|
+
if (!matches.length)
|
|
119
|
+
continue;
|
|
120
|
+
let rest = digits.slice(size);
|
|
121
|
+
const entry = matches.find(item => item.areaCodes?.some(code => rest.startsWith(code))) ?? matches.find(item => !item.areaCodes) ?? matches[0];
|
|
122
|
+
// "+44 (0)7911…" carries a trunk zero it should not; drop it when the rest is still a full number without it.
|
|
123
|
+
if (entry.trunk && rest.startsWith(entry.trunk) && rest.length - entry.trunk.length >= Math.min(...lengthsOf(entry)))
|
|
124
|
+
rest = rest.slice(entry.trunk.length);
|
|
125
|
+
return { country: entry, national: capDigits(entry, rest) };
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** Formats an E.164 number for display, such as "+44 7400 123456". Returns the input when it cannot be read. */
|
|
130
|
+
export function formatPhoneNumber(e164) {
|
|
131
|
+
const parsed = parsePhoneNumber(e164);
|
|
132
|
+
return parsed ? `+${parsed.country.dial} ${formatNational(parsed.country, parsed.national)}` : e164;
|
|
133
|
+
}
|
|
134
|
+
const enterEase = [...motionTokens.ease.enter];
|
|
135
|
+
const standardEase = [...motionTokens.ease.standard];
|
|
136
|
+
const { blur } = motionTokens;
|
|
137
|
+
/** Duration springs restated as stiffness and damping, so a retarget mid flight keeps the velocity it already has. */
|
|
138
|
+
const physical = (visualDuration, bounce) => {
|
|
139
|
+
const root = (2 * Math.PI) / (visualDuration * 1.2);
|
|
140
|
+
return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
|
|
141
|
+
};
|
|
142
|
+
const GROW = physical(.4, .12), SHRINK = physical(.32, 0), GLIDE = physical(.28, .08), WIDTH = physical(.42, .16);
|
|
143
|
+
const PANEL_MAX = 340, CLOSED_RADIUS = 17, OPEN_RADIUS = 22, PAGE = 8;
|
|
144
|
+
const subscribe = () => () => { };
|
|
145
|
+
function useReducedFlag() {
|
|
146
|
+
const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
|
|
147
|
+
return !!useReducedMotion() && hydrated;
|
|
148
|
+
}
|
|
149
|
+
/** The flag and calling code roll in the direction of the list: a country further down rises from below. */
|
|
150
|
+
const layerVariants = {
|
|
151
|
+
enter: (dir) => ({ opacity: 0, y: `${dir * .6}em`, filter: `blur(${blur.soft}px)` }),
|
|
152
|
+
rest: { opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } },
|
|
153
|
+
exit: (dir) => ({ opacity: 0, y: `${dir * -.5}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: .12, ease: standardEase } }),
|
|
154
|
+
};
|
|
155
|
+
const layerFade = { enter: { opacity: 0 }, rest: { opacity: 1, y: 0, filter: "none" }, exit: { opacity: 0, transition: { duration: .08 } } };
|
|
156
|
+
function matchesQuery(entry, needle) {
|
|
157
|
+
if (!needle)
|
|
158
|
+
return true;
|
|
159
|
+
const digits = onlyDigits(needle);
|
|
160
|
+
if (digits && /^[+\d\s()-]+$/.test(needle))
|
|
161
|
+
return entry.dial.startsWith(digits) || digits.startsWith(entry.dial);
|
|
162
|
+
if (needle.replace("+", "") === "")
|
|
163
|
+
return true;
|
|
164
|
+
return entry.name.toLowerCase().includes(needle) || entry.iso.toLowerCase() === needle;
|
|
165
|
+
}
|
|
166
|
+
/** Position in the formatted text just after the nth digit. */
|
|
167
|
+
/**
|
|
168
|
+
* A search that is really a whole international number: "+" and more digits than any calling code and area code take
|
|
169
|
+
* ("+1 202 555 0142" typed key by key after "+" opened the list). It belongs in the number field, under its country.
|
|
170
|
+
*/
|
|
171
|
+
function numberInSearch(query, pool) {
|
|
172
|
+
const text = query.trim();
|
|
173
|
+
if (!/^\+[\d\s().-]+$/.test(text))
|
|
174
|
+
return null;
|
|
175
|
+
const digits = onlyDigits(text);
|
|
176
|
+
if (digits.length < 5)
|
|
177
|
+
return null;
|
|
178
|
+
const parsed = parsePhoneNumber(`+${digits}`, pool);
|
|
179
|
+
return parsed && parsed.national ? parsed : null;
|
|
180
|
+
}
|
|
181
|
+
function caretAfterDigits(text, count) {
|
|
182
|
+
if (count <= 0) {
|
|
183
|
+
const first = text.search(/\d/);
|
|
184
|
+
return first < 0 ? text.length : Math.min(first, text.length);
|
|
185
|
+
}
|
|
186
|
+
let seen = 0;
|
|
187
|
+
for (let index = 0; index < text.length; index++)
|
|
188
|
+
if (/\d/.test(text[index]) && ++seen === count)
|
|
189
|
+
return index + 1;
|
|
190
|
+
return text.length;
|
|
191
|
+
}
|
|
192
|
+
function MessageRow({ id, text, tone }) {
|
|
193
|
+
const reduced = useReducedFlag();
|
|
194
|
+
return _jsx(motion.span, { className: styles.messageSlot, initial: reduced ? false : { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0, transition: reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.instant } } }, transition: reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast } }, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { id: id, className: tone === "error" ? styles.error : styles.hint, role: tone === "error" ? "alert" : undefined, initial: reduced ? { opacity: 0 } : { opacity: 0, y: "0.35em", filter: `blur(${blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: .1 } }, transition: { duration: reduced ? .12 : motionTokens.duration.standard, ease: enterEase }, children: text }, text) }) });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A phone number field with a country picker. The country button grows into a searchable list, the number formats as it is
|
|
198
|
+
* typed with a faint guide showing the rest of the expected shape, pasted or autofilled international numbers pick their own
|
|
199
|
+
* country, and the value comes out in E.164. Typing "+" in the number jumps into the picker with a calling code search.
|
|
200
|
+
*/
|
|
201
|
+
export const PhoneInput = forwardRef(function PhoneInput({ label, hideLabel = false, value, defaultValue, onValueChange, country: countryProp, defaultCountry = "US", onCountryChange, countries, preferredCountries = ["US", "CA", "GB"], description, error, validate = true, disabled = false, required, name, id, className, onBlur, }, forwardedRef) {
|
|
202
|
+
const reduced = useReducedFlag();
|
|
203
|
+
const uid = useId();
|
|
204
|
+
const inputId = id ?? `${uid}-number`;
|
|
205
|
+
const listId = `${uid}-list`;
|
|
206
|
+
const hintId = `${inputId}-hint`, errorId = `${inputId}-error`;
|
|
207
|
+
const optionId = (key) => `${uid}-opt-${key}`;
|
|
208
|
+
const pool = useMemo(() => countries?.length ? PHONE_COUNTRIES.filter(entry => countries.includes(entry.iso)) : PHONE_COUNTRIES, [countries]);
|
|
209
|
+
const fallback = BY_ISO.get(defaultCountry) ?? PHONE_COUNTRIES[0];
|
|
210
|
+
const [state, setState] = useState(() => {
|
|
211
|
+
const parsed = parsePhoneNumber(value ?? defaultValue ?? "", pool);
|
|
212
|
+
return parsed ? { iso: parsed.country.iso, digits: parsed.national } : { iso: fallback.iso, digits: "" };
|
|
213
|
+
});
|
|
214
|
+
const iso = countryProp ?? state.iso;
|
|
215
|
+
const current = BY_ISO.get(iso) ?? fallback;
|
|
216
|
+
const digits = state.digits;
|
|
217
|
+
const e164 = toE164(current, digits);
|
|
218
|
+
// A controlled value that differs from what the field last produced is read back in.
|
|
219
|
+
const [seenValue, setSeenValue] = useState(value);
|
|
220
|
+
if (value !== seenValue) {
|
|
221
|
+
setSeenValue(value);
|
|
222
|
+
if (value !== undefined && value !== e164) {
|
|
223
|
+
const parsed = parsePhoneNumber(value, pool);
|
|
224
|
+
setState(parsed ? { iso: parsed.country.iso, digits: parsed.national } : { iso, digits: "" });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const formatted = formatNational(current, digits);
|
|
228
|
+
const status = statusOf(current, digits);
|
|
229
|
+
const [touched, setTouched] = useState(false);
|
|
230
|
+
const lengths = lengthsOf(current);
|
|
231
|
+
const lengthCopy = lengths.length === 1 ? `${lengths[0]}` : `${lengths.slice(0, -1).join(", ")} or ${lengths[lengths.length - 1]}`;
|
|
232
|
+
const builtIn = validate && touched && (status === "incomplete" || status === "too-long") ? `Numbers in ${current.name} have ${lengthCopy} digits` : undefined;
|
|
233
|
+
const message = error ?? builtIn;
|
|
234
|
+
/* The guide: the rest of the example number, in the shape this country expects, drawn faintly after what was typed. */
|
|
235
|
+
const guide = useMemo(() => {
|
|
236
|
+
const [trunk, rest] = splitTrunk(current, digits);
|
|
237
|
+
if (rest.length >= current.example.length)
|
|
238
|
+
return "";
|
|
239
|
+
const full = formatNational(current, trunk + rest + current.example.slice(rest.length));
|
|
240
|
+
return full.startsWith(formatted) ? full.slice(formatted.length) : "";
|
|
241
|
+
}, [current, digits, formatted]);
|
|
242
|
+
const inputRef = useRef(null);
|
|
243
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
244
|
+
const pendingCaret = useRef(null);
|
|
245
|
+
const [, rerender] = useReducer((count) => count + 1, 0);
|
|
246
|
+
const emit = useCallback((entry, nextDigits) => {
|
|
247
|
+
const nextStatus = statusOf(entry, nextDigits);
|
|
248
|
+
onValueChange?.(toE164(entry, nextDigits), { country: entry, formatted: formatNational(entry, nextDigits), status: nextStatus, valid: nextStatus === "valid" });
|
|
249
|
+
}, [onValueChange]);
|
|
250
|
+
const setNumber = (nextDigits, caretDigits, entry = current) => {
|
|
251
|
+
const capped = capDigits(entry, nextDigits);
|
|
252
|
+
pendingCaret.current = caretDigits === null ? null : Math.min(caretDigits, capped.length);
|
|
253
|
+
if (entry.iso !== current.iso)
|
|
254
|
+
onCountryChange?.(entry.iso);
|
|
255
|
+
// Nothing changed (a refused digit): React restores the old text after this handler, so the caret is placed on the next render.
|
|
256
|
+
if (capped === digits && entry.iso === current.iso) {
|
|
257
|
+
rerender();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
setState({ iso: entry.iso, digits: capped });
|
|
261
|
+
emit(entry, capped);
|
|
262
|
+
};
|
|
263
|
+
function placeCaret() {
|
|
264
|
+
const input = inputRef.current, count = pendingCaret.current;
|
|
265
|
+
pendingCaret.current = null;
|
|
266
|
+
if (!input || count === null || document.activeElement !== input)
|
|
267
|
+
return;
|
|
268
|
+
const position = caretAfterDigits(input.value, count);
|
|
269
|
+
input.setSelectionRange(position, position);
|
|
270
|
+
}
|
|
271
|
+
useLayoutEffect(placeCaret);
|
|
272
|
+
const [announcement, setAnnouncement] = useState("");
|
|
273
|
+
/* ---------------------------------------------- Number entry ---------------------------------------------- */
|
|
274
|
+
function onNumberChange(input) {
|
|
275
|
+
const raw = input.value;
|
|
276
|
+
// Autofill and dropped text arrive as a change: an international number chooses its own country.
|
|
277
|
+
if (raw.includes("+") || (/^\s*00/.test(raw) && onlyDigits(raw).length > 6)) {
|
|
278
|
+
const parsed = parsePhoneNumber(raw.slice(Math.max(0, raw.indexOf("+"))), pool);
|
|
279
|
+
if (parsed && parsed.national) {
|
|
280
|
+
if (parsed.country.iso !== current.iso)
|
|
281
|
+
setAnnouncement(`Country set to ${parsed.country.name}`);
|
|
282
|
+
setNumber(parsed.national, parsed.national.length, parsed.country);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const caret = input.selectionStart ?? raw.length;
|
|
287
|
+
setNumber(onlyDigits(raw), onlyDigits(raw.slice(0, caret)).length);
|
|
288
|
+
}
|
|
289
|
+
function onNumberKeyDown(event) {
|
|
290
|
+
const input = event.currentTarget;
|
|
291
|
+
if (event.key === "+" && !event.metaKey && !event.ctrlKey) {
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
openList("+");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0;
|
|
297
|
+
if (start !== end || event.metaKey || event.ctrlKey || event.altKey)
|
|
298
|
+
return;
|
|
299
|
+
// Deleting a separator deletes the digit beside it instead of doing nothing.
|
|
300
|
+
if (event.key === "Backspace" && start > 0 && !/\d/.test(input.value[start - 1])) {
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
const index = onlyDigits(input.value.slice(0, start)).length;
|
|
303
|
+
if (index > 0)
|
|
304
|
+
setNumber(digits.slice(0, index - 1) + digits.slice(index), index - 1);
|
|
305
|
+
}
|
|
306
|
+
if (event.key === "Delete" && start < input.value.length && !/\d/.test(input.value[start])) {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
const index = onlyDigits(input.value.slice(0, start)).length;
|
|
309
|
+
setNumber(digits.slice(0, index) + digits.slice(index + 1), index);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function onPaste(event) {
|
|
313
|
+
const text = event.clipboardData.getData("text");
|
|
314
|
+
if (!text)
|
|
315
|
+
return;
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
const input = event.currentTarget;
|
|
318
|
+
const parsed = /^\s*(\+|00)/.test(text) ? parsePhoneNumber(text, pool) : null;
|
|
319
|
+
if (parsed) {
|
|
320
|
+
if (parsed.country.iso !== current.iso)
|
|
321
|
+
setAnnouncement(`Country set to ${parsed.country.name}`);
|
|
322
|
+
setNumber(parsed.national, parsed.national.length, parsed.country);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let pasted = onlyDigits(text);
|
|
326
|
+
// "1 415 555 0132" pasted into a US field: the leading calling code is dropped when the number would not fit otherwise.
|
|
327
|
+
if (pasted.startsWith(current.dial) && pasted.length > capDigits(current, pasted).length)
|
|
328
|
+
pasted = pasted.slice(current.dial.length);
|
|
329
|
+
// A whole number replaces what was there; a fragment goes in at the caret.
|
|
330
|
+
if (pasted.length >= Math.min(...lengthsOf(current))) {
|
|
331
|
+
setNumber(pasted, pasted.length);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const a = onlyDigits(input.value.slice(0, input.selectionStart ?? 0)).length;
|
|
335
|
+
const b = onlyDigits(input.value.slice(0, input.selectionEnd ?? 0)).length;
|
|
336
|
+
setNumber(digits.slice(0, a) + pasted + digits.slice(b), a + pasted.length);
|
|
337
|
+
}
|
|
338
|
+
/* ---------------------------------------------- Country picker ---------------------------------------------- */
|
|
339
|
+
const [open, setOpen] = useState(false);
|
|
340
|
+
const [query, setQuery] = useState("");
|
|
341
|
+
const [active, setActive] = useState(null);
|
|
342
|
+
const [roll, setRoll] = useState(1);
|
|
343
|
+
const needle = query.trim().toLowerCase();
|
|
344
|
+
const sections = useMemo(() => {
|
|
345
|
+
const sorted = [...pool].sort((a, b) => a.name.localeCompare(b.name));
|
|
346
|
+
if (needle && needle !== "+") {
|
|
347
|
+
const hits = sorted.filter(entry => matchesQuery(entry, needle));
|
|
348
|
+
const digitsOnly = onlyDigits(needle);
|
|
349
|
+
if (digitsOnly)
|
|
350
|
+
hits.sort((a, b) => Number(b.dial === digitsOnly) - Number(a.dial === digitsOnly) || a.dial.length - b.dial.length);
|
|
351
|
+
return [{ key: "results", rows: hits.map(entry => ({ key: `r-${entry.iso}`, entry })) }];
|
|
352
|
+
}
|
|
353
|
+
const preferred = preferredCountries.map(code => pool.find(entry => entry.iso === code)).filter((entry) => !!entry);
|
|
354
|
+
const all = { key: "all", label: preferred.length ? "All countries" : undefined, rows: sorted.map(entry => ({ key: `a-${entry.iso}`, entry })) };
|
|
355
|
+
return preferred.length ? [{ key: "preferred", label: "Suggested", rows: preferred.map(entry => ({ key: `p-${entry.iso}`, entry })) }, all] : [all];
|
|
356
|
+
}, [needle, pool, preferredCountries]);
|
|
357
|
+
const rows = useMemo(() => sections.flatMap(section => section.rows), [sections]);
|
|
358
|
+
const activeKey = active !== null && rows.some(row => row.key === active) ? active : rows[0]?.key ?? null;
|
|
359
|
+
const orderOf = useMemo(() => new Map([...pool].sort((a, b) => a.name.localeCompare(b.name)).map((entry, index) => [entry.iso, index])), [pool]);
|
|
360
|
+
const rootRef = useRef(null);
|
|
361
|
+
const controlRef = useRef(null);
|
|
362
|
+
const measureRef = useRef(null);
|
|
363
|
+
const listFaceRef = useRef(null);
|
|
364
|
+
const scrollRef = useRef(null);
|
|
365
|
+
const triggerRef = useRef(null);
|
|
366
|
+
const searchRef = useRef(null);
|
|
367
|
+
const rowRefs = useRef(new Map());
|
|
368
|
+
/* One surface whose width, height, and corners spring between the country button and the open list. */
|
|
369
|
+
const anchorW = useMotionValue("auto");
|
|
370
|
+
const shapeW = useMotionValue("100%"), shapeH = useMotionValue("100%"), radius = useMotionValue(CLOSED_RADIUS);
|
|
371
|
+
const sizes = useRef({ trigger: 0, lid: 0, panel: 0, list: 0 });
|
|
372
|
+
const live = useRef({ open: false, reduced: false, measured: false });
|
|
373
|
+
useLayoutEffect(() => { live.current.reduced = reduced; }, [reduced]);
|
|
374
|
+
const place = useCallback((animated) => {
|
|
375
|
+
const { trigger, lid, panel, list } = sizes.current;
|
|
376
|
+
if (!trigger || !lid)
|
|
377
|
+
return;
|
|
378
|
+
const isOpen = live.current.open;
|
|
379
|
+
const next = isOpen ? { w: Math.max(trigger, panel), h: lid + 2 + list, r: OPEN_RADIUS } : { w: trigger, h: lid, r: CLOSED_RADIUS };
|
|
380
|
+
if (!animated || live.current.reduced || !live.current.measured) {
|
|
381
|
+
anchorW.jump(trigger);
|
|
382
|
+
shapeW.jump(next.w);
|
|
383
|
+
shapeH.jump(next.h);
|
|
384
|
+
radius.jump(next.r);
|
|
385
|
+
live.current.measured = true;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const growing = isOpen;
|
|
389
|
+
animate(anchorW, trigger, WIDTH);
|
|
390
|
+
animate(shapeW, next.w, growing ? GROW : SHRINK);
|
|
391
|
+
animate(shapeH, next.h, growing ? GROW : SHRINK);
|
|
392
|
+
animate(radius, next.r, growing ? GROW : SHRINK);
|
|
393
|
+
}, [anchorW, radius, shapeH, shapeW]);
|
|
394
|
+
const read = useCallback(() => {
|
|
395
|
+
const measure = measureRef.current, control = controlRef.current, face = listFaceRef.current, root = rootRef.current;
|
|
396
|
+
if (!measure || !control || !face || !root)
|
|
397
|
+
return false;
|
|
398
|
+
const panel = Math.min(PANEL_MAX, control.offsetWidth);
|
|
399
|
+
root.style.setProperty("--pi-panel-w", `${panel}px`);
|
|
400
|
+
const previous = sizes.current;
|
|
401
|
+
const next = { trigger: measure.offsetWidth, lid: control.clientHeight, panel, list: face.offsetHeight };
|
|
402
|
+
sizes.current = next;
|
|
403
|
+
return next.trigger !== previous.trigger || next.lid !== previous.lid || next.panel !== previous.panel || (live.current.open && next.list !== previous.list);
|
|
404
|
+
}, []);
|
|
405
|
+
useLayoutEffect(() => {
|
|
406
|
+
const measure = measureRef.current, control = controlRef.current, face = listFaceRef.current;
|
|
407
|
+
if (read())
|
|
408
|
+
place(live.current.measured);
|
|
409
|
+
if (!measure || !control || !face || typeof ResizeObserver === "undefined")
|
|
410
|
+
return;
|
|
411
|
+
const observer = new ResizeObserver(() => { if (read())
|
|
412
|
+
place(live.current.measured); });
|
|
413
|
+
observer.observe(measure);
|
|
414
|
+
observer.observe(control);
|
|
415
|
+
observer.observe(face);
|
|
416
|
+
return () => observer.disconnect();
|
|
417
|
+
}, [place, read]);
|
|
418
|
+
const pendingFocus = useRef(null);
|
|
419
|
+
useLayoutEffect(() => {
|
|
420
|
+
if (live.current.open !== open) {
|
|
421
|
+
live.current.open = open;
|
|
422
|
+
read();
|
|
423
|
+
place(true);
|
|
424
|
+
}
|
|
425
|
+
const target = pendingFocus.current;
|
|
426
|
+
pendingFocus.current = null;
|
|
427
|
+
if (target === "search")
|
|
428
|
+
searchRef.current?.focus({ preventScroll: true });
|
|
429
|
+
if (target === "trigger")
|
|
430
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
431
|
+
if (target === "number") {
|
|
432
|
+
const input = inputRef.current;
|
|
433
|
+
input?.focus({ preventScroll: true });
|
|
434
|
+
input?.setSelectionRange(input.value.length, input.value.length);
|
|
435
|
+
}
|
|
436
|
+
}, [open, place, read]);
|
|
437
|
+
/* The highlight glides between rows on its own spring. */
|
|
438
|
+
const hy = useMotionValue(0), hh = useMotionValue(0), ho = useMotionValue(0);
|
|
439
|
+
const scrollIntent = useRef(false);
|
|
440
|
+
useLayoutEffect(() => {
|
|
441
|
+
const node = open && activeKey ? rowRefs.current.get(activeKey) : undefined;
|
|
442
|
+
if (!node) {
|
|
443
|
+
animate(ho, 0, { duration: reduced || !open ? 0 : .12, ease: standardEase });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const top = node.offsetTop, height = node.offsetHeight;
|
|
447
|
+
if (ho.get() < .05 || reduced) {
|
|
448
|
+
hy.jump(top);
|
|
449
|
+
hh.jump(height);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
animate(hy, top, GLIDE);
|
|
453
|
+
animate(hh, height, GLIDE);
|
|
454
|
+
}
|
|
455
|
+
animate(ho, 1, { duration: reduced ? 0 : .12, ease: enterEase });
|
|
456
|
+
const scroller = scrollRef.current;
|
|
457
|
+
if (scroller && scrollIntent.current) {
|
|
458
|
+
scrollIntent.current = false;
|
|
459
|
+
const pad = 6;
|
|
460
|
+
if (top < scroller.scrollTop + pad)
|
|
461
|
+
scroller.scrollTop = top - pad;
|
|
462
|
+
else if (top + height > scroller.scrollTop + scroller.clientHeight - pad)
|
|
463
|
+
scroller.scrollTop = top + height - scroller.clientHeight + pad;
|
|
464
|
+
}
|
|
465
|
+
}, [activeKey, hh, ho, hy, open, reduced, sections]);
|
|
466
|
+
function openList(seed = "") {
|
|
467
|
+
if (disabled || open)
|
|
468
|
+
return;
|
|
469
|
+
setQuery(seed);
|
|
470
|
+
const selectedRow = seed ? null : sections.flatMap(section => section.rows).find(row => row.entry.iso === current.iso && !row.key.startsWith("p-")) ?? null;
|
|
471
|
+
setActive(selectedRow?.key ?? null);
|
|
472
|
+
scrollIntent.current = true;
|
|
473
|
+
pendingFocus.current = "search";
|
|
474
|
+
setAnnouncement("");
|
|
475
|
+
setOpen(true);
|
|
476
|
+
}
|
|
477
|
+
const close = useCallback((focus) => {
|
|
478
|
+
pendingFocus.current = focus;
|
|
479
|
+
setOpen(false);
|
|
480
|
+
}, [setOpen]);
|
|
481
|
+
function pick(entry) {
|
|
482
|
+
if (!entry)
|
|
483
|
+
return;
|
|
484
|
+
if (entry.iso !== current.iso) {
|
|
485
|
+
setRoll(Math.sign((orderOf.get(entry.iso) ?? 0) - (orderOf.get(current.iso) ?? 0)) || 1);
|
|
486
|
+
const nextDigits = capDigits(entry, digits);
|
|
487
|
+
if (countryProp === undefined)
|
|
488
|
+
setState({ iso: entry.iso, digits: nextDigits });
|
|
489
|
+
else
|
|
490
|
+
setState(last => ({ ...last, digits: nextDigits }));
|
|
491
|
+
onCountryChange?.(entry.iso);
|
|
492
|
+
emit(entry, nextDigits);
|
|
493
|
+
setAnnouncement(`${entry.name}, +${entry.dial}`);
|
|
494
|
+
}
|
|
495
|
+
close("number");
|
|
496
|
+
}
|
|
497
|
+
function move(key) {
|
|
498
|
+
if (!key)
|
|
499
|
+
return;
|
|
500
|
+
scrollIntent.current = true;
|
|
501
|
+
setActive(key);
|
|
502
|
+
}
|
|
503
|
+
function onSearchKeyDown(event) {
|
|
504
|
+
const at = rows.findIndex(row => row.key === activeKey);
|
|
505
|
+
switch (event.key) {
|
|
506
|
+
case "ArrowDown":
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
move(rows[Math.min(rows.length - 1, at + 1)]?.key);
|
|
509
|
+
return;
|
|
510
|
+
case "ArrowUp":
|
|
511
|
+
event.preventDefault();
|
|
512
|
+
move(rows[Math.max(0, at - 1)]?.key);
|
|
513
|
+
return;
|
|
514
|
+
case "PageDown":
|
|
515
|
+
event.preventDefault();
|
|
516
|
+
move(rows[Math.min(rows.length - 1, at + PAGE)]?.key);
|
|
517
|
+
return;
|
|
518
|
+
case "PageUp":
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
move(rows[Math.max(0, at - PAGE)]?.key);
|
|
521
|
+
return;
|
|
522
|
+
case "Home":
|
|
523
|
+
if (query)
|
|
524
|
+
return;
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
move(rows[0]?.key);
|
|
527
|
+
return;
|
|
528
|
+
case "End":
|
|
529
|
+
if (query)
|
|
530
|
+
return;
|
|
531
|
+
event.preventDefault();
|
|
532
|
+
move(rows[rows.length - 1]?.key);
|
|
533
|
+
return;
|
|
534
|
+
case "Enter":
|
|
535
|
+
event.preventDefault();
|
|
536
|
+
pick(rows[at]?.entry);
|
|
537
|
+
return;
|
|
538
|
+
case "Escape":
|
|
539
|
+
event.preventDefault();
|
|
540
|
+
event.stopPropagation();
|
|
541
|
+
close("trigger");
|
|
542
|
+
return;
|
|
543
|
+
case "Tab":
|
|
544
|
+
close(null);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function onTriggerKeyDown(event) {
|
|
549
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
550
|
+
event.preventDefault();
|
|
551
|
+
openList();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (event.key.length === 1 && event.key !== " " && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
555
|
+
event.preventDefault();
|
|
556
|
+
openList(event.key);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function onQuery(next) {
|
|
560
|
+
// Typing a whole number after "+" (the habit of writing "+1 202 555 0142") moves it into the number field as soon
|
|
561
|
+
// as it is more than a calling code, under its country; the rest of it goes on in the number field.
|
|
562
|
+
const typed = numberInSearch(next, pool);
|
|
563
|
+
if (typed) {
|
|
564
|
+
if (typed.country.iso !== current.iso)
|
|
565
|
+
setAnnouncement(`Country set to ${typed.country.name}`);
|
|
566
|
+
setNumber(typed.national, typed.national.length, typed.country);
|
|
567
|
+
setQuery("");
|
|
568
|
+
close("number");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
setQuery(next);
|
|
572
|
+
setActive(null);
|
|
573
|
+
if (scrollRef.current)
|
|
574
|
+
scrollRef.current.scrollTop = 0;
|
|
575
|
+
const text = next.trim().toLowerCase();
|
|
576
|
+
const count = text && text !== "+" ? pool.filter(entry => matchesQuery(entry, text)).length : pool.length;
|
|
577
|
+
setAnnouncement(text ? count ? `${count} ${count === 1 ? "country" : "countries"}` : "No matches" : "");
|
|
578
|
+
}
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
if (!open)
|
|
581
|
+
return;
|
|
582
|
+
const down = (event) => { if (!rootRef.current?.contains(event.target))
|
|
583
|
+
close(null); };
|
|
584
|
+
document.addEventListener("pointerdown", down);
|
|
585
|
+
return () => document.removeEventListener("pointerdown", down);
|
|
586
|
+
}, [close, open]);
|
|
587
|
+
const onRootBlur = (event) => {
|
|
588
|
+
const next = event.relatedTarget;
|
|
589
|
+
if (open && next && !event.currentTarget.contains(next))
|
|
590
|
+
close(null);
|
|
591
|
+
};
|
|
592
|
+
const showCheck = status === "valid";
|
|
593
|
+
const invalid = !!message;
|
|
594
|
+
const describedBy = [description ? hintId : null, message ? errorId : null].filter(Boolean).join(" ") || undefined;
|
|
595
|
+
const rootStyle = { "--pi-closed-r": `${CLOSED_RADIUS}px` };
|
|
596
|
+
const face = (entry) => _jsxs(_Fragment, { children: [_jsx("span", { className: styles.flag, "aria-hidden": "true", children: flagOf(entry.iso) }), _jsxs("span", { className: styles.dial, children: ["+", entry.dial] })] });
|
|
597
|
+
return _jsxs("div", { ref: rootRef, className: [styles.field, className].filter(Boolean).join(" "), style: rootStyle, "data-open": open || undefined, "data-disabled": disabled || undefined, onBlur: onRootBlur, children: [_jsx("label", { htmlFor: inputId, className: hideLabel ? styles.srOnly : styles.label, children: label }), _jsxs("div", { ref: controlRef, className: styles.control, "data-invalid": invalid || undefined, children: [_jsxs(motion.div, { className: styles.anchor, style: { width: anchorW }, children: [_jsx("span", { ref: measureRef, className: `${styles.trigger} ${styles.measure}`, "aria-hidden": "true", children: face(current) }), _jsxs(motion.div, { className: styles.shape, style: { width: shapeW, height: shapeH, borderRadius: radius }, children: [_jsxs("div", { className: styles.lid, children: [_jsx("button", { ref: triggerRef, type: "button", className: styles.trigger, disabled: disabled, inert: open || undefined, tabIndex: open ? -1 : 0, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listId, "aria-label": `Country, ${current.name} +${current.dial}`, onClick: () => (open ? close("trigger") : openList()), onKeyDown: onTriggerKeyDown, children: _jsx("span", { className: styles.slot, "data-hidden": open || undefined, children: _jsx(AnimatePresence, { initial: false, custom: roll, children: _jsx(motion.span, { className: styles.layer, custom: roll, variants: reduced ? layerFade : layerVariants, initial: "enter", animate: "rest", exit: "exit", transition: reduced ? { duration: .12 } : { y: GLIDE, opacity: { duration: .2, ease: enterEase }, filter: { duration: .22, ease: enterEase } }, children: face(current) }, current.iso) }) }) }), _jsxs(motion.div, { className: styles.searchRow, inert: !open || undefined, initial: false, animate: open ? { opacity: 1, filter: "blur(0px)" } : { opacity: 0, filter: reduced ? "blur(0px)" : `blur(${blur.subtle}px)` }, transition: open ? { duration: .18, ease: enterEase, delay: reduced ? 0 : .05 } : { duration: .1, ease: standardEase }, children: [_jsx(Search, { className: styles.searchIcon, size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { ref: searchRef, className: styles.search, type: "text", role: "combobox", "aria-label": "Search countries or calling codes", "aria-expanded": open, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": open && activeKey ? optionId(activeKey) : undefined, placeholder: "Country or code", value: query, autoComplete: "off", spellCheck: false, onChange: event => onQuery(event.target.value), onKeyDown: onSearchKeyDown }), _jsx(AnimatePresence, { initial: false, children: query && _jsx(motion.button, { type: "button", className: styles.clear, "aria-label": "Clear search", onPointerDown: event => event.preventDefault(), onClick: () => { onQuery(""); searchRef.current?.focus(); }, initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: .1 } }, transition: reduced ? { duration: 0 } : GLIDE, children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) }, "clear") }), _jsx("button", { type: "button", className: styles.closeHit, "aria-label": "Close country list", onClick: () => close("trigger") })] }), _jsx(motion.span, { className: styles.chevron, "aria-hidden": "true", initial: false, animate: { rotate: open ? 180 : 0 }, transition: reduced ? { duration: 0 } : GLIDE, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] }), _jsx(motion.div, { ref: listFaceRef, className: styles.listFace, inert: !open || undefined, "aria-hidden": !open || undefined, initial: false, animate: open ? { opacity: 1, y: 0, filter: "blur(0px)" } : { opacity: 0, y: reduced ? 0 : -6, filter: reduced ? "blur(0px)" : `blur(${blur.subtle}px)` }, transition: open ? { y: GROW, opacity: { duration: .2, ease: enterEase, delay: reduced ? 0 : .04 }, filter: { duration: .22, ease: enterEase, delay: .04 } } : { duration: .1, ease: standardEase }, children: _jsx("div", { ref: scrollRef, className: styles.scroll, children: _jsxs("div", { id: listId, className: styles.options, role: "listbox", "aria-label": "Countries", children: [_jsx(motion.span, { className: styles.highlight, style: { y: hy, height: hh, opacity: ho }, "aria-hidden": "true" }), sections.map(section => {
|
|
598
|
+
const body = section.rows.map(row => {
|
|
599
|
+
const selected = row.entry.iso === current.iso;
|
|
600
|
+
return _jsxs("div", { id: optionId(row.key), role: "option", "aria-selected": selected, className: styles.option, "data-active": row.key === activeKey || undefined, ref: node => { if (node)
|
|
601
|
+
rowRefs.current.set(row.key, node);
|
|
602
|
+
else
|
|
603
|
+
rowRefs.current.delete(row.key); }, onPointerMove: event => { if (event.pointerType === "mouse" && row.key !== activeKey)
|
|
604
|
+
setActive(row.key); }, onPointerDown: event => event.preventDefault(), onClick: () => pick(row.entry), children: [_jsx("span", { className: styles.flag, "aria-hidden": "true", children: flagOf(row.entry.iso) }), _jsx("span", { className: styles.name, children: row.entry.name }), _jsxs("span", { className: styles.meta, children: ["+", row.entry.dial] }), _jsx("span", { className: styles.check, "data-on": selected || undefined, "aria-hidden": "true", children: _jsx(Check, { size: 16, strokeWidth: 1.75 }) })] }, row.key);
|
|
605
|
+
});
|
|
606
|
+
return section.label
|
|
607
|
+
? _jsxs("div", { role: "group", "aria-labelledby": `${uid}-${section.key}`, className: styles.group, children: [_jsx("div", { id: `${uid}-${section.key}`, className: styles.groupLabel, role: "presentation", children: section.label }), body] }, section.key)
|
|
608
|
+
: _jsx("div", { role: "presentation", className: styles.group, children: body }, section.key);
|
|
609
|
+
}), rows.length === 0 && _jsxs("p", { className: styles.empty, children: ["No countries match \u201C", query.trim(), "\u201D"] })] }) }) })] })] }), _jsxs("div", { className: styles.numberWrap, children: [_jsxs("span", { className: styles.guide, "aria-hidden": "true", children: [_jsx("span", { className: styles.guideTyped, children: formatted }), guide] }), _jsx("input", { ref: inputRef, id: inputId, className: styles.number, type: "tel", inputMode: "tel", autoComplete: "tel", value: formatted, disabled: disabled, required: required, "aria-invalid": invalid || undefined, "aria-describedby": describedBy, "aria-label": hideLabel ? label : undefined, onChange: event => onNumberChange(event.currentTarget), onKeyDown: onNumberKeyDown, onPaste: onPaste, onBlur: event => { setTouched(digits.length > 0); onBlur?.(event); } }), _jsx(AnimatePresence, { initial: false, children: showCheck && _jsx(motion.span, { className: styles.valid, "aria-hidden": "true", initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .6, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)" }, exit: { opacity: 0, scale: reduced ? 1 : .8, transition: { duration: .1 } }, transition: reduced ? { duration: .12 } : motionTokens.spring.snappy, children: _jsx(Check, { size: 16, strokeWidth: 2 }) }, "ok") })] })] }), _jsx(AnimatePresence, { initial: false, children: description && !message && _jsx(MessageRow, { id: hintId, text: description, tone: "hint" }, "hint") }), _jsx(AnimatePresence, { initial: false, children: message && _jsx(MessageRow, { id: errorId, text: message, tone: "error" }, "error") }), name && _jsx("input", { type: "hidden", name: name, value: e164 }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement || (showCheck ? `Valid ${current.name} number` : "") })] });
|
|
610
|
+
});
|
|
611
|
+
PhoneInput.displayName = "PhoneInput";
|
|
612
|
+
export default PhoneInput;
|