Cart Drawer
blockcommerceShopping cart drawer & full cart page view featuring free shipping progress meter, interactive line items with quantity steppers and variant tags, promo code application, order summary with tax & shipping calculation, trust badges, and empty state.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/cart-drawer.json$npx shadcn@latest add https://uipkge.dev/r/lit/cart-drawer.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/cart-drawer.json$bunx shadcn@latest add https://uipkge.dev/r/lit/cart-drawer.jsonManifest:
https://uipkge.dev/r/lit/cart-drawer.jsonInstalls to:src/uip/blocks/cart-drawer/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'drawer' | 'page' | drawer | optional |
currency | string | USD | optional |
free-shipping-threshold | number | 300 | optional |
tax-rate | number | 0.08 | optional |
initial-promo-code | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CartIteminterface CartItem {
id: string
name: string
variant: string
price: number
originalPrice?: number
qty: number
inStock?: boolean
image: string
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-cart-drawer': UipCartDrawer
}
}Files installed (1)
src/uip/blocks/cart-drawer/cart-drawer.ts28.9 kB
import { LitElement, css, html, nothing } from 'lit' import { Leaf, Lock, Minus, Plus, RotateCcw, ShieldCheck, ShoppingBag, Tag, Trash2, Truck, X } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/input/input' import type { UipInput } from '../../components/input/input' import '../../components/progress/progress' import '../../components/separator/separator' export interface CartItem { id: string name: string variant: string price: number originalPrice?: number qty: number inStock?: boolean image: string } const DEFAULT_ITEMS: CartItem[] = [ { id: 'item-1', name: 'Aero Minimalist Runner', variant: 'Size: 10.5 · Color: Matte Black', price: 120, originalPrice: 140, qty: 1, inStock: true, image: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=300&auto=format&fit=crop&q=80', }, { id: 'item-2', name: 'Technical Shell Parka', variant: 'Size: L · Color: Mineral Gray', price: 95, originalPrice: 110, qty: 1, inStock: true, image: 'https://images.unsplash.com/photo-1591047139829-d91aecb6caea?w=300&auto=format&fit=crop&q=80', }, { id: 'item-3', name: 'Minimalist Leather Cardholder', variant: 'Size: Slim · Color: Saddle Brown', price: 70, originalPrice: 85, qty: 1, inStock: true, image: 'https://images.unsplash.com/photo-1627123424574-724758594e93?w=300&auto=format&fit=crop&q=80', }, ] /** * <uip-cart-drawer> — the registry CartDrawer block as a web component. * * Class strings are React's CartDrawer verbatim. React state (useState) becomes * { state: true } fields; `items` is a property (attribute: false — set it from * JS, like the demo's empty-cart story does). `onCheckout` (items, total) / * `onContinueShopping` / `onClose` / `onUpdateQuantity` (id, qty) / * `onRemoveItem` (id) / `onApplyPromo` (code) / `onRemovePromo` become * `checkout` / `continue-shopping` / `close` / `update-quantity` / * `remove-item` / `apply-promo` / `remove-promo` CustomEvents (bubbles, * composed; payloads in `detail`). React's `promoError` state is never set by * the source (every non-empty code succeeds) so it is not ported. */ export class UipCartDrawer extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { variant: { reflect: true }, currency: {}, freeShippingThreshold: { type: Number, attribute: 'free-shipping-threshold' }, taxRate: { type: Number, attribute: 'tax-rate' }, initialPromoCode: { attribute: 'initial-promo-code' }, items: { attribute: false }, cartItems: { state: true }, promoCodeInput: { state: true }, appliedPromo: { state: true }, promoDiscount: { state: true }, promoSuccess: { state: true }, } variant: 'drawer' | 'page' = 'drawer' currency = 'USD' freeShippingThreshold = 300 taxRate = 0.08 initialPromoCode = '' items: CartItem[] = DEFAULT_ITEMS private cartItems: CartItem[] = [] private promoCodeInput = '' private appliedPromo = '' private promoDiscount = 0 private promoSuccess = '' constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'cart-drawer') } willUpdate(changed: Map<string, unknown>) { if (changed.has('items')) { this.cartItems = (this.items ?? []).map((item) => ({ ...item })) } if (changed.has('initialPromoCode')) { this.appliedPromo = this.initialPromoCode this.promoDiscount = this.initialPromoCode ? 20 : 0 } } private get totalItemsCount() { return this.cartItems.reduce((acc, item) => acc + item.qty, 0) } private get subtotal() { return this.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0) } private get isFreeShippingUnlocked() { return this.subtotal >= this.freeShippingThreshold && this.cartItems.length > 0 } private get amountAwayFromFreeShipping() { return Math.max(0, this.freeShippingThreshold - this.subtotal) } private get shippingProgress() { if (this.cartItems.length === 0) return 0 return Math.min(100, Math.round((this.subtotal / this.freeShippingThreshold) * 100)) } private get shippingFee() { if (this.cartItems.length === 0) return 0 return this.isFreeShippingUnlocked ? 0 : 15 } private get discountedSubtotal() { return Math.max(0, this.subtotal - this.promoDiscount) } private get estimatedTax() { if (this.cartItems.length === 0) return 0 return Number((this.discountedSubtotal * this.taxRate).toFixed(2)) } private get total() { if (this.cartItems.length === 0) return 0 return Number((this.discountedSubtotal + this.shippingFee + this.estimatedTax).toFixed(2)) } private formatCurrency(amount: number) { try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: this.currency, minimumFractionDigits: 2, }).format(amount) } catch { return `$${amount.toFixed(2)}` } } private updateQuantity(id: string, delta: number) { const next: CartItem[] = [] for (const item of this.cartItems) { if (item.id !== id) { next.push(item) continue } const newQty = item.qty + delta if (newQty <= 0) { this.dispatchEvent(new CustomEvent('remove-item', { detail: { id }, bubbles: true, composed: true })) continue } this.dispatchEvent(new CustomEvent('update-quantity', { detail: { id, qty: newQty }, bubbles: true, composed: true })) next.push({ ...item, qty: newQty }) } this.cartItems = next } private removeItem(id: string) { this.cartItems = this.cartItems.filter((item) => item.id !== id) this.dispatchEvent(new CustomEvent('remove-item', { detail: { id }, bubbles: true, composed: true })) } private handleApplyPromo() { this.promoSuccess = '' const trimmed = this.promoCodeInput.trim().toUpperCase() if (!trimmed) return if (trimmed === 'SUMMER20' || trimmed === 'SAVE20') { this.appliedPromo = trimmed this.promoDiscount = 20 this.promoSuccess = '$20.00 coupon discount applied!' } else if (trimmed === 'SAVE10') { this.appliedPromo = trimmed this.promoDiscount = 10 this.promoSuccess = '$10.00 coupon discount applied!' } else { this.appliedPromo = trimmed this.promoDiscount = 15 this.promoSuccess = 'Promo code applied!' } this.promoCodeInput = '' this.dispatchEvent(new CustomEvent('apply-promo', { detail: { code: trimmed }, bubbles: true, composed: true })) } private handleRemovePromo() { this.appliedPromo = '' this.promoDiscount = 0 this.promoSuccess = '' this.dispatchEvent(new CustomEvent('remove-promo', { bubbles: true, composed: true })) } private handleCheckout() { this.dispatchEvent( new CustomEvent('checkout', { detail: { items: this.cartItems.map((i) => ({ ...i })), total: this.total }, bubbles: true, composed: true, }), ) } private resetCart() { this.cartItems = DEFAULT_ITEMS.map((item) => ({ ...item })) } private renderPromoRow(placeholder: string) { if (this.appliedPromo) { return html`<div class="bg-primary/5 border-primary/20 flex items-center justify-between rounded-lg border px-3 py-2 text-xs"> <div class="flex items-center gap-2"> ${icon(Tag, 'tag', 'text-primary size-3.5')} <span class="text-foreground font-medium">${this.appliedPromo}</span> <span class="text-success text-success font-semibold tabular-nums">-${this.formatCurrency(this.promoDiscount)}</span> </div> <uip-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-6" aria-label="Remove promo code" @click=${() => this.handleRemovePromo()} > ${icon(X, 'x', 'size-3.5')} </uip-button> </div>` } return html`<div class="flex gap-2"> <uip-input .value=${this.promoCodeInput} placeholder=${placeholder} size="small" class="text-xs" @input=${(e: Event) => { this.promoCodeInput = (e.target as UipInput).value ?? '' }} @keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() this.handleApplyPromo() } }} ></uip-input> <uip-button variant="outline" size="sm" class="shrink-0 text-xs" ?disabled=${!this.promoCodeInput.trim()} @click=${() => this.handleApplyPromo()} > Apply </uip-button> </div>` } private renderCostRows(page: boolean) { return html`<div class="space-y-1.5 text-xs"> <div class="flex justify-between"> <span class="text-muted-foreground">Subtotal</span> <span class="text-foreground font-medium tabular-nums">${this.formatCurrency(this.subtotal)}</span> </div> ${this.promoDiscount > 0 ? html`<div class="text-success flex justify-between"> <span>Coupon (${this.appliedPromo})</span> <span class="font-medium tabular-nums">-${this.formatCurrency(this.promoDiscount)}</span> </div>` : nothing} <div class="flex justify-between"> <span class="text-muted-foreground">Estimated Shipping</span> ${this.shippingFee === 0 ? html`<span class="text-success font-medium">Free</span>` : html`<span class="text-foreground font-medium tabular-nums">${this.formatCurrency(this.shippingFee)}</span>`} </div> <div class="flex justify-between"> <span class="text-muted-foreground">${page ? 'Estimated Tax (8%)' : 'Estimated Tax'}</span> <span class="text-foreground font-medium tabular-nums">${this.formatCurrency(this.estimatedTax)}</span> </div> <uip-separator class="my-2"></uip-separator> ${page ? html`<div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 text-base font-semibold"> <span>Total</span> <span class="tabular-nums">${this.formatCurrency(this.total)}</span> </div>` : html`<div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 text-sm"> <span class="font-semibold">Total</span> <span class="font-semibold tabular-nums">${this.formatCurrency(this.total)}</span> </div>`} </div>` } render() { return html` <div data-slot="cart-drawer" class=${cn('w-full')}> ${this.variant === 'drawer' ? this.renderDrawer() : this.renderPage()} </div> ` } private renderDrawer() { return html`<div class="bg-card text-card-foreground border-border/80 mx-auto flex h-full max-h-[820px] min-h-[640px] w-full max-w-md flex-col overflow-hidden rounded-2xl border shadow-xs" > <div class="border-border/60 flex shrink-0 items-center justify-between border-b px-5 py-4"> <div class="flex items-center gap-2"> <h2 class="text-base font-semibold tracking-tight">Shopping Cart</h2> ${this.totalItemsCount > 0 ? html`<uip-badge variant="secondary" class="tabular-nums">${this.totalItemsCount}</uip-badge>` : nothing} </div> <uip-button variant="ghost" size="icon-sm" aria-label="Close cart drawer" @click=${() => this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))} > ${icon(X, 'x', 'size-4')} </uip-button> </div> ${this.cartItems.length > 0 ? html`<div class="bg-muted/30 border-border/40 shrink-0 border-b px-5 py-3.5"> <div class="flex items-center justify-between gap-2 text-xs"> ${this.isFreeShippingUnlocked ? html`<span class="text-primary flex items-center gap-1.5 font-medium"> ${icon(Truck, 'truck', 'size-3.5')} You've unlocked Free Express Shipping! </span>` : html`<span class="text-muted-foreground"> 🎉 You're <strong class="text-foreground font-semibold tabular-nums" >${this.formatCurrency(this.amountAwayFromFreeShipping)}</strong > away from Free Shipping! </span>`} <span class="text-muted-foreground font-medium tabular-nums">${this.shippingProgress}%</span> </div> <uip-progress value=${this.shippingProgress} class="mt-2 h-1.5"></uip-progress> </div>` : nothing} <div class="flex-1 overflow-y-auto p-5"> ${this.cartItems.length === 0 ? html`<div class="flex h-full flex-col items-center justify-center py-12 text-center"> <div class="bg-muted text-muted-foreground mb-4 flex size-14 items-center justify-center rounded-full"> ${icon(ShoppingBag, 'shopping-bag', 'size-7 stroke-[1.5]')} </div> <h3 class="text-base font-semibold">Your cart is empty</h3> <p class="text-muted-foreground mt-1 max-w-xs text-xs leading-relaxed"> Looks like you haven't added any products to your cart yet. Explore our latest items! </p> <uip-button class="mt-5 gap-2" size="sm" @click=${() => this.resetCart()}> ${icon(ShoppingBag, 'shopping-bag', 'size-3.5')} Explore Products </uip-button> </div>` : html`<div class="space-y-4"> <ul class="divide-border/60 divide-y" role="list"> ${this.cartItems.map( (item) => html`<li class="flex gap-3.5 py-4 first:pt-0 last:pb-0"> <div class="border-border/60 bg-muted/40 relative size-20 shrink-0 overflow-hidden rounded-lg border shadow-xs"> <img src=${item.image} alt=${item.name} class="size-full object-cover" /> </div> <div class="flex min-w-0 flex-1 flex-col justify-between"> <div> <div class="flex items-start justify-between gap-2"> <h3 class="text-foreground truncate text-sm leading-snug font-medium">${item.name}</h3> <uip-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-destructive -mt-1 -mr-1.5 size-7 shrink-0" aria-label=${`Remove ${item.name} from cart`} @click=${() => this.removeItem(item.id)} > ${icon(Trash2, 'trash-2', 'size-3.5')} </uip-button> </div> <p class="text-muted-foreground mt-0.5 text-xs">${item.variant}</p> <div class="mt-1 flex items-center gap-1.5"> <span class="bg-success size-1.5 rounded-full"></span> <span class="text-success text-xs font-medium">In stock</span> </div> </div> <div class="mt-2.5 flex items-center justify-between"> <div class="border-border/80 bg-background flex items-center rounded-md border shadow-xs"> <uip-button variant="ghost" size="icon-sm" class="size-7 rounded-none rounded-l-md" aria-label=${`Decrease quantity for ${item.name}`} @click=${() => this.updateQuantity(item.id, -1)} > ${icon(Minus, 'minus', 'size-3')} </uip-button> <span class="w-7 text-center text-xs font-medium tabular-nums">${item.qty}</span> <uip-button variant="ghost" size="icon-sm" class="size-7 rounded-none rounded-r-md" aria-label=${`Increase quantity for ${item.name}`} @click=${() => this.updateQuantity(item.id, 1)} > ${icon(Plus, 'plus', 'size-3')} </uip-button> </div> <div class="text-right"> ${item.originalPrice ? html`<span class="text-muted-foreground mr-1.5 text-xs tabular-nums line-through" >${this.formatCurrency(item.originalPrice * item.qty)}</span >` : nothing} <span class="text-foreground text-sm font-semibold tabular-nums" >${this.formatCurrency(item.price * item.qty)}</span > </div> </div> </div> </li>`, )} </ul> <uip-separator class="my-3"></uip-separator> <div class="space-y-2"> ${this.renderPromoRow('Promo code (e.g. SUMMER20)')} ${this.promoSuccess ? html`<p class="text-success text-xs font-medium">${this.promoSuccess}</p>` : nothing} </div> </div>`} </div> ${this.cartItems.length > 0 ? html`<div class="border-border/60 bg-card shrink-0 space-y-3 border-t p-5"> ${this.renderCostRows(false)} <div class="space-y-2 pt-1"> <uip-button class="w-full justify-center gap-2 font-medium shadow-xs" size="default" @click=${() => this.handleCheckout()}> ${icon(Lock, 'lock', 'size-3.5')} Proceed to Checkout </uip-button> <uip-button variant="ghost" class="text-muted-foreground hover:text-foreground w-full text-xs" size="sm" @click=${() => this.dispatchEvent(new CustomEvent('continue-shopping', { bubbles: true, composed: true }))} > Continue Shopping </uip-button> </div> <div class="border-border/40 text-muted-foreground grid grid-cols-1 gap-2 border-t pt-3 text-center text-xs sm:grid-cols-3"> <div class="flex flex-col items-center gap-1"> ${icon(RotateCcw, 'rotate-ccw', 'size-3.5')} <span>30-day returns</span> </div> <div class="flex flex-col items-center gap-1"> ${icon(ShieldCheck, 'shield-check', 'size-3.5')} <span>256-bit secure</span> </div> <div class="flex flex-col items-center gap-1"> ${icon(Leaf, 'leaf', 'size-3.5')} <span>Carbon neutral</span> </div> </div> </div>` : nothing} </div>` } private renderPage() { return html`<div class="mx-auto w-full max-w-6xl space-y-6"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div> <h1 class="text-2xl font-bold tracking-tight">Shopping Cart</h1> <p class="text-muted-foreground mt-0.5 text-xs">Review your selected items and calculate shipping before checkout.</p> </div> ${this.totalItemsCount > 0 ? html`<uip-badge variant="outline" class="w-fit text-xs tabular-nums">${this.totalItemsCount} items in cart</uip-badge>` : nothing} </div> ${this.cartItems.length > 0 ? html`<div class="bg-card border-border/80 rounded-xl border p-4 shadow-xs"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-2 text-xs"> ${this.isFreeShippingUnlocked ? html`${icon(Truck, 'truck', 'text-primary size-4')} <span class="text-primary font-semibold">You've unlocked Free Express Shipping on this order!</span>` : html`<span class="text-muted-foreground"> 🎉 You're <strong class="text-foreground font-semibold tabular-nums" >${this.formatCurrency(this.amountAwayFromFreeShipping)}</strong > away from Free Shipping! </span>`} </div> <span class="text-muted-foreground text-xs font-medium tabular-nums">${this.shippingProgress}% completed</span> </div> <uip-progress value=${this.shippingProgress} class="mt-2.5 h-2"></uip-progress> </div>` : nothing} ${this.cartItems.length === 0 ? html`<div class="bg-card border-border/80 flex flex-col items-center justify-center rounded-2xl border py-16 text-center shadow-xs"> <div class="bg-muted text-muted-foreground mb-4 flex size-16 items-center justify-center rounded-full"> ${icon(ShoppingBag, 'shopping-bag', 'size-8 stroke-[1.5]')} </div> <h2 class="text-lg font-semibold">Your cart is currently empty</h2> <p class="text-muted-foreground mt-1 max-w-sm text-sm"> Before you proceed to checkout you must add some products to your shopping cart. </p> <uip-button class="mt-6 gap-2" size="default" @click=${() => this.resetCart()}> ${icon(ShoppingBag, 'shopping-bag', 'size-4')} Explore Products </uip-button> </div>` : html`<div class="grid grid-cols-1 items-start gap-8 lg:grid-cols-12"> <div class="space-y-4 lg:col-span-8"> <div class="bg-card border-border/80 divide-border/60 divide-y rounded-2xl border shadow-xs"> ${this.cartItems.map( (item) => html`<div class="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-4"> <div class="border-border/60 bg-muted/40 relative size-20 shrink-0 overflow-hidden rounded-lg border shadow-xs"> <img src=${item.image} alt=${item.name} class="size-full object-cover" /> </div> <div> <h3 class="text-foreground text-sm font-semibold">${item.name}</h3> <p class="text-muted-foreground mt-0.5 text-xs">${item.variant}</p> <div class="mt-1.5 flex items-center gap-2"> <span class="bg-success size-1.5 rounded-full"></span> <span class="text-success text-xs font-medium">In stock</span> <span class="text-muted-foreground text-xs font-normal tabular-nums"> · ${this.formatCurrency(item.price)} each </span> </div> </div> </div> <div class="flex items-center justify-between gap-6 sm:justify-end"> <div class="border-border/80 bg-background flex items-center rounded-md border shadow-xs"> <uip-button variant="ghost" size="icon-sm" class="size-8 rounded-none rounded-l-md" aria-label=${`Decrease quantity for ${item.name}`} @click=${() => this.updateQuantity(item.id, -1)} > ${icon(Minus, 'minus', 'size-3.5')} </uip-button> <span class="w-8 text-center text-xs font-medium tabular-nums">${item.qty}</span> <uip-button variant="ghost" size="icon-sm" class="size-8 rounded-none rounded-r-md" aria-label=${`Increase quantity for ${item.name}`} @click=${() => this.updateQuantity(item.id, 1)} > ${icon(Plus, 'plus', 'size-3.5')} </uip-button> </div> <div class="w-24 text-right"> ${item.originalPrice ? html`<span class="text-muted-foreground block text-xs tabular-nums line-through" >${this.formatCurrency(item.originalPrice * item.qty)}</span >` : nothing} <span class="text-foreground text-base font-semibold tabular-nums" >${this.formatCurrency(item.price * item.qty)}</span > </div> <uip-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-destructive size-8" aria-label=${`Remove ${item.name} from cart`} @click=${() => this.removeItem(item.id)} > ${icon(Trash2, 'trash-2', 'size-4')} </uip-button> </div> </div>`, )} </div> <div class="bg-card border-border/80 grid grid-cols-1 gap-4 rounded-xl border p-4 shadow-xs sm:grid-cols-3"> <div class="flex items-center gap-3 text-xs"> <div class="bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-md"> ${icon(RotateCcw, 'rotate-ccw', 'size-4')} </div> <div> <p class="font-medium">Free 30-Day Returns</p> <p class="text-muted-foreground">Hassle-free return policy</p> </div> </div> <div class="flex items-center gap-3 text-xs"> <div class="bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-md"> ${icon(ShieldCheck, 'shield-check', 'size-4')} </div> <div> <p class="font-medium">256-Bit SSL Security</p> <p class="text-muted-foreground">Bank-grade data encryption</p> </div> </div> <div class="flex items-center gap-3 text-xs"> <div class="bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-md"> ${icon(Leaf, 'leaf', 'size-4')} </div> <div> <p class="font-medium">Carbon Neutral</p> <p class="text-muted-foreground">Offset delivery emissions</p> </div> </div> </div> </div> <div class="space-y-4 lg:sticky lg:top-6 lg:col-span-4"> <div class="bg-card border-border/80 space-y-4 rounded-2xl border p-5 shadow-xs"> <h2 class="text-base font-semibold">Order Summary</h2> <div class="space-y-2"> <label class="text-muted-foreground text-xs font-medium">Have a coupon code?</label> ${this.renderPromoRow('Enter code (SUMMER20)')} </div> <uip-separator></uip-separator> ${this.renderCostRows(true)} <uip-button class="w-full justify-center gap-2 font-medium shadow-xs" size="lg" @click=${() => this.handleCheckout()}> ${icon(Lock, 'lock', 'size-4')} Proceed to Checkout </uip-button> <uip-button variant="ghost" class="text-muted-foreground hover:text-foreground w-full text-xs" size="sm" @click=${() => this.dispatchEvent(new CustomEvent('continue-shopping', { bubbles: true, composed: true }))} > Continue Shopping </uip-button> </div> </div> </div>`} </div>` } } customElements.get('uip-cart-drawer') || customElements.define('uip-cart-drawer', UipCartDrawer) declare global { interface HTMLElementTagNameMap { 'uip-cart-drawer': UipCartDrawer } }
Raw manifest:https://uipkge.dev/r/lit/cart-drawer.json