@velkymx/vibeui 0.8.1 → 0.8.2

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 (52) hide show
  1. package/CLAUDE.md +48 -0
  2. package/dist/vibeui.css +1 -1
  3. package/dist/vibeui.es.js +149 -148
  4. package/dist/vibeui.umd.js +1 -1
  5. package/docs/README.md +153 -0
  6. package/docs/components/advanced/popover.md +150 -0
  7. package/docs/components/advanced/scrollspy.md +64 -0
  8. package/docs/components/advanced/tooltip.md +111 -0
  9. package/docs/components/card/card.md +215 -0
  10. package/docs/components/core/alert.md +72 -0
  11. package/docs/components/core/badge.md +81 -0
  12. package/docs/components/core/button-group.md +105 -0
  13. package/docs/components/core/button.md +127 -0
  14. package/docs/components/core/close-button.md +82 -0
  15. package/docs/components/core/link.md +36 -0
  16. package/docs/components/core/placeholder.md +135 -0
  17. package/docs/components/core/spinner.md +109 -0
  18. package/docs/components/data/datatable.md +416 -0
  19. package/docs/components/interactive/accordion.md +92 -0
  20. package/docs/components/interactive/carousel.md +97 -0
  21. package/docs/components/interactive/collapse.md +105 -0
  22. package/docs/components/interactive/dropdown.md +93 -0
  23. package/docs/components/interactive/modal.md +148 -0
  24. package/docs/components/interactive/offcanvas.md +89 -0
  25. package/docs/components/interactive/toast.md +114 -0
  26. package/docs/components/layout/col.md +123 -0
  27. package/docs/components/layout/container.md +59 -0
  28. package/docs/components/layout/row.md +113 -0
  29. package/docs/components/list/list-group.md +221 -0
  30. package/docs/components/navigation/breadcrumb.md +116 -0
  31. package/docs/components/navigation/nav.md +88 -0
  32. package/docs/components/navigation/navbar.md +106 -0
  33. package/docs/components/navigation/pagination.md +146 -0
  34. package/docs/components/progress/progress.md +182 -0
  35. package/docs/composables/back-button.md +28 -0
  36. package/docs/composables/breakpoints.md +54 -0
  37. package/docs/composables/color-mode.md +141 -0
  38. package/docs/forms/README.md +88 -0
  39. package/docs/forms/form-checkbox.md +50 -0
  40. package/docs/forms/form-datepicker.md +50 -0
  41. package/docs/forms/form-group.md +80 -0
  42. package/docs/forms/form-input.md +55 -0
  43. package/docs/forms/form-radio.md +58 -0
  44. package/docs/forms/form-select.md +54 -0
  45. package/docs/forms/form-spinbutton.md +55 -0
  46. package/docs/forms/form-switch.md +47 -0
  47. package/docs/forms/form-textarea.md +51 -0
  48. package/docs/forms/form-wysiwyg.md +64 -0
  49. package/docs/forms/input-group.md +51 -0
  50. package/docs/forms/validation.md +599 -0
  51. package/llms.txt +422 -0
  52. package/package.json +5 -2
