mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-27 18:30:35 +01:00
feat(Alert): new component (#449)
This commit is contained in:
11
docs/components/content/examples/AlertExampleHtml.vue
Normal file
11
docs/components/content/examples/AlertExampleHtml.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<UAlert title="Heads <i>up</i>!" icon="i-heroicons-command-line">
|
||||||
|
<template #title="{ title }">
|
||||||
|
<span v-html="title" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #description>
|
||||||
|
You can add <b>components</b> to your app using the <u>cli</u>.
|
||||||
|
</template>
|
||||||
|
</UAlert>
|
||||||
|
</template>
|
||||||
188
docs/content/2.elements/2.alert.md
Normal file
188
docs/content/2.elements/2.alert.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
description: Display an alert element to draw attention.
|
||||||
|
links:
|
||||||
|
- label: GitHub
|
||||||
|
icon: i-simple-icons-github
|
||||||
|
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Alert.vue
|
||||||
|
navigation.badge: Edge
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Pass a `title` to your Alert.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
props:
|
||||||
|
title: 'Heads up!'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
You can add a `description` in addition of the `title`.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
props:
|
||||||
|
description: 'You can add components to your app using the cli.'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Icon
|
||||||
|
|
||||||
|
Use any icon from [Iconify](https://icones.js.org) by setting the `icon` prop by using this pattern: `i-{collection_name}-{icon_name}`.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
props:
|
||||||
|
icon: 'i-heroicons-command-line'
|
||||||
|
description: 'You can add components to your app using the cli.'
|
||||||
|
excludedProps:
|
||||||
|
- icon
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Avatar
|
||||||
|
|
||||||
|
Use the [avatar](/elements/avatar) prop as an `object` and configure it with any of its props.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
props:
|
||||||
|
description: 'You can add components to your app using the cli.'
|
||||||
|
avatar:
|
||||||
|
src: 'https://avatars.githubusercontent.com/u/739984?v=4'
|
||||||
|
excludedProps:
|
||||||
|
- avatar
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Style
|
||||||
|
|
||||||
|
Use the `color` and `variant` props to change the visual style of the Alert.
|
||||||
|
|
||||||
|
- `color` can be any color from the `ui.colors` object or `white` (default).
|
||||||
|
- `variant` can be `solid` (default), `outline`, `soft` or `subtle`.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
description: 'You can add components to your app using the cli.'
|
||||||
|
props:
|
||||||
|
icon: 'i-heroicons-command-line'
|
||||||
|
color: 'primary'
|
||||||
|
variant: 'solid'
|
||||||
|
extraColors:
|
||||||
|
- white
|
||||||
|
excludedProps:
|
||||||
|
- icon
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Close
|
||||||
|
|
||||||
|
Use the `close-button` prop to hide or customize the close button on the Alert.
|
||||||
|
|
||||||
|
You can pass all the props of the [Button](/elements/button) component to customize it through the `close-button` prop or globally through `ui.alert.default.closeButton`.
|
||||||
|
|
||||||
|
It defaults to `null` which means no close button will be displayed. A `close` event will be emitted when the close button is clicked.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
props:
|
||||||
|
closeButton:
|
||||||
|
icon: 'i-heroicons-x-mark-20-solid'
|
||||||
|
color: 'gray'
|
||||||
|
variant: 'link'
|
||||||
|
padded: false
|
||||||
|
excludedProps:
|
||||||
|
- closeButton
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
Use the `actions` prop to add actions to the Alert.
|
||||||
|
|
||||||
|
Like for `closeButton`, you can pass all the props of the [Button](/elements/button) component plus a `click` function in the action but also customize the default style for the actions globally through `ui.alert.default.actionButton`.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
props:
|
||||||
|
actions:
|
||||||
|
- label: Action 1
|
||||||
|
- variant: 'ghost'
|
||||||
|
color: 'gray'
|
||||||
|
label: Action 2
|
||||||
|
excludedProps:
|
||||||
|
- actions
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
Actions will render differently whether you have a `description` set.
|
||||||
|
|
||||||
|
::component-card
|
||||||
|
---
|
||||||
|
baseProps:
|
||||||
|
title: 'Heads up!'
|
||||||
|
description: 'You can add components to your app using the cli.'
|
||||||
|
props:
|
||||||
|
actions:
|
||||||
|
- variant: 'solid'
|
||||||
|
color: 'primary'
|
||||||
|
label: Action 1
|
||||||
|
- variant: 'outline'
|
||||||
|
color: 'primary'
|
||||||
|
label: Action 2
|
||||||
|
excludedProps:
|
||||||
|
- actions
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
### `title` / `description`
|
||||||
|
|
||||||
|
Use the `#title` and `#description` slots to customize the Alert.
|
||||||
|
|
||||||
|
This can be handy when you want to display HTML content. To achieve this, you can define those slots and use the `v-html` directive.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:alert-example-html
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<UAlert title="Heads <i>up</i>!" icon="i-heroicons-command-line">
|
||||||
|
<template #title="{ title }">
|
||||||
|
<span v-html="title" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #description>
|
||||||
|
You can add <b>components</b> to your app using the <u>cli</u>.
|
||||||
|
</template>
|
||||||
|
</UAlert>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
:component-props
|
||||||
|
|
||||||
|
## Preset
|
||||||
|
|
||||||
|
:component-preset
|
||||||
@@ -283,9 +283,7 @@ baseProps:
|
|||||||
timeout: 0
|
timeout: 0
|
||||||
props:
|
props:
|
||||||
actions:
|
actions:
|
||||||
- variant: 'ghost'
|
- label: Action 1
|
||||||
color: 'gray'
|
|
||||||
label: Action 1
|
|
||||||
- variant: 'solid'
|
- variant: 'solid'
|
||||||
color: 'gray'
|
color: 'gray'
|
||||||
label: Action 2
|
label: Action 2
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ const kebabCase = (str: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const safelistByComponent = {
|
const safelistByComponent = {
|
||||||
|
alert: (colorsAsRegex) => [{
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-50`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`bg-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`text-(${colorsAsRegex})-500`)
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-400`),
|
||||||
|
variants: ['dark']
|
||||||
|
}, {
|
||||||
|
pattern: new RegExp(`ring-(${colorsAsRegex})-500`)
|
||||||
|
}],
|
||||||
avatar: (colorsAsRegex) => [{
|
avatar: (colorsAsRegex) => [{
|
||||||
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
|
||||||
variants: ['dark']
|
variants: ['dark']
|
||||||
|
|||||||
@@ -291,6 +291,44 @@ const accordion = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alert = {
|
||||||
|
wrapper: 'w-full relative overflow-hidden',
|
||||||
|
title: 'text-sm font-medium',
|
||||||
|
description: 'mt-1 text-sm leading-4 opacity-90',
|
||||||
|
shadow: '',
|
||||||
|
rounded: 'rounded-lg',
|
||||||
|
padding: 'p-3',
|
||||||
|
icon: {
|
||||||
|
base: 'flex-shrink-0 w-5 h-5'
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
base: 'flex-shrink-0 self-center',
|
||||||
|
size: 'md'
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
white: {
|
||||||
|
solid: 'text-gray-900 dark:text-white bg-white dark:bg-gray-900 ring-1 ring-gray-200 dark:ring-gray-800'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
solid: 'bg-{color}-500 dark:bg-{color}-400 text-white dark:text-gray-900',
|
||||||
|
outline: 'text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400',
|
||||||
|
soft: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400',
|
||||||
|
subtle: 'bg-{color}-50 dark:bg-{color}-400 dark:bg-opacity-10 text-{color}-500 dark:text-{color}-400 ring-1 ring-inset ring-{color}-500 dark:ring-{color}-400 ring-opacity-25 dark:ring-opacity-25'
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
color: 'white',
|
||||||
|
variant: 'solid',
|
||||||
|
icon: null,
|
||||||
|
closeButton: null,
|
||||||
|
actionButton: {
|
||||||
|
size: 'xs',
|
||||||
|
color: 'primary',
|
||||||
|
variant: 'link'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const kbd = {
|
const kbd = {
|
||||||
base: 'inline-flex items-center justify-center text-gray-900 dark:text-white',
|
base: 'inline-flex items-center justify-center text-gray-900 dark:text-white',
|
||||||
padding: 'px-1',
|
padding: 'px-1',
|
||||||
@@ -942,7 +980,7 @@ const notification = {
|
|||||||
wrapper: 'w-full pointer-events-auto',
|
wrapper: 'w-full pointer-events-auto',
|
||||||
container: 'relative overflow-hidden',
|
container: 'relative overflow-hidden',
|
||||||
title: 'text-sm font-medium text-gray-900 dark:text-white',
|
title: 'text-sm font-medium text-gray-900 dark:text-white',
|
||||||
description: 'mt-1 text-sm leading-5 text-gray-500 dark:text-gray-400',
|
description: 'mt-1 text-sm leading-4 text-gray-500 dark:text-gray-400',
|
||||||
background: 'bg-white dark:bg-gray-900',
|
background: 'bg-white dark:bg-gray-900',
|
||||||
shadow: 'shadow-lg',
|
shadow: 'shadow-lg',
|
||||||
rounded: 'rounded-lg',
|
rounded: 'rounded-lg',
|
||||||
@@ -1003,6 +1041,7 @@ export default {
|
|||||||
dropdown,
|
dropdown,
|
||||||
kbd,
|
kbd,
|
||||||
accordion,
|
accordion,
|
||||||
|
alert,
|
||||||
input,
|
input,
|
||||||
formGroup,
|
formGroup,
|
||||||
textarea,
|
textarea,
|
||||||
|
|||||||
130
src/runtime/components/elements/Alert.vue
Normal file
130
src/runtime/components/elements/Alert.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="alertClass">
|
||||||
|
<div class="flex gap-3" :class="{ 'items-start': (description || $slots.description), 'items-center': !description && !$slots.description }">
|
||||||
|
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
|
||||||
|
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||||
|
|
||||||
|
<div class="w-0 flex-1">
|
||||||
|
<p :class="ui.title">
|
||||||
|
<slot name="title" :title="title">
|
||||||
|
{{ title }}
|
||||||
|
</slot>
|
||||||
|
</p>
|
||||||
|
<p v-if="description || $slots.description" :class="ui.description">
|
||||||
|
<slot name="description" :description="description">
|
||||||
|
{{ description }}
|
||||||
|
</slot>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="(description || $slots.description) && actions.length" class="mt-3 flex items-center gap-2">
|
||||||
|
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 flex items-center gap-3">
|
||||||
|
<div v-if="!description && !$slots.description && actions.length" class="flex items-center gap-2">
|
||||||
|
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="action.click" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton v-if="closeButton" v-bind="{ ...ui.default.closeButton, ...closeButton }" @click.stop="$emit('close')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
import UIcon from '../elements/Icon.vue'
|
||||||
|
import UAvatar from '../elements/Avatar.vue'
|
||||||
|
import UButton from '../elements/Button.vue'
|
||||||
|
import type { Avatar} from '../../types/avatar'
|
||||||
|
import type { Button } from '../../types/button'
|
||||||
|
import { classNames } from '../../utils'
|
||||||
|
import { useAppConfig } from '#imports'
|
||||||
|
// TODO: Remove
|
||||||
|
// @ts-expect-error
|
||||||
|
import appConfig from '#build/app.config'
|
||||||
|
|
||||||
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
UIcon,
|
||||||
|
UAvatar,
|
||||||
|
UButton
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.alert.default.icon
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: Object as PropType<Avatar>,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
type: Object as PropType<Button>,
|
||||||
|
default: () => appConfig.ui.alert.default.closeButton
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: Array as PropType<Button & { click: Function }[]>,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.alert.default.color,
|
||||||
|
validator (value: string) {
|
||||||
|
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.alert.color)].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: () => appConfig.ui.alert.default.variant,
|
||||||
|
validator (value: string) {
|
||||||
|
return [
|
||||||
|
...Object.keys(appConfig.ui.alert.variant),
|
||||||
|
...Object.values(appConfig.ui.alert.color).flatMap(value => Object.keys(value))
|
||||||
|
].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<Partial<typeof appConfig.ui.alert>>,
|
||||||
|
default: () => appConfig.ui.alert
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
setup (props) {
|
||||||
|
// TODO: Remove
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
const ui = computed<Partial<typeof appConfig.ui.alert>>(() => defu({}, props.ui, appConfig.ui.alert))
|
||||||
|
|
||||||
|
const alertClass = computed(() => {
|
||||||
|
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||||
|
|
||||||
|
return classNames(
|
||||||
|
ui.value.wrapper,
|
||||||
|
ui.value.rounded,
|
||||||
|
ui.value.shadow,
|
||||||
|
ui.value.padding,
|
||||||
|
variant?.replaceAll('{color}', props.color)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
ui,
|
||||||
|
alertClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
|
<div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
|
||||||
<div :class="[ui.container, ui.rounded, ui.ring]">
|
<div :class="[ui.container, ui.rounded, ui.ring]">
|
||||||
<div :class="ui.padding">
|
<div :class="ui.padding">
|
||||||
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
|
<div class="flex gap-3" :class="{ 'items-start': description || $slots.description, 'items-center': !description && !$slots.description }">
|
||||||
<UIcon v-if="icon" :name="icon" :class="iconClass" />
|
<UIcon v-if="icon" :name="icon" :class="iconClass" />
|
||||||
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
|
||||||
|
|
||||||
@@ -13,18 +13,18 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</slot>
|
</slot>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="description" :class="ui.description">
|
<p v-if="(description || $slots.description)" :class="ui.description">
|
||||||
<slot name="description" :description="description">
|
<slot name="description" :description="description">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</slot>
|
</slot>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
|
<div v-if="(description || $slots.description) && actions.length" class="mt-3 flex items-center gap-2">
|
||||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0 flex items-center gap-3">
|
<div class="flex-shrink-0 flex items-center gap-3">
|
||||||
<div v-if="!description && actions.length" class="flex items-center gap-2">
|
<div v-if="!description && !$slots.description && actions.length" class="flex items-center gap-2">
|
||||||
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
<UButton v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.actionButton, ...action }" @click.stop="onAction(action)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user