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:
parent
7a4ae9ff6a
commit
5238d61f24
10 changed files with 127 additions and 69 deletions
|
|
@ -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'
|
||||||
|
|
|
||||||
19
packages/library/src/components/ui/badge/Badge.vue
Normal file
19
packages/library/src/components/ui/badge/Badge.vue
Normal 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>
|
||||||
24
packages/library/src/components/ui/badge/index.ts
Normal file
24
packages/library/src/components/ui/badge/index.ts
Normal 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>
|
||||||
|
|
@ -21,5 +21,5 @@ export {
|
||||||
Card,
|
Card,
|
||||||
Dialog,
|
Dialog,
|
||||||
Message,
|
Message,
|
||||||
Tag,
|
Badge,
|
||||||
} from './lib'
|
} from './lib'
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
19
packages/library/tests/Badge.spec.ts
Normal file
19
packages/library/tests/Badge.spec.ts
Normal 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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
46
packages/playground/stories/Badge.stories.ts
Normal file
46
packages/playground/stories/Badge.stories.ts
Normal 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>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue