@velkymx/vibeui 0.8.2 → 1.0.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 (200) hide show
  1. package/AGENTS.md +44 -0
  2. package/README.md +245 -58
  3. package/dist/purify.es-1D-VntFD.mjs +679 -0
  4. package/dist/src/App.vue.d.ts +3 -0
  5. package/dist/src/components/HelloWorld.vue.d.ts +6 -0
  6. package/dist/src/components/VibeAccordion.vue.d.ts +89 -0
  7. package/dist/{components → src/components}/VibeAlert.vue.d.ts +24 -30
  8. package/dist/src/components/VibeAutocomplete.vue.d.ts +74 -0
  9. package/dist/{components → src/components}/VibeBadge.vue.d.ts +10 -18
  10. package/dist/src/components/VibeBreadcrumb.vue.d.ts +51 -0
  11. package/dist/{components → src/components}/VibeButton.vue.d.ts +16 -20
  12. package/dist/{components → src/components}/VibeButtonGroup.vue.d.ts +10 -18
  13. package/dist/{components → src/components}/VibeCard.vue.d.ts +51 -24
  14. package/dist/{components → src/components}/VibeCarousel.vue.d.ts +27 -49
  15. package/dist/src/components/VibeChartBar.vue.d.ts +61 -0
  16. package/dist/src/components/VibeChartLine.vue.d.ts +70 -0
  17. package/dist/src/components/VibeChartPie.vue.d.ts +34 -0
  18. package/dist/{components → src/components}/VibeCloseButton.vue.d.ts +9 -7
  19. package/dist/{components → src/components}/VibeCol.vue.d.ts +9 -17
  20. package/dist/src/components/VibeCollapse.vue.d.ts +76 -0
  21. package/dist/src/components/VibeContainer.vue.d.ts +36 -0
  22. package/dist/src/components/VibeDataTable.vue.d.ts +131 -0
  23. package/dist/src/components/VibeDatePicker.vue.d.ts +120 -0
  24. package/dist/src/components/VibeDraggable.vue.d.ts +77 -0
  25. package/dist/{components → src/components}/VibeDropdown.vue.d.ts +46 -43
  26. package/dist/src/components/VibeDroppable.vue.d.ts +70 -0
  27. package/dist/src/components/VibeFileInput.vue.d.ts +127 -0
  28. package/dist/{components → src/components}/VibeFormCheckbox.vue.d.ts +30 -22
  29. package/dist/{components → src/components}/VibeFormDatepicker.vue.d.ts +27 -30
  30. package/dist/{components → src/components}/VibeFormGroup.vue.d.ts +11 -14
  31. package/dist/{components → src/components}/VibeFormInput.vue.d.ts +22 -23
  32. package/dist/{components → src/components}/VibeFormRadio.vue.d.ts +23 -24
  33. package/dist/{components → src/components}/VibeFormSelect.vue.d.ts +28 -34
  34. package/dist/{components → src/components}/VibeFormSpinbutton.vue.d.ts +31 -34
  35. package/dist/{components → src/components}/VibeFormSwitch.vue.d.ts +22 -23
  36. package/dist/{components → src/components}/VibeFormTextarea.vue.d.ts +24 -25
  37. package/dist/{components → src/components}/VibeFormWysiwyg.vue.d.ts +24 -23
  38. package/dist/src/components/VibeHero.vue.d.ts +108 -0
  39. package/dist/{components → src/components}/VibeIcon.vue.d.ts +27 -7
  40. package/dist/{components → src/components}/VibeInputGroup.vue.d.ts +11 -13
  41. package/dist/{components → src/components}/VibeLink.vue.d.ts +14 -18
  42. package/dist/{components → src/components}/VibeListGroup.vue.d.ts +26 -22
  43. package/dist/{components → src/components}/VibeModal.vue.d.ts +29 -45
  44. package/dist/{components → src/components}/VibeNav.vue.d.ts +40 -20
  45. package/dist/{components → src/components}/VibeNavbar.vue.d.ts +12 -19
  46. package/dist/src/components/VibeNavbarBrand.vue.d.ts +34 -0
  47. package/dist/src/components/VibeNavbarNav.vue.d.ts +80 -0
  48. package/dist/{components → src/components}/VibeNavbarToggle.vue.d.ts +7 -5
  49. package/dist/{components → src/components}/VibeOffcanvas.vue.d.ts +28 -43
  50. package/dist/{components → src/components}/VibePagination.vue.d.ts +38 -29
  51. package/dist/{components → src/components}/VibePlaceholder.vue.d.ts +9 -17
  52. package/dist/src/components/VibePopover.vue.d.ts +72 -0
  53. package/dist/src/components/VibeProgress.vue.d.ts +37 -0
  54. package/dist/src/components/VibeResizable.vue.d.ts +140 -0
  55. package/dist/{components → src/components}/VibeRow.vue.d.ts +9 -17
  56. package/dist/src/components/VibeScrollspy.vue.d.ts +89 -0
  57. package/dist/src/components/VibeSkeleton.vue.d.ts +54 -0
  58. package/dist/src/components/VibeSlider.vue.d.ts +77 -0
  59. package/dist/src/components/VibeSortable.vue.d.ts +59 -0
  60. package/dist/{components → src/components}/VibeSpinner.vue.d.ts +4 -7
  61. package/dist/src/components/VibeStepper.vue.d.ts +133 -0
  62. package/dist/src/components/VibeTab.vue.d.ts +41 -0
  63. package/dist/src/components/VibeTabContent.vue.d.ts +56 -0
  64. package/dist/src/components/VibeTabs.vue.d.ts +78 -0
  65. package/dist/{components → src/components}/VibeToast.vue.d.ts +40 -42
  66. package/dist/src/components/VibeToastHost.vue.d.ts +16 -0
  67. package/dist/src/components/VibeTooltip.vue.d.ts +63 -0
  68. package/dist/src/components/chart/chartColors.d.ts +3 -0
  69. package/dist/src/components/chart/chartResize.d.ts +2 -0
  70. package/dist/src/components/chart/chartTooltip.d.ts +2 -0
  71. package/dist/src/components/chart/chartTypes.d.ts +6 -0
  72. package/dist/src/components/chart/drawBar.d.ts +5 -0
  73. package/dist/src/components/chart/drawLine.d.ts +18 -0
  74. package/dist/src/components/chart/drawPie.d.ts +4 -0
  75. package/dist/src/components/dndStore.d.ts +7 -0
  76. package/dist/{components → src/components}/index.d.ts +18 -1
  77. package/dist/{composables → src/composables}/useBreakpoints.d.ts +1 -0
  78. package/dist/{composables → src/composables}/useColorMode.d.ts +1 -1
  79. package/dist/src/composables/useForm.d.ts +22 -0
  80. package/dist/src/composables/useId.d.ts +2 -0
  81. package/dist/src/composables/usePosition.d.ts +20 -0
  82. package/dist/src/composables/useToast.d.ts +52 -0
  83. package/dist/src/directives/vTooltip.d.ts +24 -0
  84. package/dist/{index.d.ts → src/index.d.ts} +6 -0
  85. package/dist/src/injectionKeys.d.ts +28 -0
  86. package/dist/{types.d.ts → src/types.d.ts} +32 -6
  87. package/dist/src/utils/safeCss.d.ts +16 -0
  88. package/dist/src/utils/safeHref.d.ts +7 -0
  89. package/dist/src/utils/sanitizeHtml.d.ts +13 -0
  90. package/dist/vibeui.css +2 -1
  91. package/dist/vibeui.es.js +8351 -3508
  92. package/dist/vibeui.umd.js +3 -1
  93. package/docs/README.md +85 -13
  94. package/docs/components/advanced/popover.md +27 -52
  95. package/docs/components/advanced/scrollspy.md +57 -15
  96. package/docs/components/advanced/tooltip.md +26 -47
  97. package/docs/components/card/card.md +9 -1
  98. package/docs/components/charts/chart-bar.md +129 -0
  99. package/docs/components/charts/chart-line.md +136 -0
  100. package/docs/components/charts/chart-pie.md +102 -0
  101. package/docs/components/core/alert.md +27 -5
  102. package/docs/components/core/badge.md +0 -6
  103. package/docs/components/core/button-group.md +0 -6
  104. package/docs/components/core/button.md +1 -1
  105. package/docs/components/core/close-button.md +5 -5
  106. package/docs/components/core/link.md +60 -17
  107. package/docs/components/core/placeholder.md +0 -6
  108. package/docs/components/core/skeleton.md +40 -0
  109. package/docs/components/core/spinner.md +0 -6
  110. package/docs/components/data/datatable.md +29 -10
  111. package/docs/components/hero/hero.md +64 -0
  112. package/docs/components/interactive/accordion.md +29 -0
  113. package/docs/components/interactive/carousel.md +19 -4
  114. package/docs/components/interactive/collapse.md +3 -2
  115. package/docs/components/interactive/date-picker.md +78 -0
  116. package/docs/components/interactive/draggable.md +91 -0
  117. package/docs/components/interactive/dropdown.md +54 -18
  118. package/docs/components/interactive/modal.md +15 -20
  119. package/docs/components/interactive/offcanvas.md +4 -1
  120. package/docs/components/interactive/resizable.md +73 -0
  121. package/docs/components/interactive/slider.md +57 -0
  122. package/docs/components/interactive/sortable.md +52 -0
  123. package/docs/components/interactive/stepper.md +83 -0
  124. package/docs/components/interactive/tabs.md +66 -0
  125. package/docs/components/interactive/toast.md +93 -30
  126. package/docs/components/layout/col.md +0 -6
  127. package/docs/components/layout/container.md +0 -6
  128. package/docs/components/layout/row.md +0 -6
  129. package/docs/components/list/list-group.md +4 -0
  130. package/docs/components/navigation/breadcrumb.md +4 -0
  131. package/docs/components/navigation/nav.md +64 -1
  132. package/docs/components/navigation/navbar.md +2 -0
  133. package/docs/components/navigation/pagination.md +16 -0
  134. package/docs/components/progress/progress.md +7 -1
  135. package/docs/composables/color-mode.md +44 -6
  136. package/docs/composables/use-form.md +78 -0
  137. package/docs/composables/use-position.md +68 -0
  138. package/docs/composables/use-toast.md +91 -0
  139. package/docs/directives/v-tooltip.md +58 -0
  140. package/docs/forms/README.md +60 -61
  141. package/docs/forms/autocomplete.md +96 -0
  142. package/docs/forms/file-input.md +97 -0
  143. package/docs/forms/form-checkbox.md +75 -25
  144. package/docs/forms/form-datepicker.md +58 -28
  145. package/docs/forms/form-group.md +79 -44
  146. package/docs/forms/form-input.md +88 -31
  147. package/docs/forms/form-radio.md +60 -36
  148. package/docs/forms/form-select.md +75 -31
  149. package/docs/forms/form-spinbutton.md +72 -33
  150. package/docs/forms/form-switch.md +53 -25
  151. package/docs/forms/form-textarea.md +60 -29
  152. package/docs/forms/form-wysiwyg.md +73 -38
  153. package/docs/forms/input-group.md +53 -34
  154. package/docs/forms/validation.md +135 -521
  155. package/docs/getting-started/starter-template.md +136 -0
  156. package/docs/superpowers/plans/2026-05-12-vibe-charts.md +1999 -0
  157. package/docs/superpowers/plans/2026-05-29-e2e-browser-testing.md +615 -0
  158. package/docs/superpowers/specs/2026-05-12-charting-design.md +185 -0
  159. package/docs/superpowers/specs/2026-05-29-e2e-browser-testing-design.md +168 -0
  160. package/docs/utilities/position.md +82 -0
  161. package/docs/versioning.md +57 -0
  162. package/examples/README.md +201 -0
  163. package/examples/album.html +262 -0
  164. package/examples/blog.html +166 -0
  165. package/examples/carousel.html +114 -0
  166. package/examples/checkout.html +275 -0
  167. package/examples/cover.html +89 -0
  168. package/examples/dashboard.html +156 -0
  169. package/examples/index.html +129 -0
  170. package/examples/jumbotron.html +56 -0
  171. package/examples/mobile-dashboard.html +138 -0
  172. package/examples/pricing.html +172 -0
  173. package/examples/product.html +145 -0
  174. package/examples/sidebars.html +166 -0
  175. package/examples/sign-in.html +119 -0
  176. package/examples/starter.html +314 -0
  177. package/examples/sticky-footer-navbar.html +70 -0
  178. package/examples/sticky-footer.html +60 -0
  179. package/examples/test-simple.html +91 -0
  180. package/llms.txt +407 -47
  181. package/package.json +42 -13
  182. package/dist/App.vue.d.ts +0 -2
  183. package/dist/components/HelloWorld.vue.d.ts +0 -5
  184. package/dist/components/VibeAccordion.vue.d.ts +0 -90
  185. package/dist/components/VibeBreadcrumb.vue.d.ts +0 -47
  186. package/dist/components/VibeCollapse.vue.d.ts +0 -84
  187. package/dist/components/VibeContainer.vue.d.ts +0 -44
  188. package/dist/components/VibeDataTable.vue.d.ts +0 -76
  189. package/dist/components/VibeNavbarBrand.vue.d.ts +0 -42
  190. package/dist/components/VibeNavbarNav.vue.d.ts +0 -61
  191. package/dist/components/VibePopover.vue.d.ts +0 -95
  192. package/dist/components/VibeProgress.vue.d.ts +0 -45
  193. package/dist/components/VibeScrollspy.vue.d.ts +0 -77
  194. package/dist/components/VibeTabContent.vue.d.ts +0 -74
  195. package/dist/components/VibeTooltip.vue.d.ts +0 -86
  196. package/dist/composables/useId.d.ts +0 -5
  197. package/dist/types/index.d.ts +0 -6
  198. /package/dist/{composables → src/composables}/useBackButton.d.ts +0 -0
  199. /package/dist/{composables → src/composables}/useFormValidation.d.ts +0 -0
  200. /package/dist/{main.d.ts → src/main.d.ts} +0 -0
