feat: migrate Message to shadcn-vue Alert
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fed19667f7
commit
5d840fcc16
12 changed files with 253 additions and 102 deletions
|
|
@ -10,3 +10,4 @@ export { Checkbox } from './ui/checkbox'
|
|||
export { CheckboxGroup } from './ui/checkbox-group'
|
||||
export { RadioGroup, RadioGroupItem } from './ui/radio-group'
|
||||
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardAction } from './ui/card'
|
||||
export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants } from './ui/alert'
|
||||
|
|
|
|||
32
packages/library/src/components/ui/alert/Alert.vue
Normal file
32
packages/library/src/components/ui/alert/Alert.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { XIcon } from '@lucide/vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { alertVariants } from '.'
|
||||
import AlertAction from './AlertAction.vue'
|
||||
import type { AlertVariants } from '.'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: AlertVariants['variant']
|
||||
closable?: boolean
|
||||
}>(), {
|
||||
variant: 'info',
|
||||
closable: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="alert" role="alert" :class="cn(alertVariants({ variant }), props.class)">
|
||||
<slot />
|
||||
<AlertAction v-if="closable">
|
||||
<Button variant="secondary" size="icon" class="size-6 bg-transparent hover:bg-black/10" @click="emit('close')">
|
||||
<XIcon class="size-3.5" />
|
||||
<span class="sr-only">Dismiss</span>
|
||||
</Button>
|
||||
</AlertAction>
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/alert/AlertAction.vue
Normal file
17
packages/library/src/components/ui/alert/AlertAction.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-action"
|
||||
:class="cn('absolute top-2 right-2', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
:class="cn('text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/alert/AlertTitle.vue
Normal file
17
packages/library/src/components/ui/alert/AlertTitle.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
:class="cn('font-medium group-has-[>svg]/alert:col-start-2 cn-font-heading [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
26
packages/library/src/components/ui/alert/index.ts
Normal file
26
packages/library/src/components/ui/alert/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Alert } from './Alert.vue'
|
||||
export { default as AlertAction } from './AlertAction.vue'
|
||||
export { default as AlertDescription } from './AlertDescription.vue'
|
||||
export { default as AlertTitle } from './AlertTitle.vue'
|
||||
|
||||
export const alertVariants = cva(
|
||||
'relative grid w-full gap-1 rounded-lg border px-3 py-2.5 text-sm has-data-[slot=alert-action]:pr-10',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
info: 'bg-info/15 border-info/30 text-foreground [&_[data-slot=alert-title]]:text-info',
|
||||
success: 'bg-success/15 border-success/30 text-foreground [&_[data-slot=alert-title]]:text-success',
|
||||
warning: 'bg-warning/15 border-warning/30 text-foreground [&_[data-slot=alert-title]]:text-warning',
|
||||
danger: 'bg-destructive/15 border-destructive/30 text-foreground [&_[data-slot=alert-title]]:text-destructive',
|
||||
secondary: 'bg-secondary/40 border-border text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'info',
|
||||
},
|
||||
},
|
||||
)
|
||||
export type AlertVariants = VariantProps<typeof alertVariants>
|
||||
|
|
@ -26,6 +26,9 @@ export {
|
|||
CardFooter,
|
||||
CardAction,
|
||||
Dialog,
|
||||
Message,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertAction,
|
||||
Badge,
|
||||
} from './lib'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { App } from 'vue'
|
|||
import {
|
||||
Select as PrimeSelect,
|
||||
Dialog as PrimeDialog,
|
||||
Message as PrimeMessage,
|
||||
} from 'primevue'
|
||||
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
|
||||
import {
|
||||
|
|
@ -25,6 +24,11 @@ import {
|
|||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertAction,
|
||||
alertVariants,
|
||||
} from '../components'
|
||||
|
||||
export {
|
||||
|
|
@ -46,6 +50,11 @@ export {
|
|||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertAction,
|
||||
alertVariants,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -110,14 +119,6 @@ const nychthemeron: ThemeLibrary = {
|
|||
'content',
|
||||
'footer',
|
||||
]),
|
||||
Message: ptThemeable(PrimeMessage, 'message', [
|
||||
'contentWrapper',
|
||||
'content',
|
||||
'icon',
|
||||
'text',
|
||||
'closeButton',
|
||||
'closeIcon',
|
||||
]),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +127,6 @@ const _engine = new ThemeEngine(nychthemeron)
|
|||
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
|
||||
export const Select: typeof PrimeSelect = _engine.getComponent<typeof PrimeSelect>('Select')
|
||||
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
|
||||
export const Message: typeof PrimeMessage = _engine.getComponent<typeof PrimeMessage>('Message')
|
||||
|
||||
export const createNychthemeron = (): {
|
||||
Button: typeof Button
|
||||
|
|
@ -147,7 +147,10 @@ export const createNychthemeron = (): {
|
|||
CardFooter: typeof CardFooter
|
||||
CardAction: typeof CardAction
|
||||
Dialog: typeof PrimeDialog
|
||||
Message: typeof PrimeMessage
|
||||
Alert: typeof Alert
|
||||
AlertTitle: typeof AlertTitle
|
||||
AlertDescription: typeof AlertDescription
|
||||
AlertAction: typeof AlertAction
|
||||
Badge: typeof Badge
|
||||
install: (app: App) => void
|
||||
} => ({
|
||||
|
|
@ -169,7 +172,10 @@ export const createNychthemeron = (): {
|
|||
CardFooter,
|
||||
CardAction,
|
||||
Dialog,
|
||||
Message,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertAction,
|
||||
Badge,
|
||||
install(app: App) {
|
||||
app.component('NychButton', Button)
|
||||
|
|
@ -190,7 +196,10 @@ export const createNychthemeron = (): {
|
|||
app.component('NychCardFooter', CardFooter)
|
||||
app.component('NychCardAction', CardAction)
|
||||
app.component('NychDialog', Dialog)
|
||||
app.component('NychMessage', Message)
|
||||
app.component('NychAlert', Alert)
|
||||
app.component('NychAlertTitle', AlertTitle)
|
||||
app.component('NychAlertDescription', AlertDescription)
|
||||
app.component('NychAlertAction', AlertAction)
|
||||
app.component('NychBadge', Badge)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
40
packages/library/tests/Alert.spec.ts
Normal file
40
packages/library/tests/Alert.spec.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { h } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { Alert, AlertTitle, AlertDescription } from '../src/lib'
|
||||
|
||||
describe('Alert', () => {
|
||||
it('defaults to the info variant', () => {
|
||||
const wrapper = mount(Alert)
|
||||
expect(wrapper.classes().join(' ')).toContain('bg-info/15')
|
||||
})
|
||||
|
||||
it('applies the danger variant class', () => {
|
||||
const wrapper = mount(Alert, { props: { variant: 'danger' } })
|
||||
expect(wrapper.classes().join(' ')).toContain('bg-destructive/15')
|
||||
})
|
||||
|
||||
it('renders title and description slots', () => {
|
||||
const wrapper = mount(Alert, {
|
||||
slots: {
|
||||
default: () => [
|
||||
h(AlertTitle, undefined, { default: () => 'The Oracle has spoken.' }),
|
||||
h(AlertDescription, undefined, { default: () => 'Heed the omen.' }),
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toContain('The Oracle has spoken.')
|
||||
expect(wrapper.text()).toContain('Heed the omen.')
|
||||
})
|
||||
|
||||
it('does not render a close button unless closable is set', () => {
|
||||
const wrapper = mount(Alert)
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('emits close when the close button is clicked', async () => {
|
||||
const wrapper = mount(Alert, { props: { closable: true } })
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -5,7 +5,6 @@ import {
|
|||
LoadingIcon,
|
||||
Select,
|
||||
Dialog,
|
||||
Message,
|
||||
} from '../src/index'
|
||||
|
||||
//eslint-disable-next-line
|
||||
|
|
@ -64,7 +63,6 @@ describe('themed pt components', () => {
|
|||
const cases = [
|
||||
{ name: 'Select', component: Select, base: 'nych-select' },
|
||||
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' },
|
||||
{ name: 'Message', component: Message, base: 'nych-message' },
|
||||
]
|
||||
|
||||
it.each(cases)('$name is a valid Vue component', ({ component }) => {
|
||||
|
|
|
|||
77
packages/playground/stories/Alert.stories.ts
Normal file
77
packages/playground/stories/Alert.stories.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { Alert as NychAlert, AlertTitle as NychAlertTitle, AlertDescription as NychAlertDescription } from '@nychthemeron/library'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Alert',
|
||||
component: NychAlert,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: { type: 'select' },
|
||||
options: ['info', 'success', 'warning', 'danger', 'secondary'],
|
||||
},
|
||||
closable: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof NychAlert>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Info: Story = {
|
||||
args: { variant: 'info' },
|
||||
render: (args) => ({
|
||||
components: { NychAlert, NychAlertTitle },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<NychAlert v-bind="args" style="width: 24rem;">
|
||||
<NychAlertTitle>The Oracle has spoken.</NychAlertTitle>
|
||||
</NychAlert>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Closable: Story = {
|
||||
args: { variant: 'info', closable: true },
|
||||
render: (args) => ({
|
||||
components: { NychAlert, NychAlertTitle },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<NychAlert v-bind="args" style="width: 24rem;">
|
||||
<NychAlertTitle>This omen may be dismissed.</NychAlertTitle>
|
||||
</NychAlert>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithDescription: Story = {
|
||||
render: () => ({
|
||||
components: { NychAlert, NychAlertTitle, NychAlertDescription },
|
||||
template: `
|
||||
<NychAlert variant="warning" style="width: 24rem;">
|
||||
<NychAlertTitle>The ferryman grows impatient.</NychAlertTitle>
|
||||
<NychAlertDescription>One obol is required to cross the Styx.</NychAlertDescription>
|
||||
</NychAlert>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { NychAlert, NychAlertTitle },
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem; width: 24rem;">
|
||||
<NychAlert variant="info"><NychAlertTitle>The Oracle has spoken.</NychAlertTitle></NychAlert>
|
||||
<NychAlert variant="success"><NychAlertTitle>The offering was accepted.</NychAlertTitle></NychAlert>
|
||||
<NychAlert variant="warning"><NychAlertTitle>The ferryman grows impatient.</NychAlertTitle></NychAlert>
|
||||
<NychAlert variant="danger"><NychAlertTitle>The gods have refused your plea.</NychAlertTitle></NychAlert>
|
||||
<NychAlert variant="secondary"><NychAlertTitle>A neutral notice from the keeper.</NychAlertTitle></NychAlert>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { Message as NychMessage } from '@nychthemeron/library'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Message',
|
||||
component: NychMessage,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
severity: {
|
||||
control: { type: 'select' },
|
||||
options: ['info', 'success', 'warn', 'error', 'secondary'],
|
||||
description: 'Message severity',
|
||||
},
|
||||
closable: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Show a close button',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof NychMessage>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Info: Story = {
|
||||
args: { severity: 'info' },
|
||||
render: (args) => ({
|
||||
components: { NychMessage },
|
||||
setup: () => ({ args }),
|
||||
template: `<NychMessage v-bind="args" style="width: 24rem;">The Oracle has spoken.</NychMessage>`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Success: Story = {
|
||||
args: { severity: 'success' },
|
||||
render: (args) => ({
|
||||
components: { NychMessage },
|
||||
setup: () => ({ args }),
|
||||
template: `<NychMessage v-bind="args" style="width: 24rem;">The offering was accepted.</NychMessage>`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Warn: Story = {
|
||||
args: { severity: 'warn' },
|
||||
render: (args) => ({
|
||||
components: { NychMessage },
|
||||
setup: () => ({ args }),
|
||||
template: `<NychMessage v-bind="args" style="width: 24rem;">The ferryman grows impatient.</NychMessage>`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: { severity: 'error' },
|
||||
render: (args) => ({
|
||||
components: { NychMessage },
|
||||
setup: () => ({ args }),
|
||||
template: `<NychMessage v-bind="args" style="width: 24rem;">The gods have refused your plea.</NychMessage>`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Closable: Story = {
|
||||
args: { severity: 'info', closable: true },
|
||||
render: (args) => ({
|
||||
components: { NychMessage },
|
||||
setup: () => ({ args }),
|
||||
template: `<NychMessage v-bind="args" style="width: 24rem;">This omen may be dismissed.</NychMessage>`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllSeverities: Story = {
|
||||
render: () => ({
|
||||
components: { NychMessage },
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem; width: 24rem;">
|
||||
<NychMessage severity="info">The Oracle has spoken.</NychMessage>
|
||||
<NychMessage severity="success">The offering was accepted.</NychMessage>
|
||||
<NychMessage severity="warn">The ferryman grows impatient.</NychMessage>
|
||||
<NychMessage severity="error">The gods have refused your plea.</NychMessage>
|
||||
<NychMessage severity="secondary">A neutral notice from the keeper.</NychMessage>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
Loading…
Reference in a new issue