@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,205 @@
1
+ <template>
2
+ <div :class="[ 'flex flex-col', 'w-full', 'gap-2' ]">
3
+ <!-- Label -->
4
+ <legend
5
+ v-if="legend"
6
+ :class="[
7
+ 'text-sm',
8
+ 'font-semibold',
9
+ 'text-left',
10
+ ]"
11
+ >
12
+ {{ legend }}
13
+ </legend>
14
+ <!-- Checkbox with Label -->
15
+ <div
16
+ :class="[
17
+ 'flex items-center gap-3',
18
+ 'text-sm',
19
+ hasError ? 'text-text-error' : 'text-text-default'
20
+ ]"
21
+ >
22
+ <!-- Label (inverted)-->
23
+ <label
24
+ v-if="inverse"
25
+ :for="id"
26
+ :class="[
27
+ disabled && 'text-text-neutral-disabled',
28
+ labelSizeClass,
29
+ ]"
30
+ v-html="label"
31
+ />
32
+
33
+ <!-- Hidden Native Checkbox -->
34
+ <input
35
+ :id="id"
36
+ type="checkbox"
37
+ :checked="modelValue"
38
+ class="hidden"
39
+ :disabled="disabled"
40
+ @change="handleChange"
41
+ >
42
+
43
+ <!-- Custom Checkbox -->
44
+ <div
45
+ :class="[
46
+ 'flex items-center justify-center',
47
+ controlFieldSizeClass,
48
+ 'border',
49
+ 'rounded',
50
+ 'flex items-center justify-center',
51
+ 'transition-colors',
52
+ modelValue ? 'bg-background-primary-brand-checked border-border-primary-brand-active' : 'bg-neutral-white border-border-default',
53
+ disabled ? 'bg-background-neutral-disabled cursor-not-allowed' : 'cursor-pointer'
54
+ ]"
55
+ @click="toggleCheckbox"
56
+ >
57
+ <MdiIcon
58
+ v-if="modelValue"
59
+ icon="mdiCheckBold"
60
+ :size="checkboxIconSizeClass"
61
+ preserveAspectRatio="xMidYMid meet"
62
+ :class="disabled ? 'text-icon-neutral-disabled' : 'text-icon-neutral-on-filled-bg'"
63
+ />
64
+ </div>
65
+
66
+ <!-- Label (natural position) -->
67
+ <label
68
+ v-if="!inverse"
69
+ :for="id"
70
+ :class="[
71
+ disabled && 'text-text-neutral-disabled',
72
+ labelSizeClass,
73
+ ]"
74
+ v-html="label"
75
+ />
76
+ </div>
77
+
78
+ <!-- Help Text -->
79
+ <p
80
+ v-if="helpText || hasError"
81
+ :class="[
82
+ 'text-xs',
83
+ 'text-left',
84
+ hasError ? 'text-text-error' : 'text-text-neutral-subtler'
85
+ ]"
86
+ >
87
+ {{ hasError ? error : helpText }}
88
+ </p>
89
+ </div>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ // Props
94
+ const props = defineProps({
95
+ id: {
96
+ type: String as PropType<string>,
97
+ required: true,
98
+ },
99
+ label: {
100
+ type: String as PropType<string>,
101
+ default: 'Text',
102
+ },
103
+ legend: String as PropType<string>,
104
+ helpText: String as PropType<string>,
105
+ modelValue: {
106
+ type: Boolean as PropType<boolean>,
107
+ default: false,
108
+ },
109
+ validator: {
110
+ type: Function as PropType<(value: boolean) => string | null>,
111
+ default: () => null,
112
+ },
113
+ error: {
114
+ type: String as PropType<string>,
115
+ default: '',
116
+ },
117
+ disabled: {
118
+ type: Boolean as PropType<boolean>,
119
+ default: false,
120
+ },
121
+ required: {
122
+ type: Boolean as PropType<boolean>,
123
+ default: false,
124
+ },
125
+ size: {
126
+ type: String as PropType<ControlFieldSize>,
127
+ default: ControlFieldSize.MD,
128
+ validator: (value: ControlFieldSize) => Object.values(ControlFieldSize).includes(value),
129
+ },
130
+ inverse: { // Sets the checkbox on the right side of the text
131
+ type: Boolean as PropType<boolean>,
132
+ default: false,
133
+ },
134
+ })
135
+
136
+ // Emits
137
+ const emit = defineEmits(['update:modelValue', 'update:error'])
138
+
139
+ // Composables
140
+ const validationMode = useInjectedValidationMode()
141
+
142
+ // Computed
143
+ const hasError = computed(() => props.error !== '')
144
+
145
+ // Computed classes
146
+ const controlFieldSizeClass = computed(() => {
147
+ const sizeVariant = {
148
+ [ControlFieldSize.MD]: 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]',
149
+ [ControlFieldSize.LG]: 'w-[32px] h-[32px] min-w-[32px] min-h-[32px]',
150
+ }
151
+ return sizeVariant[props.size as ControlFieldSize] || 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]'
152
+ })
153
+
154
+ const checkboxIconSizeClass = computed(() => {
155
+ const sizeVariant = {
156
+ [ControlFieldSize.MD]: '16',
157
+ [ControlFieldSize.LG]: '20',
158
+ }
159
+ return sizeVariant[props.size as ControlFieldSize] || '16'
160
+ })
161
+
162
+ const labelSizeClass = computed(() => {
163
+ const sizeVariant = {
164
+ [ControlFieldSize.MD]: 'text-sm',
165
+ [ControlFieldSize.LG]: 'text-base',
166
+ }
167
+ return sizeVariant[props.size as ControlFieldSize] || 'text-sm'
168
+ })
169
+
170
+ // Handlers
171
+ const handleChange = () => {
172
+ if (validationMode.value === FormValidationMode.BLUR) {
173
+ runValidation()
174
+ }
175
+ }
176
+
177
+ const toggleCheckbox = () => {
178
+ if (props.disabled) return
179
+
180
+ if (validationMode.value === FormValidationMode.BLUR) {
181
+ runValidation()
182
+ }
183
+
184
+ emit('update:modelValue', !props.modelValue)
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) {
200
+ const result = props.validator(value)
201
+ emit('update:error', result ?? '')
202
+ }
203
+ }
204
+ )
205
+ </script>
@@ -0,0 +1,99 @@
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
+ <template v-if="!$slots.default">
23
+ <template v-if="!isEmpty">
24
+ <div
25
+ v-if="text"
26
+ class="flex gap-2 items-center"
27
+ >
28
+ <p
29
+ :class="[
30
+ 'text-sm',
31
+ ]"
32
+ >
33
+ {{ text }}
34
+ </p>
35
+
36
+ <ActionIconButton
37
+ v-if="hasCopyToClipboardButton"
38
+ icon="mdiContentCopy"
39
+ :size="ButtonSize.XS"
40
+ @click="copyToClipboard(text.toString(), copyToClipboardMessage)"
41
+ />
42
+ </div>
43
+ </template>
44
+ <template v-else>
45
+ <p
46
+ :class="[
47
+ 'text-sm text-text-neutral-subtle',
48
+ ]"
49
+ >
50
+ {{ emptyText }}
51
+ </p>
52
+ </template>
53
+ </template>
54
+
55
+ <!-- Slot for other type of items -->
56
+ <slot />
57
+
58
+ <!-- Help Text -->
59
+ <p
60
+ v-if="helpText"
61
+ :class="[
62
+ 'text-xs text-left',
63
+ 'text-text-neutral-subtle',
64
+ ]"
65
+ >
66
+ {{ helpText }}
67
+ </p>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ // Props
73
+ const props = defineProps({
74
+ id: {
75
+ type: String as PropType<string>,
76
+ required: true,
77
+ },
78
+ label: String as PropType<string>,
79
+ text: [String, Number] as PropType<string | number>,
80
+ emptyText: {
81
+ type: String as PropType<string>,
82
+ default: 'Not defined'
83
+ },
84
+ helpText: String as PropType<string>,
85
+ hasCopyToClipboardButton: {
86
+ type: Boolean as PropType<boolean>,
87
+ default: false
88
+ },
89
+ copyToClipboardMessage: {
90
+ type: String as PropType<string>,
91
+ default: 'Copied to clipboard'
92
+ },
93
+ })
94
+
95
+ // Computed
96
+ const isEmpty = computed(() => {
97
+ return props.text === '' || props.text === null || props.text === undefined || props.text === 0
98
+ })
99
+ </script>
@@ -0,0 +1,326 @@
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="['text-sm', 'font-semibold', 'text-left', hasError && 'text-text-error']"
8
+ >
9
+ {{ label }}
10
+ </label>
11
+
12
+ <!-- Dropzone Container -->
13
+ <div class="flex w-full gap-4">
14
+ <div
15
+ v-if="showPreview && previewImageUrl && selectedFiles.length === 0"
16
+ :class="[
17
+ 'flex flex-col items-center gap-2',
18
+ 'border border-border-default rounded-lg p-2',
19
+ 'bg-background-neutral-subtle',
20
+ previewContainerClasses,
21
+ ]"
22
+ >
23
+ <img
24
+ :src="previewImageUrl"
25
+ :alt="label || 'Preview'"
26
+ class="w-full h-full object-cover rounded-md"
27
+ >
28
+ </div>
29
+
30
+ <div
31
+ :class="[
32
+ 'flex flex-col gap-2',
33
+ showPreview && previewImageUrl && selectedFiles.length === 0
34
+ ? 'flex-1'
35
+ : 'w-full',
36
+ ]"
37
+ >
38
+ <Vue3Dropzone
39
+ :key="dropzoneKey"
40
+ v-model="selectedFiles"
41
+ :multiple
42
+ :accept="normalizedAccept"
43
+ :maxFileSize
44
+ :maxFiles
45
+ :disabled
46
+ :height="selectedFiles.length === 0 ? 'auto' : undefined"
47
+ :class="['w-full']"
48
+ @error="handleError"
49
+ >
50
+ <template #placeholder-img>
51
+ <MdiIcon
52
+ :icon="icon"
53
+ preserveAspectRatio="xMidYMid meet"
54
+ class="min-w-[40px] min-h-[40px] aspect-square text-icon-default"
55
+ />
56
+ </template>
57
+ <template #title>
58
+ <p class="text-center text-sm text-text-default font-medium">
59
+ {{ computedTitleText }}
60
+ </p>
61
+ </template>
62
+ <template #button="{ fileInput }">
63
+ <div class="w-full flex justify-center my-4">
64
+ <ActionButton
65
+ :text="computedButtonText"
66
+ :styleType="ButtonStyleType.PRIMARY_BRAND_SOFT"
67
+ :disabled="disabled"
68
+ @click="fileInput?.click()"
69
+ />
70
+ </div>
71
+ </template>
72
+ <template #description>
73
+ <p class="text-xs text-text-neutral-subtle text-center">
74
+ {{ acceptedFileTypes }} {{ upToText }} {{ maxFileSize }}MB
75
+ </p>
76
+ </template>
77
+ <!-- <template v-if="!isImageFile" #preview>
78
+ <div class="w-full flex flex-col gap-2">
79
+ <div
80
+ v-for="(file, index) in selectedFiles"
81
+ :key="index"
82
+ class="flex flex-row gap-3 p-3 border border-border-default rounded-md bg-background-neutral w-full"
83
+ >
84
+ <div class="flex items-start justify-center pt-1">
85
+ <MdiIcon
86
+ icon="mdiFileDocumentOutline"
87
+ size="24"
88
+ class="text-icon-default min-w-[24px]"
89
+ />
90
+ </div>
91
+
92
+ <div class="flex flex-col flex-1 overflow-hidden">
93
+ <p class="text-sm font-medium break-words whitespace-normal leading-snug mt-1.5 select-none">
94
+ {{ file.name || (file as any)?.file?.name || 'Unnamed file' }}
95
+ </p>
96
+ </div>
97
+
98
+ <ActionIconButton
99
+ icon="mdiClose"
100
+ :styleType="ButtonStyleType.DELETE_FILLED"
101
+ :size="ButtonSize.SM"
102
+ @click="removeFile(index)"
103
+ />
104
+ </div>
105
+ </div>
106
+ </template> -->
107
+ </Vue3Dropzone>
108
+
109
+ <!-- Help Text -->
110
+ <p
111
+ v-if="hasError || helpText"
112
+ :class="[
113
+ 'text-xs text-left',
114
+ hasError ? 'text-text-error' : 'text-text-neutral-subtle',
115
+ ]"
116
+ >
117
+ {{ hasError ? error : helpText }}
118
+ </p>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+
124
+ <script setup lang="ts">
125
+ // Imports
126
+ //@ts-ignore
127
+ import Vue3Dropzone from '@jaxtheprime/vue3-dropzone'
128
+ import '@jaxtheprime/vue3-dropzone/dist/style.css'
129
+
130
+ // Props
131
+ const props = defineProps({
132
+ id: {
133
+ type: String as PropType<string>,
134
+ required: true,
135
+ },
136
+ label: String as PropType<string>,
137
+ title: {
138
+ type: String as PropType<string>,
139
+ default: undefined,
140
+ },
141
+ helpText: String as PropType<string>,
142
+ icon: {
143
+ type: String as PropType<any>,
144
+ default: 'mdiUploadOutline',
145
+ },
146
+ buttonText: String as PropType<string>,
147
+ upToText: {
148
+ type: String as PropType<string>,
149
+ default: 'up to',
150
+ },
151
+ modelValue: {
152
+ type: Array as () => File[],
153
+ default: () => [],
154
+ },
155
+ validator: {
156
+ type: Function as PropType<(value: unknown) => string | null>,
157
+ default: () => null,
158
+ },
159
+ error: {
160
+ type: String as PropType<string>,
161
+ default: '',
162
+ },
163
+ disabled: {
164
+ type: Boolean as PropType<boolean>,
165
+ default: false,
166
+ },
167
+ required: {
168
+ type: Boolean as PropType<boolean>,
169
+ default: false,
170
+ },
171
+ multiple: {
172
+ type: Boolean as PropType<boolean>,
173
+ default: false,
174
+ },
175
+ accept: {
176
+ type: [String, Array] as PropType<string | string[]>,
177
+ default: () => ['application/pdf'],
178
+ },
179
+ maxFileSize: {
180
+ type: Number as PropType<number>,
181
+ default: 5,
182
+ },
183
+ maxFiles: {
184
+ type: Number as PropType<number>,
185
+ default: 1,
186
+ },
187
+ fileUploadErrorMessage: {
188
+ type: String as PropType<string>,
189
+ default: 'The size or format of one ore more files is incorrect.',
190
+ },
191
+ showPreview: {
192
+ type: Boolean as PropType<boolean>,
193
+ default: false,
194
+ },
195
+ previewImageUrl: String as PropType<string>,
196
+ previewContainerClasses: {
197
+ type: String as PropType<string>,
198
+ default: 'w-[120px] h-[120px] min-w-[120px]',
199
+ },
200
+ })
201
+
202
+ // Emits
203
+ const emit = defineEmits(['update:modelValue', 'update:error'])
204
+
205
+ // States
206
+ const dropzoneKey = ref(0) // Used to re-render the Dropzone if invalid filetype is passed
207
+
208
+ // Composables
209
+ const validationMode = useInjectedValidationMode()
210
+
211
+ // Initialize toast
212
+ const { $toast } = useNuxtApp()
213
+
214
+ // Computed
215
+ const hasError = computed(() => props.error !== '')
216
+
217
+ // Used to check if the file is an image or not
218
+ const isImageFile = computed(() => {
219
+ const accepted = Array.isArray(props.accept) ? props.accept : [props.accept]
220
+
221
+ const acceptsImages = accepted.some(type =>
222
+ typeof type === 'string' && type.startsWith('image/')
223
+ )
224
+
225
+ if (!acceptsImages) return false
226
+
227
+ return selectedFiles.value.length > 0 &&
228
+ selectedFiles.value.every(file => {
229
+ const realFile = (file as any)?.file || file
230
+ return realFile?.type?.startsWith('image/')
231
+ })
232
+ })
233
+
234
+ // Converts Array of strings into a string
235
+ const normalizedAccept = computed(() => {
236
+ return Array.isArray(props.accept) ? props.accept.join(',') : props.accept
237
+ })
238
+
239
+ // Used for description
240
+ const acceptedFileTypes = computed(() => {
241
+ return (Array.isArray(props.accept) ? props.accept : [props.accept])
242
+ .map(ext => {
243
+ // Extract file extension from MIME type
244
+ const match = ext.match(/\/([a-zA-Z0-9]+)/)
245
+ return match ? match[1].toUpperCase() : ext.toUpperCase()
246
+ })
247
+ .join(', ')
248
+ })
249
+
250
+ const computedTitleText = computed(() => {
251
+ if (props.showPreview && props.previewImageUrl && selectedFiles.value.length === 0) {
252
+ return props.title || 'Upload a new file to replace current one'
253
+ }
254
+
255
+ if (!props.title) {
256
+ if (props.multiple) return 'Drag & drop files here or click to upload'
257
+ else return 'Drag & drop a file here or click to upload'
258
+ } else return props.title
259
+ })
260
+
261
+ const computedButtonText = computed(() => {
262
+ if (props.showPreview && props.previewImageUrl && selectedFiles.value.length === 0) {
263
+ return props.buttonText || 'Replace file'
264
+ }
265
+
266
+ if (!props.buttonText) {
267
+ if (props.multiple) return 'Upload files'
268
+ else return 'Upload a file'
269
+ } else return props.buttonText
270
+ })
271
+
272
+ const selectedFiles = computed({
273
+ get: () => props.modelValue,
274
+ set: (newFiles: File[]) => {
275
+ emit('update:modelValue', newFiles)
276
+
277
+ if (props.required) {
278
+ runValidation()
279
+ }
280
+
281
+ if (newFiles.length === 0) {
282
+ dropzoneKey.value++ // This forces Vue3Dropzone to re-render
283
+ }
284
+ },
285
+ })
286
+
287
+ // Handlers
288
+ const handleError = (error: File[]) => {
289
+ if (error) {
290
+ $toast.error(props.fileUploadErrorMessage, { toastId: 'files-error' })
291
+ }
292
+ }
293
+
294
+
295
+ // Validation logic
296
+ const runValidation = () => {
297
+ if (!props.required || !props.validator) return
298
+
299
+ const result = props.validator(props.modelValue)
300
+
301
+ emit('update:error', result ?? '')
302
+ }
303
+
304
+
305
+ watch(selectedFiles, newFiles => {
306
+ if (props.required && selectedFiles && validationMode.value === FormValidationMode.BLUR) {
307
+ runValidation()
308
+ }
309
+
310
+ const hasInvalidFile = newFiles.some(file => {
311
+ const actualFile = (file as any).file || file
312
+
313
+ const isMimeAccepted = Array.isArray(props.accept)
314
+ ? props.accept.includes(actualFile.type)
315
+ : actualFile.type === props.accept
316
+
317
+ return !isMimeAccepted
318
+ })
319
+
320
+ // If one file is invalid, clears the selected files and forces Dropzone re-render
321
+ if (hasInvalidFile) {
322
+ emit('update:modelValue', [])
323
+ dropzoneKey.value++
324
+ }
325
+ })
326
+ </script>