@imaginario27/air-ui-ds 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/assets/css/defaults.css +55 -0
  2. package/assets/css/main.css +238 -0
  3. package/assets/css/theme/colors.css +106 -0
  4. package/assets/css/theme/primitives.css +105 -0
  5. package/assets/css/theme/ui-theme.css +454 -0
  6. package/assets/images/placeholders/missing-image-placeholder.png +0 -0
  7. package/components/accordions/Accordion.vue +31 -0
  8. package/components/accordions/AccordionGroup.vue +78 -0
  9. package/components/accordions/AccordionItem.vue +39 -0
  10. package/components/action-panels/ActionPanel.vue +49 -0
  11. package/components/alerts/Alert.vue +159 -0
  12. package/components/avatars/Avatar.vue +152 -0
  13. package/components/avatars/AvatarStack.vue +97 -0
  14. package/components/avatars/AvatarStackCounter.vue +74 -0
  15. package/components/badges/Badge.vue +221 -0
  16. package/components/badges/BadgeStack.vue +110 -0
  17. package/components/badges/IconBadge.vue +57 -0
  18. package/components/badges/IconTextBadge.vue +50 -0
  19. package/components/breadcrumbs/Breadcrumbs.vue +54 -0
  20. package/components/buttons/ActionButton.vue +395 -0
  21. package/components/buttons/ActionIconButton.vue +283 -0
  22. package/components/buttons/AlertButton.vue +125 -0
  23. package/components/buttons/AlertIconButton.vue +105 -0
  24. package/components/buttons/PaginationButton.vue +45 -0
  25. package/components/buttons/options/OptionButton.vue +61 -0
  26. package/components/buttons/options/OptionButtonGroup.vue +155 -0
  27. package/components/buttons/options/OptionButtonSlider.vue +154 -0
  28. package/components/buttons/toggle/ToggleButton.vue +142 -0
  29. package/components/buttons/toggle/ToggleButtonGroup.vue +73 -0
  30. package/components/cards/Card.vue +33 -0
  31. package/components/cards/CardActions.vue +5 -0
  32. package/components/cards/CardBody.vue +5 -0
  33. package/components/cards/CardFooter.vue +20 -0
  34. package/components/cards/CardHeader.vue +5 -0
  35. package/components/cards/CardTitle.vue +13 -0
  36. package/components/cards/specific/ContactDetailsCard.vue +47 -0
  37. package/components/cards/specific/FeatureCard.vue +59 -0
  38. package/components/cards/specific/HelpTopicCard.vue +62 -0
  39. package/components/cards/specific/MetricCard.vue +42 -0
  40. package/components/cards/specific/TestimonialCard.vue +57 -0
  41. package/components/cards/specific/subscription/CurrentActiveSubscriptionCard.vue +105 -0
  42. package/components/cards/specific/subscription/SubscriptionPlanCard.vue +178 -0
  43. package/components/cards/specific/subscription/UniqueSubscriptionPlanCard.vue +106 -0
  44. package/components/collapsibles/Collapsible.vue +33 -0
  45. package/components/content/ContentItem.vue +144 -0
  46. package/components/content/ContentItemImage.vue +125 -0
  47. package/components/dividers/Divider.vue +35 -0
  48. package/components/dividers/TextLineDivider.vue +58 -0
  49. package/components/dropdowns/DropdownMenu.vue +207 -0
  50. package/components/dropdowns/DropdownMenuActions.vue +11 -0
  51. package/components/dropdowns/DropdownMenuItem.vue +240 -0
  52. package/components/dropdowns/DropdownSelect.vue +469 -0
  53. package/components/dropdowns/DropdownSelectItem.vue +182 -0
  54. package/components/empty-states/EmptyState.vue +170 -0
  55. package/components/features/Feature.vue +77 -0
  56. package/components/forms/DataDetails.vue +7 -0
  57. package/components/forms/DataDetailsActions.vue +23 -0
  58. package/components/forms/DataDetailsFieldGroup.vue +35 -0
  59. package/components/forms/DataDetailsRow.vue +22 -0
  60. package/components/forms/Form.vue +25 -0
  61. package/components/forms/FormActions.vue +23 -0
  62. package/components/forms/FormFieldGroup.vue +35 -0
  63. package/components/forms/FormRow.vue +22 -0
  64. package/components/forms/fields/ButtonField.vue +119 -0
  65. package/components/forms/fields/CheckboxField.vue +205 -0
  66. package/components/forms/fields/DataField.vue +99 -0
  67. package/components/forms/fields/FileUploadField.vue +326 -0
  68. package/components/forms/fields/InputField.vue +371 -0
  69. package/components/forms/fields/OptionButtonsGroupField.vue +120 -0
  70. package/components/forms/fields/RepeaterField.vue +109 -0
  71. package/components/forms/fields/SearchField.vue +184 -0
  72. package/components/forms/fields/SelectField.vue +233 -0
  73. package/components/forms/fields/SliderField.vue +759 -0
  74. package/components/forms/fields/SwitchField.vue +257 -0
  75. package/components/forms/fields/TextareaField.vue +205 -0
  76. package/components/forms/fields/ToggleButtonsGroupField.vue +65 -0
  77. package/components/forms/fields/radio/RadioButtonField.vue +238 -0
  78. package/components/forms/fields/radio/RadioField.vue +157 -0
  79. package/components/forms/fields/radio/RadioGroupField.vue +156 -0
  80. package/components/icons/ContainedIcon.vue +130 -0
  81. package/components/images/QRCode.vue +124 -0
  82. package/components/layouts/ContainerWrapper.vue +13 -0
  83. package/components/layouts/ContentBody.vue +30 -0
  84. package/components/layouts/Grid.vue +25 -0
  85. package/components/layouts/Heading.vue +159 -0
  86. package/components/layouts/MainContent.vue +26 -0
  87. package/components/layouts/MaxWidthContainer.vue +15 -0
  88. package/components/layouts/Overtitle.vue +25 -0
  89. package/components/layouts/headers/CompactHeader.vue +181 -0
  90. package/components/layouts/headers/PageHeader.vue +102 -0
  91. package/components/layouts/headers/WebAppHeader.vue +54 -0
  92. package/components/layouts/section/Section.vue +90 -0
  93. package/components/layouts/section/SectionBody.vue +12 -0
  94. package/components/layouts/section/SectionHeader.vue +12 -0
  95. package/components/layouts/section/SectionTitle.vue +13 -0
  96. package/components/lists/List.vue +69 -0
  97. package/components/lists/ListItem.vue +58 -0
  98. package/components/loaders/Loading.vue +83 -0
  99. package/components/loaders/LoadingScreen.vue +285 -0
  100. package/components/modals/DangerModalDialog.vue +149 -0
  101. package/components/modals/InfoModalDialog.vue +143 -0
  102. package/components/modals/ModalActions.vue +22 -0
  103. package/components/modals/ModalContent.vue +5 -0
  104. package/components/modals/ModalDescription.vue +5 -0
  105. package/components/modals/ModalDialog.vue +122 -0
  106. package/components/modals/ModalHeaderGroup.vue +19 -0
  107. package/components/modals/ModalHeadings.vue +5 -0
  108. package/components/modals/ModalSubtitle.vue +14 -0
  109. package/components/modals/ModalTitle.vue +14 -0
  110. package/components/modals/SuccessModalDialog.vue +90 -0
  111. package/components/modules/AppLogo.vue +46 -0
  112. package/components/modules/SVGImage.vue +44 -0
  113. package/components/navigation/links/NavLink.vue +112 -0
  114. package/components/navigation/nav-menu/NavFooterMenu.vue +91 -0
  115. package/components/navigation/nav-menu/NavMenu.vue +36 -0
  116. package/components/navigation/nav-menu/NavMenuItem.vue +44 -0
  117. package/components/navigation/nav-sidebar/BottomUserNavBar.vue +83 -0
  118. package/components/navigation/nav-sidebar/NavSidebar.vue +172 -0
  119. package/components/navigation/nav-sidebar/NavSidebarMenu.vue +14 -0
  120. package/components/navigation/nav-sidebar/NavSidebarMenuItem.vue +76 -0
  121. package/components/navigation/nav-sidebar/NavSidebarMenuSectionTitle.vue +54 -0
  122. package/components/navigation/table-of-contents/TableOfContents.vue +35 -0
  123. package/components/navigation/table-of-contents/TableOfContentsItem.vue +40 -0
  124. package/components/navigation/table-of-contents/TableOfContentsSidebar.vue +29 -0
  125. package/components/pagination/ButtonPagination.vue +274 -0
  126. package/components/pagination/RowsPerPage.vue +60 -0
  127. package/components/pagination/SimplePagination.vue +97 -0
  128. package/components/password/SecurePasswordCondition.vue +41 -0
  129. package/components/password/SecurePasswordConditions.vue +83 -0
  130. package/components/placeholders/ContentPlaceholder.vue +41 -0
  131. package/components/popovers/Popover.vue +128 -0
  132. package/components/rating/InteractiveRating.vue +94 -0
  133. package/components/rating/Rating.vue +60 -0
  134. package/components/rating/RatingItem.vue +54 -0
  135. package/components/skeletons/Skeleton.vue +11 -0
  136. package/components/spinners/Spinner.vue +13 -0
  137. package/components/steppers/CircleStepper.vue +122 -0
  138. package/components/steppers/Step.vue +72 -0
  139. package/components/steppers/StepIndicator.vue +228 -0
  140. package/components/steppers/TabStepper.vue +126 -0
  141. package/components/steppers/vertical-stepper/VerticalStep.vue +223 -0
  142. package/components/steppers/vertical-stepper/VerticalStepper.vue +63 -0
  143. package/components/tables/Table.vue +26 -0
  144. package/components/tables/TableBody.vue +5 -0
  145. package/components/tables/TableCell.vue +34 -0
  146. package/components/tables/TableCellActions.vue +7 -0
  147. package/components/tables/TableHeader.vue +5 -0
  148. package/components/tables/TableHeaderCell.vue +15 -0
  149. package/components/tables/TableRow.vue +14 -0
  150. package/components/tables/TableWrapper.vue +12 -0
  151. package/components/tabs/Tab.vue +145 -0
  152. package/components/tabs/TabBar.vue +64 -0
  153. package/components/tabs/TabContent.vue +5 -0
  154. package/components/tabs/TabsContainer.vue +5 -0
  155. package/components/transitions/HorizontalExpansionTransition.vue +12 -0
  156. package/components/transitions/VerticalExpansionTransition.vue +14 -0
  157. package/components/users/Author.vue +113 -0
  158. package/components/users/User.vue +53 -0
  159. package/composables/useAccordion.ts +12 -0
  160. package/composables/useDarkMode.ts +9 -0
  161. package/composables/useDropdownMenu.ts +25 -0
  162. package/composables/useForm.ts +134 -0
  163. package/composables/useFormValidationMode.ts +11 -0
  164. package/composables/useIsMobile.ts +27 -0
  165. package/composables/useMobileSidebar.ts +32 -0
  166. package/composables/useShiki.ts +12 -0
  167. package/composables/useTableOfContents.ts +50 -0
  168. package/composables/useToastifyConfig.ts +7 -0
  169. package/eslint.config.mjs +14 -0
  170. package/models/constants/app.ts +8 -0
  171. package/models/constants/form.ts +22 -0
  172. package/models/enums/alerts.ts +6 -0
  173. package/models/enums/aspect-ratios.ts +9 -0
  174. package/models/enums/avatars.ts +21 -0
  175. package/models/enums/badges.ts +10 -0
  176. package/models/enums/buttons.ts +38 -0
  177. package/models/enums/colors.ts +9 -0
  178. package/models/enums/content.ts +4 -0
  179. package/models/enums/counters.ts +4 -0
  180. package/models/enums/dividers.ts +9 -0
  181. package/models/enums/dropdowns.ts +18 -0
  182. package/models/enums/effects.ts +6 -0
  183. package/models/enums/emptyPlaceholders.ts +5 -0
  184. package/models/enums/formFields.ts +19 -0
  185. package/models/enums/formValidations.ts +4 -0
  186. package/models/enums/headings.ts +11 -0
  187. package/models/enums/icons.ts +22 -0
  188. package/models/enums/images.ts +16 -0
  189. package/models/enums/lists.ts +10 -0
  190. package/models/enums/loaders.ts +15 -0
  191. package/models/enums/navigation.ts +18 -0
  192. package/models/enums/order.ts +10 -0
  193. package/models/enums/orientations.ts +4 -0
  194. package/models/enums/pages.ts +10 -0
  195. package/models/enums/positions.ts +21 -0
  196. package/models/enums/rating.ts +12 -0
  197. package/models/enums/sections.ts +8 -0
  198. package/models/enums/selects.ts +16 -0
  199. package/models/enums/sliders.ts +4 -0
  200. package/models/enums/steppers.ts +20 -0
  201. package/models/enums/tabs.ts +11 -0
  202. package/models/enums/triggers.ts +4 -0
  203. package/models/types/accordions.ts +6 -0
  204. package/models/types/avatars.ts +4 -0
  205. package/models/types/badges.ts +4 -0
  206. package/models/types/buttons.ts +26 -0
  207. package/models/types/dropdowns.ts +20 -0
  208. package/models/types/forms.ts +14 -0
  209. package/models/types/navigation.ts +11 -0
  210. package/models/types/pagination.ts +4 -0
  211. package/models/types/pdfExportTable.ts +6 -0
  212. package/models/types/radio.ts +9 -0
  213. package/models/types/selects.ts +14 -0
  214. package/models/types/steppers.ts +17 -0
  215. package/models/types/tableOfContent.ts +6 -0
  216. package/models/types/tabs.ts +7 -0
  217. package/nuxt.config.ts +40 -0
  218. package/package.json +57 -0
  219. package/plugins/vue3-toastify.ts +14 -0
  220. package/tsconfig.json +7 -0
