feat: migrate Card to shadcn-vue compound API
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d999cd95f6
commit
fed19667f7
14 changed files with 246 additions and 39 deletions
|
|
@ -9,3 +9,4 @@ export { Badge, badgeVariants } from './ui/badge'
|
|||
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'
|
||||
|
|
|
|||
21
packages/library/src/components/ui/card/Card.vue
Normal file
21
packages/library/src/components/ui/card/Card.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
size?: "default" | "sm"
|
||||
}>(), {
|
||||
size: "default",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card"
|
||||
:data-size="size"
|
||||
:class="cn('ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/card/CardAction.vue
Normal file
17
packages/library/src/components/ui/card/CardAction.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="card-action"
|
||||
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/card/CardContent.vue
Normal file
17
packages/library/src/components/ui/card/CardContent.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="card-content"
|
||||
:class="cn('px-4 group-data-[size=sm]/card:px-3', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/card/CardDescription.vue
Normal file
17
packages/library/src/components/ui/card/CardDescription.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="card-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/card/CardFooter.vue
Normal file
17
packages/library/src/components/ui/card/CardFooter.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="card-footer"
|
||||
:class="cn('bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/card/CardHeader.vue
Normal file
17
packages/library/src/components/ui/card/CardHeader.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="card-header"
|
||||
:class="cn('gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
packages/library/src/components/ui/card/CardTitle.vue
Normal file
17
packages/library/src/components/ui/card/CardTitle.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="card-title"
|
||||
:class="cn('text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
7
packages/library/src/components/ui/card/index.ts
Normal file
7
packages/library/src/components/ui/card/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { default as Card } from "./Card.vue"
|
||||
export { default as CardAction } from "./CardAction.vue"
|
||||
export { default as CardContent } from "./CardContent.vue"
|
||||
export { default as CardDescription } from "./CardDescription.vue"
|
||||
export { default as CardFooter } from "./CardFooter.vue"
|
||||
export { default as CardHeader } from "./CardHeader.vue"
|
||||
export { default as CardTitle } from "./CardTitle.vue"
|
||||
|
|
@ -19,6 +19,12 @@ export {
|
|||
Switch,
|
||||
Select,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
Dialog,
|
||||
Message,
|
||||
Badge,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { App } from 'vue'
|
||||
import {
|
||||
Select as PrimeSelect,
|
||||
Card as PrimeCard,
|
||||
Dialog as PrimeDialog,
|
||||
Message as PrimeMessage,
|
||||
} from 'primevue'
|
||||
|
|
@ -19,6 +18,13 @@ import {
|
|||
CheckboxGroup,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
} from '../components'
|
||||
|
||||
export {
|
||||
|
|
@ -33,6 +39,13 @@ export {
|
|||
CheckboxGroup,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -89,15 +102,6 @@ const nychthemeron: ThemeLibrary = {
|
|||
'optionLabel',
|
||||
'emptyMessage',
|
||||
]),
|
||||
Card: ptThemeable(PrimeCard, 'card', [
|
||||
'header',
|
||||
'body',
|
||||
'caption',
|
||||
'title',
|
||||
'subtitle',
|
||||
'content',
|
||||
'footer',
|
||||
]),
|
||||
Dialog: ptThemeable(PrimeDialog, 'dialog', [
|
||||
'mask',
|
||||
'header',
|
||||
|
|
@ -121,7 +125,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 Card: typeof PrimeCard = _engine.getComponent<typeof PrimeCard>('Card')
|
||||
export const Dialog: typeof PrimeDialog = _engine.getComponent<typeof PrimeDialog>('Dialog')
|
||||
export const Message: typeof PrimeMessage = _engine.getComponent<typeof PrimeMessage>('Message')
|
||||
|
||||
|
|
@ -136,7 +139,13 @@ export const createNychthemeron = (): {
|
|||
RadioGroupItem: typeof RadioGroupItem
|
||||
Switch: typeof Switch
|
||||
Select: typeof PrimeSelect
|
||||
Card: typeof PrimeCard
|
||||
Card: typeof Card
|
||||
CardHeader: typeof CardHeader
|
||||
CardTitle: typeof CardTitle
|
||||
CardDescription: typeof CardDescription
|
||||
CardContent: typeof CardContent
|
||||
CardFooter: typeof CardFooter
|
||||
CardAction: typeof CardAction
|
||||
Dialog: typeof PrimeDialog
|
||||
Message: typeof PrimeMessage
|
||||
Badge: typeof Badge
|
||||
|
|
@ -153,6 +162,12 @@ export const createNychthemeron = (): {
|
|||
Switch,
|
||||
Select,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardAction,
|
||||
Dialog,
|
||||
Message,
|
||||
Badge,
|
||||
|
|
@ -168,6 +183,12 @@ export const createNychthemeron = (): {
|
|||
app.component('NychSwitch', Switch)
|
||||
app.component('NychSelect', Select)
|
||||
app.component('NychCard', Card)
|
||||
app.component('NychCardHeader', CardHeader)
|
||||
app.component('NychCardTitle', CardTitle)
|
||||
app.component('NychCardDescription', CardDescription)
|
||||
app.component('NychCardContent', CardContent)
|
||||
app.component('NychCardFooter', CardFooter)
|
||||
app.component('NychCardAction', CardAction)
|
||||
app.component('NychDialog', Dialog)
|
||||
app.component('NychMessage', Message)
|
||||
app.component('NychBadge', Badge)
|
||||
|
|
|
|||
32
packages/library/tests/Card.spec.ts
Normal file
32
packages/library/tests/Card.spec.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { h } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../src/lib'
|
||||
|
||||
describe('Card', () => {
|
||||
it('renders header, title, description, content, and footer together', () => {
|
||||
const wrapper = mount(Card, {
|
||||
slots: {
|
||||
default: () => [
|
||||
h(CardHeader, undefined, {
|
||||
default: () => [
|
||||
h(CardTitle, undefined, { default: () => 'Elysium' }),
|
||||
h(CardDescription, undefined, { default: () => 'The blessed fields' }),
|
||||
],
|
||||
}),
|
||||
h(CardContent, undefined, { default: () => 'A resting place for the virtuous.' }),
|
||||
h(CardFooter, undefined, { default: () => 'Enter' }),
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(wrapper.text()).toContain('Elysium')
|
||||
expect(wrapper.text()).toContain('The blessed fields')
|
||||
expect(wrapper.text()).toContain('A resting place for the virtuous.')
|
||||
expect(wrapper.text()).toContain('Enter')
|
||||
})
|
||||
|
||||
it('exposes a data-slot attribute for styling hooks', () => {
|
||||
const wrapper = mount(Card)
|
||||
expect(wrapper.attributes('data-slot')).toBe('card')
|
||||
})
|
||||
})
|
||||
|
|
@ -4,7 +4,6 @@ import {
|
|||
createNychthemeron,
|
||||
LoadingIcon,
|
||||
Select,
|
||||
Card,
|
||||
Dialog,
|
||||
Message,
|
||||
} from '../src/index'
|
||||
|
|
@ -64,7 +63,6 @@ describe('LoadingIcon', () => {
|
|||
describe('themed pt components', () => {
|
||||
const cases = [
|
||||
{ name: 'Select', component: Select, base: 'nych-select' },
|
||||
{ name: 'Card', component: Card, base: 'nych-card' },
|
||||
{ name: 'Dialog', component: Dialog, base: 'nych-dialog' },
|
||||
{ name: 'Message', component: Message, base: 'nych-message' },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { Card as NychCard, Button as NychButton, Badge as NychBadge } from '@nychthemeron/library'
|
||||
import {
|
||||
Card as NychCard,
|
||||
CardHeader as NychCardHeader,
|
||||
CardTitle as NychCardTitle,
|
||||
CardDescription as NychCardDescription,
|
||||
CardContent as NychCardContent,
|
||||
CardFooter as NychCardFooter,
|
||||
Button as NychButton,
|
||||
Badge as NychBadge,
|
||||
} from '@nychthemeron/library'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Card',
|
||||
|
|
@ -16,15 +25,17 @@ type Story = StoryObj<typeof meta>
|
|||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { NychCard },
|
||||
components: { NychCard, NychCardHeader, NychCardTitle, NychCardDescription, NychCardContent },
|
||||
template: `
|
||||
<NychCard style="width: 22rem;">
|
||||
<template #title>Elysium</template>
|
||||
<template #subtitle>The blessed fields</template>
|
||||
<template #content>
|
||||
<NychCardHeader>
|
||||
<NychCardTitle>Elysium</NychCardTitle>
|
||||
<NychCardDescription>The blessed fields</NychCardDescription>
|
||||
</NychCardHeader>
|
||||
<NychCardContent>
|
||||
A resting place reserved for the heroic and the virtuous, where the
|
||||
air is mild and the laurel never wilts.
|
||||
</template>
|
||||
</NychCardContent>
|
||||
</NychCard>
|
||||
`,
|
||||
}),
|
||||
|
|
@ -32,19 +43,29 @@ export const Default: Story = {
|
|||
|
||||
export const WithFooter: Story = {
|
||||
render: () => ({
|
||||
components: { NychCard, NychButton },
|
||||
components: {
|
||||
NychCard,
|
||||
NychCardHeader,
|
||||
NychCardTitle,
|
||||
NychCardDescription,
|
||||
NychCardContent,
|
||||
NychCardFooter,
|
||||
NychButton,
|
||||
},
|
||||
template: `
|
||||
<NychCard style="width: 22rem;">
|
||||
<template #title>Passage of the Styx</template>
|
||||
<template #subtitle>One obol required</template>
|
||||
<template #content>
|
||||
<NychCardHeader>
|
||||
<NychCardTitle>Passage of the Styx</NychCardTitle>
|
||||
<NychCardDescription>One obol required</NychCardDescription>
|
||||
</NychCardHeader>
|
||||
<NychCardContent>
|
||||
Charon ferries the souls of the dead across the river that divides
|
||||
the world of the living from the world of the dead.
|
||||
</template>
|
||||
<template #footer>
|
||||
<NychButton label="Pay the toll" />
|
||||
<NychButton label="Turn back" severity="secondary" />
|
||||
</template>
|
||||
</NychCardContent>
|
||||
<NychCardFooter>
|
||||
<NychButton>Pay the toll</NychButton>
|
||||
<NychButton variant="secondary">Turn back</NychButton>
|
||||
</NychCardFooter>
|
||||
</NychCard>
|
||||
`,
|
||||
}),
|
||||
|
|
@ -52,21 +73,19 @@ export const WithFooter: Story = {
|
|||
|
||||
export const WithHeaderAndTag: Story = {
|
||||
render: () => ({
|
||||
components: { NychCard, NychBadge },
|
||||
components: { NychCard, NychCardHeader, NychCardTitle, NychCardContent, NychBadge },
|
||||
template: `
|
||||
<NychCard style="width: 22rem;">
|
||||
<template #header>
|
||||
<div style="height: 120px; background: linear-gradient(135deg, var(--surface-2), var(--surface-3));"></div>
|
||||
</template>
|
||||
<template #title>
|
||||
<span style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<div style="height: 120px; background: linear-gradient(135deg, var(--surface-2), var(--surface-3));"></div>
|
||||
<NychCardHeader>
|
||||
<NychCardTitle style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
Oracle of Delphi <NychBadge variant="info">Sacred</NychBadge>
|
||||
</span>
|
||||
</template>
|
||||
<template #content>
|
||||
</NychCardTitle>
|
||||
</NychCardHeader>
|
||||
<NychCardContent>
|
||||
Seekers travelled from across the Aegean to hear the Pythia speak the
|
||||
will of Apollo in riddling verse.
|
||||
</template>
|
||||
</NychCardContent>
|
||||
</NychCard>
|
||||
`,
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue