Skip to content
2 changes: 1 addition & 1 deletion packages/panels/dist/theme.css

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions packages/tables/resources/css/table.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,124 @@
.fi-ta-table {
@apply w-full table-auto divide-y divide-gray-200 text-start dark:divide-white/5;

&.fi-stacked-on-mobile {
@apply block sm:table sm:table-auto;

& > thead {
@apply block sm:table-header-group;

&:not(:has(.fi-ta-stacked-header-row)) {
@apply hidden sm:table-header-group;
}

& > tr:not(.fi-ta-stacked-header-row) {
@apply hidden sm:table-row;
}

& > tr > .fi-ta-header-cell {
@apply hidden sm:table-cell;
}

& > tr > .fi-ta-selection-cell {
@apply hidden sm:table-cell;
}
}

& > tbody {
@apply block whitespace-normal sm:table-row-group sm:whitespace-nowrap;
}

& > tbody > tr {
@apply relative block py-2 sm:static sm:table-row sm:py-0;

&.fi-selected {
&::before {
@apply bg-primary-600 dark:bg-primary-500 absolute inset-y-0 start-0 w-0.5 sm:hidden;
content: '';
}

& > *:first-child::before {
@apply hidden sm:block;
}
}
}

& > tbody > tr > .fi-ta-selection-cell {
@apply absolute end-5 top-0 sm:static sm:table-cell sm:w-1 sm:px-3 sm:py-4 sm:first-of-type:ps-6 sm:last-of-type:pe-6;
}

& > tbody > tr > .fi-ta-cell:not(.fi-ta-selection-cell) {
@apply block px-4 sm:table-cell sm:px-0;

&.sm\:fi-hidden {
@apply sm:hidden;
}

&.md\:fi-hidden {
@apply md:hidden;
}

&.lg\:fi-hidden {
@apply lg:hidden;
}

&.xl\:fi-hidden {
@apply xl:hidden;
}

&.\32xl\:fi-hidden {
@apply 2xl:hidden;
}

&.sm\:fi-visible {
@apply hidden sm:table-cell;
}

&.md\:fi-visible {
@apply hidden md:table-cell;
}

&.lg\:fi-visible {
@apply hidden lg:table-cell;
}

&.xl\:fi-visible {
@apply hidden xl:table-cell;
}

&.\32xl\:fi-visible {
@apply hidden 2xl:table-cell;
}

& > .fi-ta-cell-label {
@apply pt-2 text-sm leading-6 font-semibold text-gray-500 sm:hidden dark:text-gray-400;
}

& > .fi-ta-cell-content {
@apply text-sm break-words text-gray-800 sm:block dark:text-gray-200;

& .fi-ta-text,
& .fi-ta-checkbox,
& .fi-ta-color,
& .fi-ta-icon,
& .fi-ta-image,
& .fi-ta-select,
& .fi-ta-text-input,
& .fi-ta-toggle {
@apply px-0 pt-0 pb-1 sm:px-3 sm:py-4;
}
}

&:has(.fi-ta-actions) {
@apply py-2 sm:px-3 sm:py-0;

& > .fi-ta-actions {
@apply w-full flex-wrap justify-start gap-x-3 gap-y-2 sm:w-auto sm:flex-nowrap sm:justify-end sm:gap-3;
}
}
}
}

& > thead {
@apply divide-y divide-gray-200 dark:divide-white/5;

Expand All @@ -21,3 +139,19 @@
@apply bg-gray-50 dark:bg-white/5;
}
}

.fi-ta-stacked-header-row {
@apply block w-full border-y-0 sm:hidden;

& .fi-ta-stacked-header-cell {
@apply flex w-full items-center gap-4 bg-gray-50 px-4 py-3 font-normal dark:bg-white/5;

& .fi-ta-page-checkbox {
@apply ms-auto shrink-0;
}

& .fi-ta-stacked-sorting {
@apply flex flex-1 gap-x-3;
}
}
}
162 changes: 161 additions & 1 deletion packages/tables/resources/views/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
$selectsGroupsOnly = $selectsGroupsOnly();
$recordCheckboxPosition = $getRecordCheckboxPosition();
$isStriped = $isStriped();
$isStackedOnMobile = $isStackedOnMobile();
$isLoaded = $isLoaded();
$hasFilters = $isFilterable();
$filtersLayout = $getFiltersLayout();
Expand Down Expand Up @@ -1279,8 +1280,165 @@ class="fi-ta-record-collapse-btn fi-icon-btn"
</table>
@endif
@elseif ((! ($content || $hasColumnsLayout)) && ($records !== null))
<table class="fi-ta-table">
@php
$sortableColumns = $isStackedOnMobile ? array_filter(
$columns,
fn (\Filament\Tables\Columns\Column $column): bool => $column->isSortable(),
) : [];
@endphp

<table
@class([
'fi-ta-table',
'fi-stacked-on-mobile' => $isStackedOnMobile,
])
>
<thead>
@if ($isStackedOnMobile && (count($sortableColumns) || ($isSelectionEnabled && ($maxSelectableRecords !== 1) && (! $selectsGroupsOnly))) && (! $isReordering))
<tr class="fi-ta-stacked-header-row">
<th
colspan="100%"
class="fi-ta-stacked-header-cell"
>
@if (count($sortableColumns))
<div
x-data="{
sort: $wire.$entangle('tableSort', true),
column: null,
direction: null,
}"
x-init="
if (sort) {
;[column, direction] = sort.split(':')
direction ??= 'asc'
}

