{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "type": "registry:ui",
  "description": "Plain HTML table primitives — `<Table>`, `<TableHeader>`, `<TableRow>`, `<TableCell>` — with the registry’s borders, padding, and tokens already applied. Use this when Data Table is too heavy.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/table/table.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\nexport type TableDensity = 'compact' | 'cozy' | 'comfortable'\n\n/** One column: `key` reads `row[key]` and names the column's parts (`head-<key>`, `cell-<key>`). */\nexport interface TableColumn {\n  key: string\n  /** Header text (defaults to `key`). Rich markup: slot=\"head-<key>\". */\n  header?: string\n}\n\n/** A footer cell (React's `<TableFooter><TableRow><TableCell colSpan>`). */\nexport interface TableFooterCell {\n  value?: string\n  colSpan?: number\n}\n\nexport type TableRowData = Record<string, unknown>\n\n/**\n * <uip-table> — the registry Table as ONE data-driven web component.\n *\n * Why data-driven: the HTML parser drops or re-parents custom elements placed\n * between <table>/<tr>/<td>, so `<uip-table-row>`-style parts can't keep\n * table semantics; and a light-DOM <table> the element merely decorates\n * would be styled by the page's CSS, not the shared shadow sheet. So the\n * whole <table> (container > table > caption/thead/tbody/tfoot > tr > th/td)\n * is rendered in one shadow root from `columns` / `rows` / `footer`, with\n * React's class strings and data-slot attributes. Native table semantics\n * (roles, headers, colspan) are the browser's own.\n *\n * Rich cell content (badges, links, checkboxes) is slotted light DOM, so it\n * keeps the page's styles and framework bindings:\n *   slot=\"head-<key>\"            header cell\n *   slot=\"cell-<rowIndex>-<key>\" body cell (fallback: String(row[key]))\n *   slot=\"footer-<r>-<c>\"        footer cell (fallback: cell.value)\n *   slot=\"caption\"               caption (fallback: `caption`)\n *   slot=\"empty\"                 empty-state row, shown when `rows` is empty (fallback: `empty`) —\n *                                React's \"Empty\" story row: one spanning cell,\n *                                `text-muted-foreground h-24 text-center`, no row hover\n *\n * Properties: `density`, `caption`, `empty`.\n *\n * React's per-element `className`s become shadow parts, styled from the page\n * with `class=\"[&::part(<name>)]:…\"` on the host:\n *   container (React's `containerClassName`), base (<table>, React's\n *   `<Table className>`), caption, header (<thead>), body (<tbody>),\n *   footer (<tfoot>), row (every body <tr>, plus `row-odd` / `row-even` —\n *   `::part()` can't take `:nth-child`, so zebra striping is\n *   `[&::part(row-odd)]:bg-muted/40`), head (every <th>) + `head-<key>`,\n *   cell (every body / footer <td>) + `cell-<key>` for body cells and\n *   `footer-cell` + `footer-cell-<index>` for footer cells, and `empty-row` /\n *   `empty-cell` for the empty-state row.\n * E.g. a right-aligned amount column:\n *   `[&::part(head-amount)]:text-right [&::part(cell-amount)]:text-right`.\n */\nexport class UipTable extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    columns: { type: Array },\n    rows: { type: Array },\n    footer: { type: Array },\n    caption: {},\n    empty: {},\n    density: { reflect: true },\n  }\n\n  columns: TableColumn[] = []\n  rows: TableRowData[] = []\n  footer: TableFooterCell[][] = []\n  caption?: string\n  empty?: string\n  density?: TableDensity\n  private childObserver?: MutationObserver\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'table')\n    // Caption / empty slots decide whether their wrappers render at all.\n    this.childObserver = new MutationObserver(() => this.requestUpdate())\n    this.childObserver.observe(this, { childList: true, attributes: true, attributeFilter: ['slot'], subtree: true })\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.childObserver?.disconnect()\n  }\n\n  private hasSlotted(name: string) {\n    return [...this.children].some((c) => c.getAttribute('slot') === name)\n  }\n\n  render() {\n    const densityClass =\n      this.density === 'compact'\n        ? '[&_td]:py-1.5 [&_td]:text-xs [&_th]:h-8 [&_th]:text-xs'\n        : this.density === 'comfortable'\n          ? '[&_td]:py-3 [&_th]:h-12'\n          : // cozy is the TableCell/TableHead baseline (py-2 / h-10) -- no override.\n            ''\n    const rowClasses = 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors duration-150'\n    const headClasses =\n      '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]'\n    const cellClasses =\n      'px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]'\n    const showCaption = this.caption != null || this.hasSlotted('caption')\n    const showEmpty = this.rows.length === 0 && (this.empty != null || this.hasSlotted('empty'))\n\n    return html`<div\n      part=\"container\"\n      data-uipkge=\"\"\n      data-slot=\"table-container\"\n      class=\"relative w-full overflow-auto\"\n    >\n      <table part=\"base\" data-uipkge=\"\" data-slot=\"table\" class=${cn('w-full caption-bottom text-sm', densityClass)}>\n        ${showCaption\n          ? html`<caption part=\"caption\" data-uipkge=\"\" data-slot=\"table-caption\" class=\"text-muted-foreground mt-4 text-sm\">\n              <slot name=\"caption\">${this.caption ?? nothing}</slot>\n            </caption>`\n          : nothing}\n        ${this.columns.length\n          ? html`<thead part=\"header\" data-uipkge=\"\" data-slot=\"table-header\" class=\"bg-muted/50 [&_tr]:border-b\">\n              <tr data-uipkge=\"\" data-slot=\"table-row\" class=${rowClasses}>\n                ${this.columns.map(\n                  (c) => html`<th part=${`head head-${c.key}`} data-uipkge=\"\" data-slot=\"table-head\" class=${headClasses}>\n                    <slot name=${`head-${c.key}`}>${c.header ?? c.key}</slot>\n                  </th>`,\n                )}\n              </tr>\n            </thead>`\n          : nothing}\n        <tbody part=\"body\" data-uipkge=\"\" data-slot=\"table-body\" class=\"[&_tr:last-child]:border-0\">\n          ${this.rows.map(\n            (row, r) => html`<tr part=${`row ${r % 2 === 0 ? 'row-odd' : 'row-even'}`} data-uipkge=\"\" data-slot=\"table-row\" class=${rowClasses}>\n              ${this.columns.map(\n                (c) => html`<td part=${`cell cell-${c.key}`} data-uipkge=\"\" data-slot=\"table-cell\" class=${cellClasses}>\n                  <slot name=${`cell-${r}-${c.key}`}>${row[c.key] == null ? nothing : String(row[c.key])}</slot>\n                </td>`,\n              )}\n            </tr>`,\n          )}\n          ${showEmpty\n            ? html`<tr part=\"empty-row\" data-uipkge=\"\" data-slot=\"table-row\" class=${cn(rowClasses, 'hover:bg-transparent')}>\n                <td\n                  part=\"empty-cell\"\n                  data-uipkge=\"\"\n                  data-slot=\"table-cell\"\n                  colspan=${Math.max(1, this.columns.length)}\n                  class=${cn(cellClasses, 'text-muted-foreground h-24 text-center text-sm')}\n                >\n                  <slot name=\"empty\">${this.empty ?? nothing}</slot>\n                </td>\n              </tr>`\n            : nothing}\n        </tbody>\n        ${this.footer.length\n          ? html`<tfoot\n              part=\"footer\"\n              data-uipkge=\"\"\n              data-slot=\"table-footer\"\n              class=\"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\"\n            >\n              ${this.footer.map(\n                (cells, r) => html`<tr data-uipkge=\"\" data-slot=\"table-row\" class=${rowClasses}>\n                  ${cells.map(\n                    (cell, i) => html`<td\n                      part=${`cell footer-cell footer-cell-${i}`}\n                      data-uipkge=\"\"\n                      data-slot=\"table-cell\"\n                      colspan=${cell.colSpan ?? nothing}\n                      class=${cellClasses}\n                    >\n                      <slot name=${`footer-${r}-${i}`}>${cell.value ?? nothing}</slot>\n                    </td>`,\n                  )}\n                </tr>`,\n              )}\n            </tfoot>`\n          : nothing}\n      </table>\n    </div>`\n  }\n}\n\ncustomElements.get('uip-table') || customElements.define('uip-table', UipTable)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-table': UipTable\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/table/table.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
