Order Management Page
blockcommerceE-commerce and SaaS order fulfillment admin table: header with export CSV and new order action, 4 KPI summary metric cards, search and multi-status filter toolbar with date range selector, selectable order rows with customer avatars and status badges, slide-over order details drawer with line items breakdown and tracking info, bulk action bar, and pagination.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/order-management-page.json$npx shadcn@latest add https://uipkge.dev/r/lit/order-management-page.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/order-management-page.json$bunx shadcn@latest add https://uipkge.dev/r/lit/order-management-page.jsonManifest:
https://uipkge.dev/r/lit/order-management-page.jsonInstalls to:src/uip/blocks/order-management-page/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
OrderIteminterface OrderItem {
name: string
sku: string
quantity: number
price: number
}CustomerInfointerface CustomerInfo {
name: string
email: string
avatar?: string
address: string
city: string
state: string
zip: string
}Orderinterface Order {
id: string
orderNumber: string
date: string
customer: CustomerInfo
items: OrderItem[]
status: OrderStatus
paymentMethod: string
subtotal: number
shipping: number
tax: number
total: number
carrier?: string
trackingNumber?: string
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-order-management-page': UipOrderManagementPage
}
}Files installed (1)
src/uip/blocks/order-management-page/order-management-page.ts37.6 kB
import { LitElement, css, html, nothing } from 'lit' import { ArrowUpRight, Calendar, ChevronDown, Clock, DollarSign, Download, Eye, FileText, MoreHorizontal, PackageCheck, Plus, RotateCcw, Search, ShoppingCart, TrendingUp, Truck, } 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/avatar/avatar' import '../../components/badge/badge' import '../../components/button/button' import '../../components/card/card' import '../../components/checkbox/checkbox' import '../../components/dropdown-menu/dropdown-menu' import '../../components/input/input' import type { UipInput } from '../../components/input/input' import '../../components/sheet/sheet' export type OrderStatus = 'paid' | 'processing' | 'shipped' | 'refunded' export interface OrderItem { name: string sku: string quantity: number price: number } export interface CustomerInfo { name: string email: string avatar?: string address: string city: string state: string zip: string } export interface Order { id: string orderNumber: string date: string customer: CustomerInfo items: OrderItem[] status: OrderStatus paymentMethod: string subtotal: number shipping: number tax: number total: number carrier?: string trackingNumber?: string } const orders: Order[] = [ { id: 'ord-1', orderNumber: '#ORD-8492', date: 'Oct 24, 2026', customer: { name: 'Sarah Chen', email: '[email protected]', address: '742 Evergreen Terrace', city: 'Springfield', state: 'OR', zip: '97477', }, items: [ { name: 'Ergonomic Mechanical Keyboard', sku: 'KB-ERGO-01', quantity: 1, price: 189 }, { name: 'Desk Mat Pro (Midnight)', sku: 'MAT-PRO-BLK', quantity: 1, price: 39 }, ], status: 'processing', paymentMethod: 'Mastercard •••• 8492', subtotal: 228, shipping: 0, tax: 18.24, total: 246.24, carrier: 'FedEx Priority', trackingNumber: 'FX-9281034-US', }, { id: 'ord-2', orderNumber: '#ORD-8491', date: 'Oct 24, 2026', customer: { name: 'Marcus Vance', email: '[email protected]', address: '100 Enterprise Way, Suite 400', city: 'San Francisco', state: 'CA', zip: '94105', }, items: [ { name: 'UltraWide Monitor 34"', sku: 'MON-34-UW', quantity: 1, price: 699 }, { name: 'Dual Monitor Arm Mount', sku: 'ACC-ARM-02', quantity: 1, price: 129 }, { name: 'Braided DisplayPort Cable 2m', sku: 'CAB-DP-02', quantity: 1, price: 24 }, ], status: 'shipped', paymentMethod: 'Visa •••• 4242', subtotal: 852, shipping: 0, tax: 68.16, total: 920.16, carrier: 'UPS Ground', trackingNumber: '1Z999AA10123456784', }, { id: 'ord-3', orderNumber: '#ORD-8490', date: 'Oct 23, 2026', customer: { name: 'Elena Rostova', email: '[email protected]', address: '450 Innovation Parkway', city: 'Austin', state: 'TX', zip: '78701', }, items: [{ name: 'SaaS Enterprise Annual License', sku: 'LIC-ENT-YR', quantity: 1, price: 1200 }], status: 'paid', paymentMethod: 'Visa •••• 9104', subtotal: 1200, shipping: 0, tax: 0, total: 1200, }, { id: 'ord-4', orderNumber: '#ORD-8489', date: 'Oct 23, 2026', customer: { name: 'David Kim', email: '[email protected]', address: '88 King Street West', city: 'Seattle', state: 'WA', zip: '98104', }, items: [ { name: 'USB-C Multiport Dock Pro', sku: 'HUB-USBC-8P', quantity: 2, price: 79 }, { name: 'Thunderbolt 4 Braided Cable', sku: 'CAB-TB4-1M', quantity: 2, price: 29 }, ], status: 'shipped', paymentMethod: 'Apple Pay', subtotal: 216, shipping: 0, tax: 17.28, total: 233.28, carrier: 'DHL Express', trackingNumber: 'DHL-8492019-EXP', }, { id: 'ord-5', orderNumber: '#ORD-8488', date: 'Oct 22, 2026', customer: { name: 'Olivia Taylor', email: '[email protected]', address: '12 Marina Boulevard', city: 'Boston', state: 'MA', zip: '02210', }, items: [{ name: 'Noise-Cancelling Studio Headphones', sku: 'AUD-NC-700', quantity: 1, price: 349 }], status: 'refunded', paymentMethod: 'Visa •••• 1128', subtotal: 349, shipping: 0, tax: 27.92, total: 376.92, }, { id: 'ord-6', orderNumber: '#ORD-8487', date: 'Oct 22, 2026', customer: { name: 'Liam Johansson', email: '[email protected]', address: '240 Queen Street', city: 'New York', state: 'NY', zip: '10001', }, items: [ { name: 'Aluminum Laptop Riser', sku: 'STN-ALUM-SLV', quantity: 1, price: 65 }, { name: 'Wireless Precision Mouse', sku: 'MOU-WL-01', quantity: 1, price: 99 }, ], status: 'processing', paymentMethod: 'Mastercard •••• 5531', subtotal: 164, shipping: 0, tax: 13.12, total: 177.12, carrier: 'USPS Priority', trackingNumber: '9400100000000000000000', }, { id: 'ord-7', orderNumber: '#ORD-8486', date: 'Oct 21, 2026', customer: { name: 'Aisha Patel', email: '[email protected]', address: '500 Central Avenue', city: 'Chicago', state: 'IL', zip: '60601', }, items: [{ name: 'Cloud Storage Tier 3 (500GB)', sku: 'SRV-STR-500', quantity: 1, price: 450 }], status: 'paid', paymentMethod: 'Amex •••• 3009', subtotal: 450, shipping: 0, tax: 36, total: 486, }, { id: 'ord-8', orderNumber: '#ORD-8485', date: 'Oct 20, 2026', customer: { name: 'Lucas Moreau', email: '[email protected]', address: '15 Rue de la Paix', city: 'Boulder', state: 'CO', zip: '80302', }, items: [ { name: 'Broadcast Studio Microphone', sku: 'MIC-POD-01', quantity: 1, price: 229 }, { name: 'Dual-Layer Pop Filter', sku: 'ACC-POP-01', quantity: 1, price: 25 }, { name: 'Gold-Plated XLR Cable 3m', sku: 'CAB-XLR-03', quantity: 1, price: 19 }, ], status: 'shipped', paymentMethod: 'Visa •••• 7712', subtotal: 273, shipping: 0, tax: 21.84, total: 294.84, carrier: 'FedEx Ground', trackingNumber: 'FX-7712093-US', }, ] const statusBadgeConfig: Record<OrderStatus, { label: string; class: string; dot: string }> = { paid: { label: 'Paid', class: 'bg-success/10 text-success border-success/20', dot: 'bg-success' }, processing: { label: 'Processing', class: 'bg-info/10 text-info border-info/20', dot: 'bg-info' }, shipped: { label: 'Shipped', class: 'bg-chart-1/10 text-chart-1 border-chart-1/20', dot: 'bg-chart-1' }, refunded: { label: 'Refunded', class: 'bg-destructive/10 text-destructive border-destructive/20', dot: 'bg-destructive' }, } function formatCurrency(val: number) { return val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } function initials(name: string) { return name .split(' ') .map((p) => p[0]) .slice(0, 2) .join('') .toUpperCase() } /** * <uip-order-management-page> — the registry OrderManagementPage block as a web component. * * Class strings and order data are React's verbatim. React state becomes * { state: true } fields. The block takes no props and fires no events; search, * status filter, bulk selection, and the details sheet are internal. * * Two deliberate departures: (1) the orders grid uses a native <table> with * the registry Table part classes copied verbatim — <uip-table>'s data-driven * API can't express per-row/per-cell custom classes, badges, or row clicks; * (2) the search icon moves from an absolute overlay into <uip-input>'s * prefix slot (same visual result, no overlay positioning to maintain). */ export class UipOrderManagementPage 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 = { search: { state: true }, statusFilter: { state: true }, selected: { state: true }, selectedOrder: { state: true }, drawerOpen: { state: true }, } private search = '' private statusFilter: 'all' | OrderStatus = 'all' private selected: Set<string> = new Set() private selectedOrder: Order | null = null private drawerOpen = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'order-management-page') } private get filteredOrders(): Order[] { const query = this.search.trim().toLowerCase() return orders.filter((order) => { const matchesStatus = this.statusFilter === 'all' || order.status === this.statusFilter const matchesQuery = !query || order.orderNumber.toLowerCase().includes(query) || order.customer.name.toLowerCase().includes(query) || order.customer.email.toLowerCase().includes(query) return matchesStatus && matchesQuery }) } private get statusCounts(): Record<string, number> { const counts: Record<string, number> = { all: orders.length, paid: 0, processing: 0, shipped: 0, refunded: 0 } for (const ord of orders) counts[ord.status] = (counts[ord.status] || 0) + 1 return counts } private toggleAll(checked: boolean) { this.selected = checked ? new Set(this.filteredOrders.map((ord) => ord.id)) : new Set() } private toggleRow(id: string, checked: boolean) { const next = new Set(this.selected) if (checked) next.add(id) else next.delete(id) this.selected = next } private clearSelection() { this.selected = new Set() } private openOrderDetails(order: Order) { this.selectedOrder = order this.drawerOpen = true } render() { const filteredOrders = this.filteredOrders const counts = this.statusCounts const statusOptions = [ { value: 'all' as const, label: 'All', count: counts.all }, { value: 'paid' as const, label: 'Paid', count: counts.paid }, { value: 'processing' as const, label: 'Processing', count: counts.processing }, { value: 'shipped' as const, label: 'Shipped', count: counts.shipped }, { value: 'refunded' as const, label: 'Refunded', count: counts.refunded }, ] const allSelected = filteredOrders.length > 0 && filteredOrders.every((ord) => this.selected.has(ord.id)) const someSelected = filteredOrders.some((ord) => this.selected.has(ord.id)) const order = this.selectedOrder return html` <div data-slot="order-management-page" class="w-full space-y-6"> <div class="flex flex-wrap items-center justify-between gap-4"> <div> <h2 class="text-foreground text-2xl font-bold tracking-tight">Orders</h2> <p class="text-muted-foreground mt-1 text-sm">Manage and track customer purchases</p> </div> <div class="flex items-center gap-2"> <uip-button aria-label="Download attachment" variant="outline" size="sm" class="text-xs"> ${icon(Download, 'download', 'mr-1.5 size-3.5')} Export CSV </uip-button> <uip-button size="sm" class="text-xs"> ${icon(Plus, 'plus', 'mr-1.5 size-3.5')} New Order </uip-button> </div> </div> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <uip-card class="shadow-xs"> <uip-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Total Orders</p> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> ${icon(ShoppingCart, 'shopping-cart', 'size-4')} </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">1,429</span> <span class="text-success inline-flex items-center gap-0.5 text-xs font-medium"> ${icon(TrendingUp, 'trending-up', 'size-3.5')} +8.2% </span> </div> <p class="text-muted-foreground mt-1 text-xs">vs. previous 30 days</p> </uip-card-content> </uip-card> <uip-card class="shadow-xs"> <uip-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Pending Fulfillment</p> <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> ${icon(Clock, 'clock', 'size-4')} </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">28</span> <span class="bg-warning/10 text-warning inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"> 12 urgent </span> </div> <p class="text-muted-foreground mt-1 text-xs">Requires warehouse dispatch</p> </uip-card-content> </uip-card> <uip-card class="shadow-xs"> <uip-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Processing</p> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md"> ${icon(PackageCheck, 'package-check', 'size-4')} </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">45</span> <span class="bg-info/10 text-info inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"> In queue </span> </div> <p class="text-muted-foreground mt-1 text-xs">Packaging & label generation</p> </uip-card-content> </uip-card> <uip-card class="shadow-xs"> <uip-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Total Revenue</p> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> ${icon(DollarSign, 'dollar-sign', 'size-4')} </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$128,450</span> <span class="text-success inline-flex items-center gap-0.5 text-xs font-medium"> ${icon(ArrowUpRight, 'arrow-up-right', 'size-3.5')} +14.5% </span> </div> <p class="text-muted-foreground mt-1 text-xs">vs. previous period</p> </uip-card-content> </uip-card> </div> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <div class="flex flex-1 flex-wrap items-center gap-2"> <uip-input .value=${this.search} placeholder="Search order ID, customer..." class="h-9 min-w-[240px] flex-1 text-xs sm:max-w-xs" @input=${(e: Event) => { this.search = (e.target as UipInput).value ?? '' }} > <span slot="prefix">${icon(Search, 'search', 'size-4')}</span> </uip-input> <div class="bg-muted/60 flex items-center gap-1 rounded-lg border p-1"> ${statusOptions.map( (status) => html`<button type="button" class=${cn( 'rounded-md px-2.5 py-1 text-xs font-medium transition-colors', this.statusFilter === status.value ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} @click=${() => { this.statusFilter = status.value }} > ${status.label} ${status.count !== undefined ? html`<span class=${cn( 'ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-normal tabular-nums', this.statusFilter === status.value ? 'bg-muted text-foreground' : 'text-muted-foreground', )} > ${status.count} </span>` : nothing} </button>`, )} </div> </div> <div class="flex items-center gap-2"> <uip-button variant="outline" size="sm" class="text-xs font-normal"> ${icon(Calendar, 'calendar', 'text-muted-foreground mr-1.5 size-3.5')} Last 30 days ${icon(ChevronDown, 'chevron-down', 'text-muted-foreground ml-1.5 size-3.5')} </uip-button> </div> </div> ${this.selected.size > 0 ? html`<div class="bg-muted/50 flex flex-wrap items-center justify-between gap-2 rounded-lg border px-4 py-2.5 shadow-xs"> <div class="flex items-center gap-2"> <span class="bg-primary text-primary-foreground flex size-5 items-center justify-center rounded-full text-xs font-medium"> ${this.selected.size} </span> <span class="text-foreground text-sm font-medium"> ${this.selected.size} ${this.selected.size === 1 ? 'order' : 'orders'} selected </span> </div> <div class="flex items-center gap-2"> <uip-button aria-label="Download attachment" variant="outline" size="sm" class="text-xs" @click=${() => this.clearSelection()} > ${icon(Download, 'download', 'mr-1.5 size-3.5')} Export selected </uip-button> <uip-button variant="outline" size="sm" class="text-xs" @click=${() => this.clearSelection()}> ${icon(Truck, 'truck', 'mr-1.5 size-3.5')} Mark as shipped </uip-button> <uip-button variant="ghost" size="sm" class="text-destructive hover:text-destructive text-xs" @click=${() => this.clearSelection()} > Cancel </uip-button> </div> </div>` : nothing} <div data-uipkge="" data-slot="table-container" class="bg-card relative w-full overflow-auto rounded-lg border shadow-xs"> <table data-uipkge="" data-slot="table" class="w-full caption-bottom text-sm"> <thead data-uipkge="" data-slot="table-header" class="bg-muted/50 [&_tr]:border-b"> <tr data-uipkge="" data-slot="table-row" class="border-b transition-colors duration-150 hover:bg-transparent"> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] w-10" > <uip-checkbox .checked=${allSelected} ?indeterminate=${!allSelected && someSelected} aria-label="Select all orders" @checked-change=${(e: CustomEvent<{ checked: boolean }>) => this.toggleAll(e.detail.checked)} ></uip-checkbox> </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] min-w-[110px]" > Order ID </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] min-w-[100px]" > Date </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] min-w-[200px]" > Customer </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] min-w-[220px]" > Items </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] min-w-[90px] text-right" > Total </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] min-w-[120px]" > Fulfillment </th> <th data-uipkge="" data-slot="table-head" class="text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] w-10 text-right" > <span class="sr-only">Actions</span> </th> </tr> </thead> <tbody data-uipkge="" data-slot="table-body" class="[&_tr:last-child]:border-0"> ${filteredOrders.map( (ord) => html`<tr data-uipkge="" data-slot="table-row" class="data-[state=selected]:bg-muted border-b transition-colors duration-150 group hover:bg-muted/40 cursor-pointer" @click=${() => this.openOrderDetails(ord)} > <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] w-10" @click=${(e: Event) => e.stopPropagation()} > <uip-checkbox .checked=${this.selected.has(ord.id)} aria-label=${`Select order ${ord.orderNumber}`} @checked-change=${(e: CustomEvent<{ checked: boolean }>) => this.toggleRow(ord.id, e.detail.checked)} ></uip-checkbox> </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" > <span class="text-foreground font-mono text-xs font-semibold hover:underline">${ord.orderNumber}</span> </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-muted-foreground text-xs tabular-nums" > ${ord.date} </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" > <div class="flex items-center gap-2.5"> <uip-avatar class="size-7"> <uip-avatar-fallback class="text-xs font-medium">${initials(ord.customer.name)}</uip-avatar-fallback> </uip-avatar> <div class="min-w-0"> <p class="text-foreground truncate text-sm font-medium">${ord.customer.name}</p> <p class="text-muted-foreground truncate text-xs">${ord.customer.email}</p> </div> </div> </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" > <div class="text-xs"> <span class="text-foreground font-medium"> ${ord.items.reduce((acc, item) => acc + item.quantity, 0)} ${ord.items.reduce((acc, item) => acc + item.quantity, 0) === 1 ? 'item' : 'items'} </span> <span class="text-muted-foreground ml-1.5 inline-block max-w-[180px] truncate align-bottom"> • ${ord.items[0]?.name}${ord.items.length > 1 ? ` +${ord.items.length - 1} more` : ''} </span> </div> </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-foreground text-right font-mono text-xs font-semibold tabular-nums" > $${formatCurrency(ord.total)} </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" > <uip-badge variant="outline" class=${cn('gap-1.5 px-2 py-0.5 text-xs font-medium capitalize', statusBadgeConfig[ord.status].class)} > <span class=${cn('size-1.5 rounded-full', statusBadgeConfig[ord.status].dot)} aria-hidden="true"></span> ${statusBadgeConfig[ord.status].label} </uip-badge> </td> <td data-uipkge="" data-slot="table-cell" class="px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-right" @click=${(e: Event) => e.stopPropagation()} > <uip-dropdown-menu class="[&::part(content)]:w-48 [&::part(refund)]:text-destructive"> <uip-button slot="trigger" variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground"> ${icon(MoreHorizontal, 'ellipsis', 'size-4')} <span class="sr-only">Actions for ${ord.orderNumber}</span> </uip-button> <uip-dropdown-menu-content align="end"> <uip-dropdown-menu-label>Order actions</uip-dropdown-menu-label> <uip-dropdown-menu-item @select=${() => this.openOrderDetails(ord)}> ${icon(Eye, 'eye', 'mr-2 size-4')} View details </uip-dropdown-menu-item> <uip-dropdown-menu-item> ${icon(FileText, 'file-text', 'mr-2 size-4')} Download invoice </uip-dropdown-menu-item> ${ord.status === 'processing' ? html`<uip-dropdown-menu-item> ${icon(Truck, 'truck', 'mr-2 size-4')} Mark as shipped </uip-dropdown-menu-item>` : nothing} <uip-dropdown-menu-separator></uip-dropdown-menu-separator> ${ord.status !== 'refunded' ? html`<uip-dropdown-menu-item part="refund"> ${icon(RotateCcw, 'rotate-ccw', 'mr-2 size-4')} Issue refund </uip-dropdown-menu-item>` : nothing} </uip-dropdown-menu-content> </uip-dropdown-menu> </td> </tr>`, )} ${filteredOrders.length === 0 ? html`<tr data-uipkge="" data-slot="table-row" class="border-b transition-colors duration-150"> <td data-uipkge="" data-slot="table-cell" colSpan="8" class="px-3 py-2 align-middle text-sm whitespace-nowrap text-muted-foreground h-32 text-center text-sm" > No orders match your filter criteria. </td> </tr>` : nothing} </tbody> </table> </div> <div class="flex flex-wrap items-center justify-between gap-4"> <p class="text-muted-foreground text-xs tabular-nums">Showing 1-${filteredOrders.length} of 1,429 orders</p> <div class="flex items-center gap-2"> <uip-button variant="outline" size="sm" class="text-xs" disabled>Previous</uip-button> <uip-button variant="outline" size="sm" class="text-xs">Next</uip-button> </div> </div> <uip-sheet side="right" ?open=${this.drawerOpen} heading=${order?.orderNumber ?? ''} description=${order ? `Placed on ${order.date}` : ''} @open-change=${(e: CustomEvent<{ open: boolean }>) => { this.drawerOpen = e.detail.open }} > ${order ? html`<uip-sheet-body> <div class="flex h-full flex-col"> <div class="flex items-center justify-between pr-6"> <span class="text-foreground font-mono text-base font-bold">${order.orderNumber}</span> <uip-badge variant="outline" class=${cn('gap-1.5 px-2.5 py-0.5 text-xs font-medium capitalize', statusBadgeConfig[order.status].class)} > <span class=${cn('size-1.5 rounded-full', statusBadgeConfig[order.status].dot)} aria-hidden="true"></span> ${statusBadgeConfig[order.status].label} </uip-badge> </div> <div class="flex-1 space-y-6 overflow-y-auto py-5 text-sm"> <div class="bg-muted/20 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Customer & Shipping</h4> <span class="text-muted-foreground font-mono text-xs">${order.paymentMethod}</span> </div> <div class="flex items-center gap-3"> <uip-avatar class="size-9"> <uip-avatar-fallback class="text-xs font-semibold">${initials(order.customer.name)}</uip-avatar-fallback> </uip-avatar> <div class="min-w-0"> <p class="text-foreground font-medium">${order.customer.name}</p> <p class="text-muted-foreground text-xs">${order.customer.email}</p> </div> </div> <div class="text-muted-foreground border-t pt-2.5 text-xs"> <p class="text-foreground font-medium">Delivery address:</p> <p>${order.customer.address}</p> <p>${order.customer.city}, ${order.customer.state} ${order.customer.zip}</p> </div> </div> ${order.carrier && order.trackingNumber ? html`<div class="border-border bg-card space-y-2 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Fulfillment</span> <uip-badge variant="secondary" class="text-xs font-normal">In Transit</uip-badge> </div> <div class="flex items-center justify-between pt-1 text-xs"> <span class="text-muted-foreground">Carrier:</span> <span class="text-foreground font-medium">${order.carrier}</span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Tracking ID:</span> <span class="text-foreground font-mono font-medium">${order.trackingNumber}</span> </div> </div>` : nothing} <div class="space-y-3"> <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Line Items</h4> <div class="divide-border bg-card divide-y rounded-lg border"> ${order.items.map( (item) => html`<div class="flex items-center justify-between gap-3 p-3.5"> <div class="min-w-0 flex-1"> <p class="text-foreground truncate text-sm font-medium">${item.name}</p> <p class="text-muted-foreground font-mono text-xs"> SKU: ${item.sku} • Qty: ${item.quantity} × $${formatCurrency(item.price)} </p> </div> <div class="text-foreground font-mono text-xs font-semibold tabular-nums"> $${formatCurrency(item.price * item.quantity)} </div> </div>`, )} </div> </div> <div class="bg-card space-y-2 rounded-lg border p-4 text-xs"> <div class="text-muted-foreground flex justify-between"> <span>Subtotal</span> <span class="text-foreground font-mono tabular-nums">$${formatCurrency(order.subtotal)}</span> </div> <div class="text-muted-foreground flex justify-between"> <span>Estimated Shipping</span> <span class="text-foreground font-mono tabular-nums" >${order.shipping === 0 ? 'Free' : `$${formatCurrency(order.shipping)}`}</span > </div> <div class="text-muted-foreground flex justify-between"> <span>Tax</span> <span class="text-foreground font-mono tabular-nums">$${formatCurrency(order.tax)}</span> </div> <div class="text-foreground flex justify-between border-t pt-2 text-sm font-medium"> <span>Total</span> <span class="text-foreground font-mono font-bold tabular-nums">$${formatCurrency(order.total)}</span> </div> </div> </div> </div> </uip-sheet-body>` : nothing} <uip-button slot="footer" aria-label="Download attachment" variant="outline" size="sm" class="text-xs"> ${icon(Download, 'download', 'mr-1.5 size-3.5')} Invoice PDF </uip-button> <uip-button slot="footer" variant="secondary" size="sm" class="text-xs" data-sheet-close>Close</uip-button> </uip-sheet> </div> ` } } customElements.get('uip-order-management-page') || customElements.define('uip-order-management-page', UipOrderManagementPage) declare global { interface HTMLElementTagNameMap { 'uip-order-management-page': UipOrderManagementPage } }
Raw manifest:https://uipkge.dev/r/lit/order-management-page.json