Menu

Change language

GitHub repo

Record Detail Page

blocklayout

Admin 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

$npx shadcn@latest add https://uipkge.dev/r/lit/record-detail-page.json
Manifest:https://uipkge.dev/r/lit/record-detail-page.jsonInstalls to:src/uip/blocks/record-detail-page/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

DataListRow
interface DataListRow {
  label: string
  value: string
}
Section
interface Section {
  heading: string
  icon: IconNode
  iconName: string
  rows: DataListRow[]
}
ActivityEvent
interface ActivityEvent {
  icon: IconNode
  iconName: string
  sentence: string
  time: string
}
Order
interface Order {
  id: string
  amount: string
  status: 'Paid' | 'Pending' | 'Refunded'
}
HTMLElementTagNameMap
interface 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