@velkymx/vibeui 0.9.0 → 1.1.0
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.
- package/AGENTS.md +44 -0
- package/CLAUDE.md +48 -0
- package/README.md +222 -197
- package/dist/App.vue.d.ts +2 -1
- package/dist/components/HelloWorld.vue.d.ts +2 -1
- package/dist/components/VibeAccordion.vue.d.ts +39 -40
- package/dist/components/VibeAlert.vue.d.ts +21 -37
- package/dist/components/VibeAutocomplete.vue.d.ts +59 -31
- package/dist/components/VibeBadge.vue.d.ts +10 -18
- package/dist/components/VibeBreadcrumb.vue.d.ts +25 -21
- package/dist/components/VibeButton.vue.d.ts +13 -17
- package/dist/components/VibeButtonGroup.vue.d.ts +11 -19
- package/dist/components/VibeCard.vue.d.ts +51 -24
- package/dist/components/VibeCarousel.vue.d.ts +27 -49
- package/dist/components/VibeChartBar.vue.d.ts +61 -0
- package/dist/components/VibeChartLine.vue.d.ts +70 -0
- package/dist/components/VibeChartPie.vue.d.ts +34 -0
- package/dist/components/VibeCloseButton.vue.d.ts +9 -7
- package/dist/components/VibeCol.vue.d.ts +9 -17
- package/dist/components/VibeCollapse.vue.d.ts +24 -32
- package/dist/components/VibeContainer.vue.d.ts +9 -17
- package/dist/components/VibeDataTable.vue.d.ts +122 -63
- package/dist/components/VibeDatePicker.vue.d.ts +4 -6
- package/dist/components/VibeDraggable.vue.d.ts +9 -13
- package/dist/components/VibeDropdown.vue.d.ts +46 -43
- package/dist/components/VibeDroppable.vue.d.ts +9 -13
- package/dist/components/VibeFileInput.vue.d.ts +14 -20
- package/dist/components/VibeFormCheckbox.vue.d.ts +30 -22
- package/dist/components/VibeFormDatepicker.vue.d.ts +27 -30
- package/dist/components/VibeFormErrorSummary.vue.d.ts +17 -0
- package/dist/components/VibeFormGroup.vue.d.ts +10 -13
- package/dist/components/VibeFormInput.vue.d.ts +60 -25
- package/dist/components/VibeFormRadio.vue.d.ts +23 -24
- package/dist/components/VibeFormSelect.vue.d.ts +28 -34
- package/dist/components/VibeFormSpinbutton.vue.d.ts +31 -34
- package/dist/components/VibeFormSwitch.vue.d.ts +22 -23
- package/dist/components/VibeFormTextarea.vue.d.ts +24 -25
- package/dist/components/VibeFormWysiwyg.vue.d.ts +23 -22
- package/dist/components/VibeHero.vue.d.ts +117 -0
- package/dist/components/VibeIcon.vue.d.ts +27 -7
- package/dist/components/VibeInputGroup.vue.d.ts +12 -14
- package/dist/components/VibeLink.vue.d.ts +14 -18
- package/dist/components/VibeListGroup.vue.d.ts +26 -22
- package/dist/components/VibeModal.vue.d.ts +47 -45
- package/dist/components/VibeNav.vue.d.ts +40 -20
- package/dist/components/VibeNavbar.vue.d.ts +12 -19
- package/dist/components/VibeNavbarBrand.vue.d.ts +9 -17
- package/dist/components/VibeNavbarNav.vue.d.ts +52 -33
- package/dist/components/VibeNavbarToggle.vue.d.ts +7 -5
- package/dist/components/VibeOffcanvas.vue.d.ts +28 -43
- package/dist/components/VibePagination.vue.d.ts +38 -29
- package/dist/components/VibePlaceholder.vue.d.ts +9 -17
- package/dist/components/VibePopover.vue.d.ts +17 -40
- package/dist/components/VibeProgress.vue.d.ts +13 -21
- package/dist/components/VibeResizable.vue.d.ts +10 -14
- package/dist/components/VibeRow.vue.d.ts +9 -17
- package/dist/components/VibeScrollspy.vue.d.ts +37 -25
- package/dist/components/VibeSkeleton.vue.d.ts +2 -1
- package/dist/components/VibeSlider.vue.d.ts +4 -5
- package/dist/components/VibeSortable.vue.d.ts +37 -18
- package/dist/components/VibeSpinner.vue.d.ts +4 -7
- package/dist/components/VibeStepper.vue.d.ts +32 -31
- package/dist/components/VibeTab.vue.d.ts +7 -11
- package/dist/components/VibeTabContent.vue.d.ts +13 -31
- package/dist/components/VibeTabs.vue.d.ts +9 -13
- package/dist/components/VibeToast.vue.d.ts +31 -44
- package/dist/components/VibeToastHost.vue.d.ts +2 -1
- package/dist/components/VibeTooltip.vue.d.ts +16 -39
- package/dist/components/chart/chartColors.d.ts +3 -0
- package/dist/components/chart/chartResize.d.ts +2 -0
- package/dist/components/chart/chartTooltip.d.ts +2 -0
- package/dist/components/chart/chartTypes.d.ts +6 -0
- package/dist/components/chart/drawBar.d.ts +5 -0
- package/dist/components/chart/drawLine.d.ts +18 -0
- package/dist/components/chart/drawPie.d.ts +4 -0
- package/dist/components/dndStore.d.ts +1 -0
- package/dist/components/index.d.ts +6 -1
- package/dist/composables/useBreakpoints.d.ts +1 -0
- package/dist/composables/useColorMode.d.ts +1 -1
- package/dist/composables/useId.d.ts +1 -4
- package/dist/directives/vTooltip.d.ts +2 -3
- package/dist/injectionKeys.d.ts +34 -0
- package/dist/purify.es-eYghtX9j.mjs +735 -0
- package/dist/types.d.ts +23 -2
- package/dist/utils/routeKey.d.ts +11 -0
- package/dist/utils/safeCss.d.ts +16 -0
- package/dist/utils/safeHref.d.ts +7 -0
- package/dist/utils/sanitizeHtml.d.ts +13 -0
- package/dist/vibeui.css +2 -1
- package/dist/vibeui.es.js +8527 -5855
- package/dist/vibeui.umd.js +3 -1
- package/docs/README.md +225 -0
- package/docs/components/advanced/popover.md +125 -0
- package/docs/components/advanced/scrollspy.md +106 -0
- package/docs/components/advanced/tooltip.md +90 -0
- package/docs/components/card/card.md +223 -0
- package/docs/components/charts/chart-bar.md +129 -0
- package/docs/components/charts/chart-line.md +136 -0
- package/docs/components/charts/chart-pie.md +102 -0
- package/docs/components/core/alert.md +94 -0
- package/docs/components/core/badge.md +75 -0
- package/docs/components/core/button-group.md +99 -0
- package/docs/components/core/button.md +145 -0
- package/docs/components/core/close-button.md +82 -0
- package/docs/components/core/link.md +79 -0
- package/docs/components/core/placeholder.md +129 -0
- package/docs/components/core/skeleton.md +40 -0
- package/docs/components/core/spinner.md +103 -0
- package/docs/components/data/datatable.md +437 -0
- package/docs/components/hero/hero.md +65 -0
- package/docs/components/interactive/accordion.md +121 -0
- package/docs/components/interactive/carousel.md +112 -0
- package/docs/components/interactive/collapse.md +106 -0
- package/docs/components/interactive/date-picker.md +78 -0
- package/docs/components/interactive/draggable.md +91 -0
- package/docs/components/interactive/dropdown.md +129 -0
- package/docs/components/interactive/modal.md +151 -0
- package/docs/components/interactive/offcanvas.md +92 -0
- package/docs/components/interactive/resizable.md +73 -0
- package/docs/components/interactive/slider.md +57 -0
- package/docs/components/interactive/sortable.md +52 -0
- package/docs/components/interactive/stepper.md +83 -0
- package/docs/components/interactive/tabs.md +66 -0
- package/docs/components/interactive/toast.md +177 -0
- package/docs/components/layout/col.md +117 -0
- package/docs/components/layout/container.md +53 -0
- package/docs/components/layout/row.md +107 -0
- package/docs/components/list/list-group.md +225 -0
- package/docs/components/navigation/breadcrumb.md +120 -0
- package/docs/components/navigation/nav.md +151 -0
- package/docs/components/navigation/navbar.md +114 -0
- package/docs/components/navigation/pagination.md +163 -0
- package/docs/components/progress/progress.md +188 -0
- package/docs/composables/back-button.md +28 -0
- package/docs/composables/breakpoints.md +54 -0
- package/docs/composables/color-mode.md +179 -0
- package/docs/composables/use-form.md +78 -0
- package/docs/composables/use-position.md +68 -0
- package/docs/composables/use-toast.md +91 -0
- package/docs/directives/v-tooltip.md +58 -0
- package/docs/forms/README.md +87 -0
- package/docs/forms/autocomplete.md +96 -0
- package/docs/forms/file-input.md +97 -0
- package/docs/forms/form-checkbox.md +100 -0
- package/docs/forms/form-datepicker.md +80 -0
- package/docs/forms/form-error-summary.md +96 -0
- package/docs/forms/form-group.md +118 -0
- package/docs/forms/form-input.md +140 -0
- package/docs/forms/form-radio.md +82 -0
- package/docs/forms/form-select.md +98 -0
- package/docs/forms/form-spinbutton.md +94 -0
- package/docs/forms/form-switch.md +75 -0
- package/docs/forms/form-textarea.md +82 -0
- package/docs/forms/form-wysiwyg.md +99 -0
- package/docs/forms/input-group.md +70 -0
- package/docs/forms/validation.md +213 -0
- package/docs/getting-started/starter-template.md +136 -0
- package/docs/superpowers/plans/2026-05-12-vibe-charts.md +1999 -0
- package/docs/superpowers/plans/2026-05-29-e2e-browser-testing.md +615 -0
- package/docs/superpowers/specs/2026-05-12-charting-design.md +185 -0
- package/docs/superpowers/specs/2026-05-29-e2e-browser-testing-design.md +168 -0
- package/docs/utilities/position.md +82 -0
- package/docs/versioning.md +57 -0
- package/examples/README.md +201 -0
- package/examples/album.html +262 -0
- package/examples/blog.html +166 -0
- package/examples/carousel.html +114 -0
- package/examples/checkout.html +275 -0
- package/examples/cover.html +89 -0
- package/examples/dashboard.html +156 -0
- package/examples/index.html +129 -0
- package/examples/jumbotron.html +56 -0
- package/examples/mobile-dashboard.html +138 -0
- package/examples/pricing.html +172 -0
- package/examples/product.html +145 -0
- package/examples/sidebars.html +166 -0
- package/examples/sign-in.html +119 -0
- package/examples/starter.html +314 -0
- package/examples/sticky-footer-navbar.html +70 -0
- package/examples/sticky-footer.html +60 -0
- package/examples/test-simple.html +91 -0
- package/llms.txt +801 -0
- package/package.json +44 -14
- package/dist/types/index.d.ts +0 -6
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# VibeFormTextarea
|
|
2
|
+
|
|
3
|
+
Multi-line text input with optional character counting, built-in validation, and accessibility.
|
|
4
|
+
|
|
5
|
+
## Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Default | Description |
|
|
8
|
+
|------|------|---------|-------------|
|
|
9
|
+
| `modelValue` | `string` | `''` | The textarea value (v-model). |
|
|
10
|
+
| `id` | `string` | auto-generated | Element id. Auto-generated, or inherited from a parent `VibeFormGroup`. |
|
|
11
|
+
| `label` | `string` | `undefined` | Standalone label text. |
|
|
12
|
+
| `placeholder` | `string` | `undefined` | Placeholder text. |
|
|
13
|
+
| `rows` | `number \| string` | `3` | Number of visible text rows. |
|
|
14
|
+
| `maxlength` | `number \| string` | `undefined` | Maximum character count (also shown in the counter when `showCharCount` is set). |
|
|
15
|
+
| `disabled` | `boolean` | `false` | Disable the textarea. |
|
|
16
|
+
| `readonly` | `boolean` | `false` | Make the textarea read-only. |
|
|
17
|
+
| `required` | `boolean` | `false` | Mark as required. |
|
|
18
|
+
| `size` | `'sm' \| 'lg'` | `undefined` | Control size. |
|
|
19
|
+
| `validationState` | `'valid' \| 'invalid' \| null` | `null` | Visual validation state. |
|
|
20
|
+
| `validationMessage` | `string` | `undefined` | Feedback message for the current state. |
|
|
21
|
+
| `validationRules` | `ValidationRule[] \| ValidatorFunction` | `undefined` | Rules carried for use with a validation composable. |
|
|
22
|
+
| `validateOn` | `'input' \| 'blur' \| 'change'` | `'blur'` | When the `validate` event fires. |
|
|
23
|
+
| `helpText` | `string` | `undefined` | Help text below the textarea. |
|
|
24
|
+
| `noResize` | `boolean` | `false` | Disable manual resizing (`resize: none`). |
|
|
25
|
+
| `showCharCount` | `boolean` | `false` | Show a character counter (`count` or `count / maxlength`). |
|
|
26
|
+
|
|
27
|
+
## Events
|
|
28
|
+
|
|
29
|
+
| Event | Payload | Description |
|
|
30
|
+
|-------|---------|-------------|
|
|
31
|
+
| `update:modelValue` | `string` | Emitted on input. |
|
|
32
|
+
| `input` | `Event` | Native input event. |
|
|
33
|
+
| `change` | `Event` | Native change event. |
|
|
34
|
+
| `blur` | `FocusEvent` | Emitted on blur. |
|
|
35
|
+
| `focus` | `FocusEvent` | Emitted on focus. |
|
|
36
|
+
| `validate` | — | Emitted when the `validateOn` trigger occurs. |
|
|
37
|
+
|
|
38
|
+
## Slots
|
|
39
|
+
|
|
40
|
+
None.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Recommended: inside a VibeFormGroup
|
|
45
|
+
|
|
46
|
+
```vue
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
import { ref } from 'vue'
|
|
49
|
+
const bio = ref('')
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<VibeFormGroup label="Bio">
|
|
54
|
+
<VibeFormTextarea v-model="bio" :rows="5" :maxlength="280" show-char-count />
|
|
55
|
+
</VibeFormGroup>
|
|
56
|
+
</template>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Standalone
|
|
60
|
+
|
|
61
|
+
```vue
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { ref } from 'vue'
|
|
64
|
+
const notes = ref('')
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<VibeFormTextarea v-model="notes" label="Notes" no-resize />
|
|
69
|
+
</template>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Important Notes
|
|
73
|
+
|
|
74
|
+
- **Character counter:** `showCharCount` renders inside the help-text row. When inside a `VibeFormGroup` that provides its own help text, the counter is suppressed (the group owns help rendering).
|
|
75
|
+
- **Group linking:** wrapped in a `VibeFormGroup`, the textarea consumes the group id so the label and feedback link automatically.
|
|
76
|
+
|
|
77
|
+
## Bootstrap CSS Classes
|
|
78
|
+
|
|
79
|
+
- `.form-control`
|
|
80
|
+
- `.form-control-{sm|lg}`
|
|
81
|
+
- `.is-valid`, `.is-invalid`
|
|
82
|
+
- `.form-text`
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# VibeFormWysiwyg
|
|
2
|
+
|
|
3
|
+
Rich-text editor powered by Quill. HTML is sanitized with DOMPurify on the way in and on the way out (XSS defense-in-depth).
|
|
4
|
+
|
|
5
|
+
## Peer dependencies
|
|
6
|
+
|
|
7
|
+
This component requires two optional peers — install both:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install quill dompurify
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Quill and DOMPurify are loaded lazily on mount. If `quill` is missing, the component shows a warning alert and emits a `component-error` event instead of crashing.
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
| Prop | Type | Default | Description |
|
|
18
|
+
|------|------|---------|-------------|
|
|
19
|
+
| `modelValue` | `string` | `''` | HTML content (v-model). Sanitized with DOMPurify before being rendered. |
|
|
20
|
+
| `id` | `string` | auto-generated | Element id. Auto-generated, or inherited from a parent `VibeFormGroup`. |
|
|
21
|
+
| `label` | `string` | `undefined` | Label text. |
|
|
22
|
+
| `placeholder` | `string` | `'Write something...'` | Placeholder shown when empty. |
|
|
23
|
+
| `disabled` | `boolean` | `false` | Disable editing. |
|
|
24
|
+
| `readonly` | `boolean` | `false` | Make the editor read-only. |
|
|
25
|
+
| `required` | `boolean` | `false` | Mark as required. |
|
|
26
|
+
| `theme` | `'snow' \| 'bubble'` | `'snow'` | Quill theme. |
|
|
27
|
+
| `toolbar` | `unknown[] \| string \| boolean` | `undefined` | Toolbar config: a Quill toolbar array, a preset name (`'minimal'`, `'standard'`, `'full'`), `false` to hide, or omit for the default toolbar. |
|
|
28
|
+
| `mobileToolbar` | `unknown[] \| string \| boolean` | `undefined` | Toolbar used at mobile breakpoints (same value forms as `toolbar`). |
|
|
29
|
+
| `height` | `string` | `'200px'` | Minimum editor height. Must be a valid CSS length; invalid values fall back to `200px`. |
|
|
30
|
+
| `validationState` | `'valid' \| 'invalid' \| null` | `null` | Visual validation state. |
|
|
31
|
+
| `validationMessage` | `string` | `undefined` | Feedback message for the current state. |
|
|
32
|
+
| `validationRules` | `ValidationRule[] \| ValidatorFunction` | `undefined` | Rules carried for use with a validation composable. |
|
|
33
|
+
| `validateOn` | `'change' \| 'blur'` | `'blur'` | When the `validate` event fires. |
|
|
34
|
+
| `helpText` | `string` | `undefined` | Help text below the editor. |
|
|
35
|
+
|
|
36
|
+
## Events
|
|
37
|
+
|
|
38
|
+
| Event | Payload | Description |
|
|
39
|
+
|-------|---------|-------------|
|
|
40
|
+
| `update:modelValue` | `string` | Emitted with sanitized HTML as content changes. |
|
|
41
|
+
| `change` | — | Emitted on content change. |
|
|
42
|
+
| `blur` | — | Emitted on blur. |
|
|
43
|
+
| `focus` | — | Emitted on focus. |
|
|
44
|
+
| `validate` | — | Emitted when the `validateOn` trigger occurs. |
|
|
45
|
+
| `ready` | `unknown` | Emitted with the Quill instance once initialized. |
|
|
46
|
+
| `component-error` | `ComponentError` | Emitted if Quill fails to load (e.g. peer not installed). |
|
|
47
|
+
|
|
48
|
+
## Slots
|
|
49
|
+
|
|
50
|
+
None.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Recommended: inside a VibeFormGroup
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<script setup lang="ts">
|
|
58
|
+
import { ref } from 'vue'
|
|
59
|
+
const content = ref('<p>Hello <strong>world</strong></p>')
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<VibeFormGroup label="Description">
|
|
64
|
+
<VibeFormWysiwyg v-model="content" toolbar="standard" height="300px" />
|
|
65
|
+
</VibeFormGroup>
|
|
66
|
+
</template>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Handling a missing peer
|
|
70
|
+
|
|
71
|
+
```vue
|
|
72
|
+
<script setup lang="ts">
|
|
73
|
+
import { ref } from 'vue'
|
|
74
|
+
import type { ComponentError } from '@velkymx/vibeui'
|
|
75
|
+
|
|
76
|
+
const content = ref('')
|
|
77
|
+
const onError = (err: ComponentError) => console.error(err.message)
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<VibeFormWysiwyg v-model="content" @component-error="onError" />
|
|
82
|
+
</template>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Important Notes
|
|
86
|
+
|
|
87
|
+
- **Sanitization:** incoming `modelValue` HTML is sanitized before it is rendered, and the emitted HTML is sanitized too. Unsafe tags and attributes (e.g. `<script>`, inline event handlers) are stripped. Do not rely on the editor to preserve dangerous markup.
|
|
88
|
+
- **Both peers required:** `dompurify` is needed for sanitization and `quill` for the editor; install them together.
|
|
89
|
+
- **`height` is validated:** only CSS length values are accepted; anything else falls back to `200px`.
|
|
90
|
+
- **Responsive toolbar:** at mobile breakpoints `mobileToolbar` (or a compact default) is used, and the editor re-initializes while preserving content.
|
|
91
|
+
- **Group linking:** wrapped in a `VibeFormGroup`, the editor consumes the group id so the label and feedback link automatically.
|
|
92
|
+
|
|
93
|
+
## Bootstrap CSS Classes
|
|
94
|
+
|
|
95
|
+
- `.form-label`, `.form-text`
|
|
96
|
+
- `.alert`, `.alert-warning` (load-failure message)
|
|
97
|
+
- `.valid-feedback`, `.invalid-feedback`
|
|
98
|
+
|
|
99
|
+
The editor shell uses VibeUI's own `.vibe-wysiwyg-container` class plus Quill's `.ql-*` classes.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# VibeInputGroup
|
|
2
|
+
|
|
3
|
+
Wraps a control with prepended/appended text, buttons, or other elements using Bootstrap's input-group.
|
|
4
|
+
|
|
5
|
+
## Props
|
|
6
|
+
|
|
7
|
+
| Prop | Type | Default | Description |
|
|
8
|
+
|------|------|---------|-------------|
|
|
9
|
+
| `size` | `'sm' \| 'lg'` | `undefined` | Input-group size. |
|
|
10
|
+
| `prepend` | `string` | `undefined` | Convenience text rendered before the control as `.input-group-text`. |
|
|
11
|
+
| `append` | `string` | `undefined` | Convenience text rendered after the control as `.input-group-text`. |
|
|
12
|
+
| `tag` | `Tag` | `'div'` | Root element tag. |
|
|
13
|
+
|
|
14
|
+
## Events
|
|
15
|
+
|
|
16
|
+
None.
|
|
17
|
+
|
|
18
|
+
## Slots
|
|
19
|
+
|
|
20
|
+
| Slot | Description |
|
|
21
|
+
|------|-------------|
|
|
22
|
+
| default | The control to wrap (typically a `VibeFormInput` with `no-wrapper`). |
|
|
23
|
+
| `prepend` | Custom prepended content (overrides the `prepend` prop). |
|
|
24
|
+
| `append` | Custom appended content (overrides the `append` prop). |
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Text prepend / append
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { ref } from 'vue'
|
|
33
|
+
const price = ref('')
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<VibeInputGroup prepend="$" append=".00">
|
|
38
|
+
<VibeFormInput v-model="price" type="number" no-wrapper />
|
|
39
|
+
</VibeInputGroup>
|
|
40
|
+
</template>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Custom slots with a button
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { ref } from 'vue'
|
|
48
|
+
const term = ref('')
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<VibeInputGroup>
|
|
53
|
+
<VibeFormInput v-model="term" placeholder="Search" no-wrapper />
|
|
54
|
+
<template #append>
|
|
55
|
+
<VibeButton variant="primary">Go</VibeButton>
|
|
56
|
+
</template>
|
|
57
|
+
</VibeInputGroup>
|
|
58
|
+
</template>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Important Notes
|
|
62
|
+
|
|
63
|
+
- **Use `no-wrapper` on the input:** pass `no-wrapper` to `VibeFormInput` so it renders just the `<input>` and Bootstrap can style it as part of the group.
|
|
64
|
+
- **Slots override props:** providing the `prepend`/`append` slots replaces the corresponding text props.
|
|
65
|
+
|
|
66
|
+
## Bootstrap CSS Classes
|
|
67
|
+
|
|
68
|
+
- `.input-group`
|
|
69
|
+
- `.input-group-{sm|lg}`
|
|
70
|
+
- `.input-group-text`
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Form Validation
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Built-in Validators
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { validators } from '@velkymx/vibeui'
|
|
9
|
+
```
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
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>`. |
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const rules = [
|
|
27
|
+
validators.required('Email is required'),
|
|
28
|
+
validators.email()
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Custom Rules
|
|
33
|
+
|
|
34
|
+
A rule is `{ validator, message? }`. The validator may be sync or async.
|
|
35
|
+
|
|
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
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rules = [validators.required(), validators.minLength(8), passwordStrength]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Single-field validation: `useFormValidation`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { useFormValidation } from '@velkymx/vibeui'
|
|
54
|
+
|
|
55
|
+
const field = useFormValidation('') // initial value
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
|
|
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. |
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<script setup lang="ts">
|
|
76
|
+
import { useFormValidation, validators } from '@velkymx/vibeui'
|
|
77
|
+
|
|
78
|
+
const email = useFormValidation('')
|
|
79
|
+
const validateEmail = () =>
|
|
80
|
+
email.validate([validators.required(), validators.email()])
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<VibeFormGroup
|
|
85
|
+
label="Email"
|
|
86
|
+
:validation-state="email.validationState"
|
|
87
|
+
:validation-message="email.validationMessage"
|
|
88
|
+
>
|
|
89
|
+
<VibeFormInput v-model="email.value" type="email" @validate="validateEmail" />
|
|
90
|
+
</VibeFormGroup>
|
|
91
|
+
</template>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Editing an invalid field clears its `invalid` state automatically until you re-validate.
|
|
95
|
+
|
|
96
|
+
## Whole-form validation: `useForm`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { useForm } from '@velkymx/vibeui'
|
|
100
|
+
|
|
101
|
+
const form = useForm({ username: '', email: '', age: 0 })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
|
|
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. |
|
|
119
|
+
|
|
120
|
+
```vue
|
|
121
|
+
<script setup lang="ts">
|
|
122
|
+
import { useForm, validators } from '@velkymx/vibeui'
|
|
123
|
+
|
|
124
|
+
const form = useForm({ username: '', email: '', password: '' })
|
|
125
|
+
|
|
126
|
+
const rules = {
|
|
127
|
+
username: [validators.required(), validators.minLength(3)],
|
|
128
|
+
email: [validators.required(), validators.email()],
|
|
129
|
+
password: [validators.required(), validators.minLength(8)]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const onSubmit = async () => {
|
|
133
|
+
const { valid } = await form.validate(rules)
|
|
134
|
+
if (valid) console.log('submit', form.values.value)
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
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>
|
|
162
|
+
</template>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## When validation runs: `validateOn`
|
|
166
|
+
|
|
167
|
+
Form controls emit `validate` based on `validateOn`:
|
|
168
|
+
|
|
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'`.
|
|
171
|
+
|
|
172
|
+
Wire `@validate` to your validation call:
|
|
173
|
+
|
|
174
|
+
```vue
|
|
175
|
+
<VibeFormInput v-model="field.value" validate-on="input" @validate="run" />
|
|
176
|
+
```
|
|
177
|
+
|
|
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.
|
|
181
|
+
|
|
182
|
+
```vue
|
|
183
|
+
<script setup lang="ts">
|
|
184
|
+
import { useFormValidation, validators } from '@velkymx/vibeui'
|
|
185
|
+
|
|
186
|
+
const username = useFormValidation('')
|
|
187
|
+
|
|
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'
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const validateUsername = () =>
|
|
195
|
+
username.validate([validators.required(), validators.minLength(3), isAvailable])
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<template>
|
|
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>
|
|
206
|
+
</template>
|
|
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.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Starter Template
|
|
2
|
+
|
|
3
|
+
A complete, copy-pasteable minimal VibeUI app showing the recommended setup and patterns. Everything below runs as-is — no placeholders.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @velkymx/vibeui bootstrap
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Optional peer dependencies
|
|
12
|
+
|
|
13
|
+
Install these only if you use the components that need them:
|
|
14
|
+
|
|
15
|
+
- `bootstrap-icons` — required by `VibeIcon`.
|
|
16
|
+
- `quill` + `dompurify` — required by `VibeFormWysiwyg`.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install bootstrap-icons # for VibeIcon
|
|
20
|
+
npm install quill dompurify # for VibeFormWysiwyg
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## `main.ts`
|
|
24
|
+
|
|
25
|
+
Register the plugin, import Bootstrap's CSS, and restore the saved color-mode preference **before** mounting so the theme is correct on first paint.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createApp } from 'vue'
|
|
29
|
+
import VibeUI, { useColorMode } from '@velkymx/vibeui'
|
|
30
|
+
import 'bootstrap/dist/css/bootstrap.min.css'
|
|
31
|
+
import App from './App.vue'
|
|
32
|
+
|
|
33
|
+
// Restore the saved light/dark/auto preference and attach the OS-theme listener.
|
|
34
|
+
useColorMode().initColorMode()
|
|
35
|
+
|
|
36
|
+
createApp(App).use(VibeUI).mount('#app')
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **Do NOT import Bootstrap JS — VibeUI manages it.** Importing `bootstrap` (the JS bundle) yourself can create duplicate component instances and event conflicts. VibeUI loads and disposes Bootstrap's JS internally. You only import Bootstrap's **CSS**.
|
|
40
|
+
|
|
41
|
+
## `App.vue`
|
|
42
|
+
|
|
43
|
+
Mount `<VibeToastHost />` once at the root so `useToast()` works anywhere. The example below wires `useForm` validation into a `<VibeFormGroup>` + `<VibeFormInput>`, shows a confirmation in a `<VibeModal v-model>`, and fires a toast on submit.
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { ref } from 'vue'
|
|
48
|
+
import { useForm, useToast, validators } from '@velkymx/vibeui'
|
|
49
|
+
|
|
50
|
+
const toast = useToast()
|
|
51
|
+
|
|
52
|
+
const { fields, errors, validate, reset } = useForm({
|
|
53
|
+
name: '',
|
|
54
|
+
email: '',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const showConfirm = ref(false)
|
|
58
|
+
|
|
59
|
+
const onSubmit = async () => {
|
|
60
|
+
const result = await validate({
|
|
61
|
+
name: [validators.required('Name is required')],
|
|
62
|
+
email: [validators.required('Email is required'), validators.email()],
|
|
63
|
+
})
|
|
64
|
+
if (!result.valid) return
|
|
65
|
+
showConfirm.value = true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const confirm = () => {
|
|
69
|
+
showConfirm.value = false
|
|
70
|
+
toast.success(`Welcome, ${fields.name}!`)
|
|
71
|
+
reset()
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<VibeContainer class="py-4" style="max-width: 480px;">
|
|
77
|
+
<h1 class="mb-4">Sign up</h1>
|
|
78
|
+
|
|
79
|
+
<form @submit.prevent="onSubmit">
|
|
80
|
+
<VibeFormGroup label="Name">
|
|
81
|
+
<VibeFormInput
|
|
82
|
+
v-model="fields.name"
|
|
83
|
+
placeholder="Jane Doe"
|
|
84
|
+
:validation-state="errors.name ? 'invalid' : null"
|
|
85
|
+
:validation-message="errors.name"
|
|
86
|
+
/>
|
|
87
|
+
</VibeFormGroup>
|
|
88
|
+
|
|
89
|
+
<VibeFormGroup label="Email" class="mt-3">
|
|
90
|
+
<VibeFormInput
|
|
91
|
+
v-model="fields.email"
|
|
92
|
+
type="email"
|
|
93
|
+
placeholder="jane@example.com"
|
|
94
|
+
:validation-state="errors.email ? 'invalid' : null"
|
|
95
|
+
:validation-message="errors.email"
|
|
96
|
+
/>
|
|
97
|
+
</VibeFormGroup>
|
|
98
|
+
|
|
99
|
+
<VibeButton type="submit" variant="primary" class="mt-4">
|
|
100
|
+
Create account
|
|
101
|
+
</VibeButton>
|
|
102
|
+
</form>
|
|
103
|
+
|
|
104
|
+
<VibeModal v-model="showConfirm" title="Confirm details" centered>
|
|
105
|
+
<p>Create an account for <strong>{{ fields.email }}</strong>?</p>
|
|
106
|
+
<template #footer>
|
|
107
|
+
<VibeButton variant="secondary" @click="showConfirm = false">Cancel</VibeButton>
|
|
108
|
+
<VibeButton variant="primary" @click="confirm">Confirm</VibeButton>
|
|
109
|
+
</template>
|
|
110
|
+
</VibeModal>
|
|
111
|
+
|
|
112
|
+
<!-- Mount once at the app root; renders the global toast queue. -->
|
|
113
|
+
<VibeToastHost />
|
|
114
|
+
</VibeContainer>
|
|
115
|
+
</template>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> `isValid` is `false` until the first `validate()` call, so gate submit on the `validate()` result (as above) rather than disabling the button by `isValid` alone.
|
|
119
|
+
|
|
120
|
+
## Recommended project structure
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
src/
|
|
124
|
+
main.ts # createApp + plugin + initColorMode (before mount)
|
|
125
|
+
App.vue # app shell; mounts <VibeToastHost /> once
|
|
126
|
+
components/ # your reusable components
|
|
127
|
+
composables/ # app-specific composables (useForm wrappers, etc.)
|
|
128
|
+
views/ # route-level pages (if using vue-router)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Keep cross-app concerns at the root: `<VibeToastHost />` belongs in `App.vue`, and `initColorMode()` belongs in `main.ts` — both should appear exactly once.
|
|
132
|
+
|
|
133
|
+
## Coming from BootstrapVue?
|
|
134
|
+
|
|
135
|
+
The mental model is familiar — component-per-feature, data-driven `items` arrays, `v-model` for visibility/value, and props-based styling (`variant`, `size`) — but VibeUI is **not** a drop-in port. Component and prop names differ (`Vibe*` prefix), Bootstrap JS is managed for you, and interactive components lean on `v-model`. Use the [component docs](../README.md#components) to map features rather than expecting identical APIs.
|
|
136
|
+
```
|