@saasmakers/ui 0.1.60 → 0.1.61
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/app/components/bases/BaseAvatar.vue +5 -5
- package/app/components/bases/BaseButton.vue +15 -1
- package/app/components/bases/BaseIcon.vue +15 -1
- package/app/components/bases/BaseQuote.vue +15 -1
- package/app/components/bases/BaseTags.vue +5 -1
- package/app/components/fields/FieldCheckbox.vue +98 -0
- package/app/components/fields/FieldDays.vue +109 -0
- package/app/components/fields/FieldEmojis.vue +104 -0
- package/app/components/fields/FieldInput.vue +209 -0
- package/app/components/fields/FieldLabel.vue +64 -0
- package/app/components/fields/FieldMessage.vue +209 -0
- package/app/components/fields/FieldSelect.vue +291 -0
- package/app/components/fields/FieldTabs.vue +100 -0
- package/app/components/fields/FieldTextarea.vue +123 -0
- package/app/components/fields/FieldTime.vue +73 -0
- package/app/composables/useDevice.ts +11 -0
- package/app/composables/useUtils.ts +7 -0
- package/app/types/fields.d.ts +179 -0
- package/app/types/global.d.ts +10 -0
- package/nuxt.config.ts +8 -4
- package/package.json +7 -1
|
@@ -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>
|