@umbra.ui/core 0.1.18 → 0.1.20

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 (147) hide show
  1. package/dist/components/controls/Button/Button.vue +417 -0
  2. package/dist/components/controls/Button/README.md +348 -0
  3. package/dist/components/controls/Button/theme.css +200 -0
  4. package/dist/components/controls/Checkbox/Checkbox.vue +164 -0
  5. package/dist/components/controls/Checkbox/README.md +441 -0
  6. package/dist/components/controls/Checkbox/theme.css +36 -0
  7. package/dist/components/controls/Dropdown/Dropdown.vue +476 -0
  8. package/dist/components/controls/Dropdown/README.md +370 -0
  9. package/dist/components/controls/Dropdown/theme.css +50 -0
  10. package/dist/components/controls/Dropdown/types.ts +6 -0
  11. package/dist/components/controls/IconButton/IconButton.vue +267 -0
  12. package/dist/components/controls/IconButton/README.md +502 -0
  13. package/dist/components/controls/IconButton/theme.css +89 -0
  14. package/dist/components/controls/Radio/README.md +591 -0
  15. package/dist/components/controls/Radio/Radio.vue +89 -0
  16. package/dist/components/controls/Radio/theme.css +14 -0
  17. package/dist/components/controls/RangeSlider/README.md +608 -0
  18. package/dist/components/controls/RangeSlider/RangeSlider.vue +535 -0
  19. package/dist/components/controls/RangeSlider/theme.css +80 -0
  20. package/dist/components/controls/SegmentedControl/README.md +587 -0
  21. package/dist/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
  22. package/dist/components/controls/SegmentedControl/theme.css +60 -0
  23. package/dist/components/controls/SegmentedControl/types.ts +5 -0
  24. package/dist/components/controls/Slider/README.md +627 -0
  25. package/dist/components/controls/Slider/Slider.vue +260 -0
  26. package/dist/components/controls/Slider/theme.css +74 -0
  27. package/dist/components/controls/Stepper/README.md +601 -0
  28. package/dist/components/controls/Stepper/Stepper.vue +103 -0
  29. package/dist/components/controls/Stepper/theme.css +53 -0
  30. package/dist/components/controls/Switch/README.md +667 -0
  31. package/dist/components/controls/Switch/Switch.vue +127 -0
  32. package/dist/components/controls/Switch/theme.css +42 -0
  33. package/dist/components/dialogs/Alert/Alert.vue +218 -0
  34. package/dist/components/dialogs/Alert/README.md +450 -0
  35. package/dist/components/dialogs/Alert/theme.css +44 -0
  36. package/dist/components/dialogs/Alert/types.ts +11 -0
  37. package/dist/components/dialogs/Toast/README.md +522 -0
  38. package/dist/components/dialogs/Toast/Toast.vue +296 -0
  39. package/dist/components/dialogs/Toast/ToastContainer.vue +330 -0
  40. package/dist/components/dialogs/Toast/theme.css +44 -0
  41. package/dist/components/dialogs/Toast/types.ts +46 -0
  42. package/dist/components/dialogs/Toast/useToast.ts +127 -0
  43. package/dist/components/indicators/ProgressBar/ProgressBar.vue +98 -0
  44. package/dist/components/indicators/ProgressBar/README.md +744 -0
  45. package/dist/components/indicators/ProgressBar/theme.css +36 -0
  46. package/dist/components/indicators/Tooltip/README.md +723 -0
  47. package/dist/components/indicators/Tooltip/TooltipProvider.vue +142 -0
  48. package/dist/components/indicators/Tooltip/theme.css +18 -0
  49. package/dist/components/indicators/Tooltip/tooltip.ts +48 -0
  50. package/dist/components/indicators/Tooltip/types.ts +15 -0
  51. package/dist/components/indicators/Tooltip/useTooltip.ts +71 -0
  52. package/dist/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
  53. package/dist/components/inputs/AutogrowTextView/README.md +643 -0
  54. package/dist/components/inputs/AutogrowTextView/theme.css +28 -0
  55. package/dist/components/inputs/InputCard/InputCard.vue +600 -0
  56. package/dist/components/inputs/InputCard/README.md +636 -0
  57. package/dist/components/inputs/InputEmail/InputEmail.vue +698 -0
  58. package/dist/components/inputs/InputEmail/README.md +764 -0
  59. package/dist/components/inputs/InputNumber/InputNumber.vue +300 -0
  60. package/dist/components/inputs/InputNumber/README.md +749 -0
  61. package/dist/components/inputs/InputPhone/InputPhone.vue +645 -0
  62. package/dist/components/inputs/InputPhone/README.md +636 -0
  63. package/dist/components/inputs/InputSecure/InputSecure.vue +646 -0
  64. package/dist/components/inputs/InputSecure/README.md +771 -0
  65. package/dist/components/inputs/InputText/InputText.vue +225 -0
  66. package/dist/components/inputs/InputText/README.md +844 -0
  67. package/dist/components/inputs/OTP/OTP.vue +349 -0
  68. package/dist/components/inputs/OTP/README.md +736 -0
  69. package/dist/components/inputs/OTP/theme.css +50 -0
  70. package/dist/components/inputs/StringCapture/README.md +718 -0
  71. package/dist/components/inputs/StringCapture/StringCapture.vue +315 -0
  72. package/dist/components/inputs/StringCapture/theme.css +86 -0
  73. package/dist/components/inputs/Tags/README.md +897 -0
  74. package/dist/components/inputs/Tags/TagBar.vue +793 -0
  75. package/dist/components/inputs/Tags/TagCreation.vue +219 -0
  76. package/dist/components/inputs/Tags/TagPicker.vue +380 -0
  77. package/dist/components/inputs/Tags/tag-bar-styles.ts +354 -0
  78. package/dist/components/inputs/Tags/theme.css +121 -0
  79. package/dist/components/inputs/Tags/types.ts +346 -0
  80. package/dist/components/inputs/search/README.md +759 -0
  81. package/dist/components/inputs/search/SearchBar.vue +394 -0
  82. package/dist/components/inputs/search/SearchResults.vue +310 -0
  83. package/dist/components/inputs/search/theme.css +187 -0
  84. package/dist/components/inputs/search/types.ts +8 -0
  85. package/dist/components/inputs/theme.css +102 -0
  86. package/dist/components/menus/ActionMenu/ActionMenu.vue +383 -0
  87. package/dist/components/menus/ActionMenu/README.md +825 -0
  88. package/dist/components/menus/ActionMenu/theme.css +93 -0
  89. package/dist/components/models/Popover/Popover.vue +551 -0
  90. package/dist/components/models/Popover/README.md +885 -0
  91. package/dist/components/models/Popover/theme.css +52 -0
  92. package/dist/components/models/Sheet/README.md +1159 -0
  93. package/dist/components/models/Sheet/Sheet.vue +465 -0
  94. package/dist/components/models/Sheet/theme.css +72 -0
  95. package/dist/components/models/Sidebar/README.md +1228 -0
  96. package/dist/components/models/Sidebar/Sidebar.vue +480 -0
  97. package/dist/components/models/Sidebar/theme.css +90 -0
  98. package/dist/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
  99. package/dist/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
  100. package/dist/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
  101. package/dist/components/navigation/adaptive/README.md +768 -0
  102. package/dist/components/navigation/adaptive/types.ts +19 -0
  103. package/dist/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
  104. package/dist/components/navigation/adaptive/useBreakpoints.ts +41 -0
  105. package/dist/components/navigation/adaptive/useContainerMonitor.ts +214 -0
  106. package/dist/components/navigation/adaptive/useViewAnimation.ts +721 -0
  107. package/dist/components/navigation/adaptive/useViewResize.ts +211 -0
  108. package/dist/components/navigation/navstack/NavigationStack.vue +180 -0
  109. package/dist/components/navigation/navstack/README.md +994 -0
  110. package/dist/components/navigation/navstack/useNavigationStack.ts +164 -0
  111. package/dist/components/navigation/slideover/README.md +1275 -0
  112. package/dist/components/navigation/slideover/SlideoverController.vue +287 -0
  113. package/dist/components/navigation/slideover/useSlideoverController.ts +320 -0
  114. package/dist/components/navigation/splitview/README.md +1115 -0
  115. package/dist/components/navigation/splitview/SplitViewController.vue +176 -0
  116. package/dist/components/navigation/splitview/useSplitViewController.ts +388 -0
  117. package/dist/components/navigation/tabcontroller/README.md +919 -0
  118. package/dist/components/navigation/tabcontroller/TabController.vue +307 -0
  119. package/dist/components/navigation/tabcontroller/TabItem.vue +57 -0
  120. package/dist/components/navigation/tabcontroller/types.ts +24 -0
  121. package/dist/components/navigation/tabcontroller/useTabController.ts +18 -0
  122. package/dist/components/navigation/theme.css +91 -0
  123. package/dist/components/navigation/types.ts +7 -0
  124. package/dist/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
  125. package/dist/components/pickers/CollectionPicker/README.md +1115 -0
  126. package/dist/components/pickers/CollectionPicker/theme.css +14 -0
  127. package/dist/components/pickers/CollectionPicker/types.ts +11 -0
  128. package/dist/components/pickers/ColorPicker/ColorPicker.vue +376 -0
  129. package/dist/components/pickers/ColorPicker/README.md +1439 -0
  130. package/dist/components/pickers/ColorPicker/colors.ts +299 -0
  131. package/dist/components/pickers/ColorPicker/theme.css +32 -0
  132. package/dist/components/pickers/DatePicker/DatePicker.vue +660 -0
  133. package/dist/components/pickers/DatePicker/README.md +1195 -0
  134. package/dist/components/pickers/DatePicker/theme.css +22 -0
  135. package/dist/components/pickers/FilePicker/FilePicker.vue +534 -0
  136. package/dist/components/pickers/FilePicker/README.md +1542 -0
  137. package/dist/components/pickers/FilePicker/theme.css +48 -0
  138. package/dist/components/pickers/FilePicker/types.ts +10 -0
  139. package/dist/components/pickers/IconPicker/IconPicker.vue +327 -0
  140. package/dist/components/pickers/IconPicker/README.md +1161 -0
  141. package/dist/components/pickers/IconPicker/theme.css +28 -0
  142. package/dist/components/pickers/theme.css +82 -0
  143. package/dist/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
  144. package/dist/components/views/MarkdownViewer/README.md +833 -0
  145. package/dist/components/views/MarkdownViewer/theme.css +130 -0
  146. package/dist/css/umbra-ui.css +42 -0
  147. package/package.json +6 -3
@@ -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>