@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,698 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, computed } from "vue";
3
+ import "../theme.css";
4
+ import {
5
+ EnvelopeIcon,
6
+ TriangleWarningIcon,
7
+ CircleCheckIcon,
8
+ } from "@umbra-ui/icons";
9
+
10
+ export interface Props {
11
+ value?: string;
12
+ placeholder?: string;
13
+ showSuggestions?: boolean;
14
+ validateOnType?: boolean;
15
+ allowSubdomains?: boolean;
16
+ state?: "normal" | "active" | "disabled" | "readonly" | "error";
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ value: "",
21
+ placeholder: "name@example.com",
22
+ showSuggestions: true,
23
+ validateOnType: true,
24
+ allowSubdomains: true,
25
+ state: "normal",
26
+ });
27
+
28
+ const emit = defineEmits<{
29
+ "update:value": [value: string];
30
+ "update:valid": [value: boolean];
31
+ "update:suggestion": [value: string];
32
+ }>();
33
+
34
+ const internalValue = ref(props.value);
35
+ const showSuggestionsList = ref(false);
36
+ const selectedSuggestionIndex = ref(-1);
37
+ const inputRef = ref<HTMLInputElement | null>(null);
38
+ const hasInteracted = ref(false);
39
+
40
+ // Common email domains for suggestions
41
+ const commonDomains = [
42
+ "gmail.com",
43
+ "yahoo.com",
44
+ "hotmail.com",
45
+ "outlook.com",
46
+ "icloud.com",
47
+ "aol.com",
48
+ "protonmail.com",
49
+ "mail.com",
50
+ "ymail.com",
51
+ "live.com",
52
+ ];
53
+
54
+ // Common TLDs for validation
55
+ const commonTLDs = [
56
+ "com",
57
+ "org",
58
+ "net",
59
+ "edu",
60
+ "gov",
61
+ "mil",
62
+ "int",
63
+ "co",
64
+ "uk",
65
+ "de",
66
+ "fr",
67
+ "it",
68
+ "es",
69
+ "ca",
70
+ "au",
71
+ "jp",
72
+ "cn",
73
+ "in",
74
+ "io",
75
+ "dev",
76
+ "app",
77
+ "me",
78
+ "tv",
79
+ "info",
80
+ "biz",
81
+ "name",
82
+ "pro",
83
+ ];
84
+
85
+ // Updated email regex patterns with stricter domain validation
86
+ const emailRegex = props.allowSubdomains
87
+ ? /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/
88
+ : /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,}$/;
89
+
90
+ // Validate email format
91
+ // Updated email validation function
92
+ const isValidEmail = (email: string): boolean => {
93
+ if (!email) return true; // Empty is valid
94
+
95
+ // Basic format check
96
+ if (!emailRegex.test(email)) return false;
97
+
98
+ // Additional checks
99
+ const [localPart, domain] = email.split("@");
100
+
101
+ // Check local part
102
+ if (localPart.length > 64) return false;
103
+ if (localPart.startsWith(".") || localPart.endsWith(".")) return false;
104
+ if (localPart.includes("..")) return false;
105
+
106
+ // Check domain
107
+ if (domain.length > 253) return false;
108
+ if (domain.startsWith("-") || domain.endsWith("-")) return false;
109
+
110
+ // NEW: Check if domain ends with a dot
111
+ if (domain.endsWith(".")) return false;
112
+
113
+ // NEW: Check if domain starts with a dot
114
+ if (domain.startsWith(".")) return false;
115
+
116
+ // NEW: Check for consecutive dots in domain
117
+ if (domain.includes("..")) return false;
118
+
119
+ // NEW: Ensure domain has at least one dot and proper TLD
120
+ const domainParts = domain.split(".");
121
+ if (domainParts.length < 2) return false;
122
+
123
+ // Check that each domain part is valid
124
+ for (const part of domainParts) {
125
+ if (part.length === 0) return false; // Empty part (consecutive dots)
126
+ if (part.length > 63) return false; // Domain label too long
127
+ if (!/^[a-zA-Z0-9-]+$/.test(part)) return false; // Invalid characters
128
+ if (part.startsWith("-") || part.endsWith("-")) return false; // Hyphens at start/end
129
+ }
130
+
131
+ // Check TLD (last part) - should be at least 2 characters and only letters
132
+ const tld = domainParts[domainParts.length - 1];
133
+ if (tld.length < 2) return false;
134
+ if (!/^[a-zA-Z]+$/.test(tld)) return false;
135
+
136
+ return true;
137
+ };
138
+
139
+ // Check for common typos in domain
140
+ const checkForTypos = (domain: string): string | null => {
141
+ const commonTypos: Record<string, string> = {
142
+ "gmial.com": "gmail.com",
143
+ "gmai.com": "gmail.com",
144
+ "gmil.com": "gmail.com",
145
+ "gmal.com": "gmail.com",
146
+ "gamil.com": "gmail.com",
147
+ "yahooo.com": "yahoo.com",
148
+ "yaho.com": "yahoo.com",
149
+ "yahou.com": "yahoo.com",
150
+ "hotmial.com": "hotmail.com",
151
+ "hotmai.com": "hotmail.com",
152
+ "hotmil.com": "hotmail.com",
153
+ "outloook.com": "outlook.com",
154
+ "outlok.com": "outlook.com",
155
+ };
156
+
157
+ return commonTypos[domain.toLowerCase()] || null;
158
+ };
159
+
160
+ // Generate email suggestions
161
+ const generateSuggestions = computed(() => {
162
+ if (!props.showSuggestions || !internalValue.value.includes("@")) {
163
+ return [];
164
+ }
165
+
166
+ const [localPart, domainPart] = internalValue.value.split("@");
167
+ if (!localPart || !domainPart) return [];
168
+
169
+ // Check for typos first
170
+ const typoSuggestion = checkForTypos(domainPart);
171
+ if (typoSuggestion) {
172
+ return [`${localPart}@${typoSuggestion}`];
173
+ }
174
+
175
+ // Filter common domains based on input
176
+ if (domainPart.length > 0) {
177
+ return commonDomains
178
+ .filter((domain) => domain.startsWith(domainPart.toLowerCase()))
179
+ .map((domain) => `${localPart}@${domain}`)
180
+ .slice(0, 5);
181
+ }
182
+
183
+ return commonDomains.map((domain) => `${localPart}@${domain}`).slice(0, 5);
184
+ });
185
+
186
+ // Computed properties
187
+ const isValid = computed(() => isValidEmail(internalValue.value));
188
+ const hasTypo = computed(() => {
189
+ if (!internalValue.value.includes("@")) return false;
190
+ const [, domain] = internalValue.value.split("@");
191
+ return !!checkForTypos(domain);
192
+ });
193
+
194
+ const showError = computed(() => {
195
+ return (
196
+ hasInteracted.value &&
197
+ !isValid.value &&
198
+ internalValue.value.length > 0 &&
199
+ (!props.validateOnType || internalValue.value.includes("@"))
200
+ );
201
+ });
202
+
203
+ // Override state to show error when email is invalid
204
+ const effectiveState = computed(() => {
205
+ if (showError.value) {
206
+ return "error";
207
+ }
208
+ return props.state;
209
+ });
210
+
211
+ // Get the appropriate text color for icons based on state
212
+ const getIconColor = () => {
213
+ return "var(--input-text-filled)";
214
+ };
215
+
216
+ // Watch for changes to value prop
217
+ watch(
218
+ () => props.value,
219
+ (newValue) => {
220
+ if (newValue !== internalValue.value) {
221
+ internalValue.value = newValue;
222
+ }
223
+ }
224
+ );
225
+
226
+ // Handle input
227
+ const handleInput = (event: Event) => {
228
+ const target = event.target as HTMLInputElement;
229
+ let value = target.value;
230
+
231
+ // Convert to lowercase
232
+ value = value.toLowerCase();
233
+
234
+ // Remove spaces
235
+ value = value.replace(/\s/g, "");
236
+
237
+ // Prevent multiple @ symbols
238
+ const atCount = (value.match(/@/g) || []).length;
239
+ if (atCount > 1) {
240
+ // Keep only the first @
241
+ const firstAtIndex = value.indexOf("@");
242
+ const beforeAt = value.substring(0, firstAtIndex);
243
+ const afterAt = value.substring(firstAtIndex + 1).replace(/@/g, "");
244
+ value = beforeAt + "@" + afterAt;
245
+ }
246
+
247
+ internalValue.value = value;
248
+ hasInteracted.value = true;
249
+
250
+ // Show suggestions when @ is typed
251
+ if (value.includes("@") && props.showSuggestions) {
252
+ showSuggestionsList.value = true;
253
+ } else {
254
+ showSuggestionsList.value = false;
255
+ }
256
+
257
+ // Reset suggestion selection
258
+ selectedSuggestionIndex.value = -1;
259
+
260
+ // Emit events
261
+ emit("update:value", value);
262
+ emit("update:valid", isValid.value);
263
+ };
264
+
265
+ // Handle blur
266
+ const handleBlur = () => {
267
+ hasInteracted.value = true;
268
+ // Delay hiding suggestions to allow click
269
+ setTimeout(() => {
270
+ showSuggestionsList.value = false;
271
+ }, 200);
272
+ };
273
+
274
+ // Handle focus
275
+ const handleFocus = () => {
276
+ if (
277
+ internalValue.value.includes("@") &&
278
+ generateSuggestions.value.length > 0
279
+ ) {
280
+ showSuggestionsList.value = true;
281
+ }
282
+ };
283
+
284
+ // Handle keyboard navigation
285
+ const handleKeyDown = (event: KeyboardEvent) => {
286
+ if (!showSuggestionsList.value || generateSuggestions.value.length === 0) {
287
+ return;
288
+ }
289
+
290
+ switch (event.key) {
291
+ case "ArrowDown":
292
+ event.preventDefault();
293
+ selectedSuggestionIndex.value = Math.min(
294
+ selectedSuggestionIndex.value + 1,
295
+ generateSuggestions.value.length - 1
296
+ );
297
+ break;
298
+
299
+ case "ArrowUp":
300
+ event.preventDefault();
301
+ selectedSuggestionIndex.value = Math.max(
302
+ selectedSuggestionIndex.value - 1,
303
+ -1
304
+ );
305
+ break;
306
+
307
+ case "Enter":
308
+ if (selectedSuggestionIndex.value >= 0) {
309
+ event.preventDefault();
310
+ selectSuggestion(
311
+ generateSuggestions.value[selectedSuggestionIndex.value]
312
+ );
313
+ }
314
+ break;
315
+
316
+ case "Escape":
317
+ showSuggestionsList.value = false;
318
+ selectedSuggestionIndex.value = -1;
319
+ break;
320
+ }
321
+ };
322
+
323
+ // Select suggestion
324
+ const selectSuggestion = (suggestion: string) => {
325
+ internalValue.value = suggestion;
326
+ showSuggestionsList.value = false;
327
+ selectedSuggestionIndex.value = -1;
328
+
329
+ emit("update:value", suggestion);
330
+ emit("update:valid", true);
331
+ emit("update:suggestion", suggestion);
332
+
333
+ // Focus back on input
334
+ inputRef.value?.focus();
335
+ };
336
+
337
+ // Handle paste
338
+ const handlePaste = (event: ClipboardEvent) => {
339
+ event.preventDefault();
340
+ const pastedText = event.clipboardData?.getData("text") || "";
341
+
342
+ // Clean pasted email
343
+ let cleaned = pastedText.toLowerCase().trim().replace(/\s/g, "");
344
+
345
+ // Extract email from common formats like "John Doe <john@example.com>"
346
+ const emailMatch =
347
+ cleaned.match(/<([^>]+)>/) || cleaned.match(/([^\s]+@[^\s]+)/);
348
+ if (emailMatch) {
349
+ cleaned = emailMatch[1];
350
+ }
351
+
352
+ internalValue.value = cleaned;
353
+ hasInteracted.value = true;
354
+
355
+ emit("update:value", cleaned);
356
+ emit("update:valid", isValidEmail(cleaned));
357
+ };
358
+
359
+ const getPlaceholder = computed(() => {
360
+ if (props.state === "readonly") {
361
+ return "Field Cannot Be Edited";
362
+ }
363
+ return props.placeholder;
364
+ });
365
+ </script>
366
+
367
+ <template>
368
+ <div :class="$style.container">
369
+ <div :class="[$style.input_container, $style[effectiveState]]">
370
+ <input
371
+ ref="inputRef"
372
+ class="body"
373
+ type="email"
374
+ autocomplete="email"
375
+ autocapitalize="off"
376
+ autocorrect="off"
377
+ spellcheck="false"
378
+ :placeholder="getPlaceholder"
379
+ :value="internalValue"
380
+ @input="handleInput"
381
+ @blur="handleBlur"
382
+ @focus="handleFocus"
383
+ @keydown="handleKeyDown"
384
+ @paste="handlePaste"
385
+ :disabled="state === 'disabled'"
386
+ :readonly="state === 'readonly'"
387
+ />
388
+ <transition name="fade">
389
+ <div v-if="showError" :class="$style.error_icon">
390
+ <TriangleWarningIcon size="16" />
391
+ </div>
392
+ </transition>
393
+ <transition name="fade">
394
+ <div
395
+ v-if="isValid && internalValue.includes('@') && !showError"
396
+ :class="$style.valid_icon"
397
+ >
398
+ <CircleCheckIcon size="16" />
399
+ </div>
400
+ </transition>
401
+ <EnvelopeIcon v-if="!internalValue" :size="16" />
402
+ </div>
403
+
404
+ <!-- Suggestions dropdown -->
405
+ <transition name="dropdown">
406
+ <div
407
+ v-if="showSuggestionsList && generateSuggestions.length > 0"
408
+ :class="$style.suggestions"
409
+ >
410
+ <div v-if="hasTypo" :class="$style.typo_warning">Did you mean:</div>
411
+ <button
412
+ v-for="(suggestion, index) in generateSuggestions"
413
+ :key="suggestion"
414
+ :class="[
415
+ $style.suggestion,
416
+ { [$style.selected]: index === selectedSuggestionIndex },
417
+ ]"
418
+ @click="selectSuggestion(suggestion)"
419
+ @mouseenter="selectedSuggestionIndex = index"
420
+ type="button"
421
+ >
422
+ <span :class="$style.suggestion_local">{{
423
+ suggestion.split("@")[0]
424
+ }}</span>
425
+ <span :class="$style.suggestion_domain"
426
+ >@{{ suggestion.split("@")[1] }}</span
427
+ >
428
+ </button>
429
+ </div>
430
+ </transition>
431
+
432
+ <!-- Error message -->
433
+ <transition name="slide-fade">
434
+ <p v-if="showError" :class="[$style.error_message, 'footnote']">
435
+ {{
436
+ hasTypo
437
+ ? "This email might have a typo"
438
+ : "Please enter a valid email address"
439
+ }}
440
+ </p>
441
+ </transition>
442
+ </div>
443
+ </template>
444
+
445
+ <style module>
446
+ .container {
447
+ display: flex;
448
+ flex-direction: column;
449
+ gap: 0.588rem;
450
+ position: relative;
451
+ }
452
+
453
+ .input_container {
454
+ width: 100%;
455
+ padding-left: 0.882rem;
456
+ padding-right: 0.882rem;
457
+ padding-top: 0.588rem;
458
+ padding-bottom: 0.588rem;
459
+ border: var(--input-border);
460
+ border-radius: var(--input-border-radius);
461
+ transition: background-color 0.3s ease, border 0.3s ease;
462
+ display: flex;
463
+ align-items: center;
464
+ justify-content: space-between;
465
+ gap: 0.588rem;
466
+ position: relative;
467
+ }
468
+
469
+ .input_container:focus-within {
470
+ border: 1px solid var(--input-focus-border);
471
+ }
472
+
473
+ .input_container input {
474
+ border: none;
475
+ background: transparent;
476
+ outline: none;
477
+ width: 100%;
478
+ font-family: inherit;
479
+ }
480
+
481
+ /* State-based styling using CSS variables */
482
+ .input_container.normal:has(input:placeholder-shown),
483
+ .input_container.active:has(input:placeholder-shown) {
484
+ background-color: var(--input-background-normal);
485
+ color: var(--input-text-empty);
486
+ }
487
+
488
+ .input_container.normal:has(input:not(:placeholder-shown)),
489
+ .input_container.active:has(input:not(:placeholder-shown)) {
490
+ background-color: var(--input-background-filled);
491
+ color: var(--input-text-filled);
492
+ border-color: var(--input-border-filled);
493
+ }
494
+
495
+ .input_container.normal input:placeholder-shown,
496
+ .input_container.active input:placeholder-shown {
497
+ color: var(--input-text-empty);
498
+ opacity: 0.6;
499
+ }
500
+
501
+ .input_container.normal input:not(:placeholder-shown),
502
+ .input_container.active input:not(:placeholder-shown) {
503
+ color: var(--input-text-filled);
504
+ }
505
+
506
+ .input_container.disabled:has(input:placeholder-shown),
507
+ .input_container.disabled:has(input:not(:placeholder-shown)) {
508
+ background-color: var(--input-disabled-bg);
509
+ color: var(--input-disabled-text);
510
+ border-color: var(--input-disabled-border);
511
+ cursor: not-allowed;
512
+ }
513
+
514
+ .input_container.disabled input:placeholder-shown,
515
+ .input_container.disabled input:not(:placeholder-shown) {
516
+ color: var(--input-disabled-text);
517
+ cursor: not-allowed;
518
+ }
519
+
520
+ .input_container.readonly:has(input:placeholder-shown),
521
+ .input_container.readonly:has(input:not(:placeholder-shown)) {
522
+ background-color: var(--input-readonly-bg);
523
+ color: var(--input-readonly-text);
524
+ border-color: var(--input-readonly-border);
525
+ cursor: not-allowed;
526
+ }
527
+
528
+ .input_container.readonly input:placeholder-shown,
529
+ .input_container.readonly input:not(:placeholder-shown) {
530
+ color: var(--input-readonly-text);
531
+ cursor: not-allowed;
532
+ }
533
+
534
+ .input_container.error:has(input:placeholder-shown),
535
+ .input_container.error:has(input:not(:placeholder-shown)) {
536
+ background-color: var(--input-error-bg);
537
+ color: var(--input-error-text);
538
+ border-color: var(--input-error-border);
539
+ }
540
+
541
+ .input_container.error input:placeholder-shown,
542
+ .input_container.error input:not(:placeholder-shown) {
543
+ color: var(--input-error-text);
544
+ }
545
+
546
+ .input_container.active {
547
+ pointer-events: none;
548
+ }
549
+
550
+ .error_icon {
551
+ color: var(--input-text-error);
552
+ display: flex;
553
+ align-items: center;
554
+ animation: shake 0.3s ease-in-out;
555
+ }
556
+
557
+ .valid_icon {
558
+ color: var(--input-text-filled);
559
+ display: flex;
560
+ align-items: center;
561
+ }
562
+
563
+ .error_message {
564
+ color: var(--input-text-error);
565
+ }
566
+
567
+ /* Suggestions dropdown */
568
+ .suggestions {
569
+ position: absolute;
570
+ top: calc(100% + 0.25rem);
571
+ left: 0;
572
+ right: 0;
573
+ background: black;
574
+ border: 1px solid #484848;
575
+ border-radius: var(--input-border-radius);
576
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
577
+ overflow: hidden;
578
+ z-index: 1000;
579
+ }
580
+
581
+ .typo_warning {
582
+ padding: 0.5rem 0.882rem;
583
+ font-size: 0.875rem;
584
+ color: #ffe629;
585
+ background-color: rgba(255, 193, 7, 0.1);
586
+ border-bottom: 1px solid #484848;
587
+ }
588
+
589
+ .suggestion {
590
+ display: block;
591
+ width: 100%;
592
+ padding: 0.5rem 0.882rem;
593
+ text-align: left;
594
+ background: none;
595
+ border: none;
596
+ cursor: pointer;
597
+ transition: background-color 0.2s ease;
598
+ font-size: 1rem;
599
+ font-family: inherit;
600
+ color: var(--text-1);
601
+ }
602
+
603
+ .suggestion:hover,
604
+ .suggestion.selected {
605
+ background-color: #484848;
606
+ }
607
+
608
+ .suggestion_local {
609
+ color: #eeeeee;
610
+ }
611
+
612
+ .suggestion_domain {
613
+ color: #b4b4b4;
614
+ }
615
+
616
+ /* Animations */
617
+ @keyframes shake {
618
+ 0%,
619
+ 100% {
620
+ transform: translateX(0);
621
+ }
622
+ 25% {
623
+ transform: translateX(-5px);
624
+ }
625
+ 75% {
626
+ transform: translateX(5px);
627
+ }
628
+ }
629
+
630
+ .fade-enter-active,
631
+ .fade-leave-active {
632
+ transition: opacity 0.3s ease;
633
+ }
634
+
635
+ .fade-enter-from,
636
+ .fade-leave-to {
637
+ opacity: 0;
638
+ }
639
+
640
+ .slide-fade-enter-active {
641
+ transition: all 0.3s ease;
642
+ }
643
+
644
+ .slide-fade-leave-active {
645
+ transition: all 0.2s ease;
646
+ }
647
+
648
+ .slide-fade-enter-from {
649
+ transform: translateY(-10px);
650
+ opacity: 0;
651
+ }
652
+
653
+ .slide-fade-leave-to {
654
+ transform: translateY(-10px);
655
+ opacity: 0;
656
+ }
657
+
658
+ .dropdown-enter-active {
659
+ transition: all 0.2s ease;
660
+ }
661
+
662
+ .dropdown-leave-active {
663
+ transition: all 0.15s ease;
664
+ }
665
+
666
+ .dropdown-enter-from {
667
+ transform: translateY(-10px);
668
+ opacity: 0;
669
+ }
670
+
671
+ .dropdown-leave-to {
672
+ transform: translateY(-5px);
673
+ opacity: 0;
674
+ }
675
+
676
+ .input_container input::placeholder {
677
+ color: var(--input-placeholder);
678
+ }
679
+
680
+ .input_container:has(input:not(:placeholder-shown)) input::placeholder {
681
+ color: var(--input-placeholder-filled);
682
+ }
683
+
684
+ .input_container.readonly input::placeholder {
685
+ color: var(--input-readonly-placeholder);
686
+ }
687
+
688
+ .input_container.disabled input::placeholder {
689
+ color: var(--input-disabled-placeholder);
690
+ }
691
+
692
+ /* Responsive adjustments */
693
+ @media (max-width: 480px) {
694
+ .input_container input {
695
+ font-size: 16px; /* Prevents zoom on iOS */
696
+ }
697
+ }
698
+ </style>