@bagelink/vue 1.2.63 → 1.2.69

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 (49) hide show
  1. package/bin/experimentalGenTypedRoutes.ts +309 -0
  2. package/dist/components/Alert.vue.d.ts +2 -0
  3. package/dist/components/Alert.vue.d.ts.map +1 -1
  4. package/dist/components/Carousel.vue.d.ts +12 -3
  5. package/dist/components/Carousel.vue.d.ts.map +1 -1
  6. package/dist/components/Carousel2.vue.d.ts +89 -0
  7. package/dist/components/Carousel2.vue.d.ts.map +1 -0
  8. package/dist/components/form/inputs/EmailInput.vue.d.ts +48 -0
  9. package/dist/components/form/inputs/EmailInput.vue.d.ts.map +1 -0
  10. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/TelInput.vue.d.ts +60 -0
  12. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -0
  13. package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
  14. package/dist/components/form/inputs/index.d.ts +2 -1
  15. package/dist/components/form/inputs/index.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +1 -0
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/composables/useDevice.d.ts.map +1 -1
  19. package/dist/composables/useSchemaField.d.ts.map +1 -1
  20. package/dist/directives/pattern.d.ts.map +1 -1
  21. package/dist/index.cjs +11370 -10336
  22. package/dist/index.mjs +11371 -10337
  23. package/dist/style.css +1839 -1701
  24. package/dist/utils/BagelFormUtils.d.ts +7 -0
  25. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  26. package/dist/utils/constants.d.ts +1 -0
  27. package/dist/utils/constants.d.ts.map +1 -1
  28. package/dist/utils/search.d.ts +2 -2
  29. package/dist/utils/search.d.ts.map +1 -1
  30. package/package.json +7 -3
  31. package/src/components/Alert.vue +29 -7
  32. package/src/components/Carousel.vue +8 -0
  33. package/src/components/Carousel2.vue +1012 -0
  34. package/src/components/form/inputs/EmailInput.vue +476 -0
  35. package/src/components/form/inputs/SelectInput.vue +2 -2
  36. package/src/components/form/inputs/TelInput.vue +210 -350
  37. package/src/components/form/inputs/TextInput.vue +1 -1
  38. package/src/components/form/inputs/index.ts +2 -1
  39. package/src/components/index.ts +1 -0
  40. package/src/composables/useDevice.ts +1 -0
  41. package/src/composables/useSchemaField.ts +3 -1
  42. package/src/directives/pattern.ts +3 -2
  43. package/src/styles/inputs.css +137 -138
  44. package/src/styles/layout.css +1466 -1459
  45. package/src/styles/mobilLayout.css +11 -0
  46. package/src/utils/BagelFormUtils.ts +24 -0
  47. package/src/utils/constants.ts +1 -0
  48. package/src/utils/search.ts +3 -3
  49. package/src/components/form/inputs/PhoneInput.vue +0 -352