@@ -1,599 +1,213 @@
1
1
  # Form Validation
2
2
 
3
- VibeUI provides a comprehensive validation system for all form components with support for both synchronous and asynchronous validation.
3
+ VibeUI ships a validation system for all form components, supporting synchronous and asynchronous rules. Use it with the `useFormValidation` composable (single field), the `useForm` composable (whole form), or by passing rule results directly.
4
4
 
5
5
  ## Built-in Validators
6
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">
7
+ ```ts
25
8
  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
9
  ```
80
10
 
81
- #### min()
11
+ Each validator returns a `ValidationRule`. A validator function returns `true` for valid, `false` to use the rule's `message`, or a non-empty `string` as the error message. Most validators skip empty values — pair them with `required()` to make a field mandatory.
82
12
 
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.
13
+ | Validator | Signature | Description |
14
+ |-----------|-----------|-------------|
15
+ | `required` | `(message?)` | Non-empty value (also checks array length). |
16
+ | `email` | `(message?)` | Valid email format. |
17
+ | `minLength` | `(min, message?)` | String length ≥ `min`. |
18
+ | `maxLength` | `(max, message?)` | String length ≤ `max`. |
19
+ | `min` | `(min, message?)` | Numeric value ≥ `min`. |
20
+ | `max` | `(max, message?)` | Numeric value ≤ `max`. |
21
+ | `pattern` | `(regex, message?)` | Matches the regex. |
22
+ | `url` | `(message?)` | Parses as a valid URL. |
23
+ | `async` | `(fn)` | Wraps an async validator `(value) => Promise<boolean \| string>`. |
120
24
 
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">
25
+ ```ts
140
26
  const rules = [
141
- validators.url('Please enter a valid URL')
27
+ validators.required('Email is required'),
28
+ validators.email()
142
29
  ]
143
- </script>
144
- ```
145
-
146
- #### async()
147
-
148
- ```typescript
149
- validators.async(validatorFn: (value: any) => Promise<boolean | string>)
150
30
  ```
