Components
Card
A bordered content container with Header, Title, Description, Action, Content, and Footer parts. Purely presentational.
Installation
npx kosmesis add cardpnpm dlx kosmesis add cardyarn dlx kosmesis add cardbunx kosmesis add cardCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface CardSlotProps { class?: string; id?: string; children?: Children;}/** Purely presentational — no Morphos equivalent, same as upstream shadcn/ui. */@Component()export class Card extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card" class={cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", cls)} > {children} </div> ); }}@Component()export class CardHeader extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-header" class={cn( "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-[[data-slot=card-action]]:grid-cols-[1fr_auto]", cls, )} > {children} </div> ); }}@Component()export class CardTitle extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-title" class={cn("font-semibold leading-none", cls)}> {children} </div> ); }}@Component()export class CardDescription extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-description" class={cn("text-sm text-muted-foreground", cls)}> {children} </div> ); }}@Component()export class CardAction extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-action" class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", cls)} > {children} </div> ); }}@Component()export class CardContent extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-content" class={cn("px-6", cls)}> {children} </div> ); }}@Component()export class CardFooter extends StatelessComponent<CardSlotProps> { render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-footer" class={cn("flex items-center px-6 [.border-t]:pt-6", cls)}> {children} </div> ); }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class CardStyles extends Stylesheet { $root = this.css({ display: "flex", flexDirection: "column", gap: "1.5rem", borderRadius: "0.75rem", border: `1px solid ${t.border}`, backgroundColor: t.card, paddingTop: "1.5rem", paddingBottom: "1.5rem", color: t.cardForeground, boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }); $header = this.css({ display: "grid", gridAutoRows: "min-content", gridTemplateRows: "auto auto", alignItems: "start", gap: "0.375rem", paddingLeft: "1.5rem", paddingRight: "1.5rem", }).has("[data-slot=card-action]", { gridTemplateColumns: "1fr auto" }); $title = this.css({ fontWeight: 600, lineHeight: 1 }); $description = this.css({ fontSize: "0.875rem", color: t.mutedForeground }); $action = this.css({ gridColumnStart: 2, gridRowStart: 1, gridRow: "span 2 / span 2", alignSelf: "start", justifySelf: "end", }); $content = this.css({ paddingLeft: "1.5rem", paddingRight: "1.5rem" }); $footer = this.css({ display: "flex", alignItems: "center", paddingLeft: "1.5rem", paddingRight: "1.5rem" }).on( "&.kosmesis-bordered", { paddingTop: "1.5rem" }, );}export interface CardSlotProps { class?: string; id?: string; children?: Children;}/** Purely presentational — no Morphos equivalent, same as upstream shadcn/ui. */@Component()export class Card extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card" class={cx(this.$s.$root, cls)}> {children} </div> ); }}@Component()export class CardHeader extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-header" class={cx(this.$s.$header, cls)}> {children} </div> ); }}@Component()export class CardTitle extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-title" class={cx(this.$s.$title, cls)}> {children} </div> ); }}@Component()export class CardDescription extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-description" class={cx(this.$s.$description, cls)}> {children} </div> ); }}@Component()export class CardAction extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-action" class={cx(this.$s.$action, cls)}> {children} </div> ); }}@Component()export class CardContent extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-content" class={cx(this.$s.$content, cls)}> {children} </div> ); }}/** * Tailwind's version conditions the top border/padding on a `.border-t` utility class being * present (`[.border-t]:pt-6`). There's no arbitrary-class selector equivalent in * `@praxisjs/css`, so pass a literal `class="kosmesis-bordered"` (instead of `border-t`) to opt * into the same spacing when you add a top border yourself. */@Component()export class CardFooter extends StatelessComponent<CardSlotProps> { @Styled(CardStyles) $s!: CardStyles; render() { const { class: cls, id, children } = this.props; return ( <div id={id} data-slot="card-footer" class={cx(this.$s.$footer, cls)}> {children} </div> ); }}Examples
Usage
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
<Card class="w-96">
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription>Deploy your new project in one click.</CardDescription>
<CardAction>
<Button variant="ghost" size="sm">⋯</Button>
</CardAction>
</CardHeader>
<CardContent>
<p>Project settings go here.</p>
</CardContent>
<CardFooter class="border-t">
<Button class="w-full">Deploy</Button>
</CardFooter>
</Card>Parts
Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter —
all accept class, id, and children. CardHeader uses a grid layout so CardAction
(typically a menu button) sits in the top-right corner automatically when present.