@saasmakers/ui 1.3.9 → 1.4.1
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/BaseToast.stories.ts +13 -0
- package/app/components/bases/BaseToast.vue +97 -9
- package/app/components/fields/FieldInput.vue +6 -6
- package/app/components/layout/LayoutToasts.vue +8 -0
- package/app/composables/useToasts.ts +4 -3
- package/app/types/bases.d.ts +6 -0
- package/app/types/global.d.ts +2 -0
- package/package.json +1 -1
|
@@ -27,3 +27,16 @@ export const Default: StoryObj<typeof BaseToast> = {
|
|
|
27
27
|
text: 'Toast message',
|
|
28
28
|
} satisfies Partial<BaseToast>,
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export const WithAction: StoryObj<typeof BaseToast> = {
|
|
32
|
+
args: {
|
|
33
|
+
action: {
|
|
34
|
+
label: 'Undo',
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
onClick: () => console.log('action clicked'),
|
|
37
|
+
},
|
|
38
|
+
id: 'toast-2',
|
|
39
|
+
status: 'success',
|
|
40
|
+
text: 'Toast message',
|
|
41
|
+
} satisfies Partial<BaseToast>,
|
|
42
|
+
}
|
|
@@ -9,11 +9,73 @@ const props = withDefaults(defineProps<BaseToast>(), {
|
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
const emit = defineEmits<{
|
|
12
|
+
action: [event: MouseEvent, toast: BaseToast]
|
|
12
13
|
close: [event: MouseEvent, toast: BaseToast]
|
|
13
14
|
}>()
|
|
14
15
|
|
|
15
16
|
const { getIcon } = useLayerIcons()
|
|
16
17
|
|
|
18
|
+
const actionPillClasses = computed(() => {
|
|
19
|
+
switch (props.status) {
|
|
20
|
+
case 'error':
|
|
21
|
+
return 'bg-red-600/20 text-red-400 hover:bg-red-600/30 focus-visible:ring-red-500 dark:bg-red-400/15 dark:text-red-400 dark:hover:bg-red-400/25'
|
|
22
|
+
case 'info':
|
|
23
|
+
return 'bg-indigo-600/20 text-indigo-400 hover:bg-indigo-600/30 focus-visible:ring-indigo-500 dark:bg-indigo-400/15 dark:text-indigo-400 dark:hover:bg-indigo-400/25'
|
|
24
|
+
case 'success':
|
|
25
|
+
return 'bg-green-600/20 text-green-400 hover:bg-green-600/30 focus-visible:ring-green-500 dark:bg-green-400/15 dark:text-green-400 dark:hover:bg-green-400/25'
|
|
26
|
+
case 'warning':
|
|
27
|
+
return 'bg-orange-600/20 text-orange-400 hover:bg-orange-600/30 focus-visible:ring-orange-500 dark:bg-orange-400/15 dark:text-orange-400 dark:hover:bg-orange-400/25'
|
|
28
|
+
default:
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const closeHoverClasses = computed(() => {
|
|
34
|
+
switch (props.status) {
|
|
35
|
+
case 'error':
|
|
36
|
+
return 'hover:text-red-600 dark:hover:text-red-400'
|
|
37
|
+
case 'info':
|
|
38
|
+
return 'hover:text-indigo-600 dark:hover:text-indigo-400'
|
|
39
|
+
case 'success':
|
|
40
|
+
return 'hover:text-green-600 dark:hover:text-green-400'
|
|
41
|
+
case 'warning':
|
|
42
|
+
return 'hover:text-orange-600 dark:hover:text-orange-400'
|
|
43
|
+
default:
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const isDismissibleByClick = computed(() => {
|
|
49
|
+
return props.hasClose && !props.action
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const rootInteractiveBindings = computed(() => {
|
|
53
|
+
if (!isDismissibleByClick.value) {
|
|
54
|
+
return {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
onClick: onClose,
|
|
59
|
+
onKeydown: (event: KeyboardEvent) => {
|
|
60
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
61
|
+
if (event.key === ' ') {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onClose(event)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
role: 'button' as const,
|
|
69
|
+
tabindex: 0,
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
function onAction(event: KeyboardEvent | MouseEvent) {
|
|
74
|
+
if (props.action) {
|
|
75
|
+
emit('action', event as MouseEvent, props)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
17
79
|
function onClose(event: KeyboardEvent | MouseEvent) {
|
|
18
80
|
if (props.hasClose) {
|
|
19
81
|
emit('close', event as MouseEvent, props)
|
|
@@ -23,13 +85,9 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
23
85
|
|
|
24
86
|
<template>
|
|
25
87
|
<div
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
tabindex="0"
|
|
30
|
-
@click="onClose"
|
|
31
|
-
@keydown.enter="onClose"
|
|
32
|
-
@keydown.space.prevent="onClose"
|
|
88
|
+
v-bind="rootInteractiveBindings"
|
|
89
|
+
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-800 dark:text-gray-100 dark:ring-1 dark:ring-gray-700"
|
|
90
|
+
:class="{ 'cursor-pointer': isDismissibleByClick }"
|
|
33
91
|
>
|
|
34
92
|
<BaseIcon
|
|
35
93
|
class="pointer-events-none flex-initial"
|
|
@@ -37,16 +95,46 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
37
95
|
:text="text"
|
|
38
96
|
/>
|
|
39
97
|
|
|
98
|
+
<button
|
|
99
|
+
v-if="action"
|
|
100
|
+
class="ml-2 rounded-full px-2.5 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-gray-900 dark:focus-visible:ring-offset-gray-800"
|
|
101
|
+
:class="actionPillClasses"
|
|
102
|
+
type="button"
|
|
103
|
+
@click.stop="onAction"
|
|
104
|
+
>
|
|
105
|
+
<BaseText
|
|
106
|
+
bold
|
|
107
|
+
class="pointer-events-none"
|
|
108
|
+
no-wrap
|
|
109
|
+
:text="action.label"
|
|
110
|
+
uppercase
|
|
111
|
+
/>
|
|
112
|
+
</button>
|
|
113
|
+
|
|
40
114
|
<template v-if="hasClose">
|
|
41
|
-
<div class="ml-2 mr-1.5 h-5 border-l border-gray-700 flex-initial dark:border-gray-
|
|
115
|
+
<div class="ml-2 mr-1.5 h-5 border-l border-gray-700 flex-initial dark:border-gray-600" />
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
v-if="action"
|
|
119
|
+
class="cursor-pointer bg-transparent text-gray-500 flex-initial dark:text-gray-500"
|
|
120
|
+
:class="closeHoverClasses"
|
|
121
|
+
type="button"
|
|
122
|
+
@click.stop="onClose"
|
|
123
|
+
>
|
|
124
|
+
<BaseIcon
|
|
125
|
+
class="pointer-events-none"
|
|
126
|
+
:icon="getIcon('close')"
|
|
127
|
+
/>
|
|
128
|
+
</button>
|
|
42
129
|
|
|
43
130
|
<BaseIcon
|
|
131
|
+
v-else
|
|
44
132
|
class="text-gray-500 flex-initial dark:text-gray-500"
|
|
45
133
|
:class="{
|
|
46
134
|
'group-hover:text-red-600 dark:group-hover:text-red-400': status === 'error',
|
|
47
135
|
'group-hover:text-indigo-600 dark:group-hover:text-indigo-400': status === 'info',
|
|
136
|
+
'group-hover:text-green-600 dark:group-hover:text-green-400': status === 'success',
|
|
48
137
|
'group-hover:text-orange-600 dark:group-hover:text-orange-400': status === 'warning',
|
|
49
|
-
'group-hover:text-teal-600 dark:group-hover:text-teal-400': status === 'success',
|
|
50
138
|
}"
|
|
51
139
|
:icon="getIcon('close')"
|
|
52
140
|
/>
|
|
@@ -67,14 +67,11 @@ function focus() {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function
|
|
70
|
+
function onFieldBlur(event: FocusEvent) {
|
|
71
71
|
if (props.trim) {
|
|
72
72
|
modelValue.value = modelValue.value.toString().trim()
|
|
73
73
|
}
|
|
74
|
-
}
|
|
75
74
|
|
|
76
|
-
function onFieldBlur(event: FocusEvent) {
|
|
77
|
-
trimModelValue()
|
|
78
75
|
emit('blur', event)
|
|
79
76
|
}
|
|
80
77
|
|
|
@@ -107,8 +104,11 @@ function onFieldKeyDown(event: KeyboardEvent) {
|
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
else if (event.key === 'Enter' && modelValue.value) {
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
if (props.trim) {
|
|
108
|
+
modelValue.value = modelValue.value.toString().trim()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
emit('submit', event, modelValue.value.toString())
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
else if (props.type === 'number' && [
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
const { fadeInUp } = useMotion()
|
|
3
3
|
const toasts = useToasts()
|
|
4
4
|
|
|
5
|
+
async function onToastAction(_event: MouseEvent, toast: BaseToast) {
|
|
6
|
+
toasts.closeToast(toast.id)
|
|
7
|
+
|
|
8
|
+
await toast.action?.onClick()
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
function onToastClose(_event: MouseEvent, toast: BaseToast) {
|
|
6
12
|
toasts.closeToast(toast.id)
|
|
7
13
|
}
|
|
@@ -20,8 +26,10 @@ function onToastClose(_event: MouseEvent, toast: BaseToast) {
|
|
|
20
26
|
<BaseToast
|
|
21
27
|
:id="toast.id"
|
|
22
28
|
:key="toast.id"
|
|
29
|
+
:action="toast.action"
|
|
23
30
|
:status="toast.status"
|
|
24
31
|
:text="toast.text"
|
|
32
|
+
@action="onToastAction"
|
|
25
33
|
@close="onToastClose"
|
|
26
34
|
/>
|
|
27
35
|
</Motion>
|
|
@@ -17,9 +17,10 @@ export default function useToast() {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const createToast = (message: string, status: BaseStatus = 'success', i18nParams?: Record<string, number | string
|
|
20
|
+
const createToast = (message: string, status: BaseStatus = 'success', i18nParams?: Record<string, number | string>, action?: BaseToastAction) => {
|
|
21
21
|
if (import.meta.client) {
|
|
22
22
|
const toast: BaseToast = {
|
|
23
|
+
action,
|
|
23
24
|
id: crypto.randomUUID(),
|
|
24
25
|
status: status || 'success',
|
|
25
26
|
text: message.includes(' ') ? message : nuxtApp.$i18n.t(`toasts.${message}`, i18nParams || {}),
|
|
@@ -27,8 +28,8 @@ export default function useToast() {
|
|
|
27
28
|
|
|
28
29
|
toasts.value.push(toast)
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
-
setTimeout(() => closeToast(toast.id), 5 * 1000)
|
|
31
|
+
// Give more time to interact when the toast offers an action
|
|
32
|
+
setTimeout(() => closeToast(toast.id), (action ? 8 : 5) * 1000)
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
package/app/types/bases.d.ts
CHANGED
|
@@ -358,8 +358,14 @@ export type BaseTextText = string | {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
export interface BaseToast {
|
|
361
|
+
action?: BaseToastAction
|
|
361
362
|
hasClose?: boolean
|
|
362
363
|
id: string
|
|
363
364
|
status?: BaseStatus
|
|
364
365
|
text: BaseTextText
|
|
365
366
|
}
|
|
367
|
+
|
|
368
|
+
export interface BaseToastAction {
|
|
369
|
+
label: BaseTextText
|
|
370
|
+
onClick: () => Promise<void> | void
|
|
371
|
+
}
|
package/app/types/global.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ declare global {
|
|
|
57
57
|
type BaseText = Bases.BaseText
|
|
58
58
|
type BaseTextText = Bases.BaseTextText
|
|
59
59
|
type BaseToast = Bases.BaseToast
|
|
60
|
+
type BaseToastAction = Bases.BaseToastAction
|
|
61
|
+
|
|
60
62
|
// Libraries
|
|
61
63
|
type ChartistBarChartData = import('chartist').BarChartData
|
|
62
64
|
type ChartistLineChartData = import('chartist').LineChartData
|