From d32b51ee5db0d57f9657a8fe1b5651ab0336d4aa Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:18:38 +0200 Subject: [PATCH 01/12] fix(tailwind): read outside filters layout with its snake_case token HasProperties::toArray() snake_cases every property, so the layout token is stored as layout.outside_filters. The camelCase lookup always resolved to an empty string, dropping the theme's outside filter wrapper classes. --- resources/views/components/themes/tailwind/filter.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/components/themes/tailwind/filter.blade.php b/resources/views/components/themes/tailwind/filter.blade.php index f5f96ee0..f4dfc3c0 100644 --- a/resources/views/components/themes/tailwind/filter.blade.php +++ b/resources/views/components/themes/tailwind/filter.blade.php @@ -15,7 +15,7 @@
Date: Thu, 13 Aug 2026 21:18:47 +0200 Subject: [PATCH 02/12] refactor(tailwind): extract filter fields into a shared partial The outside filter panel built its field grid inline. Moving the sorting and the per-filter-type dispatch into filters/fields.blade.php lets another panel render the same fields with a different grid, without duplicating the mapping. The grid classes stay with the caller via $gridClass. --- .../themes/tailwind/filter.blade.php | 89 ++---------------- .../themes/tailwind/filters/fields.blade.php | 92 +++++++++++++++++++ 2 files changed, 99 insertions(+), 82 deletions(-) create mode 100644 resources/views/components/themes/tailwind/filters/fields.blade.php diff --git a/resources/views/components/themes/tailwind/filter.blade.php b/resources/views/components/themes/tailwind/filter.blade.php index f4dfc3c0..71fe42e4 100644 --- a/resources/views/components/themes/tailwind/filter.blade.php +++ b/resources/views/components/themes/tailwind/filter.blade.php @@ -28,87 +28,12 @@ class="{{ theme('layout.outside_filters') }} mt-2 md:mt-0" x-transition:leave-end="opacity-0 scale-90" class="pg-filter-container" > - @php - $customConfig = []; - - $filtersFromColumns = collect($filtersFromColumns ?? []) - ->filter(fn($column) => filled(data_get($column, 'filters'))); - - if ($filtersFromColumns->isEmpty() && $__partial) { - $filtersFromColumns = collect($__partial->columns) - ->filter(fn($column) => filled(data_get($column, 'filters'))); - } - - $componentFilters = collect($__partial ? $__partial->filters() : []); - $filterOrderMap = $componentFilters->pluck('field')->flip(); - - // Sort filters based on the order they appear in filters() method - $sortedFilters = $filtersFromColumns->sortBy(function ($column) use ($filterOrderMap) { - $fieldName = data_get($column, 'filters.field'); - return $filterOrderMap->get($fieldName, 999); // 999 for fields not found in filters() - }); - @endphp -
- @foreach ($sortedFilters as $column) - @php - $filter = data_get($column, 'filters'); - $title = data_get($column, 'title'); - $baseClass = data_get($filter, 'baseClass'); - $className = str(data_get($filter, 'className')); - @endphp - -
- @if ($className->contains('FilterMultiSelect')) - - @elseif ($className->contains(['FilterDateTimePicker', 'FilterDatePicker'])) - @includeIf(theme_view('filter.date_picker'), [ - 'filter' => $filter, - 'tableName' => $tableName, - 'classAttr' => 'w-full', - 'type' => $className->contains('FilterDateTimePicker') ? 'datetime' : 'date', - '__partial' => $__partial, - ]) - @elseif ($className->contains(['FilterSelect', 'FilterEnumSelect'])) - @includeIf(theme_view('filter.select'), [ - 'filter' => $filter, - '__partial' => $__partial, - ]) - @elseif ($className->contains('FilterNumber')) - @includeIf(theme_view('filter.number'), [ - 'filter' => $filter, - '__partial' => $__partial, - ]) - @elseif ($className->contains('FilterInputText')) - @includeIf(theme_view('filter.input_text'), [ - 'filter' => $filter, - '__partial' => $__partial, - ]) - @elseif ($className->contains('FilterBoolean')) - @includeIf(theme_view('filter.boolean'), [ - 'filter' => $filter, - '__partial' => $__partial, - ]) - @elseif ($className->contains('FilterDynamic')) - - @endif -
- @endforeach -
+ @include('livewire-powergrid::components.themes.tailwind.filters.fields', [ + 'theme' => $theme, + 'tableName' => $tableName, + 'filtersFromColumns' => $filtersFromColumns, + 'gridClass' => 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6 gap-3', + '__partial' => $__partial, + ])
diff --git a/resources/views/components/themes/tailwind/filters/fields.blade.php b/resources/views/components/themes/tailwind/filters/fields.blade.php new file mode 100644 index 00000000..310d6d1c --- /dev/null +++ b/resources/views/components/themes/tailwind/filters/fields.blade.php @@ -0,0 +1,92 @@ +@props([ + 'theme' => null, + 'tableName' => null, + 'filtersFromColumns' => null, + 'gridClass' => '', + '__partial' => null, +]) + +@php + $__partial = $__partial ?? (isset($this) ? $this : null); + $tableName = $tableName ?? $__partial->tableName; + + $filtersFromColumns = collect($filtersFromColumns ?? []) + ->filter(fn($column) => filled(data_get($column, 'filters'))); + + if ($filtersFromColumns->isEmpty() && $__partial) { + $filtersFromColumns = collect($__partial->columns) + ->filter(fn($column) => filled(data_get($column, 'filters'))); + } + + $componentFilters = collect($__partial ? $__partial->filters() : []); + $filterOrderMap = $componentFilters->pluck('field')->flip(); + + // Sort filters based on the order they appear in filters() method + $sortedFilters = $filtersFromColumns->sortBy(function ($column) use ($filterOrderMap) { + $fieldName = data_get($column, 'filters.field'); + return $filterOrderMap->get($fieldName, 999); // 999 for fields not found in filters() + }); +@endphp + +
+ @foreach ($sortedFilters as $column) + @php + $filter = data_get($column, 'filters'); + $title = data_get($column, 'title'); + $baseClass = data_get($filter, 'baseClass'); + $className = str(data_get($filter, 'className')); + @endphp + +
+ @if ($className->contains('FilterMultiSelect')) + + @elseif ($className->contains(['FilterDateTimePicker', 'FilterDatePicker'])) + @includeIf(theme_view('filter.date_picker'), [ + 'filter' => $filter, + 'tableName' => $tableName, + 'classAttr' => 'w-full', + 'type' => $className->contains('FilterDateTimePicker') ? 'datetime' : 'date', + '__partial' => $__partial, + ]) + @elseif ($className->contains(['FilterSelect', 'FilterEnumSelect'])) + @includeIf(theme_view('filter.select'), [ + 'filter' => $filter, + '__partial' => $__partial, + ]) + @elseif ($className->contains('FilterNumber')) + @includeIf(theme_view('filter.number'), [ + 'filter' => $filter, + '__partial' => $__partial, + ]) + @elseif ($className->contains('FilterInputText')) + @includeIf(theme_view('filter.input_text'), [ + 'filter' => $filter, + '__partial' => $__partial, + ]) + @elseif ($className->contains('FilterBoolean')) + @includeIf(theme_view('filter.boolean'), [ + 'filter' => $filter, + '__partial' => $__partial, + ]) + @elseif ($className->contains('FilterDynamic')) + + @endif +
+ @endforeach +
From eeeaa05cb8a6237298a0a8f92e8db12cc81957d4 Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:18:54 +0200 Subject: [PATCH 03/12] feat(config): add flyout filter position and drawer settings Adds 'flyout' as a third value for the filter config and a filter_flyout block holding the drawer edge plus its two close behaviours, so a table can override them from boot() without touching the view. --- resources/config/livewire-powergrid.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/resources/config/livewire-powergrid.php b/resources/config/livewire-powergrid.php index 5cc19311..98275ef8 100644 --- a/resources/config/livewire-powergrid.php +++ b/resources/config/livewire-powergrid.php @@ -76,15 +76,36 @@ | Filters |-------------------------------------------------------------------------- | - | PowerGrid supports inline and outside filters. + | PowerGrid supports inline, outside and flyout filters. | 'inline': Filters data inside the table. | 'outside': Filters data outside the table. + | 'flyout': Filters data in a drawer sliding in from the side. | 'null' | */ 'filter' => 'inline', + /* + |-------------------------------------------------------------------------- + | Filter Flyout + |-------------------------------------------------------------------------- + | + | Settings for the drawer used when 'filter' is set to 'flyout'. + | 'position': which edge the drawer slides in from ('left' or 'right'). + | 'close_on_escape': close the drawer when the Escape key is pressed. + | 'close_on_click_outside': close the drawer when its backdrop is clicked. + | + | Override these per table by calling config() in the table's boot() method. + | + */ + + 'filter_flyout' => [ + 'position' => 'right', + 'close_on_escape' => true, + 'close_on_click_outside' => true, + ], + /* |-------------------------------------------------------------------------- | Filters Attributes From e637b8a8ecacb2484d2102c6f5eeefee944e2707 Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:18:54 +0200 Subject: [PATCH 04/12] feat(lang): add close button translation Used as the accessible label of the filter drawer's close button. --- resources/lang/ar/datatable.php | 1 + resources/lang/bn_BD/datatable.php | 1 + resources/lang/ca/datatable.php | 1 + resources/lang/cs/datatable.php | 1 + resources/lang/de/datatable.php | 1 + resources/lang/en/datatable.php | 1 + resources/lang/es/datatable.php | 1 + resources/lang/fa/datatable.php | 1 + resources/lang/fr/datatable.php | 1 + resources/lang/id/datatable.php | 1 + resources/lang/it/datatable.php | 1 + resources/lang/ms_MY/datatable.php | 1 + resources/lang/nl/datatable.php | 1 + resources/lang/pl/datatable.php | 1 + resources/lang/pt/datatable.php | 1 + resources/lang/pt_BR/datatable.php | 1 + resources/lang/ru/datatable.php | 1 + resources/lang/tr/datatable.php | 1 + resources/lang/uk/datatable.php | 1 + resources/lang/yr/datatable.php | 1 + 20 files changed, 20 insertions(+) diff --git a/resources/lang/ar/datatable.php b/resources/lang/ar/datatable.php index 47860a61..a770ec75 100644 --- a/resources/lang/ar/datatable.php +++ b/resources/lang/ar/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'تصفية', 'clear_all_filters' => 'مسح الكل', + 'close' => 'إغلاق', ], 'labels' => [ 'action' => 'العمليات', diff --git a/resources/lang/bn_BD/datatable.php b/resources/lang/bn_BD/datatable.php index fe976065..1ab17520 100644 --- a/resources/lang/bn_BD/datatable.php +++ b/resources/lang/bn_BD/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'ফিল্টার', 'clear_all_filters' => 'সব ফিল্টার মুছুন', + 'close' => 'বন্ধ করুন', ], 'labels' => [ 'action' => 'কার্যসমূহ', diff --git a/resources/lang/ca/datatable.php b/resources/lang/ca/datatable.php index 35006236..4ee75ca0 100644 --- a/resources/lang/ca/datatable.php +++ b/resources/lang/ca/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtre', 'clear_all_filters' => 'Clear all', + 'close' => 'Tanca', ], 'labels' => [ 'action' => 'Accions', diff --git a/resources/lang/cs/datatable.php b/resources/lang/cs/datatable.php index 97ca9f1c..0d20c650 100644 --- a/resources/lang/cs/datatable.php +++ b/resources/lang/cs/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtrovat', 'clear_all_filters' => 'Vymazat vše', + 'close' => 'Zavřít', ], 'labels' => [ 'action' => 'Akce', diff --git a/resources/lang/de/datatable.php b/resources/lang/de/datatable.php index 74c28200..b78e9d3a 100644 --- a/resources/lang/de/datatable.php +++ b/resources/lang/de/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Aktionen', 'clear_all_filters' => 'Alle löschen', + 'close' => 'Schließen', ], 'labels' => [ 'action' => 'Aktionen', diff --git a/resources/lang/en/datatable.php b/resources/lang/en/datatable.php index 08320af2..20189306 100644 --- a/resources/lang/en/datatable.php +++ b/resources/lang/en/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filter', 'clear_all_filters' => 'Clear all', + 'close' => 'Close', ], 'labels' => [ 'action' => 'Actions', diff --git a/resources/lang/es/datatable.php b/resources/lang/es/datatable.php index 61aa8154..c34fb1cd 100644 --- a/resources/lang/es/datatable.php +++ b/resources/lang/es/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtrar', 'clear_all_filters' => 'Limpiar todo', + 'close' => 'Cerrar', ], 'labels' => [ 'action' => 'Acción', diff --git a/resources/lang/fa/datatable.php b/resources/lang/fa/datatable.php index cf7b17c5..5bb9a937 100644 --- a/resources/lang/fa/datatable.php +++ b/resources/lang/fa/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'فیلتر', 'clear_all_filters' => 'حذف همه', + 'close' => 'بستن', ], 'labels' => [ 'action' => 'اقدام ها', diff --git a/resources/lang/fr/datatable.php b/resources/lang/fr/datatable.php index 7ad2e6b7..5e19eb9c 100644 --- a/resources/lang/fr/datatable.php +++ b/resources/lang/fr/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtrer', 'clear_all_filters' => 'Enlever tous les filtres', + 'close' => 'Fermer', ], 'labels' => [ 'action' => 'Actions', diff --git a/resources/lang/id/datatable.php b/resources/lang/id/datatable.php index 6770d3ee..e652b654 100644 --- a/resources/lang/id/datatable.php +++ b/resources/lang/id/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filter', 'clear_all_filters' => 'Hapus semua', + 'close' => 'Tutup', ], 'labels' => [ 'action' => 'Aksi', diff --git a/resources/lang/it/datatable.php b/resources/lang/it/datatable.php index af1ee3de..f2f302ef 100644 --- a/resources/lang/it/datatable.php +++ b/resources/lang/it/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtra', 'clear_all_filters' => 'Svuota filtri', + 'close' => 'Chiudi', ], 'labels' => [ 'action' => 'Azioni', diff --git a/resources/lang/ms_MY/datatable.php b/resources/lang/ms_MY/datatable.php index b3750967..56248792 100644 --- a/resources/lang/ms_MY/datatable.php +++ b/resources/lang/ms_MY/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Carian', 'clear_all_filters' => 'Clear all', + 'close' => 'Tutup', ], 'labels' => [ 'action' => 'Action', diff --git a/resources/lang/nl/datatable.php b/resources/lang/nl/datatable.php index 94a74c02..bd6360e7 100644 --- a/resources/lang/nl/datatable.php +++ b/resources/lang/nl/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filter', 'clear_all_filters' => 'Wis alle filters', + 'close' => 'Sluiten', ], 'labels' => [ 'action' => 'Acties', diff --git a/resources/lang/pl/datatable.php b/resources/lang/pl/datatable.php index 4aa46812..dbde0350 100644 --- a/resources/lang/pl/datatable.php +++ b/resources/lang/pl/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtry', 'clear_all_filters' => 'Wyczyść wszystkie filtry', + 'close' => 'Zamknij', ], 'labels' => [ 'action' => 'Akcje', diff --git a/resources/lang/pt/datatable.php b/resources/lang/pt/datatable.php index 87b4bf52..e83f252c 100644 --- a/resources/lang/pt/datatable.php +++ b/resources/lang/pt/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtrar', 'clear_all_filters' => 'Limpar Todos', + 'close' => 'Fechar', ], 'labels' => [ 'action' => 'Ações', diff --git a/resources/lang/pt_BR/datatable.php b/resources/lang/pt_BR/datatable.php index 3d25eaa8..0779b572 100644 --- a/resources/lang/pt_BR/datatable.php +++ b/resources/lang/pt_BR/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtrar', 'clear_all_filters' => 'Limpar Todos', + 'close' => 'Fechar', ], 'labels' => [ 'action' => 'Ações', diff --git a/resources/lang/ru/datatable.php b/resources/lang/ru/datatable.php index 59bdcd1f..e3f520aa 100644 --- a/resources/lang/ru/datatable.php +++ b/resources/lang/ru/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Фильтр', 'clear_all_filters' => 'Очистить все', + 'close' => 'Закрыть', ], 'labels' => [ 'action' => 'Действия', diff --git a/resources/lang/tr/datatable.php b/resources/lang/tr/datatable.php index e5651536..1da0a723 100644 --- a/resources/lang/tr/datatable.php +++ b/resources/lang/tr/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Filtre', 'clear_all_filters' => 'Tümünü Temizle', + 'close' => 'Kapat', ], 'labels' => [ 'action' => 'İşlemler', diff --git a/resources/lang/uk/datatable.php b/resources/lang/uk/datatable.php index ea6523b1..4d481818 100644 --- a/resources/lang/uk/datatable.php +++ b/resources/lang/uk/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'Фильтр', 'clear_all_filters' => 'Очистити все', + 'close' => 'Закрити', ], 'labels' => [ 'action' => 'Дія', diff --git a/resources/lang/yr/datatable.php b/resources/lang/yr/datatable.php index 983f736d..2c364555 100644 --- a/resources/lang/yr/datatable.php +++ b/resources/lang/yr/datatable.php @@ -4,6 +4,7 @@ 'buttons' => [ 'filter' => 'àlẹmọ', 'clear_all_filters' => 'Clear all', + 'close' => 'Tì', ], 'labels' => [ 'action' => 'Awọn Igbese', From 7c50c96c7c54f6f37757dd9adb06564222d967a7 Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:18:59 +0200 Subject: [PATCH 05/12] feat(themes): add Flyout component for filter drawer classes Filter::flyout(Closure) mirrors the other fluent sub-components and exposes the drawer's class slots: overlay, panel with its per-side anchoring, header, title, close, body, footer and clearAll. --- src/Themes/Components/Filter.php | 9 ++++ src/Themes/Components/Flyout.php | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/Themes/Components/Flyout.php diff --git a/src/Themes/Components/Filter.php b/src/Themes/Components/Filter.php index 53c63389..9716b4b0 100644 --- a/src/Themes/Components/Filter.php +++ b/src/Themes/Components/Filter.php @@ -75,4 +75,13 @@ public function inputText(Closure $callback): self return $this; } + + public function flyout(Closure $callback): self + { + $component = new Flyout(); + $callback($component); + $this->properties['flyout'] = $component->toArray(); + + return $this; + } } diff --git a/src/Themes/Components/Flyout.php b/src/Themes/Components/Flyout.php new file mode 100644 index 00000000..6e70b232 --- /dev/null +++ b/src/Themes/Components/Flyout.php @@ -0,0 +1,93 @@ +properties['view'] = $view; + + return $this; + } + + /** Backdrop rendered behind the panel. */ + public function overlay(string $overlay): self + { + $this->properties['overlay'] = $overlay; + + return $this; + } + + /** Classes shared by both drawer sides. */ + public function panel(string $panel): self + { + $this->properties['panel'] = $panel; + + return $this; + } + + /** Classes applied on top of `panel()` when the drawer is anchored left. */ + public function panelLeft(string $panelLeft): self + { + $this->properties['panelLeft'] = $panelLeft; + + return $this; + } + + /** Classes applied on top of `panel()` when the drawer is anchored right. */ + public function panelRight(string $panelRight): self + { + $this->properties['panelRight'] = $panelRight; + + return $this; + } + + public function header(string $header): self + { + $this->properties['header'] = $header; + + return $this; + } + + public function title(string $title): self + { + $this->properties['title'] = $title; + + return $this; + } + + public function close(string $close): self + { + $this->properties['close'] = $close; + + return $this; + } + + public function body(string $body): self + { + $this->properties['body'] = $body; + + return $this; + } + + public function footer(string $footer): self + { + $this->properties['footer'] = $footer; + + return $this; + } + + public function clearAll(string $clearAll): self + { + $this->properties['clearAll'] = $clearAll; + + return $this; + } +} From 6fa78930a8c6a27bc21789fe68e37930edf1846d Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:19:10 +0200 Subject: [PATCH 06/12] feat(themes): style the filter flyout in the shipped themes Tailwind carries the drawer's view alias, which DaisyUI and Flux inherit through parentTheme; each theme only sets its own classes. --- src/Themes/DaisyUI.php | 12 ++++++++++++ src/Themes/Flux.php | 12 ++++++++++++ src/Themes/Tailwind.php | 13 +++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/Themes/DaisyUI.php b/src/Themes/DaisyUI.php index 4bd75a5e..6ac80025 100644 --- a/src/Themes/DaisyUI.php +++ b/src/Themes/DaisyUI.php @@ -131,6 +131,18 @@ public function filter(): array ->input('input input-sm input-bordered w-full') ) ->input('input input-sm') + ->flyout(fn (Components\Flyout $flyout) => $flyout + ->overlay('fixed inset-0 z-40 bg-neutral/40') + ->panel('fixed inset-y-0 z-50 flex w-full max-w-full flex-col bg-base-100 shadow-xl sm:w-96 sm:max-w-[90vw]') + ->panelLeft('left-0 border-r border-base-300') + ->panelRight('right-0 border-l border-base-300') + ->header('flex items-center justify-between gap-3 border-b border-base-300 px-4 py-3') + ->title('font-semibold text-base-content') + ->close('btn btn-ghost btn-sm btn-square') + ->body('flex-1 overflow-y-auto px-4 py-4') + ->footer('border-t border-base-300 px-4 py-3') + ->clearAll('btn btn-outline btn-sm w-full') + ) ->toArray(), ]; } diff --git a/src/Themes/Flux.php b/src/Themes/Flux.php index 47b692f8..7009c78d 100644 --- a/src/Themes/Flux.php +++ b/src/Themes/Flux.php @@ -114,6 +114,18 @@ public function filter(): array ->input($this->input().' w-full') ) ->input($this->input().' w-full') + ->flyout(fn (Components\Flyout $flyout) => $flyout + ->overlay('fixed inset-0 z-40 bg-zinc-900/40') + ->panel('fixed inset-y-0 z-50 flex w-full max-w-full flex-col bg-white shadow-xl dark:bg-zinc-900 sm:w-96 sm:max-w-[90vw]') + ->panelLeft('left-0 border-r border-zinc-200 dark:border-zinc-700') + ->panelRight('right-0 border-l border-zinc-200 dark:border-zinc-700') + ->header('flex items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3 dark:border-zinc-700') + ->title('text-sm font-semibold text-zinc-800 dark:text-zinc-200') + ->close('rounded-lg p-1.5 text-zinc-500 transition-colors hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-700') + ->body('flex-1 overflow-y-auto px-4 py-4') + ->footer('border-t border-zinc-200 px-4 py-3 dark:border-zinc-700') + ->clearAll($this->button().' w-full justify-center') + ) ->toArray(), ]; } diff --git a/src/Themes/Tailwind.php b/src/Themes/Tailwind.php index db084bf0..7f771faf 100644 --- a/src/Themes/Tailwind.php +++ b/src/Themes/Tailwind.php @@ -145,6 +145,19 @@ public function filter(): array 'input' => 'focus:ring-accent focus-within:focus:ring-accent focus-within:ring-accent dark:focus-within:ring-accent flex rounded-md ring-1 transition focus-within:ring-2 dark:ring-zinc-600 dark:text-zinc-300 text-zinc-600 ring-zinc-300 dark:bg-zinc-800 bg-white dark:placeholder-zinc-400 w-full rounded-md border-0 bg-transparent py-1.5 px-2 ring-0 placeholder:text-zinc-400 focus:outline-none sm:text-sm sm:leading-6 w-full', ], 'input' => 'focus:ring-accent focus-within:focus:ring-accent focus-within:ring-accent dark:focus-within:ring-accent flex rounded-md ring-1 transition focus-within:ring-2 dark:ring-zinc-600 dark:text-zinc-300 text-zinc-600 ring-zinc-300 dark:bg-zinc-800 bg-white dark:placeholder-zinc-400 w-full rounded-md border-0 bg-transparent py-1.5 px-2 ring-0 placeholder:text-zinc-400 focus:outline-none sm:text-sm sm:leading-6 w-full', + 'flyout' => [ + 'view' => 'livewire-powergrid::components.themes.tailwind.filter-flyout', + 'overlay' => 'fixed inset-0 z-40 bg-zinc-900/40', + 'panel' => 'fixed inset-y-0 z-50 flex w-full max-w-full flex-col bg-white shadow-xl dark:bg-zinc-800 sm:w-96 sm:max-w-[90vw]', + 'panel_left' => 'left-0 border-r border-zinc-200 dark:border-zinc-700', + 'panel_right' => 'right-0 border-l border-zinc-200 dark:border-zinc-700', + 'header' => 'flex items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3 dark:border-zinc-700', + 'title' => 'text-sm font-semibold text-zinc-700 dark:text-zinc-200', + 'close' => 'rounded-md p-1 text-zinc-500 transition hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200', + 'body' => 'flex-1 overflow-y-auto px-4 py-4', + 'footer' => 'border-t border-zinc-200 px-4 py-3 dark:border-zinc-700', + 'clear_all' => 'w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-xs font-bold text-zinc-600 transition hover:text-zinc-500 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-400', + ], ], ]; } From 9126fdeda7a77defd6701093fcb3f01252f63863 Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:19:10 +0200 Subject: [PATCH 07/12] feat(tailwind): add filter flyout drawer view Renders the backdrop and the panel, sliding in from the configured edge with a matching off-screen transform. Escape and backdrop closing are opt-out via config, the Escape listener is guarded because it is bound to the window. Fields come from the shared filters/fields partial in a single column grid. --- .../themes/tailwind/filter-flyout.blade.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 resources/views/components/themes/tailwind/filter-flyout.blade.php diff --git a/resources/views/components/themes/tailwind/filter-flyout.blade.php b/resources/views/components/themes/tailwind/filter-flyout.blade.php new file mode 100644 index 00000000..b2fe3de2 --- /dev/null +++ b/resources/views/components/themes/tailwind/filter-flyout.blade.php @@ -0,0 +1,106 @@ +@props([ + 'theme' => null, + 'tableName' => null, + 'filtersFromColumns' => null, + '__partial' => null, +]) + +@php + $__partial = $__partial ?? (isset($this) ? $this : null); + $tableName = $tableName ?? $__partial->tableName; + + $flyout = $__partial->filterFlyoutOptions(); + $isLeft = $flyout['position'] === 'left'; + + // Alpine needs the off-screen class up front, so the side decides both the + // anchoring classes and the direction the panel slides in from. + $panelSide = $isLeft ? theme('filter.flyout.panel_left') : theme('filter.flyout.panel_right'); + $panelOffscreen = $isLeft ? '-translate-x-full' : 'translate-x-full'; +@endphp + +
+ + + +
From 74364297e7337917af3ff6a8b37935e08dae21da Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:19:31 +0200 Subject: [PATCH 08/12] feat(filters): render the filter panel from the configured position Adds filterPosition(), usesFilterPanel() and filterPanelView() and routes the table, the theme headers and the pg-filters partial through them, so 'outside' and 'flyout' share one panel path instead of each view comparing the config string itself. filterFlyoutOptions() normalises the drawer settings so an invalid config value cannot break the panel. --- .../components/structure/table.blade.php | 4 +- .../themes/daisyui/header.blade.php | 2 +- .../components/themes/flux/header.blade.php | 2 +- .../themes/tailwind/header.blade.php | 2 +- src/Concerns/Filter.php | 44 ++++++++++++++++++- 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/resources/views/components/structure/table.blade.php b/resources/views/components/structure/table.blade.php index fb278e34..7cef17f7 100644 --- a/resources/views/components/structure/table.blade.php +++ b/resources/views/components/structure/table.blade.php @@ -24,13 +24,13 @@ class="pg-table-wrapper {{ theme('layout.wrapper') }}" '__partial' => $this, ]) - @if (config('livewire-powergrid.filter') === 'outside' && ! $this->filterBuilderHidesDefaultFilters()) + @if ($this->usesFilterPanel() && ! $this->filterBuilderHidesDefaultFilters()) @php $filtersFromColumns = collect($columns) ->filter(fn($column) => filled(data_get($column, 'filters'))); @endphp - @include(theme_view('filter'), [ + @include(theme_view($this->filterPanelView()), [ '__partial' => $this, 'tableName' => $tableName, 'filtersFromColumns' => $filtersFromColumns, diff --git a/resources/views/components/themes/daisyui/header.blade.php b/resources/views/components/themes/daisyui/header.blade.php index 08e5037c..2caa7dc5f 100644 --- a/resources/views/components/themes/daisyui/header.blade.php +++ b/resources/views/components/themes/daisyui/header.blade.php @@ -10,7 +10,7 @@ @includeIf(theme_view('header.toggle-columns')) @includeIf(theme_view('header.soft-deletes')) - @if (config('livewire-powergrid.filter') == 'outside' && count($this->filters()) > 0 && ! $this->filterBuilderHidesDefaultFilters()) + @if ($this->usesFilterPanel() && count($this->filters()) > 0 && ! $this->filterBuilderHidesDefaultFilters()) @includeIf(theme_view('header.filters')) @endif diff --git a/resources/views/components/themes/flux/header.blade.php b/resources/views/components/themes/flux/header.blade.php index efb15b2e..7e9ff963 100644 --- a/resources/views/components/themes/flux/header.blade.php +++ b/resources/views/components/themes/flux/header.blade.php @@ -8,7 +8,7 @@ {!! $this->renderPluginZone('header') !!} @includeIf(theme_view('header.toggle-columns')) @includeIf(theme_view('header.soft-deletes')) - @if (config('livewire-powergrid.filter') == 'outside' && count($this->filters()) > 0 && ! $this->filterBuilderHidesDefaultFilters()) + @if ($this->usesFilterPanel() && count($this->filters()) > 0 && ! $this->filterBuilderHidesDefaultFilters()) @includeIf(theme_view('header.filters')) @endif diff --git a/resources/views/components/themes/tailwind/header.blade.php b/resources/views/components/themes/tailwind/header.blade.php index efb15b2e..7e9ff963 100644 --- a/resources/views/components/themes/tailwind/header.blade.php +++ b/resources/views/components/themes/tailwind/header.blade.php @@ -8,7 +8,7 @@ {!! $this->renderPluginZone('header') !!} @includeIf(theme_view('header.toggle-columns')) @includeIf(theme_view('header.soft-deletes')) - @if (config('livewire-powergrid.filter') == 'outside' && count($this->filters()) > 0 && ! $this->filterBuilderHidesDefaultFilters()) + @if ($this->usesFilterPanel() && count($this->filters()) > 0 && ! $this->filterBuilderHidesDefaultFilters()) @includeIf(theme_view('header.filters')) @endif diff --git a/src/Concerns/Filter.php b/src/Concerns/Filter.php index 316e4b45..24879a73 100644 --- a/src/Concerns/Filter.php +++ b/src/Concerns/Filter.php @@ -240,6 +240,42 @@ public function toggleFilters(): void $this->renderOutsideFiltersPartial(); } + /** Configured filter position: `inline`, `outside`, `flyout`, or an empty string when filters are disabled. */ + public function filterPosition(): string + { + $position = config('livewire-powergrid.filter'); + + return is_string($position) ? $position : ''; + } + + /** True when filters live in their own panel instead of inside the table. */ + public function usesFilterPanel(): bool + { + return in_array($this->filterPosition(), ['outside', 'flyout'], true); + } + + /** Theme view alias for the filter panel matching the configured position. */ + public function filterPanelView(): string + { + return $this->filterPosition() === 'flyout' ? 'filter.flyout' : 'filter'; + } + + /** + * Flyout settings, normalised so a bad config value can never break the drawer. + * + * @return array{position: string, close_on_escape: bool, close_on_click_outside: bool} + */ + public function filterFlyoutOptions(): array + { + $position = config('livewire-powergrid.filter_flyout.position', 'right'); + + return [ + 'position' => in_array($position, ['left', 'right'], true) ? $position : 'right', + 'close_on_escape' => boolval(config('livewire-powergrid.filter_flyout.close_on_escape', true)), + 'close_on_click_outside' => boolval(config('livewire-powergrid.filter_flyout.close_on_click_outside', true)), + ]; + } + /** * @throws Exception */ @@ -424,6 +460,10 @@ public function filterInputTextOptions(string $field, string $value, string $lab $this->renderOutsideFiltersPartial(); } + /** + * Refreshes the filter Hot Zones. Despite the name, it covers every panel + * position (`outside` and `flyout`), which share the `pg-filters` partial. + */ public function renderOutsideFiltersPartial(): void { if (! function_exists('partials')) { @@ -439,7 +479,7 @@ public function renderOutsideFiltersPartial(): void 'enabledFilters' => $this->enabledFilters, ]); - if (config('livewire-powergrid.filter') !== 'outside') { + if (! $this->usesFilterPanel()) { return; } @@ -450,7 +490,7 @@ public function renderOutsideFiltersPartial(): void partials($this) ->partial("pg-tbody-{$this->tableName}", 'livewire-powergrid::components.partials.tbody') ->partial("pg-pagination-{$this->tableName}", theme_view('footer')) - ->partial("pg-filters-{$this->tableName}", theme_view('filter')); + ->partial("pg-filters-{$this->tableName}", theme_view($this->filterPanelView())); } private function resolveFilters(): void From 590c52ca04d55c73e3fe391026bd8aa66db96a25 Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:19:40 +0200 Subject: [PATCH 09/12] fix(filters): keep the pg-filters Hot Zone on Alpine-driven toggles Closing the drawer assigns $wire.showFilters directly, which commits without registering a partial. The updated hook re-registers them so the commit stays inside the Hot Zones instead of triggering a full re-render. --- src/Concerns/Filter.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Concerns/Filter.php b/src/Concerns/Filter.php index 24879a73..76dbc471 100644 --- a/src/Concerns/Filter.php +++ b/src/Concerns/Filter.php @@ -240,6 +240,17 @@ public function toggleFilters(): void $this->renderOutsideFiltersPartial(); } + /** + * The flyout's close paths (Escape, backdrop, close button) assign `$showFilters` + * straight from Alpine so the drawer hides without waiting on the response. That + * assignment registers no partial on its own, which would drop the commit out of + * the `pg-filters` Hot Zone and force a full re-render. + */ + public function updatedShowFilters(): void + { + $this->renderOutsideFiltersPartial(); + } + /** Configured filter position: `inline`, `outside`, `flyout`, or an empty string when filters are disabled. */ public function filterPosition(): string { From 1c4e22327e83a494e238787b987d52570f2a73b3 Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:19:54 +0200 Subject: [PATCH 10/12] chore: regenerate PhpStorm meta for the current theme tokens Drops the table.view* aliases and the removed header.export entries, and picks up layout.wrapper, the actions cells, the responsive row tokens, cols.div and the toggleable colors. --- .phpstorm.meta.php | 50 ++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 47e0e14e..9a657709 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -6,6 +6,8 @@ 0, 'name', 'base_view', + 'layout.wrapper', + 'layout.outside_filters', 'header.view', 'header.layout.container', 'header.layout.sub_container', @@ -19,22 +21,17 @@ 'header.search_box.icon_close_wrapper', 'header.search_box.icon_close', 'header.search_box.icon_search', - 'table.view', - 'table.view_header', - 'table.view_row', - 'table.view_cols', - 'table.view_th_empty', - 'table.view_inline_filters', - 'table.view_checkbox_all', - 'table.view_checkbox_row', - 'table.view_radio_row', 'table.layout.container', 'table.layout.table', 'table.layout.thead', 'table.layout.tr', 'table.layout.th', + 'table.layout.th_actions', 'table.layout.tbody', 'table.layout.td', + 'table.layout.td_actions', + 'table.body.tr.responsive', + 'table.body.tr.responsive_toggle_icon', 'table.checkbox.th', 'table.checkbox.base', 'table.checkbox.label', @@ -43,6 +40,7 @@ 'table.radio.base', 'table.radio.label', 'table.radio.input', + 'cols.div', 'footer.view', 'footer.layout.container', 'footer.layout.select', @@ -72,7 +70,11 @@ 'editable.clickable', 'editable.input', 'editable.error', - 'toggleable.view' + 'toggleable.color_on', + 'toggleable.color_off', + 'toggleable.color_on_dark', + 'toggleable.color_off_dark', + 'toggleable.knob_on' ); expectedArguments( @@ -80,6 +82,8 @@ 0, 'name', 'base_view', + 'layout.wrapper', + 'layout.outside_filters', 'header.view', 'header.layout.container', 'header.layout.sub_container', @@ -93,22 +97,17 @@ 'header.search_box.icon_close_wrapper', 'header.search_box.icon_close', 'header.search_box.icon_search', - 'table.view', - 'table.view_header', - 'table.view_row', - 'table.view_cols', - 'table.view_th_empty', - 'table.view_inline_filters', - 'table.view_checkbox_all', - 'table.view_checkbox_row', - 'table.view_radio_row', 'table.layout.container', 'table.layout.table', 'table.layout.thead', 'table.layout.tr', 'table.layout.th', + 'table.layout.th_actions', 'table.layout.tbody', 'table.layout.td', + 'table.layout.td_actions', + 'table.body.tr.responsive', + 'table.body.tr.responsive_toggle_icon', 'table.checkbox.th', 'table.checkbox.base', 'table.checkbox.label', @@ -117,6 +116,7 @@ 'table.radio.base', 'table.radio.label', 'table.radio.input', + 'cols.div', 'footer.view', 'footer.layout.container', 'footer.layout.select', @@ -146,7 +146,11 @@ 'editable.clickable', 'editable.input', 'editable.error', - 'toggleable.view' + 'toggleable.color_on', + 'toggleable.color_off', + 'toggleable.color_on_dark', + 'toggleable.color_off_dark', + 'toggleable.knob_on' ); expectedArguments( @@ -162,9 +166,7 @@ 'footer', 'footer.pagination', 'header', - 'header.batch-exporting', 'header.enabled-filters', - 'header.export', 'header.filters', 'header.loading', 'header.message-soft-deletes', @@ -174,7 +176,6 @@ 'header.soft-deletes', 'header.toggle-columns', 'pagination', - 'table', 'table.checkbox-all', 'table.checkbox-row', 'table.cols', @@ -204,9 +205,7 @@ 'footer', 'footer.pagination', 'header', - 'header.batch-exporting', 'header.enabled-filters', - 'header.export', 'header.filters', 'header.loading', 'header.message-soft-deletes', @@ -216,7 +215,6 @@ 'header.soft-deletes', 'header.toggle-columns', 'pagination', - 'table', 'table.checkbox-all', 'table.checkbox-row', 'table.cols', From 495a21c5cd8edcdf21814ec2749ecdc550c0493f Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:20:00 +0200 Subject: [PATCH 11/12] chore(ide): complete filter.flyout aliases in the theme meta Registers filter.flyout in the generator's canonical alias list and adds the drawer's tokens to the meta, so theme() and theme_view() autocomplete them. --- .phpstorm.meta.php | 24 +++++++++++++++++++++++ src/Commands/GenerateThemeMetaCommand.php | 1 + 2 files changed, 25 insertions(+) diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 9a657709..3e7b55ae 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -66,6 +66,17 @@ 'filter.input_text.select', 'filter.input_text.input', 'filter.input', + 'filter.flyout.view', + 'filter.flyout.overlay', + 'filter.flyout.panel', + 'filter.flyout.panel_left', + 'filter.flyout.panel_right', + 'filter.flyout.header', + 'filter.flyout.title', + 'filter.flyout.close', + 'filter.flyout.body', + 'filter.flyout.footer', + 'filter.flyout.clear_all', 'editable.view', 'editable.clickable', 'editable.input', @@ -142,6 +153,17 @@ 'filter.input_text.select', 'filter.input_text.input', 'filter.input', + 'filter.flyout.view', + 'filter.flyout.overlay', + 'filter.flyout.panel', + 'filter.flyout.panel_left', + 'filter.flyout.panel_right', + 'filter.flyout.header', + 'filter.flyout.title', + 'filter.flyout.close', + 'filter.flyout.body', + 'filter.flyout.footer', + 'filter.flyout.clear_all', 'editable.view', 'editable.clickable', 'editable.input', @@ -159,6 +181,7 @@ 'editable', 'filter.boolean', 'filter.date_picker', + 'filter.flyout', 'filter.input_text', 'filter.multi_select', 'filter.number', @@ -198,6 +221,7 @@ 'editable', 'filter.boolean', 'filter.date_picker', + 'filter.flyout', 'filter.input_text', 'filter.multi_select', 'filter.number', diff --git a/src/Commands/GenerateThemeMetaCommand.php b/src/Commands/GenerateThemeMetaCommand.php index 4cf84f89..0cd476c6 100644 --- a/src/Commands/GenerateThemeMetaCommand.php +++ b/src/Commands/GenerateThemeMetaCommand.php @@ -118,6 +118,7 @@ private function collectViewAliases(Tailwind $theme, array $flatTokens): array 'filter.input_text', 'filter.number', 'filter.select', + 'filter.flyout', ]; $aliases = array_unique(array_merge($aliases, $canonicalAliases)); From 31c509a20169c662edf0b2856d842b78f38a371e Mon Sep 17 00:00:00 2001 From: virtualtear Date: Thu, 13 Aug 2026 21:20:04 +0200 Subject: [PATCH 12/12] docs(skills): document the filter flyout in the powergrid-theme skill Adds Flyout.php to the component table, the filter.flyout token block to the reference, and the rule that themes restyle the shared drawer rather than replacing its view. --- .../boost/skills/powergrid-theme/REFERENCE.md | 32 +++++++++++++++++++ .../boost/skills/powergrid-theme/SKILL.md | 1 + 2 files changed, 33 insertions(+) diff --git a/resources/boost/skills/powergrid-theme/REFERENCE.md b/resources/boost/skills/powergrid-theme/REFERENCE.md index 2f2be185..e3fadb4d 100644 --- a/resources/boost/skills/powergrid-theme/REFERENCE.md +++ b/resources/boost/skills/powergrid-theme/REFERENCE.md @@ -28,6 +28,7 @@ Companion to `SKILL.md`. Everything here is verified against the v7 source in | `src/Themes/Components/Footer.php` | `view()`, `layout()`, `pagination(Closure\|array\|string)`. | | `src/Themes/Components/Pagination.php` | Holds the pagination `view`; usually set via the string form. | | `src/Themes/Components/Filter.php` | Fluent filter builder (used by DaisyUI/Flux `filter()`). | +| `src/Themes/Components/Flyout.php` | Classes for the filter drawer, via `Filter::flyout(Closure)`. `view`, `overlay`, `panel`, `panelLeft`, `panelRight`, `header`, `title`, `close`, `body`, `footer`, `clearAll`. | | `src/Themes/Components/Component.php` | Generic sub-component (used in `filter()` array form, `editable()`, `toggleable()`). | | `src/Themes/Components/HasProperties.php` | Shared trait. `toArray()` snake_cases keys and prefixes `view*` values with `baseView`. | | `src/Support/ThemeManager.php` | `ThemeManager::theme($key, $default)` and `::view($alias)` — read `app('powergrid.theme')`. | @@ -177,6 +178,19 @@ public function filter(): array 'input' => '...input classes...', ], 'input' => '...generic filter input classes...', + 'flyout' => [ + 'view' => 'livewire-powergrid::components.themes.tailwind.filter-flyout', + 'overlay' => '...backdrop classes...', + 'panel' => '...classes shared by both drawer sides...', + 'panel_left' => '...anchoring classes when position is left...', + 'panel_right' => '...anchoring classes when position is right...', + 'header' => '...drawer header row...', + 'title' => '...drawer title...', + 'close' => '...close button...', + 'body' => '...scrollable filter area...', + 'footer' => '...drawer footer...', + 'clear_all' => '...clear all filters button...', + ], ], ]; } @@ -187,6 +201,24 @@ public function filter(): array It is equivalent — pick whichever you find clearer, and copy the exact form from `src/Themes/DaisyUI.php` if you want the fluent version. +### `filter.flyout` — the drawer + +`filter.flyout.*` styles the drawer used when `config('livewire-powergrid.filter')` +is `flyout`. Its blade lives at +`resources/views/components/themes/tailwind/filter-flyout.blade.php` and is shared +by every theme through `parentTheme`, so a theme normally overrides the classes +only, not `view`. Use the fluent `->flyout(fn (Components\Flyout $flyout) => ...)` +setter (`Components\Flyout`) or the plain array form above. + +Two constraints the drawer markup relies on: + +- `panel` must position the drawer itself (`fixed inset-y-0`) and set a stacking + context above `overlay`. `panel_left` / `panel_right` add only the edge + anchoring, because the blade picks one of the two based on the configured + position and pairs it with a matching slide-in transform. +- `panel` should be full width on small screens and constrained above the `sm` + breakpoint (`w-full max-w-full sm:w-96 sm:max-w-[90vw]`). + Because a subclass with `parentTheme = Tailwind::class` inherits Tailwind's `filter()` wholesale, you only need to override `filter()` when your framework's inputs need different classes or views. diff --git a/resources/boost/skills/powergrid-theme/SKILL.md b/resources/boost/skills/powergrid-theme/SKILL.md index 14b6ea1e..c783f061 100644 --- a/resources/boost/skills/powergrid-theme/SKILL.md +++ b/resources/boost/skills/powergrid-theme/SKILL.md @@ -213,6 +213,7 @@ public function template(): ?Theme - **Pagination is a string.** `->pagination('pagination')` — `Footer::pagination()` accepts `Closure|array|string`; all three shipped themes use the string alias. - **CSS classes live in `->layout(Closure)`.** Direct string properties on `Header`/`Table`/`Footer` are for view aliases only. - **`filter()`, `editable()`, `toggleable()` are separate methods**, not part of `struct()`. They are merged into `resolveTokens()` automatically. +- **The filter drawer is styled, not rebuilt.** `filter.flyout.*` (set with `->flyout(fn (Components\Flyout $f) => ...)`) drives the drawer used when `config('livewire-powergrid.filter')` is `flyout`. Its blade is shared by every theme through `parentTheme`, so override the classes and leave `view` alone. `panel` must carry the positioning (`fixed inset-y-0`) and stay above `overlay`; `panel_left`/`panel_right` add only the edge anchoring. - **`toggleable()` fills color tokens, not a view.** It sets `colorOn`/`colorOff`/`colorOnDark`/`colorOffDark`/`knobOn`; the shipped Toggleable blade reads them via `theme('toggleable.color_on')` etc. Do not give it a `->view()`. - **Feature views are auto-resolved.** export, toggle-columns, soft-deletes, etc. resolve via `baseView + alias` (then the `parentTheme` chain, then `components.structure.*`). Do not declare them in `struct()`. - **View aliases**: a value without `::` is prefixed with `baseView + '.'`. Use a fully-qualified `livewire-powergrid::...` path only to point at another theme's view.