@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.
- package/assets/css/defaults.css +55 -0
- package/assets/css/main.css +238 -0
- package/assets/css/theme/colors.css +106 -0
- package/assets/css/theme/primitives.css +105 -0
- package/assets/css/theme/ui-theme.css +454 -0
- package/assets/images/placeholders/missing-image-placeholder.png +0 -0
- package/components/accordions/Accordion.vue +31 -0
- package/components/accordions/AccordionGroup.vue +78 -0
- package/components/accordions/AccordionItem.vue +39 -0
- package/components/action-panels/ActionPanel.vue +49 -0
- package/components/alerts/Alert.vue +159 -0
- package/components/avatars/Avatar.vue +152 -0
- package/components/avatars/AvatarStack.vue +97 -0
- package/components/avatars/AvatarStackCounter.vue +74 -0
- package/components/badges/Badge.vue +221 -0
- package/components/badges/BadgeStack.vue +110 -0
- package/components/badges/IconBadge.vue +57 -0
- package/components/badges/IconTextBadge.vue +50 -0
- package/components/breadcrumbs/Breadcrumbs.vue +54 -0
- package/components/buttons/ActionButton.vue +395 -0
- package/components/buttons/ActionIconButton.vue +283 -0
- package/components/buttons/AlertButton.vue +125 -0
- package/components/buttons/AlertIconButton.vue +105 -0
- package/components/buttons/PaginationButton.vue +45 -0
- package/components/buttons/options/OptionButton.vue +61 -0
- package/components/buttons/options/OptionButtonGroup.vue +155 -0
- package/components/buttons/options/OptionButtonSlider.vue +154 -0
- package/components/buttons/toggle/ToggleButton.vue +142 -0
- package/components/buttons/toggle/ToggleButtonGroup.vue +73 -0
- package/components/cards/Card.vue +33 -0
- package/components/cards/CardActions.vue +5 -0
- package/components/cards/CardBody.vue +5 -0
- package/components/cards/CardFooter.vue +20 -0
- package/components/cards/CardHeader.vue +5 -0
- package/components/cards/CardTitle.vue +13 -0
- package/components/cards/specific/ContactDetailsCard.vue +47 -0
- package/components/cards/specific/FeatureCard.vue +59 -0
- package/components/cards/specific/HelpTopicCard.vue +62 -0
- package/components/cards/specific/MetricCard.vue +42 -0
- package/components/cards/specific/TestimonialCard.vue +57 -0
- package/components/cards/specific/subscription/CurrentActiveSubscriptionCard.vue +105 -0
- package/components/cards/specific/subscription/SubscriptionPlanCard.vue +178 -0
- package/components/cards/specific/subscription/UniqueSubscriptionPlanCard.vue +106 -0
- package/components/collapsibles/Collapsible.vue +33 -0
- package/components/content/ContentItem.vue +144 -0
- package/components/content/ContentItemImage.vue +125 -0
- package/components/dividers/Divider.vue +35 -0
- package/components/dividers/TextLineDivider.vue +58 -0
- package/components/dropdowns/DropdownMenu.vue +207 -0
- package/components/dropdowns/DropdownMenuActions.vue +11 -0
- package/components/dropdowns/DropdownMenuItem.vue +240 -0
- package/components/dropdowns/DropdownSelect.vue +469 -0
- package/components/dropdowns/DropdownSelectItem.vue +182 -0
- package/components/empty-states/EmptyState.vue +170 -0
- package/components/features/Feature.vue +77 -0
- package/components/forms/DataDetails.vue +7 -0
- package/components/forms/DataDetailsActions.vue +23 -0
- package/components/forms/DataDetailsFieldGroup.vue +35 -0
- package/components/forms/DataDetailsRow.vue +22 -0
- package/components/forms/Form.vue +25 -0
- package/components/forms/FormActions.vue +23 -0
- package/components/forms/FormFieldGroup.vue +35 -0
- package/components/forms/FormRow.vue +22 -0
- package/components/forms/fields/ButtonField.vue +119 -0
- package/components/forms/fields/CheckboxField.vue +205 -0
- package/components/forms/fields/DataField.vue +99 -0
- package/components/forms/fields/FileUploadField.vue +326 -0
- package/components/forms/fields/InputField.vue +371 -0
- package/components/forms/fields/OptionButtonsGroupField.vue +120 -0
- package/components/forms/fields/RepeaterField.vue +109 -0
- package/components/forms/fields/SearchField.vue +184 -0
- package/components/forms/fields/SelectField.vue +233 -0
- package/components/forms/fields/SliderField.vue +759 -0
- package/components/forms/fields/SwitchField.vue +257 -0
- package/components/forms/fields/TextareaField.vue +205 -0
- package/components/forms/fields/ToggleButtonsGroupField.vue +65 -0
- package/components/forms/fields/radio/RadioButtonField.vue +238 -0
- package/components/forms/fields/radio/RadioField.vue +157 -0
- package/components/forms/fields/radio/RadioGroupField.vue +156 -0
- package/components/icons/ContainedIcon.vue +130 -0
- package/components/images/QRCode.vue +124 -0
- package/components/layouts/ContainerWrapper.vue +13 -0
- package/components/layouts/ContentBody.vue +30 -0
- package/components/layouts/Grid.vue +25 -0
- package/components/layouts/Heading.vue +159 -0
- package/components/layouts/MainContent.vue +26 -0
- package/components/layouts/MaxWidthContainer.vue +15 -0
- package/components/layouts/Overtitle.vue +25 -0
- package/components/layouts/headers/CompactHeader.vue +181 -0
- package/components/layouts/headers/PageHeader.vue +102 -0
- package/components/layouts/headers/WebAppHeader.vue +54 -0
- package/components/layouts/section/Section.vue +90 -0
- package/components/layouts/section/SectionBody.vue +12 -0
- package/components/layouts/section/SectionHeader.vue +12 -0
- package/components/layouts/section/SectionTitle.vue +13 -0
- package/components/lists/List.vue +69 -0
- package/components/lists/ListItem.vue +58 -0
- package/components/loaders/Loading.vue +83 -0
- package/components/loaders/LoadingScreen.vue +285 -0
- package/components/modals/DangerModalDialog.vue +149 -0
- package/components/modals/InfoModalDialog.vue +143 -0
- package/components/modals/ModalActions.vue +22 -0
- package/components/modals/ModalContent.vue +5 -0
- package/components/modals/ModalDescription.vue +5 -0
- package/components/modals/ModalDialog.vue +122 -0
- package/components/modals/ModalHeaderGroup.vue +19 -0
- package/components/modals/ModalHeadings.vue +5 -0
- package/components/modals/ModalSubtitle.vue +14 -0
- package/components/modals/ModalTitle.vue +14 -0
- package/components/modals/SuccessModalDialog.vue +90 -0
- package/components/modules/AppLogo.vue +46 -0
- package/components/modules/SVGImage.vue +44 -0
- package/components/navigation/links/NavLink.vue +112 -0
- package/components/navigation/nav-menu/NavFooterMenu.vue +91 -0
- package/components/navigation/nav-menu/NavMenu.vue +36 -0
- package/components/navigation/nav-menu/NavMenuItem.vue +44 -0
- package/components/navigation/nav-sidebar/BottomUserNavBar.vue +83 -0
- package/components/navigation/nav-sidebar/NavSidebar.vue +172 -0
- package/components/navigation/nav-sidebar/NavSidebarMenu.vue +14 -0
- package/components/navigation/nav-sidebar/NavSidebarMenuItem.vue +76 -0
- package/components/navigation/nav-sidebar/NavSidebarMenuSectionTitle.vue +54 -0
- package/components/navigation/table-of-contents/TableOfContents.vue +35 -0
- package/components/navigation/table-of-contents/TableOfContentsItem.vue +40 -0
- package/components/navigation/table-of-contents/TableOfContentsSidebar.vue +29 -0
- package/components/pagination/ButtonPagination.vue +274 -0
- package/components/pagination/RowsPerPage.vue +60 -0
- package/components/pagination/SimplePagination.vue +97 -0
- package/components/password/SecurePasswordCondition.vue +41 -0
- package/components/password/SecurePasswordConditions.vue +83 -0
- package/components/placeholders/ContentPlaceholder.vue +41 -0
- package/components/popovers/Popover.vue +128 -0
- package/components/rating/InteractiveRating.vue +94 -0
- package/components/rating/Rating.vue +60 -0
- package/components/rating/RatingItem.vue +54 -0
- package/components/skeletons/Skeleton.vue +11 -0
- package/components/spinners/Spinner.vue +13 -0
- package/components/steppers/CircleStepper.vue +122 -0
- package/components/steppers/Step.vue +72 -0
- package/components/steppers/StepIndicator.vue +228 -0
- package/components/steppers/TabStepper.vue +126 -0
- package/components/steppers/vertical-stepper/VerticalStep.vue +223 -0
- package/components/steppers/vertical-stepper/VerticalStepper.vue +63 -0
- package/components/tables/Table.vue +26 -0
- package/components/tables/TableBody.vue +5 -0
- package/components/tables/TableCell.vue +34 -0
- package/components/tables/TableCellActions.vue +7 -0
- package/components/tables/TableHeader.vue +5 -0
- package/components/tables/TableHeaderCell.vue +15 -0
- package/components/tables/TableRow.vue +14 -0
- package/components/tables/TableWrapper.vue +12 -0
- package/components/tabs/Tab.vue +145 -0
- package/components/tabs/TabBar.vue +64 -0
- package/components/tabs/TabContent.vue +5 -0
- package/components/tabs/TabsContainer.vue +5 -0
- package/components/transitions/HorizontalExpansionTransition.vue +12 -0
- package/components/transitions/VerticalExpansionTransition.vue +14 -0
- package/components/users/Author.vue +113 -0
- package/components/users/User.vue +53 -0
- package/composables/useAccordion.ts +12 -0
- package/composables/useDarkMode.ts +9 -0
- package/composables/useDropdownMenu.ts +25 -0
- package/composables/useForm.ts +134 -0
- package/composables/useFormValidationMode.ts +11 -0
- package/composables/useIsMobile.ts +27 -0
- package/composables/useMobileSidebar.ts +32 -0
- package/composables/useShiki.ts +12 -0
- package/composables/useTableOfContents.ts +50 -0
- package/composables/useToastifyConfig.ts +7 -0
- package/eslint.config.mjs +14 -0
- package/models/constants/app.ts +8 -0
- package/models/constants/form.ts +22 -0
- package/models/enums/alerts.ts +6 -0
- package/models/enums/aspect-ratios.ts +9 -0
- package/models/enums/avatars.ts +21 -0
- package/models/enums/badges.ts +10 -0
- package/models/enums/buttons.ts +38 -0
- package/models/enums/colors.ts +9 -0
- package/models/enums/content.ts +4 -0
- package/models/enums/counters.ts +4 -0
- package/models/enums/dividers.ts +9 -0
- package/models/enums/dropdowns.ts +18 -0
- package/models/enums/effects.ts +6 -0
- package/models/enums/emptyPlaceholders.ts +5 -0
- package/models/enums/formFields.ts +19 -0
- package/models/enums/formValidations.ts +4 -0
- package/models/enums/headings.ts +11 -0
- package/models/enums/icons.ts +22 -0
- package/models/enums/images.ts +16 -0
- package/models/enums/lists.ts +10 -0
- package/models/enums/loaders.ts +15 -0
- package/models/enums/navigation.ts +18 -0
- package/models/enums/order.ts +10 -0
- package/models/enums/orientations.ts +4 -0
- package/models/enums/pages.ts +10 -0
- package/models/enums/positions.ts +21 -0
- package/models/enums/rating.ts +12 -0
- package/models/enums/sections.ts +8 -0
- package/models/enums/selects.ts +16 -0
- package/models/enums/sliders.ts +4 -0
- package/models/enums/steppers.ts +20 -0
- package/models/enums/tabs.ts +11 -0
- package/models/enums/triggers.ts +4 -0
- package/models/types/accordions.ts +6 -0
- package/models/types/avatars.ts +4 -0
- package/models/types/badges.ts +4 -0
- package/models/types/buttons.ts +26 -0
- package/models/types/dropdowns.ts +20 -0
- package/models/types/forms.ts +14 -0
- package/models/types/navigation.ts +11 -0
- package/models/types/pagination.ts +4 -0
- package/models/types/pdfExportTable.ts +6 -0
- package/models/types/radio.ts +9 -0
- package/models/types/selects.ts +14 -0
- package/models/types/steppers.ts +17 -0
- package/models/types/tableOfContent.ts +6 -0
- package/models/types/tabs.ts +7 -0
- package/nuxt.config.ts +40 -0
- package/package.json +57 -0
- package/plugins/vue3-toastify.ts +14 -0
- 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>
|