@saasmakers/ui 1.4.35 → 1.4.36
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/BaseShortcut.stories.ts +24 -0
- package/app/components/bases/BaseShortcut.vue +63 -0
- package/app/components/bases/BaseToast.stories.ts +28 -0
- package/app/components/bases/BaseToast.vue +12 -5
- package/app/components/layout/LayoutToasts.vue +7 -2
- package/app/composables/useLayerIcons.ts +1 -0
- package/app/types/bases.d.ts +7 -0
- package/app/types/global.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import BaseShortcut from './BaseShortcut.vue'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
component: BaseShortcut,
|
|
5
|
+
title: 'Bases/BaseShortcut',
|
|
6
|
+
|
|
7
|
+
argTypes: {
|
|
8
|
+
active: { control: 'boolean' },
|
|
9
|
+
shortcut: { control: 'text' },
|
|
10
|
+
},
|
|
11
|
+
} satisfies Meta<typeof BaseShortcut>
|
|
12
|
+
|
|
13
|
+
export const Enter: StoryObj<typeof BaseShortcut> = { args: { shortcut: 'Enter' } satisfies Partial<BaseShortcut> }
|
|
14
|
+
|
|
15
|
+
export const Letter: StoryObj<typeof BaseShortcut> = { args: { shortcut: 'y' } satisfies Partial<BaseShortcut> }
|
|
16
|
+
|
|
17
|
+
export const Escape: StoryObj<typeof BaseShortcut> = { args: { shortcut: 'Escape' } satisfies Partial<BaseShortcut> }
|
|
18
|
+
|
|
19
|
+
export const Inactive: StoryObj<typeof BaseShortcut> = {
|
|
20
|
+
args: {
|
|
21
|
+
active: false,
|
|
22
|
+
shortcut: 'Enter',
|
|
23
|
+
} satisfies Partial<BaseShortcut>,
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { onKeyStroke } from '@vueuse/core'
|
|
3
|
+
import type { BaseShortcut } from '../../types/bases'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<BaseShortcut>(), {
|
|
6
|
+
active: true,
|
|
7
|
+
shortcut: 'Enter',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits<{
|
|
11
|
+
trigger: [event: KeyboardEvent]
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const { getIcon } = useLayerIcons()
|
|
15
|
+
|
|
16
|
+
function isTypingTarget(target: EventTarget | null) {
|
|
17
|
+
const element = target as HTMLElement | null
|
|
18
|
+
const typingTags = ['INPUT', 'SELECT', 'TEXTAREA']
|
|
19
|
+
|
|
20
|
+
if (!element) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (element.isContentEditable) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return typingTags.includes(element.tagName)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onKeyStroke(
|
|
32
|
+
event => event.key.toLowerCase() === props.shortcut.toLowerCase(),
|
|
33
|
+
(event) => {
|
|
34
|
+
if (!props.active) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isTypingTarget(event.target)) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
event.preventDefault()
|
|
43
|
+
emit('trigger', event)
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<kbd
|
|
50
|
+
class="h-3.5 min-w-3.5 inline-flex items-center justify-center border border-current/50 rounded px-0.5 text-2xs leading-none"
|
|
51
|
+
:class="{ 'font-mono uppercase': shortcut.toLowerCase() !== 'enter' }"
|
|
52
|
+
>
|
|
53
|
+
<Icon
|
|
54
|
+
v-if="shortcut.toLowerCase() === 'enter'"
|
|
55
|
+
class="size-2"
|
|
56
|
+
:name="getIcon('enter')"
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<template v-else>
|
|
60
|
+
{{ shortcut === 'Escape' ? 'esc' : shortcut }}
|
|
61
|
+
</template>
|
|
62
|
+
</kbd>
|
|
63
|
+
</template>
|
|
@@ -40,3 +40,31 @@ export const WithAction: StoryObj<typeof BaseToast> = {
|
|
|
40
40
|
text: 'Toast message',
|
|
41
41
|
} satisfies Partial<BaseToast>,
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
export const WithActionShortcutEnter: StoryObj<typeof BaseToast> = {
|
|
45
|
+
args: {
|
|
46
|
+
action: {
|
|
47
|
+
label: 'Yes',
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
onClick: () => console.log('action clicked'),
|
|
50
|
+
shortcut: 'Enter',
|
|
51
|
+
},
|
|
52
|
+
id: 'toast-3',
|
|
53
|
+
status: 'success',
|
|
54
|
+
text: 'Confirm this action?',
|
|
55
|
+
} satisfies Partial<BaseToast>,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const WithActionShortcutY: StoryObj<typeof BaseToast> = {
|
|
59
|
+
args: {
|
|
60
|
+
action: {
|
|
61
|
+
label: 'Yes',
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
onClick: () => console.log('action clicked'),
|
|
64
|
+
shortcut: 'y',
|
|
65
|
+
},
|
|
66
|
+
id: 'toast-4',
|
|
67
|
+
status: 'info',
|
|
68
|
+
text: 'Confirm this action?',
|
|
69
|
+
} satisfies Partial<BaseToast>,
|
|
70
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { BaseToast } from '../../types/bases'
|
|
3
3
|
|
|
4
4
|
const props = withDefaults(defineProps<BaseToast>(), {
|
|
5
|
+
hasActiveShortcut: true,
|
|
5
6
|
hasClose: true,
|
|
6
7
|
id: '',
|
|
7
8
|
status: 'info',
|
|
@@ -9,21 +10,21 @@ const props = withDefaults(defineProps<BaseToast>(), {
|
|
|
9
10
|
})
|
|
10
11
|
|
|
11
12
|
const emit = defineEmits<{
|
|
12
|
-
action: [event: MouseEvent, toast: BaseToast]
|
|
13
|
-
close: [event: MouseEvent, toast: BaseToast]
|
|
13
|
+
action: [event: KeyboardEvent | MouseEvent, toast: BaseToast]
|
|
14
|
+
close: [event: KeyboardEvent | MouseEvent, toast: BaseToast]
|
|
14
15
|
}>()
|
|
15
16
|
|
|
16
17
|
const { getIcon } = useLayerIcons()
|
|
17
18
|
|
|
18
19
|
function onAction(event: KeyboardEvent | MouseEvent) {
|
|
19
20
|
if (props.action) {
|
|
20
|
-
emit('action', event
|
|
21
|
+
emit('action', event, props)
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function onClose(event: KeyboardEvent | MouseEvent) {
|
|
25
26
|
if (props.hasClose) {
|
|
26
|
-
emit('close', event
|
|
27
|
+
emit('close', event, props)
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
</script>
|
|
@@ -46,7 +47,7 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
46
47
|
|
|
47
48
|
<button
|
|
48
49
|
v-if="action"
|
|
49
|
-
class="ml-1.5 min-h-6 inline-flex items-center justify-center rounded-md px-2 py-0.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-800"
|
|
50
|
+
class="ml-1.5 min-h-6 inline-flex items-center justify-center gap-1.25 rounded-md px-2 py-0.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-800"
|
|
50
51
|
:class="{
|
|
51
52
|
'bg-red-100 text-red-700 hover:bg-red-200 focus-visible:ring-red-500 dark:bg-red-400/15 dark:text-red-400 dark:hover:bg-red-400/25': status === 'error',
|
|
52
53
|
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 focus-visible:ring-indigo-500 dark:bg-indigo-400/15 dark:text-indigo-400 dark:hover:bg-indigo-400/25': status === 'info',
|
|
@@ -64,6 +65,12 @@ function onClose(event: KeyboardEvent | MouseEvent) {
|
|
|
64
65
|
:text="action.label"
|
|
65
66
|
uppercase
|
|
66
67
|
/>
|
|
68
|
+
|
|
69
|
+
<BaseShortcut
|
|
70
|
+
:active="hasActiveShortcut"
|
|
71
|
+
:shortcut="action.shortcut ?? 'Enter'"
|
|
72
|
+
@trigger="onAction"
|
|
73
|
+
/>
|
|
67
74
|
</button>
|
|
68
75
|
|
|
69
76
|
<template v-if="hasClose">
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
const { fadeInUp } = useMotion()
|
|
3
3
|
const toasts = useToasts()
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const activeShortcutToastId = computed(() => {
|
|
6
|
+
return [...toasts.toasts.value].reverse().find(item => item.action)?.id
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
async function onToastAction(_event: KeyboardEvent | MouseEvent, toast: BaseToast) {
|
|
6
10
|
toasts.closeToast(toast.id)
|
|
7
11
|
|
|
8
12
|
await toast.action?.onClick()
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
function onToastClose(_event: MouseEvent, toast: BaseToast) {
|
|
15
|
+
function onToastClose(_event: KeyboardEvent | MouseEvent, toast: BaseToast) {
|
|
12
16
|
toasts.closeToast(toast.id)
|
|
13
17
|
}
|
|
14
18
|
</script>
|
|
@@ -27,6 +31,7 @@ function onToastClose(_event: MouseEvent, toast: BaseToast) {
|
|
|
27
31
|
:id="toast.id"
|
|
28
32
|
:key="toast.id"
|
|
29
33
|
:action="toast.action"
|
|
34
|
+
:has-active-shortcut="toast.id === activeShortcutToastId"
|
|
30
35
|
:status="toast.status"
|
|
31
36
|
:text="toast.text"
|
|
32
37
|
@action="onToastAction"
|
|
@@ -21,6 +21,7 @@ const icons = {
|
|
|
21
21
|
closeCircle: 'hugeicons:cancel-circle',
|
|
22
22
|
default: 'hugeicons:help-circle',
|
|
23
23
|
drag: 'mdi:drag-horizontal-variant',
|
|
24
|
+
enter: 'mdi:keyboard-return',
|
|
24
25
|
exclamationCircle: 'hugeicons:alert-circle',
|
|
25
26
|
infoCircle: 'hugeicons:information-circle',
|
|
26
27
|
plus: 'hugeicons:add-01',
|
package/app/types/bases.d.ts
CHANGED
|
@@ -275,6 +275,11 @@ export type BaseQuoteBackground = 'gray' | 'gray-light' | 'white'
|
|
|
275
275
|
|
|
276
276
|
export type BaseQuoteSize = 'base' | 'sm' | 'xs'
|
|
277
277
|
|
|
278
|
+
export interface BaseShortcut {
|
|
279
|
+
active?: boolean
|
|
280
|
+
shortcut?: string
|
|
281
|
+
}
|
|
282
|
+
|
|
278
283
|
export type BaseSize
|
|
279
284
|
= | '2xl'
|
|
280
285
|
| '2xs'
|
|
@@ -365,6 +370,7 @@ export type BaseTextText = string | {
|
|
|
365
370
|
|
|
366
371
|
export interface BaseToast {
|
|
367
372
|
action?: BaseToastAction
|
|
373
|
+
hasActiveShortcut?: boolean
|
|
368
374
|
hasClose?: boolean
|
|
369
375
|
id: string
|
|
370
376
|
status?: BaseStatus
|
|
@@ -374,4 +380,5 @@ export interface BaseToast {
|
|
|
374
380
|
export interface BaseToastAction {
|
|
375
381
|
label: BaseTextText
|
|
376
382
|
onClick: () => Promise<void> | void
|
|
383
|
+
shortcut?: string
|
|
377
384
|
}
|
package/app/types/global.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ declare global {
|
|
|
45
45
|
type BaseQuote = import('./bases').BaseQuote
|
|
46
46
|
type BaseQuoteBackground = import('./bases').BaseQuoteBackground
|
|
47
47
|
type BaseQuoteSize = import('./bases').BaseQuoteSize
|
|
48
|
+
type BaseShortcut = import('./bases').BaseShortcut
|
|
48
49
|
type BaseSize = import('./bases').BaseSize
|
|
49
50
|
type BaseSpinner = import('./bases').BaseSpinner
|
|
50
51
|
type BaseStatus = import('./bases').BaseStatus
|