@@ -0,0 +1,257 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'flex flex-col',
5
+ 'w-full',
6
+ 'gap-2',
7
+ ]"
8
+ >
9
+ <!-- Legend -->
10
+ <legend
11
+ v-if="legend"
12
+ :class="[
13
+ 'text-sm',
14
+ 'font-semibold',
15
+ 'text-left',
16
+ ]"
17
+ >
18
+ {{ legend }}
19
+ </legend>
20
+
21
+ <!-- Main wrapper -->
22
+ <div
23
+ :class="[
24
+ 'flex items-center gap-3',
25
+ label || icon || customIcon ? 'justify-between' : 'justify-end',
26
+ 'text-sm w-full',
27
+ hasError ? 'text-text-error' : 'text-text-default',
28
+ checkboxWrapperClass && checkboxWrapperClass
29
+ ]"
30
+ >
31
+ <!-- Label + Icon block (only if present) -->
32
+ <div
33
+ v-if="label || icon || customIcon"
34
+ class="flex gap-2.5 w-full"
35
+ >
36
+ <!-- Icon -->
37
+ <template v-if="icon || customIcon">
38
+ <MdiIcon
39
+ v-if="icon && !customIcon"
40
+ :icon="icon"
41
+ :size="iconSize"
42
+ preserveAspectRatio="xMidYMid meet"
43
+ class="text-icon-default"
44
+ />
45
+ <div
46
+ v-else
47
+ :class="[
48
+ customIconSizeClass,
49
+ 'text-icon-default'
50
+ ]"
51
+ v-html="customIcon"
52
+ />
53
+ </template>
54
+
55
+ <!-- Label -->
56
+ <label
57
+ v-if="label"
58
+ :for="id"
59
+ :class="[
60
+ disabled && 'text-text-neutral-disabled',
61
+ labelSizeClass,
62
+ customIcon && 'mt-1',
63
+ labelClass && labelClass
64
+ ]"
65
+ v-html="label"
66
+ />
67
+ </div>
68
+
69
+ <!-- Hidden native checkbox -->
70
+ <input
71
+ :id="id"
72
+ type="checkbox"
73
+ :checked="modelValue"
74
+ class="hidden"
75
+ :disabled="disabled"
76
+ @change="handleChange"
77
+ >
78
+
79
+ <!-- Custom Switch -->
80
+ <div
81
+ :class="[
82
+ 'relative flex items-center',
83
+ controlFieldSizeClass,
84
+ 'rounded-full transition-colors',
85
+ 'border border-border-default',
86
+ modelValue ? checkedBackgroundClass : 'bg-background-neutral-subtle',
87
+ disabled ? 'bg-background-neutral-disabled cursor-not-allowed opacity-disabled' : 'cursor-pointer'
88
+ ]"
89
+ @click="toggleCheckbox"
90
+ >
91
+ <div
92
+ :class="[
93
+ 'absolute bg-icon-on-filled rounded-full shadow-md transform transition-transform',
94
+ 'aspect-square',
95
+ controlFieldHandlerSizeClass,
96
+ modelValue ? 'translate-x-6' : 'translate-x-1',
97
+ 'bg-icon-neutral-on-filled-bg'
98
+ ]"
99
+ />
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Help Text -->
104
+ <p
105
+ v-if="helpText || hasError"
106
+ :class="[
107
+ 'text-xs',
108
+ 'text-left',
109
+ hasError ? 'text-text-error' : 'text-text-neutral-subtle'
110
+ ]"
111
+ >
112
+ {{ hasError ? error : helpText }}
113
+ </p>
114
+ </div>
115
+ </template>
116
+
117
+
118
+ <script setup lang="ts">
119
+ // Props
120
+ const props = defineProps({
121
+ id: {
122
+ type: String as PropType<string>,
123
+ required: true,
124
+ },
125
+ label: String as PropType<string>,
126
+ legend: String as PropType<string>,
127
+ helpText: String as PropType<string>,
128
+ required: {
129
+ type: Boolean as PropType<boolean>,
130
+ default: false,
131
+ },
132
+ modelValue: {
133
+ type: Boolean as PropType<boolean>,
134
+ default: false,
135
+ },
136
+ validator: {
137
+ type: Function as PropType<(value: boolean) => string | null>,
138
+ default: () => null,
139
+ },
140
+ error: {
141
+ type: String as PropType<string>,
142
+ default: '',
143
+ },
144
+ disabled: {
145
+ type: Boolean as PropType<boolean>,
146
+ default: false,
147
+ },
148
+ size: {
149
+ type: String as PropType<ControlFieldSize>,
150
+ default: ControlFieldSize.MD,
151
+ validator: (value: ControlFieldSize) => Object.values(ControlFieldSize).includes(value),
152
+ },
153
+ icon: String as PropType<any>,
154
+ customIcon: String as PropType<any>,
155
+ styleType: {
156
+ type: String as PropType<SwitchStyle>,
157
+ default: SwitchStyle.BRAND,
158
+ validator: (value: SwitchStyle) => Object.values(SwitchStyle).includes(value),
159
+ },
160
+ checkboxWrapperClass: String as PropType<string>,
161
+ labelClass: String as PropType<string>,
162
+ })
163
+
164
+ // Emits
165
+ const emit = defineEmits(['update:modelValue', 'update:error'])
166
+
167
+ // Composables
168
+ const validationMode = useInjectedValidationMode()
169
+
170
+ // Computed
171
+ const hasError = computed(() => props.error !== '')
172
+
173
+ // Computed classes
174
+ const controlFieldSizeClass = computed(() => {
175
+ const sizeVariant = {
176
+ [ControlFieldSize.MD]: 'w-[44px] h-[24px] min-w-[44px] min-h-[24px]',
177
+ [ControlFieldSize.LG]: 'w-[56px] h-[32px] min-w-[56px] min-h-[32px]',
178
+ }
179
+ return sizeVariant[props.size as ControlFieldSize] || 'w-[44px] h-[24px] min-w-[44px] min-h-[24px]'
180
+ })
181
+
182
+ const controlFieldHandlerSizeClass = computed(() => {
183
+ const sizeVariant = {
184
+ [ControlFieldSize.MD]: 'w-[16px] h-[16px]',
185
+ [ControlFieldSize.LG]: 'w-[24px] h-[24px]',
186
+ }
187
+ return sizeVariant[props.size as ControlFieldSize] || 'w-[16px] h-[16px]'
188
+ })
189
+
190
+ const labelSizeClass = computed(() => {
191
+ const sizeVariant = {
192
+ [ControlFieldSize.MD]: 'text-sm',
193
+ [ControlFieldSize.LG]: 'text-base',
194
+ }
195
+ return sizeVariant[props.size as ControlFieldSize] || 'text-sm'
196
+ })
197
+
198
+ const iconSize = computed(() => {
199
+ const sizeVariant = {
200
+ [ControlFieldSize.MD]: '20',
201
+ [ControlFieldSize.LG]: '24',
202
+ }
203
+ return sizeVariant[props.size as ControlFieldSize] || '20'
204
+ })
205
+
206
+ const customIconSizeClass = computed(() => {
207
+ const sizeVariant = {
208
+ [ControlFieldSize.MD]: 'w-[20px] h-[20px]',
209
+ [ControlFieldSize.LG]: 'w-[24px] h-[24px]',
210
+ }
211
+ return sizeVariant[props.size as ControlFieldSize] || 'w-[20px] h-[20px]'
212
+ })
213
+
214
+ const checkedBackgroundClass = computed(() => {
215
+ const backgroundVariant = {
216
+ [SwitchStyle.BRAND]: 'bg-background-primary-brand-checked',
217
+ [SwitchStyle.SUCCESS]: 'bg-background-success-bold',
218
+ }
219
+ return backgroundVariant[props.styleType as SwitchStyle] || 'bg-background-primary-brand-checked'
220
+ })
221
+
222
+ // Handlers
223
+ const handleChange = () => {
224
+ if (validationMode.value === FormValidationMode.BLUR) {
225
+ runValidation()
226
+ }
227
+ }
228
+
229
+ const toggleCheckbox = () => {
230
+ if (props.disabled) return
231
+
232
+ if (validationMode.value === FormValidationMode.BLUR) {
233
+ runValidation()
234
+ }
235
+
236
+ emit('update:modelValue', !props.modelValue)
237
+ }
238
+
239
+ const runValidation = () => {
240
+ if (!props.required || !props.validator) return
241
+
242
+ const result = props.validator(props.modelValue)
243
+
244
+ emit('update:error', result ?? '')
245
+ }
246
+
247
+ // Watchers
248
+ watch(
249
+ () => props.modelValue,
250
+ value => {
251
+ if (validationMode.value === FormValidationMode.BLUR && props.validator && props.required) {
252
+ const result = props.validator(value)
253
+ emit('update:error', result ?? '')
254
+ }
255
+ }
256
+ )
257
+ </script>
@@ -0,0 +1,205 @@
1
+ <template>
2
+ <div :class="[ 'flex flex-col', 'w-full', 'gap-2' ]">
3
+ <!-- Label -->
4
+ <label
5
+ v-if="label"
6
+ :for="id"
7
+ :class="[
8
+ 'text-sm',
9
+ 'font-semibold',
10
+ 'text-left',
11
+ hasError && 'text-text-error'
12
+ ]"
13
+ >
14
+ {{ label }}
15
+ </label>
16
+
17
+ <!-- Textarea Container -->
18
+ <div
19
+ :class="[
20
+ 'flex gap-2',
21
+ 'border',
22
+ 'rounded-md',
23
+ 'transition-all',
24
+ 'p-4',
25
+ 'relative',
26
+ minHeightClass,
27
+ hasError ? 'border-border-error text-text-error' : 'border-border-default',
28
+ isFocused && 'ring-2 ring-border-primary-brand-default',
29
+ filled ? 'text-text-default' : 'text-text-neutral-subtler',
30
+ ]"
31
+ >
32
+ <!-- Textarea -->
33
+ <textarea
34
+ :id
35
+ :placeholder
36
+ :value="modelValue"
37
+ :maxlength="maxLength"
38
+ :readonly
39
+ :autocomplete
40
+ :autofocus
41
+ :wrap
42
+ :spellcheck
43
+ :disabled
44
+ :class="[
45
+ 'w-full',
46
+ 'outline-none',
47
+ 'bg-transparent',
48
+ 'text-sm',
49
+ 'resize-none',
50
+ ]"
51
+ @focus="handleFocus"
52
+ @blur="handleBlur"
53
+ @input="handleInput"
54
+ />
55
+
56
+ <!-- Error Icon -->
57
+ <span v-if="hasError" class="text-icon-error absolute top-4 right-4">
58
+ <MdiIcon
59
+ icon="mdiAlertCircle"
60
+ size="20"
61
+ preserveAspectRatio="xMidYMid meet"
62
+ />
63
+ </span>
64
+ </div>
65
+
66
+ <!-- Help Text -->
67
+ <p
68
+ v-if="hasError || helpText"
69
+ :class="[
70
+ 'text-xs text-left',
71
+ hasError ? 'text-text-error' : 'text-text-neutral-subtle'
72
+ ]"
73
+ >
74
+ {{ hasError ? error : helpText }}
75
+ </p>
76
+
77
+ <!-- Character Counter -->
78
+ <p v-if="hasCharCounter && maxLength" class="text-xs text-right text-text-neutral-subtle">
79
+ {{ modelValue.length }} / {{ maxLength }}
80
+ </p>
81
+ </div>
82
+ </template>
83
+
84
+ <script setup lang="ts">
85
+ // Props
86
+ const props = defineProps({
87
+ id: {
88
+ type: String as PropType<string>,
89
+ required: true,
90
+ },
91
+ label: String as PropType<string>,
92
+ placeholder: {
93
+ type: String as PropType<string>,
94
+ default: 'Placeholder',
95
+ },
96
+ helpText: String as PropType<string>,
97
+ minHeightClass: {
98
+ type: String as PropType<string>,
99
+ default: 'min-h-[150px]',
100
+ },
101
+ modelValue: {
102
+ type: String as PropType<string>,
103
+ default: '',
104
+ },
105
+ validator: {
106
+ type: Function as PropType<(value: unknown) => string | null>,
107
+ default: () => null,
108
+ },
109
+ error: {
110
+ type: String as PropType<string>,
111
+ default: '',
112
+ },
113
+ maxLength: {
114
+ type: Number as PropType<number>,
115
+ required: true,
116
+ },
117
+ hasCharCounter: {
118
+ type: Boolean as PropType<boolean>,
119
+ default: true,
120
+ },
121
+ readonly: {
122
+ type: Boolean as PropType<boolean>,
123
+ default: false,
124
+ },
125
+ autocomplete: {
126
+ type: String as PropType<'on' | 'off'>,
127
+ default: 'off',
128
+ },
129
+ autofocus: {
130
+ type: Boolean as PropType<boolean>,
131
+ default: false,
132
+ },
133
+ wrap: {
134
+ type: String as PropType<'soft' | 'hard'>,
135
+ default: 'soft',
136
+ },
137
+ spellcheck: {
138
+ type: Boolean,
139
+ default: false,
140
+ },
141
+ disabled: {
142
+ type: Boolean as PropType<boolean>,
143
+ default: false,
144
+ },
145
+ required: {
146
+ type: Boolean as PropType<boolean>,
147
+ default: false,
148
+ },
149
+ })
150
+
151
+ // Emits
152
+ const emit = defineEmits(['update:modelValue', 'update:error'])
153
+
154
+ // Composables
155
+ const validationMode = useInjectedValidationMode()
156
+
157
+ // States
158
+ const isFocused = ref(false)
159
+
160
+ // Computed States
161
+ const hasError = computed(() => props.error !== '')
162
+ const filled = computed(() => !!props.modelValue.trim())
163
+
164
+ // Methods
165
+ const handleFocus = () => {
166
+ isFocused.value = true
167
+ }
168
+
169
+ const handleBlur = () => {
170
+ isFocused.value = false
171
+
172
+ if (validationMode.value === FormValidationMode.BLUR) {
173
+ runValidation()
174
+ }
175
+ }
176
+
177
+ const handleInput = (event: Event) => {
178
+ if (props.disabled) return
179
+
180
+ const target = event.target as HTMLInputElement
181
+ const value = target.value
182
+
183
+ // Emit the updated value to the parent
184
+ emit('update:modelValue', value)
185
+ }
186
+
187
+ const runValidation = () => {
188
+ if (!props.required || !props.validator) return
189
+
190
+ const result = props.validator(props.modelValue)
191
+
192
+ emit('update:error', result ?? '')
193
+ }
194
+
195
+ // Watchers
196
+ watch(
197
+ () => props.modelValue,
198
+ value => {
199
+ if (validationMode.value === FormValidationMode.BLUR && props.validator && props.required) {
200
+ const result = props.validator(value)
201
+ emit('update:error', result ?? '')
202
+ }
203
+ }
204
+ )
205
+ </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'flex flex-col',
5
+ 'w-full',
6
+ 'gap-2'
7
+ ]"
8
+ >
9
+ <!-- Label -->
10
+ <label
11
+ v-if="label"
12
+ :for="id"
13
+ :class="[
14
+ 'text-sm',
15
+ 'font-semibold',
16
+ 'text-left'
17
+ ]"
18
+ >
19
+ {{ label }}
20
+ </label>
21
+
22
+ <ToggleButtonGroup
23
+ :buttons
24
+ :modelValue
25
+ :groupStyle
26
+ @update:modelValue="emit('update:modelValue', $event)"
27
+ />
28
+
29
+ <!-- Help Text -->
30
+ <p
31
+ v-if="helpText"
32
+ :class="[
33
+ 'text-xs text-left',
34
+ 'text-text-neutral-subtler'
35
+ ]"
36
+ >
37
+ {{ helpText }}
38
+ </p>
39
+ </div>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ // Props
44
+ defineProps({
45
+ id: {
46
+ type: String as PropType<string>,
47
+ required: true,
48
+ },
49
+ label: String as PropType<string>,
50
+ helpText: String as PropType<string>,
51
+ buttons: Array as PropType<ToggleButton[]>,
52
+ disabled: {
53
+ type: Boolean as PropType<boolean>,
54
+ default: false,
55
+ },
56
+ modelValue: {
57
+ type: String as PropType<string>,
58
+ required: true,
59
+ },
60
+ groupStyle: String as PropType<ToggleButtonGroupStyle>,
61
+ })
62
+
63
+ // Emits
64
+ const emit = defineEmits(['update:modelValue'])
65
+ </script>