@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,759 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'flex flex-col',
5
+ 'w-full',
6
+ 'gap-2',
7
+ ]"
8
+ >
9
+ <!-- Label wrapper -->
10
+ <div class="flex gap-2 justify-between">
11
+ <label
12
+ v-if="label"
13
+ :for="id"
14
+ :class="[
15
+ 'text-sm',
16
+ 'font-semibold',
17
+ 'text-left',
18
+ ]"
19
+ >
20
+ {{ label }}
21
+ </label>
22
+
23
+ <!-- Wrapper -->
24
+ <div class="flex items-center gap-2">
25
+
26
+ <!-- Ranged slider -->
27
+ <template v-if="type === SliderType.RANGE">
28
+ <!-- Minimum editable value -->
29
+ <div class="relative">
30
+ <input
31
+ v-if="editingIndex === 0"
32
+ ref="minInputRef"
33
+ v-model="editingValue"
34
+ type="number"
35
+ :min="min"
36
+ :max="max"
37
+ :step="stepSize"
38
+ :disabled="disabled"
39
+ :class="[
40
+ 'w-20',
41
+ 'px-2',
42
+ 'py-1',
43
+ 'text-sm',
44
+ 'font-semibold',
45
+ 'text-center',
46
+ 'border',
47
+ 'border-border-primary-brand-default',
48
+ 'rounded',
49
+ 'focus:outline-none',
50
+ 'focus:ring-2',
51
+ 'focus:ring-primary-brand-200',
52
+ 'focus:border-border-primary-brand-default',
53
+ ]"
54
+ @blur="stopEditing"
55
+ @keydown.enter="stopEditing"
56
+ @keydown.escape="cancelEditing"
57
+ >
58
+ <button
59
+ v-else
60
+ type="button"
61
+ :disabled="disabled"
62
+ :class="[
63
+ 'flex items-center justify-center',
64
+ 'min-w-[64px] px-2 py-1',
65
+ 'text-sm font-semibold',
66
+ 'bg-background-neutral-subtle',
67
+ 'border border-border-default',
68
+ 'rounded',
69
+ 'transition-all duration-200',
70
+ disabled
71
+ ? 'opacity-50 cursor-not-allowed'
72
+ : 'hover:bg-background-neutral-sublter hover:border-border-primary-brand-default hover:text-text-primary-brand-default cursor-pointer',
73
+ 'focus:outline-none focus:ring-2 focus:ring-primary-brand-200'
74
+ ]"
75
+ @click="startEditing(0)"
76
+ @keydown.enter="startEditing(0)"
77
+ @keydown.space.prevent="startEditing(0)"
78
+ >
79
+ <span>
80
+ {{ `${currentValuePrefix || ''}${(modelValue as [number, number])[0]}${currentValueSuffix || ''}` }}
81
+ </span>
82
+ <MdiIcon
83
+ icon="mdiPencil"
84
+ size="14px"
85
+ preserveAspectRatio="xMidYMid meet"
86
+ class="ml-1 opacity-60"
87
+ />
88
+ </button>
89
+
90
+ <!-- Tooltip -->
91
+ <div
92
+ v-if="!disabled && editingIndex !== 0"
93
+ :class="[
94
+ 'absolute',
95
+ '-top-8',
96
+ 'left-1/2',
97
+ 'transform',
98
+ '-translate-x-1/2',
99
+ 'opacity-0',
100
+ 'group-hover:opacity-100',
101
+ 'transition-opacity',
102
+ 'duration-200',
103
+ 'pointer-events-none',
104
+ ]"
105
+ >
106
+ <div
107
+ :class="[
108
+ 'bg-gray-800',
109
+ 'text-text-on-filled',
110
+ 'text-xs',
111
+ 'px-2',
112
+ 'py-1',
113
+ 'rounded',
114
+ 'whitespace-nowrap',
115
+ ]"
116
+ >
117
+ {{ $t('slider.clickToEdit') }}
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <span class="text-sm font-semibold text-text-neutral-subtle">
123
+ -
124
+ </span>
125
+
126
+ <!-- Maximum editable value -->
127
+ <div class="relative">
128
+ <input
129
+ v-if="editingIndex === 1"
130
+ ref="maxInputRef"
131
+ v-model="editingValue"
132
+ type="number"
133
+ :min="min"
134
+ :max="max"
135
+ :step="stepSize"
136
+ :disabled="disabled"
137
+ :class="[
138
+ 'w-20',
139
+ 'px-2',
140
+ 'py-1',
141
+ 'text-sm',
142
+ 'font-semibold',
143
+ 'text-center',
144
+ 'border',
145
+ 'border-border-primary-brand-default',
146
+ 'rounded',
147
+ 'focus:outline-none',
148
+ 'focus:ring-2',
149
+ 'focus:ring-primary-brand-200',
150
+ 'focus:border-border-primary-brand-default',
151
+ ]"
152
+ @blur="stopEditing"
153
+ @keydown.enter="stopEditing"
154
+ @keydown.escape="cancelEditing"
155
+ >
156
+ <button
157
+ v-else
158
+ type="button"
159
+ :disabled="disabled"
160
+ :class="[
161
+ 'flex items-center justify-center',
162
+ 'min-w-[64px] px-2 py-1',
163
+ 'text-sm font-semibold',
164
+ 'bg-background-neutral-subtle',
165
+ 'border border-border-default',
166
+ 'rounded',
167
+ 'transition-all duration-200',
168
+ disabled
169
+ ? 'opacity-50 cursor-not-allowed'
170
+ : 'hover:bg-background-neutral-sublter hover:border-border-primary-brand-default hover:text-text-primary-brand-default cursor-pointer',
171
+ 'focus:outline-none focus:ring-2 focus:ring-primary-brand-200'
172
+ ]"
173
+ @click="startEditing(1)"
174
+ @keydown.enter="startEditing(1)"
175
+ @keydown.space.prevent="startEditing(1)"
176
+ >
177
+ <span>{{ `${currentValuePrefix || ''}${(modelValue as [number, number])[1]}${currentValueSuffix || ''}` }}</span>
178
+ <MdiIcon
179
+ icon="mdiPencil"
180
+ size="14px"
181
+ preserveAspectRatio="xMidYMid meet"
182
+ class="ml-1 opacity-60"
183
+ />
184
+ </button>
185
+ </div>
186
+ </template>
187
+
188
+ <!-- Single slider -->
189
+ <template v-else>
190
+ <!-- Unique editable slider -->
191
+ <div class="relative group">
192
+ <input
193
+ v-if="editingIndex === 0"
194
+ ref="singleInputRef"
195
+ v-model="editingValue"
196
+ type="number"
197
+ :min="min"
198
+ :max="max"
199
+ :step="stepSize"
200
+ :disabled="disabled"
201
+ :class="[
202
+ 'w-24',
203
+ 'px-2',
204
+ 'py-1',
205
+ 'text-sm',
206
+ 'font-semibold',
207
+ 'text-center',
208
+ 'border',
209
+ 'border-border-primary-brand-default',
210
+ 'rounded',
211
+ 'focus:outline-none',
212
+ 'focus:ring-2',
213
+ 'focus:ring-primary-brand-200',
214
+ 'focus:border-border-primary-brand-default',
215
+ ]"
216
+ @blur="stopEditing"
217
+ @keydown.enter="stopEditing"
218
+ @keydown.escape="cancelEditing"
219
+ >
220
+ <button
221
+ v-else
222
+ type="button"
223
+ :disabled="disabled"
224
+ :class="[
225
+ 'group flex items-center justify-center',
226
+ 'min-w-[80px] px-3 py-1.5',
227
+ 'text-sm font-semibold',
228
+ 'bg-background-neutral-subtle',
229
+ 'border border-border-default',
230
+ 'rounded',
231
+ 'transition-all duration-200',
232
+ disabled
233
+ ? 'opacity-50 cursor-not-allowed'
234
+ : 'hover:bg-background-neutral-sublter hover:border-border-primary-brand-default hover:text-text-primary-brand-default cursor-pointer hover:shadow-sm',
235
+ 'focus:outline-none focus:ring-2 focus:ring-primary-brand-200'
236
+ ]"
237
+ @click="startEditing(0)"
238
+ @keydown.enter="startEditing(0)"
239
+ @keydown.space.prevent="startEditing(0)"
240
+ >
241
+ <span>
242
+ {{ `${currentValuePrefix || ''}${modelValue}${currentValueSuffix || ''}` }}
243
+ </span>
244
+ <MdiIcon
245
+ icon="mdiPencil"
246
+ size="14px"
247
+ preserveAspectRatio="xMidYMid meet"
248
+ :class="[
249
+ 'ml-1',
250
+ 'opacity-60',
251
+ 'group-hover:opacity-100',
252
+ 'transition-opacity',
253
+ 'duration-200',
254
+ ]"
255
+ />
256
+ </button>
257
+
258
+ <!-- Tooltip de ayuda -->
259
+ <div
260
+ v-if="!disabled && editingIndex !== 0"
261
+ :class="[
262
+ 'absolute',
263
+ '-top-8',
264
+ 'left-1/2',
265
+ 'transform',
266
+ '-translate-x-1/2',
267
+ 'opacity-0',
268
+ 'group-hover:opacity-100',
269
+ 'transition-opacity',
270
+ 'duration-200',
271
+ 'pointer-events-none',
272
+ 'z-10',
273
+ ]"
274
+ >
275
+ <div
276
+ :class="[
277
+ 'bg-gray-800',
278
+ 'text-white',
279
+ 'text-xs',
280
+ 'px-2',
281
+ 'py-1',
282
+ 'rounded',
283
+ 'whitespace-nowrap',
284
+ ]"
285
+ >
286
+ {{ $t('slider.clickToEdit') || 'Click para editar' }}
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </template>
291
+ </div>
292
+ </div>
293
+
294
+
295
+
296
+ <!-- Slider container -->
297
+ <div
298
+ ref="sliderRef"
299
+ :class="[
300
+ 'relative w-full h-4',
301
+ disabled && 'opacity-disabled cursor-not-allowed',
302
+ ]"
303
+ >
304
+ <!-- Base track -->
305
+ <div
306
+ :class="[
307
+ 'absolute',
308
+ 'top-1/2',
309
+ 'left-0',
310
+ 'w-full',
311
+ 'h-[4px]',
312
+ 'bg-background-neutral-sublter',
313
+ 'rounded-full',
314
+ '-translate-y-1/2',
315
+ ]"
316
+ />
317
+
318
+ <!-- Filled track (under thumbs) -->
319
+ <div
320
+ ref="filledTrackRef"
321
+ :class="[
322
+ 'absolute',
323
+ 'top-1/2',
324
+ 'h-[4px]',
325
+ 'bg-background-primary-brand-active',
326
+ 'rounded-full',
327
+ '-translate-y-1/2',
328
+ ]"
329
+ :style="trackStyle"
330
+ />
331
+
332
+ <!-- Thumbs -->
333
+ <template v-for="(val, index) in thumbValues" :key="index">
334
+ <div
335
+ :class="[
336
+ 'absolute',
337
+ 'top-1/2',
338
+ 'z-1',
339
+ 'w-[20px]',
340
+ 'h-[20px]',
341
+ 'bg-icon-neutral-on-filled-bg',
342
+ 'border-4',
343
+ 'border-border-primary-brand-default',
344
+ 'rounded-full',
345
+ disabled ? 'cursor-not-allowed' : 'cursor-pointer',
346
+ '-translate-y-1/2',
347
+ getThumbAlignClass(val),
348
+ ]"
349
+ :style="{ left: `${getThumbPosition(val)}%` }"
350
+ @mousedown.stop="startDrag(index)"
351
+ >
352
+ <div
353
+ v-if="showTooltip"
354
+ v-show="draggingIndex === index"
355
+ :class="[
356
+ 'absolute',
357
+ 'bottom-full',
358
+ 'mb-2',
359
+ 'px-2',
360
+ 'py-1',
361
+ 'text-xs',
362
+ 'font-semibold',
363
+ 'text-text-neutral-on-filled',
364
+ 'bg-[rgba(24,41,83,0.7)]',
365
+ 'rounded',
366
+ getTooltipAlignClass(val),
367
+ ]"
368
+ >
369
+ {{ `${currentValuePrefix || ''}${Math.round(val)}${currentValueSuffix || ''}` }}
370
+ </div>
371
+ </div>
372
+ </template>
373
+ </div>
374
+
375
+ <!-- Range info -->
376
+ <div
377
+ v-if="showRangeInfo"
378
+ class="flex gap-2 justify-between text-xs text-text-neutral-subtle"
379
+ >
380
+ <span>
381
+ {{ minText }} {{ `${currentValuePrefix || ''}${min}${currentValueSuffix || ''}` }}
382
+ </span>
383
+ <span>
384
+ {{ maxText }} {{ `${currentValuePrefix || ''}${max}${currentValueSuffix || ''}` }}
385
+ </span>
386
+ </div>
387
+
388
+ <!-- Help Text -->
389
+ <p
390
+ v-if="helpText"
391
+ :class="[
392
+ 'text-xs text-left',
393
+ 'text-text-neutral-subtle',
394
+ ]"
395
+ >
396
+ {{ helpText }}
397
+ </p>
398
+ </div>
399
+ </template>
400
+
401
+ <script setup lang="ts">
402
+ // filepath: c:\Users\David\Documents\Trabajo\digis_front_new\components\forms\fields\SliderField.vue
403
+
404
+ // Props
405
+ const props = defineProps({
406
+ id: {
407
+ type: String as PropType<string>,
408
+ required: true,
409
+ },
410
+ label: String as PropType<string>,
411
+ helpText: String as PropType<string>,
412
+ required: {
413
+ type: Boolean as PropType<boolean>,
414
+ default: false,
415
+ },
416
+ modelValue: {
417
+ type: [Number, Array] as PropType<number | [number, number]>,
418
+ default: 0,
419
+ },
420
+ type: {
421
+ type: String as PropType<SliderType>,
422
+ default: SliderType.SINGLE,
423
+ validator: (value: SliderType) => Object.values(SliderType).includes(value),
424
+ },
425
+ steps: {
426
+ type: Number as PropType<number>,
427
+ default: 1,
428
+ },
429
+ min: {
430
+ type: Number as PropType<number>,
431
+ default: 0,
432
+ },
433
+ minText: {
434
+ type: String as PropType<string>,
435
+ default: 'Min: ',
436
+ },
437
+ max: {
438
+ type: Number as PropType<number>,
439
+ default: 100,
440
+ },
441
+ maxText: {
442
+ type: String as PropType<string>,
443
+ default: 'Max: ',
444
+ },
445
+ currentValuePrefix: String as PropType<string>,
446
+ currentValueSuffix: String as PropType<string>,
447
+ showTooltip: {
448
+ type: Boolean as PropType<boolean>,
449
+ default: false,
450
+ },
451
+ showRangeInfo: {
452
+ type: Boolean as PropType<boolean>,
453
+ default: true,
454
+ },
455
+ disabled: {
456
+ type: Boolean as PropType<boolean>,
457
+ default: false,
458
+ },
459
+ })
460
+
461
+ // Emits
462
+ const emit = defineEmits(['update:modelValue'])
463
+
464
+ // States
465
+ const sliderRef = ref<HTMLElement | null>(null)
466
+ const filledTrackRef = ref<HTMLElement | null>(null)
467
+ const draggingIndex = ref<number | null>(null)
468
+ const sliderWidth = ref(0)
469
+ const trackWidth = ref(0)
470
+
471
+ // AÑADIDO: Estados para la edición de valores
472
+ const editingIndex = ref<number | null>(null)
473
+ const editingValue = ref<string>('')
474
+ const originalValue = ref<number | [number, number]>()
475
+
476
+ // AÑADIDO: Referencias a los inputs
477
+ const minInputRef = ref<HTMLInputElement | null>(null)
478
+ const maxInputRef = ref<HTMLInputElement | null>(null)
479
+ const singleInputRef = ref<HTMLInputElement | null>(null)
480
+
481
+ // Computed
482
+ const thumbValues = computed(() => {
483
+ return props.type === SliderType.RANGE
484
+ ? (props.modelValue as [number, number])
485
+ : [props.modelValue as number]
486
+ })
487
+
488
+ const stepSize = computed(() => {
489
+ if (props.steps <= 1) return 1
490
+ return (props.max - props.min) / (props.steps - 1)
491
+ })
492
+
493
+ // Methods
494
+ const percentage = (value: number) => {
495
+ return ((value - props.min) / (props.max - props.min)) * 100
496
+ }
497
+
498
+ const getThumbAlignClass = (value: number) => {
499
+ const percent = percentage(value)
500
+ if (percent <= 5) return 'left-0'
501
+ if (percent >= 95) return '-translate-x-[20px]'
502
+ return 'left-1/2 -translate-x-1/2'
503
+ }
504
+
505
+ const getTooltipAlignClass = (value: number) => {
506
+ const percent = percentage(value)
507
+ return percent >= 50 ? '-translate-x-1/2' : ''
508
+ }
509
+
510
+ const getThumbPosition = (value: number) => {
511
+ return percentage(value)
512
+ }
513
+
514
+ const trackStyle = computed(() => {
515
+ if (props.type === SliderType.RANGE) {
516
+ const [start, end] = thumbValues.value
517
+ const startPercent = percentage(start)
518
+ const endPercent = percentage(end)
519
+
520
+ return {
521
+ left: `${startPercent}%`,
522
+ width: `${endPercent - startPercent}%`,
523
+ }
524
+ } else {
525
+ const percent = percentage(thumbValues.value[0])
526
+ return {
527
+ left: '0%',
528
+ width: `${percent}%`,
529
+ }
530
+ }
531
+ })
532
+
533
+ const startDrag = (index: number) => {
534
+ if (props.disabled) return
535
+
536
+ draggingIndex.value = index
537
+ document.addEventListener('mousemove', onDrag)
538
+ document.addEventListener('mouseup', stopDrag)
539
+ }
540
+
541
+ const onDrag = (event: MouseEvent) => {
542
+ if (draggingIndex.value === null || !sliderRef.value) return
543
+
544
+ const rect = sliderRef.value.getBoundingClientRect()
545
+ const percent = (event.clientX - rect.left) / rect.width
546
+ const rawValue = props.min + percent * (props.max - props.min)
547
+
548
+ let clamped = Math.max(props.min, Math.min(rawValue, props.max))
549
+
550
+ if (props.type === SliderType.SINGLE) {
551
+ const step = stepSize.value
552
+ const stepsFromMin = Math.round((clamped - props.min) / step)
553
+ clamped = props.min + stepsFromMin * step
554
+ clamped = Math.round(clamped)
555
+ emit('update:modelValue', clamped)
556
+ } else {
557
+ // MODIFICADO: Validación para rangos durante el drag
558
+ const range = [...(thumbValues.value as [number, number])]
559
+ const roundedValue = Math.round(clamped)
560
+
561
+ if (draggingIndex.value === 0) {
562
+ // Arrastrando el thumb mínimo
563
+ range[0] = Math.min(roundedValue, range[1] - 1) // Asegurar que sea menor que el máximo
564
+ } else if (draggingIndex.value === 1) {
565
+ // Arrastrando el thumb máximo
566
+ range[1] = Math.max(roundedValue, range[0] + 1) // Asegurar que sea mayor que el mínimo
567
+ }
568
+
569
+ emit('update:modelValue', range)
570
+ }
571
+ }
572
+
573
+ const stopDrag = () => {
574
+ draggingIndex.value = null
575
+ document.removeEventListener('mousemove', onDrag)
576
+ document.removeEventListener('mouseup', stopDrag)
577
+ }
578
+
579
+
580
+ const cancelEditing = () => {
581
+ editingIndex.value = null
582
+ editingValue.value = ''
583
+
584
+ // Restaurar el valor original si es necesario
585
+ if (originalValue.value !== undefined) {
586
+ emit('update:modelValue', originalValue.value)
587
+ }
588
+ }
589
+
590
+ onMounted(() => {
591
+ if (sliderRef.value) {
592
+ sliderWidth.value = sliderRef.value.offsetWidth
593
+ }
594
+ if (filledTrackRef.value) {
595
+ trackWidth.value = filledTrackRef.value.offsetWidth
596
+ }
597
+ })
598
+
599
+ watch(() => props.modelValue, () => {
600
+ if (sliderRef.value) {
601
+ sliderWidth.value = sliderRef.value.offsetWidth
602
+ }
603
+ if (filledTrackRef.value) {
604
+ trackWidth.value = filledTrackRef.value.offsetWidth
605
+ }
606
+ })
607
+
608
+ // MODIFICADO: Método startEditing mejorado
609
+ const startEditing = (index: number) => {
610
+ if (props.disabled) return
611
+
612
+ editingIndex.value = index
613
+ originalValue.value = props.type === SliderType.RANGE
614
+ ? [...(props.modelValue as [number, number])]
615
+ : props.modelValue as number
616
+
617
+ if (props.type === SliderType.RANGE) {
618
+ editingValue.value = String((props.modelValue as [number, number])[index])
619
+ } else {
620
+ editingValue.value = String(props.modelValue as number)
621
+ }
622
+
623
+ // Focus el input en el siguiente tick
624
+ nextTick(() => {
625
+ if (index === 0 && minInputRef.value) {
626
+ minInputRef.value.focus()
627
+ minInputRef.value.select()
628
+ } else if (index === 1 && maxInputRef.value) {
629
+ maxInputRef.value.focus()
630
+ maxInputRef.value.select()
631
+ } else if (singleInputRef.value) {
632
+ singleInputRef.value.focus()
633
+ singleInputRef.value.select()
634
+ }
635
+ })
636
+ }
637
+
638
+ // MODIFICADO: Método stopEditing con mejor feedback
639
+ const stopEditing = () => {
640
+ if (editingIndex.value === null) return
641
+
642
+ const newValue = parseFloat(editingValue.value)
643
+
644
+ // Validar que el valor esté dentro del rango permitido
645
+ if (isNaN(newValue) || newValue < props.min || newValue > props.max) {
646
+ showEditingError('Valor fuera del rango permitido')
647
+ return
648
+ }
649
+
650
+ // Aplicar el step si está definido
651
+ let adjustedValue = newValue
652
+ if (props.steps > 1) {
653
+ const step = stepSize.value
654
+ const stepsFromMin = Math.round((newValue - props.min) / step)
655
+ adjustedValue = props.min + stepsFromMin * step
656
+ }
657
+
658
+ // AÑADIDO: Validación específica para rangos
659
+ if (props.type === SliderType.RANGE) {
660
+ const currentRange = [...(props.modelValue as [number, number])]
661
+
662
+ if (editingIndex.value === 0) {
663
+ // Editando valor mínimo - debe ser menor que el máximo
664
+ if (adjustedValue >= currentRange[1]) {
665
+ showEditingError('El valor mínimo debe ser menor que el máximo')
666
+ return
667
+ }
668
+ } else if (editingIndex.value === 1) {
669
+ // Editando valor máximo - debe ser mayor que el mínimo
670
+ if (adjustedValue <= currentRange[0]) {
671
+ showEditingError('El valor máximo debe ser mayor que el mínimo')
672
+ return
673
+ }
674
+ }
675
+
676
+ // Si llegamos aquí, el valor es válido
677
+ currentRange[editingIndex.value] = adjustedValue
678
+ emit('update:modelValue', currentRange)
679
+ } else {
680
+ // Para slider simple, solo actualizar el valor
681
+ emit('update:modelValue', adjustedValue)
682
+ }
683
+
684
+ // Limpiar estado de edición
685
+ editingIndex.value = null
686
+ editingValue.value = ''
687
+ }
688
+
689
+ // AÑADIDO: Función para mostrar errores de edición
690
+ const showEditingError = (message: string) => {
691
+ const inputElement = editingIndex.value === 0 ? minInputRef.value :
692
+ editingIndex.value === 1 ? maxInputRef.value :
693
+ singleInputRef.value
694
+
695
+ if (inputElement) {
696
+ // Mostrar error visual
697
+ inputElement.classList.add('border-red-500', 'bg-red-50')
698
+
699
+ // Crear tooltip de error temporal
700
+ const errorTooltip = document.createElement('div')
701
+ errorTooltip.className = 'absolute -top-8 left-1/2 transform -translate-x-1/2 bg-red-600 text-white text-xs px-2 py-1 rounded whitespace-nowrap z-20'
702
+ errorTooltip.textContent = message
703
+
704
+ const container = inputElement.parentElement
705
+ if (container) {
706
+ container.style.position = 'relative'
707
+ container.appendChild(errorTooltip)
708
+ }
709
+
710
+ // Limpiar después de 3 segundos
711
+ setTimeout(() => {
712
+ inputElement.classList.remove('border-red-500', 'bg-red-50')
713
+ if (errorTooltip.parentElement) {
714
+ errorTooltip.parentElement.removeChild(errorTooltip)
715
+ }
716
+ }, 3000)
717
+ }
718
+
719
+ // Restaurar valor original después de un momento
720
+ setTimeout(() => {
721
+ cancelEditing()
722
+ }, 1500)
723
+ }
724
+
725
+ // AÑADIDO: Función para validar el rango completo
726
+ const validateRange = (range: [number, number]): [number, number] => {
727
+ let [min, max] = range
728
+
729
+ // Asegurar que estén dentro de los límites
730
+ min = Math.max(props.min, Math.min(min, props.max))
731
+ max = Math.max(props.min, Math.min(max, props.max))
732
+
733
+ // Asegurar que el máximo sea mayor que el mínimo
734
+ if (max <= min) {
735
+ max = min + 1
736
+ // Si el máximo excede el límite, ajustar el mínimo
737
+ if (max > props.max) {
738
+ max = props.max
739
+ min = max - 1
740
+ }
741
+ }
742
+
743
+ return [min, max]
744
+ }
745
+
746
+ // AÑADIDO: Watcher para validar cambios externos en el modelValue
747
+ watch(() => props.modelValue, (newValue) => {
748
+ if (props.type === SliderType.RANGE && Array.isArray(newValue)) {
749
+ const validatedRange = validateRange(newValue as [number, number])
750
+
751
+ // Solo emitir si hay cambios
752
+ if (validatedRange[0] !== newValue[0] || validatedRange[1] !== newValue[1]) {
753
+ nextTick(() => {
754
+ emit('update:modelValue', validatedRange)
755
+ })
756
+ }
757
+ }
758
+ }, { deep: true })
759
+ </script>