feat: migrate Tag to shadcn-vue Badge

Also fixes Card.stories.ts, which imported the now-removed Tag export.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Matthew McPeak 2026-07-11 14:32:18 -04:00
parent 7a4ae9ff6a
commit 5238d61f24
10 changed files with 127 additions and 69 deletions

View file

@ -5,3 +5,4 @@ export type { ButtonVariants } from './ui/button'
export { Input } from './ui/input' export { Input } from './ui/input'
export { Textarea } from './ui/textarea' export { Textarea } from './ui/textarea'
export { Switch } from './ui/switch' export { Switch } from './ui/switch'
export { Badge, badgeVariants } from './ui/badge'

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { BadgeVariants } from '.'
import { cn } from '@/lib/utils'
import { badgeVariants } from '.'
const props = withDefaults(defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>(), {
variant: 'primary',
})
</script>
<template>
<span data-slot="badge" :class="cn(badgeVariants({ variant: props.variant }), props.class)">
<slot />
</span>
</template>

View file

@ -0,0 +1,24 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 text-xs font-semibold [&>svg]:size-3',
{
variants: {
variant: {
primary: 'bg-primary text-primary-foreground',
secondary: 'bg-secondary text-secondary-foreground',
info: 'bg-info text-info-foreground',
success: 'bg-success text-success-foreground',
warning: 'bg-warning text-warning-foreground',
danger: 'bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'primary',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View file

@ -21,5 +21,5 @@ export {
Card, Card,
Dialog, Dialog,
Message, Message,
Tag, Badge,
} from './lib' } from './lib'

View file

@ -8,12 +8,20 @@ import {
Card as PrimeCard, Card as PrimeCard,
Dialog as PrimeDialog, Dialog as PrimeDialog,
Message as PrimeMessage, Message as PrimeMessage,
Tag as PrimeTag,
} from 'primevue' } from 'primevue'
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine' import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
import { NychLoadingIcon, Button, buttonVariants, Input, Textarea, Switch } from '../components' import {
NychLoadingIcon,
Button,
buttonVariants,
Input,
Textarea,
Switch,
Badge,
badgeVariants,
} from '../components'
export { Button, buttonVariants, Input, Textarea, Switch } export { Button, buttonVariants, Input, Textarea, Switch, Badge, badgeVariants }
/** /**
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API. * Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
@ -98,7 +106,6 @@ const nychthemeron: ThemeLibrary = {
'closeButton', 'closeButton',
'closeIcon', 'closeIcon',
]), ]),
Tag: ptThemeable(PrimeTag, 'tag', ['label', 'icon']),
}, },
} }
@ -116,7 +123,6 @@ export const Select: typeof PrimeSelect = _engine.getComponent<typeof PrimeSelec
export const Card: typeof PrimeCard = _engine.getComponent<typeof PrimeCard>('Card') export const Card: typeof PrimeCard = _engine.getComponent<typeof PrimeCard>('Card')
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog') export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
export const Message: typeof PrimeMessage = _engine.getComponent<typeof PrimeMessage>('Message') export const Message: typeof PrimeMessage = _engine.getComponent<typeof PrimeMessage>('Message')
export const Tag: typeof PrimeTag = _engine.getComponent<typeof PrimeTag>('Tag')
export const createNychthemeron = (): { export const createNychthemeron = (): {
Button: typeof Button Button: typeof Button
@ -132,7 +138,7 @@ export const createNychthemeron = (): {
Card: typeof PrimeCard Card: typeof PrimeCard
Dialog: typeof PrimeDialog Dialog: typeof PrimeDialog
Message: typeof PrimeMessage Message: typeof PrimeMessage
Tag: typeof PrimeTag Badge: typeof Badge
install: (app: App) => void install: (app: App) => void
} => ({ } => ({
Button, Button,
@ -148,7 +154,7 @@ export const createNychthemeron = (): {
Card, Card,
Dialog, Dialog,
Message, Message,
Tag, Badge,
install(app: App) { install(app: App) {
app.component('NychButton', Button) app.component('NychButton', Button)
app.component('NychLoadingIcon', LoadingIcon) app.component('NychLoadingIcon', LoadingIcon)
@ -163,6 +169,6 @@ export const createNychthemeron = (): {
app.component('NychCard', Card) app.component('NychCard', Card)
app.component('NychDialog', Dialog) app.component('NychDialog', Dialog)
app.component('NychMessage', Message) app.component('NychMessage', Message)
app.component('NychTag', Tag) app.component('NychBadge', Badge)
}, },
}) })

View file

@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { Badge } from '../src/lib'
describe('Badge', () => {
it('defaults to the primary variant', () => {
const wrapper = mount(Badge, { slots: { default: 'Sacred' } })
expect(wrapper.classes().join(' ')).toContain('bg-primary')
})
it.each(['secondary', 'info', 'success', 'warning', 'danger'] as const)(
'applies the %s variant class',
(variant) => {
const wrapper = mount(Badge, { props: { variant } })
const expectedClass = variant === 'danger' ? 'bg-destructive' : `bg-${variant}`
expect(wrapper.classes().join(' ')).toContain(expectedClass)
},
)
})

View file

@ -11,7 +11,6 @@ import {
Card, Card,
Dialog, Dialog,
Message, Message,
Tag,
} from '../src/index' } from '../src/index'
//eslint-disable-next-line //eslint-disable-next-line
@ -76,7 +75,6 @@ describe('themed pt components', () => {
{ name: 'Card', component: Card, base: 'nych-card' }, { name: 'Card', component: Card, base: 'nych-card' },
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' }, { name: 'Dialog', component: Dialog, base: 'nych-dialog' },
{ name: 'Message', component: Message, base: 'nych-message' }, { name: 'Message', component: Message, base: 'nych-message' },
{ name: 'Tag', component: Tag, base: 'nych-tag' },
] ]
it.each(cases)('$name is a valid Vue component', ({ component }) => { it.each(cases)('$name is a valid Vue component', ({ component }) => {

View file

@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { Badge as NychBadge } from '@nychthemeron/library'
const meta = {
title: 'Components/Badge',
component: NychBadge,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'info', 'success', 'warning', 'danger'],
},
},
} satisfies Meta<typeof NychBadge>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { variant: 'primary' },
render: (args) => ({
components: { NychBadge },
setup: () => ({ args }),
template: `<NychBadge v-bind="args">Mortal</NychBadge>`,
}),
}
export const AllVariants: Story = {
render: () => ({
components: { NychBadge },
template: `
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<NychBadge variant="primary">Primary</NychBadge>
<NychBadge variant="secondary">Secondary</NychBadge>
<NychBadge variant="info">Info</NychBadge>
<NychBadge variant="success">Success</NychBadge>
<NychBadge variant="warning">Warning</NychBadge>
<NychBadge variant="danger">Danger</NychBadge>
</div>
`,
}),
}

View file

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite' import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { Card as NychCard, Button as NychButton, Tag as NychTag } from '@nychthemeron/library' import { Card as NychCard, Button as NychButton, Badge as NychBadge } from '@nychthemeron/library'
const meta = { const meta = {
title: 'Components/Card', title: 'Components/Card',
@ -52,7 +52,7 @@ export const WithFooter: Story = {
export const WithHeaderAndTag: Story = { export const WithHeaderAndTag: Story = {
render: () => ({ render: () => ({
components: { NychCard, NychTag }, components: { NychCard, NychBadge },
template: ` template: `
<NychCard style="width: 22rem;"> <NychCard style="width: 22rem;">
<template #header> <template #header>
@ -60,7 +60,7 @@ export const WithHeaderAndTag: Story = {
</template> </template>
<template #title> <template #title>
<span style="display: flex; align-items: center; gap: 0.5rem;"> <span style="display: flex; align-items: center; gap: 0.5rem;">
Oracle of Delphi <NychTag value="Sacred" severity="info" /> Oracle of Delphi <NychBadge variant="info">Sacred</NychBadge>
</span> </span>
</template> </template>
<template #content> <template #content>

View file

@ -1,55 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { Tag as NychTag } from '@nychthemeron/library'
const meta = {
title: 'Components/Tag',
component: NychTag,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
severity: {
control: { type: 'select' },
options: ['secondary', 'success', 'info', 'warn', 'danger', 'contrast'],
description: 'Tag color severity',
},
value: {
control: { type: 'text' },
description: 'Tag label',
},
rounded: {
control: { type: 'boolean' },
description: 'Pill-shaped tag',
},
},
} satisfies Meta<typeof NychTag>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { value: 'Mortal' },
}
export const Rounded: Story = {
args: { value: 'Immortal', severity: 'info', rounded: true },
}
export const Severities: Story = {
render: () => ({
components: { NychTag },
template: `
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
<NychTag value="Primary" />
<NychTag value="Secondary" severity="secondary" />
<NychTag value="Info" severity="info" />
<NychTag value="Success" severity="success" />
<NychTag value="Warn" severity="warn" />
<NychTag value="Danger" severity="danger" />
<NychTag value="Contrast" severity="contrast" />
</div>
`,
}),
}