@@ -0,0 +1,476 @@
1
+ <script setup lang="ts">
2
+ import type { IconType, ValidateInputBaseT } from '@bagelink/vue'
3
+ import { Icon, useDebounceFn } from '@bagelink/vue'
4
+ import { onMounted, ref, watch, nextTick } from 'vue'
5
+ import { EMAIL_REGEX } from '../../../utils/constants'
6
+
7
+ const props = withDefaults(
8
+ defineProps<EmailInputProps>(),
9
+ {
10
+ modelValue: '',
11
+ autocorrect: true,
12
+ serverValidate: false,
13
+ preventFakeEmails: false,
14
+ pattern: '[a-z0-9._%+\\-]+@[a-z0-9.\\-]+\\.[a-z]{2,}',
15
+ },
16
+ )
17
+
18
+ const emit = defineEmits(['update:modelValue', 'debounce'])
19
+
20
+ export interface EmailInputProps extends ValidateInputBaseT {
21
+ id?: string
22
+ title?: string
23
+ helptext?: string
24
+ name?: string
25
+ placeholder?: string
26
+ modelValue?: string
27
+ label?: string
28
+ small?: boolean
29
+ required?: boolean
30
+ pattern?: string
31
+ defaultValue?: string
32
+ shrink?: boolean
33
+ disabled?: boolean
34
+ nativeInputAttrs?: { [key: string]: any }
35
+ icon?: IconType
36
+ iconStart?: IconType
37
+ autocomplete?: AutoFillField
38
+ autofocus?: boolean
39
+ onFocusout?: (e: FocusEvent) => void
40
+ onFocus?: (e: FocusEvent) => void
41
+ // Additional props
42
+ autocorrect?: boolean
43
+ serverValidate?: boolean
44
+ preventFakeEmails?: boolean
45
+ }
46
+
47
+ // Common email providers for autocorrection
48
+ const COMMON_EMAIL_DOMAINS = [
49
+ 'gmail.com',
50
+ 'yahoo.com',
51
+ 'hotmail.com',
52
+ 'outlook.com',
53
+ 'aol.com',
54
+ 'icloud.com',
55
+ 'protonmail.com',
56
+ 'mail.com',
57
+ 'zoho.com',
58
+ 'yandex.com',
59
+ 'gmx.com',
60
+ 'live.com',
61
+ 'msn.com',
62
+ ]
63
+
64
+ // List of known disposable/fake email providers
65
+ const FAKE_EMAIL_DOMAINS = [
66
+ 'mailinator.com',
67
+ 'tempmail.com',
68
+ '10minutemail.com',
69
+ 'guerrillamail.com',
70
+ 'guerrillamail.info',
71
+ 'throwawaymail.com',
72
+ 'yopmail.com',
73
+ 'tempinbox.com',
74
+ 'dispostable.com',
75
+ 'mailnesia.com',
76
+ 'trashmail.com',
77
+ 'sharklasers.com',
78
+ 'temp-mail.org',
79
+ 'fakeinbox.com',
80
+ 'getnada.com',
81
+ 'armyspy.com',
82
+ 'cuvox.de',
83
+ 'dayrep.com',
84
+ 'einrot.com',
85
+ 'fleckens.hu',
86
+ 'gustr.com',
87
+ 'jourrapide.com',
88
+ 'rhyta.com',
89
+ 'superrito.com',
90
+ 'teleworm.us',
91
+ 'nbmbb.com',
92
+ 'poplk.com'
93
+ ]
94
+
95
+ let inputVal = $ref<string>('')
96
+ const suggestedCorrection = ref<string | null>(null)
97
+ const validationMessage = ref('')
98
+ const isValidating = ref(false)
99
+ const isValidEmail = ref(true)
100
+ const validatedEmails = new Map<string, boolean>()
101
+
102
+ const input = $ref<HTMLInputElement>()
103
+
104
+ // Use custom validation function
105
+ function validateEmail(value: string) {
106
+ if (!value) return
107
+
108
+ // Basic format validation
109
+ if (!EMAIL_REGEX.test(value)) {
110
+ return 'Please enter a valid email address'
111
+ }
112
+
113
+ // Check for fake email providers if enabled
114
+ if (props.preventFakeEmails && value.includes('@')) {
115
+ const [, domain] = value.split('@')
116
+ if (domain) {
117
+ const domainLower = domain.toLowerCase()
118
+ if (FAKE_EMAIL_DOMAINS.includes(domainLower)) {
119
+ return 'Please use a non-disposable email address'
120
+ }
121
+ }
122
+ }
123
+
124
+ // Return validation message if set by server validation
125
+ if (validationMessage.value) {
126
+ return validationMessage.value
127
+ }
128
+
129
+ return undefined
130
+ }
131
+
132
+ const debouncedEmit = useDebounceFn(() => { emit('debounce', inputVal) }, 700)
133
+
134
+ // Validate input directly when value changes
135
+ function validateInput() {
136
+ if (!input) return
137
+
138
+ input.setCustomValidity('')
139
+ if (!inputVal) return
140
+ const validationResult = validateEmail(inputVal)
141
+ if (typeof validationResult === 'string') {
142
+ input.setCustomValidity(validationResult)
143
+ }
144
+ }
145
+
146
+ // Perform server validation of email
147
+ async function validateEmailWithServer(email: string) {
148
+ if (!props.serverValidate || !email || !EMAIL_REGEX.test(email)) return
149
+
150
+ // If we've already validated this email, use cached result
151
+ if (validatedEmails.has(email)) {
152
+ isValidEmail.value = validatedEmails.get(email) || false
153
+ return
154
+ }
155
+
156
+ isValidating.value = true
157
+ validationMessage.value = ''
158
+
159
+ try {
160
+ // Simple DNS MX record check - you can replace with a more sophisticated API endpoint
161
+ const response = await fetch(`https://api.mailcheck.ai/domain/${email.split('@')[1]}`, {
162
+ method: 'GET',
163
+ headers: { 'Content-Type': 'application/json' }
164
+ })
165
+
166
+ if (!response.ok) throw new Error('Validation service unavailable')
167
+
168
+ const result = await response.json()
169
+ const isValid = result.status === 'valid' || result.has_mx === true
170
+
171
+ isValidEmail.value = isValid
172
+ validatedEmails.set(email, isValid)
173
+
174
+ if (!isValid) {
175
+ validationMessage.value = 'This email domain appears to be invalid'
176
+ input?.setCustomValidity(validationMessage.value)
177
+ } else {
178
+ input?.setCustomValidity('')
179
+ }
180
+ } catch (error) {
181
+ console.error('Email validation error:', error)
182
+ } finally {
183
+ isValidating.value = false
184
+ }
185
+ }
186
+
187
+ // Check for email typos and suggest corrections
188
+ function checkForTypos(email: string) {
189
+ if (!props.autocorrect || !email) return
190
+
191
+ // Handle case where domain is incomplete (missing TLD)
192
+ if (email.includes('@') && !email.includes('.')) {
193
+ const [username, partialDomain] = email.split('@')
194
+ const partialDomainLower = partialDomain.toLowerCase()
195
+
196
+ // Check if this is a partial match for a common domain
197
+ for (const commonDomain of COMMON_EMAIL_DOMAINS) {
198
+ if (commonDomain.startsWith(partialDomainLower)) {
199
+ suggestedCorrection.value = `${username}@${commonDomain}`
200
+ return
201
+ }
202
+ }
203
+ }
204
+
205
+ // Standard typo checking for complete emails
206
+ if (!email.includes('@')) return
207
+
208
+ const [username, domain] = email.split('@')
209
+ const domainLower = domain.toLowerCase()
210
+
211
+ // Don't suggest if it's already a common domain
212
+ if (COMMON_EMAIL_DOMAINS.includes(domainLower)) {
213
+ suggestedCorrection.value = null
214
+ return
215
+ }
216
+
217
+ // Find close matches using Levenshtein distance
218
+ for (const commonDomain of COMMON_EMAIL_DOMAINS) {
219
+ if (calculateLevenshteinDistance(domainLower, commonDomain) <= 2) {
220
+ suggestedCorrection.value = `${username}@${commonDomain}`.toLowerCase()
221
+ return
222
+ }
223
+ }
224
+
225
+ suggestedCorrection.value = null
226
+ }
227
+
228
+ // Apply the suggested correction
229
+ function applyCorrection() {
230
+ if (suggestedCorrection.value) {
231
+ inputVal = suggestedCorrection.value
232
+ suggestedCorrection.value = null
233
+ emit('update:modelValue', inputVal)
234
+ debouncedEmit()
235
+ }
236
+ }
237
+
238
+ // Calculate Levenshtein distance between two strings
239
+ function calculateLevenshteinDistance(a: string, b: string): number {
240
+ const matrix: number[][] = []
241
+
242
+ // Initialize matrix
243
+ for (let i = 0; i <= a.length; i++) {
244
+ matrix[i] = [i]
245
+ }
246
+
247
+ for (let j = 0; j <= b.length; j++) {
248
+ matrix[0][j] = j
249
+ }
250
+
251
+ for (let i = 1; i <= a.length; i++) {
252
+ for (let j = 1; j <= b.length; j++) {
253
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1
254
+ matrix[i][j] = Math.min(
255
+ matrix[i - 1][j] + 1,
256
+ matrix[i][j - 1] + 1,
257
+ matrix[i - 1][j - 1] + cost
258
+ )
259
+ }
260
+ }
261
+
262
+ return matrix[a.length][b.length]
263
+ }
264
+
265
+ const debouncedServerValidate = useDebounceFn(() => validateEmailWithServer(inputVal), 1000)
266
+
267
+ function updateInputVal() {
268
+ if (props.disabled) return
269
+
270
+ // Remove typo checking while typing - only do this on focusout now
271
+ // checkForTypos(inputVal)
272
+
273
+ // Clear any previous suggestions
274
+ suggestedCorrection.value = null
275
+
276
+ validateInput()
277
+
278
+ // Debounced server validation
279
+ if (props.serverValidate) {
280
+ debouncedServerValidate()
281
+ }
282
+
283
+ emit('update:modelValue', inputVal)
284
+ debouncedEmit()
285
+ }
286
+
287
+ function handleFocusout(e: FocusEvent) {
288
+ // Check for typos when the user leaves the field
289
+ checkForTypos(inputVal)
290
+
291
+ // Immediately validate on blur
292
+ if (props.serverValidate && EMAIL_REGEX.test(inputVal)) {
293
+ validateEmailWithServer(inputVal)
294
+ }
295
+
296
+ if (props.onFocusout) props.onFocusout(e)
297
+ }
298
+
299
+ watch(
300
+ () => props.modelValue,
301
+ (newVal) => {
302
+ if (newVal !== inputVal) {
303
+ inputVal = newVal || ''
304
+ nextTick(() => { validateInput() })
305
+ }
306
+ },
307
+ { immediate: true },
308
+ )
309
+
310
+ watch(
311
+ () => inputVal,
312
+ () => { validateInput() },
313
+ { immediate: true }
314
+ )
315
+
316
+ const hasFocus = () => document.activeElement === input
317
+ const focus = () => input?.focus()
318
+ defineExpose({ focus, hasFocus })
319
+
320
+ onMounted(() => {
321
+ if (props.autofocus) setTimeout(() => input?.focus(), 10)
322
+ if (props.defaultValue && !props.modelValue) inputVal = props.defaultValue
323
+ })
324
+ </script>
325
+
326
+ <template>
327
+ <div
328
+ class="bagel-input text-input"
329
+ :class="{
330
+ small,
331
+ shrink,
332
+ 'textInputIconWrap': icon,
333
+ 'txtInputIconStart': iconStart,
334
+ 'is-validating': isValidating,
335
+ }"
336
+ :title="title"
337
+ >
338
+ <label :for="id">
339
+ <div class="flex">
340
+ {{ label }} <span v-if="required">*</span>
341
+ <span v-if="helptext" class="opacity-7 light">{{ helptext }}</span>
342
+ <span
343
+ v-if="suggestedCorrection"
344
+ class="pointer nowrap inline-block ms-auto color-red txt-10px p-0"
345
+ @click.prevent="applyCorrection"
346
+ >
347
+ did you mean {{ suggestedCorrection }}?
348
+ </span>
349
+ <span v-if="isValidating" class="validating">Validating email...</span>
350
+ </div>
351
+ <input
352
+ :id
353
+ ref="input"
354
+ v-model="inputVal"
355
+ v-pattern:lower
356
+ class="ltr"
357
+ :name
358
+ :title
359
+ autocomplete="email"
360
+ type="email"
361
+ :placeholder="placeholder || label"
362
+ :disabled
363
+ :required
364
+ v-bind="nativeInputAttrs"
365
+ @focusout="handleFocusout"
366
+ @focus="onFocus"
367
+ @input="updateInputVal"
368
+ >
369
+ <Icon
370
+ v-if="iconStart"
371
+ class="iconStart"
372
+ :icon="iconStart"
373
+ />
374
+ <Icon
375
+ v-if="icon"
376
+ :icon="icon"
377
+ />
378
+ </label>
379
+ </div>
380
+ </template>
381
+
382
+ <style>
383
+ .bagel-input.shrink,
384
+ .bagel-input.shrink input {
385
+ min-width: unset !important;
386
+ /* width: auto; */
387
+ }
388
+
389
+ .bagel-input label {
390
+ font-size: var(--label-font-size);
391
+ }
392
+ </style>
393
+
394
+ <style scoped>
395
+ .bagel-input textarea {
396
+ min-height: unset;
397
+ font-size: var(--input-font-size);
398
+ }
399
+
400
+ .bagel-input.text-input textarea {
401
+ resize: none;
402
+ }
403
+
404
+ .code textarea {
405
+ font-family: 'Inconsolata', monospace;
406
+ background: var(--bgl-code-bg) !important;
407
+ color: var(--bgl-light-text) !important;
408
+ }
409
+ .code textarea::placeholder {
410
+ color: var(--bgl-light-text) !important;
411
+ opacity: 0.3;
412
+ }
413
+
414
+ .bagel-input.small {
415
+ margin-bottom: 0;
416
+ height: 30px;
417
+ }
418
+
419
+ .bagel-input.dense label {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 0.5rem;
423
+ }
424
+
425
+ .bagel-input input:disabled {
426
+ background: #f5f5f5;
427
+ }
428
+
429
+ .bagel-input label {
430
+ font-size: var(--label-font-size);
431
+ }
432
+
433
+ .textInputIconWrap .bgl_icon-font {
434
+ color: var(--input-color);
435
+ position: absolute;
436
+ inset-inline-end:calc(var(--input-height) / 3 - 0.25rem);
437
+ margin-top: calc(var(--input-height) / 2 + 0.1rem);
438
+ line-height: 0;
439
+ }
440
+ .textInputIconWrap input{
441
+ padding-inline-end: calc(var(--input-height) / 3 + 1.5rem);
442
+ }
443
+
444
+ .txtInputIconStart .iconStart {
445
+ color: var(--input-color);
446
+ position: absolute;
447
+ inset-inline-start:calc(var(--input-height) / 3 - 0.25rem);
448
+ margin-top: calc(var(--input-height) / 2 );
449
+ line-height: 0;
450
+ }
451
+ .txtInputIconStart input, .txtInputIconStart textarea{
452
+ padding-inline-start: calc(var(--input-height) / 3 + 1.5rem);
453
+ }
454
+
455
+ .bagel-input.small textarea {
456
+ height: 30px;
457
+ }
458
+
459
+ .suggestion a {
460
+ font-weight: bold;
461
+ text-decoration: underline;
462
+ cursor: pointer;
463
+ }
464
+
465
+ .validating {
466
+ margin-top: 0.25rem;
467
+ font-size: 0.8rem;
468
+ color: var(--bgl-gray);
469
+ font-style: italic;
470
+ }
471
+
472
+ .is-validating input {
473
+ border-color: var(--bgl-primary-tint);
474
+ background-color: rgba(var(--bgl-primary-rgb), 0.05);
475
+ }
476
+ </style>
@@ -55,7 +55,7 @@ const selectedLabel = $computed((): string => {
55
55
  })
56
56
  const searchPlaceholder = $computed(() => props.searchPlaceholder ?? selectedLabel ?? 'Search')
57
57
 
58
- const { results, isLoading } = useSearch<Option>({ searchTerm: () => searchTerm, serverSearch: props.onSearch, items: props.options })
58
+ const { results, isLoading } = useSearch<Option>({ searchTerm: () => searchTerm, serverSearch: props.onSearch, items: () => props.options })
59
59
 
60
60
  let highlightedIndex = $ref(-1)
61
61
 
@@ -205,7 +205,7 @@ onMounted(() => {
205
205
  >
206
206
  <template #trigger>
207
207
  <label>
208
- {{ label }}
208
+ {{ label }} <span v-if="required">*</span>
209
209
  <div class="flex gap-05">
210
210
  <TextInput
211
211
  v-if="searchable && open"