@cocoar/vue-ui 3.1.0 → 3.2.0-beta.11
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/bin/cli.mjs +53 -0
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
- package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
- package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
- package/dist/components/data-list/data-list-builder.d.ts +288 -0
- package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
- package/dist/components/data-list/index.d.ts +12 -0
- package/dist/components/data-list/index.d.ts.map +1 -0
- package/dist/components/data-list/internal/compare.d.ts +14 -0
- package/dist/components/data-list/internal/compare.d.ts.map +1 -0
- package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
- package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
- package/dist/components/data-list/internal/search.d.ts +16 -0
- package/dist/components/data-list/internal/search.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
- package/dist/components/data-list/types.d.ts +185 -0
- package/dist/components/data-list/types.d.ts.map +1 -0
- package/dist/components/data-list/useDataListModel.d.ts +79 -0
- package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
- package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
- package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
- package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
- package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
- package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
- package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
- package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
- package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
- package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
- package/dist/composables/dragRegistry.d.ts +12 -0
- package/dist/composables/dragRegistry.d.ts.map +1 -1
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useDragDrop.d.ts +62 -6
- package/dist/composables/useDragDrop.d.ts.map +1 -1
- package/dist/composables/useVirtualList.d.ts +21 -1
- package/dist/composables/useVirtualList.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4508 -2039
- package/package.json +9 -3
- package/skills/cocoar-vue-ui/SKILL.md +244 -0
- package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
- package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
- package/skills/cocoar-vue-ui/references/components/button.md +252 -0
- package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +372 -0
- package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1647 -0
- package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
- package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
- package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +486 -0
- package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
- package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
- package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
- package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
- package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
- package/skills/cocoar-vue-ui/references/components/card.md +151 -0
- package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
- package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
- package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
- package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
- package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
- package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
- package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
- package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
- package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
- package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
- package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
- package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
- package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
- package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
- package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
- package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
- package/skills/cocoar-vue-ui/references/components/link.md +235 -0
- package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
- package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
- package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
- package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
- package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
- package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
- package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
- package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
- package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
- package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
- package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
- package/skills/cocoar-vue-ui/references/components/note.md +96 -0
- package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
- package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
- package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
- package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
- package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
- package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
- package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
- package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
- package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
- package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
- package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
- package/skills/cocoar-vue-ui/references/components/select.md +463 -0
- package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
- package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
- package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
- package/skills/cocoar-vue-ui/references/components/table.md +170 -0
- package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
- package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
- package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
- package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
- package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
- package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
- package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
- package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
- package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
- package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
- package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
- package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
- package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
- package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
- package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
- package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
- package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
- package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
- package/skills/cocoar-vue-ui/references/guide/getting-started.md +116 -0
- package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
- package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
- package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/form-field.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Form Field
|
|
4
|
+
|
|
5
|
+
A wrapper component that provides a label, hint text, and an inline error indicator around any form control. Instead of each input managing its own label and validation display, `CoarFormField` handles these concerns in one place.
|
|
6
|
+
|
|
7
|
+
> **Tip: Field-status indicator**
|
|
8
|
+
>
|
|
9
|
+
> `hint`, `warning`, and `error` all surface through one **status icon** in the label row, opening a popover that lists everything that applies. The icon picks the most severe state: error → red `circle-alert`, else warning → orange `triangle-alert`, else hint → grey `info`. When nothing is set, no icon renders.
|
|
10
|
+
>
|
|
11
|
+
> The icon is conditionally rendered — when it appears the label-text shifts right by `icon-width + gap`. That small horizontal nudge is the attention signal; the form's vertical geometry stays stable. Hover the icon for a peek, click to pin the popover open.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { CoarFormField } from '@cocoar/vue-ui';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
Wrap any form control in `CoarFormField` and pass `label`, `hint`, or `error` props. The label is automatically associated with the input inside via generated IDs.
|
|
20
|
+
|
|
21
|
+
**Demo — `form-field/demos/FormFieldBasic.vue`**
|
|
22
|
+
|
|
23
|
+
```vue
|
|
24
|
+
<template>
|
|
25
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 320px;">
|
|
26
|
+
<CoarFormField label="Full Name" hint="Enter your first and last name">
|
|
27
|
+
<CoarTextInput v-model="name" placeholder="Jane Doe" />
|
|
28
|
+
</CoarFormField>
|
|
29
|
+
<CoarFormField label="Email" error="Please enter a valid email address">
|
|
30
|
+
<CoarTextInput value="not-an-email" />
|
|
31
|
+
</CoarFormField>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { ref } from 'vue';
|
|
37
|
+
import { CoarTextInput, CoarFormField } from '@cocoar/vue-ui';
|
|
38
|
+
|
|
39
|
+
const name = ref('');
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Layout and Label Position
|
|
44
|
+
|
|
45
|
+
`CoarFormField` can place its label before or after any control, either stacked or inline. All four combinations have a visible effect. Inline layouts are especially useful when the field label is the only label of a checkbox or switch; leave the control's own `label` unset in that case.
|
|
46
|
+
|
|
47
|
+
**Demo — `form-field/demos/FormFieldLayouts.vue`**
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<template>
|
|
51
|
+
<div class="layout-demo">
|
|
52
|
+
<section>
|
|
53
|
+
<h4>Inline — label after</h4>
|
|
54
|
+
<CoarFormField
|
|
55
|
+
label="User active"
|
|
56
|
+
hint="Disabled users cannot sign in."
|
|
57
|
+
layout="inline"
|
|
58
|
+
label-position="after"
|
|
59
|
+
>
|
|
60
|
+
<CoarCheckbox v-model="active" />
|
|
61
|
+
</CoarFormField>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section>
|
|
65
|
+
<h4>Inline — label before</h4>
|
|
66
|
+
<CoarFormField
|
|
67
|
+
label="User active"
|
|
68
|
+
hint="Disabled users cannot sign in."
|
|
69
|
+
layout="inline"
|
|
70
|
+
label-position="before"
|
|
71
|
+
>
|
|
72
|
+
<CoarCheckbox v-model="active" />
|
|
73
|
+
</CoarFormField>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<section>
|
|
77
|
+
<h4>Text input — inline after</h4>
|
|
78
|
+
<CoarFormField
|
|
79
|
+
label="Name"
|
|
80
|
+
hint="This intentionally demonstrates that every prop combination has an effect."
|
|
81
|
+
layout="inline"
|
|
82
|
+
label-position="after"
|
|
83
|
+
>
|
|
84
|
+
<CoarTextInput v-model="name" placeholder="Ada Lovelace" />
|
|
85
|
+
</CoarFormField>
|
|
86
|
+
</section>
|
|
87
|
+
|
|
88
|
+
<section>
|
|
89
|
+
<h4>Text input — stacked after</h4>
|
|
90
|
+
<CoarFormField
|
|
91
|
+
label="Name"
|
|
92
|
+
hint="The label can also deliberately follow the control."
|
|
93
|
+
layout="stacked"
|
|
94
|
+
label-position="after"
|
|
95
|
+
>
|
|
96
|
+
<CoarTextInput v-model="name" placeholder="Ada Lovelace" />
|
|
97
|
+
</CoarFormField>
|
|
98
|
+
</section>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import { ref } from 'vue';
|
|
104
|
+
import { CoarCheckbox, CoarFormField, CoarTextInput } from '@cocoar/vue-ui';
|
|
105
|
+
|
|
106
|
+
const active = ref(false);
|
|
107
|
+
const name = ref('');
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<style scoped>
|
|
111
|
+
.layout-demo {
|
|
112
|
+
display: grid;
|
|
113
|
+
gap: 24px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.layout-demo section {
|
|
117
|
+
display: grid;
|
|
118
|
+
gap: 8px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.layout-demo h4 {
|
|
122
|
+
margin: 0;
|
|
123
|
+
color: var(--coar-text-neutral-secondary);
|
|
124
|
+
font-size: 12px;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Status Indicator
|
|
131
|
+
|
|
132
|
+
Toggle hint / warning / error individually and watch the icon shift severity and the popover stack content in priority order (hint → errors → warnings). Hover the icon, or click to pin.
|
|
133
|
+
|
|
134
|
+
**Demo — `form-field/demos/FormFieldStatusIndicator.vue`**
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<template>
|
|
138
|
+
<div class="si-demo">
|
|
139
|
+
<p class="si-demo-instructions">
|
|
140
|
+
Pick a combination — the icon updates per-severity (error wins → red,
|
|
141
|
+
else warning → orange, else hint → grey). Hover the icon for a peek,
|
|
142
|
+
click to pin the popover open. The popover always lists everything that
|
|
143
|
+
applies, in this order: hint, errors, warnings.
|
|
144
|
+
</p>
|
|
145
|
+
|
|
146
|
+
<div class="si-controls">
|
|
147
|
+
<CoarCheckbox v-model="withHint" label="Hint" />
|
|
148
|
+
<CoarCheckbox v-model="withWarning" label="Warning" />
|
|
149
|
+
<CoarCheckbox v-model="withMultiErrors" label="Errors (2)" />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="si-form">
|
|
153
|
+
<CoarFormField
|
|
154
|
+
label="Password"
|
|
155
|
+
:hint="withHint ? 'At least 8 characters, mixed case + a digit.' : ''"
|
|
156
|
+
:warning="withWarning ? 'This password is on the common-passwords list.' : ''"
|
|
157
|
+
:error="
|
|
158
|
+
withMultiErrors
|
|
159
|
+
? ['Too short (min 8 characters).', 'Needs an uppercase letter.']
|
|
160
|
+
: ''
|
|
161
|
+
"
|
|
162
|
+
required
|
|
163
|
+
>
|
|
164
|
+
<CoarTextInput v-model="value" placeholder="Enter a password…" />
|
|
165
|
+
</CoarFormField>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</template>
|
|
169
|
+
|
|
170
|
+
<script setup lang="ts">
|
|
171
|
+
import { ref } from 'vue';
|
|
172
|
+
import { CoarCheckbox, CoarFormField, CoarTextInput } from '@cocoar/vue-ui';
|
|
173
|
+
|
|
174
|
+
const value = ref('');
|
|
175
|
+
const withHint = ref(true);
|
|
176
|
+
const withWarning = ref(false);
|
|
177
|
+
const withMultiErrors = ref(false);
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<style scoped>
|
|
181
|
+
.si-demo {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
gap: 16px;
|
|
185
|
+
max-width: 480px;
|
|
186
|
+
}
|
|
187
|
+
.si-demo-instructions {
|
|
188
|
+
margin: 0;
|
|
189
|
+
font-size: 13px;
|
|
190
|
+
color: var(--coar-text-neutral-secondary);
|
|
191
|
+
}
|
|
192
|
+
.si-controls {
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-wrap: wrap;
|
|
195
|
+
gap: 12px;
|
|
196
|
+
padding: 12px;
|
|
197
|
+
background: var(--coar-bg-neutral-secondary);
|
|
198
|
+
border-radius: 6px;
|
|
199
|
+
}
|
|
200
|
+
.si-form {
|
|
201
|
+
padding: 12px 0;
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Live Rules
|
|
207
|
+
|
|
208
|
+
Pass a `rules` array for live-evaluated validation. Each rule has a `label`, a `fulfilled: boolean` (the consumer computes this from reactive state — Vue re-evaluates on every state change), and optional `whenPass` / `whenFail` flags that control what to render in each state. **Defaults**: `whenPass: 'success'` and `whenFail: 'pending'` — that's the password-checklist UX (✓ green when fulfilled, ○ grey when not), so a bare `{ label, fulfilled }` rule is the common case.
|
|
209
|
+
|
|
210
|
+
Import the named types for IntelliSense:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import type {
|
|
214
|
+
CoarFormFieldRule, // the rule object
|
|
215
|
+
CoarFormFieldRulePassMode, // 'success' | 'hide'
|
|
216
|
+
CoarFormFieldRuleFailMode, // 'pending' | 'warning' | 'error' | 'hide'
|
|
217
|
+
} from '@cocoar/vue-ui';
|
|
218
|
+
|
|
219
|
+
const rules: CoarFormFieldRule[] = [...];
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Demo — `form-field/demos/FormFieldPasswordRules.vue`**
|
|
223
|
+
|
|
224
|
+
```vue
|
|
225
|
+
<template>
|
|
226
|
+
<div class="pwr-demo">
|
|
227
|
+
<p class="pwr-instructions">
|
|
228
|
+
Three rule patterns coexist below. <strong>Password</strong> uses
|
|
229
|
+
the default checklist style (✓ green when fulfilled, ○ grey when
|
|
230
|
+
not, popover icon walks grey → orange → green). <strong>Confirm
|
|
231
|
+
password</strong> is a single match-rule with the same defaults.
|
|
232
|
+
<strong>Display name</strong> shows the live-error pattern
|
|
233
|
+
(<code>whenPass: 'hide'</code>, <code>whenFail: 'error'</code>) —
|
|
234
|
+
type past 20 characters and the indicator flips red immediately,
|
|
235
|
+
delete and it vanishes. No icon when within the limit; live
|
|
236
|
+
validation is the kind of rule that should disappear when fine.
|
|
237
|
+
</p>
|
|
238
|
+
|
|
239
|
+
<div class="pwr-form">
|
|
240
|
+
<CoarFormField
|
|
241
|
+
label="Password"
|
|
242
|
+
:rules="passwordRules"
|
|
243
|
+
required
|
|
244
|
+
>
|
|
245
|
+
<CoarPasswordInput v-model="password" placeholder="Enter a password…" />
|
|
246
|
+
</CoarFormField>
|
|
247
|
+
|
|
248
|
+
<CoarFormField
|
|
249
|
+
label="Confirm password"
|
|
250
|
+
:rules="confirmRules"
|
|
251
|
+
required
|
|
252
|
+
>
|
|
253
|
+
<CoarPasswordInput v-model="confirm" placeholder="Repeat the password…" />
|
|
254
|
+
</CoarFormField>
|
|
255
|
+
|
|
256
|
+
<CoarFormField label="Display name" :rules="displayNameRules">
|
|
257
|
+
<CoarTextInput v-model="displayName" placeholder="Max 20 characters…" />
|
|
258
|
+
</CoarFormField>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</template>
|
|
262
|
+
|
|
263
|
+
<script setup lang="ts">
|
|
264
|
+
import { computed, ref } from 'vue';
|
|
265
|
+
import {
|
|
266
|
+
CoarFormField,
|
|
267
|
+
CoarPasswordInput,
|
|
268
|
+
CoarTextInput,
|
|
269
|
+
type CoarFormFieldRule,
|
|
270
|
+
} from '@cocoar/vue-ui';
|
|
271
|
+
|
|
272
|
+
const password = ref('');
|
|
273
|
+
const confirm = ref('');
|
|
274
|
+
const displayName = ref('');
|
|
275
|
+
|
|
276
|
+
// Defaults: `whenPass: 'success'`, `whenFail: 'pending'` → password-checklist
|
|
277
|
+
// style. No need to spell them out.
|
|
278
|
+
const passwordRules = computed<CoarFormFieldRule[]>(() => [
|
|
279
|
+
{ label: 'At least 8 characters', fulfilled: password.value.length >= 8 },
|
|
280
|
+
{ label: 'Contains an uppercase letter', fulfilled: /[A-Z]/.test(password.value) },
|
|
281
|
+
{ label: 'Contains a lowercase letter', fulfilled: /[a-z]/.test(password.value) },
|
|
282
|
+
{ label: 'Contains a digit', fulfilled: /\d/.test(password.value) },
|
|
283
|
+
{ label: 'Contains a symbol', fulfilled: /[^A-Za-z0-9]/.test(password.value) },
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
// Confirm-password match is a HARD requirement — the field is invalid until
|
|
287
|
+
// the match holds. `whenFail: 'error'` drives `aria-invalid="true"` and
|
|
288
|
+
// `hasError` on the field so a Save button bound to that signal can stay
|
|
289
|
+
// disabled. The popover shows the rule in the Errors section while broken,
|
|
290
|
+
// and (because `whenPass` defaults to `'success'`) flips to a green ✓ in
|
|
291
|
+
// the checklist once the passwords match.
|
|
292
|
+
const confirmRules = computed<CoarFormFieldRule[]>(() => [
|
|
293
|
+
{
|
|
294
|
+
label: 'Matches the password',
|
|
295
|
+
fulfilled: confirm.value === password.value && confirm.value.length > 0,
|
|
296
|
+
whenFail: 'error',
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
// Live-validation pattern: whenPass=hide, whenFail=error. The rule vanishes
|
|
301
|
+
// when the value is fine and flips to a red error indicator the moment the
|
|
302
|
+
// user types past the limit. Aria-invalid + error border are driven
|
|
303
|
+
// automatically; consumer doesn't have to track an `error` string.
|
|
304
|
+
// Importing `CoarFormFieldRule` gives IntelliSense on `whenPass`/`whenFail`.
|
|
305
|
+
const displayNameRules = computed<CoarFormFieldRule[]>(() => [
|
|
306
|
+
{
|
|
307
|
+
label: 'Max 20 characters',
|
|
308
|
+
fulfilled: displayName.value.length <= 20,
|
|
309
|
+
whenPass: 'hide',
|
|
310
|
+
whenFail: 'error',
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
<style scoped>
|
|
316
|
+
.pwr-demo {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
gap: 16px;
|
|
320
|
+
max-width: 460px;
|
|
321
|
+
}
|
|
322
|
+
.pwr-instructions {
|
|
323
|
+
margin: 0;
|
|
324
|
+
font-size: 13px;
|
|
325
|
+
color: var(--coar-text-neutral-secondary);
|
|
326
|
+
}
|
|
327
|
+
.pwr-form {
|
|
328
|
+
display: flex;
|
|
329
|
+
flex-direction: column;
|
|
330
|
+
gap: 16px;
|
|
331
|
+
}
|
|
332
|
+
</style>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Display modes
|
|
336
|
+
|
|
337
|
+
The two-axis `whenPass` / `whenFail` covers every common pattern:
|
|
338
|
+
|
|
339
|
+
| `whenPass` | `whenFail` | Pattern | Example |
|
|
340
|
+
|---|---|---|---|
|
|
341
|
+
| `'success'` (default) | `'pending'` (default) | Progress checklist | Password policies — ✓ green when met, ○ grey when not |
|
|
342
|
+
| `'hide'` | `'error'` | Live validation | "Max 20 chars" — disappears when ok, red error when not |
|
|
343
|
+
| `'hide'` | `'warning'` | Live advisory | "Looks like a tracking link" — disappears when fine, orange warning when not |
|
|
344
|
+
| `'success'` | `'error'` | Required with progress tick | Hard requirement that's also part of a checklist |
|
|
345
|
+
|
|
346
|
+
The defaults (`whenPass: 'success'`, `whenFail: 'pending'`) give you the password-checklist pattern with zero extra config.
|
|
347
|
+
|
|
348
|
+
### Trigger-icon severity
|
|
349
|
+
|
|
350
|
+
The icon reflects **what's visible in the popover** — pick the highest-severity section that has at least one entry:
|
|
351
|
+
|
|
352
|
+
1. Popover has ≥1 entry in the **Errors** section → red `circle-alert`
|
|
353
|
+
2. else popover has ≥1 entry in the **Warnings** section → orange `triangle-alert`
|
|
354
|
+
3. else popover has ≥1 **success** item (a fulfilled `whenPass: 'success'` rule, i.e. a green ✓) → green `check-circle-2`
|
|
355
|
+
4. else popover has ≥1 **pending** checklist item or a hint → grey `info`
|
|
356
|
+
5. else → no icon at all
|
|
357
|
+
|
|
358
|
+
Notice that **success wins over pending** — once the user has fulfilled *any* rule, the icon flips green for positive reinforcement. The unfulfilled rules still appear as ○ in the popover so the user can hover to see the "could do more" detail; the icon just doesn't shout orange about them.
|
|
359
|
+
|
|
360
|
+
For genuinely **required** rules (must-do-this), use `whenFail: 'error'` — those make the field invalid and the icon stays red until satisfied. Pending is for **optional** progress (part of an X-of-Y, polish-up rules, or any "could do more for strength but already valid" pattern). The Save-button-disabled state binds to `hasError` (or the `aria-invalid` attribute on the input), not to the icon color.
|
|
361
|
+
|
|
362
|
+
### Why no green check for `whenPass: 'hide'` rules
|
|
363
|
+
|
|
364
|
+
A rule whose natural state is "no problem" shouldn't show a green checkmark when satisfied — there's nothing to celebrate; the field is just fine. The trigger icon stays unset unless something is actively wrong or you have explicit `whenPass: 'success'` rules to show progress.
|
|
365
|
+
|
|
366
|
+
### Icon ≠ Validity — composing rules for "Save disabled" UX
|
|
367
|
+
|
|
368
|
+
**The icon shows visual state. `hasError` (via the `FORM_FIELD_INJECTION_KEY`) shows validity.** They're related but deliberately decoupled — `whenFail: 'pending'` and `whenFail: 'warning'` rules contribute to the icon but **not** to `hasError`. Only `whenFail: 'error'` rules (plus the `error` string-prop) make the field invalid.
|
|
369
|
+
|
|
370
|
+
That decoupling is intentional. It lets you express more than "every rule is mandatory":
|
|
371
|
+
|
|
372
|
+
#### Pattern: each rule individually required
|
|
373
|
+
|
|
374
|
+
Each rule both shows progress AND drives validity. `whenFail: 'error'` on every entry — the rule appears in the popover's Errors section while unfulfilled, flips to a green ✓ in the checklist when fulfilled.
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
const rules = computed<CoarFormFieldRule[]>(() => [
|
|
378
|
+
{ label: 'At least 8 chars', fulfilled: pw.length >= 8, whenFail: 'error' },
|
|
379
|
+
{ label: 'Contains an uppercase letter', fulfilled: /[A-Z]/.test(pw), whenFail: 'error' },
|
|
380
|
+
// …
|
|
381
|
+
]);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### Pattern: X of Y must be satisfied
|
|
385
|
+
|
|
386
|
+
The 4 individual rules stay as `pending` progress (default), and a 5th **aggregate rule** with `whenFail: 'error'` checks the count. Both coexist in the popover — the user sees per-rule progress AND why the field is currently invalid. The Save button binds to the aggregate's `fulfilled` flag (or to the field's injected `hasError`), not to the icon.
|
|
387
|
+
|
|
388
|
+
**Demo — `form-field/demos/FormFieldXofYRules.vue`**
|
|
389
|
+
|
|
390
|
+
```vue
|
|
391
|
+
<template>
|
|
392
|
+
<div class="xy-demo">
|
|
393
|
+
<p class="xy-instructions">
|
|
394
|
+
"Pick at least <strong>3 of the 4</strong> character types" — the four
|
|
395
|
+
individual rules stay as visual progress (✓ green / ○ grey, default
|
|
396
|
+
<code>whenFail: 'pending'</code>), and a fifth aggregate rule with
|
|
397
|
+
<code>whenFail: 'error'</code> guards validity. Save stays disabled
|
|
398
|
+
until the aggregate is satisfied — bound to the rule's
|
|
399
|
+
<code>fulfilled</code> flag, not the icon. Hover the icon to see both
|
|
400
|
+
the progress and the gating-error in one popover.
|
|
401
|
+
</p>
|
|
402
|
+
|
|
403
|
+
<div class="xy-form">
|
|
404
|
+
<CoarFormField label="Password" :rules="rules" required>
|
|
405
|
+
<CoarPasswordInput v-model="password" placeholder="Mix character types…" />
|
|
406
|
+
</CoarFormField>
|
|
407
|
+
|
|
408
|
+
<div class="xy-actions">
|
|
409
|
+
<span class="xy-progress">
|
|
410
|
+
{{ metCount }} of 4 character types
|
|
411
|
+
</span>
|
|
412
|
+
<CoarButton variant="primary" :disabled="!aggregateMet" @click="onSave">
|
|
413
|
+
{{ saved ? 'Saved!' : 'Save' }}
|
|
414
|
+
</CoarButton>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</template>
|
|
419
|
+
|
|
420
|
+
<script setup lang="ts">
|
|
421
|
+
import { computed, ref } from 'vue';
|
|
422
|
+
import {
|
|
423
|
+
CoarButton,
|
|
424
|
+
CoarFormField,
|
|
425
|
+
CoarPasswordInput,
|
|
426
|
+
type CoarFormFieldRule,
|
|
427
|
+
} from '@cocoar/vue-ui';
|
|
428
|
+
|
|
429
|
+
const password = ref('');
|
|
430
|
+
const saved = ref(false);
|
|
431
|
+
|
|
432
|
+
// 4 individual character-class rules — pure progress visual (defaults).
|
|
433
|
+
// They show as a checklist; they don't make the field invalid on their own.
|
|
434
|
+
const individualRules = computed<CoarFormFieldRule[]>(() => [
|
|
435
|
+
{ label: 'Contains an uppercase letter', fulfilled: /[A-Z]/.test(password.value) },
|
|
436
|
+
{ label: 'Contains a lowercase letter', fulfilled: /[a-z]/.test(password.value) },
|
|
437
|
+
{ label: 'Contains a digit', fulfilled: /\d/.test(password.value) },
|
|
438
|
+
{ label: 'Contains a symbol', fulfilled: /[^A-Za-z0-9]/.test(password.value) },
|
|
439
|
+
]);
|
|
440
|
+
|
|
441
|
+
const metCount = computed(() => individualRules.value.filter((r) => r.fulfilled).length);
|
|
442
|
+
const aggregateMet = computed(() => metCount.value >= 3);
|
|
443
|
+
|
|
444
|
+
// Combine: 4 progress rules + 1 aggregate-error rule. The aggregate is the
|
|
445
|
+
// validity gate — `whenFail: 'error'` makes the field invalid until the
|
|
446
|
+
// count threshold is reached, and the rule shows as a red error in the
|
|
447
|
+
// popover's Errors section while broken. `whenPass: 'hide'` keeps the
|
|
448
|
+
// aggregate from showing as a green ✓ when satisfied (it's a meta-rule
|
|
449
|
+
// about the OTHER rules, not a separate thing the user achieved).
|
|
450
|
+
//
|
|
451
|
+
// The `password.length === 0` clause keeps the aggregate silent on initial
|
|
452
|
+
// empty input — the field has rules but the user hasn't started yet, so
|
|
453
|
+
// don't shout an error. The icon falls back to the grey "click me, there
|
|
454
|
+
// are rules" hint state; once the user types one char, the aggregate
|
|
455
|
+
// evaluates for real.
|
|
456
|
+
const rules = computed<CoarFormFieldRule[]>(() => [
|
|
457
|
+
...individualRules.value,
|
|
458
|
+
{
|
|
459
|
+
label: `At least 3 of the 4 character types (currently ${metCount.value})`,
|
|
460
|
+
fulfilled: aggregateMet.value || password.value.length === 0,
|
|
461
|
+
whenPass: 'hide',
|
|
462
|
+
whenFail: 'error',
|
|
463
|
+
},
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
function onSave() {
|
|
467
|
+
saved.value = true;
|
|
468
|
+
setTimeout(() => (saved.value = false), 1500);
|
|
469
|
+
}
|
|
470
|
+
</script>
|
|
471
|
+
|
|
472
|
+
<style scoped>
|
|
473
|
+
.xy-demo {
|
|
474
|
+
display: flex;
|
|
475
|
+
flex-direction: column;
|
|
476
|
+
gap: 16px;
|
|
477
|
+
max-width: 460px;
|
|
478
|
+
}
|
|
479
|
+
.xy-instructions {
|
|
480
|
+
margin: 0;
|
|
481
|
+
font-size: 13px;
|
|
482
|
+
color: var(--coar-text-neutral-secondary);
|
|
483
|
+
}
|
|
484
|
+
.xy-instructions code {
|
|
485
|
+
background: var(--coar-bg-neutral-tertiary);
|
|
486
|
+
padding: 1px 4px;
|
|
487
|
+
border-radius: 3px;
|
|
488
|
+
font-size: 12px;
|
|
489
|
+
}
|
|
490
|
+
.xy-form {
|
|
491
|
+
display: flex;
|
|
492
|
+
flex-direction: column;
|
|
493
|
+
gap: 16px;
|
|
494
|
+
}
|
|
495
|
+
.xy-actions {
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
justify-content: space-between;
|
|
499
|
+
gap: 12px;
|
|
500
|
+
}
|
|
501
|
+
.xy-progress {
|
|
502
|
+
font-size: 13px;
|
|
503
|
+
color: var(--coar-text-neutral-secondary);
|
|
504
|
+
}
|
|
505
|
+
</style>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The same pattern handles any aggregate constraint: "at least 2 tags selected", "between 5 and 50 items", "either email OR phone filled" — one extra rule with `whenFail: 'error'` does the gating.
|
|
509
|
+
|
|
510
|
+
> **Tip: Why the icon goes green at 3/4 (not orange)**
|
|
511
|
+
>
|
|
512
|
+
> The severity model picks the highest-severity item visible in the popover. With 3 ✓ and 1 ○ showing, success wins over pending — the icon goes green. The unfulfilled rule stays as ○ in the popover so the user can hover to see "could do more for strength", but the summary icon respects that validity is passing. See [Trigger-icon severity](#trigger-icon-severity) for the full priority order.
|
|
513
|
+
|
|
514
|
+
## On-Submit Validation
|
|
515
|
+
|
|
516
|
+
The canonical on-submit-validation pattern: every required field starts clean, the user fills what they like, hits **Submit**, and any missing / invalid fields flip to the error state. The error icon appears in the label row, shifting the label-text right by `icon-width + gap` — a small horizontal nudge that catches the eye **without** any vertical row push that would move the Submit button or rearrange the form.
|
|
517
|
+
|
|
518
|
+
**Demo — `form-field/demos/FormFieldZeroShift.vue`**
|
|
519
|
+
|
|
520
|
+
```vue
|
|
521
|
+
<template>
|
|
522
|
+
<div class="zs-demo">
|
|
523
|
+
<p class="zs-demo-instructions">
|
|
524
|
+
Click <strong>Submit</strong> with empty fields — every required field flips
|
|
525
|
+
to the error state at the same time. The labels shift right by the icon
|
|
526
|
+
width: a small horizontal nudge that catches the eye, while the form's
|
|
527
|
+
vertical layout stays put (no row appears below the input, the Submit
|
|
528
|
+
button doesn't move). Hover any error icon for the message.
|
|
529
|
+
</p>
|
|
530
|
+
|
|
531
|
+
<div class="zs-form">
|
|
532
|
+
<CoarFormField label="First name" :error="errors.first" required>
|
|
533
|
+
<CoarTextInput v-model="first" placeholder="Jane" />
|
|
534
|
+
</CoarFormField>
|
|
535
|
+
<CoarFormField label="Last name" :error="errors.last" required>
|
|
536
|
+
<CoarTextInput v-model="last" placeholder="Doe" />
|
|
537
|
+
</CoarFormField>
|
|
538
|
+
<CoarFormField label="Email" :error="errors.email" required>
|
|
539
|
+
<CoarTextInput v-model="email" placeholder="jane@example.com" />
|
|
540
|
+
</CoarFormField>
|
|
541
|
+
<CoarFormField label="Account type" :error="errors.kind" required>
|
|
542
|
+
<CoarSelect v-model="kind" :options="kinds" placeholder="Pick one" />
|
|
543
|
+
</CoarFormField>
|
|
544
|
+
<CoarFormField label="Country" :error="errors.country" required>
|
|
545
|
+
<CoarSelect v-model="country" :options="countries" placeholder="Pick one" />
|
|
546
|
+
</CoarFormField>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
<div class="zs-actions">
|
|
550
|
+
<CoarButton @click="reset">Reset</CoarButton>
|
|
551
|
+
<CoarButton variant="primary" @click="submit">
|
|
552
|
+
Submit{{ errorCount > 0 ? ` (${errorCount})` : '' }}
|
|
553
|
+
</CoarButton>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</template>
|
|
557
|
+
|
|
558
|
+
<script setup lang="ts">
|
|
559
|
+
import { computed, reactive, ref } from 'vue';
|
|
560
|
+
import {
|
|
561
|
+
CoarButton,
|
|
562
|
+
CoarFormField,
|
|
563
|
+
CoarSelect,
|
|
564
|
+
CoarTextInput,
|
|
565
|
+
type CoarSelectOption,
|
|
566
|
+
} from '@cocoar/vue-ui';
|
|
567
|
+
|
|
568
|
+
const first = ref('');
|
|
569
|
+
const last = ref('');
|
|
570
|
+
const email = ref('');
|
|
571
|
+
const kind = ref<string | null>(null);
|
|
572
|
+
const country = ref<string | null>(null);
|
|
573
|
+
const attempted = ref(false);
|
|
574
|
+
|
|
575
|
+
const kinds: CoarSelectOption<string>[] = [
|
|
576
|
+
{ value: 'personal', label: 'Personal' },
|
|
577
|
+
{ value: 'business', label: 'Business' },
|
|
578
|
+
];
|
|
579
|
+
const countries: CoarSelectOption<string>[] = [
|
|
580
|
+
{ value: 'us', label: 'United States' },
|
|
581
|
+
{ value: 'de', label: 'Germany' },
|
|
582
|
+
{ value: 'at', label: 'Austria' },
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
// Errors only populate after the first submit attempt — until then the form
|
|
586
|
+
// is "clean" even with empty fields. After the first submit, errors update
|
|
587
|
+
// reactively as the user types (live-revalidate). This is the most common
|
|
588
|
+
// on-submit pattern; CoarFormField doesn't care about the timing, it just
|
|
589
|
+
// reflects whatever string you pass.
|
|
590
|
+
const errors = reactive({
|
|
591
|
+
first: '',
|
|
592
|
+
last: '',
|
|
593
|
+
email: '',
|
|
594
|
+
kind: '',
|
|
595
|
+
country: '',
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const errorCount = computed(
|
|
599
|
+
() => Object.values(errors).filter((m) => m.length > 0).length,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
function validate() {
|
|
603
|
+
errors.first = first.value.trim() ? '' : 'First name is required.';
|
|
604
|
+
errors.last = last.value.trim() ? '' : 'Last name is required.';
|
|
605
|
+
errors.email = email.value.trim()
|
|
606
|
+
? /.+@.+\..+/.test(email.value)
|
|
607
|
+
? ''
|
|
608
|
+
: 'Enter a valid email address.'
|
|
609
|
+
: 'Email is required.';
|
|
610
|
+
errors.kind = kind.value ? '' : 'Pick an account type.';
|
|
611
|
+
errors.country = country.value ? '' : 'Pick a country.';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function submit() {
|
|
615
|
+
attempted.value = true;
|
|
616
|
+
validate();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function reset() {
|
|
620
|
+
first.value = '';
|
|
621
|
+
last.value = '';
|
|
622
|
+
email.value = '';
|
|
623
|
+
kind.value = null;
|
|
624
|
+
country.value = null;
|
|
625
|
+
attempted.value = false;
|
|
626
|
+
Object.assign(errors, { first: '', last: '', email: '', kind: '', country: '' });
|
|
627
|
+
}
|
|
628
|
+
</script>
|
|
629
|
+
|
|
630
|
+
<style scoped>
|
|
631
|
+
.zs-demo {
|
|
632
|
+
display: flex;
|
|
633
|
+
flex-direction: column;
|
|
634
|
+
gap: 16px;
|
|
635
|
+
}
|
|
636
|
+
.zs-demo-instructions {
|
|
637
|
+
margin: 0;
|
|
638
|
+
font-size: 13px;
|
|
639
|
+
color: var(--coar-text-neutral-secondary);
|
|
640
|
+
}
|
|
641
|
+
.zs-form {
|
|
642
|
+
display: grid;
|
|
643
|
+
grid-template-columns: 1fr 1fr;
|
|
644
|
+
gap: 16px;
|
|
645
|
+
max-width: 560px;
|
|
646
|
+
}
|
|
647
|
+
.zs-actions {
|
|
648
|
+
display: flex;
|
|
649
|
+
gap: 8px;
|
|
650
|
+
justify-content: flex-end;
|
|
651
|
+
max-width: 560px;
|
|
652
|
+
}
|
|
653
|
+
</style>
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
The shift is the point. A reserved-slot approach (icon hidden via `visibility` to keep the label at a fixed offset) would be visually quieter but would also lose the cue — users wouldn't notice the state change. The icon-in-label-with-shift pattern strikes the balance: noticeable, but never disorienting.
|
|
657
|
+
|
|
658
|
+
## Grouping Controls
|
|
659
|
+
|
|
660
|
+
Use `CoarFormField` to add a group label and shared error to a set of checkboxes or radio buttons. Each checkbox keeps its own inline `label` prop for the option text.
|
|
661
|
+
|
|
662
|
+
**Demo — `form-field/demos/FormFieldCheckboxGroup.vue`**
|
|
663
|
+
|
|
664
|
+
```vue
|
|
665
|
+
<template>
|
|
666
|
+
<div style="max-width: 400px">
|
|
667
|
+
<CoarFormField
|
|
668
|
+
label="Permissions"
|
|
669
|
+
:error="permissionsError"
|
|
670
|
+
hint="Select at least one permission"
|
|
671
|
+
required
|
|
672
|
+
>
|
|
673
|
+
<CoarCheckboxGroup v-model="permissions">
|
|
674
|
+
<CoarCheckbox value="read" label="Read — View content" />
|
|
675
|
+
<CoarCheckbox value="write" label="Write — Create and edit content" />
|
|
676
|
+
<CoarCheckbox value="admin" label="Admin — Manage users and settings" />
|
|
677
|
+
</CoarCheckboxGroup>
|
|
678
|
+
</CoarFormField>
|
|
679
|
+
</div>
|
|
680
|
+
</template>
|
|
681
|
+
|
|
682
|
+
<script setup lang="ts">
|
|
683
|
+
import { ref, computed } from 'vue';
|
|
684
|
+
import { CoarCheckbox, CoarCheckboxGroup, CoarFormField } from '@cocoar/vue-ui';
|
|
685
|
+
|
|
686
|
+
const permissions = ref<string[]>([]);
|
|
687
|
+
|
|
688
|
+
const permissionsError = computed(() => {
|
|
689
|
+
if (permissions.value.length === 0) {
|
|
690
|
+
return 'At least one permission is required';
|
|
691
|
+
}
|
|
692
|
+
return '';
|
|
693
|
+
});
|
|
694
|
+
</script>
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## Registration Form
|
|
698
|
+
|
|
699
|
+
A complete registration form with submit-on-click validation. Errors only appear after the user attempts to submit.
|
|
700
|
+
|
|
701
|
+
**Demo — `form-field/demos/FormRegistration.vue`**
|
|
702
|
+
|
|
703
|
+
```vue
|
|
704
|
+
<template>
|
|
705
|
+
<form class="demo-form" @submit.prevent="onSubmit">
|
|
706
|
+
<CoarFormField label="Full Name" :error="errors.name" required>
|
|
707
|
+
<CoarTextInput v-model="form.name" placeholder="Jane Doe" required />
|
|
708
|
+
</CoarFormField>
|
|
709
|
+
|
|
710
|
+
<CoarFormField label="Email" :error="errors.email" hint="We'll never share your email" required>
|
|
711
|
+
<CoarTextInput v-model="form.email" placeholder="jane@example.com" required />
|
|
712
|
+
</CoarFormField>
|
|
713
|
+
|
|
714
|
+
<CoarFormField label="Password" :error="errors.password" hint="At least 8 characters" required>
|
|
715
|
+
<CoarPasswordInput v-model="form.password" required />
|
|
716
|
+
</CoarFormField>
|
|
717
|
+
|
|
718
|
+
<CoarFormField label="Role" :error="errors.role" required>
|
|
719
|
+
<CoarSelect v-model="form.role" :options="roles" placeholder="Select a role..." />
|
|
720
|
+
</CoarFormField>
|
|
721
|
+
|
|
722
|
+
<CoarFormField label="Department">
|
|
723
|
+
<CoarSelect v-model="form.department" :options="departments" placeholder="Optional..." />
|
|
724
|
+
</CoarFormField>
|
|
725
|
+
|
|
726
|
+
<CoarFormField :error="errors.terms">
|
|
727
|
+
<CoarCheckbox v-model="form.terms" label="I agree to the terms and conditions" />
|
|
728
|
+
</CoarFormField>
|
|
729
|
+
|
|
730
|
+
<div class="demo-form__actions">
|
|
731
|
+
<CoarButton type="submit" :disabled="!isValid">Create Account</CoarButton>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<CoarNote v-if="submitted" variant="success" padding="s">
|
|
735
|
+
Account created successfully!
|
|
736
|
+
</CoarNote>
|
|
737
|
+
</form>
|
|
738
|
+
</template>
|
|
739
|
+
|
|
740
|
+
<script setup lang="ts">
|
|
741
|
+
import { reactive, ref, computed } from 'vue';
|
|
742
|
+
import { CoarFormField, CoarTextInput, CoarPasswordInput, CoarSelect, CoarCheckbox, CoarButton, CoarNote } from '@cocoar/vue-ui';
|
|
743
|
+
import type { CoarSelectOption } from '@cocoar/vue-ui';
|
|
744
|
+
|
|
745
|
+
const form = reactive({
|
|
746
|
+
name: '',
|
|
747
|
+
email: '',
|
|
748
|
+
password: '',
|
|
749
|
+
role: null as string | null,
|
|
750
|
+
department: null as string | null,
|
|
751
|
+
terms: false,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const submitted = ref(false);
|
|
755
|
+
const touched = ref(false);
|
|
756
|
+
|
|
757
|
+
const roles: CoarSelectOption<string>[] = [
|
|
758
|
+
{ value: 'developer', label: 'Developer' },
|
|
759
|
+
{ value: 'designer', label: 'Designer' },
|
|
760
|
+
{ value: 'manager', label: 'Manager' },
|
|
761
|
+
{ value: 'qa', label: 'QA Engineer' },
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
const departments: CoarSelectOption<string>[] = [
|
|
765
|
+
{ value: 'engineering', label: 'Engineering' },
|
|
766
|
+
{ value: 'product', label: 'Product' },
|
|
767
|
+
{ value: 'marketing', label: 'Marketing' },
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
const errors = computed(() => {
|
|
771
|
+
if (!touched.value) return { name: '', email: '', password: '', role: '', terms: '' };
|
|
772
|
+
return {
|
|
773
|
+
name: form.name.length === 0 ? 'Name is required' : '',
|
|
774
|
+
email: form.email.length === 0 ? 'Email is required' : !form.email.includes('@') ? 'Enter a valid email' : '',
|
|
775
|
+
password: form.password.length === 0 ? 'Password is required' : form.password.length < 8 ? 'At least 8 characters' : '',
|
|
776
|
+
role: !form.role ? 'Please select a role' : '',
|
|
777
|
+
terms: !form.terms ? 'You must accept the terms' : '',
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const isValid = computed(() =>
|
|
782
|
+
form.name.length > 0 &&
|
|
783
|
+
form.email.includes('@') &&
|
|
784
|
+
form.password.length >= 8 &&
|
|
785
|
+
form.role !== null &&
|
|
786
|
+
form.terms
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
function onSubmit() {
|
|
790
|
+
touched.value = true;
|
|
791
|
+
if (isValid.value) {
|
|
792
|
+
submitted.value = true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
</script>
|
|
796
|
+
|
|
797
|
+
<style scoped>
|
|
798
|
+
.demo-form {
|
|
799
|
+
display: flex;
|
|
800
|
+
flex-direction: column;
|
|
801
|
+
gap: var(--coar-spacing-m);
|
|
802
|
+
max-width: 400px;
|
|
803
|
+
}
|
|
804
|
+
.demo-form__actions {
|
|
805
|
+
padding-top: var(--coar-spacing-s);
|
|
806
|
+
}
|
|
807
|
+
</style>
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
## Settings Panel
|
|
811
|
+
|
|
812
|
+
A settings page using every form control type — text inputs, textareas, selects, checkboxes, radio groups, and switches — all wrapped in `CoarFormField` for consistent layout.
|
|
813
|
+
|
|
814
|
+
**Demo — `form-field/demos/FormSettings.vue`**
|
|
815
|
+
|
|
816
|
+
```vue
|
|
817
|
+
<template>
|
|
818
|
+
<form class="demo-form" @submit.prevent>
|
|
819
|
+
<CoarFormField label="Display Name" hint="This is shown publicly">
|
|
820
|
+
<CoarTextInput v-model="displayName" placeholder="Your display name" />
|
|
821
|
+
</CoarFormField>
|
|
822
|
+
|
|
823
|
+
<CoarFormField label="Bio">
|
|
824
|
+
<CoarTextInput v-model="bio" placeholder="Tell us about yourself..." :rows="3" />
|
|
825
|
+
</CoarFormField>
|
|
826
|
+
|
|
827
|
+
<CoarFormField label="Language">
|
|
828
|
+
<CoarSelect v-model="language" :options="languages" />
|
|
829
|
+
</CoarFormField>
|
|
830
|
+
|
|
831
|
+
<CoarFormField label="Notifications">
|
|
832
|
+
<div class="demo-checks">
|
|
833
|
+
<CoarCheckbox v-model="emailNotifs" label="Email notifications" />
|
|
834
|
+
<CoarCheckbox v-model="pushNotifs" label="Push notifications" />
|
|
835
|
+
<CoarCheckbox v-model="weeklyDigest" label="Weekly digest" />
|
|
836
|
+
</div>
|
|
837
|
+
</CoarFormField>
|
|
838
|
+
|
|
839
|
+
<CoarFormField label="Theme">
|
|
840
|
+
<CoarRadioGroup v-model="theme" name="theme" orientation="horizontal">
|
|
841
|
+
<CoarRadioButton value="light" label="Light" />
|
|
842
|
+
<CoarRadioButton value="dark" label="Dark" />
|
|
843
|
+
<CoarRadioButton value="system" label="System" />
|
|
844
|
+
</CoarRadioGroup>
|
|
845
|
+
</CoarFormField>
|
|
846
|
+
|
|
847
|
+
<CoarFormField label="Experimental Features">
|
|
848
|
+
<CoarSwitch v-model="experiments" label="Enable beta features" />
|
|
849
|
+
</CoarFormField>
|
|
850
|
+
|
|
851
|
+
<div class="demo-form__actions">
|
|
852
|
+
<CoarButton>Save Settings</CoarButton>
|
|
853
|
+
</div>
|
|
854
|
+
</form>
|
|
855
|
+
</template>
|
|
856
|
+
|
|
857
|
+
<script setup lang="ts">
|
|
858
|
+
import { ref } from 'vue';
|
|
859
|
+
import { CoarFormField, CoarTextInput, CoarSelect, CoarCheckbox, CoarRadioGroup, CoarRadioButton, CoarSwitch, CoarButton } from '@cocoar/vue-ui';
|
|
860
|
+
import type { CoarSelectOption } from '@cocoar/vue-ui';
|
|
861
|
+
|
|
862
|
+
const displayName = ref('Jane Doe');
|
|
863
|
+
const bio = ref('');
|
|
864
|
+
const language = ref('en');
|
|
865
|
+
const emailNotifs = ref(true);
|
|
866
|
+
const pushNotifs = ref(false);
|
|
867
|
+
const weeklyDigest = ref(true);
|
|
868
|
+
const theme = ref('system');
|
|
869
|
+
const experiments = ref(false);
|
|
870
|
+
|
|
871
|
+
const languages: CoarSelectOption<string>[] = [
|
|
872
|
+
{ value: 'en', label: 'English' },
|
|
873
|
+
{ value: 'de', label: 'Deutsch' },
|
|
874
|
+
{ value: 'fr', label: 'Français' },
|
|
875
|
+
{ value: 'es', label: 'Español' },
|
|
876
|
+
];
|
|
877
|
+
</script>
|
|
878
|
+
|
|
879
|
+
<style scoped>
|
|
880
|
+
.demo-form {
|
|
881
|
+
display: flex;
|
|
882
|
+
flex-direction: column;
|
|
883
|
+
gap: var(--coar-spacing-m);
|
|
884
|
+
max-width: 400px;
|
|
885
|
+
}
|
|
886
|
+
.demo-checks {
|
|
887
|
+
display: flex;
|
|
888
|
+
flex-direction: column;
|
|
889
|
+
gap: var(--coar-spacing-s);
|
|
890
|
+
}
|
|
891
|
+
.demo-form__actions {
|
|
892
|
+
padding-top: var(--coar-spacing-s);
|
|
893
|
+
}
|
|
894
|
+
</style>
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
## Validation with vee-validate
|
|
898
|
+
|
|
899
|
+
`CoarFormField` integrates seamlessly with [vee-validate](https://vee-validate.logaretm.com/) and [Zod](https://zod.dev/) schemas. Use `useField()` to get reactive `value` and `errorMessage` refs, then bind them to the input and `CoarFormField` respectively.
|
|
900
|
+
|
|
901
|
+
**Demo — `form-field/demos/FormVeeValidate.vue`**
|
|
902
|
+
|
|
903
|
+
```vue
|
|
904
|
+
<template>
|
|
905
|
+
<form class="demo-form" @submit="onSubmit">
|
|
906
|
+
<CoarFormField label="Email" :error="emailError" required>
|
|
907
|
+
<CoarTextInput v-model="email" placeholder="jane@example.com" required />
|
|
908
|
+
</CoarFormField>
|
|
909
|
+
|
|
910
|
+
<CoarFormField label="Password" :error="passwordError" hint="At least 8 characters" required>
|
|
911
|
+
<CoarPasswordInput v-model="password" required />
|
|
912
|
+
</CoarFormField>
|
|
913
|
+
|
|
914
|
+
<CoarFormField label="Confirm Password" :error="confirmError" required>
|
|
915
|
+
<CoarPasswordInput v-model="confirm" required />
|
|
916
|
+
</CoarFormField>
|
|
917
|
+
|
|
918
|
+
<CoarFormField :error="termsError">
|
|
919
|
+
<CoarCheckbox v-model="terms" label="I accept the terms of service" />
|
|
920
|
+
</CoarFormField>
|
|
921
|
+
|
|
922
|
+
<div class="demo-form__actions">
|
|
923
|
+
<CoarButton type="submit">Sign Up</CoarButton>
|
|
924
|
+
</div>
|
|
925
|
+
|
|
926
|
+
<CoarNote v-if="submitted" variant="success" padding="s">
|
|
927
|
+
Form submitted successfully!
|
|
928
|
+
</CoarNote>
|
|
929
|
+
</form>
|
|
930
|
+
</template>
|
|
931
|
+
|
|
932
|
+
<script setup lang="ts">
|
|
933
|
+
import { ref } from 'vue';
|
|
934
|
+
import { useForm, useField } from 'vee-validate';
|
|
935
|
+
import { toTypedSchema } from '@vee-validate/zod';
|
|
936
|
+
import { z } from 'zod';
|
|
937
|
+
import { CoarFormField, CoarTextInput, CoarPasswordInput, CoarCheckbox, CoarButton, CoarNote } from '@cocoar/vue-ui';
|
|
938
|
+
|
|
939
|
+
const schema = toTypedSchema(
|
|
940
|
+
z.object({
|
|
941
|
+
email: z.string().min(1, 'Email is required').email('Enter a valid email'),
|
|
942
|
+
password: z.string().min(8, 'At least 8 characters'),
|
|
943
|
+
confirm: z.string().min(1, 'Please confirm your password'),
|
|
944
|
+
terms: z.literal(true, { errorMap: () => ({ message: 'You must accept the terms' }) }),
|
|
945
|
+
}).refine((data) => data.password === data.confirm, {
|
|
946
|
+
message: 'Passwords do not match',
|
|
947
|
+
path: ['confirm'],
|
|
948
|
+
}),
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
const submitted = ref(false);
|
|
952
|
+
|
|
953
|
+
const { handleSubmit } = useForm({ validationSchema: schema });
|
|
954
|
+
|
|
955
|
+
const { value: email, errorMessage: emailError } = useField<string>('email', undefined, { initialValue: '' });
|
|
956
|
+
const { value: password, errorMessage: passwordError } = useField<string>('password', undefined, { initialValue: '' });
|
|
957
|
+
const { value: confirm, errorMessage: confirmError } = useField<string>('confirm', undefined, { initialValue: '' });
|
|
958
|
+
const { value: terms, errorMessage: termsError } = useField<boolean>('terms', undefined, { initialValue: false });
|
|
959
|
+
|
|
960
|
+
const onSubmit = handleSubmit(() => {
|
|
961
|
+
submitted.value = true;
|
|
962
|
+
});
|
|
963
|
+
</script>
|
|
964
|
+
|
|
965
|
+
<style scoped>
|
|
966
|
+
.demo-form {
|
|
967
|
+
display: flex;
|
|
968
|
+
flex-direction: column;
|
|
969
|
+
gap: var(--coar-spacing-m);
|
|
970
|
+
max-width: 400px;
|
|
971
|
+
}
|
|
972
|
+
.demo-form__actions {
|
|
973
|
+
padding-top: var(--coar-spacing-s);
|
|
974
|
+
}
|
|
975
|
+
</style>
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
> **Tip: Other validation libraries**
|
|
979
|
+
>
|
|
980
|
+
> `CoarFormField` is library-agnostic — it just takes an `error` string. Any validation approach works: vee-validate, vuelidate, or plain computed properties.
|
|
981
|
+
|
|
982
|
+
## Standalone Form Controls
|
|
983
|
+
|
|
984
|
+
Form controls work without `CoarFormField` when no label or validation is needed — inline search inputs, table checkboxes, toolbar buttons.
|
|
985
|
+
|
|
986
|
+
```vue
|
|
987
|
+
<!-- No label needed -->
|
|
988
|
+
<CoarTextInput v-model="search" placeholder="Search..." />
|
|
989
|
+
|
|
990
|
+
<!-- Inline checkbox with its own label -->
|
|
991
|
+
<CoarCheckbox v-model="agree" label="I agree" />
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
## Accessibility
|
|
995
|
+
|
|
996
|
+
`CoarFormField` generates unique IDs automatically and wires them through `aria-describedby` on the child input:
|
|
997
|
+
|
|
998
|
+
- **Label**: `<label for="...">` points to the input — clicking the label focuses the control.
|
|
999
|
+
- **Hint** / **errors** / **warnings**: each rendered as a visually-hidden span with its own ID. The child input's `aria-describedby` is the **space-separated list** of all currently-present IDs (hint first, then every error, then every warning). Screen readers read them all on focus.
|
|
1000
|
+
- **Errors** additionally carry `role="alert"` so they're announced when they appear (warnings + hint are silent — they're descriptive, not urgent).
|
|
1001
|
+
- **`aria-invalid="true"`** on the input is driven by `error` only; warnings keep the input valid.
|
|
1002
|
+
- **Required**: asterisk is `aria-hidden="true"` — use `required` on the input itself for semantics.
|
|
1003
|
+
|
|
1004
|
+
## API
|
|
1005
|
+
|
|
1006
|
+
### Props
|
|
1007
|
+
|
|
1008
|
+
| Prop | Type | Default | Description |
|
|
1009
|
+
|------|------|---------|-------------|
|
|
1010
|
+
| `label` | `string` | `undefined` | Label text rendered next to the status icon. |
|
|
1011
|
+
| `hint` | `string` | `''` | Informational help text. Sits at the top of the popover (grey). When it's the only thing set, the icon is a grey `info` glyph. |
|
|
1012
|
+
| `error` | `string \| readonly string[]` | `[]` | Validation error(s). Drives the red `circle-alert` icon + `aria-invalid="true"` on the child input. Single string is sugar for a one-item array. Each entry is announced as a `role="alert"` to screen readers. |
|
|
1013
|
+
| `warning` | `string \| readonly string[]` | `[]` | Non-blocking warning(s). Drives the orange `triangle-alert` icon (when no error is also set). The input stays valid; SR announcements are non-urgent. |
|
|
1014
|
+
| `rules` | `readonly CoarFormFieldRule[]` | `[]` | Live-validation rules. Each rule has `label`, `fulfilled: boolean`, and optional `whenPass: 'success' \| 'hide'` (default `'success'`) + `whenFail: 'pending' \| 'warning' \| 'error' \| 'hide'` (default `'pending'`). See [Live Rules](#live-rules) for the four common patterns. Rules with `whenFail: 'error'` drive the input's `aria-invalid="true"`. |
|
|
1015
|
+
| `layout` | `'stacked' \| 'inline'` | `'stacked'` | Places the label/status cluster and control vertically or on one row. |
|
|
1016
|
+
| `labelPosition` | `'before' \| 'after'` | `'before'` | Places the complete label/status cluster before or after the control. Works in both layouts. |
|
|
1017
|
+
| `required` | `boolean` | `false` | Show required asterisk next to label. |
|
|
1018
|
+
| `disabled` | `boolean` | `false` | Disabled state — propagated to child inputs. |
|
|
1019
|
+
| `id` | `string` | auto | Explicit input ID (auto-generated if omitted). |
|
|
1020
|
+
|
|
1021
|
+
### Slots
|
|
1022
|
+
|
|
1023
|
+
| Slot | Description |
|
|
1024
|
+
|------|-------------|
|
|
1025
|
+
| `default` | The form control(s) to wrap |
|