151
31
 
152
- Creates a custom async validator for API validation.
32
+ ## Custom Rules
153
33
 
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
- })
34
+ A rule is `{ validator, message? }`. The validator may be sync or async.
161
35
 
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"'
36
+ ```ts
37
+ const passwordStrength = {
38
+ validator: (value: unknown) => {
39
+ const v = String(value ?? '')
40
+ if (!v) return true
41
+ if (!/[A-Z]/.test(v)) return 'Needs an uppercase letter'
42
+ if (!/\d/.test(v)) return 'Needs a number'
43
+ return true
44
+ }
180
45
  }
181
46
 
182
- const rules = [customValidator]
183
- </script>
47
+ const rules = [validators.required(), validators.minLength(8), passwordStrength]
184
48
  ```
185
49
 
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'
50
+ ## Single-field validation: `useFormValidation`
203
51
 
204
- return true
205
- },
206
- message: 'Password does not meet requirements'
207
- }
52
+ ```ts
53
+ import { useFormValidation } from '@velkymx/vibeui'
208
54
 
209
- const rules = [
210
- validators.required(),
211
- validators.minLength(8),
212
- passwordStrengthValidator
213
- ]
214
- </script>
55
+ const field = useFormValidation('') // initial value
215
56
  ```
216
57
 
