mirror of
https://github.com/HoshinoSuzumi/rayine-ui.git
synced 2025-04-07 12:48:50 +08:00
✨ feat(message): new component messages
and message
This commit is contained in:
parent
f95e068bbe
commit
440613047a
10
docs/app.vue
10
docs/app.vue
@ -5,11 +5,11 @@ provide('navigation', navigation)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RayMessages>
|
<NuxtLayout>
|
||||||
<NuxtLayout>
|
<NuxtPage />
|
||||||
<NuxtPage />
|
</NuxtLayout>
|
||||||
</NuxtLayout>
|
|
||||||
</RayMessages>
|
<RayMessages />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
<div
|
<div
|
||||||
class="py-20 flex flex-col justify-center items-center rounded-xl border border-neutral-200 dark:border-neutral-800 pattern">
|
class="py-20 flex flex-col justify-center items-center rounded-xl border border-neutral-200 dark:border-neutral-800 pattern"
|
||||||
|
>
|
||||||
<div class="text-xl text-neutral-600 dark:text-neutral-300 font-bold">
|
<div class="text-xl text-neutral-600 dark:text-neutral-300 font-bold">
|
||||||
<h2 class="text-xl">
|
<h2 class="text-xl">
|
||||||
RayineUI <span class="font-medium">is a multi-purpose</span>
|
RayineUI <span class="font-medium">is a multi-purpose</span>
|
||||||
@ -26,9 +25,6 @@ const message = useMessage()
|
|||||||
<RayButton variant="outline" to="/components">
|
<RayButton variant="outline" to="/components">
|
||||||
Explore Components
|
Explore Components
|
||||||
</RayButton>
|
</RayButton>
|
||||||
<RayButton variant="outline" @click="message.success('rayui')">
|
|
||||||
Message
|
|
||||||
</RayButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,78 +1,81 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { inject, ref, onMounted } from 'vue'
|
import { ref, onMounted, defineComponent, type PropType, toRef, computed } from 'vue'
|
||||||
import type { MessageProviderApi, Message } from '../../types/message'
|
import { twJoin, twMerge } from 'tailwind-merge'
|
||||||
|
import type { Message, MessageType } from '../../types/message'
|
||||||
|
import { message } from '../../ui.config'
|
||||||
|
import type { DeepPartial, Strategy } from '../../types'
|
||||||
|
import { useMessage, useRayUI } from '#build/imports'
|
||||||
|
|
||||||
const providerApi = inject<MessageProviderApi>('ray-message-provider')
|
const config = message
|
||||||
|
|
||||||
const props = defineProps({
|
export default defineComponent({
|
||||||
message: {
|
props: {
|
||||||
require: true,
|
message: {
|
||||||
type: Object,
|
type: Object as PropType<Message>,
|
||||||
|
require: true,
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
setup(props) {
|
||||||
|
const { ui, attrs } = useRayUI('message', toRef(props, 'ui'), config)
|
||||||
|
|
||||||
const message = ref<Message>(props.message as Message)
|
const resolvedColor = computed(() => {
|
||||||
|
if (!props.message?.type) return props.message?.color || ui.value.default.color || 'primary'
|
||||||
|
return ({
|
||||||
|
info: 'blue',
|
||||||
|
success: 'emerald',
|
||||||
|
warning: 'orange',
|
||||||
|
error: 'rose',
|
||||||
|
} as Record<MessageType, string>)[props.message.type]
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
const containerClass = computed(() => {
|
||||||
setTimeout(() => {
|
return twMerge(twJoin(
|
||||||
providerApi?.destroy(message.value.id)
|
ui.value.container,
|
||||||
}, message.value?.duration || 3000)
|
ui.value.rounded,
|
||||||
|
ui.value.background.replaceAll('{color}', resolvedColor.value),
|
||||||
|
ui.value.content.replaceAll('{color}', resolvedColor.value),
|
||||||
|
ui.value.border.replaceAll('{color}', resolvedColor.value),
|
||||||
|
), props.class)
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const messageBody = ref<Message>(props.message as Message)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
message.remove(messageBody.value.id)
|
||||||
|
}, messageBody.value?.duration || ui.value.default.duration)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
ui,
|
||||||
|
attrs,
|
||||||
|
messageBody,
|
||||||
|
containerClass,
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="ui.wrapper" v-bind="attrs">
|
||||||
class="message"
|
<div :class="containerClass">
|
||||||
:class="{
|
<IconCircleSuccess v-if="messageBody.type === 'success'" class="text-xl" />
|
||||||
[message.type]: message.type,
|
<IconCircleWarning v-if="messageBody.type === 'warning'" class="text-xl" />
|
||||||
}"
|
<IconCircleError v-if="messageBody.type === 'error'" class="text-xl" />
|
||||||
>
|
<IconCircleInfo v-if="messageBody.type === 'info'" class="text-xl" />
|
||||||
<IconCircleSuccess
|
<span>
|
||||||
v-if="message.type === 'success'"
|
{{ messageBody.content }}
|
||||||
class="text-xl"
|
</span>
|
||||||
/>
|
</div>
|
||||||
<IconCircleWarning
|
|
||||||
v-if="message.type === 'warning'"
|
|
||||||
class="text-xl"
|
|
||||||
/>
|
|
||||||
<IconCircleError
|
|
||||||
v-if="message.type === 'error'"
|
|
||||||
class="text-xl"
|
|
||||||
/>
|
|
||||||
<IconCircleInfo
|
|
||||||
v-if="message.type === 'info'"
|
|
||||||
class="text-xl"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{{ message.content }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.message {
|
|
||||||
min-width: 80px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .2);
|
|
||||||
@apply h-fit px-2 py-1.5 border bg-white border-gray-300 rounded-md text-gray-500 text-xs font-sans flex items-center gap-1.5 first-of-type:mt-2.5 mt-2.5 font-bold pointer-events-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.info {
|
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, .2);
|
|
||||||
@apply !text-blue-500 !border-blue-400 !bg-blue-50 dark:!text-blue-300 dark:!border-blue-600 dark:!bg-blue-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.success {
|
|
||||||
box-shadow: 0 4px 12px rgba(16, 185, 129, .2);
|
|
||||||
@apply !text-emerald-500 !border-emerald-400 !bg-emerald-50 dark:!text-emerald-300 dark:!border-emerald-600 dark:!bg-emerald-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.warning {
|
|
||||||
box-shadow: 0 4px 12px rgba(249, 115, 22, .2);
|
|
||||||
@apply !text-orange-500 !border-orange-400 !bg-orange-50 dark:!text-orange-300 dark:!border-orange-600 dark:!bg-orange-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.error {
|
|
||||||
box-shadow: 0 4px 12px rgba(244, 63, 94, .2);
|
|
||||||
@apply !text-rose-500 !border-rose-400 !bg-rose-50 dark:!text-rose-300 dark:!border-rose-600 dark:!bg-rose-900;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref, toRef, type PropType } from 'vue'
|
import { computed, defineComponent, ref, toRef, type PropType } from 'vue'
|
||||||
|
import { twJoin, twMerge } from 'tailwind-merge'
|
||||||
import type { Message, MessageType } from '../../types/message'
|
import type { Message, MessageType } from '../../types/message'
|
||||||
import { useNuxtApp } from '#app'
|
import { messages } from '../../ui.config'
|
||||||
import { messages } from '../../ui.config';
|
import type { DeepPartial, Strategy } from '../../types'
|
||||||
import { twJoin, twMerge } from 'tailwind-merge';
|
import { useState } from '#imports'
|
||||||
import { useRayUI } from '#build/imports';
|
import { useRayUI } from '#build/imports'
|
||||||
import type { DeepPartial, Strategy } from '../../types';
|
|
||||||
|
|
||||||
const config = messages
|
const config = messages
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
max: {
|
|
||||||
type: Number,
|
|
||||||
default: 5,
|
|
||||||
},
|
|
||||||
class: {
|
class: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@ -22,113 +18,39 @@ export default defineComponent({
|
|||||||
ui: {
|
ui: {
|
||||||
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
|
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { ui, attrs } = useRayUI('messages', toRef(props, 'ui'), config)
|
const { ui, attrs } = useRayUI('messages', toRef(props, 'ui'), config)
|
||||||
|
|
||||||
const nuxtApp = useNuxtApp()
|
const messages = useState<Message[]>('messages', () => [])
|
||||||
const messageList = ref<Message[]>([])
|
|
||||||
|
|
||||||
const wrapperClass = computed(() => {
|
const wrapperClass = computed(() => {
|
||||||
return twMerge(twJoin(
|
return twMerge(twJoin(
|
||||||
ui.value.wrapper,
|
ui.value.wrapper,
|
||||||
ui.value.position
|
ui.value.position,
|
||||||
), props.class)
|
), props.class)
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMessage = (content: string, type: MessageType, duration: number = 3000) => {
|
|
||||||
const { max } = props
|
|
||||||
messageList.value.push({
|
|
||||||
id: (Date.now() + Math.random() * 100).toString(32).toUpperCase(),
|
|
||||||
content,
|
|
||||||
type,
|
|
||||||
duration,
|
|
||||||
})
|
|
||||||
if (messageList.value.length > max) {
|
|
||||||
messageList.value.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerApi = {
|
|
||||||
destroy: (id: string) => {
|
|
||||||
if (!messageList.value.find(message => message.id === id)) return
|
|
||||||
messageList.value.splice(messageList.value.findIndex(message => message.id === id), 1)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
info: (content: string, duration: number = 3000) => {
|
|
||||||
createMessage(content, 'info', duration)
|
|
||||||
},
|
|
||||||
success: (content: string, duration: number = 3000) => {
|
|
||||||
createMessage(content, 'success', duration)
|
|
||||||
},
|
|
||||||
warning: (content: string, duration: number = 3000) => {
|
|
||||||
createMessage(content, 'warning', duration)
|
|
||||||
},
|
|
||||||
error: (content: string, duration: number = 3000) => {
|
|
||||||
createMessage(content, 'error', duration)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
nuxtApp.vueApp.provide('ray-message-provider', providerApi)
|
|
||||||
nuxtApp.vueApp.provide('ray-message', api)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
attrs,
|
attrs,
|
||||||
messageList,
|
messages,
|
||||||
wrapperClass
|
wrapperClass,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<slot></slot>
|
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass" v-bind="attrs">
|
||||||
<div :class="ui.container">
|
<div :class="ui.container">
|
||||||
<TransitionGroup name="message">
|
<TransitionGroup v-bind="ui.transition" appear>
|
||||||
<RayMessage
|
<RayMessage v-for="message of messages" :key="message.id" :message="message" />
|
||||||
v-for="(message) in messageList"
|
|
||||||
:key="message.id"
|
|
||||||
:message="message"
|
|
||||||
/>
|
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</teleport>
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#message-provider .message-wrapper {
|
|
||||||
@apply z-[50000] fixed inset-0 flex flex-col items-center pointer-events-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-move,
|
|
||||||
.message-leave-active {
|
|
||||||
transition: all .8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-enter-active {
|
|
||||||
transition: all .8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-enter-from {
|
|
||||||
filter: blur(2px);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-leave-to {
|
|
||||||
filter: blur(6px);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-leave-active {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -1,10 +1,46 @@
|
|||||||
import { inject } from 'vue'
|
import type { Message, MessageType } from '../types/message'
|
||||||
import type { MessageApi } from '../types/message'
|
import { useState } from '#imports'
|
||||||
|
|
||||||
export const useMessage = () => {
|
export const useMessage = () => {
|
||||||
const message = inject<MessageApi>('ray-message')
|
const messages = useState<Message[]>('messages', () => [])
|
||||||
if (!message) {
|
|
||||||
throw new Error('No outer message-provider found!')
|
const add = (message: Partial<Message>) => {
|
||||||
|
const msg = {
|
||||||
|
id: (Date.now() + Math.random() * 100).toString(32).toUpperCase(),
|
||||||
|
duration: 3000,
|
||||||
|
...message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.value.some(m => m.id === msg.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.value.push(msg as Message)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = (id: string, message: Partial<Message>) => {
|
||||||
|
const msg = messages.value.find(msg => msg.id === id)
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(msg, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = (id: string) => {
|
||||||
|
messages.value = messages.value.filter(msg => msg.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
clear,
|
||||||
}
|
}
|
||||||
return message
|
|
||||||
}
|
}
|
||||||
|
5
src/runtime/types/message.d.ts
vendored
5
src/runtime/types/message.d.ts
vendored
@ -1,9 +1,14 @@
|
|||||||
|
import type { message } from '../ui.config'
|
||||||
|
import type colors from '#ray-colors'
|
||||||
|
|
||||||
export type MessageType = 'success' | 'warning' | 'error' | 'info'
|
export type MessageType = 'success' | 'warning' | 'error' | 'info'
|
||||||
|
export type MessageColor = (typeof colors)[number]
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
id: string
|
id: string
|
||||||
content: string
|
content: string
|
||||||
type: MessageType
|
type: MessageType
|
||||||
|
color?: MessageColor
|
||||||
duration?: number
|
duration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,4 +3,4 @@ export { default as button } from './elements/button'
|
|||||||
|
|
||||||
// overlays
|
// overlays
|
||||||
export { default as message } from './overlays/message'
|
export { default as message } from './overlays/message'
|
||||||
export { default as messages } from "./overlays/messages";
|
export { default as messages } from './overlays/messages'
|
||||||
|
@ -1 +1,12 @@
|
|||||||
export default {}
|
export default {
|
||||||
|
wrapper: 'mx-auto w-fit pointer-events-auto',
|
||||||
|
container: 'px-2 py-1.5 border flex items-center gap-1.5',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
border: 'border-{color}-400 dark:border-{color}-600',
|
||||||
|
background: 'bg-{color}-50 dark:bg-{color}-900',
|
||||||
|
content: 'text-xs font-sans font-bold text-{color}-500 dark:text-{color}-300',
|
||||||
|
default: {
|
||||||
|
color: 'primary',
|
||||||
|
duration: 4000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
wrapper: "fixed flex flex-col w-full pointer-events-none z-[500]",
|
wrapper: 'fixed flex flex-col z-[500] pointer-events-none',
|
||||||
position: "bottom-0 end-0",
|
position: 'start-0 end-0',
|
||||||
container: "px-4 sm:px-6 py-6 space-y-3 overflow-y-auto",
|
container: 'pt-2.5 flex flex-col items-center gap-2',
|
||||||
};
|
transition: {
|
||||||
|
moveClass: 'transform ease-out duration-300 transition',
|
||||||
|
enterActiveClass: 'transform ease-out duration-300 transition',
|
||||||
|
leaveActiveClass: 'transform ease-out duration-300 transition absolute',
|
||||||
|
enterFromClass: '-translate-y-2 opacity-0',
|
||||||
|
enterToClass: 'translate-y-0 opacity-100',
|
||||||
|
leaveFromClass: 'translate-y-0 opacity-100',
|
||||||
|
leaveToClass: '-translate-y-2 opacity-0',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -131,6 +131,29 @@ const safelistForComponent: Record<
|
|||||||
variants: ['focus-visible'],
|
variants: ['focus-visible'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
message: colorsToRegex => [
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-50$`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-900$`),
|
||||||
|
variants: ['dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^text-(${colorsToRegex})-500$`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^text-(${colorsToRegex})-300$`),
|
||||||
|
variants: ['dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^border-(${colorsToRegex})-400$`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^border-(${colorsToRegex})-600$`),
|
||||||
|
variants: ['dark'],
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateSafelist = (colors: string[], globalColors: string[]) => {
|
export const generateSafelist = (colors: string[], globalColors: string[]) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user