@@ -0,0 +1,599 @@
1
+ # Form Validation
2
+
3
+ VibeUI provides a comprehensive validation system for all form components with support for both synchronous and asynchronous validation.
4
+
5
+ ## Built-in Validators
6
+
7
+ ### Import
8
+
9
+ ```typescript
10
+ import { validators } from '@velkymx/vibeui'
11
+ ```
12
+
13
+ ### Available Validators
14
+
15
+ #### required()
16
+
17
+ ```typescript
18
+ validators.required(message?: string)
19
+ ```
20
+
21
+ Ensures the field has a value.
22
+
23
+ ```vue
24
+ <script setup lang="ts">
25
+ import { validators } from '@velkymx/vibeui'
26
+
27
+ const rules = [
28
+ validators.required('This field is required')
29
+ ]
30
+ </script>
31
+ ```
32
+
33
+ #### email()
34
+
35
+ ```typescript
36
+ validators.email(message?: string)
37
+ ```
38
+
39
+ Validates email format.
40
+
41
+ ```vue
42
+ <script setup lang="ts">
43
+ const rules = [
44
+ validators.email('Please enter a valid email address')
45
+ ]
46
+ </script>
47
+ ```
48
+
49
+ #### minLength()
50
+
51
+ ```typescript
52
+ validators.minLength(min: number, message?: string)
53
+ ```
54
+
55
+ Validates minimum string length.
56
+
57
+ ```vue
58
+ <script setup lang="ts">
59
+ const rules = [
60
+ validators.minLength(8, 'Password must be at least 8 characters')
61
+ ]
62
+ </script>
63
+ ```
64
+
65
+ #### maxLength()
66
+
67
+ ```typescript
68
+ validators.maxLength(max: number, message?: string)
69
+ ```
70
+
71
+ Validates maximum string length.
72
+
73
+ ```vue
74
+ <script setup lang="ts">
75
+ const rules = [
76
+ validators.maxLength(50, 'Username must not exceed 50 characters')
77
+ ]
78
+ </script>
79
+ ```
80
+
81
+ #### min()
82
+
83
+ ```typescript
84
+ validators.min(min: number, message?: string)
85
+ ```
86
+
87
+ Validates minimum numeric value.
88
+
89
+ ```vue
90
+ <script setup lang="ts">
91
+ const rules = [
92
+ validators.min(18, 'You must be at least 18 years old')
93
+ ]
94
+ </script>
95
+ ```
96
+
97
+ #### max()
98
+
99
+ ```typescript
100
+ validators.max(max: number, message?: string)
101
+ ```
102
+
103
+ Validates maximum numeric value.
104
+
105
+ ```vue
106
+ <script setup lang="ts">
107
+ const rules = [
108
+ validators.max(120, 'Please enter a valid age')
109
+ ]
110
+ </script>
111
+ ```
112
+
113
+ #### pattern()
114
+
115
+ ```typescript
116
+ validators.pattern(regex: RegExp, message?: string)
117
+ ```
118
+
119
+ Validates against a custom regex pattern.
120
+
121
+ ```vue
122
+ <script setup lang="ts">
123
+ const rules = [
124
+ validators.pattern(/^\d{5}$/, 'ZIP code must be 5 digits'),
125
+ validators.pattern(/^[A-Z]/, 'Must start with uppercase letter')
126
+ ]
127
+ </script>
128
+ ```
129
+
130
+ #### url()
131
+
132
+ ```typescript
133
+ validators.url(message?: string)
134
+ ```
135
+
136
+ Validates URL format.
137
+
138
+ ```vue
139
+ <script setup lang="ts">
140
+ const rules = [
141
+ validators.url('Please enter a valid URL')
142
+ ]
143
+ </script>
144
+ ```
145
+
146
+ #### async()
147
+
148
+ ```typescript
149
+ validators.async(validatorFn: (value: any) => Promise<boolean | string>)
150
+ ```
151
+
152
+ Creates a custom async validator for API validation.
153
+
154
+ ```vue
155
+ <script setup lang="ts">
156
+ const checkUsername = validators.async(async (value) => {
157
+ const response = await fetch(`/api/check-username?username=${value}`)
158
+ const data = await response.json()
159
+ return data.available || 'Username is already taken'
160
+ })
161
+
162
+ const rules = [
163
+ validators.required(),
164
+ checkUsername
165
+ ]
166
+ </script>
167
+ ```
168
+
169
+ ## Custom Validators
170
+
171
+ ### Simple Custom Validator
172
+
173
+ ```vue
174
+ <script setup lang="ts">
175
+ const customValidator = {
176
+ validator: (value) => {
177
+ return value === 'correct' || 'Value must be "correct"'
178
+ },
179
+ message: 'Value must be "correct"'
180
+ }
181
+
182
+ const rules = [customValidator]
183
+ </script>
184
+ ```
185
+
186
+ ### Complex Custom Validator
187
+
188
+ ```vue
189
+ <script setup lang="ts">
190
+ const passwordStrengthValidator = {
191
+ validator: (value) => {
192
+ if (!value) return true
193
+
194
+ const hasUpperCase = /[A-Z]/.test(value)
195
+ const hasLowerCase = /[a-z]/.test(value)
196
+ const hasNumber = /\d/.test(value)
197
+ const hasSpecialChar = /[!@#$%^&*]/.test(value)
198
+
199
+ if (!hasUpperCase) return 'Password must contain an uppercase letter'
200
+ if (!hasLowerCase) return 'Password must contain a lowercase letter'
201
+ if (!hasNumber) return 'Password must contain a number'
202
+ if (!hasSpecialChar) return 'Password must contain a special character'
203
+
204
+ return true
205
+ },
206
+ message: 'Password does not meet requirements'
207
+ }
208
+
209
+ const rules = [
210
+ validators.required(),
211
+ validators.minLength(8),
212
+ passwordStrengthValidator
213
+ ]
214
+ </script>
215
+ ```
216
+
217
+ ## Using the Validation Composable
218
+
219
+ ### Basic Usage
220
+
221
+ ```vue
222
+ <script setup lang="ts">
223
+ import { useFormValidation, validators } from '@velkymx/vibeui'
224
+
225
+ const email = useFormValidation('')
226
+
227
+ const validateEmail = () => {
228
+ email.validate([
229
+ validators.required('Email is required'),
230
+ validators.email('Please enter a valid email')
231
+ ])
232
+ }
233
+ </script>
234
+
235
+ <template>
236
+ <VibeFormInput
237
+ v-model="email.value"
238
+ id="email"
239
+ type="email"
240
+ label="Email"
241
+ :validation-state="email.validationState"
242
+ :validation-message="email.validationMessage"
243
+ @validate="validateEmail"
244
+ />
245
+ </template>
246
+ ```
247
+
248
+ ### Composable API
249
+
250
+ ```typescript
251
+ const field = useFormValidation(initialValue)
252
+
253
+ // Properties
254
+ field.value // The field value
255
+ field.validationState // 'valid' | 'invalid' | null
256
+ field.validationMessage // Validation message
257
+ field.isDirty // Has the value changed
258
+ field.isTouched // Has the field been focused
259
+ field.isValidating // Is validation in progress
260
+ field.isValid // Computed: validationState === 'valid'
261
+ field.isInvalid // Computed: validationState === 'invalid'
262
+
263
+ // Methods
264
+ await field.validate(rules) // Run validation
265
+ field.reset() // Reset to initial value
266
+ field.markAsTouched() // Mark as touched
267
+ field.markAsDirty() // Mark as dirty
268
+ ```
269
+
270
+ ## Validation Patterns
271
+
272
+ ### Validate on Blur
273
+
274
+ ```vue
275
+ <template>
276
+ <VibeFormInput
277
+ v-model="email"
278
+ id="email"
279
+ @validate="validateEmail"
280
+ validate-on="blur"
281
+ />
282
+ </template>
283
+ ```
284
+
285
+ ### Validate on Change
286
+
287
+ ```vue
288
+ <template>
289
+ <VibeFormInput
290
+ v-model="password"
291
+ id="password"
292
+ @validate="validatePassword"
293
+ validate-on="change"
294
+ />
295
+ </template>
296
+ ```
297
+
298
+ ### Validate on Input
299
+
300
+ ```vue
301
+ <template>
302
+ <VibeFormInput
303
+ v-model="username"
304
+ id="username"
305
+ @validate="validateUsername"
306
+ validate-on="input"
307
+ />
308
+ </template>
309
+ ```
310
+
311
+ ### Manual Validation
312
+
313
+ ```vue
314
+ <script setup lang="ts">
315
+ const email = ref('')
316
+ const validationState = ref(null)
317
+ const validationMessage = ref('')
318
+
319
+ const validateEmail = async () => {
320
+ const rules = [validators.required(), validators.email()]
321
+
322
+ for (const rule of rules) {
323
+ const result = await rule.validator(email.value)
324
+ if (result !== true) {
325
+ validationState.value = 'invalid'
326
+ validationMessage.value = typeof result === 'string' ? result : rule.message
327
+ return
328
+ }
329
+ }
330
+
331
+ validationState.value = 'valid'
332
+ validationMessage.value = ''
333
+ }
334
+
335
+ const handleSubmit = async () => {
336
+ await validateEmail()
337
+
338
+ if (validationState.value === 'valid') {
339
+ // Submit form
340
+ }
341
+ }
342
+ </script>
343
+ ```
344
+
345
+ ## API Validation
346
+
347
+ ### Check Username Availability
348
+
349
+ ```vue
350
+ <script setup lang="ts">
351
+ import { ref } from 'vue'
352
+ import { validators } from '@velkymx/vibeui'
353
+
354
+ const username = ref('')
355
+ const validationState = ref(null)
356
+ const validationMessage = ref('')
357
+ const isChecking = ref(false)
358
+
359
+ const checkUsernameAvailability = validators.async(async (value) => {
360
+ isChecking.value = true
361
+
362
+ try {
363
+ const response = await fetch(`/api/users/check-username`, {
364
+ method: 'POST',
365
+ headers: { 'Content-Type': 'application/json' },
366
+ body: JSON.stringify({ username: value })
367
+ })
368
+
369
+ const data = await response.json()
370
+
371
+ if (!data.available) {
372
+ return data.message || 'Username is already taken'
373
+ }
374
+
375
+ return true
376
+ } catch (error) {
377
+ return 'Error checking username availability'
378
+ } finally {
379
+ isChecking.value = false
380
+ }
381
+ })
382
+
383
+ const validateUsername = async () => {
384
+ const rules = [
385
+ validators.required('Username is required'),
386
+ validators.minLength(3, 'Username must be at least 3 characters'),
387
+ validators.maxLength(20, 'Username must not exceed 20 characters'),
388
+ validators.pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
389
+ checkUsernameAvailability
390
+ ]
391
+
392
+ for (const rule of rules) {
393
+ const result = await rule.validator(username.value)
394
+ if (result !== true) {
395
+ validationState.value = 'invalid'
396
+ validationMessage.value = typeof result === 'string' ? result : rule.message
397
+ return
398
+ }
399
+ }
400
+
401
+ validationState.value = 'valid'
402
+ validationMessage.value = 'Username is available!'
403
+ }
404
+ </script>
405
+
406
+ <template>
407
+ <VibeFormInput
408
+ v-model="username"
409
+ id="username"
410
+ label="Username"
411
+ :validation-state="validationState"
412
+ :validation-message="validationMessage"
413
+ @validate="validateUsername"
414
+ validate-on="blur"
415
+ :disabled="isChecking"
416
+ >
417
+ <template v-if="isChecking">
418
+ <VibeSpinner size="sm" class="position-absolute end-0 top-50 translate-middle-y me-3" />
419
+ </template>
420
+ </VibeFormInput>
421
+ </template>
422
+ ```
423
+
424
+ ### Debounced API Validation
425
+
426
+ ```vue
427
+ <script setup lang="ts">
428
+ import { ref } from 'vue'
429
+ import { validators } from '@velkymx/vibeui'
430
+
431
+ // Debounce helper
432
+ function debounce(fn, delay) {
433
+ let timeout
434
+ return (...args) => {
435
+ clearTimeout(timeout)
436
+ return new Promise((resolve) => {
437
+ timeout = setTimeout(() => resolve(fn(...args)), delay)
438
+ })
439
+ }
440
+ }
441
+
442
+ const email = ref('')
443
+ const validationState = ref(null)
444
+ const validationMessage = ref('')
445
+
446
+ const checkEmailDebounced = debounce(async (value) => {
447
+ const response = await fetch(`/api/check-email?email=${value}`)
448
+ const data = await response.json()
449
+ return data.available || 'Email is already registered'
450
+ }, 500)
451
+
452
+ const checkEmail = validators.async(checkEmailDebounced)
453
+
454
+ const validateEmail = async () => {
455
+ const rules = [
456
+ validators.required(),
457
+ validators.email(),
458
+ checkEmail
459
+ ]
460
+
461
+ for (const rule of rules) {
462
+ const result = await rule.validator(email.value)
463
+ if (result !== true) {
464
+ validationState.value = 'invalid'
465
+ validationMessage.value = typeof result === 'string' ? result : rule.message
466
+ return
467
+ }
468
+ }
469
+
470
+ validationState.value = 'valid'
471
+ validationMessage.value = 'Email is available!'
472
+ }
473
+ </script>
474
+
475
+ <template>
476
+ <VibeFormInput
477
+ v-model="email"
478
+ id="email"
479
+ type="email"
480
+ label="Email"
481
+ :validation-state="validationState"
482
+ :validation-message="validationMessage"
483
+ @validate="validateEmail"
484
+ validate-on="input"
485
+ />
486
+ </template>
487
+ ```
488
+
489
+ ## Form-Level Validation
490
+
491
+ ```vue
492
+ <script setup lang="ts">
493
+ import { ref, reactive } from 'vue'
494
+ import { validators } from '@velkymx/vibeui'
495
+
496
+ const form = reactive({
497
+ username: '',
498
+ email: '',
499
+ password: '',
500
+ confirmPassword: ''
501
+ })
502
+
503
+ const validation = reactive({
504
+ username: { state: null, message: '' },
505
+ email: { state: null, message: '' },
506
+ password: { state: null, message: '' },
507
+ confirmPassword: { state: null, message: '' }
508
+ })
509
+
510
+ const isFormValid = ref(false)
511
+
512
+ const validateField = async (field, rules) => {
513
+ for (const rule of rules) {
514
+ const result = await rule.validator(form[field])
515
+ if (result !== true) {
516
+ validation[field].state = 'invalid'
517
+ validation[field].message = typeof result === 'string' ? result : rule.message
518
+ return false
519
+ }
520
+ }
521
+
522
+ validation[field].state = 'valid'
523
+ validation[field].message = ''
524
+ return true
525
+ }
526
+
527
+ const validateForm = async () => {
528
+ const results = await Promise.all([
529
+ validateField('username', [validators.required(), validators.minLength(3)]),
530
+ validateField('email', [validators.required(), validators.email()]),
531
+ validateField('password', [validators.required(), validators.minLength(8)]),
532
+ validateField('confirmPassword', [
533
+ validators.required(),
534
+ {
535
+ validator: (value) => value === form.password || 'Passwords do not match',
536
+ message: 'Passwords do not match'
537
+ }
538
+ ])
539
+ ])
540
+
541
+ isFormValid.value = results.every(result => result === true)
542
+ return isFormValid.value
543
+ }
544
+
545
+ const handleSubmit = async () => {
546
+ if (await validateForm()) {
547
+ console.log('Form is valid, submitting...', form)
548
+ }
549
+ }
550
+ </script>
551
+
552
+ <template>
553
+ <form @submit.prevent="handleSubmit">
554
+ <VibeFormInput
555
+ v-model="form.username"
556
+ id="username"
557
+ label="Username"
558
+ :validation-state="validation.username.state"
559
+ :validation-message="validation.username.message"
560
+ @validate="validateField('username', [validators.required(), validators.minLength(3)])"
561
+ />
562
+
563
+ <VibeFormInput
564
+ v-model="form.email"
565
+ id="email"
566
+ type="email"
567
+ label="Email"
568
+ :validation-state="validation.email.state"
569
+ :validation-message="validation.email.message"
570
+ @validate="validateField('email', [validators.required(), validators.email()])"
571
+ />
572
+
573
+ <VibeFormInput
574
+ v-model="form.password"
575
+ id="password"
576
+ type="password"
577
+ label="Password"
578
+ :validation-state="validation.password.state"
579
+ :validation-message="validation.password.message"
580
+ @validate="validateField('password', [validators.required(), validators.minLength(8)])"
581
+ />
582
+
583
+ <VibeFormInput
584
+ v-model="form.confirmPassword"
585
+ id="confirmPassword"
586
+ type="password"
587
+ label="Confirm Password"
588
+ :validation-state="validation.confirmPassword.state"
589
+ :validation-message="validation.confirmPassword.message"
590
+ @validate="validateField('confirmPassword', [
591
+ validators.required(),
592
+ { validator: (v) => v === form.password || 'Passwords do not match', message: 'Passwords do not match' }
593
+ ])"
594
+ />
595
+
596
+ <VibeButton type="submit" variant="primary">Register</VibeButton>
597
+ </form>
598
+ </template>
599
+ ```