@umbra.ui/core 0.1.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.
Files changed (272) hide show
  1. package/dist/components/controls/Dropdown/types.d.ts +5 -0
  2. package/dist/components/controls/Dropdown/types.d.ts.map +1 -0
  3. package/dist/components/controls/Dropdown/types.js +1 -0
  4. package/dist/components/controls/SegmentedControl/types.d.ts +6 -0
  5. package/dist/components/controls/SegmentedControl/types.d.ts.map +1 -0
  6. package/dist/components/controls/SegmentedControl/types.js +1 -0
  7. package/dist/components/dialogs/Alert/types.d.ts +7 -0
  8. package/dist/components/dialogs/Alert/types.d.ts.map +1 -0
  9. package/dist/components/dialogs/Alert/types.js +1 -0
  10. package/dist/components/dialogs/Toast/types.d.ts +34 -0
  11. package/dist/components/dialogs/Toast/types.d.ts.map +1 -0
  12. package/dist/components/dialogs/Toast/types.js +10 -0
  13. package/dist/components/dialogs/Toast/useToast.d.ts +36 -0
  14. package/dist/components/dialogs/Toast/useToast.d.ts.map +1 -0
  15. package/dist/components/dialogs/Toast/useToast.js +90 -0
  16. package/dist/components/indicators/Tooltip/tooltip.d.ts +3 -0
  17. package/dist/components/indicators/Tooltip/tooltip.d.ts.map +1 -0
  18. package/dist/components/indicators/Tooltip/tooltip.js +33 -0
  19. package/dist/components/indicators/Tooltip/types.d.ts +14 -0
  20. package/dist/components/indicators/Tooltip/types.d.ts.map +1 -0
  21. package/dist/components/indicators/Tooltip/types.js +1 -0
  22. package/dist/components/indicators/Tooltip/useTooltip.d.ts +18 -0
  23. package/dist/components/indicators/Tooltip/useTooltip.d.ts.map +1 -0
  24. package/dist/components/indicators/Tooltip/useTooltip.js +57 -0
  25. package/dist/components/inputs/Tags/tag-bar-styles.d.ts +14 -0
  26. package/dist/components/inputs/Tags/tag-bar-styles.d.ts.map +1 -0
  27. package/dist/components/inputs/Tags/tag-bar-styles.js +313 -0
  28. package/dist/components/inputs/Tags/types.d.ts +93 -0
  29. package/dist/components/inputs/Tags/types.d.ts.map +1 -0
  30. package/dist/components/inputs/Tags/types.js +216 -0
  31. package/dist/components/inputs/search/types.d.ts +9 -0
  32. package/dist/components/inputs/search/types.d.ts.map +1 -0
  33. package/dist/components/inputs/search/types.js +1 -0
  34. package/dist/components/navigation/adaptive/types.d.ts +16 -0
  35. package/dist/components/navigation/adaptive/types.d.ts.map +1 -0
  36. package/dist/components/navigation/adaptive/types.js +1 -0
  37. package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts +27 -0
  38. package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts.map +1 -0
  39. package/dist/components/navigation/adaptive/useAdaptiveLayout.js +40 -0
  40. package/dist/components/navigation/adaptive/useBreakpoints.d.ts +6 -0
  41. package/dist/components/navigation/adaptive/useBreakpoints.d.ts.map +1 -0
  42. package/dist/components/navigation/adaptive/useBreakpoints.js +37 -0
  43. package/dist/components/navigation/adaptive/useContainerMonitor.d.ts +93 -0
  44. package/dist/components/navigation/adaptive/useContainerMonitor.d.ts.map +1 -0
  45. package/dist/components/navigation/adaptive/useContainerMonitor.js +145 -0
  46. package/dist/components/navigation/adaptive/useViewAnimation.d.ts +31 -0
  47. package/dist/components/navigation/adaptive/useViewAnimation.d.ts.map +1 -0
  48. package/dist/components/navigation/adaptive/useViewAnimation.js +591 -0
  49. package/dist/components/navigation/adaptive/useViewResize.d.ts +52 -0
  50. package/dist/components/navigation/adaptive/useViewResize.d.ts.map +1 -0
  51. package/dist/components/navigation/adaptive/useViewResize.js +146 -0
  52. package/dist/components/navigation/navstack/useNavigationStack.d.ts +25 -0
  53. package/dist/components/navigation/navstack/useNavigationStack.d.ts.map +1 -0
  54. package/dist/components/navigation/navstack/useNavigationStack.js +133 -0
  55. package/dist/components/navigation/slideover/useSlideoverController.d.ts +20 -0
  56. package/dist/components/navigation/slideover/useSlideoverController.d.ts.map +1 -0
  57. package/dist/components/navigation/slideover/useSlideoverController.js +267 -0
  58. package/dist/components/navigation/splitview/useSplitViewController.d.ts +20 -0
  59. package/dist/components/navigation/splitview/useSplitViewController.d.ts.map +1 -0
  60. package/dist/components/navigation/splitview/useSplitViewController.js +325 -0
  61. package/dist/components/navigation/tabcontroller/types.d.ts +21 -0
  62. package/dist/components/navigation/tabcontroller/types.d.ts.map +1 -0
  63. package/dist/components/navigation/tabcontroller/types.js +1 -0
  64. package/dist/components/navigation/tabcontroller/useTabController.d.ts +5 -0
  65. package/dist/components/navigation/tabcontroller/useTabController.d.ts.map +1 -0
  66. package/dist/components/navigation/tabcontroller/useTabController.js +10 -0
  67. package/dist/components/navigation/types.d.ts +8 -0
  68. package/dist/components/navigation/types.d.ts.map +1 -0
  69. package/dist/components/navigation/types.js +1 -0
  70. package/dist/components/pickers/CollectionPicker/types.d.ts +11 -0
  71. package/dist/components/pickers/CollectionPicker/types.d.ts.map +1 -0
  72. package/dist/components/pickers/CollectionPicker/types.js +1 -0
  73. package/dist/components/pickers/ColorPicker/colors.d.ts +13 -0
  74. package/dist/components/pickers/ColorPicker/colors.d.ts.map +1 -0
  75. package/dist/components/pickers/ColorPicker/colors.js +266 -0
  76. package/dist/components/pickers/FilePicker/types.d.ts +10 -0
  77. package/dist/components/pickers/FilePicker/types.d.ts.map +1 -0
  78. package/dist/components/pickers/FilePicker/types.js +1 -0
  79. package/dist/index.d.ts +91 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +196 -0
  82. package/dist/theme.d.ts +73 -0
  83. package/dist/theme.d.ts.map +1 -0
  84. package/dist/theme.js +279 -0
  85. package/dist/themes/blank.d.ts +7 -0
  86. package/dist/themes/blank.d.ts.map +1 -0
  87. package/dist/themes/blank.js +543 -0
  88. package/dist/themes/crimson-dark.d.ts +4 -0
  89. package/dist/themes/crimson-dark.d.ts.map +1 -0
  90. package/dist/themes/crimson-dark.js +552 -0
  91. package/dist/themes/cyan-light.d.ts +4 -0
  92. package/dist/themes/cyan-light.d.ts.map +1 -0
  93. package/dist/themes/cyan-light.js +552 -0
  94. package/dist/themes/dark.d.ts +4 -0
  95. package/dist/themes/dark.d.ts.map +1 -0
  96. package/dist/themes/dark.js +551 -0
  97. package/dist/themes/gold-dark.d.ts +4 -0
  98. package/dist/themes/gold-dark.d.ts.map +1 -0
  99. package/dist/themes/gold-dark.js +552 -0
  100. package/dist/themes/grass-dark.d.ts +4 -0
  101. package/dist/themes/grass-dark.d.ts.map +1 -0
  102. package/dist/themes/grass-dark.js +552 -0
  103. package/dist/themes/indigo.d.ts +4 -0
  104. package/dist/themes/indigo.d.ts.map +1 -0
  105. package/dist/themes/indigo.js +552 -0
  106. package/dist/themes/light.d.ts +4 -0
  107. package/dist/themes/light.d.ts.map +1 -0
  108. package/dist/themes/light.js +551 -0
  109. package/dist/themes/orange-dark.d.ts +4 -0
  110. package/dist/themes/orange-dark.d.ts.map +1 -0
  111. package/dist/themes/orange-dark.js +551 -0
  112. package/dist/themes/orange-light.d.ts +4 -0
  113. package/dist/themes/orange-light.d.ts.map +1 -0
  114. package/dist/themes/orange-light.js +551 -0
  115. package/package.json +62 -0
  116. package/src/components/controls/Button/Button.vue +417 -0
  117. package/src/components/controls/Button/README.md +348 -0
  118. package/src/components/controls/Button/theme.css +200 -0
  119. package/src/components/controls/Checkbox/Checkbox.vue +164 -0
  120. package/src/components/controls/Checkbox/README.md +441 -0
  121. package/src/components/controls/Checkbox/theme.css +36 -0
  122. package/src/components/controls/Dropdown/Dropdown.vue +476 -0
  123. package/src/components/controls/Dropdown/README.md +370 -0
  124. package/src/components/controls/Dropdown/theme.css +50 -0
  125. package/src/components/controls/Dropdown/types.ts +6 -0
  126. package/src/components/controls/IconButton/IconButton.vue +267 -0
  127. package/src/components/controls/IconButton/README.md +502 -0
  128. package/src/components/controls/IconButton/theme.css +89 -0
  129. package/src/components/controls/Radio/README.md +591 -0
  130. package/src/components/controls/Radio/Radio.vue +89 -0
  131. package/src/components/controls/Radio/theme.css +14 -0
  132. package/src/components/controls/RangeSlider/README.md +608 -0
  133. package/src/components/controls/RangeSlider/RangeSlider.vue +535 -0
  134. package/src/components/controls/RangeSlider/theme.css +80 -0
  135. package/src/components/controls/SegmentedControl/README.md +587 -0
  136. package/src/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
  137. package/src/components/controls/SegmentedControl/theme.css +60 -0
  138. package/src/components/controls/SegmentedControl/types.ts +5 -0
  139. package/src/components/controls/Slider/README.md +627 -0
  140. package/src/components/controls/Slider/Slider.vue +260 -0
  141. package/src/components/controls/Slider/theme.css +74 -0
  142. package/src/components/controls/Stepper/README.md +601 -0
  143. package/src/components/controls/Stepper/Stepper.vue +103 -0
  144. package/src/components/controls/Stepper/theme.css +53 -0
  145. package/src/components/controls/Switch/README.md +667 -0
  146. package/src/components/controls/Switch/Switch.vue +127 -0
  147. package/src/components/controls/Switch/theme.css +42 -0
  148. package/src/components/dialogs/Alert/Alert.vue +218 -0
  149. package/src/components/dialogs/Alert/README.md +450 -0
  150. package/src/components/dialogs/Alert/theme.css +44 -0
  151. package/src/components/dialogs/Alert/types.ts +11 -0
  152. package/src/components/dialogs/Toast/README.md +522 -0
  153. package/src/components/dialogs/Toast/Toast.vue +296 -0
  154. package/src/components/dialogs/Toast/ToastContainer.vue +330 -0
  155. package/src/components/dialogs/Toast/theme.css +44 -0
  156. package/src/components/dialogs/Toast/types.ts +46 -0
  157. package/src/components/dialogs/Toast/useToast.ts +127 -0
  158. package/src/components/indicators/ProgressBar/ProgressBar.vue +98 -0
  159. package/src/components/indicators/ProgressBar/README.md +744 -0
  160. package/src/components/indicators/ProgressBar/theme.css +36 -0
  161. package/src/components/indicators/Tooltip/README.md +723 -0
  162. package/src/components/indicators/Tooltip/TooltipProvider.vue +142 -0
  163. package/src/components/indicators/Tooltip/theme.css +18 -0
  164. package/src/components/indicators/Tooltip/tooltip.ts +48 -0
  165. package/src/components/indicators/Tooltip/types.ts +15 -0
  166. package/src/components/indicators/Tooltip/useTooltip.ts +71 -0
  167. package/src/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
  168. package/src/components/inputs/AutogrowTextView/README.md +643 -0
  169. package/src/components/inputs/AutogrowTextView/theme.css +28 -0
  170. package/src/components/inputs/InputCard/InputCard.vue +600 -0
  171. package/src/components/inputs/InputCard/README.md +636 -0
  172. package/src/components/inputs/InputEmail/InputEmail.vue +698 -0
  173. package/src/components/inputs/InputEmail/README.md +764 -0
  174. package/src/components/inputs/InputNumber/InputNumber.vue +300 -0
  175. package/src/components/inputs/InputNumber/README.md +749 -0
  176. package/src/components/inputs/InputPhone/InputPhone.vue +645 -0
  177. package/src/components/inputs/InputPhone/README.md +636 -0
  178. package/src/components/inputs/InputSecure/InputSecure.vue +646 -0
  179. package/src/components/inputs/InputSecure/README.md +771 -0
  180. package/src/components/inputs/InputText/InputText.vue +225 -0
  181. package/src/components/inputs/InputText/README.md +844 -0
  182. package/src/components/inputs/OTP/OTP.vue +349 -0
  183. package/src/components/inputs/OTP/README.md +736 -0
  184. package/src/components/inputs/OTP/theme.css +50 -0
  185. package/src/components/inputs/StringCapture/README.md +718 -0
  186. package/src/components/inputs/StringCapture/StringCapture.vue +315 -0
  187. package/src/components/inputs/StringCapture/theme.css +86 -0
  188. package/src/components/inputs/Tags/README.md +897 -0
  189. package/src/components/inputs/Tags/TagBar.vue +793 -0
  190. package/src/components/inputs/Tags/TagCreation.vue +219 -0
  191. package/src/components/inputs/Tags/TagPicker.vue +380 -0
  192. package/src/components/inputs/Tags/tag-bar-styles.ts +354 -0
  193. package/src/components/inputs/Tags/theme.css +121 -0
  194. package/src/components/inputs/Tags/types.ts +346 -0
  195. package/src/components/inputs/search/README.md +759 -0
  196. package/src/components/inputs/search/SearchBar.vue +394 -0
  197. package/src/components/inputs/search/SearchResults.vue +310 -0
  198. package/src/components/inputs/search/theme.css +187 -0
  199. package/src/components/inputs/search/types.ts +8 -0
  200. package/src/components/inputs/theme.css +102 -0
  201. package/src/components/menus/ActionMenu/ActionMenu.vue +383 -0
  202. package/src/components/menus/ActionMenu/README.md +825 -0
  203. package/src/components/menus/ActionMenu/theme.css +93 -0
  204. package/src/components/models/Popover/Popover.vue +551 -0
  205. package/src/components/models/Popover/README.md +885 -0
  206. package/src/components/models/Popover/theme.css +52 -0
  207. package/src/components/models/Sheet/README.md +1159 -0
  208. package/src/components/models/Sheet/Sheet.vue +465 -0
  209. package/src/components/models/Sheet/theme.css +72 -0
  210. package/src/components/models/Sidebar/README.md +1228 -0
  211. package/src/components/models/Sidebar/Sidebar.vue +480 -0
  212. package/src/components/models/Sidebar/theme.css +90 -0
  213. package/src/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
  214. package/src/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
  215. package/src/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
  216. package/src/components/navigation/adaptive/README.md +768 -0
  217. package/src/components/navigation/adaptive/types.ts +19 -0
  218. package/src/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
  219. package/src/components/navigation/adaptive/useBreakpoints.ts +41 -0
  220. package/src/components/navigation/adaptive/useContainerMonitor.ts +214 -0
  221. package/src/components/navigation/adaptive/useViewAnimation.ts +721 -0
  222. package/src/components/navigation/adaptive/useViewResize.ts +211 -0
  223. package/src/components/navigation/navstack/NavigationStack.vue +180 -0
  224. package/src/components/navigation/navstack/README.md +994 -0
  225. package/src/components/navigation/navstack/useNavigationStack.ts +164 -0
  226. package/src/components/navigation/slideover/README.md +1275 -0
  227. package/src/components/navigation/slideover/SlideoverController.vue +287 -0
  228. package/src/components/navigation/slideover/useSlideoverController.ts +320 -0
  229. package/src/components/navigation/splitview/README.md +1115 -0
  230. package/src/components/navigation/splitview/SplitViewController.vue +176 -0
  231. package/src/components/navigation/splitview/useSplitViewController.ts +388 -0
  232. package/src/components/navigation/tabcontroller/README.md +919 -0
  233. package/src/components/navigation/tabcontroller/TabController.vue +307 -0
  234. package/src/components/navigation/tabcontroller/TabItem.vue +57 -0
  235. package/src/components/navigation/tabcontroller/types.ts +24 -0
  236. package/src/components/navigation/tabcontroller/useTabController.ts +18 -0
  237. package/src/components/navigation/theme.css +91 -0
  238. package/src/components/navigation/types.ts +7 -0
  239. package/src/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
  240. package/src/components/pickers/CollectionPicker/README.md +1115 -0
  241. package/src/components/pickers/CollectionPicker/theme.css +14 -0
  242. package/src/components/pickers/CollectionPicker/types.ts +11 -0
  243. package/src/components/pickers/ColorPicker/ColorPicker.vue +376 -0
  244. package/src/components/pickers/ColorPicker/README.md +1439 -0
  245. package/src/components/pickers/ColorPicker/colors.ts +299 -0
  246. package/src/components/pickers/ColorPicker/theme.css +32 -0
  247. package/src/components/pickers/DatePicker/DatePicker.vue +660 -0
  248. package/src/components/pickers/DatePicker/README.md +1195 -0
  249. package/src/components/pickers/DatePicker/theme.css +22 -0
  250. package/src/components/pickers/FilePicker/FilePicker.vue +534 -0
  251. package/src/components/pickers/FilePicker/README.md +1542 -0
  252. package/src/components/pickers/FilePicker/theme.css +48 -0
  253. package/src/components/pickers/FilePicker/types.ts +10 -0
  254. package/src/components/pickers/IconPicker/IconPicker.vue +327 -0
  255. package/src/components/pickers/IconPicker/README.md +1161 -0
  256. package/src/components/pickers/IconPicker/theme.css +28 -0
  257. package/src/components/pickers/theme.css +82 -0
  258. package/src/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
  259. package/src/components/views/MarkdownViewer/README.md +833 -0
  260. package/src/components/views/MarkdownViewer/theme.css +130 -0
  261. package/src/index.ts +263 -0
  262. package/src/theme.ts +378 -0
  263. package/src/themes/crimson-dark.ts +556 -0
  264. package/src/themes/cyan-light.ts +556 -0
  265. package/src/themes/dark.ts +557 -0
  266. package/src/themes/gold-dark.ts +556 -0
  267. package/src/themes/grass-dark.ts +556 -0
  268. package/src/themes/indigo.ts +556 -0
  269. package/src/themes/light.ts +557 -0
  270. package/src/themes/orange-dark.ts +557 -0
  271. package/src/themes/orange-light.ts +557 -0
  272. package/src/vue.d.ts +45 -0