217
- ## Using the Validation Composable
58
+ Returns:
218
59
 
219
- ### Basic Usage
60
+ | Member | Type | Description |
61
+ |--------|------|-------------|
62
+ | `value` | `Ref` | The field value (bind with `v-model="field.value"`). |
63
+ | `validationState` | `Ref<'valid' \| 'invalid' \| null>` | Current state. |
64
+ | `validationMessage` | `Ref<string>` | Current message. |
65
+ | `isDirty` | `Ref<boolean>` | Set via `markAsDirty()`. |
66
+ | `isTouched` | `Ref<boolean>` | Set via `markAsTouched()`. |
67
+ | `isValidating` | `Ref<boolean>` | True while an async rule is running. |
68
+ | `isValid` | `ComputedRef<boolean>` | `validationState === 'valid'`. |
69
+ | `isInvalid` | `ComputedRef<boolean>` | `validationState === 'invalid'`. |
70
+ | `validate(rules)` | `(rules) => Promise<FormValidationResult>` | Runs rules; concurrency-safe (only the latest call wins). |
71
+ | `reset()` | `() => void` | Reset to initial value and clear state. |
72
+ | `markAsTouched()` / `markAsDirty()` | `() => void` | Manual flag setters. |
220
73
 
221
74
  ```vue
222
75
  <script setup lang="ts">
223
76
  import { useFormValidation, validators } from '@velkymx/vibeui'
224
77
 
225
78
  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
- }
79
+ const validateEmail = () =>
80
+ email.validate([validators.required(), validators.email()])
233
81
  </script>
234
82
 
235
83
  <template>
236
- <VibeFormInput
237
- v-model="email.value"
238
- id="email"
239
- type="email"
84
+ <VibeFormGroup
240
85
  label="Email"
241
86
  :validation-state="email.validationState"
242
87
  :validation-message="email.validationMessage"
243
- @validate="validateEmail"
244
- />
88
+ >
89
+ <VibeFormInput v-model="email.value" type="email" @validate="validateEmail" />
90
+ </VibeFormGroup>
245
91
  </template>
246
92
  ```