$watch('sort', function () {
if (! sort) {
return
}

;[column, direction] = sort.split(':')
direction ??= 'asc'
})

$watch('direction', function () {
sort = column ? `${column}:${direction}` : null
})

$watch('column', function (newColumn, oldColumn) {
if (! newColumn) {
direction = null
sort = column ? `${column}:${direction}` : null

return
}

if (oldColumn) {
sort = column ? `${column}:${direction}` : null

return
}

direction = 'asc'
sort = column ? `${column}:${direction}` : null
})
"
class="fi-ta-stacked-sorting"
>
<label>
<x-filament::input.wrapper
:prefix="__('filament-tables::table.sorting.fields.column.label')"
>
<x-filament::input.select
x-model="column"
>
<option
value=""
>
{{ $defaultSortOptionLabel }}
</option>

@foreach ($sortableColumns as $sortableColumn)
<option
value="{{ $sortableColumn->getName() }}"
>
{{ $sortableColumn->getLabel() }}
</option>
@endforeach
</x-filament::input.select>
</x-filament::input.wrapper>
</label>

<label
x-cloak
x-show="column"
>
<span
class="fi-sr-only"
>
{{ __('filament-tables::table.sorting.fields.direction.label') }}
</span>

<x-filament::input.wrapper>
<x-filament::input.select
x-model="direction"
>
<option
value="asc"
>
{{ __('filament-tables::table.sorting.fields.direction.options.asc') }}
</option>

<option
value="desc"
>
{{ __('filament-tables::table.sorting.fields.direction.options.desc') }}
</option>
</x-filament::input.select>
</x-filament::input.wrapper>
</label>
</div>
@endif

@if ($isSelectionEnabled && ($maxSelectableRecords !== 1) && (! $selectsGroupsOnly))
<input
aria-label="{{ __('filament-tables::table.fields.bulk_select_page.label') }}"
type="checkbox"
@if ($isSelectionDisabled)
disabled
@elseif ($maxSelectableRecords)
x-bind:disabled="
const recordsOnPage = getRecordsOnPage()

return recordsOnPage.length && ! areRecordsToggleable(recordsOnPage)
"
@endif
x-bind:checked="
const recordsOnPage = getRecordsOnPage()

if (recordsOnPage.length && areRecordsSelected(recordsOnPage)) {
$el.checked = true

return 'checked'
}

$el.checked = false

return null
"
x-on:click="toggleSelectRecordsOnPage"
{{-- Make sure the "checked" state gets re-evaluated after a Livewire request: --}}
wire:key="{{ $this->getId() }}.table.bulk-select-page.checkbox.stacked.{{ \Illuminate\Support\Str::random() }}"
wire:loading.attr="disabled"
wire:target="{{ implode(',', \Filament\Tables\Table::LOADING_TARGETS) }}"
class="fi-ta-page-checkbox fi-checkbox-input"
/>
@endif
</th>
</tr>
@endif

@if ($hasColumnGroups)
<tr class="fi-ta-table-head-groups-row">
@if (count($records))
Expand Down Expand Up @@ -2017,6 +2175,7 @@ class="fi-ta-record-checkbox fi-checkbox-input"
])
}}
>
{!! $isStackedOnMobile ? '<div class="fi-ta-cell-label">' . e($column->getLabel()) . '</div><div class="fi-ta-cell-content">' : '' !!}
<{{ $columnWrapperTag }}
@if ($columnWrapperTag === 'a')
{{ \Filament\Support\generate_href_html($columnUrl ?: $recordUrl, $columnUrl ? $column->shouldOpenUrlInNewTab() : $openRecordUrlInNewTab, hasNestedClickEventHandler: true) }}
Expand All @@ -2036,6 +2195,7 @@ class="fi-ta-record-checkbox fi-checkbox-input"
>
{{ $column }}
</{{ $columnWrapperTag }}>
{!! $isStackedOnMobile ? '</div>' : '' !!}
</td>
@endforeach

Expand Down
1 change: 1 addition & 0 deletions packages/tables/src/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Table extends ViewComponent
use HasDefaultDataFormattingSettings;
use HasExtraAttributes;
use Table\Concerns\BelongsToLivewire;
use Table\Concerns\CanBeStackedOnMobile;
use Table\Concerns\CanBeStriped;
use Table\Concerns\CanDeferLoading;
use Table\Concerns\CanGroupRecords;
Expand Down
22 changes: 22 additions & 0 deletions packages/tables/src/Table/Concerns/CanBeStackedOnMobile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Filament\Tables\Table\Concerns;

use Closure;

trait CanBeStackedOnMobile
{
protected bool | Closure $stackedOnMobile = false;

public function stackedOnMobile(bool | Closure $condition = true): static
{
$this->stackedOnMobile = $condition;

return $this;
}

public function isStackedOnMobile(): bool
{
return (bool) $this->evaluate($this->stackedOnMobile);
}
}