Components
Receipt
A purchase receipt, either a thermal-paper printout or a modern card. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add receiptpnpm dlx kosmesis add receiptyarn dlx kosmesis add receiptbunx kosmesis add receiptInstall the following dependencies:
npm install @morphos/iconspnpm add @morphos/iconsyarn add @morphos/iconsbun add @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { cn } from "@/lib/utils";export type ReceiptKind = "retail" | "restaurant" | "fuel";export type ReceiptVariant = "physical" | "modern";export interface ReceiptItem { name: string; quantity?: number; price: string;}export interface ReceiptTipSuggestion { label: string; amount: string;}export interface ReceiptFuelDetails { pump?: string; fuelType?: string; pricePerUnit?: string; volume?: string;}export interface ReceiptProps { kind?: ReceiptKind; variant?: ReceiptVariant; merchant: string; address?: string; date: string; receiptNumber?: string; items?: ReceiptItem[]; table?: string; server?: string; tipSuggestions?: ReceiptTipSuggestion[]; fuel?: ReceiptFuelDetails; subtotal?: string; tax?: string; total: string; paymentMethod?: string; footerNote?: string; class?: string;}function zigzagEdge(teeth: number, fromX: number, toX: number, tipY: number, valleyY: number): string[] { const points: string[] = []; for (let i = 0; i <= teeth; i++) { const x = fromX + ((toX - fromX) * i) / teeth; points.push(`${String(x)}% ${String(i % 2 === 0 ? tipY : valleyY)}%`); } return points;}const TORN_EDGE_CLIP_PATH = `polygon(${[...zigzagEdge(16, 0, 100, 0, 2.5), ...zigzagEdge(16, 100, 0, 100, 97.5)].join(", ")})`;@Component()export class Receipt extends StatelessComponent<ReceiptProps> { private renderHeader(physical: boolean) { const { merchant, address } = this.props; if (physical) { return ( <div class="flex flex-col items-center gap-0.5 text-center"> <span class="text-sm font-bold tracking-wide uppercase">{merchant}</span> {address && <span class="text-[11px] text-neutral-500">{address}</span>} </div> ); } return ( <div class="flex items-center gap-2"> <span class="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"> <Icon name="Receipt" size={16} /> </span> <div class="flex flex-col gap-0.5"> <span class="text-sm font-semibold">{merchant}</span> {address && <span class="text-xs text-muted-foreground">{address}</span>} </div> </div> ); } private renderDivider(physical: boolean) { return <div class={cn("my-3", physical ? "border-t border-dashed border-neutral-300" : "border-t border-border")} />; } private renderMetaRow(physical: boolean) { const { date, receiptNumber, kind, table, server } = this.props; return ( <div class="flex flex-col gap-1.5"> <div class={cn("flex justify-between", physical ? "text-[11px] text-neutral-500" : "text-xs text-muted-foreground")}> <span>{date}</span> {receiptNumber && <span>{`#${receiptNumber}`}</span>} </div> {kind === "restaurant" && (table ?? server) && ( <div class={cn("flex justify-between", physical ? "text-[11px] text-neutral-500" : "text-xs text-muted-foreground")}> {table && <span>{`Table ${table}`}</span>} {server && <span>{`Server: ${server}`}</span>} </div> )} </div> ); } private renderItems(physical: boolean) { const items = this.props.items ?? []; return ( <div class="flex flex-col gap-1.5"> {items.map((item, i) => ( <div key={`${item.name}-${String(i)}`} class={cn("flex justify-between gap-3", !physical && "items-center text-sm")}> <span class={physical ? "truncate" : "text-foreground"}> {!physical && item.quantity && item.quantity > 1 && <span class="mr-1.5 text-muted-foreground">{`${String(item.quantity)}×`}</span>} {physical && item.quantity && item.quantity > 1 ? `${String(item.quantity)}x ${item.name}` : item.name} </span> <span class={cn("tabular-nums", !physical && "font-medium")}>{item.price}</span> </div> ))} </div> ); } private renderFuelGrid(physical: boolean) { const { pump, fuelType, pricePerUnit, volume } = this.props.fuel ?? {}; const entries: Array<[string, string]> = [ ...(pump ? ([["Pump", pump]] as Array<[string, string]>) : []), ...(fuelType ? ([["Fuel", fuelType]] as Array<[string, string]>) : []), ...(pricePerUnit ? ([["Price / unit", pricePerUnit]] as Array<[string, string]>) : []), ...(volume ? ([["Volume", volume]] as Array<[string, string]>) : []), ]; return ( <div class="grid grid-cols-2 gap-x-4 gap-y-2"> {entries.map(([label, value]) => ( <div key={label} class="flex flex-col"> <span class={physical ? "text-[10px] tracking-wide text-neutral-500 uppercase" : "text-[10px] tracking-wide text-muted-foreground uppercase"}>{label}</span> <span class={cn("tabular-nums", physical ? "font-mono text-sm font-semibold" : "text-sm font-medium")}>{value}</span> </div> ))} </div> ); } private renderTotals(physical: boolean) { const { subtotal, tax, total } = this.props; const muted = physical ? "text-neutral-500" : "text-muted-foreground"; return ( <div class={cn("flex flex-col gap-1", physical ? "text-[12px]" : "gap-1.5 text-sm")}> {subtotal && ( <div class={cn("flex justify-between", muted)}> <span>Subtotal</span> <span class="tabular-nums">{subtotal}</span> </div> )} {tax && ( <div class={cn("flex justify-between", muted)}> <span>Tax</span> <span class="tabular-nums">{tax}</span> </div> )} <div class={cn("flex justify-between font-bold", physical ? "text-sm" : "text-base font-semibold")}> <span>Total</span> <span class="tabular-nums">{total}</span> </div> </div> ); } private renderTipSuggestions(physical: boolean) { const suggestions = this.props.tipSuggestions; if (!suggestions?.length) return null; return ( <div class={cn("flex justify-between gap-2", physical ? "text-[11px] text-neutral-500" : "text-xs text-muted-foreground")}> {suggestions.map((s) => ( <div key={s.label} class="flex flex-1 flex-col items-center gap-0.5 rounded-md border border-dashed border-current/30 py-1.5"> <span>{s.label}</span> <span class={cn("tabular-nums", physical ? "font-mono font-semibold text-neutral-900" : "font-semibold text-foreground")}>{s.amount}</span> </div> ))} </div> ); } private renderSignatureLine() { return ( <div class="mt-3 flex flex-col items-center gap-1"> <div class="h-6 w-full border-b border-dashed border-neutral-300" /> <span class="text-[10px] tracking-wide text-neutral-500 uppercase">Signature</span> </div> ); } private renderBody(physical: boolean) { const { kind = "retail" } = this.props; return ( <> {this.renderMetaRow(physical)} {this.renderDivider(physical)} {kind === "fuel" ? this.renderFuelGrid(physical) : this.renderItems(physical)} {this.renderDivider(physical)} {this.renderTotals(physical)} {kind === "restaurant" && this.props.tipSuggestions?.length && <div class="mt-3">{this.renderTipSuggestions(physical)}</div>} {kind === "restaurant" && physical && this.renderSignatureLine()} </> ); } private renderPhysical() { const { paymentMethod, footerNote, class: cls } = this.props; return ( <div data-slot="receipt" data-variant="physical" class={cn("mx-auto w-full max-w-xs bg-neutral-50 px-5 pt-7 pb-9 font-mono text-[13px] text-neutral-900 shadow-md", cls)} style={{ clipPath: TORN_EDGE_CLIP_PATH }} > {this.renderHeader(true)} {this.renderDivider(true)} {this.renderBody(true)} {paymentMethod && <div class="mt-3 text-center text-[11px] text-neutral-500 uppercase">{paymentMethod}</div>} <div class="mt-4 h-6 bg-[repeating-linear-gradient(90deg,black_0px,black_2px,transparent_2px,transparent_4px)] opacity-80" /> {footerNote && <div class="mt-3 text-center text-[11px] text-neutral-500 italic">{footerNote}</div>} </div> ); } private renderModern() { const { paymentMethod, footerNote, class: cls } = this.props; return ( <div data-slot="receipt" data-variant="modern" class={cn("mx-auto w-full max-w-sm rounded-xl border bg-card p-5 text-card-foreground shadow-xs", cls)}> {this.renderHeader(false)} {this.renderDivider(false)} {this.renderBody(false)} {(paymentMethod ?? footerNote) && ( <div class="mt-4 flex flex-col gap-1 border-t border-border pt-3 text-xs text-muted-foreground"> {paymentMethod && <span>{paymentMethod}</span>} {footerNote && <span>{footerNote}</span>} </div> )} </div> ); } render() { return this.props.variant === "physical" ? this.renderPhysical() : this.renderModern(); }}Install the following dependencies:
npm install @praxisjs/css @morphos/iconspnpm add @praxisjs/css @morphos/iconsyarn add @praxisjs/css @morphos/iconsbun add @praxisjs/css @morphos/iconsCopy 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 { Icon } from "@morphos/icons";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);function zigzagEdge(teeth: number, fromX: number, toX: number, tipY: number, valleyY: number): string[] { const points: string[] = []; for (let i = 0; i <= teeth; i++) { const x = fromX + ((toX - fromX) * i) / teeth; points.push(`${String(x)}% ${String(i % 2 === 0 ? tipY : valleyY)}%`); } return points;}const TORN_EDGE_CLIP_PATH = `polygon(${[...zigzagEdge(16, 0, 100, 0, 2.5), ...zigzagEdge(16, 100, 0, 100, 97.5)].join(", ")})`;class ReceiptStyles extends Stylesheet { $physicalShell = this.css({ margin: "0 auto", width: "100%", maxWidth: "20rem", backgroundColor: "oklch(0.98 0 0)", padding: "1.75rem 1.25rem 2.25rem", fontFamily: "ui-monospace, monospace", fontSize: "13px", color: "oklch(0.2 0 0)", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", clipPath: TORN_EDGE_CLIP_PATH, }); $modernShell = this.css({ margin: "0 auto", width: "100%", maxWidth: "24rem", borderRadius: "0.75rem", border: `1px solid ${t.border}`, backgroundColor: t.card, color: t.cardForeground, padding: "1.25rem", boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }); $headerPhysical = this.css({ display: "flex", flexDirection: "column", alignItems: "center", gap: "0.125rem", textAlign: "center" }); $merchantPhysical = this.css({ fontSize: "0.875rem", fontWeight: 700, letterSpacing: "0.05em", textTransform: "uppercase" }); $addressPhysical = this.css({ fontSize: "11px", color: "oklch(0.55 0 0)" }); $headerModernRow = this.css({ display: "flex", alignItems: "center", gap: "0.5rem" }); $iconBadge = this.css({ display: "flex", height: "2rem", width: "2rem", flexShrink: 0, alignItems: "center", justifyContent: "center", borderRadius: "9999px", backgroundColor: `color-mix(in oklab, ${t.primary} 10%, transparent)`, color: t.primary, }); $headerModernMeta = this.css({ display: "flex", flexDirection: "column", gap: "0.125rem" }); $merchantModern = this.css({ fontSize: "0.875rem", fontWeight: 600 }); $mutedModern = this.css({ fontSize: "0.75rem", color: t.mutedForeground }); $dividerPhysical = this.css({ margin: "0.75rem 0", borderTop: "1px dashed oklch(0.85 0 0)" }); $dividerModern = this.css({ margin: "1rem 0", borderTop: `1px solid ${t.border}` }); $metaRowPhysical = this.css({ display: "flex", justifyContent: "space-between", fontSize: "11px", color: "oklch(0.55 0 0)" }); $metaRowModern = this.css({ display: "flex", justifyContent: "space-between", fontSize: "0.75rem", color: t.mutedForeground }); $metaCol = this.css({ display: "flex", flexDirection: "column", gap: "0.375rem" }); $items = this.css({ display: "flex", flexDirection: "column", gap: "0.375rem" }); $itemRowPhysical = this.css({ display: "flex", justifyContent: "space-between", gap: "0.75rem" }); $itemRowModern = this.css({ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.75rem", fontSize: "0.875rem" }); $itemNamePhysical = this.css({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }); $itemNameModern = this.css({ color: t.foreground }); $qtyModern = this.css({ marginRight: "0.375rem", color: t.mutedForeground }); $priceModern = this.css({ fontWeight: 500, fontVariantNumeric: "tabular-nums" }); $tabularNums = this.css({ fontVariantNumeric: "tabular-nums" }); $fuelGrid = this.css({ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", columnGap: "1rem", rowGap: "0.5rem" }); $fuelEntry = this.css({ display: "flex", flexDirection: "column" }); $fuelLabelPhysical = this.css({ fontSize: "10px", letterSpacing: "0.05em", color: "oklch(0.55 0 0)", textTransform: "uppercase" }); $fuelLabelModern = this.css({ fontSize: "10px", letterSpacing: "0.05em", color: t.mutedForeground, textTransform: "uppercase" }); $fuelValuePhysical = this.css({ fontFamily: "ui-monospace, monospace", fontSize: "0.875rem", fontWeight: 600 }); $fuelValueModern = this.css({ fontSize: "0.875rem", fontWeight: 500 }); $totalsPhysical = this.css({ display: "flex", flexDirection: "column", gap: "0.25rem", fontSize: "12px" }); $totalsModern = this.css({ display: "flex", flexDirection: "column", gap: "0.375rem", fontSize: "0.875rem" }); $totalRowMutedPhysical = this.css({ display: "flex", justifyContent: "space-between", color: "oklch(0.55 0 0)" }); $totalRowMutedModern = this.css({ display: "flex", justifyContent: "space-between", color: t.mutedForeground }); $totalRowPhysical = this.css({ display: "flex", justifyContent: "space-between", fontSize: "0.875rem", fontWeight: 700 }); $totalRowModern = this.css({ display: "flex", justifyContent: "space-between", fontSize: "1rem", fontWeight: 600 }); $tipRow = this.css({ display: "flex", justifyContent: "space-between", gap: "0.5rem", marginTop: "0.75rem" }); $tipRowPhysical = this.css({ fontSize: "11px", color: "oklch(0.55 0 0)" }); $tipRowModern = this.css({ fontSize: "0.75rem", color: t.mutedForeground }); $tipBox = this.css({ display: "flex", flex: "1 1 0%", flexDirection: "column", alignItems: "center", gap: "0.125rem", borderRadius: "0.375rem", border: "1px dashed color-mix(in oklab, currentColor 30%, transparent)", padding: "0.375rem 0", }); $tipAmountPhysical = this.css({ fontFamily: "ui-monospace, monospace", fontWeight: 600, color: "oklch(0.2 0 0)" }); $tipAmountModern = this.css({ fontWeight: 600, color: t.foreground }); $signatureWrap = this.css({ marginTop: "0.75rem", display: "flex", flexDirection: "column", alignItems: "center", gap: "0.25rem" }); $signatureLine = this.css({ height: "1.5rem", width: "100%", borderBottom: "1px dashed oklch(0.85 0 0)" }); $signatureLabel = this.css({ fontSize: "10px", letterSpacing: "0.05em", color: "oklch(0.55 0 0)", textTransform: "uppercase" }); $paymentPhysical = this.css({ marginTop: "0.75rem", textAlign: "center", fontSize: "11px", color: "oklch(0.55 0 0)", textTransform: "uppercase" }); $barcode = this.css({ marginTop: "1rem", height: "1.5rem", opacity: 0.8, backgroundImage: "repeating-linear-gradient(90deg, black 0px, black 2px, transparent 2px, transparent 4px)", }); $footerNotePhysical = this.css({ marginTop: "0.75rem", textAlign: "center", fontSize: "11px", color: "oklch(0.55 0 0)", fontStyle: "italic" }); $modernFooter = this.css({ marginTop: "1rem", display: "flex", flexDirection: "column", gap: "0.25rem", borderTop: `1px solid ${t.border}`, paddingTop: "0.75rem", fontSize: "0.75rem", color: t.mutedForeground, });}export type ReceiptKind = "retail" | "restaurant" | "fuel";export type ReceiptVariant = "physical" | "modern";export interface ReceiptItem { name: string; quantity?: number; price: string;}export interface ReceiptTipSuggestion { label: string; amount: string;}export interface ReceiptFuelDetails { pump?: string; fuelType?: string; pricePerUnit?: string; volume?: string;}export interface ReceiptProps { kind?: ReceiptKind; variant?: ReceiptVariant; merchant: string; address?: string; date: string; receiptNumber?: string; items?: ReceiptItem[]; table?: string; server?: string; tipSuggestions?: ReceiptTipSuggestion[]; fuel?: ReceiptFuelDetails; subtotal?: string; tax?: string; total: string; paymentMethod?: string; footerNote?: string; class?: string;}@Component()export class Receipt extends StatelessComponent<ReceiptProps> { @Styled(ReceiptStyles) $s!: ReceiptStyles; private renderHeader(physical: boolean) { const { merchant, address } = this.props; if (physical) { return ( <div class={this.$s.$headerPhysical}> <span class={this.$s.$merchantPhysical}>{merchant}</span> {address && <span class={this.$s.$addressPhysical}>{address}</span>} </div> ); } return ( <div class={this.$s.$headerModernRow}> <span class={this.$s.$iconBadge}> <Icon name="Receipt" size={16} /> </span> <div class={this.$s.$headerModernMeta}> <span class={this.$s.$merchantModern}>{merchant}</span> {address && <span class={this.$s.$mutedModern}>{address}</span>} </div> </div> ); } private renderDivider(physical: boolean) { return <div class={physical ? this.$s.$dividerPhysical : this.$s.$dividerModern} />; } private renderMetaRow(physical: boolean) { const { date, receiptNumber, kind, table, server } = this.props; const rowClass = physical ? this.$s.$metaRowPhysical : this.$s.$metaRowModern; return ( <div class={this.$s.$metaCol}> <div class={rowClass}> <span>{date}</span> {receiptNumber && <span>{`#${receiptNumber}`}</span>} </div> {kind === "restaurant" && (table ?? server) && ( <div class={rowClass}> {table && <span>{`Table ${table}`}</span>} {server && <span>{`Server: ${server}`}</span>} </div> )} </div> ); } private renderItems(physical: boolean) { const items = this.props.items ?? []; return ( <div class={this.$s.$items}> {items.map((item, i) => ( <div key={`${item.name}-${String(i)}`} class={physical ? this.$s.$itemRowPhysical : this.$s.$itemRowModern}> <span class={physical ? this.$s.$itemNamePhysical : this.$s.$itemNameModern}> {!physical && item.quantity && item.quantity > 1 && <span class={this.$s.$qtyModern}>{`${String(item.quantity)}×`}</span>} {physical && item.quantity && item.quantity > 1 ? `${String(item.quantity)}x ${item.name}` : item.name} </span> <span class={physical ? this.$s.$tabularNums : cx(this.$s.$priceModern)}>{item.price}</span> </div> ))} </div> ); } private renderFuelGrid(physical: boolean) { const { pump, fuelType, pricePerUnit, volume } = this.props.fuel ?? {}; const entries: Array<[string, string]> = [ ...(pump ? ([["Pump", pump]] as Array<[string, string]>) : []), ...(fuelType ? ([["Fuel", fuelType]] as Array<[string, string]>) : []), ...(pricePerUnit ? ([["Price / unit", pricePerUnit]] as Array<[string, string]>) : []), ...(volume ? ([["Volume", volume]] as Array<[string, string]>) : []), ]; return ( <div class={this.$s.$fuelGrid}> {entries.map(([label, value]) => ( <div key={label} class={this.$s.$fuelEntry}> <span class={physical ? this.$s.$fuelLabelPhysical : this.$s.$fuelLabelModern}>{label}</span> <span class={physical ? this.$s.$fuelValuePhysical : this.$s.$fuelValueModern}>{value}</span> </div> ))} </div> ); } private renderTotals(physical: boolean) { const { subtotal, tax, total } = this.props; const mutedRow = physical ? this.$s.$totalRowMutedPhysical : this.$s.$totalRowMutedModern; return ( <div class={physical ? this.$s.$totalsPhysical : this.$s.$totalsModern}> {subtotal && ( <div class={mutedRow}> <span>Subtotal</span> <span class={this.$s.$tabularNums}>{subtotal}</span> </div> )} {tax && ( <div class={mutedRow}> <span>Tax</span> <span class={this.$s.$tabularNums}>{tax}</span> </div> )} <div class={physical ? this.$s.$totalRowPhysical : this.$s.$totalRowModern}> <span>Total</span> <span class={this.$s.$tabularNums}>{total}</span> </div> </div> ); } private renderTipSuggestions(physical: boolean) { const suggestions = this.props.tipSuggestions; if (!suggestions?.length) return null; return ( <div class={cx(this.$s.$tipRow, physical ? this.$s.$tipRowPhysical : this.$s.$tipRowModern)}> {suggestions.map((s) => ( <div key={s.label} class={this.$s.$tipBox}> <span>{s.label}</span> <span class={physical ? this.$s.$tipAmountPhysical : this.$s.$tipAmountModern}>{s.amount}</span> </div> ))} </div> ); } private renderSignatureLine() { return ( <div class={this.$s.$signatureWrap}> <div class={this.$s.$signatureLine} /> <span class={this.$s.$signatureLabel}>Signature</span> </div> ); } private renderBody(physical: boolean) { const { kind = "retail" } = this.props; return ( <> {this.renderMetaRow(physical)} {this.renderDivider(physical)} {kind === "fuel" ? this.renderFuelGrid(physical) : this.renderItems(physical)} {this.renderDivider(physical)} {this.renderTotals(physical)} {kind === "restaurant" && this.props.tipSuggestions?.length ? this.renderTipSuggestions(physical) : null} {kind === "restaurant" && physical && this.renderSignatureLine()} </> ); } private renderPhysical() { const { paymentMethod, footerNote, class: cls } = this.props; return ( <div data-slot="receipt" data-variant="physical" class={cx(this.$s.$physicalShell, cls)}> {this.renderHeader(true)} {this.renderDivider(true)} {this.renderBody(true)} {paymentMethod && <div class={this.$s.$paymentPhysical}>{paymentMethod}</div>} <div class={this.$s.$barcode} /> {footerNote && <div class={this.$s.$footerNotePhysical}>{footerNote}</div>} </div> ); } private renderModern() { const { paymentMethod, footerNote, class: cls } = this.props; return ( <div data-slot="receipt" data-variant="modern" class={cx(this.$s.$modernShell, cls)}> {this.renderHeader(false)} {this.renderDivider(false)} {this.renderBody(false)} {(paymentMethod ?? footerNote) && ( <div class={this.$s.$modernFooter}> {paymentMethod && <span>{paymentMethod}</span>} {footerNote && <span>{footerNote}</span>} </div> )} </div> ); } render() { return this.props.variant === "physical" ? this.renderPhysical() : this.renderModern(); }}Examples
Physical
Kind: Restaurant
Kind: Fuel
About
Purely presentational — no Morphos equivalent. Two independent props control the result:
variantswaps the chrome rather than just colors:"modern"(default) — a theme-aware card built from the same tokens as the rest of the registry (bg-card,border-border, ...), so it adapts to light/dark mode like any other surface."physical"— a thermal-paper printout. It uses fixed paper colors instead of theme tokens (a real receipt doesn't invert for dark mode) and a torn-edgeclip-pathcomputed once at module load, jagged at both the top and bottom the way a receipt looks after being torn off the roll.
kindswaps the body layout:"retail"(default) — the itemized list shown above."restaurant"— adds atable/serverrow under the date, renderstipSuggestionsas a row of suggested amounts, and (physical only) a blank signature line at the bottom."fuel"— replaces the item list with afuelstat grid (pump,fuelType,pricePerUnit,volume) instead of itemized products.
items, subtotal, tax, and paymentMethod/footerNote are all optional beyond total — omit
what a given receipt doesn't have. table/server/tipSuggestions only render for
kind="restaurant", fuel only for kind="fuel".
Usage
import { Receipt } from "@/components/ui/receipt";
<Receipt
merchant="Kosmesis Café"
address="221B Baker Street"
date="Jul 12, 2026 — 09:41"
receiptNumber="10492"
items={[
{ name: "Flat White", quantity: 2, price: "$9.00" },
{ name: "Blueberry Muffin", price: "$4.50" },
]}
subtotal="$13.50"
tax="$1.08"
total="$14.58"
paymentMethod="Visa •••• 4242"
footerNote="Thank you for stopping by!"
/>
// Restaurant, with a suggested tip row and a signature line (physical only)
<Receipt
variant="physical"
kind="restaurant"
merchant="Trattoria Nove"
date="Jul 18, 2026 — 20:12"
table="7"
server="Marco"
items={[{ name: "Margherita Pizza", price: "$16.00" }]}
total="$16.00"
tipSuggestions={[
{ label: "15%", amount: "$2.40" },
{ label: "18%", amount: "$2.88" },
{ label: "20%", amount: "$3.20" },
]}
/>
// Fuel, with a pump/price/volume stat grid instead of an item list
<Receipt kind="fuel" merchant="Roadside Fuel Co." date="Jul 20, 2026 — 14:03" fuel={{ pump: "4", fuelType: "Regular", pricePerUnit: "$3.42/gal", volume: "12.4 gal" }} total="$42.41" />Props
| Prop | Type | Default |
|---|---|---|
merchant | string | — |
address | string | — |
date | string | — |
receiptNumber | string | — |
items | { name: string; quantity?: number; price: string }[] | — |
table | string | — |
server | string | — |
tipSuggestions | { label: string; amount: string }[] | — |
fuel | { pump?: string; fuelType?: string; pricePerUnit?: string; volume?: string } | — |
subtotal | string | — |
tax | string | — |
total | string | — |
paymentMethod | string | — |
footerNote | string | — |
kind | "retail" | "restaurant" | "fuel" | "retail" |
variant | "physical" | "modern" | "modern" |
class | string | — |