247
93
 
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
94
+ Editing an invalid field clears its `invalid` state automatically until you re-validate.
271
95
 
272
- ### Validate on Blur
96
+ ## Whole-form validation: `useForm`
273
97
 
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
98
+ ```ts
99
+ import { useForm } from '@velkymx/vibeui'
286
100
 
287
- ```vue
288
- <template>
289
- <VibeFormInput
290
- v-model="password"
291
- id="password"
292
- @validate="validatePassword"
293
- validate-on="change"
294
- />
295
- </template>
101
+ const form = useForm({ username: '', email: '', age: 0 })
296
102
  ```
297
103
 
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
- ```
104
+ Returns:
310
105
 
311
- ### Manual Validation
106
+ | Member | Type | Description |
107
+ |--------|------|-------------|
108
+ | `fields` | reactive `T` | Form values; bind with `v-model="form.fields.x"`. |
109
+ | `errors` | reactive map | Per-field error messages (`''` when valid). |
110
+ | `touched` | reactive map | Per-field touched flags. |
111
+ | `isDirty` | `ComputedRef<boolean>` | True once any field differs from its initial value. |
112
+ | `isValid` | `ComputedRef<boolean>` | **`false` until the first `validate()`/`validateField()` call**, then reflects whether all errors are empty. |
113
+ | `values` | `ComputedRef<T>` | Snapshot copy of `fields`. |
114
+ | `validate(rules)` | `(rules) => Promise<{ valid, errors }>` | Validate the listed fields. |
115
+ | `validateField(key, rule)` | `(key, rule) => Promise<string>` | Validate one field; returns its message. |
116
+ | `reset()` | `() => void` | Restore initial values and clear errors/touched/validated state. |
117
+ | `markTouched(key)` / `markAllTouched()` | — | Touched flag setters. |
118
+ | `setField(key, value)` | — | Programmatically set a field. |
312
119
 
313
120
  ```vue
314
121
  <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
- }
122
+ import { useForm, validators } from '@velkymx/vibeui'
334
123
 
335
- const handleSubmit = async () => {
336
- await validateEmail()
124
+ const form = useForm({ username: '', email: '', password: '' })
337
125
 
338
- if (validationState.value === 'valid') {
339
- // Submit form
340
- }
126
+ const rules = {
127
+ username: [validators.required(), validators.minLength(3)],
128
+ email: [validators.required(), validators.email()],
129
+ password: [validators.required(), validators.minLength(8)]
341
130
  }
342
- </script>
343
- ```
344
-
345
- ## API Validation
346
-
347
- ### Check Username Availability
348
131
 
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!'
132
+ const onSubmit = async () => {
133
+ const { valid } = await form.validate(rules)
134
+ if (valid) console.log('submit', form.values.value)
403
135
  }
