@maz-ui/mcp 5.0.0-beta.5 → 5.0.0-beta.51

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 (164) hide show
  1. package/README.md +1 -1
  2. package/dist/mcp.d.mts +22 -9
  3. package/dist/mcp.d.ts +22 -9
  4. package/dist/mcp.mjs +160 -83
  5. package/docs/generated-docs/maz-avatar.doc.md +30 -30
  6. package/docs/generated-docs/maz-bottom-sheet.doc.md +16 -13
  7. package/docs/generated-docs/maz-btn.doc.md +21 -19
  8. package/docs/generated-docs/maz-checkbox.doc.md +16 -17
  9. package/docs/generated-docs/maz-circular-progress-bar.doc.md +1 -1
  10. package/docs/generated-docs/maz-code-highlight.doc.md +11 -0
  11. package/docs/generated-docs/maz-container.doc.md +1 -0
  12. package/docs/generated-docs/maz-date-picker.doc.md +41 -41
  13. package/docs/generated-docs/maz-drawer.doc.md +7 -8
  14. package/docs/generated-docs/maz-dropdown.doc.md +3 -0
  15. package/docs/generated-docs/maz-expand-animation.doc.md +4 -4
  16. package/docs/generated-docs/maz-fullscreen-loader.doc.md +5 -5
  17. package/docs/generated-docs/maz-gallery.doc.md +15 -15
  18. package/docs/generated-docs/maz-input-code.doc.md +16 -16
  19. package/docs/generated-docs/maz-input-phone-number.doc.md +43 -42
  20. package/docs/generated-docs/maz-input-price.doc.md +14 -14
  21. package/docs/generated-docs/maz-input-tags.doc.md +16 -16
  22. package/docs/generated-docs/maz-input.doc.md +33 -33
  23. package/docs/generated-docs/maz-lazy-img.doc.md +14 -14
  24. package/docs/generated-docs/maz-loading-bar.doc.md +4 -4
  25. package/docs/generated-docs/maz-markdown-editor.doc.md +65 -0
  26. package/docs/generated-docs/maz-popover.doc.md +1 -1
  27. package/docs/generated-docs/maz-pull-to-refresh.doc.md +39 -20
  28. package/docs/generated-docs/maz-radio-buttons.doc.md +17 -17
  29. package/docs/generated-docs/maz-radio.doc.md +16 -16
  30. package/docs/generated-docs/maz-reading-progress-bar.doc.md +4 -4
  31. package/docs/generated-docs/maz-select-country.doc.md +27 -26
  32. package/docs/generated-docs/maz-sidebar-content.doc.md +5 -0
  33. package/docs/generated-docs/maz-sidebar-footer.doc.md +5 -0
  34. package/docs/generated-docs/maz-sidebar-group.doc.md +11 -0
  35. package/docs/generated-docs/maz-sidebar-header.doc.md +5 -0
  36. package/docs/generated-docs/maz-sidebar-menu-button.doc.md +27 -0
  37. package/docs/generated-docs/maz-sidebar-menu-item.doc.md +5 -0
  38. package/docs/generated-docs/maz-sidebar-menu-sub.doc.md +16 -0
  39. package/docs/generated-docs/maz-sidebar-menu.doc.md +5 -0
  40. package/docs/generated-docs/maz-sidebar-separator.doc.md +0 -0
  41. package/docs/generated-docs/maz-sidebar-trigger.doc.md +5 -0
  42. package/docs/generated-docs/maz-sidebar.doc.md +36 -0
  43. package/docs/generated-docs/maz-slider.doc.md +1 -1
  44. package/docs/generated-docs/maz-spinner.doc.md +4 -4
  45. package/docs/generated-docs/maz-switch.doc.md +14 -14
  46. package/docs/generated-docs/maz-table-virtualizer.doc.md +20 -0
  47. package/docs/generated-docs/maz-table.doc.md +43 -37
  48. package/docs/generated-docs/maz-tabs-bar.doc.md +20 -9
  49. package/docs/generated-docs/maz-tabs.doc.md +6 -3
  50. package/docs/generated-docs/maz-textarea.doc.md +27 -25
  51. package/docs/generated-docs/maz-ticker.doc.md +1 -1
  52. package/docs/generated-docs/maz-timeline.doc.md +4 -4
  53. package/docs/generated-docs/maz-window-mockup.doc.md +23 -0
  54. package/docs/src/blog/v4.md +1 -1
  55. package/docs/src/blog/v5.md +5 -7
  56. package/docs/src/components/maz-avatar.md +52 -4
  57. package/docs/src/components/maz-bottom-sheet.md +300 -169
  58. package/docs/src/components/maz-btn.md +1 -1
  59. package/docs/src/components/maz-checklist.md +10 -10
  60. package/docs/src/components/maz-code-highlight.md +233 -0
  61. package/docs/src/components/maz-date-picker.md +55 -1
  62. package/docs/src/components/maz-dialog.md +46 -0
  63. package/docs/src/components/maz-dropdown.md +32 -16
  64. package/docs/src/components/maz-icon.md +16 -5
  65. package/docs/src/components/maz-input-phone-number.md +30 -0
  66. package/docs/src/components/maz-lazy-img.md +64 -0
  67. package/docs/src/components/maz-markdown-editor.md +369 -0
  68. package/docs/src/components/maz-pull-to-refresh.md +176 -17
  69. package/docs/src/components/maz-select-country.md +138 -1
  70. package/docs/src/components/maz-sidebar.md +719 -0
  71. package/docs/src/components/maz-table.md +156 -1
  72. package/docs/src/components/maz-tabs.md +110 -3
  73. package/docs/src/components/maz-textarea.md +58 -0
  74. package/docs/src/components/maz-timeline.md +60 -0
  75. package/docs/src/components/maz-window-mockup.md +249 -0
  76. package/docs/src/composables/use-drag.md +239 -0
  77. package/docs/src/composables/use-form-validator.md +297 -16
  78. package/docs/src/composables/use-pull-to-refresh.md +79 -0
  79. package/docs/src/composables/use-swipe.md +26 -11
  80. package/docs/src/directives/click-outside.md +8 -15
  81. package/docs/src/directives/fullscreen-img.md +1 -1
  82. package/docs/src/directives/lazy-img.md +44 -11
  83. package/docs/src/directives/tooltip.md +24 -1
  84. package/docs/src/directives/zoom-img.md +1 -1
  85. package/docs/src/ecosystem/eslint-config.md +95 -1
  86. package/docs/src/{guide/icons.md → ecosystem/icons/index.md} +1 -1
  87. package/docs/src/ecosystem/node/exec-promise.md +87 -0
  88. package/docs/src/ecosystem/node/index.md +53 -0
  89. package/docs/src/ecosystem/node/logger.md +146 -0
  90. package/docs/src/ecosystem/node/print-banner.md +93 -0
  91. package/docs/src/{guide → ecosystem}/nuxt.md +102 -47
  92. package/docs/src/{guide → ecosystem}/themes.md +152 -71
  93. package/docs/src/{guide → ecosystem}/translations.md +1 -1
  94. package/docs/src/ecosystem/utils/camel-case.md +31 -0
  95. package/docs/src/{helpers → ecosystem/utils}/capitalize.md +2 -3
  96. package/docs/src/ecosystem/utils/check-availability.md +79 -0
  97. package/docs/src/ecosystem/utils/cookie.md +80 -0
  98. package/docs/src/ecosystem/utils/debounce-callback.md +38 -0
  99. package/docs/src/ecosystem/utils/debounce-id.md +69 -0
  100. package/docs/src/ecosystem/utils/debounce.md +65 -0
  101. package/docs/src/ecosystem/utils/fetch-locale-ip.md +33 -0
  102. package/docs/src/ecosystem/utils/format-json.md +33 -0
  103. package/docs/src/ecosystem/utils/format-phone-number.md +37 -0
  104. package/docs/src/ecosystem/utils/get-browser-locale.md +29 -0
  105. package/docs/src/ecosystem/utils/get-error-message.md +39 -0
  106. package/docs/src/ecosystem/utils/idle-timeout.md +90 -0
  107. package/docs/src/ecosystem/utils/index.md +60 -0
  108. package/docs/src/ecosystem/utils/is-client.md +32 -0
  109. package/docs/src/ecosystem/utils/is-equal.md +38 -0
  110. package/docs/src/ecosystem/utils/is-server.md +31 -0
  111. package/docs/src/ecosystem/utils/is-standalone-mode.md +43 -0
  112. package/docs/src/ecosystem/utils/kebab-case.md +36 -0
  113. package/docs/src/ecosystem/utils/normalize-string.md +77 -0
  114. package/docs/src/ecosystem/utils/pascal-case.md +35 -0
  115. package/docs/src/ecosystem/utils/script-loader.md +77 -0
  116. package/docs/src/ecosystem/utils/sleep.md +59 -0
  117. package/docs/src/ecosystem/utils/snake-case.md +36 -0
  118. package/docs/src/ecosystem/utils/swipe-handler.md +91 -0
  119. package/docs/src/ecosystem/utils/textarea-autogrow.md +41 -0
  120. package/docs/src/ecosystem/utils/throttle-id.md +48 -0
  121. package/docs/src/ecosystem/utils/throttle.md +57 -0
  122. package/docs/src/ecosystem/utils/truthy-filter.md +31 -0
  123. package/docs/src/ecosystem/utils/types/deep-key-of.md +48 -0
  124. package/docs/src/ecosystem/utils/types/deep-partial.md +42 -0
  125. package/docs/src/ecosystem/utils/types/deep-required.md +39 -0
  126. package/docs/src/ecosystem/utils/types/flatten-object-keys.md +44 -0
  127. package/docs/src/ecosystem/utils/types/generic-instance-type.md +42 -0
  128. package/docs/src/ecosystem/utils/types/infer-maybe-ref.md +35 -0
  129. package/docs/src/ecosystem/utils/upper-first.md +32 -0
  130. package/docs/src/ecosystem/utils/user-visibility.md +69 -0
  131. package/docs/src/guide/getting-started.md +15 -13
  132. package/docs/src/guide/global-defaults.md +102 -0
  133. package/docs/src/guide/maz-ui-provider.md +6 -3
  134. package/docs/src/guide/migration-v4.md +13 -9
  135. package/docs/src/guide/migration-v5.md +211 -15
  136. package/docs/src/guide/resolvers.md +8 -8
  137. package/docs/src/guide/tailwind.md +4 -0
  138. package/docs/src/guide/vue.md +4 -4
  139. package/docs/src/index.md +13 -13
  140. package/docs/src/plugins/aos.md +1 -1
  141. package/package.json +11 -10
  142. package/docs/src/helpers/camel-case.md +0 -14
  143. package/docs/src/helpers/check-availability.md +0 -14
  144. package/docs/src/helpers/debounce-callback.md +0 -14
  145. package/docs/src/helpers/debounce-id.md +0 -14
  146. package/docs/src/helpers/debounce.md +0 -14
  147. package/docs/src/helpers/is-client.md +0 -14
  148. package/docs/src/helpers/is-equal.md +0 -14
  149. package/docs/src/helpers/is-standalone-mode.md +0 -14
  150. package/docs/src/helpers/kebab-case.md +0 -14
  151. package/docs/src/helpers/normalize-string.md +0 -14
  152. package/docs/src/helpers/pascal-case.md +0 -14
  153. package/docs/src/helpers/script-loader.md +0 -14
  154. package/docs/src/helpers/sleep.md +0 -14
  155. package/docs/src/helpers/snake-case.md +0 -14
  156. package/docs/src/helpers/throttle-id.md +0 -14
  157. package/docs/src/helpers/throttle.md +0 -14
  158. /package/docs/src/{guide → ecosystem/icons}/icon-set.md +0 -0
  159. /package/docs/src/{guide → ecosystem}/mcp.md +0 -0
  160. /package/docs/src/{helpers → ecosystem/utils}/country-code-to-unicode-flag.md +0 -0
  161. /package/docs/src/{helpers → ecosystem/utils}/currency.md +0 -0
  162. /package/docs/src/{helpers → ecosystem/utils}/date.md +0 -0
  163. /package/docs/src/{helpers → ecosystem/utils}/get-country-flag-url.md +0 -0
  164. /package/docs/src/{helpers → ecosystem/utils}/number.md +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: useFormValidator
