feat(message): new component messages and message

This commit is contained in:
Timothy Yin 2024-11-22 16:53:46 +08:00
parent f95e068bbe
commit 440613047a
10 changed files with 188 additions and 183 deletions

View File

@ -5,11 +5,11 @@ provide('navigation', navigation)
</script>
<template>
<RayMessages>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</RayMessages>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<RayMessages />
</template>
<style>

View File

@ -1,12 +1,11 @@
<script lang="ts" setup>
const router = useRouter()
const message = useMessage()
</script>
<template>
<div class="flex flex-col gap-8">
<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">
<h2 class="text-xl">
RayineUI <span class="font-medium">is a multi-purpose</span>
@ -26,9 +25,6 @@ const message = useMessage()
<RayButton variant="outline" to="/components">
Explore Components
</RayButton>
<RayButton variant="outline" @click="message.success('rayui')">
Message
</RayButton>
</div>
</div>
</template>

View File

@ -1,78 +1,81 @@
<script lang="ts" setup>
import { inject, ref, onMounted } from 'vue'
import type { MessageProviderApi, Message } from '../../types/message'
<script lang="ts">
import { ref, onMounted, defineComponent, type PropType, toRef, computed } from 'vue'
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({
message: {
require: true,
type: Object,
export default defineComponent({
props: {
message: {
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(() => {
setTimeout(() => {
providerApi?.destroy(message.value.id)
}, message.value?.duration || 3000)
const containerClass = computed(() => {
return twMerge(twJoin(
ui.value.container,
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>
<template>
<div
class="message"
:class="{
[message.type]: message.type,
}"
>
<IconCircleSuccess
v-if="message.type === 'success'"
class="text-xl"
/>
<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 :class="ui.wrapper" v-bind="attrs">
<div :class="containerClass">
<IconCircleSuccess v-if="messageBody.type === 'success'" class="text-xl" />
<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" />
<span>
{{ messageBody.content }}
</span>
</div>
</div>
</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>

View File

@ -1,20 +1,16 @@
<script lang="ts">
import { computed, defineComponent, ref, toRef, type PropType } from 'vue'
import { twJoin, twMerge } from 'tailwind-merge'
import type { Message, MessageType } from '../../types/message'
import { useNuxtApp } from '#app'
import { messages } from '../../ui.config';
import { twJoin, twMerge } from 'tailwind-merge';
import { useRayUI } from '#build/imports';
import type { DeepPartial, Strategy } from '../../types';
import { messages } from '../../ui.config'
import type { DeepPartial, Strategy } from '../../types'
import { useState } from '#imports'
import { useRayUI } from '#build/imports'
const config = messages
export default defineComponent({
props: {
max: {
type: Number,
default: 5,
},
class: {
type: String,
default: '',
@ -22,113 +18,39 @@ export default defineComponent({
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
}
},
},
setup(props) {
const { ui, attrs } = useRayUI('messages', toRef(props, 'ui'), config)
const nuxtApp = useNuxtApp()
const messageList = ref<Message[]>([])
const messages = useState<Message[]>('messages', () => [])
const wrapperClass = computed(() => {
return twMerge(twJoin(
ui.value.wrapper,
ui.value.position
ui.value.position,
), 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 {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
messageList,
wrapperClass
messages,
wrapperClass,
}
}
},
})
</script>
<template>
<slot></slot>
<teleport to="body">
<div :class="wrapperClass">
<div :class="wrapperClass" v-bind="attrs">
<div :class="ui.container">
<TransitionGroup name="message">
<RayMessage
v-for="(message) in messageList"
:key="message.id"
:message="message"
/>
<TransitionGroup v-bind="ui.transition" appear>
<RayMessage v-for="message of messages" :key="message.id" :message="message" />
</TransitionGroup>
</div>
</div>
</teleport>
</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>

View File

@ -1,10 +1,46 @@
import { inject } from 'vue'
import type { MessageApi } from '../types/message'
import type { Message, MessageType } from '../types/message'
import { useState } from '#imports'
export const useMessage = () => {
const message = inject<MessageApi>('ray-message')
if (!message) {
throw new Error('No outer message-provider found!')
const messages = useState<Message[]>('messages', () => [])
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
}

View File

@ -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 MessageColor = (typeof colors)[number]
export interface Message {
id: string
content: string
type: MessageType
color?: MessageColor
duration?: number
}

View File

@ -3,4 +3,4 @@ export { default as button } from './elements/button'
// overlays
export { default as message } from './overlays/message'
export { default as messages } from "./overlays/messages";
export { default as messages } from './overlays/messages'

View File

@ -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,
},
}

View File

@ -1,5 +1,14 @@
export default {
wrapper: "fixed flex flex-col w-full pointer-events-none z-[500]",
position: "bottom-0 end-0",
container: "px-4 sm:px-6 py-6 space-y-3 overflow-y-auto",
};
wrapper: 'fixed flex flex-col z-[500] pointer-events-none',
position: 'start-0 end-0',
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',
},
}

View File

@ -131,6 +131,29 @@ const safelistForComponent: Record<
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[]) => {