404
136
  </script>
405
137
 
406
138
  <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>
139
+ <form @submit.prevent="onSubmit">
140
+ <VibeFormGroup label="Username" :validation-message="form.errors.username"
141
+ :validation-state="form.errors.username ? 'invalid' : null">
142
+ <VibeFormInput v-model="form.fields.username"
143
+ @validate="form.validateField('username', rules.username)" />
144
+ </VibeFormGroup>
145
+
146
+ <VibeFormGroup label="Email" :validation-message="form.errors.email"
147
+ :validation-state="form.errors.email ? 'invalid' : null">
148
+ <VibeFormInput v-model="form.fields.email" type="email"
149
+ @validate="form.validateField('email', rules.email)" />
150
+ </VibeFormGroup>
151
+
152
+ <VibeFormGroup label="Password" :validation-message="form.errors.password"
153
+ :validation-state="form.errors.password ? 'invalid' : null">
154
+ <VibeFormInput v-model="form.fields.password" type="password"
155
+ @validate="form.validateField('password', rules.password)" />
156
+ </VibeFormGroup>
157
+
158
+ <VibeButton type="submit" variant="primary" :disabled="!form.isValid">
159
+ Register
160
+ </VibeButton>
161
+ </form>
421
162
  </template>
422
163
  ```
423
164
 
424
- ### Debounced API Validation
425
-
426
- ```vue
427
- <script setup lang="ts">
428
- import { ref } from 'vue'
429
- import { validators } from '@velkymx/vibeui'
165
+ ## When validation runs: `validateOn`
430
166
 
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
- }
167
+ Form controls emit `validate` based on `validateOn`:
441
168
 
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
- }
169
+ - Text-like controls (input, textarea, datepicker, spinbutton, wysiwyg): `'input' \| 'blur' \| 'change'`, default `'blur'`.
170
+ - Choice controls (checkbox, radio, switch, select): `'change' \| 'blur'`, default `'change'`.
469
171
 
470
- validationState.value = 'valid'
471
- validationMessage.value = 'Email is available!'
472
- }
473
- </script>
172
+ Wire `@validate` to your validation call:
474
173
 
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>
174
+ ```vue
175
+ <VibeFormInput v-model="field.value" validate-on="input" @validate="run" />
487
176
  ```
488
177
 
489
- ## Form-Level Validation
178
+ ## Async / API Validation
179
+
180
+ `validators.async` runs an async check (e.g. uniqueness). Because `useFormValidation.validate` is concurrency-safe, only the latest call applies its result.
490
181
 
491
182
  ```vue
492
183
  <script setup lang="ts">
493
- import { ref, reactive } from 'vue'
494
- import { validators } from '@velkymx/vibeui'
184
+ import { useFormValidation, validators } from '@velkymx/vibeui'
495
185
 
496
- const form = reactive({
497
- username: '',
498
- email: '',
499
- password: '',
500
- confirmPassword: ''
501
- })
186
+ const username = useFormValidation('')
502
187
 
503
- const validation = reactive({
504
- username: { state: null, message: '' },
505
- email: { state: null, message: '' },
506
- password: { state: null, message: '' },
507
- confirmPassword: { state: null, message: '' }
188
+ const isAvailable = validators.async(async (value) => {
189
+ const res = await fetch(`/api/check-username?u=${encodeURIComponent(String(value))}`)
190
+ const data = await res.json()
191
+ return data.available || 'Username is already taken'
508
192
  })
509
193
 
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
- }
194
+ const validateUsername = () =>
195
+ username.validate([validators.required(), validators.minLength(3), isAvailable])
550
196
  </script>
551
197
 
552
198
  <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>
199
+ <VibeFormGroup
200
+ label="Username"
201
+ :validation-state="username.validationState"
202
+ :validation-message="username.validationMessage"
203
+ >
204
+ <VibeFormInput v-model="username.value" @validate="validateUsername" />
205
+ </VibeFormGroup>
598
206
  </template>
599
207
  ```
208
+
209
+ ## Notes
210
+
211
+ - **`useForm.isValid` starts `false`** and only becomes meaningful after the first validation runs — safe to use for disabling a submit button before any input.
212
+ - **Editing clears `invalid`** (in `useFormValidation`) so messages don't linger while the user fixes the field.
213
+ - **Empty-value skipping:** most validators pass on empty values; add `required()` to enforce presence.