@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,371 @@
1
+ <template>
2
+ <div :class="['flex flex-col', 'w-full', 'gap-2']">
3
+ <div class="w-full flex gap-2 justify-between">
4
+ <!-- Label -->
5
+ <label
6
+ v-if="label"
7
+ :for="id"
8
+ :class="[
9
+ 'text-sm',
10
+ 'font-semibold',
11
+ 'text-left',
12
+ hasError && 'text-text-error',
13
+ ]"
14
+ >
15
+ {{ label }}
16
+ </label>
17
+ <NavLink
18
+ v-if="linkText"
19
+ :text="linkText"
20
+ :to="linkUrl"
21
+ textClass="font-normal"
22
+ />
23
+ </div>
24
+
25
+ <!-- Input Container -->
26
+ <div
27
+ :class="[
28
+ 'flex gap-2',
29
+ 'items-center',
30
+ 'border',
31
+ 'rounded-md',
32
+ 'transition-all',
33
+ icon ? 'pl-3' : 'pl-4',
34
+ suffixIcon ? 'pr-3' : 'pr-4',
35
+ inputSizeClass,
36
+ type === 'color' && 'w-fit',
37
+ hasError ? 'border-border-error text-text-error' : 'border-border-default',
38
+ isFocused && 'ring-2 ring-border-primary-brand-default',
39
+ filled ? 'text-text-default' : 'text-text-neutral-subtler',
40
+ disabled ? 'bg-background-neutral-disabled' : 'bg-neutral-white',
41
+ disabled && 'cursor-not-allowed',
42
+ ]"
43
+ >
44
+ <!-- Icon -->
45
+ <span
46
+ v-if="icon"
47
+ class="text-icon-neutral-subtler"
48
+ >
49
+ <MdiIcon
50
+ :icon="icon"
51
+ size="20"
52
+ preserveAspectRatio="xMidYMid meet"
53
+ />
54
+ </span>
55
+
56
+ <!-- Input -->
57
+ <input
58
+ :id
59
+ :type
60
+ :placeholder
61
+ :value="modelValue"
62
+ :maxlength="maxLength"
63
+ :readonly
64
+ :autocomplete
65
+ :autofocus
66
+ :disabled
67
+ :class="[
68
+ 'w-full',
69
+ 'outline-none',
70
+ 'bg-transparent',
71
+ 'text-sm',
72
+ type === 'color' && '!w-[40px] rounded-full border-0 hover:cursor-pointer',
73
+ disabled && 'cursor-not-allowed',
74
+ ]"
75
+ v-bind="{
76
+ ...dynamicProps,
77
+ }"
78
+ @focus="handleFocus"
79
+ @blur="handleBlur"
80
+ @input="handleInput"
81
+ >
82
+
83
+ <!-- Color info -->
84
+ <span
85
+ v-if="type === 'color'"
86
+ class="text-sm"
87
+ >
88
+ {{ modelValue }}
89
+ </span>
90
+
91
+ <!-- Suffix Icon (clickable) -->
92
+ <button
93
+ v-if="suffixIcon"
94
+ type="button"
95
+ :class="[
96
+ 'text-icon-neutral-subtler',
97
+ 'hover:text-icon-neutral-subtle',
98
+ 'focus:outline-none',
99
+ 'transition-colors',
100
+ 'cursor-pointer',
101
+ ]"
102
+ @click="handleSuffixClick"
103
+ >
104
+ <MdiIcon
105
+ :icon="suffixIcon"
106
+ size="20"
107
+ preserveAspectRatio="xMidYMid meet"
108
+ />
109
+ </button>
110
+
111
+ <!-- Error Icon -->
112
+ <span
113
+ v-if="hasError && !suffixIcon"
114
+ class="text-icon-error"
115
+ >
116
+ <MdiIcon
117
+ icon="mdiAlertCircle"
118
+ size="20"
119
+ preserveAspectRatio="xMidYMid meet"
120
+ />
121
+ </span>
122
+ </div>
123
+
124
+ <!-- Help Text -->
125
+ <p
126
+ v-if="hasError || helpText"
127
+ :class="[
128
+ 'text-xs text-left',
129
+ hasError ? 'text-text-error' : 'text-text-neutral-subtle',
130
+ ]"
131
+ >
132
+ {{ hasError ? error : helpText }}
133
+ </p>
134
+ </div>
135
+ </template>
136
+
137
+ <script setup lang="ts">
138
+ // Props
139
+ const props = defineProps({
140
+ id: {
141
+ type: String as PropType<string>,
142
+ required: true,
143
+ },
144
+ label: String as PropType<string>,
145
+ type: {
146
+ type: String as PropType<AllowedInputType>,
147
+ default: 'text',
148
+ },
149
+ placeholder: {
150
+ type: String as PropType<string>,
151
+ default: 'Placeholder',
152
+ },
153
+ helpText: String as PropType<string>,
154
+ icon: String as PropType<any>,
155
+ suffixIcon: String as PropType<any>,
156
+ linkText: String as PropType<string>,
157
+ linkUrl: String as PropType<string>,
158
+ size: {
159
+ type: String as PropType<InputSize>,
160
+ default: InputSize.MD,
161
+ validator: (value: InputSize) => Object.values(InputSize).includes(value),
162
+ },
163
+ modelValue: {
164
+ type: [String, Number, null] as PropType<string | number | null>,
165
+ default: '',
166
+ },
167
+ validator: {
168
+ type: Function as PropType<(value: unknown) => string | null>,
169
+ default: () => null,
170
+ },
171
+ error: {
172
+ type: String as PropType<string>,
173
+ default: '',
174
+ },
175
+ maxLength: {
176
+ type: Number as PropType<number>,
177
+ required: false,
178
+ },
179
+ filterAlphabetic: {
180
+ type: Boolean as PropType<boolean>,
181
+ default: false,
182
+ },
183
+ permitNegativeNumber: {
184
+ type: Boolean as PropType<boolean>,
185
+ default: false,
186
+ },
187
+ min: String as PropType<string>,
188
+ max: String as PropType<string>,
189
+ step: String as PropType<string>,
190
+ pattern: String as PropType<string>,
191
+ readonly: {
192
+ type: Boolean as PropType<boolean>,
193
+ default: false,
194
+ },
195
+ autocomplete: {
196
+ type: String as PropType<'on' | 'off'>,
197
+ default: 'off',
198
+ },
199
+ autofocus: {
200
+ type: Boolean as PropType<boolean>,
201
+ default: false,
202
+ },
203
+ disabled: {
204
+ type: Boolean as PropType<boolean>,
205
+ default: false,
206
+ },
207
+ required: {
208
+ type: Boolean as PropType<boolean>,
209
+ default: false,
210
+ },
211
+ })
212
+
213
+ // Emits
214
+ const emit = defineEmits(['update:modelValue', 'update:error', 'click:suffix'])
215
+
216
+ // States
217
+ const isFocused = ref(false)
218
+
219
+ // Composables
220
+ const validationMode = useInjectedValidationMode()
221
+
222
+ // Computed States
223
+ const hasError = computed(() => props.error !== '')
224
+ const filled = computed(() => {
225
+ if (typeof props.modelValue === 'string') {
226
+ return props.modelValue.trim().length > 0
227
+ }
228
+ if (typeof props.modelValue === 'number') {
229
+ return true
230
+ }
231
+ return false
232
+ })
233
+
234
+ // Computed classes
235
+ const inputSizeClass = computed(() => {
236
+ const sizeVariant = {
237
+ [InputSize.MD]: 'h-[36px]',
238
+ [InputSize.LG]: 'h-[44px]',
239
+ }
240
+ return sizeVariant[props.size as InputSize] || 'h-[36px]'
241
+ })
242
+
243
+ // Handlers
244
+ const handleFocus = () => {
245
+ isFocused.value = true
246
+ }
247
+
248
+ const handleBlur = () => {
249
+ isFocused.value = false
250
+
251
+ if (validationMode.value === FormValidationMode.BLUR) {
252
+ runValidation()
253
+ }
254
+ }
255
+
256
+ const handleInput = (event: Event) => {
257
+ if (props.disabled) return
258
+
259
+ const target = event.target as HTMLInputElement
260
+ let value = target.value
261
+
262
+ // Apply filtering + autoformatting for phone type field
263
+ if (props.pattern && props.type === 'tel') {
264
+ // Remove all non-digit characters
265
+ const rawValue = value.replace(/\D/g, '').slice(0, 10)
266
+
267
+ // Autoformat: XXX-XXX-XXXX
268
+ if (rawValue.length <= 3) {
269
+ value = rawValue
270
+ } else if (rawValue.length <= 6) {
271
+ value = `${rawValue.slice(0, 3)}-${rawValue.slice(3)}`
272
+ } else {
273
+ value = `${rawValue.slice(0, 3)}-${rawValue.slice(3, 6)}-${rawValue.slice(6, 10)}`
274
+ }
275
+ }
276
+
277
+ // Apply optional filtering
278
+ if (props.filterAlphabetic) {
279
+ value = filterAlphabetic(value)
280
+ target.value = value
281
+ }
282
+
283
+ // Enforce non-negative numbers if applicable
284
+ if (props.type === 'number') {
285
+ let numericValue = Number.parseFloat(value)
286
+
287
+ // Handle invalid number input (e.g. empty or non-numeric)
288
+ if (Number.isNaN(numericValue)) {
289
+ numericValue = 0
290
+ }
291
+
292
+ // Enforce min/max manually if provided
293
+ const min = props.min === undefined ? undefined : Number.parseFloat(props.min)
294
+ const max = props.max === undefined ? undefined : Number.parseFloat(props.max)
295
+
296
+ if (!Number.isNaN(numericValue)) {
297
+ if (min !== undefined && numericValue < min) {
298
+ numericValue = min
299
+ }
300
+
301
+ if (max !== undefined && numericValue > max) {
302
+ numericValue = max
303
+ }
304
+
305
+ value = numericValue.toString()
306
+ target.value = value
307
+ }
308
+
309
+ // Prevent negative numbers if not permitted
310
+ if (!props.permitNegativeNumber && numericValue < 0) {
311
+ value = '0'
312
+ target.value = value
313
+ }
314
+ }
315
+
316
+ // Emit the updated value to the parent
317
+ emit('update:modelValue', value)
318
+ }
319
+
320
+ const handleSuffixClick = () => {
321
+ emit('click:suffix')
322
+ }
323
+
324
+ const runValidation = () => {
325
+ if (!props.required || !props.validator) return
326
+
327
+ const result = props.validator(props.modelValue)
328
+
329
+ emit('update:error', result ?? '')
330
+ }
331
+
332
+ // Watchers
333
+ watch(
334
+ () => props.modelValue,
335
+ value => {
336
+ if (validationMode.value === FormValidationMode.BLUR && props.validator) {
337
+ const result = props.validator(value)
338
+ emit('update:error', result ?? '')
339
+ }
340
+ }
341
+ )
342
+
343
+ // Props for the dynamic component
344
+ const dynamicProps = computed(() => {
345
+ const specificFields = [
346
+ 'number',
347
+ 'date',
348
+ 'datetime-local',
349
+ 'time',
350
+ 'month',
351
+ 'week'
352
+ ]
353
+
354
+ if(specificFields.includes(props.type)) {
355
+ return {
356
+ type: props.type,
357
+ placeholder: props.placeholder,
358
+ min: props.min,
359
+ max: props.max,
360
+ step: props.step,
361
+ pattern: props.pattern,
362
+ }
363
+ } else if(props.type === 'tel') {
364
+ return {
365
+ pattern: props.pattern,
366
+ }
367
+ } else {
368
+ return {}
369
+ }
370
+ })
371
+ </script>
@@ -0,0 +1,120 @@
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
+ hasError && 'text-text-error',
18
+ ]"
19
+ >
20
+ {{ label }}
21
+ </label>
22
+
23
+ <OptionButtonGroup
24
+ :buttons
25
+ :modelValue
26
+ :disabled
27
+ :styleType="buttonStyle"
28
+ :size="buttonSize"
29
+ :isRounded
30
+ :isMultiple
31
+ :hasAllButton
32
+ :allButtonText
33
+ :allButtonValue
34
+ @update:modelValue="emit('update:modelValue', $event)"
35
+ />
36
+
37
+ <!-- Help Text -->
38
+ <p
39
+ v-if="hasError || helpText"
40
+ :class="[
41
+ 'text-xs text-left',
42
+ hasError ? 'text-text-error' : 'text-text-neutral-subtle',
43
+ ]"
44
+ >
45
+ {{ hasError ? error : helpText }}
46
+ </p>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ // Props
52
+ const props = defineProps({
53
+ id: {
54
+ type: String as PropType<string>,
55
+ required: true,
56
+ },
57
+ label: String as PropType<string>,
58
+ helpText: String as PropType<string>,
59
+ buttons: Array as PropType<ToggleButton[]>,
60
+ modelValue: {
61
+ type: [String, Array] as PropType<string | string[]>,
62
+ required: true,
63
+ },
64
+ validator: {
65
+ type: Function as PropType<(value: unknown) => string | null>,
66
+ default: () => null,
67
+ },
68
+ error: {
69
+ type: String as PropType<string>,
70
+ default: '',
71
+ },
72
+ disabled: {
73
+ type: Boolean as PropType<boolean>,
74
+ default: false,
75
+ },
76
+ required: {
77
+ type: Boolean as PropType<boolean>,
78
+ default: false,
79
+ },
80
+ buttonStyle: {
81
+ type: String as PropType<
82
+ ButtonStyleType.NEUTRAL_OUTLINED | ButtonStyleType.PRIMARY_BRAND_SOFT
83
+ >,
84
+ default: ButtonStyleType.NEUTRAL_OUTLINED,
85
+ validator: (value: ButtonStyleType.NEUTRAL_OUTLINED | ButtonStyleType.PRIMARY_BRAND_SOFT) =>
86
+ [ButtonStyleType.NEUTRAL_OUTLINED, ButtonStyleType.PRIMARY_BRAND_SOFT].includes(value),
87
+ },
88
+ buttonSize: String as PropType<ButtonSize>,
89
+ isRounded: Boolean as PropType<boolean>,
90
+ isMultiple: Boolean as PropType<boolean>,
91
+ hasAllButton: Boolean as PropType<boolean>,
92
+ allButtonText: String as PropType<string>,
93
+ allButtonValue: String as PropType<string>,
94
+ })
95
+
96
+ // Emits
97
+ const emit = defineEmits(['update:modelValue', 'update:error'])
98
+
99
+ // Composables
100
+ const validationMode = useInjectedValidationMode()
101
+
102
+ // Computed States
103
+ const hasError = computed(() => props.error !== '')
104
+
105
+ // Methods
106
+ const runValidation = () => {
107
+ if (!props.required || !props.validator) return
108
+
109
+ const result = props.validator(props.modelValue)
110
+
111
+ emit('update:error', result ?? '')
112
+ }
113
+
114
+ // Watchers
115
+ watch(() => props.modelValue, () => {
116
+ if (validationMode.value === FormValidationMode.BLUR) {
117
+ runValidation()
118
+ }
119
+ })
120
+ </script>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <div class="w-full flex flex-col gap-4">
3
+ <div
4
+ v-for="(item, index) in items"
5
+ :key="index"
6
+ class="flex items-start gap-4"
7
+ >
8
+ <!-- Content slot -->
9
+ <div class="flex-grow">
10
+ <slot
11
+ :item="item"
12
+ :index="index"
13
+ />
14
+ </div>
15
+
16
+ <!-- Action buttons -->
17
+ <div class="flex flex-col items-center gap-2 mt-2">
18
+ <!-- Add button (show only for last item) -->
19
+ <ActionIconButton
20
+ v-if="index === items.length - 1"
21
+ :styleType="ButtonStyleType.NEUTRAL_OUTLINED"
22
+ icon="mdiPlusCircleOutline"
23
+ :size="ButtonSize.SM"
24
+ @click="addItem"
25
+ />
26
+
27
+ <!-- Delete button (show for all except when there's only one item) -->
28
+ <ActionIconButton
29
+ v-if="items.length > 1"
30
+ :styleType="ButtonStyleType.DELETE_SOFT"
31
+ icon="mdiMinusCircleOutline"
32
+ :size="ButtonSize.SM"
33
+ @click="removeItem(index)"
34
+ />
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ const props = defineProps({
42
+ modelValue: {
43
+ type: Array,
44
+ default: () => [],
45
+ },
46
+ defaultValue: {
47
+ type: Object,
48
+ default: () => ({}),
49
+ },
50
+ })
51
+
52
+ const emit = defineEmits(['update:modelValue'])
53
+
54
+ // Local items state
55
+ const items = ref([...props.modelValue])
56
+
57
+ // CORREGIDO: Flag para evitar bucles infinitos
58
+ const isUpdatingFromParent = ref(false)
59
+
60
+ // CORREGIDO: Watch para cambios externos con protección
61
+ watch(
62
+ () => props.modelValue,
63
+ (newValue) => {
64
+ if (!isUpdatingFromParent.value) {
65
+ items.value = [...newValue]
66
+ }
67
+ },
68
+ { deep: true }
69
+ )
70
+
71
+ // CORREGIDO: Watch para cambios internos con protección
72
+ watch(
73
+ items,
74
+ (newValue) => {
75
+ isUpdatingFromParent.value = true
76
+ emit('update:modelValue', newValue)
77
+ nextTick(() => {
78
+ isUpdatingFromParent.value = false
79
+ })
80
+ },
81
+ { deep: true }
82
+ )
83
+
84
+ // Add new item
85
+ const addItem = () => {
86
+ // CORREGIDO: Crear una copia profunda del defaultValue
87
+ const newItem = JSON.parse(JSON.stringify(props.defaultValue))
88
+ items.value.push(newItem)
89
+ }
90
+
91
+ // Remove item at index
92
+ const removeItem = (index: number) => {
93
+ if (items.value.length > 1) {
94
+ items.value.splice(index, 1)
95
+ }
96
+ }
97
+
98
+ // CORREGIDO: Inicialización más segura
99
+ if (props.modelValue.length === 0) {
100
+ // Solo inicializar si no hay datos del padre
101
+ nextTick(() => {
102
+ if (items.value.length === 0) {
103
+ addItem()
104
+ }
105
+ })
106
+ } else {
107
+ items.value = [...props.modelValue]
108
+ }
109
+ </script>