Components
Cart Item
A single cart row with a quantity stepper. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add cart-itempnpm dlx kosmesis add cart-itemyarn dlx kosmesis add cart-itembunx kosmesis add cart-itemCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { cn } from "@/lib/utils";export interface CartItemProps { image?: string; name: string; variant?: string; price: string; quantity: number; onQuantityChange?: (quantity: number) => void; onRemove?: () => void; class?: string;}@Component()export class CartItem extends StatelessComponent<CartItemProps> { render() { const { image, name, variant, price, quantity, onQuantityChange, onRemove, class: cls } = this.props; return ( <div data-slot="cart-item" class={cn("flex items-center gap-3 py-3", cls)}> {image && <img src={image} alt={name} class="size-14 shrink-0 rounded-md object-cover" />} <div class="flex min-w-0 flex-1 flex-col gap-0.5"> <p class="truncate text-sm font-medium">{name}</p> {variant && <p class="text-xs text-muted-foreground">{variant}</p>} <div class="mt-1 flex items-center gap-1"> <button type="button" aria-label="Decrease quantity" disabled={quantity <= 1} class="flex size-6 items-center justify-center rounded border text-xs hover:bg-accent disabled:pointer-events-none disabled:opacity-30" onClick={() => { onQuantityChange?.(Math.max(1, quantity - 1)); }} > − </button> <span class="w-6 text-center text-xs">{quantity}</span> <button type="button" aria-label="Increase quantity" class="flex size-6 items-center justify-center rounded border text-xs hover:bg-accent" onClick={() => { onQuantityChange?.(quantity + 1); }} > + </button> </div> </div> <div class="flex flex-col items-end gap-2"> <span class="text-sm font-medium">{price}</span> {onRemove && ( <button type="button" aria-label="Remove item" class="text-xs text-muted-foreground hover:text-destructive" onClick={onRemove}> Remove </button> )} </div> </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 { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class CartItemStyles extends Stylesheet { $root = this.css({ display: "flex", alignItems: "center", gap: "0.75rem", padding: "0.75rem 0" }); $image = this.css({ height: "3.5rem", width: "3.5rem", flexShrink: 0, borderRadius: "0.375rem", objectFit: "cover" }); $body = this.css({ display: "flex", minWidth: 0, flex: "1 1 0%", flexDirection: "column", gap: "0.125rem" }); $name = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: "0.875rem", fontWeight: 500 }); $variant = this.css({ fontSize: "0.75rem", color: t.mutedForeground }); $stepper = this.css({ marginTop: "0.25rem", display: "flex", alignItems: "center", gap: "0.25rem" }); $stepperButton = this.css({ display: "flex", height: "1.5rem", width: "1.5rem", alignItems: "center", justifyContent: "center", borderRadius: "0.25rem", border: `1px solid ${t.border}`, fontSize: "0.75rem", cursor: "pointer", }) .on("&:hover", { backgroundColor: t.accent }) .on("&:disabled", { pointerEvents: "none", opacity: 0.3 }); $quantity = this.css({ width: "1.5rem", textAlign: "center", fontSize: "0.75rem" }); $right = this.css({ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "0.5rem" }); $price = this.css({ fontSize: "0.875rem", fontWeight: 500 }); $remove = this.css({ fontSize: "0.75rem", color: t.mutedForeground, cursor: "pointer" }).on("&:hover", { color: t.destructive, });}export interface CartItemProps { image?: string; name: string; variant?: string; price: string; quantity: number; onQuantityChange?: (quantity: number) => void; onRemove?: () => void; class?: string;}@Component()export class CartItem extends StatelessComponent<CartItemProps> { @Styled(CartItemStyles) $s!: CartItemStyles; render() { const { image, name, variant, price, quantity, onQuantityChange, onRemove, class: cls } = this.props; return ( <div data-slot="cart-item" class={cx(this.$s.$root, cls)}> {image && <img src={image} alt={name} class={this.$s.$image} />} <div class={this.$s.$body}> <p class={this.$s.$name}>{name}</p> {variant && <p class={this.$s.$variant}>{variant}</p>} <div class={this.$s.$stepper}> <button type="button" aria-label="Decrease quantity" disabled={quantity <= 1} class={this.$s.$stepperButton} onClick={() => { onQuantityChange?.(Math.max(1, quantity - 1)); }} > − </button> <span class={this.$s.$quantity}>{quantity}</span> <button type="button" aria-label="Increase quantity" class={this.$s.$stepperButton} onClick={() => { onQuantityChange?.(quantity + 1); }} > + </button> </div> </div> <div class={this.$s.$right}> <span class={this.$s.$price}>{price}</span> {onRemove && ( <button type="button" aria-label="Remove item" class={this.$s.$remove} onClick={onRemove}> Remove </button> )} </div> </div> ); }}Examples
About
Purely presentational — no Morphos equivalent. The remove button only renders when onRemove is
given; the quantity stepper's decrease button disables at quantity === 1 (use onRemove to go
below that).
Usage
import { CartItem } from "@/components/ui/cart-item";
<CartItem
image="/headphones.jpg"
name="Wireless headphones"
variant="Black"
price="$89.00"
quantity={1}
onQuantityChange={(quantity) => updateQuantity(quantity)}
onRemove={() => removeItem()}
/>Props
| Prop | Type | Default |
|---|---|---|
image | string | — |
name | string | — |
variant | string | — |
price | string | — |
quantity | number | — |
onQuantityChange | (quantity: number) => void | — |
onRemove | () => void | — |
class | string | — |