diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 47e0e14ef..3e7b55aec 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', @@ -68,11 +66,26 @@ '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', 'editable.error', - 'toggleable.view' + 'toggleable.color_on', + 'toggleable.color_off', + 'toggleable.color_on_dark', + 'toggleable.color_off_dark', + 'toggleable.knob_on' ); expectedArguments( @@ -80,6 +93,8 @@ 0, 'name', 'base_view', + 'layout.wrapper', + 'layout.outside_filters', 'header.view', 'header.layout.container', 'header.layout.sub_container', @@ -93,22 +108,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 +127,7 @@ 'table.radio.base', 'table.radio.label', 'table.radio.input', + 'cols.div', 'footer.view', 'footer.layout.container', 'footer.layout.select', @@ -142,11 +153,26 @@ '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', 'editable.error', - 'toggleable.view' + 'toggleable.color_on', + 'toggleable.color_off', + 'toggleable.color_on_dark', + 'toggleable.color_off_dark', + 'toggleable.knob_on' ); expectedArguments( @@ -155,6 +181,7 @@ 'editable', 'filter.boolean', 'filter.date_picker', + 'filter.flyout', 'filter.input_text', 'filter.multi_select', 'filter.number', @@ -162,9 +189,7 @@ 'footer', 'footer.pagination', 'header', - 'header.batch-exporting', 'header.enabled-filters', - 'header.export', 'header.filters', 'header.loading', 'header.message-soft-deletes', @@ -174,7 +199,6 @@ 'header.soft-deletes', 'header.toggle-columns', 'pagination', - 'table', 'table.checkbox-all', 'table.checkbox-row', 'table.cols', @@ -197,6 +221,7 @@ 'editable', 'filter.boolean', 'filter.date_picker', + 'filter.flyout', 'filter.input_text', 'filter.multi_select', 'filter.number', @@ -204,9 +229,7 @@ 'footer', 'footer.pagination', 'header', - 'header.batch-exporting', 'header.enabled-filters', - 'header.export', 'header.filters', 'header.loading', 'header.message-soft-deletes', @@ -216,7 +239,6 @@ 'header.soft-deletes', 'header.toggle-columns', 'pagination', - 'table', 'table.checkbox-all', 'table.checkbox-row', 'table.cols', diff --git a/resources/boost/skills/powergrid-theme/REFERENCE.md b/resources/boost/skills/powergrid-theme/REFERENCE.md index 2f2be1853..e3fadb4d9 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 14b6ea1ed..c783f0610 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. diff --git a/resources/config/livewire-powergrid.php b/resources/config/livewire-powergrid.php index 5cc19311e..98275ef8c 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 diff --git a/resources/lang/ar/datatable.php b/resources/lang/ar/datatable.php index 47860a618..a770ec75a 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 fe9760650..1ab175203 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 35006236f..4ee75ca07 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 97ca9f1cf..0d20c650f 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 74c282002..b78e9d3ae 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 08320af24..20189306e 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 61aa81547..c34fb1cdd 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 cf7b17c5b..5bb9a937c 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 7ad2e6b71..5e19eb9c2 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 6770d3eed..e652b6540 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 af1ee3de4..f2f302efc 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 b3750967b..56248792f 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 94a74c024..bd6360e76 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 4aa46812c..dbde0350f 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 87b4bf52c..e83f252cd 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 3d25eaa8f..0779b572d 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 59bdcd1fd..e3f520aaf 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 e5651536e..1da0a7231 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 ea6523b19..4d4818186 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 983f736dd..2c3645554 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', diff --git a/resources/views/components/structure/table.blade.php b/resources/views/components/structure/table.blade.php index fb278e348..7cef17f7d 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 08e5037cf..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 efb15b2e8..7e9ff9638 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/filter-flyout.blade.php b/resources/views/components/themes/tailwind/filter-flyout.blade.php new file mode 100644 index 000000000..b2fe3de21 --- /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 + +
+ + + +
diff --git a/resources/views/components/themes/tailwind/filter.blade.php b/resources/views/components/themes/tailwind/filter.blade.php index f5f96ee0b..71fe42e4a 100644 --- a/resources/views/components/themes/tailwind/filter.blade.php +++ b/resources/views/components/themes/tailwind/filter.blade.php @@ -15,7 +15,7 @@
- @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 000000000..310d6d1c3 --- /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 +
diff --git a/resources/views/components/themes/tailwind/header.blade.php b/resources/views/components/themes/tailwind/header.blade.php index efb15b2e8..7e9ff9638 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/Commands/GenerateThemeMetaCommand.php b/src/Commands/GenerateThemeMetaCommand.php index 4cf84f892..0cd476c6b 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)); diff --git a/src/Concerns/Filter.php b/src/Concerns/Filter.php index 316e4b45c..76dbc4716 100644 --- a/src/Concerns/Filter.php +++ b/src/Concerns/Filter.php @@ -240,6 +240,53 @@ 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 + { + $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 +471,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 +490,7 @@ public function renderOutsideFiltersPartial(): void 'enabledFilters' => $this->enabledFilters, ]); - if (config('livewire-powergrid.filter') !== 'outside') { + if (! $this->usesFilterPanel()) { return; } @@ -450,7 +501,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 diff --git a/src/Themes/Components/Filter.php b/src/Themes/Components/Filter.php index 53c633898..9716b4b01 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 000000000..6e70b2329 --- /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; + } +} diff --git a/src/Themes/DaisyUI.php b/src/Themes/DaisyUI.php index 4bd75a5e4..6ac80025e 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 47b692f8d..7009c78d7 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 db084bf03..7f771faf3 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', + ], ], ]; }