From 5d840fcc16e9f9096f21f3f6b5b5bcf7130ce8ad Mon Sep 17 00:00:00 2001 From: Matthew L McPeak Date: Sat, 11 Jul 2026 14:45:45 -0400 Subject: [PATCH] feat: migrate Message to shadcn-vue Alert Co-Authored-By: Claude Sonnet 5 --- packages/library/src/components/index.ts | 1 + .../library/src/components/ui/alert/Alert.vue | 32 +++++++ .../src/components/ui/alert/AlertAction.vue | 17 ++++ .../components/ui/alert/AlertDescription.vue | 17 ++++ .../src/components/ui/alert/AlertTitle.vue | 17 ++++ .../library/src/components/ui/alert/index.ts | 26 ++++++ packages/library/src/index.ts | 5 +- packages/library/src/lib/index.ts | 35 +++++--- packages/library/tests/Alert.spec.ts | 40 +++++++++ packages/library/tests/lib.spec.ts | 2 - packages/playground/stories/Alert.stories.ts | 77 +++++++++++++++++ .../playground/stories/Message.stories.ts | 86 ------------------- 12 files changed, 253 insertions(+), 102 deletions(-) create mode 100644 packages/library/src/components/ui/alert/Alert.vue create mode 100644 packages/library/src/components/ui/alert/AlertAction.vue create mode 100644 packages/library/src/components/ui/alert/AlertDescription.vue create mode 100644 packages/library/src/components/ui/alert/AlertTitle.vue create mode 100644 packages/library/src/components/ui/alert/index.ts create mode 100644 packages/library/tests/Alert.spec.ts create mode 100644 packages/playground/stories/Alert.stories.ts delete mode 100644 packages/playground/stories/Message.stories.ts diff --git a/packages/library/src/components/index.ts b/packages/library/src/components/index.ts index fb22734..ee48c97 100644 --- a/packages/library/src/components/index.ts +++ b/packages/library/src/components/index.ts @@ -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' diff --git a/packages/library/src/components/ui/alert/Alert.vue b/packages/library/src/components/ui/alert/Alert.vue new file mode 100644 index 0000000..3c7d08d --- /dev/null +++ b/packages/library/src/components/ui/alert/Alert.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/library/src/components/ui/alert/AlertAction.vue b/packages/library/src/components/ui/alert/AlertAction.vue new file mode 100644 index 0000000..c709e80 --- /dev/null +++ b/packages/library/src/components/ui/alert/AlertAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/library/src/components/ui/alert/AlertDescription.vue b/packages/library/src/components/ui/alert/AlertDescription.vue new file mode 100644 index 0000000..987502b --- /dev/null +++ b/packages/library/src/components/ui/alert/AlertDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/library/src/components/ui/alert/AlertTitle.vue b/packages/library/src/components/ui/alert/AlertTitle.vue new file mode 100644 index 0000000..1f6e612 --- /dev/null +++ b/packages/library/src/components/ui/alert/AlertTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/library/src/components/ui/alert/index.ts b/packages/library/src/components/ui/alert/index.ts new file mode 100644 index 0000000..88c8a32 --- /dev/null +++ b/packages/library/src/components/ui/alert/index.ts @@ -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 diff --git a/packages/library/src/index.ts b/packages/library/src/index.ts index ad818d1..d0e62cd 100644 --- a/packages/library/src/index.ts +++ b/packages/library/src/index.ts @@ -26,6 +26,9 @@ export { CardFooter, CardAction, Dialog, - Message, + Alert, + AlertTitle, + AlertDescription, + AlertAction, Badge, } from './lib' diff --git a/packages/library/src/lib/index.ts b/packages/library/src/lib/index.ts index 78bb977..d44a8d3 100644 --- a/packages/library/src/lib/index.ts +++ b/packages/library/src/lib/index.ts @@ -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('LoadingIcon') export const Select: typeof PrimeSelect = _engine.getComponent('Select') export const Dialog: typeof PrimeDialog = _engine.getComponent('Dialog') -export const Message: typeof PrimeMessage = _engine.getComponent('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) }, }) diff --git a/packages/library/tests/Alert.spec.ts b/packages/library/tests/Alert.spec.ts new file mode 100644 index 0000000..6a9b0c0 --- /dev/null +++ b/packages/library/tests/Alert.spec.ts @@ -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) + }) +}) diff --git a/packages/library/tests/lib.spec.ts b/packages/library/tests/lib.spec.ts index 6080c56..c6aa745 100644 --- a/packages/library/tests/lib.spec.ts +++ b/packages/library/tests/lib.spec.ts @@ -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 }) => { diff --git a/packages/playground/stories/Alert.stories.ts b/packages/playground/stories/Alert.stories.ts new file mode 100644 index 0000000..9f876be --- /dev/null +++ b/packages/playground/stories/Alert.stories.ts @@ -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 + +export default meta +type Story = StoryObj + +export const Info: Story = { + args: { variant: 'info' }, + render: (args) => ({ + components: { NychAlert, NychAlertTitle }, + setup: () => ({ args }), + template: ` + + The Oracle has spoken. + + `, + }), +} + +export const Closable: Story = { + args: { variant: 'info', closable: true }, + render: (args) => ({ + components: { NychAlert, NychAlertTitle }, + setup: () => ({ args }), + template: ` + + This omen may be dismissed. + + `, + }), +} + +export const WithDescription: Story = { + render: () => ({ + components: { NychAlert, NychAlertTitle, NychAlertDescription }, + template: ` + + The ferryman grows impatient. + One obol is required to cross the Styx. + + `, + }), +} + +export const AllVariants: Story = { + render: () => ({ + components: { NychAlert, NychAlertTitle }, + template: ` +
+ The Oracle has spoken. + The offering was accepted. + The ferryman grows impatient. + The gods have refused your plea. + A neutral notice from the keeper. +
+ `, + }), +} diff --git a/packages/playground/stories/Message.stories.ts b/packages/playground/stories/Message.stories.ts deleted file mode 100644 index 0e3a745..0000000 --- a/packages/playground/stories/Message.stories.ts +++ /dev/null @@ -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 - -export default meta -type Story = StoryObj - -export const Info: Story = { - args: { severity: 'info' }, - render: (args) => ({ - components: { NychMessage }, - setup: () => ({ args }), - template: `The Oracle has spoken.`, - }), -} - -export const Success: Story = { - args: { severity: 'success' }, - render: (args) => ({ - components: { NychMessage }, - setup: () => ({ args }), - template: `The offering was accepted.`, - }), -} - -export const Warn: Story = { - args: { severity: 'warn' }, - render: (args) => ({ - components: { NychMessage }, - setup: () => ({ args }), - template: `The ferryman grows impatient.`, - }), -} - -export const Error: Story = { - args: { severity: 'error' }, - render: (args) => ({ - components: { NychMessage }, - setup: () => ({ args }), - template: `The gods have refused your plea.`, - }), -} - -export const Closable: Story = { - args: { severity: 'info', closable: true }, - render: (args) => ({ - components: { NychMessage }, - setup: () => ({ args }), - template: `This omen may be dismissed.`, - }), -} - -export const AllSeverities: Story = { - render: () => ({ - components: { NychMessage }, - template: ` -
- The Oracle has spoken. - The offering was accepted. - The ferryman grows impatient. - The gods have refused your plea. - A neutral notice from the keeper. -
- `, - }), -}