@saasmakers/ui 0.1.60 → 0.1.62

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.
@@ -0,0 +1,64 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldLabel } from '../../types/fields'
3
+
4
+ withDefaults(defineProps<FieldLabel>(), {
5
+ disabled: false,
6
+ forField: '',
7
+ hasMarginBottom: false,
8
+ hasMarginLeft: false,
9
+ icon: undefined,
10
+ label: '',
11
+ lineThrough: false,
12
+ loading: false,
13
+ required: false,
14
+ size: 'base',
15
+ truncate: false,
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ click: [event: MouseEvent]
20
+ }>()
21
+
22
+ function onClick(event: MouseEvent) {
23
+ emit('click', event)
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <label
29
+ class="flex select-none items-center font-semibold tracking-tight"
30
+ :class="{
31
+ 'cursor-pointer': !loading && !disabled,
32
+ 'cursor-not-allowed': disabled,
33
+ 'cursor-wait': loading,
34
+ 'text-gray-900 dark:text-gray-100': !lineThrough,
35
+ 'text-gray-500 dark:text-gray-500 line-through': lineThrough,
36
+ 'hover:text-black dark:hover:text-white': !loading && !disabled && !lineThrough,
37
+ 'w-0 truncate': truncate,
38
+
39
+ 'mb-1.5': size === 'xs' && hasMarginBottom,
40
+ 'mb-2': size === 'sm' && hasMarginBottom,
41
+ 'mb-2.5': size === 'base' && hasMarginBottom,
42
+ 'mb-3': size === 'lg' && hasMarginBottom,
43
+ 'ml-1': size === 'xs' && hasMarginLeft,
44
+ 'ml-1.5': size === 'sm' && hasMarginLeft,
45
+ 'ml-2': size === 'base' && hasMarginLeft,
46
+ 'ml-2.5': size === 'lg' && hasMarginLeft,
47
+ }"
48
+ :for="forField"
49
+ @click="onClick"
50
+ >
51
+ <BaseIcon
52
+ :icon="icon"
53
+ :size="size"
54
+ :text="label"
55
+ />
56
+
57
+ <BaseText
58
+ v-if="required"
59
+ class="ml-1 text-red-700 dark:text-red-300"
60
+ :size="size"
61
+ text="*"
62
+ />
63
+ </label>
64
+ </template>
@@ -0,0 +1,209 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldMessage } from '../../types/fields'
3
+
4
+ const props = withDefaults(defineProps<FieldMessage>(), {
5
+ description: '',
6
+ hideError: true,
7
+ size: 'base',
8
+ validation: undefined,
9
+ })
10
+
11
+ // Composable
12
+ const { t } = useI18n()
13
+
14
+ const validationMessage = computed(() => {
15
+ // Available rules
16
+ // https://vuelidate.js.org/#sub-builtin-validators
17
+ let message = ''
18
+
19
+ if (props.validation && props.validation.$dirty) {
20
+ // Required, Required If, Required Unless
21
+ if (props.validation.required === false || props.validation.requiredIf === false || props.validation.requiredUnless === false) {
22
+ message = t('required')
23
+ }
24
+ // Min Length
25
+ else if (props.validation.minLength === false) {
26
+ const min = props.validation.$params.minLength.min
27
+
28
+ message = t('minLength', { min })
29
+ }
30
+ // Max Length
31
+ else if (props.validation.maxLength === false) {
32
+ const max = props.validation.$params.maxLength.max
33
+
34
+ message = t('maxLength', { max })
35
+ }
36
+ // Min Value
37
+ else if (props.validation.minValue === false) {
38
+ const min = props.validation.$params.minValue.min
39
+
40
+ message = t('minValue', { min })
41
+ }
42
+ // Max Value
43
+ else if (props.validation.maxValue === false) {
44
+ const max = props.validation.$params.maxValue.max
45
+
46
+ message = t('maxValue', { max })
47
+ }
48
+ // Between
49
+ else if (props.validation.between === false) {
50
+ const min = props.validation.$params.between.min
51
+ const max = props.validation.$params.between.max
52
+
53
+ message = t('between', {
54
+ max,
55
+ min,
56
+ })
57
+ }
58
+ // Alpha
59
+ else if (props.validation.alpha === false) {
60
+ message = t('alpha')
61
+ }
62
+ // Alpha Num
63
+ else if (props.validation.alphaNum === false) {
64
+ message = t('alphaNum')
65
+ }
66
+ // Numeric
67
+ else if (props.validation.numeric === false) {
68
+ message = t('numeric')
69
+ }
70
+ // Integer
71
+ else if (props.validation.integer === false) {
72
+ message = t('integer')
73
+ }
74
+ // Integer
75
+ else if (props.validation.decimal === false) {
76
+ message = t('decimal')
77
+ }
78
+ // Email
79
+ else if (props.validation.email === false) {
80
+ message = t('email')
81
+ }
82
+ // IP Address
83
+ else if (props.validation.ipAddress === false) {
84
+ message = t('ipAddress')
85
+ }
86
+ // Mac Address
87
+ else if (props.validation.macAddress === false) {
88
+ message = t('maxAddress')
89
+ }
90
+ // Same As
91
+ else if (props.validation.sameAs === false) {
92
+ const field = props.validation.$params.sameAs.eq
93
+
94
+ message = t('sameAs', { field })
95
+ }
96
+ // Url
97
+ else if (props.validation.url === false) {
98
+ message = t('url')
99
+ }
100
+ // Other rules
101
+ else if (props.validation.$invalid === true) {
102
+ message = t('invalid')
103
+ }
104
+ }
105
+
106
+ return message
107
+ })
108
+
109
+ const status = computed(() => {
110
+ if (validationMessage.value && !props.hideError) {
111
+ return 'error'
112
+ }
113
+ else if (props.description) {
114
+ return 'default'
115
+ }
116
+ })
117
+
118
+ const text = computed<BaseTextText>(() => {
119
+ if (validationMessage.value && !props.hideError) {
120
+ return validationMessage.value
121
+ }
122
+ else if (props.description) {
123
+ return props.description
124
+ }
125
+ else {
126
+ return ''
127
+ }
128
+ })
129
+ </script>
130
+
131
+ <template>
132
+ <BaseIcon
133
+ v-if="text"
134
+ class="text-left tracking-tighter"
135
+ :class="{
136
+ 'text-red-700 dark:text-red-300': status === 'error',
137
+ 'text-gray-700 dark:text-gray-300': status === 'default',
138
+
139
+ 'mt-2': size === 'xs',
140
+ 'mt-2.5': size === 'sm',
141
+ 'mt-3': size === 'base',
142
+ 'mt-3.5': size === 'lg',
143
+ }"
144
+ size="sm"
145
+ :text="text"
146
+ />
147
+ </template>
148
+
149
+ <i18n lang="json">
150
+ {
151
+ "en": {
152
+ "alpha": "The value accepts only alphabet characters",
153
+ "alphaNum": "The value accepts only alphanumerics",
154
+ "between": "Value should be between {min} and {max}",
155
+ "decimal": "The value accepts only positive and negative decimal numbers",
156
+ "email": "The value is not a valid email",
157
+ "integer": "The value accepts only positive and negative integers",
158
+ "invalid": "The value is invalid",
159
+ "ipAddress": "The value accepts only a valid IPv4 address",
160
+ "macAddress": "The value accepts only a valid MAC address",
161
+ "maxLength": "This value is too long (max: {max})",
162
+ "maxValue": "Maximum value allowed: {max}",
163
+ "minLength": "The value is too short (min: {min})",
164
+ "minValue": "Minimum value allowed: {min}",
165
+ "numeric": "The value accepts only numerics",
166
+ "required": "A value is required",
167
+ "sameAs": "The value does not match: {field}",
168
+ "url": "The value is not a valid url"
169
+ },
170
+ "fr": {
171
+ "alpha": "La valeur n'accepte que les caractères alphabétiques",
172
+ "alphaNum": "La valeur n'accepte que les alphanumériques",
173
+ "between": "La valeur doit être entre {min} et {max}",
174
+ "decimal": "La valeur naccepte que les nombres décimaux positifs et négatifs",
175
+ "email": "La valeur n'est pas un email valide",
176
+ "integer": "La valeur n'accepte que des entiers positifs et négatifs",
177
+ "ipAddress": "La valeur n'accepte qu'une adresse IPv4 valide",
178
+ "invalid": "La valeur n'est pas valide",
179
+ "macAddress": "La valeur n'accepte qu'une adresse MAC valide",
180
+ "maxLength": "La valeur est trop longue (max: {max})",
181
+ "maxValue": "La valeur maximum acceptée: {max}",
182
+ "minLength": "La valeur est trop petite (min: {min})",
183
+ "minValue": "La valeur minimum acceptée: {min}",
184
+ "numeric": "La valeur n'accepte que des chiffres",
185
+ "required": "La valeur est requise",
186
+ "sameAs": "La valeur ne correspond pas à: {field}",
187
+ "url": "La valeur n'est pas une URL valide"
188
+ },
189
+ "ja": {
190
+ "alpha": "値はアルファベット文字のみを受け入れます",
191
+ "alphaNum": "値はアルファベットと数字のみを受け入れます",
192
+ "between": "値は {min} から {max} の間である必要があります",
193
+ "decimal": "値は正の小数と負の小数のみを受け入れます",
194
+ "email": "値は有効なメールアドレスではありません",
195
+ "integer": "値は正の整数と負の整数のみを受け入れます",
196
+ "invalid": "値は無効です",
197
+ "ipAddress": "値は有効なIPv4アドレスのみを受け入れます",
198
+ "macAddress": "値は有効なMACアドレスのみを受け入れます",
199
+ "maxLength": "値は長すぎます (最大: {max})",
200
+ "maxValue": "最大値: {max}",
201
+ "minLength": "値は短すぎます (最小: {min})",
202
+ "minValue": "最小値: {max}",
203
+ "numeric": "値は数字のみを受け入れます",
204
+ "required": "値は必須です",
205
+ "sameAs": "値は一致しません: {field}",
206
+ "url": "値は有効なURLではありません"
207
+ }
208
+ }
209
+ </i18n>
@@ -0,0 +1,291 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldSelect, FieldSelectOption } from '../../types/fields'
3
+ import { vOnClickOutside } from '@vueuse/components'
4
+ import { getIcon } from '../../composables/useIcons'
5
+
6
+ const props = withDefaults(defineProps<FieldSelect>(), {
7
+ border: 'full',
8
+ caret: true,
9
+ columns: () => [],
10
+ description: '',
11
+ direction: 'bottom',
12
+ disabled: false,
13
+ hideError: false,
14
+ label: '',
15
+ labelIcon: undefined,
16
+ maxHeight: 'xs',
17
+ modelValue: undefined,
18
+ openOnHover: false,
19
+ options: () => [],
20
+ padding: true,
21
+ placeholder: '',
22
+ required: false,
23
+ size: 'base',
24
+ validation: undefined,
25
+ })
26
+
27
+ const emit = defineEmits<{
28
+ 'change': [event: MouseEvent, value?: number | string]
29
+ 'click': [event: MouseEvent, value?: number | string]
30
+ 'optionClick': [event: MouseEvent, value?: number | string]
31
+ 'update:modelValue': [value?: number | string]
32
+ }>()
33
+
34
+ const { fadeIn } = useMotion()
35
+
36
+ const opened = ref(false)
37
+ const uuid = ref(`${Math.floor((1 + Math.random()) * 0x100000)}`)
38
+
39
+ const computedColumns = computed(() => {
40
+ if (props.columns.length) {
41
+ return props.columns
42
+ }
43
+
44
+ return [{ options: computedOptions.value }]
45
+ })
46
+
47
+ const computedOptions = computed(() => {
48
+ const options = []
49
+
50
+ // Add index to each option
51
+ if (props.columns.length) {
52
+ for (const column of props.columns) {
53
+ for (const columnOption of column.options) {
54
+ options.push(columnOption)
55
+ }
56
+ }
57
+ }
58
+ else {
59
+ for (const columnOption of props.options) {
60
+ options.push(columnOption)
61
+ }
62
+ }
63
+
64
+ return options
65
+ })
66
+
67
+ const selectedOption = computed(() => {
68
+ return computedOptions.value.find((option) => {
69
+ return option.value === props.modelValue
70
+ })
71
+ })
72
+
73
+ function reset() {
74
+ opened.value = false
75
+ }
76
+
77
+ function selectOption(event: MouseEvent, value?: number | string) {
78
+ emit('change', event, value)
79
+ emit('update:modelValue', value)
80
+
81
+ reset()
82
+ }
83
+
84
+ function onClose() {
85
+ reset()
86
+ }
87
+
88
+ function onContainerClick(event: MouseEvent) {
89
+ if (!props.disabled) {
90
+ opened.value = !opened.value
91
+
92
+ emit('click', event, selectedOption.value?.value)
93
+ }
94
+ }
95
+
96
+ function onContainerKeypress(event: KeyboardEvent) {
97
+ if (['Enter', 'Space'].includes(event.code)) {
98
+ (event.target as HTMLElement)?.click()
99
+ }
100
+ }
101
+
102
+ function onLabelClick() {
103
+ if (!props.disabled) {
104
+ opened.value = !opened.value
105
+ }
106
+ }
107
+
108
+ function onMouseEnter() {
109
+ if (props.openOnHover) {
110
+ opened.value = true
111
+ }
112
+ }
113
+
114
+ function onMouseLeave() {
115
+ if (props.openOnHover) {
116
+ opened.value = false
117
+ }
118
+ }
119
+
120
+ function onOptionClick(event: MouseEvent, option: FieldSelectOption) {
121
+ // Check that the option is not currently selected
122
+ if ((selectedOption.value || {}).value !== option.value) {
123
+ selectOption(event, option.value)
124
+ }
125
+
126
+ else {
127
+ reset()
128
+ }
129
+
130
+ emit('optionClick', event, option.value)
131
+ }
132
+ </script>
133
+
134
+ <template>
135
+ <div class="flex flex-col">
136
+ <FieldLabel
137
+ v-if="label"
138
+ :disabled="disabled"
139
+ :for-field="uuid"
140
+ has-margin-bottom
141
+ :icon="labelIcon"
142
+ :label="label"
143
+ :required="required"
144
+ :size="size"
145
+ @click="onLabelClick"
146
+ />
147
+
148
+ <div
149
+ v-on-click-outside="onClose"
150
+ class="text-left"
151
+ :class="{
152
+ 'cursor-not-allowed': disabled,
153
+ 'cursor-pointer': !disabled,
154
+ }"
155
+ @mouseenter="onMouseEnter"
156
+ @mouseleave="onMouseLeave"
157
+ >
158
+ <div
159
+ class="relative"
160
+ :class="{ 'shadow-sm': border === 'full' }"
161
+ >
162
+ <div
163
+ class="group flex items-center rounded-lg bg-white outline-none dark:bg-gray-900"
164
+ :class="{
165
+ 'border shadow-sm border-gray-200 dark:border-gray-800': border === 'full',
166
+ 'border-b border-gray-200 dark:border-gray-800': border === 'bottom',
167
+ 'border-0': border === 'none',
168
+ 'border-gray-400 dark:border-gray-600': opened,
169
+ 'hover:border-gray-300 dark:hover:border-gray-700': !opened,
170
+ 'focus:border-gray-400 dark:focus:border-gray-600': !disabled,
171
+
172
+ 'text-2xs': size === 'xs',
173
+ 'text-xs': size === 'sm',
174
+ 'text-sm': size === 'base',
175
+ 'text-base': size === 'lg',
176
+
177
+ 'px-4': padding,
178
+
179
+ 'h-8': size === 'xs',
180
+ 'h-10': size === 'sm',
181
+ 'h-12': size === 'base',
182
+ 'h-14': size === 'lg',
183
+ }"
184
+ tabindex="0"
185
+ @click="onContainerClick"
186
+ @keypress.prevent="onContainerKeypress"
187
+ >
188
+ <template v-if="selectedOption">
189
+ <BaseIcon
190
+ v-if="selectedOption.icon"
191
+ class="pointer-events-none mr-2"
192
+ :icon="selectedOption.icon"
193
+ />
194
+
195
+ <span class="flex-1 truncate font-medium uppercase">
196
+ {{ selectedOption.text }}
197
+ </span>
198
+ </template>
199
+
200
+ <span
201
+ v-else-if="placeholder"
202
+ class="flex-1 truncate text-gray-600 font-medium uppercase dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-gray-100"
203
+ >
204
+ {{ placeholder }}
205
+ </span>
206
+
207
+ <BaseIcon
208
+ v-if="caret"
209
+ class="ml-2 flex-initial"
210
+ :class="{ 'rotate-180 transform': opened }"
211
+ color="gray"
212
+ :icon="getIcon('arrowDown')"
213
+ />
214
+ </div>
215
+
216
+ <Motion
217
+ v-if="opened"
218
+ :animate="fadeIn.animate"
219
+ as="div"
220
+ class="absolute left-0 right-0 z-10 mt-0.5 cursor-default"
221
+ :class="{
222
+ 'top-0': direction === 'bottom',
223
+ 'bottom-0': direction === 'top',
224
+ }"
225
+ :initial="fadeIn.initial"
226
+ >
227
+ <div
228
+ class="options min-w-xs overflow-y-auto border border-gray-400 rounded-lg bg-white text-gray-600 font-medium dark:border-gray-600 dark:bg-gray-900 dark:text-gray-400"
229
+ :class="{
230
+ 'md:min-w-xl p-1 pb-0': computedColumns.length >= 2,
231
+
232
+ 'mt-8': ['xs', 'sm'].includes(size) && !padding && direction === 'bottom',
233
+ 'mt-10': ((['xs', 'sm'].includes(size) && padding) || (size === 'base' && !padding)) && direction === 'bottom',
234
+ 'mt-12': size === 'base' && padding && direction === 'bottom',
235
+ 'mb-8': ['xs', 'sm'].includes(size) && !padding && direction === 'top',
236
+ 'mb-10': ((['xs', 'sm'].includes(size) && padding) || (size === 'base' && !padding)) && direction === 'top',
237
+ 'mb-12': size === 'base' && padding && direction === 'top',
238
+
239
+ 'max-h-xs': maxHeight === 'xs',
240
+ 'max-h-sm': maxHeight === 'sm',
241
+ 'max-h-md': maxHeight === 'md',
242
+ 'max-h-lg': maxHeight === 'lg',
243
+ }"
244
+ >
245
+ <div
246
+ v-for="(column, columnIndex) in computedColumns"
247
+ :key="columnIndex"
248
+ >
249
+ <BaseIcon
250
+ v-if="column.title"
251
+ class="px-2 py-1 text-gray-900 dark:text-gray-100"
252
+ :text="column.title"
253
+ />
254
+
255
+ <div
256
+ v-for="option in column.options"
257
+ :key="option.value"
258
+ class="group flex cursor-pointer items-center outline-none"
259
+ :class="{
260
+ 'border-b border-gray-200 dark:border-gray-800 px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-900 last:border-b-0': computedColumns.length < 2,
261
+ 'px-2 py-1 last:mb-2': computedColumns.length >= 2,
262
+
263
+ 'font-medium underline': selectedOption && option.value === selectedOption.value,
264
+ 'text-gray-900 dark:text-gray-100': selectedOption && option.value === selectedOption.value,
265
+ 'bg-white dark:bg-gray-900': selectedOption && option.value !== selectedOption.value,
266
+
267
+ }"
268
+ @click="onOptionClick($event, option)"
269
+ >
270
+ <BaseIcon
271
+ v-if="option.text"
272
+ class="pointer-events-none mr-2 flex-initial"
273
+ :icon="option.icon"
274
+ :size="size"
275
+ :text="option.text"
276
+ />
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </Motion>
281
+ </div>
282
+ </div>
283
+
284
+ <FieldMessage
285
+ :description="description"
286
+ :hide-error="hideError"
287
+ :size="size"
288
+ :validation="validation"
289
+ />
290
+ </div>
291
+ </template>
@@ -0,0 +1,100 @@
1
+ <script lang="ts" setup>
2
+ import type { FieldTabs, FieldTabsAction } from '../../types/fields'
3
+ import { NuxtLinkLocale } from '#components'
4
+
5
+ const props = withDefaults(defineProps<FieldTabs>(), {
6
+ minimizeOnMobile: false,
7
+ modelValue: undefined,
8
+ multiple: false,
9
+ size: 'base',
10
+ tabs: undefined,
11
+ theme: 'rounded',
12
+ })
13
+
14
+ const emit = defineEmits<{
15
+ 'change': [event: MouseEvent, tabValue: number | string, action: FieldTabsAction, activeTabs: Array<number | string>]
16
+ 'click': [event: MouseEvent, tabValue: number | string, activeTabs: Array<number | string>]
17
+ 'update:modelValue': [value?: Array<number | string> | number | string]
18
+ }>()
19
+
20
+ function onTabClick(event: MouseEvent, tabValue: number | string) {
21
+ let activeTabs = [tabValue]
22
+
23
+ // When multiple values are not allowed and tab is not already active
24
+ if (!props.multiple && props.modelValue !== tabValue) {
25
+ emit('change', event, tabValue, 'added', activeTabs)
26
+ }
27
+
28
+ // When multiple values are allowed
29
+ if (props.multiple) {
30
+ // Remove the tab when already active
31
+ if (Array.isArray(props.modelValue) && props.modelValue.includes(tabValue)) {
32
+ activeTabs = props.modelValue.filter(item => item !== tabValue)
33
+
34
+ emit('change', event, tabValue, 'removed', activeTabs)
35
+ }
36
+ // Push the tab when not already active
37
+ else {
38
+ activeTabs = Array.isArray(props.modelValue) ? [...props.modelValue, tabValue] : [tabValue]
39
+ emit('change', event, tabValue, 'added', activeTabs)
40
+ }
41
+ }
42
+
43
+ emit('click', event, tabValue, activeTabs)
44
+ emit('update:modelValue', props.multiple ? activeTabs : tabValue)
45
+ }
46
+ </script>
47
+
48
+ <template>
49
+ <div
50
+ class="item-center flex border border-gray-300 shadow-inner dark:border-gray-700"
51
+ :class="{
52
+ 'gap-0 border-b border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-900': theme === 'border',
53
+ 'gap-2 rounded-lg bg-gray-200 dark:bg-gray-800 p-0.5 sm:p-1': theme === 'rounded',
54
+ }"
55
+ >
56
+ <component
57
+ :is="tab.to ? NuxtLinkLocale : 'div'"
58
+ v-for="tab in tabs"
59
+ :key="tab.value"
60
+ class="flex cursor-pointer items-center justify-center text-xs"
61
+ :class="{
62
+ 'text-gray-900 dark:text-gray-100': modelValue === tab.value,
63
+ 'text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100': modelValue !== tab.value,
64
+ 'flex-1': !minimizeOnMobile || modelValue !== tab.value,
65
+ 'flex-2 sm:flex-1': minimizeOnMobile && modelValue === tab.value,
66
+
67
+ 'py-1.5 px-2.5': size === 'xs' && theme === 'rounded',
68
+ 'py-2 px-3': size === 'sm' && theme === 'rounded',
69
+ 'py-2.5 px-3.5': size === 'base' && theme === 'rounded' || size === 'xs' && theme === 'border',
70
+ 'py-3 px-4': size === 'lg' && theme === 'rounded' || size === 'sm' && theme === 'border',
71
+ 'py-3.5 px-4.5': size === 'base' && theme === 'border',
72
+ 'py-4 px-5': size === 'lg' && theme === 'border',
73
+
74
+ 'border-b-2 border-gray-300 dark:border-gray-700 transition duration-500': theme === 'border',
75
+ 'border-gray-900 dark:border-gray-100': modelValue === tab.value && theme === 'border',
76
+ 'border-gray-100 dark:border-gray-900': modelValue !== tab.value && theme === 'border',
77
+
78
+ 'rounded-lg': theme === 'rounded',
79
+ 'bg-white dark:bg-gray-900 shadow': modelValue === tab.value && theme === 'rounded',
80
+ 'bg-gray-200 dark:bg-gray-800': modelValue !== tab.value && theme === 'rounded',
81
+ }"
82
+ :to="tab.to"
83
+ @click="onTabClick($event, tab.value)"
84
+ >
85
+ <BaseIcon
86
+ v-if="tab.icon"
87
+ class="mr-1.5 flex-initial"
88
+ :color="modelValue === tab.value ? tab.activeColor : 'black'"
89
+ :icon="tab.icon"
90
+ />
91
+
92
+ <span
93
+ class="flex-initial"
94
+ :class="{ 'hidden sm:inline': minimizeOnMobile && modelValue !== tab.value }"
95
+ >
96
+ {{ tab.label }}
97
+ </span>
98
+ </component>
99
+ </div>
100
+ </template>