@saasmakers/ui 0.1.50 → 0.1.51
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 +177 -0
- package/app/components/bases/BaseToast.vue +50 -0
- package/app/components/layout/LayoutToasts.vue +30 -0
- package/app/composables/useToasts.ts +41 -0
- package/app/composables/useUtils.ts +12 -0
- package/app/types/bases.d.ts +17 -0
- package/app/types/global.d.ts +4 -0
- package/package.json +3 -1
- package/public/images/bases/BaseAvatar/default.svg +22 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { NuxtLinkLocale } from '#components'
|
|
3
|
+
import { numbro } from '../../composables/useUtils'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
bordered: {
|
|
7
|
+
default: true,
|
|
8
|
+
type: Boolean,
|
|
9
|
+
},
|
|
10
|
+
borderWidth: {
|
|
11
|
+
default: 3,
|
|
12
|
+
type: Number,
|
|
13
|
+
},
|
|
14
|
+
circular: {
|
|
15
|
+
default: true,
|
|
16
|
+
type: Boolean,
|
|
17
|
+
},
|
|
18
|
+
editable: {
|
|
19
|
+
default: false,
|
|
20
|
+
type: Boolean,
|
|
21
|
+
},
|
|
22
|
+
habitsCompleted: {
|
|
23
|
+
default: undefined,
|
|
24
|
+
type: Number,
|
|
25
|
+
},
|
|
26
|
+
maxSizeMb: {
|
|
27
|
+
default: undefined,
|
|
28
|
+
type: Number,
|
|
29
|
+
},
|
|
30
|
+
shadow: {
|
|
31
|
+
default: true,
|
|
32
|
+
type: Boolean,
|
|
33
|
+
},
|
|
34
|
+
src: {
|
|
35
|
+
default: undefined,
|
|
36
|
+
type: String,
|
|
37
|
+
},
|
|
38
|
+
to: {
|
|
39
|
+
default: undefined,
|
|
40
|
+
type: Object as PropType<RouteLocationNamedI18n>,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits<{
|
|
45
|
+
avatarSelected: [event: Event, file: File]
|
|
46
|
+
click: [event: MouseEvent]
|
|
47
|
+
}>()
|
|
48
|
+
|
|
49
|
+
const { createToast } = useToasts()
|
|
50
|
+
|
|
51
|
+
const hovered = ref(false)
|
|
52
|
+
const error = ref(false)
|
|
53
|
+
const loaded = ref(false)
|
|
54
|
+
const fileInput = ref<HTMLInputElement>()
|
|
55
|
+
|
|
56
|
+
const { t } = useI18n()
|
|
57
|
+
|
|
58
|
+
function onAvatarSelected(event: Event) {
|
|
59
|
+
const file = (event.target as HTMLInputElement).files?.[0]
|
|
60
|
+
|
|
61
|
+
if (file) {
|
|
62
|
+
if (props.maxSizeMb && file.size > props.maxSizeMb * 1024 * 1024) {
|
|
63
|
+
return createToast('fileSizeTooLarge', 'error', { maxSizeMb: props.maxSizeMb })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
emit('avatarSelected', event, file)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onClick(event: MouseEvent) {
|
|
71
|
+
emit('click', event)
|
|
72
|
+
|
|
73
|
+
if (props.editable) {
|
|
74
|
+
fileInput.value?.click()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onError() {
|
|
79
|
+
error.value = true
|
|
80
|
+
loaded.value = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onMouseEnter() {
|
|
84
|
+
hovered.value = true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onMouseLeave() {
|
|
88
|
+
hovered.value = false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function onLoad() {
|
|
92
|
+
loaded.value = true
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<component
|
|
98
|
+
:is="to ? NuxtLinkLocale : 'div'"
|
|
99
|
+
class="relative flex"
|
|
100
|
+
:to="to"
|
|
101
|
+
@mouseenter="onMouseEnter"
|
|
102
|
+
@mouseleave="onMouseLeave"
|
|
103
|
+
>
|
|
104
|
+
<div
|
|
105
|
+
:aria-label="editable ? t('globals.edit') : undefined"
|
|
106
|
+
class="relative h-full w-full overflow-hidden"
|
|
107
|
+
:class="{
|
|
108
|
+
'rounded-full': circular,
|
|
109
|
+
'shadow': shadow,
|
|
110
|
+
'cursor-pointer': to || editable,
|
|
111
|
+
}"
|
|
112
|
+
:role="editable ? 'button' : undefined"
|
|
113
|
+
@click="onClick"
|
|
114
|
+
>
|
|
115
|
+
<img
|
|
116
|
+
class="h-full w-full object-cover drag-none"
|
|
117
|
+
:class="{
|
|
118
|
+
'rounded-full': circular,
|
|
119
|
+
'border-white dark:border-white': bordered,
|
|
120
|
+
'border-2': borderWidth === 2,
|
|
121
|
+
'border-3': borderWidth === 3,
|
|
122
|
+
'border-4': borderWidth === 4,
|
|
123
|
+
}"
|
|
124
|
+
loading="lazy"
|
|
125
|
+
:src="!error && src ? src : '/images/bases/BaseAvatar/default.svg'"
|
|
126
|
+
@error="onError"
|
|
127
|
+
@load="onLoad"
|
|
128
|
+
>
|
|
129
|
+
|
|
130
|
+
<BaseText
|
|
131
|
+
v-if="editable && hovered"
|
|
132
|
+
class="absolute bottom-0 left-0 right-0 w-full bg-gray-900 text-center text-white dark:bg-gray-900 dark:text-white"
|
|
133
|
+
size="3xs"
|
|
134
|
+
:text="t('globals.edit')"
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<input
|
|
138
|
+
v-if="editable"
|
|
139
|
+
ref="fileInput"
|
|
140
|
+
accept="image/*"
|
|
141
|
+
class="hidden"
|
|
142
|
+
type="file"
|
|
143
|
+
@change="onAvatarSelected"
|
|
144
|
+
>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<BaseText
|
|
148
|
+
v-if="habitsCompleted && (!editable || (editable && !hovered))"
|
|
149
|
+
v-tooltip="t('habitsCompleted')"
|
|
150
|
+
class="absolute left-1/2 transform select-none rounded bg-indigo-800 text-center text-white opacity-95 shadow -bottom-0.5 -translate-x-1/2 dark:bg-indigo-200 dark:text-black"
|
|
151
|
+
:class="{
|
|
152
|
+
'w-6': habitsCompleted < 100,
|
|
153
|
+
'w-7': habitsCompleted >= 100 && habitsCompleted < 1000,
|
|
154
|
+
'w-9': habitsCompleted >= 1000 && habitsCompleted < 10000,
|
|
155
|
+
'w-10': habitsCompleted >= 10000 && habitsCompleted < 100000,
|
|
156
|
+
'w-11': habitsCompleted >= 100000 && habitsCompleted < 1000000,
|
|
157
|
+
'w-13': habitsCompleted >= 1000000,
|
|
158
|
+
}"
|
|
159
|
+
size="2xs"
|
|
160
|
+
:text="numbro(habitsCompleted)"
|
|
161
|
+
/>
|
|
162
|
+
</component>
|
|
163
|
+
</template>
|
|
164
|
+
|
|
165
|
+
<i18n lang="json">
|
|
166
|
+
{
|
|
167
|
+
"en": {
|
|
168
|
+
"habitsCompleted": "Habits completed"
|
|
169
|
+
},
|
|
170
|
+
"fr": {
|
|
171
|
+
"habitsCompleted": "Habitudes accomplies"
|
|
172
|
+
},
|
|
173
|
+
"ja": {
|
|
174
|
+
"habitsCompleted": "習慣を完了しました"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</i18n>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { BaseToast } from '../../types/bases'
|
|
3
|
+
import { getIcon } from '../../composables/useIcons'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<BaseToast>(), {
|
|
6
|
+
hasClose: true,
|
|
7
|
+
id: '',
|
|
8
|
+
status: 'info',
|
|
9
|
+
text: '',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
close: [event: MouseEvent, toast: BaseToast]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
function onClose(event: MouseEvent) {
|
|
17
|
+
if (props.hasClose) {
|
|
18
|
+
emit('close', event, props)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div
|
|
25
|
+
class="group flex select-none items-center rounded-full bg-gray-900 px-3 py-2 text-base text-white font-normal dark:bg-gray-100 dark:text-black"
|
|
26
|
+
:class="{ 'cursor-pointer': hasClose }"
|
|
27
|
+
@click="onClose"
|
|
28
|
+
>
|
|
29
|
+
<BaseIcon
|
|
30
|
+
class="flex-initial"
|
|
31
|
+
:status="status"
|
|
32
|
+
:text="text"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<template v-if="hasClose">
|
|
36
|
+
<div class="ml-2 mr-1.5 h-5 border-l border-gray-700 flex-initial dark:border-gray-300" />
|
|
37
|
+
|
|
38
|
+
<BaseIcon
|
|
39
|
+
class="text-gray-500 flex-initial dark:text-gray-500"
|
|
40
|
+
:class="{
|
|
41
|
+
'group-hover:text-red-600 dark:group-hover:text-red-400': status === 'error',
|
|
42
|
+
'group-hover:text-indigo-600 dark:group-hover:text-indigo-400': status === 'info',
|
|
43
|
+
'group-hover:text-orange-600 dark:group-hover:text-orange-400': status === 'warning',
|
|
44
|
+
'group-hover:text-teal-600 dark:group-hover:text-teal-400': status === 'success',
|
|
45
|
+
}"
|
|
46
|
+
:icon="getIcon('close')"
|
|
47
|
+
/>
|
|
48
|
+
</template>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
const { fadeInUp } = useMotion()
|
|
3
|
+
const toasts = useToasts()
|
|
4
|
+
|
|
5
|
+
function onToastClose(_event: MouseEvent, toast: BaseToast) {
|
|
6
|
+
toasts.closeToast(toast.id)
|
|
7
|
+
}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="fixed bottom-0 left-0 right-0 z-10 flex flex-col items-center overflow-hidden safe-bottom">
|
|
12
|
+
<div class="mb-20 flex flex-col items-center">
|
|
13
|
+
<Motion
|
|
14
|
+
v-for="toast in toasts.toasts.value"
|
|
15
|
+
:key="toast.id"
|
|
16
|
+
:animate="fadeInUp.animate"
|
|
17
|
+
class="mb-2 flex-initial last:mb-0"
|
|
18
|
+
:initial="fadeInUp.initial"
|
|
19
|
+
>
|
|
20
|
+
<BaseToast
|
|
21
|
+
:id="toast.id"
|
|
22
|
+
:key="toast.id"
|
|
23
|
+
:status="toast.status"
|
|
24
|
+
:text="toast.text"
|
|
25
|
+
@close="onToastClose"
|
|
26
|
+
/>
|
|
27
|
+
</Motion>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const toasts = ref<BaseToast[]>([])
|
|
2
|
+
|
|
3
|
+
export default function useToast() {
|
|
4
|
+
const nuxtApp = useNuxtApp()
|
|
5
|
+
|
|
6
|
+
const clearToasts = () => {
|
|
7
|
+
if (import.meta.client) {
|
|
8
|
+
toasts.value = []
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const closeToast = (toastId: string) => {
|
|
13
|
+
if (import.meta.client) {
|
|
14
|
+
toasts.value = toasts.value.filter((item) => {
|
|
15
|
+
return item.id !== toastId
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const createToast = (message: string, status: BaseStatus = 'success', i18nParams?: { [key: string]: number | string }) => {
|
|
21
|
+
if (import.meta.client) {
|
|
22
|
+
const toast: BaseToast = {
|
|
23
|
+
id: Math.floor((1 + Math.random()) * 0x10000).toString(16),
|
|
24
|
+
status: status || 'success',
|
|
25
|
+
text: message.includes(' ') ? message : nuxtApp.$i18n.t(`toasts.${message}`, i18nParams || {}),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toasts.value.push(toast)
|
|
29
|
+
|
|
30
|
+
// Remove toast after 5 seconds
|
|
31
|
+
setTimeout(() => closeToast(toast.id), 5 * 1000)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
clearToasts,
|
|
37
|
+
closeToast,
|
|
38
|
+
createToast,
|
|
39
|
+
toasts,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import numbroLib from 'numbro'
|
|
2
|
+
|
|
3
|
+
export function numbro(number: '∞' | number | undefined, format?: string) {
|
|
4
|
+
if (!number && number !== 0) {
|
|
5
|
+
return ''
|
|
6
|
+
}
|
|
7
|
+
else if (number === '∞' || number % 1 !== 0) {
|
|
8
|
+
return `${number}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return numbroLib(number).format(format || '0,0')
|
|
12
|
+
}
|
package/app/types/bases.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ export interface BaseAlert {
|
|
|
35
35
|
text?: BaseTextText
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
interface BaseAvatar {
|
|
39
|
+
bordered?: boolean
|
|
40
|
+
borderWidth?: number
|
|
41
|
+
circular?: boolean
|
|
42
|
+
habitsCompleted?: number
|
|
43
|
+
shadow?: boolean
|
|
44
|
+
src?: string
|
|
45
|
+
to?: RouteLocationNamedI18n
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
export interface BaseBordered {
|
|
39
49
|
background?: BaseBackground
|
|
40
50
|
borderColor?: BaseBorderedColor
|
|
@@ -182,3 +192,10 @@ export interface BaseText {
|
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
export type BaseTextText = string | { base: string, sm: string }
|
|
195
|
+
|
|
196
|
+
export interface BaseToast {
|
|
197
|
+
hasClose?: boolean
|
|
198
|
+
id: string
|
|
199
|
+
status?: BaseStatus
|
|
200
|
+
text: BaseTextText
|
|
201
|
+
}
|
package/app/types/global.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare global {
|
|
|
6
6
|
|
|
7
7
|
// Bases
|
|
8
8
|
type BaseAlert = Bases.BaseAlert
|
|
9
|
+
type BaseAvatar = Bases.BaseAvatar
|
|
9
10
|
type BaseBackground = Bases.BaseBackground
|
|
10
11
|
type BaseBordered = Bases.BaseBordered
|
|
11
12
|
type BaseBorderedColor = Bases.BaseBorderedColor
|
|
@@ -38,6 +39,9 @@ declare global {
|
|
|
38
39
|
type BaseStatus = Bases.BaseStatus
|
|
39
40
|
type BaseText = Bases.BaseText
|
|
40
41
|
type BaseTextText = Bases.BaseTextText
|
|
42
|
+
type BaseToast = Bases.BaseToast
|
|
43
|
+
type BaseToasts = Bases.BaseToasts
|
|
44
|
+
type BaseToastsAlignment = Bases.BaseToastsAlignment
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
export * from './bases'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasmakers/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.51",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Reusable Nuxt UI components for SaaS Makers projects",
|
|
7
7
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"app",
|
|
20
20
|
"nuxt.config.ts",
|
|
21
|
+
"public",
|
|
21
22
|
"uno.config.ts"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"@unocss/reset": "66.5.10",
|
|
43
44
|
"floating-vue": "5.2.2",
|
|
44
45
|
"motion-v": "1.7.4",
|
|
46
|
+
"numbro": "2.5.0",
|
|
45
47
|
"unocss": "66.5.4",
|
|
46
48
|
"vue": "3.5.22",
|
|
47
49
|
"vue-router": "4.6.3"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="200" height="200" fill="#F8FAFC"/>
|
|
3
|
+
<g clip-path="url(#clip0)">
|
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M171.236 99.9916C171.236 73.1755 149.464 51.4038 122.648 51.4038H77.4786C50.6624 51.4038 28.891 73.1755 28.891 99.9916C28.891 126.808 50.6624 148.579 77.4786 148.579H122.648C149.464 148.579 171.236 126.808 171.236 99.9916Z" fill="url(#paint0_radial)"/>
|
|
5
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M158.977 107.28C158.977 80.0811 132.65 71.9116 100.695 71.9116C64.5672 71.9116 41.159 79.4983 41.159 107.28C41.159 132.536 67.2716 141.093 100.258 141.093C138.413 141.093 158.977 135.644 158.977 107.28Z" fill="url(#paint1_radial)"/>
|
|
6
|
+
<path d="M75.8 124.527C81.5881 124.527 86.2802 118.069 86.2802 110.103C86.2802 102.137 81.5881 95.6787 75.8 95.6787C70.012 95.6787 65.3198 102.137 65.3198 110.103C65.3198 118.069 70.012 124.527 75.8 124.527Z" fill="#F9F9F9"/>
|
|
7
|
+
<path d="M126.74 124.747C132.528 124.747 137.22 118.289 137.22 110.323C137.22 102.356 132.528 95.8984 126.74 95.8984C120.952 95.8984 116.26 102.356 116.26 110.323C116.26 118.289 120.952 124.747 126.74 124.747Z" fill="#F9F9F9"/>
|
|
8
|
+
</g>
|
|
9
|
+
<defs>
|
|
10
|
+
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(93.6026 101.03) rotate(-0.40466) scale(149.159 170.801)">
|
|
11
|
+
<stop stop-color="#FCFCFC"/>
|
|
12
|
+
<stop offset="1" stop-color="#D4D4D4"/>
|
|
13
|
+
</radialGradient>
|
|
14
|
+
<radialGradient id="paint1_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(101.232 106.114) rotate(-0.246549) scale(90.3212 53.0581)">
|
|
15
|
+
<stop stop-color="#2E3748"/>
|
|
16
|
+
<stop offset="1" stop-color="#030812"/>
|
|
17
|
+
</radialGradient>
|
|
18
|
+
<clipPath id="clip0">
|
|
19
|
+
<rect width="150" height="150" fill="white" transform="translate(25 25)"/>
|
|
20
|
+
</clipPath>
|
|
21
|
+
</defs>
|
|
22
|
+
</svg>
|