Data display
Table code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/table.blade.php
@props([
'striped' => false,
'stackable' => false,
'flush' => false,
'sticky' => false,
'maxHeight' => '24rem',
])
@php
/*
| Responsive table shell. Compose with x-ui.table.head, x-ui.table.row,
| x-ui.table.heading and x-ui.table.cell.
|
| Set `flush` to drop the outer border/rounding when embedding inside a card.
| Set `sticky` to pin the header while the body scrolls (bounded by
| `maxHeight`). Pass `resizable` to individual headings for drag-to-resize
| columns, and `expandable` to rows for a toggleable detail row.
|
| Usage:
| <x-ui.table>
| <x-slot:head>
| <x-ui.table.heading>Name</x-ui.table.heading>
| </x-slot:head>
| <x-ui.table.row><x-ui.table.cell>Ada</x-ui.table.cell></x-ui.table.row>
| </x-ui.table>
|
| <x-ui.table sticky max-height="20rem">...</x-ui.table>
|
| <x-ui.card padding="none">
| <x-ui.card.header title="Team" />
| <x-ui.table flush>...</x-ui.table>
| </x-ui.card>
*/
$wrapperClass = ($sticky ? 'overflow-auto' : 'overflow-x-auto').($stackable ? ' max-md:overflow-visible' : '');
if (! $flush) {
$wrapperClass .= ' rounded-2xl border border-gray-200 dark:border-gray-800';
}
$stickyClass = $sticky
? ' [&_thead_th]:sticky [&_thead_th]:top-0 [&_thead_th]:z-10 [&_thead_th]:bg-gray-50 dark:[&_thead_th]:bg-gray-900 [&_thead]:shadow-[0_1px_0_0_var(--color-gray-200)] dark:[&_thead]:shadow-[0_1px_0_0_var(--color-gray-800)]'
: '';
@endphp
<div {{ $attributes->merge(['class' => $wrapperClass]) }} @if ($sticky) style="max-height: {{ $maxHeight }}" @endif>
<table
@if ($stackable) data-stackable @endif
class="min-w-full divide-y divide-gray-200 dark:divide-gray-800 {{ $stackable ? 'max-md:block' : '' }} {{ $striped ? '[&_tbody_tr:nth-child(odd)]:bg-gray-50/60 dark:[&_tbody_tr:nth-child(odd)]:bg-gray-800/30' : '' }}{{ $stickyClass }}"
>
@isset($head)
<thead class="bg-gray-50 dark:bg-gray-900/60 {{ $stackable ? 'hidden md:table-header-group' : '' }}">
<tr>{{ $head }}</tr>
</thead>
@endisset
<tbody class="divide-y divide-gray-200 dark:divide-gray-800 bg-white dark:bg-gray-900 {{ $stackable ? 'max-md:block max-md:divide-y-0' : '' }}">
{{ $slot }}
</tbody>
</table>
</div>
resources/views/components/ui/table/cell.blade.php
@props([
'align' => 'left',
'muted' => false,
'label' => null,
'summary' => false,
'stackable' => false,
])
@php
/* Table body cell. align: left, center, right */
$alignClass = ['left' => 'text-left', 'center' => 'text-center', 'right' => 'text-right'][$align] ?? 'text-left';
$color = $muted ? 'text-gray-500 dark:text-gray-400' : 'text-gray-700 dark:text-gray-200';
$justify = ['left' => 'justify-start', 'center' => 'justify-center', 'right' => 'justify-end'][$align] ?? 'justify-start';
@endphp
@if ($stackable && $summary)
<td
x-on:click="if (window.matchMedia('(max-width: 767px)').matches) expanded = ! expanded"
x-bind:aria-expanded="expanded"
{{ $attributes->merge(['class' => "block w-full cursor-pointer px-5 py-4 text-sm md:cursor-default md:table-cell md:px-6 md:whitespace-nowrap {$alignClass} {$color}"]) }}
>
<div class="flex items-center justify-between gap-3 md:block">
<div class="min-w-0 flex-1">
{{ $slot }}
</div>
<x-ui.icon
name="caret-down"
weight="bold"
size="sm"
class="shrink-0 text-gray-400 transition-transform duration-200 md:hidden"
x-bind:class="expanded && 'rotate-180'"
/>
</div>
</td>
@elseif ($stackable)
<td
x-on:click.stop
{{ $attributes->merge(['class' => 'max-md:hidden max-md:group-data-[expanded=true]:block text-sm md:table-cell md:whitespace-nowrap md:px-6 md:py-4']) }}
>
<div class="flex items-center justify-between gap-4 border-t border-gray-100 px-5 py-3 md:contents md:border-0 md:px-0 md:py-0 dark:border-gray-800">
@if ($label)
<span class="shrink-0 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 md:hidden">
{{ $label }}
</span>
@endif
<div class="flex min-w-0 flex-1 items-center gap-2 {{ $justify }} md:block {{ $alignClass }} {{ $color }}">
{{ $slot }}
</div>
</div>
</td>
@else
<td {{ $attributes->merge(['class' => "px-6 py-4 text-sm whitespace-nowrap {$alignClass} {$color}"]) }}>
{{ $slot }}
</td>
@endif
resources/views/components/ui/table/heading.blade.php
@props([
'align' => 'left',
'sortable' => false,
'sort' => null,
'href' => null,
'resizable' => false,
])
@php
/*
| Table column header cell.
|
| align: left, center, right
| sortable: render a sort affordance with an indicator
| sort: current direction for this column — 'asc', 'desc' or null
| href: link to toggle sorting (falls back to a plain button)
| resizable: add a drag handle on the trailing edge to resize the column
|
| Usage:
| <x-ui.table.heading>Name</x-ui.table.heading>
| <x-ui.table.heading sortable sort="asc" :href="route('admin.users.index', ['sort' => 'name'])">Name</x-ui.table.heading>
| <x-ui.table.heading resizable>Name</x-ui.table.heading>
*/
$alignClass = ['left' => 'text-left', 'center' => 'text-center', 'right' => 'text-right'][$align] ?? 'text-left';
$sortIcon = match ($sort) {
'asc' => 'caret-up',
'desc' => 'caret-down',
default => 'arrows-down-up',
};
$justify = ['left' => 'justify-start', 'center' => 'justify-center', 'right' => 'justify-end'][$align] ?? 'justify-start';
@endphp
<th
scope="col"
@if ($resizable)
x-data="{
begin(event) {
const th = this.$el;
const startX = event.clientX;
const startWidth = th.offsetWidth;
const move = (e) => {
th.style.width = Math.max(64, startWidth + e.clientX - startX) + 'px';
};
const up = () => {
window.removeEventListener('mousemove', move);
window.removeEventListener('mouseup', up);
};
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', up);
},
}"
@endif
{{ $attributes->merge(['class' => "px-6 py-3.5 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 {$alignClass}".($resizable ? ' relative' : '')]) }}
>
@if ($sortable)
<{{ $href ? 'a' : 'button' }}
@if ($href) href="{{ $href }}" @else type="button" @endif
class="group inline-flex items-center gap-1.5 {{ $justify }} font-semibold uppercase tracking-wider transition hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer"
>
{{ $slot }}
<x-ui.icon
:name="$sortIcon"
weight="bold"
size="xs"
class="{{ $sort ? 'text-brand-500' : 'text-gray-300 dark:text-gray-600 group-hover:text-gray-400' }}"
/>
</{{ $href ? 'a' : 'button' }}>
@else
{{ $slot }}
@endif
@if ($resizable)
<span
@mousedown.prevent="begin($event)"
class="absolute inset-y-0 -end-1 z-10 w-2 cursor-col-resize select-none rounded transition-colors hover:bg-brand-400/50"
aria-hidden="true"
></span>
@endif
</th>
resources/views/components/ui/table/row.blade.php
@props([
'hover' => true,
'stackable' => false,
'expandable' => false,
'colspan' => 100,
])
@php
/*
| Table body row. Set `expandable` and provide an `expansion` slot to reveal
| a full-width detail row on click; a caret cell is appended to the main row
| automatically.
|
| Usage:
| <x-ui.table.row expandable>
| <x-ui.table.cell>Ada</x-ui.table.cell>
| <x-slot:expansion>Extra detail about Ada.</x-slot:expansion>
| </x-ui.table.row>
*/
$hoverClass = $hover ? 'md:hover:bg-gray-50 md:dark:hover:bg-gray-800/40' : '';
@endphp
@if ($expandable)
@php
$rowKey = 'expand-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10));
$detailId = $rowKey.'-detail';
@endphp
<tr
x-data="{
expanded: false,
toggle() {
this.expanded = ! this.expanded;
this.$dispatch('{{ $rowKey }}', { expanded: this.expanded });
},
}"
x-on:click="toggle()"
{{ $attributes->merge(['class' => 'cursor-pointer transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/40']) }}
>
{{ $slot }}
<td class="w-10 pe-4 text-end">
<button
type="button"
aria-controls="{{ $detailId }}"
aria-expanded="false"
x-bind:aria-expanded="expanded.toString()"
class="inline-flex cursor-pointer items-center justify-center rounded p-1 text-gray-400 transition-colors hover:text-gray-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 dark:hover:text-gray-200"
>
<span class="sr-only">Toggle row details</span>
<x-ui.icon
name="caret-down"
weight="bold"
size="sm"
class="inline-block transition-transform duration-200"
x-bind:class="expanded && 'rotate-180'"
/>
</button>
</td>
</tr>
<tr
id="{{ $detailId }}"
x-data="{ show: false }"
x-on:{{ $rowKey }}.window="show = $event.detail.expanded"
x-show="show"
x-cloak
class="bg-gray-50/70 dark:bg-gray-800/30"
>
<td colspan="{{ (int) $colspan }}" class="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
{{ $expansion ?? '' }}
</td>
</tr>
@else
<tr
@if ($stackable)
x-data="{ expanded: false }"
x-bind:data-expanded="expanded ? 'true' : 'false'"
@endif
{{ $attributes->merge([
'class' => $stackable
? "group block border-b border-gray-200 transition-colors md:table-row md:border-0 dark:border-gray-800 {$hoverClass}"
: ($hover ? 'transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/40' : ''),
]) }}
>
{{ $slot }}
</tr>
@endif