Record Detail Page
blocklayoutAdmin record detail page: breadcrumb header with status badge and Edit / Delete actions, two-column layout with a summary card (avatar, identity, key metrics), Details / Activity tabs (labeled description-list sections and an icon timeline feed), plus a side panel with owner metadata, relative timestamps, tag badges, and a recent-orders mini list. All data is hardcoded inline for you to replace.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/record-detail-page.json$npx shadcn@latest add https://uipkge.dev/r/lit/record-detail-page.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/record-detail-page.json$bunx shadcn@latest add https://uipkge.dev/r/lit/record-detail-page.jsonManifest:
https://uipkge.dev/r/lit/record-detail-page.jsonInstalls to:src/uip/blocks/record-detail-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.
DataListRowinterface DataListRow {
label: string
value: string
}Sectioninterface Section {
heading: string
icon: IconNode
iconName: string
rows: DataListRow[]
}ActivityEventinterface ActivityEvent {
icon: IconNode
iconName: string
sentence: string
time: string
}Orderinterface Order {
id: string
amount: string
status: 'Paid' | 'Pending' | 'Refunded'
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-record-detail-page': UipRecordDetailPage
}
}Files installed (1)
src/uip/blocks/record-detail-page/record-detail-page.ts12.2 kB
import { LitElement, css, html } from 'lit' import { Building2, ChevronRight, CreditCard, FileText, Mail, MessageSquare, Pencil, Plus, ReceiptText, Trash2, UserPlus, type IconNode, } from 'lucide' 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/separator/separator' import '../../components/tabs/tabs' interface DataListRow { label: string value: string } interface Section { heading: string icon: IconNode iconName: string rows: DataListRow[] } interface ActivityEvent { icon: IconNode iconName: string sentence: string time: string } interface Order { id: string amount: string status: 'Paid' | 'Pending' | 'Refunded' } const summaryRows: DataListRow[] = [ { label: 'Account ID', value: 'CUS-004182' }, { label: 'Plan', value: 'Business annual' }, { label: 'Lifetime value', value: '$48,200' }, ] const sections: Section[] = [ { heading: 'Contact', icon: Mail, iconName: 'mail', rows: [ { label: 'Email', value: '[email protected]' }, { label: 'Phone', value: '+1 (415) 555-0134' }, { label: 'Website', value: 'acmecorp.com' }, { label: 'Address', value: '500 Market St, San Francisco, CA' }, ], }, { heading: 'Company', icon: Building2, iconName: 'building-2', rows: [ { label: 'Industry', value: 'Logistics & supply chain' }, { label: 'Employees', value: '240–500' }, { label: 'Tax ID', value: 'US-84-2917365' }, { label: 'Payment terms', value: 'Net 30' }, ], }, ] const activity: ActivityEvent[] = [ { icon: ReceiptText, iconName: 'receipt-text', sentence: 'Invoice INV-2091 paid — $12,400', time: '2h ago' }, { icon: MessageSquare, iconName: 'message-square', sentence: 'Priya left a note on the renewal deal', time: 'Yesterday' }, { icon: UserPlus, iconName: 'user-plus', sentence: 'Dana Whitfield added as billing contact', time: '3d ago' }, { icon: FileText, iconName: 'file-text', sentence: 'Contract Q3-amendment.pdf uploaded', time: '1w ago' }, ] const tags = ['enterprise', 'renewal-q4', 'priority-support'] const orders: Order[] = [ { id: 'ORD-8841', amount: '$12,400.00', status: 'Paid' }, { id: 'ORD-8790', amount: '$3,180.50', status: 'Pending' }, { id: 'ORD-8612', amount: '$940.00', status: 'Refunded' }, ] /** * <uip-record-detail-page> — the registry RecordDetailPage block as a web * component. * * Class strings are React's RecordDetailPage verbatim: breadcrumb header, * summary card, Details / Activity tabs, meta + recent-orders side panel. * All data is hardcoded inline for you to replace. No props, no events: edit * the markup in place. * * Deviation: React's `className` on Card parts reaches the shadow DOM via * `[&::part(base)]:…` on the host (their utilities only exist in the shadow * sheet); the separator's `mb-4` likewise becomes * `[&::part(base)]:mb-4` (its host is `display: contents`). */ export class UipRecordDetailPage 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; }`] constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'record-detail-page') } render() { return html` <div data-slot="record-detail-page" class="bg-background text-foreground w-full"> <div class="flex flex-wrap items-center justify-between gap-4"> <div class="flex min-w-0 items-center gap-1.5"> <uip-button variant="link" size="sm" class="text-muted-foreground h-auto min-h-6 px-0"> Customers </uip-button> ${icon(ChevronRight, 'chevron-right', 'text-muted-foreground size-4 shrink-0')} <span class="truncate text-sm font-semibold">Acme Corp</span> <uip-badge variant="secondary" class="ml-2"> Active </uip-badge> </div> <div class="flex items-center gap-2"> <uip-button variant="outline" size="sm"> ${icon(Pencil, 'pencil', 'size-4')} Edit </uip-button> <uip-button variant="outline" size="sm" class="text-destructive hover:text-destructive"> ${icon(Trash2, 'trash-2', 'size-4')} Delete </uip-button> </div> </div> <div class="mt-6 grid gap-6 lg:grid-cols-3"> <div class="space-y-6 lg:col-span-2"> <uip-card> <uip-card-header class="[&::part(base)]:flex-row [&::part(base)]:items-center [&::part(base)]:gap-4 [&::part(base)]:space-y-0" > <uip-avatar class="[&::part(base)]:size-14"> <uip-avatar-image src="https://i.pravatar.cc/112?img=32" alt="Acme Corp logo"></uip-avatar-image> <uip-avatar-fallback>AC</uip-avatar-fallback> </uip-avatar> <div class="min-w-0"> <uip-card-title class="[&::part(base)]:text-lg">Acme Corp</uip-card-title> <uip-card-description class="[&::part(base)]:truncate"> [email protected] · Customer since March 2023 </uip-card-description> </div> </uip-card-header> <uip-card-content> <uip-separator class="[&::part(base)]:mb-4"></uip-separator> <dl class="grid gap-x-8 gap-y-3 sm:grid-cols-3"> ${summaryRows.map( (row) => html` <div> <dt class="text-muted-foreground text-xs">${row.label}</dt> <dd class="mt-0.5 text-sm font-medium">${row.value}</dd> </div> `, )} </dl> </uip-card-content> </uip-card> <uip-tabs value="details"> <span slot="trigger" data-value="details">Details</span> <span slot="trigger" data-value="activity">Activity</span> <div slot="content" data-value="details" class="space-y-6 pt-4"> ${sections.map( (section) => html` <section> <h3 class="flex items-center gap-2 text-sm font-semibold"> ${icon(section.icon, section.iconName, 'text-muted-foreground size-4')} ${section.heading} </h3> <dl class="border-border mt-3 divide-y rounded-lg border"> ${section.rows.map( (row) => html` <div class="px-4 py-2.5 sm:flex sm:items-center sm:gap-4"> <dt class="text-muted-foreground text-xs sm:w-36 sm:text-sm">${row.label}</dt> <dd class="mt-0.5 text-sm font-medium sm:mt-0">${row.value}</dd> </div> `, )} </dl> </section> `, )} </div> <div slot="content" data-value="activity" class="pt-4"> <ol class="before:bg-border relative space-y-6 before:absolute before:inset-y-1 before:left-[15px] before:w-px"> ${activity.map( (event) => html` <li class="relative flex items-start gap-4 pl-0"> <span class="bg-background ring-border relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full ring-1" > ${icon(event.icon, event.iconName, 'text-muted-foreground size-4')} </span> <div class="min-w-0 pt-1"> <p class="truncate text-sm">${event.sentence}</p> <time class="text-muted-foreground text-xs">${event.time}</time> </div> </li> `, )} </ol> </div> </uip-tabs> </div> <div class="space-y-6"> <uip-card> <uip-card-header> <uip-card-title class="[&::part(base)]:text-sm [&::part(base)]:font-medium">Record meta</uip-card-title> </uip-card-header> <uip-card-content class="[&::part(base)]:space-y-4"> <div class="flex items-center gap-3"> <uip-avatar class="size-8"> <uip-avatar-image src="https://i.pravatar.cc/64?img=47" alt="Priya Raman"></uip-avatar-image> <uip-avatar-fallback>PR</uip-avatar-fallback> </uip-avatar> <div class="min-w-0"> <p class="text-muted-foreground text-xs">Owner</p> <p class="truncate text-sm font-medium">Priya Raman</p> </div> </div> <uip-separator></uip-separator> <dl class="space-y-3"> <div class="flex items-center justify-between gap-4"> <dt class="text-muted-foreground text-xs">Created</dt> <dd class="text-sm font-medium">Mar 14, 2023</dd> </div> <div class="flex items-center justify-between gap-4"> <dt class="text-muted-foreground text-xs">Updated</dt> <dd class="text-sm font-medium">2 hours ago</dd> </div> </dl> <uip-separator></uip-separator> <div> <p class="text-muted-foreground mb-2 text-xs">Tags</p> <div class="flex flex-wrap gap-1.5"> ${tags.map((tag) => html`<uip-badge variant="secondary">${tag}</uip-badge>`)} </div> </div> </uip-card-content> </uip-card> <uip-card> <uip-card-header class="[&::part(base)]:flex-row [&::part(base)]:items-center [&::part(base)]:justify-between [&::part(base)]:space-y-0" > <uip-card-title class="[&::part(base)]:text-sm [&::part(base)]:font-medium">Recent orders</uip-card-title> <uip-button variant="ghost" size="icon" aria-label="View all orders"> ${icon(CreditCard, 'credit-card', 'size-4')} </uip-button> </uip-card-header> <uip-card-content> <ul class="divide-y"> ${orders.map( (order) => html` <li class="flex items-center gap-3 py-2.5 first:pt-0 last:pb-0"> <span class="font-mono text-xs">${order.id}</span> <span class="text-muted-foreground ml-auto text-sm tabular-nums">${order.amount}</span> <uip-badge variant=${order.status === 'Paid' ? 'default' : order.status === 'Pending' ? 'secondary' : 'outline'} class="[&::part(base)]:min-w-[72px] [&::part(base)]:justify-center" > ${order.status} </uip-badge> </li> `, )} </ul> <uip-button variant="outline" size="sm" class="mt-4 w-full"> ${icon(Plus, 'plus', 'size-4')} New order </uip-button> </uip-card-content> </uip-card> </div> </div> </div> ` } } customElements.get('uip-record-detail-page') || customElements.define('uip-record-detail-page', UipRecordDetailPage) declare global { interface HTMLElementTagNameMap { 'uip-record-detail-page': UipRecordDetailPage } }
Raw manifest:https://uipkge.dev/r/lit/record-detail-page.json