@saasmakers/ui 0.1.59 → 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/BaseTag.vue +179 -0
- package/app/components/bases/BaseTags.vue +242 -0
- 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/useIcons.ts +4 -0
- package/app/composables/useUtils.ts +7 -0
- package/app/types/bases.d.ts +42 -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,109 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { FieldDays } from '../../types/fields'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<FieldDays>(), { modelValue: () => [] })
|
|
5
|
+
|
|
6
|
+
const emit = defineEmits<{
|
|
7
|
+
'change': [event: MouseEvent, value: number[]]
|
|
8
|
+
'update:modelValue': [value: number[]]
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
|
|
13
|
+
const daysToDisplay = computed(() => {
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
label: t('days.monday'),
|
|
17
|
+
value: 1,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: t('days.tuesday'),
|
|
21
|
+
value: 2,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: t('days.wednesday'),
|
|
25
|
+
value: 3,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: t('days.thursday'),
|
|
29
|
+
value: 4,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: t('days.friday'),
|
|
33
|
+
value: 5,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: t('days.saturday'),
|
|
37
|
+
value: 6,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: t('days.sunday'),
|
|
41
|
+
value: 0,
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function onUpdateDays(event: MouseEvent, day: number) {
|
|
47
|
+
const days = [...props.modelValue]
|
|
48
|
+
|
|
49
|
+
days.includes(day) ? days.splice(days.indexOf(day), 1) : days.push(day)
|
|
50
|
+
|
|
51
|
+
emit('change', event, days)
|
|
52
|
+
emit('update:modelValue', days)
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div class="flex items-center">
|
|
58
|
+
<BaseText
|
|
59
|
+
v-for="day in daysToDisplay"
|
|
60
|
+
:key="day.value"
|
|
61
|
+
class="mr-2 h-7 w-7 flex cursor-pointer items-center justify-center border rounded-full shadow-inner last:mr-0"
|
|
62
|
+
:class="{
|
|
63
|
+
'border-indigo-700 dark:border-indigo-300 bg-gray-100 dark:bg-gray-900 text-indigo-700 dark:text-indigo-300': modelValue.includes(day.value),
|
|
64
|
+
'border-red-700 dark:border-red-300 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300': !modelValue.includes(day.value),
|
|
65
|
+
}"
|
|
66
|
+
size="xs"
|
|
67
|
+
:text="day.label.charAt(0)"
|
|
68
|
+
@click="onUpdateDays($event, day.value)"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<i18n lang="json">
|
|
74
|
+
{
|
|
75
|
+
"en": {
|
|
76
|
+
"days": {
|
|
77
|
+
"monday": "Monday",
|
|
78
|
+
"tuesday": "Tuesday",
|
|
79
|
+
"wednesday": "Wednesday",
|
|
80
|
+
"thursday": "Thursday",
|
|
81
|
+
"friday": "Friday",
|
|
82
|
+
"saturday": "Saturday",
|
|
83
|
+
"sunday": "Sunday"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"fr": {
|
|
87
|
+
"days": {
|
|
88
|
+
"monday": "Lundi",
|
|
89
|
+
"tuesday": "Mardi",
|
|
90
|
+
"wednesday": "Mercredi",
|
|
91
|
+
"thursday": "Jeudi",
|
|
92
|
+
"friday": "Vendredi",
|
|
93
|
+
"saturday": "Samedi",
|
|
94
|
+
"sunday": "Dimanche"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"ja": {
|
|
98
|
+
"days": {
|
|
99
|
+
"monday": "月曜日",
|
|
100
|
+
"tuesday": "火曜日",
|
|
101
|
+
"wednesday": "水曜日",
|
|
102
|
+
"thursday": "木曜日",
|
|
103
|
+
"friday": "金曜日",
|
|
104
|
+
"saturday": "土曜日",
|
|
105
|
+
"sunday": "日曜日"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</i18n>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { FieldEmojis } from '../../types/fields'
|
|
3
|
+
import { emojis } from '@saasmakers/shared'
|
|
4
|
+
import { refDebounced } from '@vueuse/core'
|
|
5
|
+
|
|
6
|
+
withDefaults(defineProps<FieldEmojis>(), { modelValue: undefined })
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits<{
|
|
9
|
+
'click': [event: MouseEvent, emoji: string]
|
|
10
|
+
'update:modelValue': [value: string]
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const { locale, t } = useI18n()
|
|
14
|
+
|
|
15
|
+
const searchRaw = ref('')
|
|
16
|
+
const searchQuery = refDebounced(searchRaw, 250)
|
|
17
|
+
|
|
18
|
+
const emojisFiltered = computed(() => {
|
|
19
|
+
const searchQueryCleaned = normalizeText(searchQuery.value.trim())
|
|
20
|
+
|
|
21
|
+
if (!searchQueryCleaned) {
|
|
22
|
+
return emojis
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return emojis
|
|
26
|
+
.map((category) => {
|
|
27
|
+
const filtered = category.emojis.filter((emoji) => {
|
|
28
|
+
const localizedKeywords = (emoji.keywords[locale.value] ?? [])
|
|
29
|
+
const englishKeywords = (emoji.keywords.en ?? [])
|
|
30
|
+
const allKeywords = [...localizedKeywords, ...englishKeywords]
|
|
31
|
+
|
|
32
|
+
return allKeywords.some(keyword => normalizeText(keyword).includes(searchQueryCleaned))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
...category,
|
|
37
|
+
emojis: filtered,
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.filter(category => category.emojis.length > 0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
function onEmojiClick(event: MouseEvent, emoji?: string) {
|
|
44
|
+
emit('click', event, emoji || '')
|
|
45
|
+
emit('update:modelValue', emoji || '')
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div class="border border-gray-200 rounded-lg p-4 shadow-sm dark:border-gray-800">
|
|
51
|
+
<FieldInput
|
|
52
|
+
v-model="searchRaw"
|
|
53
|
+
autofocus
|
|
54
|
+
:placeholder="t('searchEmoji')"
|
|
55
|
+
size="sm"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<div class="h-48 overflow-y-auto">
|
|
59
|
+
<template
|
|
60
|
+
v-for="category in emojisFiltered"
|
|
61
|
+
:key="category.category"
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
v-if="category.emojis.length"
|
|
65
|
+
class="mt-4"
|
|
66
|
+
>
|
|
67
|
+
<div class="mb-4">
|
|
68
|
+
<BaseText
|
|
69
|
+
class="mb-2 text-xs text-gray-700 dark:text-gray-300"
|
|
70
|
+
size="xs"
|
|
71
|
+
:text="category.category"
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<div class="flex flex-wrap">
|
|
75
|
+
<BaseEmoji
|
|
76
|
+
v-for="emoji in category.emojis"
|
|
77
|
+
:key="emoji.id"
|
|
78
|
+
class="rounded-lg p-1.5 hover:bg-gray-200 dark:hover:bg-gray-800"
|
|
79
|
+
clickable
|
|
80
|
+
:emoji="emoji.id"
|
|
81
|
+
size="xl"
|
|
82
|
+
@click="onEmojiClick"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<i18n lang="json">
|
|
93
|
+
{
|
|
94
|
+
"en": {
|
|
95
|
+
"searchEmoji": "Search an emoji"
|
|
96
|
+
},
|
|
97
|
+
"fr": {
|
|
98
|
+
"searchEmoji": "Rechercher un emoji (en anglais)"
|
|
99
|
+
},
|
|
100
|
+
"ja": {
|
|
101
|
+
"searchEmoji": "絵文字を検索 (英語)"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
</i18n>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { FieldInput } from '../../types/fields'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<FieldInput>(), {
|
|
5
|
+
alignment: 'left',
|
|
6
|
+
autocomplete: true,
|
|
7
|
+
autofocus: false,
|
|
8
|
+
background: 'gray',
|
|
9
|
+
border: 'full',
|
|
10
|
+
description: '',
|
|
11
|
+
disabled: false,
|
|
12
|
+
fullWidth: false,
|
|
13
|
+
hideError: false,
|
|
14
|
+
label: '',
|
|
15
|
+
labelIcon: undefined,
|
|
16
|
+
lineThrough: false,
|
|
17
|
+
loading: false,
|
|
18
|
+
lowercaseOnly: false,
|
|
19
|
+
max: undefined,
|
|
20
|
+
min: undefined,
|
|
21
|
+
modelValue: '',
|
|
22
|
+
placeholder: '',
|
|
23
|
+
required: false,
|
|
24
|
+
size: 'base',
|
|
25
|
+
type: 'text',
|
|
26
|
+
uppercase: false,
|
|
27
|
+
validation: undefined,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
'blur': [event: FocusEvent]
|
|
32
|
+
'focus': [event: FocusEvent]
|
|
33
|
+
'keyup': [event: KeyboardEvent, value: number | string ]
|
|
34
|
+
'submit': [event: KeyboardEvent, value: number | string ]
|
|
35
|
+
'update:modelValue': [value: number | string ]
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
const input = ref<HTMLInputElement>()
|
|
39
|
+
const uuid = ref(`${Math.floor((1 + Math.random()) * 0x100000)}`)
|
|
40
|
+
|
|
41
|
+
const { isDesktopBrowser } = useDevice()
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
if (props.autofocus && input.value && isDesktopBrowser.value) {
|
|
45
|
+
input.value.focus()
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const value = computed({
|
|
50
|
+
get() {
|
|
51
|
+
return props.modelValue
|
|
52
|
+
},
|
|
53
|
+
set(value) {
|
|
54
|
+
emit('update:modelValue', value || '')
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function blur() {
|
|
59
|
+
if (input.value) {
|
|
60
|
+
input.value.blur()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function focus() {
|
|
65
|
+
if (input.value) {
|
|
66
|
+
input.value.focus()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onFieldBlur(event: FocusEvent) {
|
|
71
|
+
emit('blur', event)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onFieldFocus(event: FocusEvent) {
|
|
75
|
+
emit('focus', event)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onFieldInput(event: Event) {
|
|
79
|
+
let value: number | string = (event.target as HTMLInputElement).value
|
|
80
|
+
|
|
81
|
+
if (props.type === 'number' && props.max && Number.parseInt(value) > props.max) {
|
|
82
|
+
value = props.max
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (props.lowercaseOnly) {
|
|
86
|
+
value = value.toString().toLowerCase()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
emit('update:modelValue', value)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onFieldKeyDown(event: KeyboardEvent) {
|
|
93
|
+
const value = (event.target as HTMLInputElement).value
|
|
94
|
+
|
|
95
|
+
if (['Control', 'Escape'].includes(event.key)) {
|
|
96
|
+
blur()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
else if (event.key === 'Enter' && props.modelValue) {
|
|
100
|
+
emit('submit', event, value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
else if (props.type === 'number' && [
|
|
104
|
+
'+',
|
|
105
|
+
'-',
|
|
106
|
+
':',
|
|
107
|
+
'^',
|
|
108
|
+
'e',
|
|
109
|
+
'E',
|
|
110
|
+
].includes(event.key)) {
|
|
111
|
+
return event.preventDefault()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
emit('keyup', event, value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Expose
|
|
118
|
+
defineExpose({ focus })
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<div
|
|
123
|
+
class="relative flex flex-col"
|
|
124
|
+
:class="{ 'w-full': fullWidth }"
|
|
125
|
+
>
|
|
126
|
+
<div
|
|
127
|
+
v-if="label"
|
|
128
|
+
class="flex justify-between"
|
|
129
|
+
>
|
|
130
|
+
<FieldLabel
|
|
131
|
+
:disabled="disabled"
|
|
132
|
+
:for-field="uuid"
|
|
133
|
+
has-margin-bottom
|
|
134
|
+
:icon="labelIcon"
|
|
135
|
+
:label="label"
|
|
136
|
+
:required="required"
|
|
137
|
+
:size="size"
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<slot name="label" />
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div
|
|
144
|
+
class="flex items-center"
|
|
145
|
+
:class="{
|
|
146
|
+
'h-8': size === 'xs',
|
|
147
|
+
'h-10': size === 'sm',
|
|
148
|
+
'h-12': size === 'base',
|
|
149
|
+
'h-14': size === 'lg',
|
|
150
|
+
}"
|
|
151
|
+
>
|
|
152
|
+
<slot name="inputLeft" />
|
|
153
|
+
|
|
154
|
+
<input
|
|
155
|
+
:id="uuid"
|
|
156
|
+
ref="input"
|
|
157
|
+
v-model="value"
|
|
158
|
+
:autocomplete="autocomplete ? 'on' : 'off'"
|
|
159
|
+
class="h-full w-full flex-1 appearance-none items-center rounded-lg outline-none placeholder-gray-600 dark:placeholder-gray-400 focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100"
|
|
160
|
+
:class="{
|
|
161
|
+
'text-gray-900 dark:text-gray-100': !lineThrough,
|
|
162
|
+
'text-gray-500 dark:text-gray-500 line-through': lineThrough,
|
|
163
|
+
'font-medium tracking-tight uppercase': uppercase,
|
|
164
|
+
'font-normal': !uppercase,
|
|
165
|
+
|
|
166
|
+
'text-center': alignment === 'center',
|
|
167
|
+
'text-left': alignment === 'left',
|
|
168
|
+
'text-right': alignment === 'right',
|
|
169
|
+
|
|
170
|
+
'bg-gray-100 dark:bg-gray-900': background === 'gray',
|
|
171
|
+
'bg-white dark:bg-gray-900': background === 'white',
|
|
172
|
+
|
|
173
|
+
'border shadow-sm border-gray-200 dark:border-gray-800 px-4 hover:border-gray-300 focus:border-gray-400 dark:hover:border-gray-700 dark:focus:border-gray-600': border === 'full',
|
|
174
|
+
'border-b border-gray-200 dark:border-gray-800 px-10': border === 'bottom',
|
|
175
|
+
'border-0 p-0': border === 'none',
|
|
176
|
+
|
|
177
|
+
'text-xs': size === 'xs',
|
|
178
|
+
'text-sm': size === 'sm',
|
|
179
|
+
'text-base': size === 'base',
|
|
180
|
+
'text-lg': size === 'lg',
|
|
181
|
+
}"
|
|
182
|
+
:max="max"
|
|
183
|
+
:min="min"
|
|
184
|
+
:placeholder="placeholder"
|
|
185
|
+
spellcheck="false"
|
|
186
|
+
:type="type"
|
|
187
|
+
@blur="onFieldBlur"
|
|
188
|
+
@focus="onFieldFocus"
|
|
189
|
+
@input="onFieldInput"
|
|
190
|
+
@keydown="onFieldKeyDown"
|
|
191
|
+
>
|
|
192
|
+
|
|
193
|
+
<BaseSpinner
|
|
194
|
+
v-if="loading"
|
|
195
|
+
class="ml-4 flex-initial"
|
|
196
|
+
size="sm"
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
<slot name="inputRight" />
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<FieldMessage
|
|
203
|
+
:description="description"
|
|
204
|
+
:hide-error="hideError"
|
|
205
|
+
:size="size"
|
|
206
|
+
:validation="validation"
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
</template>
|
|
@@ -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>
|