@@ -0,0 +1,645 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, computed } from "vue";
3
+ import "../theme.css";
4
+ import {
5
+ PhoneIcon,
6
+ TriangleWarningIcon,
7
+ CircleCheckIcon,
8
+ } from "@umbra-ui/icons";
9
+
10
+ export interface Props {
11
+ value?: string;
12
+ placeholder?: string;
13
+ defaultCountry?: string; // ISO country code (e.g., 'US', 'GB')
14
+ allowInternational?: boolean;
15
+ showCountryCode?: boolean; // Whether to show country code when detected
16
+ state?: "normal" | "active" | "disabled" | "readonly" | "error";
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ value: "",
21
+ placeholder: "Enter phone number",
22
+ defaultCountry: "US",
23
+ allowInternational: true,
24
+ showCountryCode: true,
25
+ state: "normal",
26
+ });
27
+
28
+ const emit = defineEmits<{
29
+ "update:value": [value: string];
30
+ "update:formatted": [value: string];
31
+ "update:valid": [value: boolean];
32
+ }>();
33
+
34
+ const internalValue = ref(props.value);
35
+ const cursorPosition = ref<number | null>(null);
36
+ const inputRef = ref<HTMLInputElement | null>(null);
37
+
38
+ // Add this computed property to track current digit count
39
+ const currentDigitCount = computed(() => {
40
+ return internalValue.value.replace(/\D/g, "").length;
41
+ });
42
+
43
+ // Add this computed property for max allowed digits
44
+ const maxAllowedDigits = computed(() => {
45
+ const cleaned = internalValue.value.replace(/\D/g, "");
46
+ const country = detectCountry(cleaned);
47
+
48
+ if (country === "US") {
49
+ return cleaned.startsWith("1") ? 11 : 10;
50
+ } else if (country === "GB") {
51
+ return cleaned.startsWith("44") ? 12 : 11;
52
+ }
53
+ return 15; // International max
54
+ });
55
+
56
+ // Country code patterns and formatting rules
57
+ const countryFormats = {
58
+ US: {
59
+ pattern: /^(\+?1)?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})$/,
60
+ format: (match: RegExpMatchArray) => {
61
+ const countryCode = match[1];
62
+ const areaCode = match[2];
63
+ const prefix = match[3];
64
+ const lineNumber = match[4];
65
+ if (countryCode) {
66
+ return `+1 (${areaCode}) ${prefix}-${lineNumber}`;
67
+ }
68
+ return `(${areaCode}) ${prefix}-${lineNumber}`;
69
+ },
70
+ placeholder: "(555) 123-4567",
71
+ maxLength: 14,
72
+ countryCode: "1",
73
+ },
74
+ GB: {
75
+ pattern: /^(\+?44)?[\s.-]?(\d{4,5})[\s.-]?(\d{6})$/,
76
+ format: (match: RegExpMatchArray) => {
77
+ const countryCode = match[1];
78
+ const prefix = match[2];
79
+ const number = match[3];
80
+ if (countryCode) {
81
+ return `+44 ${prefix} ${number}`;
82
+ }
83
+ return `${prefix} ${number}`;
84
+ },
85
+ placeholder: "20 1234 5678",
86
+ maxLength: 13,
87
+ countryCode: "44",
88
+ },
89
+ // Add more country formats as needed
90
+ };
91
+
92
+ // Detect country from number
93
+ const detectCountry = (number: string): string => {
94
+ const cleaned = number.replace(/\D/g, "");
95
+
96
+ // Check for country codes at the beginning
97
+ if (
98
+ cleaned.startsWith("1") &&
99
+ (cleaned.length === 11 || cleaned.length > 10)
100
+ ) {
101
+ return "US"; // US with country code
102
+ }
103
+ if (cleaned.startsWith("44")) {
104
+ return "GB";
105
+ }
106
+
107
+ // Default to US for 10-digit numbers or shorter
108
+ if (cleaned.length <= 10) {
109
+ return "US"; // US without country code
110
+ }
111
+
112
+ return props.defaultCountry;
113
+ };
114
+
115
+ // Format phone number based on country
116
+ const formatPhoneNumber = (
117
+ value: string
118
+ ): { formatted: string; cursorPos: number } => {
119
+ // Ensure we only work with digits
120
+ let cleaned = value.replace(/\D/g, "");
121
+
122
+ if (cleaned.length === 0) return { formatted: "", cursorPos: 0 };
123
+
124
+ const country = detectCountry(cleaned);
125
+ let formatted = "";
126
+
127
+ // For US numbers
128
+ if (country === "US") {
129
+ // Check if number starts with country code 1
130
+ if (cleaned.startsWith("1") && cleaned.length > 10) {
131
+ // Has country code - strictly limit to 11 digits total
132
+ const validDigits = cleaned.slice(0, 11);
133
+ const withoutCountryCode = validDigits.slice(1);
134
+
135
+ formatted = "+1 ";
136
+ for (let i = 0; i < withoutCountryCode.length && i < 10; i++) {
137
+ if (i === 0) formatted += "(";
138
+ if (i === 3) formatted += ") ";
139
+ if (i === 6) formatted += "-";
140
+ formatted += withoutCountryCode[i];
141
+ }
142
+ } else {
143
+ // No country code - strictly limit to 10 digits
144
+ const validDigits = cleaned.slice(0, 10);
145
+
146
+ for (let i = 0; i < validDigits.length; i++) {
147
+ if (i === 0) formatted += "(";
148
+ if (i === 3) formatted += ") ";
149
+ if (i === 6) formatted += "-";
150
+ formatted += validDigits[i];
151
+ }
152
+ }
153
+ } else if (country === "GB") {
154
+ // UK formatting - limit digits appropriately
155
+ if (cleaned.startsWith("44")) {
156
+ const validDigits = cleaned.slice(0, 12); // 44 + 10 digits
157
+ formatted = "+44 ";
158
+ const withoutCountryCode = validDigits.slice(2);
159
+
160
+ // Format UK number (various formats exist, this is simplified)
161
+ for (let i = 0; i < withoutCountryCode.length; i++) {
162
+ if (i === 4) formatted += " ";
163
+ formatted += withoutCountryCode[i];
164
+ }
165
+ } else {
166
+ const validDigits = cleaned.slice(0, 11);
167
+
168
+ for (let i = 0; i < validDigits.length; i++) {
169
+ if (i === 5) formatted += " ";
170
+ formatted += validDigits[i];
171
+ }
172
+ }
173
+ } else {
174
+ // Generic international formatting - respect E.164 max length
175
+ const validDigits = cleaned.slice(0, 15);
176
+
177
+ if (validDigits.length > 10) {
178
+ // Assume first 1-3 digits are country code
179
+ const possibleCountryCodeLength = validDigits.startsWith("1")
180
+ ? 1
181
+ : validDigits.startsWith("44")
182
+ ? 2
183
+ : validDigits.startsWith("86")
184
+ ? 2
185
+ : 3;
186
+ formatted = `+${validDigits.slice(
187
+ 0,
188
+ possibleCountryCodeLength
189
+ )} ${validDigits.slice(possibleCountryCodeLength)}`;
190
+ } else {
191
+ formatted = validDigits;
192
+ }
193
+ }
194
+
195
+ // Calculate cursor position
196
+ const cursorPos = formatted.length;
197
+
198
+ return { formatted, cursorPos };
199
+ };
200
+
201
+ // Validate phone number
202
+ const isValidPhoneNumber = (value: string): boolean => {
203
+ const cleaned = value.replace(/\D/g, "");
204
+ if (cleaned.length === 0) return true; // Empty is valid
205
+
206
+ const country = detectCountry(cleaned);
207
+
208
+ if (country === "US") {
209
+ // Valid US numbers: EXACTLY 10 digits or EXACTLY 11 digits starting with 1
210
+ if (cleaned.length === 10) {
211
+ return true;
212
+ }
213
+ if (cleaned.length === 11 && cleaned.startsWith("1")) {
214
+ return true;
215
+ }
216
+ return false; // Any other length is invalid
217
+ }
218
+
219
+ if (country === "GB") {
220
+ // Valid UK numbers: typically 11 digits (including country code 44)
221
+ if (cleaned.startsWith("44")) {
222
+ return cleaned.length === 12; // 44 + 10 digits
223
+ }
224
+ return cleaned.length === 11; // Without country code
225
+ }
226
+
227
+ // General international validation
228
+ return cleaned.length >= 7 && cleaned.length <= 15; // ITU-T E.164 standard
229
+ };
230
+
231
+ // Computed properties
232
+ const isValid = computed(() => isValidPhoneNumber(internalValue.value));
233
+ const currentPlaceholder = computed(() => {
234
+ const country = detectCountry(internalValue.value);
235
+ const format = countryFormats[country as keyof typeof countryFormats];
236
+ return format?.placeholder || props.placeholder;
237
+ });
238
+
239
+ // Override state to show error when phone number is invalid
240
+ const effectiveState = computed(() => {
241
+ if (!isValid.value && internalValue.value.length > 0) {
242
+ return "error";
243
+ }
244
+ return props.state;
245
+ });
246
+
247
+ // Get the appropriate text color for icons based on state
248
+ const getIconColor = () => {
249
+ return "var(--input-text-filled)";
250
+ };
251
+
252
+ // Watch for changes to value prop
253
+ watch(
254
+ () => props.value,
255
+ (newValue) => {
256
+ if (newValue !== internalValue.value) {
257
+ const { formatted } = formatPhoneNumber(newValue);
258
+ internalValue.value = formatted;
259
+ }
260
+ }
261
+ );
262
+
263
+ // Update handleBeforeInput to prevent typing when at max digits
264
+ const handleBeforeInput = (event: Event) => {
265
+ const inputEvent = event as InputEvent;
266
+ const data = inputEvent.data;
267
+
268
+ // Allow deletion operations
269
+ if (!data) return;
270
+
271
+ // Check if we're at max digits
272
+ if (currentDigitCount.value >= maxAllowedDigits.value) {
273
+ event.preventDefault();
274
+ return;
275
+ }
276
+
277
+ // Only allow digits - NO other characters
278
+ const isDigit = /^[0-9]+$/;
279
+ if (!isDigit.test(data)) {
280
+ event.preventDefault();
281
+ }
282
+ };
283
+
284
+ // Also update the handleInput function to prevent extra digits
285
+ const handleInput = (event: Event) => {
286
+ const target = event.target as HTMLInputElement;
287
+ const rawValue = target.value;
288
+ const caretPos = target.selectionStart || 0;
289
+
290
+ // Strip out any non-allowed characters
291
+ let digitsOnly = rawValue.replace(/[^\d]/g, "");
292
+
293
+ // Determine maximum allowed digits based on country
294
+ const country = detectCountry(digitsOnly);
295
+ let maxDigits = 15; // Default international max
296
+
297
+ if (country === "US") {
298
+ // For US: 10 digits without country code, 11 with country code
299
+ if (digitsOnly.startsWith("1")) {
300
+ maxDigits = 11;
301
+ } else {
302
+ maxDigits = 10;
303
+ }
304
+ } else if (country === "GB") {
305
+ // For UK: 11 without country code, 12 with country code
306
+ if (digitsOnly.startsWith("44")) {
307
+ maxDigits = 12;
308
+ } else {
309
+ maxDigits = 11;
310
+ }
311
+ }
312
+
313
+ // Enforce the digit limit
314
+ digitsOnly = digitsOnly.slice(0, maxDigits);
315
+
316
+ // Format the number
317
+ const { formatted, cursorPos } = formatPhoneNumber(digitsOnly);
318
+ internalValue.value = formatted;
319
+
320
+ // Update cursor position after formatting
321
+ cursorPosition.value = cursorPos;
322
+
323
+ // Emit events
324
+ emit("update:value", formatted);
325
+ emit("update:formatted", formatted);
326
+ emit("update:valid", isValidPhoneNumber(formatted));
327
+
328
+ // Set cursor position after Vue updates the DOM
329
+ requestAnimationFrame(() => {
330
+ if (inputRef.value && cursorPosition.value !== null) {
331
+ inputRef.value.setSelectionRange(
332
+ cursorPosition.value,
333
+ cursorPosition.value
334
+ );
335
+ }
336
+ });
337
+ };
338
+
339
+ // Handle paste
340
+ const handlePaste = (event: ClipboardEvent) => {
341
+ event.preventDefault();
342
+ const pastedText = event.clipboardData?.getData("text") || "";
343
+ let cleanedNumber = pastedText.replace(/\D/g, "");
344
+
345
+ if (cleanedNumber) {
346
+ // Determine maximum allowed digits based on country
347
+ const country = detectCountry(cleanedNumber);
348
+ let maxDigits = 15; // Default international max
349
+
350
+ if (country === "US") {
351
+ // For US: 10 digits without country code, 11 with country code
352
+ if (cleanedNumber.startsWith("1")) {
353
+ maxDigits = 11;
354
+ } else {
355
+ maxDigits = 10;
356
+ }
357
+ } else if (country === "GB") {
358
+ // For UK: 11 without country code, 12 with country code
359
+ if (cleanedNumber.startsWith("44")) {
360
+ maxDigits = 12;
361
+ } else {
362
+ maxDigits = 11;
363
+ }
364
+ }
365
+
366
+ // Enforce the digit limit BEFORE formatting
367
+ cleanedNumber = cleanedNumber.slice(0, maxDigits);
368
+
369
+ const { formatted } = formatPhoneNumber(cleanedNumber);
370
+ internalValue.value = formatted;
371
+ emit("update:value", formatted);
372
+ emit("update:formatted", formatted);
373
+ emit("update:valid", isValidPhoneNumber(formatted));
374
+ }
375
+ };
376
+
377
+ // Update handleKeyDown to also check digit limit
378
+ const handleKeyDown = (event: KeyboardEvent) => {
379
+ const target = event.target as HTMLInputElement;
380
+ const key = event.key;
381
+ const caretPos = target.selectionStart || 0;
382
+
383
+ // Block any non-digit keys except control keys
384
+ const isControlKey = [
385
+ "Backspace",
386
+ "Delete",
387
+ "Tab",
388
+ "Escape",
389
+ "Enter",
390
+ "ArrowLeft",
391
+ "ArrowRight",
392
+ "ArrowUp",
393
+ "ArrowDown",
394
+ ].includes(key);
395
+ const isModifierKey = event.ctrlKey || event.metaKey || event.altKey;
396
+
397
+ // If it's a digit and we're at max, prevent it
398
+ if (
399
+ /^[0-9]$/.test(key) &&
400
+ !isModifierKey &&
401
+ currentDigitCount.value >= maxAllowedDigits.value
402
+ ) {
403
+ event.preventDefault();
404
+ return;
405
+ }
406
+
407
+ if (!isControlKey && !isModifierKey && !/^[0-9]$/.test(key)) {
408
+ event.preventDefault();
409
+ return;
410
+ }
411
+
412
+ // Allow backspace to delete formatting characters
413
+ if (key === "Backspace" && caretPos > 0) {
414
+ const charToDelete = internalValue.value[caretPos - 1];
415
+ if (["(", ")", " ", "-"].includes(charToDelete)) {
416
+ event.preventDefault();
417
+ target.setSelectionRange(caretPos - 1, caretPos - 1);
418
+ }
419
+ }
420
+ };
421
+ const getPlaceholder = computed(() => {
422
+ if (props.state === "readonly") {
423
+ return "Field Cannot Be Edited";
424
+ }
425
+ return currentPlaceholder.value;
426
+ });
427
+ </script>
428
+
429
+ <template>
430
+ <div :class="$style.container">
431
+ <div :class="[$style.input_container, $style[effectiveState]]">
432
+ <input
433
+ ref="inputRef"
434
+ class="body"
435
+ type="tel"
436
+ :placeholder="getPlaceholder"
437
+ :value="internalValue"
438
+ @beforeinput="handleBeforeInput"
439
+ @input="handleInput"
440
+ @paste="handlePaste"
441
+ @keydown="handleKeyDown"
442
+ :maxlength="20"
443
+ :disabled="state === 'disabled'"
444
+ :readonly="state === 'readonly'"
445
+ />
446
+ <PhoneIcon v-if="!internalValue" :size="16" />
447
+ <transition name="fade">
448
+ <div
449
+ v-if="!isValid && internalValue.length > 0"
450
+ :class="$style.error_icon"
451
+ >
452
+ <TriangleWarningIcon size="16" />
453
+ </div>
454
+ </transition>
455
+ </div>
456
+ <transition name="slide-fade">
457
+ <p
458
+ v-if="!isValid && internalValue.length > 0"
459
+ :class="[$style.error_message, 'footnote']"
460
+ >
461
+ Please enter a valid phone number
462
+ </p>
463
+ </transition>
464
+ </div>
465
+ </template>
466
+
467
+ <style module>
468
+ .container {
469
+ display: flex;
470
+ flex-direction: column;
471
+ gap: 0.588rem;
472
+ }
473
+
474
+ .input_container {
475
+ width: 100%;
476
+ padding-left: 0.882rem;
477
+ padding-right: 0.882rem;
478
+ padding-top: 0.588rem;
479
+ padding-bottom: 0.588rem;
480
+ border: var(--input-border);
481
+ border-radius: var(--input-border-radius);
482
+ transition: background-color 0.3s ease, border 0.3s ease;
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: space-between;
486
+ position: relative;
487
+ }
488
+
489
+ .input_container:focus-within {
490
+ border: 1px solid var(--input-focus-border);
491
+ }
492
+
493
+ .input_container input {
494
+ border: none;
495
+ background: transparent;
496
+ outline: none;
497
+ width: 100%;
498
+ font-family: inherit;
499
+ }
500
+
501
+ /* State-based styling using CSS variables */
502
+ .input_container.normal:has(input:placeholder-shown),
503
+ .input_container.active:has(input:placeholder-shown) {
504
+ background-color: var(--input-background-normal);
505
+ color: var(--input-text-empty);
506
+ }
507
+
508
+ .input_container.normal:has(input:not(:placeholder-shown)),
509
+ .input_container.active:has(input:not(:placeholder-shown)) {
510
+ background-color: var(--input-background-filled);
511
+ color: var(--input-text-filled);
512
+ border-color: var(--input-border-filled);
513
+ }
514
+
515
+ .input_container.normal input:placeholder-shown,
516
+ .input_container.active input:placeholder-shown {
517
+ color: var(--input-text-empty);
518
+ opacity: 0.6;
519
+ }
520
+
521
+ .input_container.normal input:not(:placeholder-shown),
522
+ .input_container.active input:not(:placeholder-shown) {
523
+ color: var(--input-text-filled);
524
+ }
525
+
526
+ .input_container.disabled:has(input:placeholder-shown),
527
+ .input_container.disabled:has(input:not(:placeholder-shown)) {
528
+ background-color: var(--input-disabled-bg);
529
+ color: var(--input-disabled-text);
530
+ border-color: var(--input-disabled-border);
531
+ cursor: not-allowed;
532
+ }
533
+
534
+ .input_container.disabled input:placeholder-shown,
535
+ .input_container.disabled input:not(:placeholder-shown) {
536
+ color: var(--input-disabled-text);
537
+ cursor: not-allowed;
538
+ }
539
+
540
+ .input_container.readonly:has(input:placeholder-shown),
541
+ .input_container.readonly:has(input:not(:placeholder-shown)) {
542
+ background-color: var(--input-readonly-bg);
543
+ color: var(--input-readonly-text);
544
+ border-color: var(--input-readonly-border);
545
+ cursor: not-allowed;
546
+ }
547
+
548
+ .input_container.readonly input:placeholder-shown,
549
+ .input_container.readonly input:not(:placeholder-shown) {
550
+ color: var(--input-readonly-text);
551
+ cursor: not-allowed;
552
+ }
553
+
554
+ .input_container.error:has(input:placeholder-shown),
555
+ .input_container.error:has(input:not(:placeholder-shown)) {
556
+ background-color: var(--input-error-bg);
557
+ color: var(--input-error-text);
558
+ border-color: var(--input-error-border);
559
+ }
560
+
561
+ .input_container.error input:placeholder-shown,
562
+ .input_container.error input:not(:placeholder-shown) {
563
+ color: var(--input-error-text);
564
+ }
565
+
566
+ .input_container.active {
567
+ pointer-events: none;
568
+ }
569
+
570
+ .error_icon {
571
+ color: var(--input-text-error);
572
+ display: flex;
573
+ align-items: center;
574
+ animation: shake 0.3s ease-in-out;
575
+ }
576
+
577
+ .error_message {
578
+ color: var(--input-text-error);
579
+ }
580
+
581
+ /* Animations */
582
+ @keyframes shake {
583
+ 0%,
584
+ 100% {
585
+ transform: translateX(0);
586
+ }
587
+ 25% {
588
+ transform: translateX(-5px);
589
+ }
590
+ 75% {
591
+ transform: translateX(5px);
592
+ }
593
+ }
594
+
595
+ .fade-enter-active,
596
+ .fade-leave-active {
597
+ transition: opacity 0.3s ease;
598
+ }
599
+
600
+ .fade-enter-from,
601
+ .fade-leave-to {
602
+ opacity: 0;
603
+ }
604
+
605
+ .slide-fade-enter-active {
606
+ transition: all 0.3s ease;
607
+ }
608
+
609
+ .slide-fade-leave-active {
610
+ transition: all 0.2s ease;
611
+ }
612
+
613
+ .slide-fade-enter-from {
614
+ transform: translateY(-10px);
615
+ opacity: 0;
616
+ }
617
+
618
+ .slide-fade-leave-to {
619
+ transform: translateY(-10px);
620
+ opacity: 0;
621
+ }
622
+
623
+ .input_container input::placeholder {
624
+ color: var(--input-placeholder);
625
+ }
626
+
627
+ .input_container:has(input:not(:placeholder-shown)) input::placeholder {
628
+ color: var(--input-placeholder-filled);
629
+ }
630
+
631
+ .input_container.readonly input::placeholder {
632
+ color: var(--input-readonly-placeholder);
633
+ }
634
+
635
+ .input_container.disabled input::placeholder {
636
+ color: var(--input-disabled-placeholder);
637
+ }
638
+
639
+ /* Responsive adjustments */
640
+ @media (max-width: 480px) {
641
+ .input_container input {
642
+ font-size: 16px; /* Prevents zoom on iOS */
643
+ }
644
+ }
645
+ </style>