Skip to content

Commit

Permalink
feat(Alert): new component (#449)
Browse files Browse the repository at this point in the history
benjamincanac authored Jul 27, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 8298b62 commit ab2abae
Showing 14 changed files with 392 additions and 8 deletions.
11 changes: 11 additions & 0 deletions docs/components/content/examples/AlertExampleHtml.vue
Original file line number Diff line number Diff line change
@@ -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>
File renamed without changes.
188 changes: 188 additions & 0 deletions docs/content/2.elements/2.alert.md
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 1 addition & 3 deletions docs/content/6.overlays/6.notification.md
Original file line number Diff line number Diff line change
@@ -283,9 +283,7 @@ baseProps:
timeout: 0
props:
actions:
- variant: 'ghost'
color: 'gray'
label: Action 1
- label: Action 1
- variant: 'solid'
color: 'gray'
label: Action 2
18 changes: 18 additions & 0 deletions src/colors.ts
Original file line number Diff line number Diff line change
@@ -26,6 +26,24 @@ const kebabCase = (str: string) => {
}

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) => [{
pattern: new RegExp(`bg-(${colorsAsRegex})-400`),
variants: ['dark']
41 changes: 40 additions & 1 deletion src/runtime/app.config.ts
Original file line number Diff line number Diff line change
@@ -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 = {
base: 'inline-flex items-center justify-center text-gray-900 dark:text-white',
padding: 'px-1',
@@ -942,7 +980,7 @@ const notification = {
wrapper: 'w-full pointer-events-auto',
container: 'relative overflow-hidden',
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',
shadow: 'shadow-lg',
rounded: 'rounded-lg',
@@ -1003,6 +1041,7 @@ export default {
dropdown,
kbd,
accordion,
alert,
input,
formGroup,
textarea,
130 changes: 130 additions & 0 deletions src/runtime/components/elements/Alert.vue
Original file line number Diff line number Diff line change
@@ -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>
8 changes: 4 additions & 4 deletions src/runtime/components/overlays/Notification.vue
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
<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.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" />
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />

@@ -13,18 +13,18 @@
{{ title }}
</slot>
</p>
<p v-if="description" :class="ui.description">
<p v-if="(description || $slots.description)" :class="ui.description">
<slot name="description" :description="description">
{{ description }}
</slot>
</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)" />
</div>
</div>
<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)" />
</div>

1 comment on commit ab2abae

@vercel
Copy link

@vercel vercel bot commented on ab2abae Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./

ui-git-dev-nuxtlabs.vercel.app
ui.nuxtlabs.com
ui-nuxtlabs.vercel.app

Please sign in to comment.