@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,110 @@
1
+ <template>
2
+ <div
3
+ class="flex flex-wrap gap-2"
4
+ >
5
+ <Badge
6
+ v-for="(item, index) in visibleBadges"
7
+ :key="item.text || index"
8
+ :text="item.text"
9
+ :styleType
10
+ :shape
11
+ :color
12
+ :isTransparent
13
+ :showDot
14
+ :closeable
15
+ :showIcon
16
+ :icon="item.icon"
17
+ @close="() => emit('close', item)"
18
+ />
19
+ <Badge
20
+ v-if="showEllipsisBadge"
21
+ :text="counterContent"
22
+ :style="BadgeStyle.BORDER"
23
+ :shape="BadgeShape.BADGE"
24
+ :color="ColorAccent.NEUTRAL"
25
+ :isTransparent
26
+ />
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ // Props
32
+ const props = defineProps({
33
+ items: {
34
+ type: Array as PropType<Badge[]>,
35
+ default: () => [
36
+ {
37
+ text: 'Badge 1',
38
+ },
39
+ {
40
+ text: 'Badge 2',
41
+ },
42
+ {
43
+ text: 'Badge 3'
44
+ },
45
+ {
46
+ text: 'Badge 4'
47
+ },
48
+ {
49
+ text: 'Badge 5'
50
+ },
51
+ ]
52
+ },
53
+ styleType: {
54
+ type: String as PropType<BadgeStyle>,
55
+ default: BadgeStyle.BORDER,
56
+ validator: (value: BadgeStyle) => Object.values(BadgeStyle).includes(value),
57
+ },
58
+ shape: {
59
+ type: String as PropType<BadgeShape>,
60
+ default: BadgeShape.BADGE,
61
+ validator: (value: BadgeShape) => Object.values(BadgeShape).includes(value),
62
+ },
63
+ color: {
64
+ type: String as PropType<ColorAccent>,
65
+ default: ColorAccent.NEUTRAL,
66
+ validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
67
+ },
68
+ showDot: {
69
+ type: Boolean as PropType<boolean>,
70
+ default: false,
71
+ },
72
+ closeable: {
73
+ type: Boolean as PropType<boolean>,
74
+ default: false,
75
+ },
76
+ isTransparent: {
77
+ type: Boolean as PropType<boolean>,
78
+ default: false,
79
+ },
80
+ showIcon: {
81
+ type: Boolean as PropType<boolean>,
82
+ default: false,
83
+ },
84
+ itemsLimit: {
85
+ type: Number as PropType<number>,
86
+ default: null,
87
+ },
88
+ counterType: {
89
+ type: String as PropType<StackCounterType>,
90
+ default: StackCounterType.ELLIPSIS,
91
+ validator: (value: StackCounterType) => Object.values(StackCounterType).includes(value),
92
+ },
93
+ })
94
+
95
+ // Emits
96
+ const emit = defineEmits(['close'])
97
+
98
+ // Computed functions
99
+ const visibleBadges = computed(() =>
100
+ // Only apply slicing if itemsLimit is provided and is a valid number
101
+ Number.isFinite(props.itemsLimit) ? props.items.slice(0, props.itemsLimit) : props.items
102
+ )
103
+
104
+ const showEllipsisBadge = computed(() =>
105
+ // Only show ellipsis if itemsLimit is provided and the items exceeds the max itemsLimit
106
+ Number.isFinite(props.itemsLimit) && props.items.length > props.itemsLimit
107
+ )
108
+
109
+ const counterContent = computed(() => getStackCounterContent(props.items, props.counterType, props.itemsLimit ? props.itemsLimit : null))
110
+ </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'w-[24px] h-[24px] flex justify-center items-center rounded-full',
5
+ colorClass
6
+ ]"
7
+ >
8
+ <MdiIcon
9
+ v-if="icon"
10
+ :icon
11
+ size="16px"
12
+ :class="iconColorClass"
13
+ />
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ // Props
19
+ const props = defineProps({
20
+ color: {
21
+ type: String as PropType<ColorAccent>,
22
+ default: ColorAccent.NEUTRAL,
23
+ validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
24
+ },
25
+ icon: {
26
+ type: String as PropType<any>,
27
+ default: 'mdiHelp',
28
+ },
29
+ })
30
+
31
+ // Computed Classes
32
+ const colorClass = computed(() => {
33
+ const colorVariants = {
34
+ [ColorAccent.NEUTRAL]: 'bg-background-neutral-sublter',
35
+ [ColorAccent.SUCCESS]: 'bg-background-success-subtler',
36
+ [ColorAccent.WARNING]: 'bg-background-warning-subtler',
37
+ [ColorAccent.DANGER]: 'bg-background-danger-subtler',
38
+ [ColorAccent.INFO]: 'bg-background-info-subtler',
39
+ [ColorAccent.PRIMARY_BRAND]: 'bg-background-primary-brand-soft',
40
+ [ColorAccent.SECONDARY_BRAND]: 'bg-background-secondary-brand-soft',
41
+ }
42
+ return colorVariants[props.color as ColorAccent] || 'bg-background-neutral-sublter'
43
+ })
44
+
45
+ const iconColorClass = computed(() => {
46
+ const iconVariants = {
47
+ [ColorAccent.NEUTRAL]: 'text-icon-neutral-subtle-on-subtler-bg',
48
+ [ColorAccent.SUCCESS]: 'text-icon-success',
49
+ [ColorAccent.WARNING]: 'text-icon-warning-on-bg',
50
+ [ColorAccent.DANGER]: 'text-icon-danger',
51
+ [ColorAccent.INFO]: 'text-icon-info',
52
+ [ColorAccent.PRIMARY_BRAND]: 'text-icon-primary-brand-default',
53
+ [ColorAccent.SECONDARY_BRAND]: 'text-icon-secondary-brand-default',
54
+ }
55
+ return iconVariants[props.color as ColorAccent] || 'text-icon-neutral-subtle-on-subtler-bg'
56
+ })
57
+ </script>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'inline-flex items-center gap-2 font-semibold text-xs'
5
+ ]"
6
+ >
7
+ <IconBadge
8
+ :color
9
+ :icon
10
+ />
11
+
12
+ <!-- Text -->
13
+ <span :class="textClass">
14
+ {{ text }}
15
+ </span>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ // Props
21
+ const props = defineProps({
22
+ color: {
23
+ type: String as PropType<ColorAccent>,
24
+ default: ColorAccent.NEUTRAL,
25
+ validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
26
+ },
27
+ text: {
28
+ type: String as PropType<string>,
29
+ default: 'Badge text'
30
+ },
31
+ icon: {
32
+ type: String as PropType<any>,
33
+ default: 'mdiHelp',
34
+ },
35
+ })
36
+
37
+ // Computed Classes
38
+ const textClass = computed(() => {
39
+ const textVariants = {
40
+ [ColorAccent.NEUTRAL]: 'text-text-neutral-subtle',
41
+ [ColorAccent.SUCCESS]: 'text-text-success',
42
+ [ColorAccent.WARNING]: 'text-text-warning',
43
+ [ColorAccent.DANGER]: 'text-text-danger',
44
+ [ColorAccent.INFO]: 'text-text-info',
45
+ [ColorAccent.PRIMARY_BRAND]: 'text-text-primary-brand-default',
46
+ [ColorAccent.SECONDARY_BRAND]: 'text-text-secondary-brand-default',
47
+ }
48
+ return textVariants[props.color as ColorAccent] || 'text-text-secondary'
49
+ })
50
+ </script>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <nav aria-label="breadcrumb">
3
+ <ul class="flex items-center space-x-2 text-gray-500">
4
+ <!-- Home Link -->
5
+ <li>
6
+ <NuxtLink to="/" class="flex items-center space-x-1">
7
+ <MdiIcon
8
+ icon="mdiHomeOutline"
9
+ size="20"
10
+ preserveAspectRatio="xMidYMid meet"
11
+ class="text-icon-neutral-subtler hover:text-icon-neutral-on-monochrome-hover-bg"
12
+ />
13
+ </NuxtLink>
14
+ </li>
15
+
16
+ <!-- Dynamic Breadcrumbs (Stop at Previous Level) -->
17
+ <li v-for="(crumb, index) in breadcrumbs" :key="index" class="flex items-center space-x-2">
18
+ <!-- Chevron Separator -->
19
+ <MdiIcon
20
+ icon="mdiChevronRight"
21
+ size="20"
22
+ preserveAspectRatio="xMidYMid meet"
23
+ class="text-icon-neutral-subtler"
24
+ />
25
+
26
+ <!-- Breadcrumb Link -->
27
+ <NuxtLink
28
+ :to="crumb.to"
29
+ class="text-sm text-text-neutral-subtler hover:text-text-neutral-on-monochrome-hover-bg"
30
+ >
31
+ {{ crumb.label }}
32
+ </NuxtLink>
33
+ </li>
34
+ </ul>
35
+ </nav>
36
+ </template>
37
+ <script setup lang="ts">
38
+ // Route
39
+ const route = useRoute()
40
+
41
+ // Generate breadcrumbs from route path, stopping at the previous level
42
+ const breadcrumbs = computed(() => {
43
+ const pathSegments = route.path.split('/').filter(Boolean) // Remove empty values
44
+
45
+ if (pathSegments.length <= 1) return [] // Stop at Home if only 1 segment
46
+
47
+ return pathSegments.slice(0, -1).map((segment, index) => { // Exclude last segment
48
+ return {
49
+ label: segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()), // Format text
50
+ to: '/' + pathSegments.slice(0, index + 1).join('/') // Build breadcrumb path
51
+ }
52
+ })
53
+ })
54
+ </script>
@@ -0,0 +1,395 @@
1
+ <template>
2
+ <component
3
+ :is="actionType === ButtonActionType.LINK ? NuxtLink : 'button'"
4
+ :id
5
+ :type="actionType === ButtonActionType.ACTION ? type : undefined"
6
+ :class="[
7
+ 'flex items-center justify-center',
8
+ isRounded ? 'rounded-full' : 'rounded-button',
9
+ 'aspect-square',
10
+ 'text-nowrap',
11
+ ...buttonStyleClass,
12
+ buttonSizeClass,
13
+ horizontalPaddingClass,
14
+ gapClass,
15
+ 'self-start',
16
+ isFullWidth && 'w-full',
17
+ isMobileFullWidth && 'w-full md:w-auto'
18
+ ]"
19
+ v-bind="{
20
+ ...componentProps,
21
+ ...$attrs,
22
+ ...(actionType === ButtonActionType.ACTION ? { onClick: emitClick } : {})
23
+ }"
24
+ :disabled="ButtonActionType.ACTION && isLoading || disabled"
25
+
26
+ >
27
+ <!-- Loading State -->
28
+ <template v-if="isLoading">
29
+ <div class="animate-spin">
30
+ <MdiIcon
31
+ icon="mdiLoading"
32
+ preserveAspectRatio="xMidYMid meet"
33
+ :class="iconSizeClass"
34
+ />
35
+ </div>
36
+
37
+ <span :class="[ 'font-semibold', textSizeClass, textClass ]">
38
+ {{ loadingText }}
39
+ </span>
40
+ </template>
41
+
42
+ <!-- Normal Content -->
43
+ <template v-else>
44
+ <!-- Left icon -->
45
+ <template v-if="iconPosition === IconPosition.LEFT">
46
+ <MdiIcon
47
+ v-if="icon && !svgIcon"
48
+ :icon
49
+ preserveAspectRatio="xMidYMid meet"
50
+ :class="iconSizeClass"
51
+ />
52
+ <span
53
+ v-else-if="svgIcon"
54
+ :class="iconSizeClass"
55
+ >
56
+ <SVGImage
57
+ :src="svgIcon"
58
+ :color="resolvedSvgIconColor"
59
+ />
60
+ </span>
61
+ </template>
62
+
63
+ <span :class="[ 'font-semibold', textSizeClass, textClass ]">
64
+ {{ text }}
65
+ </span>
66
+
67
+ <!-- Right icon -->
68
+ <template v-if="iconPosition === IconPosition.RIGHT">
69
+ <MdiIcon
70
+ v-if="icon"
71
+ :icon="icon"
72
+ preserveAspectRatio="xMidYMid meet"
73
+ :class="iconSizeClass"
74
+ />
75
+ <span
76
+ v-else-if="svgIcon"
77
+ :class="iconSizeClass"
78
+ >
79
+ <SVGImage
80
+ :src="svgIcon"
81
+ :color="!useSVGIconColor && svgIconColorClass"
82
+ />
83
+ </span>
84
+ </template>
85
+ </template>
86
+ </component>
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ // Imports
91
+ import { NuxtLink } from '#components'
92
+
93
+ // Component options
94
+ defineOptions({
95
+ inheritAttrs: false, // Prevents Vue from automatically applying attributes incorrectly
96
+ })
97
+
98
+ // Props
99
+ const props = defineProps({
100
+ actionType: {
101
+ type: String as PropType<ButtonActionType>,
102
+ default: ButtonActionType.ACTION,
103
+ validator: (value: ButtonActionType) => Object.values(ButtonActionType).includes(value),
104
+ },
105
+ styleType: {
106
+ type: String as PropType<ButtonStyleType>,
107
+ default: ButtonStyleType.NEUTRAL_OUTLINED,
108
+ validator: (value: ButtonStyleType) => Object.values(ButtonStyleType).includes(value),
109
+ },
110
+ type: {
111
+ type: String as PropType<'button' | 'submit' | 'reset'>,
112
+ default: 'button',
113
+ validator: (value: string) =>
114
+ ['button', 'submit', 'reset'].includes(value),
115
+ },
116
+ text: {
117
+ type: String as PropType<string>,
118
+ default: 'Button text'
119
+ },
120
+ size: {
121
+ type: String as PropType<ButtonSize>,
122
+ default: ButtonSize.LG,
123
+ validator: (value: ButtonSize) => Object.values(ButtonSize).includes(value),
124
+ },
125
+ icon: {
126
+ type: String as PropType<any>,
127
+ default: "mdiHelp"
128
+ },
129
+ iconPosition: {
130
+ type: String as PropType<IconPosition>,
131
+ default: IconPosition.NONE,
132
+ validator: (value: IconPosition) => Object.values(IconPosition).includes(value),
133
+ },
134
+ svgIcon: String as PropType<string>,
135
+ useSVGIconColor: {
136
+ type: Boolean as PropType<boolean>,
137
+ default: false,
138
+ },
139
+ disabled: {
140
+ type: Boolean as PropType<boolean>,
141
+ default: false,
142
+ },
143
+ to: {
144
+ type: String as PropType<string>,
145
+ default: '/'
146
+ },
147
+ isExternal: {
148
+ type: Boolean as PropType<boolean>,
149
+ default: false
150
+ },
151
+ isRounded: {
152
+ type: Boolean as PropType<boolean>,
153
+ default: false
154
+ },
155
+ isFullWidth: {
156
+ type: Boolean as PropType<boolean>,
157
+ default: false
158
+ },
159
+ isMobileFullWidth: {
160
+ type: Boolean as PropType<boolean>,
161
+ default: false
162
+ },
163
+ isLoading: {
164
+ type: Boolean as PropType<boolean>,
165
+ default: false,
166
+ },
167
+ loadingText: {
168
+ type: String as PropType<string>,
169
+ default: 'Processing...',
170
+ },
171
+ id: String as PropType<string>,
172
+ textClass: String as PropType<string>,
173
+ })
174
+
175
+ // Emits
176
+ const emit = defineEmits(['click'])
177
+ const emitClick = () => {
178
+ if (!props.disabled) {
179
+ emit('click')
180
+ }
181
+ }
182
+
183
+ // Computed classes
184
+ const buttonStyleClass = computed(() => {
185
+ const variant = {
186
+ [ButtonStyleType.PRIMARY_BRAND_FILLED]: [
187
+ 'bg-background-primary-brand-default',
188
+ !props.disabled && 'hover:bg-background-primary-brand-hover',
189
+ 'focus:ring-offset-2',
190
+ 'focus:ring-2',
191
+ 'focus:ring-border-primary-brand-default',
192
+ 'text-text-neutral-on-filled',
193
+ ],
194
+ [ButtonStyleType.PRIMARY_BRAND_SOFT]: [
195
+ 'bg-background-primary-brand-soft',
196
+ !props.disabled && 'hover:bg-background-primary-brand-soft-hover',
197
+ 'focus:ring-offset-2',
198
+ 'focus:ring-2',
199
+ 'focus:ring-border-primary-brand-default',
200
+ 'text-icon-primary-brand-on-soft-bg',
201
+ ],
202
+ [ButtonStyleType.PRIMARY_BRAND_TRANSPARENT]: [
203
+ 'focus:ring-offset-2',
204
+ 'focus:ring-2',
205
+ 'focus:ring-border-primary-brand-default',
206
+ 'text-text-primary-brand-default',
207
+ !props.disabled && 'hover:text-text-primary-brand-hover',
208
+ ],
209
+ [ButtonStyleType.SECONDARY_BRAND_FILLED]: [
210
+ 'bg-background-secondary-brand-default',
211
+ !props.disabled && 'hover:bg-background-secondary-brand-hover',
212
+ 'focus:ring-offset-2',
213
+ 'focus:ring-2',
214
+ 'focus:ring-border-secondary-brand',
215
+ 'text-text-neutral-on-filled',
216
+ ],
217
+ [ButtonStyleType.NEUTRAL_FILLED]: [
218
+ 'bg-background-neutral-filled-default',
219
+ !props.disabled && 'hover:bg-background-neutral-filled-hover',
220
+ 'focus:ring-offset-2',
221
+ 'focus:ring-2',
222
+ 'focus:ring-border-primary-brand-default',
223
+ 'text-text-neutral-on-neutral-filled-bg',
224
+ ],
225
+ [ButtonStyleType.NEUTRAL_OUTLINED]: [
226
+ 'border',
227
+ 'border-border-default',
228
+ 'bg-background-neutral-default',
229
+ !props.disabled && 'hover:bg-background-neutral-hover',
230
+ 'focus:ring-offset-2',
231
+ 'focus:ring-2',
232
+ 'focus:ring-border-primary-brand-default',
233
+ 'text-text-default',
234
+ ],
235
+ [ButtonStyleType.NEUTRAL_TRANSPARENT]: [
236
+ !props.disabled && 'hover:bg-background-neutral-hover',
237
+ 'focus:ring-offset-2',
238
+ 'focus:ring-2',
239
+ 'focus:ring-border-primary-brand-default',
240
+ 'text-text-default',
241
+ ],
242
+ [ButtonStyleType.NEUTRAL_TRANSPARENT_SUBTLE]: [
243
+ !props.disabled && 'hover:bg-background-neutral-hover hover:text-text-default',
244
+ 'focus:ring-offset-2',
245
+ 'focus:ring-2',
246
+ 'focus:ring-border-primary-brand-default',
247
+ 'text-text-neutral-subtler',
248
+ ],
249
+ [ButtonStyleType.DELETE_FILLED]: [
250
+ 'bg-background-delete-default',
251
+ !props.disabled && 'hover:bg-background-delete-hover',
252
+ 'focus:ring-offset-2',
253
+ 'focus:ring-2',
254
+ 'focus:ring-border-delete',
255
+ 'text-text-neutral-on-filled',
256
+ ],
257
+ [ButtonStyleType.DELETE_SOFT]: [
258
+ 'bg-background-delete-soft',
259
+ !props.disabled && 'hover:bg-background-delete-soft-hover',
260
+ 'focus:ring-offset-2',
261
+ 'focus:ring-2',
262
+ 'focus:ring-border-delete',
263
+ 'text-text-delete',
264
+ ],
265
+ [ButtonStyleType.DELETE_OUTLINED]: [
266
+ 'border',
267
+ 'border-border-delete-subtle',
268
+ 'bg-background-neutral-default',
269
+ !props.disabled && 'hover:bg-background-delete-on-neutral-hover',
270
+ 'focus:ring-offset-2',
271
+ 'focus:ring-2',
272
+ 'focus:ring-border-delete',
273
+ 'text-text-delete',
274
+ ],
275
+ [ButtonStyleType.DELETE_TRANSPARENT]: [
276
+ !props.disabled && 'hover:bg-background-delete-on-neutral-hover',
277
+ 'focus:ring-offset-2',
278
+ 'focus:ring-2',
279
+ 'focus:ring-border-delete',
280
+ 'text-text-delete',
281
+ ],
282
+ }
283
+
284
+ return variant[props.styleType as ButtonStyleType]?.filter(Boolean) || [
285
+ 'border',
286
+ 'border-border-default',
287
+ 'bg-background-neutral-default',
288
+ !props.disabled && 'hover:bg-background-neutral-hover',
289
+ 'focus:ring-offset-2',
290
+ 'focus:ring-2',
291
+ 'focus:ring-border-primary-brand-default',
292
+ 'text-text-default',
293
+ ].filter(Boolean)
294
+ })
295
+
296
+ const buttonSizeClass = computed(() => {
297
+ const variant = {
298
+ [ButtonSize.XS]: 'h-[24px]',
299
+ [ButtonSize.SM]: 'h-[28px]',
300
+ [ButtonSize.MD]: 'h-[32px]',
301
+ [ButtonSize.LG]: 'h-[36px]',
302
+ [ButtonSize.XL]: 'h-[40px]',
303
+ [ButtonSize.XXL]: 'h-[48px]',
304
+ }
305
+ return variant[props.size as ButtonSize] || 'h-[36px]'
306
+ })
307
+
308
+ const iconSizeClass = computed(() => {
309
+ const variant = {
310
+ [ButtonSize.XS]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
311
+ [ButtonSize.SM]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
312
+ [ButtonSize.MD]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
313
+ [ButtonSize.LG]: 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]',
314
+ [ButtonSize.XL]: 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]',
315
+ [ButtonSize.XXL]: 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]',
316
+ }
317
+ return variant[props.size as ButtonSize] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
318
+ })
319
+
320
+ const svgIconColorClass = computed(() => {
321
+ const variant = {
322
+ [ButtonStyleType.PRIMARY_BRAND_FILLED]: 'text-text-neutral-on-filled',
323
+ [ButtonStyleType.PRIMARY_BRAND_SOFT]: 'text-icon-primary-brand-on-soft-bg',
324
+ [ButtonStyleType.PRIMARY_BRAND_TRANSPARENT]: [
325
+ 'text-text-primary-brand-default',
326
+ !props.disabled && 'hover:text-text-primary-brand-hover',
327
+ ],
328
+ [ButtonStyleType.SECONDARY_BRAND_FILLED]: 'text-text-neutral-on-filled',
329
+ [ButtonStyleType.NEUTRAL_FILLED]: 'text-text-neutral-on-neutral-filled-bg',
330
+ [ButtonStyleType.NEUTRAL_OUTLINED]: 'text-text-default',
331
+ [ButtonStyleType.NEUTRAL_TRANSPARENT]: 'text-text-default',
332
+ [ButtonStyleType.NEUTRAL_TRANSPARENT_SUBTLE]: 'text-text-neutral-subtler',
333
+ [ButtonStyleType.DELETE_FILLED]: 'text-text-neutral-on-filled',
334
+ [ButtonStyleType.DELETE_SOFT]: 'text-text-delete',
335
+ [ButtonStyleType.DELETE_OUTLINED]: 'text-text-delete',
336
+ [ButtonStyleType.DELETE_TRANSPARENT]: 'text-text-delete',
337
+ }
338
+ return variant[props.styleType as ButtonStyleType] || 'text-text-default'
339
+ })
340
+
341
+ const textSizeClass = computed(() => {
342
+ const variant = {
343
+ [ButtonSize.XS]: 'text-xs',
344
+ [ButtonSize.SM]: 'text-sm',
345
+ [ButtonSize.MD]: 'text-sm',
346
+ [ButtonSize.LG]: 'text-sm',
347
+ [ButtonSize.XL]: 'text-sm',
348
+ [ButtonSize.XXL]: 'text-base',
349
+ }
350
+ return variant[props.size as ButtonSize] || 'text-sm'
351
+ })
352
+
353
+ const horizontalPaddingClass = computed(() => {
354
+ const variant = {
355
+ [ButtonSize.XS]: 'px-2',
356
+ [ButtonSize.SM]: 'px-2',
357
+ [ButtonSize.MD]: 'px-2.5',
358
+ [ButtonSize.LG]: 'px-3',
359
+ [ButtonSize.XL]: 'px-3.5',
360
+ [ButtonSize.XXL]: 'px-4',
361
+ }
362
+ return variant[props.size as ButtonSize] || 'px-3'
363
+ })
364
+
365
+ const gapClass = computed(() => {
366
+ const variant = {
367
+ [ButtonSize.XS]: 'gap-1',
368
+ [ButtonSize.SM]: 'gap-1.5',
369
+ [ButtonSize.MD]: 'gap-2',
370
+ [ButtonSize.LG]: 'gap-2',
371
+ [ButtonSize.XL]: 'gap-2',
372
+ [ButtonSize.XXL]: 'gap-2',
373
+ }
374
+ return variant[props.size as ButtonSize] || 'gap-2'
375
+ })
376
+
377
+ // Computed functions
378
+ const resolvedSvgIconColor = computed(() => {
379
+ return props.useSVGIconColor ? undefined : svgIconColorClass
380
+ })
381
+
382
+ // Props for the dynamic component
383
+ const componentProps = computed(() => {
384
+ if (props.actionType === ButtonActionType.LINK) {
385
+ return {
386
+ to: props.to,
387
+ target: props.isExternal ? '_blank' : '_self',
388
+ rel: props.isExternal ? 'noopener noreferrer' : undefined,
389
+ external: props.isExternal,
390
+ }
391
+ } else {
392
+ return {}
393
+ }
394
+ })
395
+ </script>