@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.
Files changed (180) hide show
  1. package/bin/cli.mjs +53 -0
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  3. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  5. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  6. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  7. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  9. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  10. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  11. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  12. package/dist/components/data-list/index.d.ts +12 -0
  13. package/dist/components/data-list/index.d.ts.map +1 -0
  14. package/dist/components/data-list/internal/compare.d.ts +14 -0
  15. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  17. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  18. package/dist/components/data-list/internal/search.d.ts +16 -0
  19. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  21. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  23. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  25. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  26. package/dist/components/data-list/types.d.ts +185 -0
  27. package/dist/components/data-list/types.d.ts.map +1 -0
  28. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  29. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  30. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  31. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  32. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  33. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  34. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  36. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  38. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  40. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  42. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  44. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  45. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  46. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  47. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  48. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  50. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  52. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  54. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  56. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  58. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  59. package/dist/composables/dragRegistry.d.ts +12 -0
  60. package/dist/composables/dragRegistry.d.ts.map +1 -1
  61. package/dist/composables/index.d.ts +2 -2
  62. package/dist/composables/index.d.ts.map +1 -1
  63. package/dist/composables/useDragDrop.d.ts +62 -6
  64. package/dist/composables/useDragDrop.d.ts.map +1 -1
  65. package/dist/composables/useVirtualList.d.ts +21 -1
  66. package/dist/composables/useVirtualList.d.ts.map +1 -1
  67. package/dist/index.css +1 -1
  68. package/dist/index.d.ts +4 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4508 -2039
  71. package/package.json +9 -3
  72. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  73. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  74. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  75. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  76. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +372 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1647 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +486 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  86. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  87. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  89. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  90. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  91. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  99. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  100. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  101. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  102. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  103. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  104. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  105. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  106. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  110. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  111. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  112. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  116. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  117. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  118. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  119. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  120. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  121. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  122. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  127. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  128. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  129. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  130. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  131. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  132. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  133. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  134. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  140. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  141. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  142. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  143. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  144. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  145. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  146. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  147. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  148. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  149. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  150. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  151. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  152. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  153. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  154. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  155. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  156. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  157. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  158. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  159. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  160. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  161. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  162. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  163. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  164. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  165. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  166. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  167. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  171. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  172. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  173. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  174. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  175. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  176. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  177. package/skills/cocoar-vue-ui/references/guide/getting-started.md +116 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  179. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  180. 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 |