3
- description: Vue composables for form validation with Valibot - useFormValidator and useFormField provide a flexible and typed approach to handle form validation in your Vue applications.
3
+ description: Vue composables for form validation with any Standard Schema library (Valibot, Zod, ArkType...) - useFormValidator and useFormField provide a flexible and typed approach to handle form validation in your Vue applications.
4
4
  ---
5
5
 
6
6
  # {{ $frontmatter.title }}
@@ -9,7 +9,7 @@ description: Vue composables for form validation with Valibot - useFormValidator
9
9
 
10
10
  ## Introduction
11
11
 
12
- `useFormValidator` and `useFormField` are two Vue composables that work together to provide powerful form validation using [Valibot](https://valibot.dev/guides/introduction/).
12
+ `useFormValidator` and `useFormField` are two Vue composables that work together to provide powerful form validation. They are **validation library agnostic**: any library implementing [Standard Schema](https://standardschema.dev) works out of the box, like [Valibot](https://valibot.dev), [Zod](https://zod.dev) or [ArkType](https://arktype.io). See [Validation Libraries](#validation-libraries).
13
13
 
14
14
  - **useFormValidator**: Initializes form validation for your entire form. Use it in your form's parent component.
15
15
  - **useFormField**: Manages individual field validation states. Use it when you need fine-grained control over a field or when fields are in child components.
@@ -23,6 +23,24 @@ description: Vue composables for form validation with Valibot - useFormValidator
23
23
 
24
24
  ## Quick Start
25
25
 
26
+ Install the validation library of your choice (Valibot is used in most examples of this page):
27
+
28
+ ::: code-group
29
+
30
+ ```bash [valibot]
31
+ pnpm add valibot
32
+ ```
33
+
34
+ ```bash [zod]
35
+ pnpm add zod
36
+ ```
37
+
38
+ ```bash [arktype]
39
+ pnpm add arktype
40
+ ```
41
+
42
+ :::
43
+
26
44
  Here's the simplest form you can create with `useFormValidator`:
27
45
 
28
46
  <ComponentDemo>
@@ -104,6 +122,186 @@ const onSubmit = handleSubmit((data) => {
104
122
  </template>
105
123
  </ComponentDemo>
106
124
 
125
+ ## Validation Libraries
126
+
127
+ `useFormValidator` relies on [Standard Schema](https://standardschema.dev), a common interface shared by the main validation libraries. Maz-UI does not ship any validation library: install the one you prefer and pass its schemas. Valibot, Zod and ArkType are declared as optional peer dependencies of `maz-ui`, so your package manager warns you if the installed version is not supported.
128
+
129
+ | Library | Supported versions |
130
+ |---------|--------------------|
131
+ | [Valibot](https://valibot.dev) | `>=1.0.0 <2.0.0` |
132
+ | [Zod](https://zod.dev) | `>=3.24.0 <5.0.0` (v3 and v4) |
133
+ | [ArkType](https://arktype.io) | `>=2.0.0 <3.0.0` |
134
+ | [Others](https://standardschema.dev/#what-schema-libraries-implement-the-spec) | Any library implementing Standard Schema V1 |
135
+
136
+ The `schema` option is always a **plain object** where each key is a field name and each value is a schema of your library. Do not wrap it with `object()` / `z.object()`.
137
+
138
+ ### Same form, different libraries
139
+
140
+ ::: code-group
141
+
142
+ ```ts [valibot]
143
+ import { useFormValidator } from 'maz-ui/composables'
144
+ import { email, minLength, nonEmpty, pipe, string } from 'valibot'
145
+
146
+ const { model, errorMessages, handleSubmit } = useFormValidator({
147
+ schema: {
148
+ email: pipe(string(), nonEmpty('Email is required'), email('Invalid email')),
149
+ password: pipe(string(), minLength(8, 'Min 8 characters')),
150
+ },
151
+ })
152
+ ```
153
+
154
+ ```ts [zod]
155
+ import { useFormValidator } from 'maz-ui/composables'
156
+ import { z } from 'zod'
157
+
158
+ const { model, errorMessages, handleSubmit } = useFormValidator({
159
+ schema: {
160
+ email: z.string().min(1, 'Email is required').pipe(z.email('Invalid email')),
161
+ password: z.string().min(8, 'Min 8 characters'),
162
+ },
163
+ })
164
+ ```
165
+
166
+ ```ts [arktype]
167
+ import { type } from 'arktype'
168
+ import { useFormValidator } from 'maz-ui/composables'
169
+
170
+ const { model, errorMessages, handleSubmit } = useFormValidator({
171
+ schema: {
172
+ email: type('string.email'),
173
+ password: type('string >= 8'),
174
+ },
175
+ })
176
+ ```
177
+
178
+ :::
179
+
180
+ ### Live example with Zod
181
+
182
+ <ComponentDemo>
183
+ <form class="maz:flex maz:flex-col maz:gap-4" @submit="onSubmitZod">
184
+ <MazInput
185
+ v-model="zodModel.username"
186
+ label="Username"
187
+ :hint="zodErrors.username"
188
+ :error="!!zodErrors.username"
189
+ :success="zodStates.username.valid"
190
+ />
191
+ <MazInput
192
+ v-model="zodModel.age"
193
+ label="Age"
194
+ type="number"
195
+ :hint="zodErrors.age"
196
+ :error="!!zodErrors.age"
197
+ :success="zodStates.age.valid"
198
+ />
199
+ <MazBtn type="submit" :loading="zodSubmitting">
200
+ Submit
201
+ </MazBtn>
202
+ </form>
203
+
204
+ <template #code>
205
+
206
+ ```vue
207
+ <script lang="ts" setup>
208
+ import { useFormValidator } from 'maz-ui/composables'
209
+ import { z } from 'zod'
210
+
211
+ const schema = {
212
+ username: z.string().min(1, 'Username is required').min(3, 'Min 3 characters'),
213
+ age: z.number({ error: 'Age is required' }).min(18, 'Min 18').max(100, 'Max 100'),
214
+ }
215
+
216
+ const {
217
+ model,
218
+ errorMessages,
219
+ fieldsStates,
220
+ isSubmitting,
221
+ handleSubmit,
222
+ } = useFormValidator({ schema })
223
+
224
+ const onSubmit = handleSubmit((data) => {
225
+ // data is typed as { username: string, age: number }
226
+ console.log('Form submitted:', data)
227
+ })
228
+ </script>
229
+
230
+ <template>
231
+ <form @submit="onSubmit">
232
+ <MazInput
233
+ v-model="model.username"
234
+ label="Username"
235
+ :hint="errorMessages.username"
236
+ :error="!!errorMessages.username"
237
+ :success="fieldsStates.username.valid"
238
+ />
239
+ <MazInput
240
+ v-model="model.age"
241
+ label="Age"
242
+ type="number"
243
+ :hint="errorMessages.age"
244
+ :error="!!errorMessages.age"
245
+ :success="fieldsStates.age.valid"
246
+ />
247
+ <MazBtn type="submit" :loading="isSubmitting">
248
+ Submit
249
+ </MazBtn>
250
+ </form>
251
+ </template>
252
+ ```
253
+
254
+ </template>
255
+ </ComponentDemo>
256
+
257
+ ### Mixing libraries
258
+
259
+ Each field is validated by its own schema, so you can mix libraries in the same form (useful during a progressive migration from one library to another):
260
+
261
+ ```ts
262
+ import { useFormValidator } from 'maz-ui/composables'
263
+ import { email, pipe, string } from 'valibot'
264
+ import { z } from 'zod'
265
+
266
+ const { model } = useFormValidator({
267
+ schema: {
268
+ email: pipe(string(), email('Invalid email')),
269
+ age: z.number().min(18, 'Min 18'),
270
+ },
271
+ })
272
+ ```
273
+
274
+ ### Custom schema
275
+
276
+ You can also write your own validator by implementing the `StandardSchemaV1` interface (exported by `maz-ui/composables`). `validate` can be synchronous or return a `Promise`:
277
+
278
+ ```ts
279
+ import type { StandardSchemaV1 } from 'maz-ui/composables'
280
+ import { useFormValidator } from 'maz-ui/composables'
281
+
282
+ const frenchZipCode: StandardSchemaV1<string, string> = {
283
+ '~standard': {
284
+ version: 1,
285
+ vendor: 'my-app',
286
+ validate: value =>
287
+ typeof value === 'string' && /^\d{5}$/.test(value)
288
+ ? { value }
289
+ : { issues: [{ message: 'Invalid zip code' }] },
290
+ },
291
+ }
292
+
293
+ const { model } = useFormValidator({
294
+ schema: { zipCode: frenchZipCode },
295
+ })
296
+ ```
297
+
298
+ ### Good to know
299
+
300
+ - **Empty values**: `undefined` and `null` field values are validated as an empty string (`''`). To make a field required, use a length rule (`nonEmpty()` with Valibot, `.min(1)` with Zod). An optional field with a format rule (email, URL...) has to accept the empty string explicitly, for example `z.email().or(z.literal(''))` with Zod or `union([literal(''), pipe(string(), email())])` with Valibot.
301
+ - **Error messages**: `errorMessages` contains the message of the first issue of each field, whatever the library.
302
+ - **Issues type**: `errors` and `fieldsStates[field].errors` are typed as `ValidationIssues`, the Standard Schema issue shape (`message` and `path`). At runtime, they are the native issues of your library, so you can cast them to access library specific properties (for example `BaseIssue<unknown>` from Valibot or `z.core.$ZodIssue` from Zod).
303
+ - **Type inference**: the `model` type (input) and the `handleSubmit` payload type (output, after transformations) are inferred from the schemas whatever the library.
304
+
107
305
  ## Understanding Form State
108
306
 
109
307
  `useFormValidator` returns several reactive values to help you manage your form:
@@ -151,7 +349,7 @@ Each field in `fieldsStates` contains:
151
349
  v-model="stateModel.age"
152
350
  label="Age (18-100)"
153
351
  type="number"
154
- :hint="stateErrors.age"s
352
+ :hint="stateErrors.age"
155
353
  :error="!!stateErrors.age"
156
354
  :success="stateFields.age.valid"
157
355
  />
@@ -365,7 +563,7 @@ Requires `useFormField` with `ref` option or `validationEvents`.
365
563
  :success="eagerEmailValid"
366
564
  :class="{ 'has-error-eager': eagerEmailHasError }"
367
565
  />
368
- <MazBtn type="submit" :loading="eagerSubmitting">Submit</MazBtn>
566
+ <MazBtn type="submit" :loading="eagerSubmitting" :disabled="!eagerIsValid">Submit</MazBtn>
369
567
  </form>
370
568
 
371
569
  <template #code>
@@ -381,7 +579,7 @@ const schema = {
381
579
  email: pipe(string(), nonEmpty('Required'), email('Invalid email')),
382
580
  }
383
581
 
384
- const { isSubmitting, handleSubmit } = useFormValidator({
582
+ const { isSubmitting, handleSubmit, isValid } = useFormValidator({
385
583
  schema,
386
584
  options: {
387
585
  mode: 'eager',
@@ -430,6 +628,8 @@ const {
430
628
  :error="emailHasError"
431
629
  :success="emailValid"
432
630
  />
631
+
632
+ <MazBtn type="submit" :loading="isSubmitting" :disabled="!isValid">Submit</MazBtn>
433
633
  </form>
434
634
  </template>
435
635
  ```
@@ -614,6 +814,8 @@ const { value: name, hasError, errorMessage, isValid } = useFormField<string>('n
614
814
 
615
815
  Pass a template ref to `useFormField`. It will automatically detect interactive elements and attach blur listeners.
616
816
 
817
+ The `ref` option is **reactive**: if the field is rendered conditionally (e.g. with `v-if`), the blur listeners are attached automatically as soon as the element appears in the DOM, and removed when it is unmounted. You don't need to handle anything special.
818
+
617
819
  ```vue
618
820
  <script setup>
619
821
  import { useFormField } from 'maz-ui/composables'
@@ -635,6 +837,41 @@ const { value, errorMessage, hasError } = useFormField<string>('email', {
635
837
  </template>
636
838
  ```
637
839
 
840
+ Because it is reactive, the same code works even when the input is wrapped in a `v-if`:
841
+
842
+ ```vue
843
+ <script setup>
844
+ import { useFormField } from 'maz-ui/composables'
845
+ import { useTemplateRef, ref } from 'vue'
846
+
847
+ const isVisible = ref(false)
848
+
849
+ const { value, errorMessage, hasError } = useFormField<string>('email', {
850
+ ref: useTemplateRef('emailRef'),
851
+ formIdentifier: 'my-form',
852
+ })
853
+ </script>
854
+
855
+ <template>
856
+ <MazInput
857
+ v-if="isVisible"
858
+ ref="emailRef"
859
+ v-model="value"
860
+ :hint="errorMessage"
861
+ :error="hasError"
862
+ />
863
+ </template>
864
+ ```
865
+
866
+ You can also pass a raw `HTMLElement` directly. In that case it won't be reactive, so the element must already exist in the DOM:
867
+
868
+ ```ts
869
+ const { value } = useFormField<string>('email', {
870
+ ref: document.querySelector('input'),
871
+ formIdentifier: 'my-form',
872
+ })
873
+ ```
874
+
638
875
  #### Option 2: Using `validationEvents`
639
876
 
640
877
  If your component emits a `blur` event, you can use `v-bind` with `validationEvents`.
@@ -660,7 +897,7 @@ const { value, errorMessage, hasError, validationEvents } = useFormField<string>
660
897
 
661
898
  ## TypeScript Type Inference
662
899
 
663
- The form model is automatically typed based on your schema:
900
+ The form model is automatically typed based on your schema, whatever the validation library:
664
901
 
665
902
  ```ts
666
903
  const schema = {
@@ -706,7 +943,23 @@ const { value: email } = useFormField<string>('email', {
706
943
 
707
944
  ## Async Validation
708
945
 
709
- Use Valibot's `pipeAsync` and `checkAsync` for async validations like checking username availability:
946
+ Async validations (like checking username availability) are supported by every library: Valibot's `pipeAsync` and `checkAsync`, Zod's async `refine`, etc. The example below uses Valibot, here is the Zod equivalent of the schema:
947
+
948
+ ```ts
949
+ import { z } from 'zod'
950
+
951
+ const schema = {
952
+ username: z
953
+ .string()
954
+ .min(1, 'Username is required')
955
+ .min(3, 'Min 3 characters')
956
+ .refine(async (value) => {
957
+ // Simulate API call
958
+ await new Promise(resolve => setTimeout(resolve, 2000))
959
+ return value !== 'taken'
960
+ }, 'Username is already taken'),
961
+ }
962
+ ```
710
963
 
711
964
  <ComponentDemo>
712
965
  <div class="maz:mb-4">
@@ -1026,7 +1279,7 @@ If your custom component isn't detected for blur events, add `data-interactive`:
1026
1279
 
1027
1280
  ```ts
1028
1281
  useFormValidator<TSchema>({
1029
- schema: TSchema, // Valibot validation schema (required)
1282
+ schema: TSchema, // Object of Standard Schema field schemas: Valibot, Zod, ArkType... (required)
1030
1283
  model?: Ref<Model>, // External model ref (optional)
1031
1284
  defaultValues?: DeepPartial<Model>, // Initial values (optional)
1032
1285
  options?: {
@@ -1074,7 +1327,7 @@ useFormField<FieldType>(
1074
1327
  options?: {
1075
1328
  defaultValue?: FieldType, // Default value for this field
1076
1329
  mode?: 'lazy' | 'aggressive' | 'eager' | 'blur' | 'progressive', // Override form mode
1077
- ref?: Ref<HTMLElement | ComponentInstance>, // Template ref for blur detection
1330
+ ref?: Ref<HTMLElement | ComponentInstance> | HTMLElement, // Reactive template ref (v-if friendly) or raw element
1078
1331
  formIdentifier?: string | symbol, // Must match useFormValidator's identifier
1079
1332
  }
1080
1333
  )
@@ -1101,6 +1354,14 @@ useFormField<FieldType>(
1101
1354
  ### Types
1102
1355
 
1103
1356
  ```ts
1357
+ // Standard Schema issue (https://standardschema.dev)
1358
+ interface ValidationIssue {
1359
+ readonly message: string
1360
+ readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>
1361
+ }
1362
+
1363
+ type ValidationIssues = ValidationIssue[]
1364
+
1104
1365
  interface FormValidatorOptions<Model> {
1105
1366
  mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
1106
1367
  throttledFields?: Partial<Record<keyof Model, number | true>>
@@ -1113,7 +1374,7 @@ interface FormValidatorOptions<Model> {
1113
1374
  interface FormFieldOptions<FieldType> {
1114
1375
  defaultValue?: FieldType
1115
1376
  mode?: 'eager' | 'lazy' | 'aggressive' | 'blur' | 'progressive'
1116
- ref?: Ref<HTMLElement | ComponentInstance>
1377
+ ref?: Ref<HTMLElement | ComponentInstance> | HTMLElement
1117
1378
  formIdentifier?: string | symbol
1118
1379
  }
1119
1380
 
@@ -1166,14 +1427,13 @@ const { value, validationEvents } = useFormField<string>('name')
1166
1427
  // Then: v-bind="validationEvents" on your input
1167
1428
  ```
1168
1429
 
1169
- ### Element Not Found Warning
1430
+ ### Conditionally Rendered Fields (`v-if`)
1170
1431
 
1171
- **Problem**: `No element found for ref in field 'name'`
1432
+ The `ref` option is reactive, so a field wrapped in `v-if` works out of the box: blur listeners are attached as soon as the element is rendered and removed when it is unmounted.
1172
1433
 
1173
- **Solutions**:
1174
- 1. Ensure the ref is bound to an HTML element or Vue component
1175
- 2. Make sure the component has a `$el` property
1176
- 3. For custom components, add `data-interactive` attribute
1434
+ If blur validation still doesn't trigger:
1435
+ 1. Ensure the `ref` is bound to an HTML element or a Vue component exposing `$el`
1436
+ 2. For custom components, add the `data-interactive` attribute
1177
1437
 
1178
1438
  ### Mismatched Form Identifiers
1179
1439
 
@@ -1214,9 +1474,29 @@ import {
1214
1474
  pipeAsync,
1215
1475
  checkAsync,
1216
1476
  } from 'valibot'
1477
+ import { z } from 'zod'
1217
1478
 
1218
1479
  const toast = useToast()
1219
1480
 
1481
+ // Zod Demo
1482
+ const zodSchema = {
1483
+ username: z.string().min(1, 'Username is required').min(3, 'Min 3 characters'),
1484
+ age: z.number({ error: 'Age is required' }).min(18, 'Min 18').max(100, 'Max 100'),
1485
+ }
1486
+
1487
+ const {
1488
+ model: zodModel,
1489
+ errorMessages: zodErrors,
1490
+ fieldsStates: zodStates,
1491
+ isSubmitting: zodSubmitting,
1492
+ handleSubmit: handleZod,
1493
+ } = useFormValidator({ schema: zodSchema })
1494
+
1495
+ const onSubmitZod = handleZod(async (data) => {
1496
+ await sleep(1000)
1497
+ toast.success(`Welcome ${data.username} (${data.age})!`, { position: 'top' })
1498
+ })
1499
+
1220
1500
  // Quick Start Demo
1221
1501
  const quickStartSchema = {
1222
1502
  email: pipe(string(), nonEmpty('Email is required'), email('Invalid email')),
@@ -1310,6 +1590,7 @@ const eagerSchema = {
1310
1590
  const {
1311
1591
  isSubmitting: eagerSubmitting,
1312
1592
  handleSubmit: handleEager,
1593
+ isValid: eagerIsValid,
1313
1594
  } = useFormValidator({
1314
1595
  schema: eagerSchema,
1315
1596
  options: { mode: 'eager', scrollToError: '.has-error-eager', identifier: 'form-eager' },
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: usePullToRefresh
3
+ description: usePullToRefresh is a headless Vue composable for the pull to refresh gesture on the window or any scroll container.
4
+ ---
5
+
6
+ # {{ $frontmatter.title }}
7
+
8
+ {{ $frontmatter.description }}
9
+
10
+ ## Introduction
11
+
12
+ `usePullToRefresh` tracks the pull to refresh gesture and gives you its state (distance, progress, ready, refreshing) to build your own indicator. It powers [MazPullToRefresh](./../components/maz-pull-to-refresh.md): use the component for a ready-made indicator, the composable when you need full control over the rendering.
13
+
14
+ The pull only starts when the target is scrolled to the top, follows a rubber band beyond the threshold, ignores horizontal and multi-touch gestures, and prevents the native bounce while pulling.
15
+
16
+ ## Basic usage
17
+
18
+ ```vue
19
+ <script lang="ts" setup>
20
+ import { usePullToRefresh } from 'maz-ui/composables'
21
+ import { useTemplateRef } from 'vue'
22
+
23
+ const scroller = useTemplateRef<HTMLElement>('scroller')
24
+
25
+ const { distance, progress, isReady, isRefreshing } = usePullToRefresh({
26
+ target: scroller,
27
+ onRefresh: () => fetchData(),
28
+ })
29
+ </script>
30
+
31
+ <template>
32
+ <div ref="scroller" class="maz:h-screen maz:overflow-y-auto">
33
+ <div class="maz:flex maz:items-end maz:justify-center maz:overflow-hidden" :style="{ height: `${distance}px` }">
34
+ <span v-if="isRefreshing">Refreshing…</span>
35
+ <span v-else-if="isReady">Release to refresh</span>
36
+ <span v-else :style="{ opacity: progress }">Pull to refresh</span>
37
+ </div>
38
+
39
+ <p>Content</p>
40
+ </div>
41
+ </template>
42
+ ```
43
+
44
+ Without `target`, the gesture is tracked on the window.
45
+
46
+ ## Options
47
+
48
+ | Option | Type | Default | Description |
49
+ | ------------- | -------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------ |
50
+ | `onRefresh` | `() => unknown` | - | Called on release past the threshold, or by `refresh()` |
51
+ | `target` | `MaybeRefOrGetter<HTMLElement \| Window \| null>` | `window` | Scroll container receiving the gesture |
52
+ | `threshold` | `MaybeRefOrGetter<number>` | `80` | Pull distance (px, after resistance) needed to trigger a refresh |
53
+ | `maxDistance` | `MaybeRefOrGetter<number>` | `threshold * 1.6` | Upper bound of the rubber band |
54
+ | `resistance` | `MaybeRefOrGetter<number>` | `0.5` | Ratio between the finger movement and the pull distance |
55
+ | `minDuration` | `MaybeRefOrGetter<number>` | `400` | Minimum duration (ms) of the refreshing state |
56
+ | `haptic` | `MaybeRefOrGetter<boolean>` | `true` | Vibrate briefly when the threshold is reached |
57
+ | `disabled` | `MaybeRefOrGetter<boolean>` | `false` | Disable the gesture |
58
+ | `onError` | `(error: unknown) => void` | - | Called when `onRefresh` throws or rejects (the error is also exposed in `error`) |
59
+
60
+ ## Return
61
+
62
+ | Property | Type | Description |
63
+ | -------------- | --------------------------- | ------------------------------------------------------------ |
64
+ | `distance` | `Readonly<Ref<number>>` | Current pull distance in px |
65
+ | `progress` | `ComputedRef<number>` | Progress towards the threshold, from 0 to 1 |
66
+ | `isPulling` | `Readonly<Ref<boolean>>` | The user is pulling |
67
+ | `isReady` | `ComputedRef<boolean>` | Releasing now triggers a refresh |
68
+ | `isRefreshing` | `Readonly<Ref<boolean>>` | `onRefresh` is running |
69
+ | `error` | `Readonly<Ref<unknown>>` | Error of the last refresh, reset on each refresh |
70
+ | `refresh` | `() => Promise<void>` | Run the refresh programmatically (for a keyboard-usable button) |
71
+ | `start` | `() => void` | Attach the listeners (done automatically on mount) |
72
+ | `stop` | `() => void` | Detach the listeners (done automatically on unmount) |
73
+
74
+ ## Notes
75
+
76
+ - Inside a component, listeners are attached on mount and removed when the scope is disposed. Outside of a component, they are attached right away.
77
+ - When `target` changes (for example a ref resolved later), the listeners move to the new element.
78
+ - `refresh()` never throws: errors go to `error` and `onError`. Calling it while a refresh is running does nothing.
79
+ - SSR safe: nothing touches the DOM before mount.
@@ -12,11 +12,19 @@ description: useSwipe is a Vue composable that simplifies the management of "swi
12
12
  `useSwipe` allows you to detect and react to swiping movements on an HTML element. It provides you with various information about the swipe movement, such as the direction, distance, start, and end coordinates.
13
13
  You can use this information to implement specific interactions in your application, such as scrolling a carousel, opening a side menu, etc.
14
14
 
15
+ It is built on top of [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events), so it works with **touch, mouse and pen** out of the box.
16
+
17
+ ::: tip
18
+ For a real-time drag gesture (follow the pointer, snap back, drag-to-dismiss…) rather than discrete directional detection, use [useDrag](./use-drag.md).
19
+ :::
20
+
15
21
  ## Key Features
16
22
 
17
23
  - Detects swipes in all 4 directions (left, right, up, down)
24
+ - Works with touch, mouse and pen (Pointer Events)
18
25
  - Provides key information about the swipe movement (start/end coordinates, horizontal/vertical distance)
19
26
  - Allows you to configure callbacks for each swipe direction
27
+ - Restrict the gesture to specific pointer types
20
28
  - Possibility to customize the swipe detection threshold
21
29
  - Automatically handles the addition and removal of event listeners
22
30
  - Can be used with any HTML element
@@ -27,7 +35,7 @@ You can use this information to implement specific interactions in your applicat
27
35
  <p>
28
36
  Swipe in any direction<br>
29
37
  <span class="maz:text-xs maz:text-muted">
30
- (You should use a real device or a mobile simulator to test the swipe functionality)
38
+ (works with touch, mouse and pen)
31
39
  </span>
32
40
  <br><br>
33
41
  Last swipe direction: {{lastSwipeDirection || 'None'}}
@@ -74,7 +82,7 @@ onUnmounted(() => {
74
82
  <p>
75
83
  Swipe in any direction<br>
76
84
  <span class="maz:text-sm maz:text-muted">
77
- (You should use a real device or a mobile simulator to test the swipe functionality)
85
+ (works with touch, mouse and pen)
78
86
  </span>
79
87
  <br><br>
80
88
  Last swipe direction: {{ lastSwipeDirection || 'None' }}
@@ -153,35 +161,41 @@ interface UseSwipeOptions {
153
161
  */
154
162
  element: HTMLElement | string | Ref<HTMLElement>
155
163
  /** Callback executed when a left swipe is detected. */
156
- onLeft?: (event: TouchEvent) => void
164
+ onLeft?: (event: PointerEvent) => void
157
165
  /** Callback executed when a right swipe is detected. */
158
- onRight?: (event: TouchEvent) => void
166
+ onRight?: (event: PointerEvent) => void
159
167
  /** Callback executed when an up swipe is detected. */
160
- onUp?: (event: TouchEvent) => void
168
+ onUp?: (event: PointerEvent) => void
161
169
  /** Callback executed when a down swipe is detected. */
162
- onDown?: (event: TouchEvent) => void
170
+ onDown?: (event: PointerEvent) => void
163
171
  /**
164
172
  * The minimum distance the swipe must travel to be considered valid.
165
173
  * @default 50
166
174
  */
167
175
  threshold?: number
168
176
  /**
169
- * Whether to prevent the default behavior of the touchmove event.
177
+ * Pointer types that can trigger the swipe.
178
+ * @default ['mouse', 'touch', 'pen']
179
+ */
180
+ pointerTypes?: ('mouse' | 'touch' | 'pen')[]
181
+ /**
182
+ * Whether to prevent the default behavior of the pointer move event
183
+ * (the move listener becomes non-passive when enabled).
170
184
  * @default false
171
185
  */
172
- preventDefaultOnTouchMove?: boolean
186
+ preventDefaultOnMove?: boolean
173
187
  /**
174
188
  * Whether to prevent the default behavior of the mousewheel event.
175
189
  * @default false
176
190
  */
177
191
  preventDefaultOnMouseWheel?: boolean
178
192
  /**
179
- * Whether to trigger the swipe event immediately on touchstart/mousedown.
193
+ * Whether to start listening immediately on instantiation.
180
194
  * @default false
181
195
  */
182
196
  immediate?: boolean
183
197
  /**
184
- * Whether to trigger the swipe event only on touchend/mouseup.
198
+ * Whether to trigger the swipe event only on pointer up.
185
199
  * @default false
186
200
  */
187
201
  triggerOnEnd?: boolean
@@ -220,4 +234,5 @@ interface UseSwipeReturn {
220
234
  - If you use the composable in a Vue component, make sure to call it in the `setup()` and clean up the event listeners in the `onUnmounted()`.
221
235
  - The composable automatically handles the addition and removal of event listeners based on the provided options.
222
236
  - You can customize the swipe detection threshold by modifying the `threshold` option.
223
- - If you want to prevent the default behavior of touchmove or mousewheel events, you can set the `preventDefaultOnTouchMove` and `preventDefaultOnMouseWheel` options, respectively.
237
+ - If you want to prevent the default behavior of pointer move or mousewheel events, you can set the `preventDefaultOnMove` and `preventDefaultOnMouseWheel` options, respectively.
238
+ - Use the `pointerTypes` option to restrict the gesture to specific input types (e.g. `['touch']` for touch-only).