@saasmakers/ui 1.3.8 → 1.4.0

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.
@@ -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,18 @@ 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
+ function onAction(event: KeyboardEvent | MouseEvent) {
19
+ if (props.action) {
20
+ emit('action', event as MouseEvent, props)
21
+ }
22
+ }
23
+
17
24
  function onClose(event: KeyboardEvent | MouseEvent) {
18
25
  if (props.hasClose) {
19
26
  emit('close', event as MouseEvent, props)
@@ -37,6 +44,30 @@ function onClose(event: KeyboardEvent | MouseEvent) {
37
44
  :text="text"
38
45
  />
39
46
 
47
+ <template v-if="action">
48
+ <div class="ml-2 mr-1.5 h-5 border-l border-gray-700 flex-initial dark:border-gray-300" />
49
+
50
+ <button
51
+ class="cursor-pointer bg-transparent flex-initial"
52
+ type="button"
53
+ @click.stop="onAction"
54
+ >
55
+ <BaseText
56
+ bold
57
+ class="pointer-events-none"
58
+ :class="{
59
+ 'text-red-400 dark:text-red-600': status === 'error',
60
+ 'text-indigo-400 dark:text-indigo-600': status === 'info',
61
+ 'text-orange-400 dark:text-orange-600': status === 'warning',
62
+ 'text-teal-400 dark:text-teal-600': status === 'success',
63
+ }"
64
+ no-wrap
65
+ :text="action.label"
66
+ uppercase
67
+ />
68
+ </button>
69
+ </template>
70
+
40
71
  <template v-if="hasClose">
41
72
  <div class="ml-2 mr-1.5 h-5 border-l border-gray-700 flex-initial dark:border-gray-300" />
42
73
 
@@ -50,6 +50,7 @@ export default {
50
50
  ] satisfies FieldSize[],
51
51
  },
52
52
  slugOnly: { control: 'boolean' },
53
+ trim: { control: 'boolean' },
53
54
  type: {
54
55
  control: 'select',
55
56
  options: [
@@ -88,3 +89,12 @@ export const SlugOnly: StoryObj<typeof FieldInput> = {
88
89
  slugOnly: true,
89
90
  } satisfies Partial<FieldInput>,
90
91
  }
92
+
93
+ export const Trim: StoryObj<typeof FieldInput> = {
94
+ args: {
95
+ label: 'First name',
96
+ modelValue: 'Julien ',
97
+ placeholder: 'Enter your first name',
98
+ trim: true,
99
+ } satisfies Partial<FieldInput>,
100
+ }
@@ -22,6 +22,7 @@ const props = withDefaults(defineProps<Omit<FieldInput, 'modelValue'>>(), {
22
22
  required: false,
23
23
  size: 'base',
24
24
  slugOnly: false,
25
+ trim: false,
25
26
  type: 'text',
26
27
  uppercase: false,
27
28
  validation: undefined,
@@ -67,6 +68,10 @@ function focus() {
67
68
  }
68
69
 
69
70
  function onFieldBlur(event: FocusEvent) {
71
+ if (props.trim) {
72
+ modelValue.value = modelValue.value.toString().trim()
73
+ }
74
+
70
75
  emit('blur', event)
71
76
  }
72
77
 
@@ -99,7 +104,11 @@ function onFieldKeyDown(event: KeyboardEvent) {
99
104
  }
100
105
 
101
106
  else if (event.key === 'Enter' && modelValue.value) {
102
- emit('submit', event, value)
107
+ if (props.trim) {
108
+ modelValue.value = modelValue.value.toString().trim()
109
+ }
110
+
111
+ emit('submit', event, modelValue.value.toString())
103
112
  }
104
113
 
105
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
- // Remove toast after 5 seconds
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
 
@@ -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
+ }
@@ -70,6 +70,7 @@ export interface FieldInput {
70
70
  required?: boolean
71
71
  size?: FieldSize
72
72
  slugOnly?: boolean
73
+ trim?: boolean
73
74
  type?: FieldInputType
74
75
  uppercase?: boolean
75
76
  validation?: VuelidateValidation
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
- "version": "1.3.8",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",