Merge branch '3.x' into upg3

This commit is contained in:
Dan Harrin
2023-06-23 14:20:19 +01:00
139 changed files with 1890 additions and 1691 deletions
+1 -1
View File
@@ -41,7 +41,7 @@
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"post-autoload-dump": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"@php artisan filament:upgrade"
],
+34 -34
View File
@@ -3032,9 +3032,9 @@
}
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -3634,9 +3634,9 @@
}
},
"node_modules/browserslist": {
"version": "4.21.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.8.tgz",
"integrity": "sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw==",
"version": "4.21.9",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
"integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"dev": true,
"funding": [
{
@@ -3653,8 +3653,8 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001502",
"electron-to-chromium": "^1.4.428",
"caniuse-lite": "^1.0.30001503",
"electron-to-chromium": "^1.4.431",
"node-releases": "^2.0.12",
"update-browserslist-db": "^1.0.11"
},
@@ -3757,9 +3757,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001503",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz",
"integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==",
"version": "1.0.30001505",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz",
"integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==",
"dev": true,
"funding": [
{
@@ -4783,9 +4783,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.430",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.430.tgz",
"integrity": "sha512-FytjTbGwz///F+ToZ5XSeXbbSaXalsVRXsz2mHityI5gfxft7ieW3HqFLkU5V1aIrY42aflICqbmFoDxW10etg==",
"version": "1.4.434",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.434.tgz",
"integrity": "sha512-5Gvm09UZTQRaWrimRtWRO5rvaX6Kpk5WHAPKDa7A4Gj6NIPuJ8w8WNpnxCXdd+CJJt6RBU6tUw0KyULoW6XuHw==",
"dev": true
},
"node_modules/elliptic": {
@@ -4856,9 +4856,9 @@
}
},
"node_modules/envinfo": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
"integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.9.0.tgz",
"integrity": "sha512-RODB4txU+xImYDemN5DqaKC0CHk05XSVkOX4pq0hK26Qx+1LChkuOyUDlGEjYb3ACr0n9qBhFjg37hQuJvpkRQ==",
"dev": true,
"bin": {
"envinfo": "dist/cli.js"
@@ -8125,9 +8125,9 @@
}
},
"node_modules/pirates": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
"integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
"engines": {
"node": ">= 6"
@@ -8791,9 +8791,9 @@
}
},
"node_modules/prettier-plugin-blade": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/prettier-plugin-blade/-/prettier-plugin-blade-1.5.6.tgz",
"integrity": "sha512-2b7dnzvJNyfFJTJUaflZ2S5dbtHkoj+gB6VGWaMJ/zzkSg2EWEEFWOFLgqCel1NZ+XbZyySZyLGx9l4eK7a5Hg==",
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-blade/-/prettier-plugin-blade-1.6.4.tgz",
"integrity": "sha512-uJjKjXXdseu4OR8AcFx1np1lrGQVEHL/dNGr/I5wSkjARtAlVuQQiuKCR3GgFF9EZB3UTfPpWLLT7SZS4iW6nQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -9489,9 +9489,9 @@
}
},
"node_modules/semver": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -10324,9 +10324,9 @@
}
},
"node_modules/terser": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.18.0.tgz",
"integrity": "sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==",
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz",
"integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -10813,9 +10813,9 @@
}
},
"node_modules/webpack": {
"version": "5.86.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.86.0.tgz",
"integrity": "sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==",
"version": "5.87.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.87.0.tgz",
"integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -10827,7 +10827,7 @@
"acorn-import-assertions": "^1.9.0",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.14.1",
"enhanced-resolve": "^5.15.0",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -10837,7 +10837,7 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.2",
"schema-utils": "^3.2.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.7",
"watchpack": "^2.4.0",
+7 -26
View File
@@ -46,40 +46,21 @@ First, use NPM to install Tailwind CSS and its `forms` and `typography` plugins:
npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss --save-dev
```
Create a new `tailwind.config.js` file. Ensure that you add Filament's `content` path, custom `colors`, and the `plugins` you installed:
Create a new `tailwind.config.js` file. Ensure that you add Filament's `preset` which configures colors and the plugins you installed:
```js
import colors from 'tailwindcss/colors'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import preset from './vendor/filament/support/tailwind.config.preset'
export default {
presets: [preset],
content: [
'./resources/**/*.blade.php',
'./app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
darkMode: 'class',
theme: {
extend: {
colors: {
danger: colors.rose,
info: colors.blue,
primary: colors.blue,
secondary: colors.gray,
success: colors.green,
warning: colors.yellow,
},
},
},
plugins: [
forms,
typography,
],
}
```
Of course, you may specify your own custom `danger`, `gray`, `info`, `primary`, `secondary`, `success` and `warning` colors, which will be used instead. But each color needs to be a [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference), or have all 50 - 950 variants specified - a single hex code or RGB value won't work here.
### Configuring styles
In `resources/css/app.css`, import Tailwind CSS, and the Filament forms CSS, which is used for action modal forms:
@@ -174,10 +155,10 @@ composer update
php artisan filament:upgrade
```
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-update-cmd` to run it automatically:
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-autoload-dump` to run it automatically:
```json
"post-update-cmd": [
"post-autoload-dump": [
// ...
"@php artisan filament:upgrade"
],
+106 -45
View File
@@ -137,21 +137,58 @@ Action::make('approvePost')
})
```
## Customizing the modal's heading, subheading, and submit action label
## Customizing the modal's heading, description, and submit action label
You may customize the heading, subheading and label of the submit button in the modal:
You may customize the heading, description and label of the submit button in the modal:
```php
Action::make('delete')
->action(fn () => $this->record->delete())
->requiresConfirmation()
->modalHeading('Delete post')
->modalSubheading('Are you sure you\'d like to delete this post? This cannot be undone.')
->modalDescription('Are you sure you\'d like to delete this post? This cannot be undone.')
->modalSubmitActionLabel('Yes, delete it')
```
<AutoScreenshot name="actions/modal/confirmation-custom-text" alt="Confirmation modal with custom text" version="3.x" />
## Adding an icon inside the modal
You may add an [icon](https://blade-ui-kit.com/blade-icons?set=1#search) inside the modal using the `modalIcon()` method:
```php
Action::make('delete')
->action(fn () => $this->record->delete())
->requiresConfirmation()
->modalIcon('heroicon-o-trash')
```
By default, the icon will inherit the color of the action button. You may customize the color of the icon using the `modalIconColor()` method:
```php
Action::make('delete')
->action(fn () => $this->record->delete())
->requiresConfirmation()
->color('danger')
->modalIcon('heroicon-o-trash')
->modalIconColor('warning')
```
## Customizing the alignment of modal content
By default, modal content will be aligned to the start, or centered if the modal is `xs` or `sm` in [width](#changing-the-modal-width). If you wish to change the alignment of content in a modal, you can use the `modalAlignment()` method and pass it `start` or `center`:
```php
Action::make('updateAuthor')
->form([
// ...
])
->action(function (array $data): void {
// ...
})
->modalAlignment('center')
```
## Custom modal content
You may define custom content to be rendered inside your modal, which you can specify by passing a Blade view into the `modalContent()` method:
@@ -179,12 +216,12 @@ Action::make('advance')
### Adding custom modal content below the form
By default, the custom content is displayed above the modal form if there is one, but you can add content below using `modalFooter()` if you wish:
By default, the custom content is displayed above the modal form if there is one, but you can add content below using `modalContentFooter()` if you wish:
```php
Action::make('advance')
->action(fn () => $this->record->advance())
->modalFooter(view('filament.pages.actions.advance'))
->modalContentFooter(view('filament.pages.actions.advance'))
```
### Adding an action to custom modal content
@@ -215,6 +252,7 @@ Now, in the view file, you can render the action button using the name that it w
</div>
```
## Using a slide-over instead of a modal
You can open a "slide-over" dialog instead of a modal by using the `slideOver()` method:
@@ -234,6 +272,21 @@ Action::make('updateAuthor')
Instead of opening in the center of the screen, the modal content will now slide in from the right and consume the entire height of the browser.
## Making the modal footer sticky
The footer of a modal is rendered inline after the content by default. Slide-overs, however, have a sticky footer that always shows when scrolling the content. You may enable this for a modal too using `stickyModalFooter()`:
```php
Action::make('updateAuthor')
->form([
// ...
])
->action(function (array $data): void {
// ...
})
->stickyModalFooter()
```
## Changing the modal width
You can change the width of the modal by using the `modalWidth()` method. Options correspond to [Tailwind's max-width scale](https://tailwindcss.com/docs/max-width). The options are `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl`, and `screen`:
@@ -272,51 +325,13 @@ use Filament\Forms\Form;
Action::make('create')
->mountUsing(function (Form $form) {
$form->fill();
// ...
})
```
Please note that the `mountUsing()` method, by default, is used by Filament to initialize the [form](#modal-forms). If you override this method, you will need to call `$form->fill()` to ensure the form is initialized correctly. If you wish to populate the form with data, you can do so by passing an array to the `fill()` method, instead of [using `fillForm()` on the action itself](#filling-the-form-with-existing-data).
## Centering modal content
By default, the modal's content will be centered if the modal is `xs` or `sm` in [width](#changing-the-modal-width). If you wish to center the content of a larger modal, you can use the `centerModal()` method:
```php
Action::make('updateAuthor')
->form([
// ...
])
->action(function (array $data): void {
// ...
})
->centerModal()
```
## Closing the modal by clicking away
By default, when you click away from a modal, it will close itself. If you wish to disable this behavior for a specific action, you can use the `closeModalByClickingAway(false)` method:
```php
Action::make('updateAuthor')
->form([
// ...
])
->action(function (array $data): void {
// ...
})
->closeModalByClickingAway(false)
```
If you'd like to change the behaviour for all modals in the application, you can do so by calling `Modal::closedByClickingAway()` inside a service provider or middleware:
```php
use Filament\Support\View\Components\Modal;
Modal::closedByClickingAway(false);
```
## Customizing the action buttons in the footer of the modal
By default, there are two actions in the footer of a modal. The first is a button to submit, which executes the `action()`. The second button closes the modal and cancels the action.
@@ -367,9 +382,55 @@ Action::make('create')
])
->action(function (array $data, array $arguments): void {
// Create
if ($arguments['another'] ?? false) {
// Reset the form and don't close the modal
}
})
```
## Closing the modal by clicking away
By default, when you click away from a modal, it will close itself. If you wish to disable this behavior for a specific action, you can use the `closeModalByClickingAway(false)` method:
```php
Action::make('updateAuthor')
->form([
// ...
])
->action(function (array $data): void {
// ...
})
->closeModalByClickingAway(false)
```
If you'd like to change the behaviour for all modals in the application, you can do so by calling `Modal::closedByClickingAway()` inside a service provider or middleware:
```php
use Filament\Support\View\Components\Modal;
Modal::closedByClickingAway(false);
```
## Hiding the modal close button
By default, modals have a close button in the top right corner. If you wish to hide the close button, you can use the `modalCloseButton(false)` method:
```php
Action::make('updateAuthor')
->form([
// ...
])
->action(function (array $data): void {
// ...
})
->modalCloseButton(false)
```
If you'd like to hide the close button for all modals in the application, you can do so by calling `Modal::closeButton(false)` inside a service provider or middleware:
```php
use Filament\Support\View\Components\Modal;
Modal::closeButton(false);
```
@@ -19,7 +19,7 @@
{{
$attributes->class([
'filament-actions-actions',
'flex flex-wrap items-center gap-4' => ! $fullWidth,
'flex flex-wrap items-center gap-3' => ! $fullWidth,
match ($alignment) {
'center' => 'justify-center',
'right' => 'flex-row-reverse space-x-reverse',
@@ -1,19 +1,28 @@
@if ($this instanceof \Filament\Actions\Contracts\HasActions && (! $this->hasActionsModalRendered))
<form wire:submit="callMountedAction">
<form wire:submit.prevent="callMountedAction">
@php
$action = $this->getMountedAction();
@endphp
<x-filament::modal
:id="$this->getId() . '-action'"
:wire:key="$action ? $this->getId() . '.actions.' . $action->getName() . '.modal' : null"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:slide-over="$action?->isModalSlideOver()"
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
display-classes="block"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->getId() }}') close()"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedActions.length) open()"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->id . '-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:description="$action?->getModalDescription()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->id . '.actions.' . $action->getName() . '.modal' : null"
x-init="livewire = $wire.__instance"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->id }}') && $wire.mountedActions.length) open()"
x-on:modal-closed.stop="
const mountedActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedActionShouldOpenModal()) }}
@@ -21,42 +30,20 @@
return
}
if ($wire.mountedFormComponentActions.length) {
if (
('mountedFormComponentActions' in livewire?.serverMemo.data) &&
livewire.serverMemo.data.mountedFormComponentActions.length
) {
return
}
$wire.unmountAction(false)
if ('mountedActions' in livewire?.serverMemo.data) {
livewire.call('unmountAction', false)
}
"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->id }}') close()"
>
@if ($action)
@if ($action->isModalCentered())
@if ($heading = $action->getModalHeading())
<x-slot name="heading">
{{ $heading }}
</x-slot>
@endif
@if ($subheading = $action->getModalSubheading())
<x-slot name="subheading">
{{ $subheading }}
</x-slot>
@endif
@else
<x-slot name="header">
@if ($heading = $action->getModalHeading())
<x-filament::modal.heading>
{{ $heading }}
</x-filament::modal.heading>
@endif
@if ($subheading = $action->getModalSubheading())
<x-filament::modal.subheading>
{{ $subheading }}
</x-filament::modal.subheading>
@endif
</x-slot>
@endif
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
@@ -65,19 +52,7 @@
{{ $this->getMountedActionForm() }}
@endif
{{ $action->getModalFooter() }}
@if (count($modalActions = $action->getVisibleModalActions()))
<x-slot name="footer">
<x-filament::modal.actions
:full-width="$action->isModalCentered()"
>
@foreach ($modalActions as $modalAction)
{{ $modalAction }}
@endforeach
</x-filament::modal.actions>
</x-slot>
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
@@ -88,21 +63,30 @@
@endif
@if ($this instanceof \Filament\Infolists\Contracts\HasInfolists && (! $this->hasInfolistsModalRendered))
<form wire:submit="callMountedInfolistAction">
<form wire:submit.prevent="callMountedInfolistAction">
@php
$action = $this->getMountedInfolistAction();
@endphp
<x-filament::modal
:id="$this->getId() . '-infolist-action'"
:wire:key="$action ? $this->getId() . '.infolist.actions.' . $action->getName() . '.modal' : null"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:slide-over="$action?->isModalSlideOver()"
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
display-classes="block"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->getId() }}') close()"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedInfolistActions.length) open()"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->id . '-infolist-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:description="$action?->getModalDescription()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->id . '.infolist.actions.' . $action->getName() . '.modal' : null"
x-init="livewire = $wire.__instance"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->id }}') && $wire.mountedInfolistActions.length) open()"
x-on:modal-closed.stop="
const mountedInfolistActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedInfolistActionShouldOpenModal()) }}
@@ -110,42 +94,20 @@
return
}
if ($wire.mountedFormComponentActions.length) {
if (
('mountedFormComponentActions' in livewire?.serverMemo.data) &&
livewire.serverMemo.data.mountedFormComponentActions.length
) {
return
}
$wire.unmountInfolistAction(false)
if ('mountedInfolistActions' in livewire?.serverMemo.data) {
livewire.call('unmountInfolistAction', false)
}
"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->id }}') close()"
>
@if ($action)
@if ($action->isModalCentered())
@if ($heading = $action->getModalHeading())
<x-slot name="heading">
{{ $heading }}
</x-slot>
@endif
@if ($subheading = $action->getModalSubheading())
<x-slot name="subheading">
{{ $subheading }}
</x-slot>
@endif
@else
<x-slot name="header">
@if ($heading = $action->getModalHeading())
<x-filament::modal.heading>
{{ $heading }}
</x-filament::modal.heading>
@endif
@if ($subheading = $action->getModalSubheading())
<x-filament::modal.subheading>
{{ $subheading }}
</x-filament::modal.subheading>
@endif
</x-slot>
@endif
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
@@ -154,19 +116,7 @@
{{ $this->getMountedInfolistActionForm() }}
@endif
{{ $action->getModalFooter() }}
@if (count($modalActions = $action->getVisibleModalActions()))
<x-slot name="footer">
<x-filament::modal.actions
:full-width="$action->isModalCentered()"
>
@foreach ($modalActions as $modalAction)
{{ $modalAction }}
@endforeach
</x-filament::modal.actions>
</x-slot>
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
@@ -177,21 +127,30 @@
@endif
@if ($this instanceof \Filament\Tables\Contracts\HasTable && (! $this->hasTableModalRendered))
<form wire:submit="callMountedTableAction">
<form wire:submit.prevent="callMountedTableAction">
@php
$action = $this->getMountedTableAction();
@endphp
<x-filament::modal
:id="$this->getId() . '-table-action'"
:wire:key="$action ? $this->getId() . '.table.actions.' . $action->getName() . '.modal' : null"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:slide-over="$action?->isModalSlideOver()"
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
display-classes="block"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->getId() }}') close()"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedTableActions.length) open()"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->id . '-table-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:description="$action?->getModalDescription()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->id . '.table.actions.' . $action->getName() . '.modal' : null"
x-init="livewire = $wire.__instance"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->id }}') && $wire.mountedTableActions.length) open()"
x-on:modal-closed.stop="
const mountedTableActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedTableActionShouldOpenModal()) }}
@@ -199,42 +158,20 @@
return
}
if ($wire.mountedFormComponentActions.length) {
if (
('mountedFormComponentActions' in livewire?.serverMemo.data) &&
livewire.serverMemo.data.mountedFormComponentActions.length
) {
return
}
$wire.unmountTableAction(false)
if ('mountedTableActions' in livewire?.serverMemo.data) {
livewire.call('unmountTableAction', false)
}
"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->id }}') close()"
>
@if ($action)
@if ($action->isModalCentered())
@if ($heading = $action->getModalHeading())
<x-slot name="heading">
{{ $heading }}
</x-slot>
@endif
@if ($subheading = $action->getModalSubheading())
<x-slot name="subheading">
{{ $subheading }}
</x-slot>
@endif
@else
<x-slot name="header">
@if ($heading = $action->getModalHeading())
<x-filament::modal.heading>
{{ $heading }}
</x-filament::modal.heading>
@endif
@if ($subheading = $action->getModalSubheading())
<x-filament::modal.subheading>
{{ $subheading }}
</x-filament::modal.subheading>
@endif
</x-slot>
@endif
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
@@ -243,38 +180,35 @@
{{ $this->getMountedTableActionForm() }}
@endif
{{ $action->getModalFooter() }}
@if (count($modalActions = $action->getVisibleModalActions()))
<x-slot name="footer">
<x-filament::modal.actions
:full-width="$action->isModalCentered()"
>
@foreach ($modalActions as $modalAction)
{{ $modalAction }}
@endforeach
</x-filament::modal.actions>
</x-slot>
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
<form wire:submit="callMountedTableBulkAction">
<form wire:submit.prevent="callMountedTableBulkAction">
@php
$action = $this->getMountedTableBulkAction();
@endphp
<x-filament::modal
:id="$this->getId() . '-table-bulk-action'"
:wire:key="$action ? $this->getId() . '.table.bulk-actions.' . $action->getName() . '.modal' : null"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:slide-over="$action?->isModalSlideOver()"
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
display-classes="block"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->getId() }}') close()"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->getId() }}') && $wire.mountedTableBulkAction) open()"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->id . '-table-bulk-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:description="$action?->getModalDescription()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->id . '.table.bulk-actions.' . $action->getName() . '.modal' : null"
x-init="livewire = $wire.__instance"
x-on:closed-form-component-action-modal.window="if (($event.detail.id === '{{ $this->id }}') && $wire.mountedTableBulkAction) open()"
x-on:modal-closed.stop="
const mountedTableBulkActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedTableBulkActionShouldOpenModal()) }}
@@ -282,42 +216,20 @@
return
}
if ($wire.mountedFormComponentActions.length) {
if (
('mountedFormComponentActions' in livewire?.serverMemo.data) &&
livewire.serverMemo.data.mountedFormComponentActions.length
) {
return
}
$wire.mountedTableBulkAction = null
if ('mountedTableBulkAction' in livewire?.serverMemo.data) {
livewire.set('mountedTableBulkAction', null)
}
"
x-on:opened-form-component-action-modal.window="if ($event.detail.id === '{{ $this->id }}') close()"
>
@if ($action)
@if ($action->isModalCentered())
@if ($heading = $action->getModalHeading())
<x-slot name="heading">
{{ $heading }}
</x-slot>
@endif
@if ($subheading = $action->getModalSubheading())
<x-slot name="subheading">
{{ $subheading }}
</x-slot>
@endif
@else
<x-slot name="header">
@if ($heading = $action->getModalHeading())
<x-filament::modal.heading>
{{ $heading }}
</x-filament::modal.heading>
@endif
@if ($subheading = $action->getModalSubheading())
<x-filament::modal.subheading>
{{ $subheading }}
</x-filament::modal.subheading>
@endif
</x-slot>
@endif
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
@@ -326,19 +238,7 @@
{{ $this->getMountedTableBulkActionForm() }}
@endif
{{ $action->getModalFooter() }}
@if (count($modalActions = $action->getVisibleModalActions()))
<x-slot name="footer">
<x-filament::modal.actions
:full-width="$action->isModalCentered()"
>
@foreach ($modalActions as $modalAction)
{{ $modalAction }}
@endforeach
</x-filament::modal.actions>
</x-slot>
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
@@ -353,52 +253,34 @@
$action = $this->getMountedFormComponentAction();
@endphp
<form wire:submit="callMountedFormComponentAction">
<form wire:submit.prevent="callMountedFormComponentAction">
<x-filament::modal
:id="$this->getId() . '-form-component-action'"
:wire:key="$action ? $this->getId() . '.' . $action->getComponent()->getStatePath() . '.actions.' . $action->getName() . '.modal' : null"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:slide-over="$action?->isModalSlideOver()"
:alignment="$action?->getModalAlignment()"
:close-button="$action?->hasModalCloseButton()"
:close-by-clicking-away="$action?->isModalClosedByClickingAway()"
display-classes="block"
:footer-actions="$action?->getVisibleModalFooterActions()"
:footer-actions-alignment="$action?->getModalFooterActionsAlignment()"
:heading="$action?->getModalHeading()"
:icon="$action?->getModalIcon()"
:icon-color="$action?->getModalIconColor()"
:id="$this->id . '-form-component-action'"
:slide-over="$action?->isModalSlideOver()"
:sticky-footer="$action?->isModalFooterSticky()"
:description="$action?->getModalDescription()"
:visible="filled($action)"
:width="$action?->getModalWidth()"
:wire:key="$action ? $this->id . '.' . $action->getComponent()->getStatePath() . '.actions.' . $action->getName() . '.modal' : null"
x-init="livewire = $wire.__instance"
x-on:modal-closed.stop="
const mountedFormComponentActionShouldOpenModal = {{ \Illuminate\Support\Js::from($action && $this->mountedFormComponentActionShouldOpenModal()) }}
if (mountedFormComponentActionShouldOpenModal) {
$wire.unmountFormComponentAction(false)
if (mountedFormComponentActionShouldOpenModal && 'mountedFormComponentActions' in livewire?.serverMemo.data) {
livewire.call('unmountFormComponentAction', false)
}
"
>
@if ($action)
@if ($action->isModalCentered())
@if ($heading = $action->getModalHeading())
<x-slot name="heading">
{{ $heading }}
</x-slot>
@endif
@if ($subheading = $action->getModalSubheading())
<x-slot name="subheading">
{{ $subheading }}
</x-slot>
@endif
@else
<x-slot name="header">
@if ($heading = $action->getModalHeading())
<x-filament::modal.heading>
{{ $heading }}
</x-filament::modal.heading>
@endif
@if ($subheading = $action->getModalSubheading())
<x-filament::modal.subheading>
{{ $subheading }}
</x-filament::modal.subheading>
@endif
</x-slot>
@endif
{{ $action->getModalContent() }}
@if (count(($infolist = $action->getInfolist())?->getComponents() ?? []))
@@ -407,19 +289,7 @@
{{ $this->getMountedFormComponentActionForm() }}
@endif
{{ $action->getModalFooter() }}
@if (count($modalActions = $action->getVisibleModalActions()))
<x-slot name="footer">
<x-filament::modal.actions
:full-width="$action->isModalCentered()"
>
@foreach ($modalActions as $modalAction)
{{ $modalAction }}
@endforeach
</x-filament::modal.actions>
</x-slot>
@endif
{{ $action->getModalContentFooter() }}
@endif
</x-filament::modal>
</form>
+151 -37
View File
@@ -11,25 +11,44 @@ use Illuminate\Contracts\View\View;
trait CanOpenModal
{
/**
* @var array<string, StaticAction>
*/
protected array $cachedExtraModalFooterActions;
/**
* @var array<StaticAction> | Closure
*/
protected array | Closure $extraModalFooterActions = [];
protected bool | Closure | null $isModalFooterSticky = null;
/**
* @var array<string, StaticAction>
*/
protected array $cachedModalActions;
/**
* @var array<StaticAction>
*/
protected array $modalActions = [];
protected bool | Closure | null $isModalCentered = null;
protected bool | Closure $isModalSlideOver = false;
protected string | Closure | null $modalAlignment = null;
/**
* @var array<string, StaticAction>
*/
protected array $cachedModalFooterActions;
/**
* @var array<StaticAction> | Closure | null
*/
protected array | Closure | null $modalFooterActions = null;
protected string | Closure | null $modalFooterActionsAlignment = null;
protected StaticAction | bool | Closure | null $modalCancelAction = null;
protected string | Closure | null $modalCancelActionLabel = null;
@@ -40,18 +59,27 @@ trait CanOpenModal
protected View | Htmlable | Closure | null $modalContent = null;
protected View | Htmlable | Closure | null $modalFooter = null;
protected View | Htmlable | Closure | null $modalContentFooter = null;
protected string | Htmlable | Closure | null $modalHeading = null;
protected string | Htmlable | Closure | null $modalSubheading = null;
protected string | Htmlable | Closure | null $modalDescription = null;
protected string | Closure | null $modalWidth = null;
protected bool | Closure | null $isModalHidden = false;
protected bool | Closure | null $hasModalCloseButton = null;
protected bool | Closure | null $isModalClosedByClickingAway = null;
protected string | Closure | null $modalIcon = null;
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null
*/
protected string | array | Closure | null $modalIconColor = null;
public function closeModalByClickingAway(bool | Closure | null $condition = true): static
{
$this->isModalClosedByClickingAway = $condition;
@@ -59,9 +87,45 @@ trait CanOpenModal
return $this;
}
/**
* @deprecated Use `modalAlignment('center')` instead.
*/
public function centerModal(bool | Closure | null $condition = true): static
{
$this->isModalCentered = $condition;
if ($this->evaluate($condition)) {
$this->modalAlignment('center');
}
return $this;
}
public function modalAlignment(string | Closure | null $alignment = null): static
{
$this->modalAlignment = $alignment;
return $this;
}
public function modalCloseButton(bool | Closure | null $condition = true): static
{
$this->hasModalCloseButton = $condition;
return $this;
}
public function modalIcon(string | Closure | null $icon = null): static
{
$this->modalIcon = $icon;
return $this;
}
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | Closure | null $color
*/
public function modalIconColor(string | array | Closure | null $color = null): static
{
$this->modalIconColor = $color;
return $this;
}
@@ -90,17 +154,14 @@ trait CanOpenModal
*/
public function modalFooterActions(array | Closure | null $actions = null): static
{
if (! is_array($actions)) {
$this->modalFooterActions = $actions;
$this->modalFooterActions = $actions;
return $this;
}
return $this;
}
$this->modalFooterActions = [];
foreach ($actions as $action) {
$this->modalFooterActions[$action->getName()] = $action;
}
public function modalFooterActionsAlignment(string | Closure | null $alignment = null): static
{
$this->modalFooterActionsAlignment = $alignment;
return $this;
}
@@ -185,9 +246,17 @@ trait CanOpenModal
return $this;
}
/**
* @deprecated Use `modalContentFooter()` instead.
*/
public function modalFooter(View | Htmlable | Closure | null $footer = null): static
{
$this->modalFooter = $footer;
return $this->modalContentFooter($footer);
}
public function modalContentFooter(View | Htmlable | Closure | null $footer = null): static
{
$this->modalContentFooter = $footer;
return $this;
}
@@ -199,9 +268,19 @@ trait CanOpenModal
return $this;
}
public function modalDescription(string | Htmlable | Closure | null $description = null): static
{
$this->modalDescription = $description;
return $this;
}
/**
* @deprecated Use `modalDescription()` instead.
*/
public function modalSubheading(string | Htmlable | Closure | null $subheading = null): static
{
$this->modalSubheading = $subheading;
$this->modalDescription($subheading);
return $this;
}
@@ -234,18 +313,18 @@ trait CanOpenModal
return [];
}
if (is_array($this->modalFooterActions)) {
return $this->modalFooterActions;
if (isset($this->cachedModalFooterActions)) {
return $this->cachedModalFooterActions;
}
if ($this->modalFooterActions instanceof Closure) {
if ($this->modalFooterActions) {
$actions = [];
foreach ($this->evaluate($this->modalFooterActions) as $action) {
$actions[$action->getName()] = $this->prepareModalAction($action);
}
return $this->modalFooterActions = $actions;
return $this->cachedModalFooterActions = $actions;
}
$actions = [];
@@ -263,11 +342,16 @@ trait CanOpenModal
$actions['cancel'] = $cancelAction;
}
if ($this->isModalCentered()) {
if ($this->getModalFooterActionsAlignment() === 'center') {
$actions = array_reverse($actions);
}
return $this->modalFooterActions = $actions;
return $this->cachedModalFooterActions = $actions;
}
public function getModalFooterActionsAlignment(): ?string
{
return $this->evaluate($this->modalFooterActionsAlignment);
}
/**
@@ -275,12 +359,8 @@ trait CanOpenModal
*/
public function getModalActions(): array
{
if (! count($this->modalActions)) {
return [];
}
if (! is_numeric(array_key_first($this->modalActions))) {
return $this->modalActions;
if (isset($this->cachedModalActions)) {
return $this->cachedModalActions;
}
$actions = $this->getModalFooterActions();
@@ -289,7 +369,7 @@ trait CanOpenModal
$actions[$action->getName()] = $this->prepareModalAction($action);
}
return $this->modalActions = $actions;
return $this->cachedModalActions = $actions;
}
public function getModalAction(string $name): ?StaticAction
@@ -325,7 +405,7 @@ trait CanOpenModal
/**
* @return array<StaticAction>
*/
public function getVisibleModalActions(): array
public function getVisibleModalFooterActions(): array
{
return array_filter(
$this->getModalFooterActions(),
@@ -377,13 +457,22 @@ trait CanOpenModal
*/
public function getExtraModalFooterActions(): array
{
if (isset($this->cachedExtraModalFooterActions)) {
return $this->cachedExtraModalFooterActions;
}
$actions = [];
foreach ($this->evaluate($this->extraModalFooterActions) as $action) {
$actions[$action->getName()] = $this->prepareModalAction($action);
}
return $this->extraModalFooterActions = $actions;
return $this->cachedExtraModalFooterActions = $actions;
}
public function getModalAlignment(): string
{
return $this->evaluate($this->modalAlignment) ?? (in_array($this->getModalWidth(), ['xs', 'sm']) ? 'center' : 'start');
}
public function getModalSubmitActionLabel(): string
@@ -401,9 +490,9 @@ trait CanOpenModal
return $this->evaluate($this->modalContent);
}
public function getModalFooter(): View | Htmlable | null
public function getModalContentFooter(): View | Htmlable | null
{
return $this->evaluate($this->modalFooter);
return $this->evaluate($this->modalContentFooter);
}
public function getModalHeading(): string | Htmlable
@@ -411,9 +500,9 @@ trait CanOpenModal
return $this->evaluate($this->modalHeading) ?? $this->getLabel();
}
public function getModalSubheading(): string | Htmlable | null
public function getModalDescription(): string | Htmlable | null
{
return $this->evaluate($this->modalSubheading);
return $this->evaluate($this->modalDescription);
}
public function getModalWidth(): string
@@ -421,9 +510,9 @@ trait CanOpenModal
return $this->evaluate($this->modalWidth) ?? '4xl';
}
public function isModalCentered(): bool
public function isModalFooterSticky(): bool
{
return $this->evaluate($this->isModalCentered) ?? in_array($this->getModalWidth(), ['xs', 'sm']);
return $this->evaluate($this->isModalFooterSticky) ?? $this->isModalSlideOver();
}
public function isModalSlideOver(): bool
@@ -436,6 +525,11 @@ trait CanOpenModal
return $this->evaluate($this->isModalHidden);
}
public function hasModalCloseButton(): bool
{
return $this->evaluate($this->hasModalCloseButton) ?? Modal::$hasCloseButton;
}
public function isModalClosedByClickingAway(): bool
{
return $this->evaluate($this->isModalClosedByClickingAway) ?? Modal::$isClosedByClickingAway;
@@ -467,4 +561,24 @@ trait CanOpenModal
return StaticAction::make($name)
->button();
}
public function getModalIcon(): ?string
{
return $this->evaluate($this->modalIcon);
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getModalIconColor(): string | array | null
{
return $this->evaluate($this->modalIconColor) ?? $this->getColor() ?? 'primary';
}
public function stickyModalFooter(bool | Closure $condition = true): static
{
$this->isModalFooterSticky = $condition;
return $this;
}
}
@@ -9,10 +9,12 @@ trait CanRequireConfirmation
{
public function requiresConfirmation(bool | Closure $condition = true): static
{
$this->modalSubheading(fn (MountableAction $action): ?string => $action->evaluate($condition) ? __('filament-actions::modal.confirmation') : null);
$this->modalAlignment(fn (MountableAction $action): ?string => $action->evaluate($condition) ? 'center' : null);
$this->modalFooterActionsAlignment(fn (MountableAction $action): ?string => $action->evaluate($condition) ? 'center' : null);
$this->modalIcon(fn (MountableAction $action): ?string => $action->evaluate($condition) ? 'heroicon-o-exclamation-triangle' : null);
$this->modalDescription(fn (MountableAction $action): ?string => $action->evaluate($condition) ? __('filament-actions::modal.confirmation') : null);
$this->modalSubmitActionLabel(fn (MountableAction $action): ?string => $action->evaluate($condition) ? __('filament-actions::modal.actions.confirm.label') : null);
$this->modalWidth(fn (MountableAction $action): ?string => $action->evaluate($condition) ? 'sm' : null);
$this->centerModal($condition);
$this->modalWidth(fn (MountableAction $action): ?string => $action->evaluate($condition) ? 'md' : null);
return $this;
}
@@ -190,9 +190,9 @@ trait InteractsWithActions
return false;
}
return $action->getModalSubheading() ||
return $action->getModalDescription() ||
$action->getModalContent() ||
$action->getModalFooter() ||
$action->getModalContentFooter() ||
$action->getInfolist() ||
$this->mountedActionHasForm();
}
+2
View File
@@ -32,6 +32,8 @@ class DeleteAction extends Action
$this->requiresConfirmation();
$this->modalIcon('heroicon-o-trash');
$this->keyBindings(['mod+d']);
$this->hidden(static function (Model $record): bool {
@@ -30,6 +30,8 @@ class ForceDeleteAction extends Action
$this->requiresConfirmation();
$this->modalIcon('heroicon-o-trash');
$this->action(function (): void {
$result = $this->process(static fn (Model $record) => $record->forceDelete());
-22
View File
@@ -21,8 +21,6 @@ class MountableAction extends StaticAction implements HasLivewire
use Concerns\HasParentActions;
use Concerns\HasWizard;
public static string $modalActionsAlignment = 'left';
protected function setUp(): void
{
parent::setUp();
@@ -83,24 +81,4 @@ class MountableAction extends StaticAction implements HasLivewire
default => parent::resolveDefaultClosureDependencyForEvaluationByName($parameterName),
};
}
public static function alignModalActionsLeft(): void
{
static::$modalActionsAlignment = 'left';
}
public static function alignModalActionsCenter(): void
{
static::$modalActionsAlignment = 'center';
}
public static function alignModalActionsRight(): void
{
static::$modalActionsAlignment = 'right';
}
public static function getModalActionsAlignment(): string
{
return static::$modalActionsAlignment;
}
}
+2
View File
@@ -32,6 +32,8 @@ class RestoreAction extends Action
$this->requiresConfirmation();
$this->modalIcon('heroicon-o-arrow-uturn-left');
$this->action(function (Model $record): void {
if (! method_exists($record, 'restore')) {
$this->failure();
@@ -60,7 +60,7 @@ class TestsActions
public function setActionData(): Closure
{
return function (array $data): static {
foreach (Arr::prependKeysWith($data, 'mountedActionsData.' . array_key_last($this->instance()->mountedActionsData) . '.') as $key => $value) {
foreach (Arr::dot($data, prepend: 'mountedActionsData.' . array_key_last($this->instance()->mountedActionsData) . '.') as $key => $value) {
$this->set($key, $value);
}
@@ -71,8 +71,8 @@ class TestsActions
public function assertActionDataSet(): Closure
{
return function (array $data): static {
foreach ($data as $key => $value) {
$this->assertSet('mountedActionsData.' . array_key_last($this->instance()->mountedActionsData) . '.' . $key, $value);
foreach (Arr::dot($data, prepend: 'mountedActionsData.' . array_key_last($this->instance()->mountedActionsData) . '.') as $key => $value) {
$this->assertSet($key, $value);
}
return $this;
File diff suppressed because one or more lines are too long
+7 -26
View File
@@ -46,40 +46,21 @@ First, use NPM to install Tailwind CSS and its `forms` and `typography` plugins:
npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss --save-dev
```
Create a new `tailwind.config.js` file. Ensure that you add Filament's `content` path, custom `colors`, and the `plugins` you installed:
Create a new `tailwind.config.js` file. Ensure that you add Filament's `preset` which configures colors and the plugins you installed:
```js
import colors from 'tailwindcss/colors'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import preset from './vendor/filament/support/tailwind.config.preset'
export default {
presets: [preset],
content: [
'./resources/**/*.blade.php',
'./app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
darkMode: 'class',
theme: {
extend: {
colors: {
danger: colors.red,
info: colors.blue,
primary: colors.amber,
secondary: colors.gray,
success: colors.green,
warning: colors.amber,
},
},
},
plugins: [
forms,
typography,
],
}
```
Of course, you may specify your own custom `danger`, `gray`, `info`, `primary`, `secondary`, `success` and `warning` colors, which will be used instead. But each color needs to be a [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference), or have all 50 - 950 variants specified - a single hex code or RGB value won't work here.
### Configuring styles
In `resources/css/app.css`, import Tailwind CSS, and the form components CSS:
@@ -172,10 +153,10 @@ composer update
php artisan filament:upgrade
```
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-update-cmd` to run it automatically:
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-autoload-dump` to run it automatically:
```json
"post-update-cmd": [
"post-autoload-dump": [
// ...
"@php artisan filament:upgrade"
],
@@ -180,6 +180,8 @@ export default function selectFormComponent({
this.select.clearStore()
this.refreshPlaceholder()
this.setChoices(choices)
if (![null, undefined, ''].includes(this.state)) {
@@ -58,14 +58,14 @@
@if (filled($prefix))
<span
@class([
'filament-input-affix-label -me-px',
'filament-input-affix-label -me-px text-sm',
$baseAffixClasses,
'rounded-s-lg' => ! $prefixIcon,
])
@if (filled($statePath))
x-bind:class="{
'text-gray-400': ! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'text-danger-400': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'text-danger-400': @js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
@endif
>
@@ -80,14 +80,14 @@
@if (filled($suffix))
<span
@class([
'filament-input-affix-label -ms-px',
'filament-input-affix-label -ms-px text-sm',
$baseAffixClasses,
'rounded-e-lg' => ! $suffixIcon,
])
@if (filled($statePath))
x-bind:class="{
'text-gray-400': ! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'text-danger-400': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'text-danger-400': @js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
@endif
>
@@ -104,7 +104,7 @@
@if (filled($statePath))
x-bind:class="{
'text-gray-400': ! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'text-danger-400': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'text-danger-400': @js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
@endif
>
@@ -86,7 +86,15 @@
return
}
setTimeout(() => $el.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }), 200)
setTimeout(
() =>
$el.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
}),
200,
)
"
class="filament-forms-builder-component-item relative rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-800 dark:ring-white/20"
>
@@ -175,7 +183,10 @@
{{ $getAction('collapse') }}
</div>
<div x-show="isCollapsed" x-cloak>
<div
x-show="isCollapsed"
x-cloak
>
{{ $getAction('expand') }}
</div>
</li>
@@ -185,7 +196,10 @@
@endif
<div
x-bind:class="{ 'invisible h-0 !m-0 overflow-y-hidden': isCollapsed, 'p-6': ! isCollapsed}"
x-bind:class="{
'invisible h-0 !m-0 overflow-y-hidden': isCollapsed,
'p-6': ! isCollapsed,
}"
>
{{ $item }}
</div>
@@ -11,7 +11,11 @@
x-data="{
areAllCheckboxesChecked: false,
checkboxListOptions: Array.from($root.querySelectorAll('.filament-forms-checkbox-list-component-option-label')),
checkboxListOptions: Array.from(
$root.querySelectorAll(
'.filament-forms-checkbox-list-component-option-label',
),
),
search: '',
@@ -32,7 +36,11 @@
},
checkIfAllCheckboxesAreChecked: function () {
this.areAllCheckboxesChecked = this.visibleCheckboxListOptions.length === this.visibleCheckboxListOptions.filter((checkboxLabel) => checkboxLabel.querySelector('input[type=checkbox]:checked')).length
this.areAllCheckboxesChecked =
this.visibleCheckboxListOptions.length ===
this.visibleCheckboxListOptions.filter((checkboxLabel) =>
checkboxLabel.querySelector('input[type=checkbox]:checked'),
).length
},
toggleAllCheckboxes: function () {
@@ -49,10 +57,17 @@
},
updateVisibleCheckboxListOptions: function () {
this.visibleCheckboxListOptions = this.checkboxListOptions.filter((checkboxListItem) => {
return checkboxListItem.querySelector('.filament-forms-checkbox-list-component-option-label-text').innerText.toLowerCase().includes(this.search.toLowerCase())
})
}
this.visibleCheckboxListOptions = this.checkboxListOptions.filter(
(checkboxListItem) => {
return checkboxListItem
.querySelector(
'.filament-forms-checkbox-list-component-option-label-text',
)
.innerText.toLowerCase()
.includes(this.search.toLowerCase())
},
)
},
}"
>
@if (! $isDisabled)
@@ -99,10 +114,12 @@
:two-xl="$getColumns('2xl')"
:direction="$gridDirection ?? 'column'"
:x-show="$isSearchable ? 'visibleCheckboxListOptions.length' : null"
:attributes="\Filament\Support\prepare_inherited_attributes($attributes->class([
'filament-forms-checkbox-list-component gap-1',
'space-y-2' => $gridDirection !== 'row',
]))"
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes->class([
'filament-forms-checkbox-list-component gap-1',
'space-y-2' => $gridDirection !== 'row',
]))
"
>
@forelse ($getOptions() as $optionValue => $optionLabel)
<div
@@ -111,7 +128,11 @@
<label
class="filament-forms-checkbox-list-component-option-label flex items-center space-x-3 rtl:space-x-reverse"
@if ($isSearchable)
x-show="$el.querySelector('.filament-forms-checkbox-list-component-option-label-text').innerText.toLowerCase().includes(search.toLowerCase())"
x-show="
$el.querySelector('.filament-forms-checkbox-list-component-option-label-text')
.innerText.toLowerCase()
.includes(search.toLowerCase())
"
@endif
>
<input
@@ -32,10 +32,10 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('color-picker', 'filament/forms') }}"
x-data="colorPickerFormComponent({
isAutofocused: @js($isAutofocused()),
isDisabled: @js($isDisabled),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }}
})"
isAutofocused: @js($isAutofocused()),
isDisabled: @js($isDisabled),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
x-on:keydown.esc="isOpen() && $event.stopPropagation()"
{{ $getExtraAlpineAttributeBag()->class(['relative flex-1']) }}
>
@@ -70,7 +70,10 @@
class="pointer-events-none absolute inset-y-0 end-0 flex items-center pe-2"
>
<span
x-bind:style="{ 'background-color': state, ...(state ? { 'background-image': 'none' } : {}) }"
x-bind:style="{
'background-color': state,
...(state ? { 'background-image': 'none' } : {}),
}"
class="filament-forms-color-picker-component-preview relative h-7 w-7 overflow-hidden rounded-md"
></span>
</span>
@@ -25,9 +25,11 @@
@if ($isNative())
<input
x-data="{}"
x-bind:class="{
'border-gray-300 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500': ! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-danger-600 ring-danger-600 dark:border-danger-400 dark:ring-danger-400': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
x-bind:class="
'border-gray-300 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500':
! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-danger-600 ring-danger-600 dark:border-danger-400 dark:ring-danger-400':
@js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
{{
$getExtraInputAttributeBag()
@@ -59,13 +61,14 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('date-time-picker', 'filament/forms') }}"
x-data="dateTimePickerFormComponent({
displayFormat: '{{ convert_date_format($getDisplayFormat())->to('day.js') }}',
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
isAutofocused: @js($isAutofocused()),
locale: @js(app()->getLocale()),
shouldCloseOnDateSelection: @js($shouldCloseOnDateSelection()),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
displayFormat:
'{{ convert_date_format($getDisplayFormat())->to('day.js') }}',
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
isAutofocused: @js($isAutofocused()),
locale: @js(app()->getLocale()),
shouldCloseOnDateSelection: @js($shouldCloseOnDateSelection()),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
x-on:keydown.esc="isOpen() && $event.stopPropagation()"
{{
$attributes
@@ -93,7 +96,11 @@
<button
x-ref="button"
x-on:click="togglePanelVisibility()"
x-on:keydown.enter.stop.prevent="if (! $el.disabled) { isOpen() ? selectDate() : togglePanelVisibility() }"
x-on:keydown.enter.stop.prevent="
if (! $el.disabled) {
isOpen() ? selectDate() : togglePanelVisibility()
}
"
x-on:keydown.arrow-left.stop.prevent="if (! $el.disabled) focusPreviousDay()"
x-on:keydown.arrow-right.stop.prevent="if (! $el.disabled) focusNextDay()"
x-on:keydown.arrow-up.stop.prevent="if (! $el.disabled) focusPreviousWeek()"
@@ -167,7 +174,9 @@
class="grow cursor-pointer border-0 px-1 py-0 text-lg font-medium text-gray-800 outline-none focus:ring-0 dark:bg-gray-700 dark:text-gray-200"
dusk="filament.forms.{{ $statePath }}.focusedMonth"
>
<template x-for="(month, index) in months">
<template
x-for="(month, index) in months"
>
<option
x-bind:value="index"
x-text="month"
@@ -219,8 +228,13 @@
x-bind:class="{
'text-gray-700 dark:text-gray-300': ! dayIsSelected(day),
'cursor-pointer': ! dayIsDisabled(day),
'bg-primary-50 dark:bg-primary-100 dark:text-gray-600': dayIsToday(day) && ! dayIsSelected(day) && focusedDate.date() !== day && ! dayIsDisabled(day),
'bg-primary-200 dark:text-gray-600': focusedDate.date() === day && ! dayIsSelected(day),
'bg-primary-50 dark:bg-primary-100 dark:text-gray-600':
dayIsToday(day) &&
! dayIsSelected(day) &&
focusedDate.date() !== day &&
! dayIsDisabled(day),
'bg-primary-200 dark:text-gray-600':
focusedDate.date() === day && ! dayIsSelected(day),
'bg-primary-500 text-white': dayIsSelected(day),
'pointer-events-none': dayIsDisabled(day),
'opacity-50': focusedDate.date() !== day && dayIsDisabled(day),
@@ -16,51 +16,56 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('file-upload', 'filament/forms') }}"
x-data="fileUploadFormComponent({
acceptedFileTypes: @js($getAcceptedFileTypes()),
deleteUploadedFileUsing: async (fileKey) => {
return await $wire.deleteUploadedFile(@js($statePath), fileKey)
},
getUploadedFilesUsing: async () => {
return await $wire.getFormUploadedFiles(@js($statePath))
},
imageCropAspectRatio: @js($imageCropAspectRatio),
imagePreviewHeight: @js($getImagePreviewHeight()),
imageResizeMode: @js($getImageResizeMode()),
imageResizeTargetHeight: @js($imageResizeTargetHeight),
imageResizeTargetWidth: @js($imageResizeTargetWidth),
imageResizeUpscale: @js($getImageResizeUpscale()),
isAvatar: {{ $isAvatar ? 'true' : 'false' }},
isDownloadable: @js($isDownloadable()),
isOpenable: @js($isOpenable()),
isPreviewable: @js($isPreviewable()),
isReorderable: @js($isReorderable()),
loadingIndicatorPosition: @js($getLoadingIndicatorPosition()),
locale: @js(app()->getLocale()),
panelAspectRatio: @js($getPanelAspectRatio()),
panelLayout: @js($getPanelLayout()),
placeholder: @js($getPlaceholder()),
maxSize: {{ ($size = $getMaxSize()) ? "'{$size} KB'" : 'null' }},
minSize: {{ ($size = $getMinSize()) ? "'{$size} KB'" : 'null' }},
removeUploadedFileUsing: async (fileKey) => {
return await $wire.removeFormUploadedFile(@js($statePath), fileKey)
},
removeUploadedFileButtonPosition: @js($getRemoveUploadedFileButtonPosition()),
reorderUploadedFilesUsing: async (files) => {
return await $wire.reorderFormUploadedFiles(@js($statePath), files)
},
shouldAppendFiles: @js($shouldAppendFiles()),
shouldOrientImageFromExif: @js($shouldOrientImagesFromExif()),
shouldTransformImage: @js($imageCropAspectRatio || $imageResizeTargetHeight || $imageResizeTargetWidth),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
uploadButtonPosition: @js($getUploadButtonPosition()),
uploadProgressIndicatorPosition: @js($getUploadProgressIndicatorPosition()),
uploadUsing: (fileKey, file, success, error, progress) => {
// @todo: $wire.upload() / @this.upload() does not exist yet
$wire.upload(`{{ $statePath }}.${fileKey}`, file, () => {
success(fileKey)
}, error, progress)
},
})"
acceptedFileTypes: @js($getAcceptedFileTypes()),
deleteUploadedFileUsing: async (fileKey) => {
return await $wire.deleteUploadedFile(@js($statePath), fileKey)
},
getUploadedFilesUsing: async () => {
return await $wire.getFormUploadedFiles(@js($statePath))
},
imageCropAspectRatio: @js($imageCropAspectRatio),
imagePreviewHeight: @js($getImagePreviewHeight()),
imageResizeMode: @js($getImageResizeMode()),
imageResizeTargetHeight: @js($imageResizeTargetHeight),
imageResizeTargetWidth: @js($imageResizeTargetWidth),
imageResizeUpscale: @js($getImageResizeUpscale()),
isAvatar: {{ $isAvatar ? 'true' : 'false' }},
isDownloadable: @js($isDownloadable()),
isOpenable: @js($isOpenable()),
isPreviewable: @js($isPreviewable()),
isReorderable: @js($isReorderable()),
loadingIndicatorPosition: @js($getLoadingIndicatorPosition()),
locale: @js(app()->getLocale()),
panelAspectRatio: @js($getPanelAspectRatio()),
panelLayout: @js($getPanelLayout()),
placeholder: @js($getPlaceholder()),
maxSize: {{ ($size = $getMaxSize()) ? "'{$size} KB'" : 'null' }},
minSize: {{ ($size = $getMinSize()) ? "'{$size} KB'" : 'null' }},
removeUploadedFileUsing: async (fileKey) => {
return await $wire.removeFormUploadedFile(@js($statePath), fileKey)
},
removeUploadedFileButtonPosition: @js($getRemoveUploadedFileButtonPosition()),
reorderUploadedFilesUsing: async (files) => {
return await $wire.reorderFormUploadedFiles(@js($statePath), files)
},
shouldAppendFiles: @js($shouldAppendFiles()),
shouldOrientImageFromExif: @js($shouldOrientImagesFromExif()),
shouldTransformImage: @js($imageCropAspectRatio || $imageResizeTargetHeight || $imageResizeTargetWidth),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
uploadButtonPosition: @js($getUploadButtonPosition()),
uploadProgressIndicatorPosition: @js($getUploadProgressIndicatorPosition()),
uploadUsing: (fileKey, file, success, error, progress) => {
$wire.upload(
`{{ $statePath }}.${fileKey}`,
file,
() => {
success(fileKey)
},
error,
progress,
)
},
})"
wire:ignore
{{
$attributes
@@ -14,8 +14,8 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('key-value', 'filament/forms') }}"
x-data="keyValueFormComponent({
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
wire:ignore
{{
$attributes
@@ -115,7 +115,9 @@
@endif
@if ($deleteAction)
<div x-on:click="deleteRow(index)">
<div
x-on:click="deleteRow(index)"
>
{{ $deleteAction }}
</div>
@endif
@@ -10,22 +10,24 @@
ax-load="visible"
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('markdown-editor', 'filament/forms') }}"
x-data="markdownEditorFormComponent({
placeholder: @js($getPlaceholder()),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
toolbarButtons: @js($getToolbarButtons()),
translations: @js(__('filament-forms::components.markdown_editor')),
uploadFileAttachmentUsing: async (file, onSuccess, onError) => {
$wire.upload(`componentFileAttachments.{{ $statePath }}`, file, () => {
$wire.getFormComponentFileAttachmentUrl('{{ $statePath }}').then((url) => {
if (! url) {
return onError()
}
placeholder: @js($getPlaceholder()),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
toolbarButtons: @js($getToolbarButtons()),
translations: @js(__('filament-forms::components.markdown_editor')),
uploadFileAttachmentUsing: async (file, onSuccess, onError) => {
$wire.upload(`componentFileAttachments.{{ $statePath }}`, file, () => {
$wire
.getFormComponentFileAttachmentUrl('{{ $statePath }}')
.then((url) => {
if (! url) {
return onError()
}
onSuccess(url)
})
})
},
})"
onSuccess(url)
})
})
},
})"
wire:ignore
{{
$attributes
@@ -16,10 +16,12 @@
:two-xl="$getColumns('2xl')"
:is-grid="! $isInline"
direction="column"
:attributes="\Filament\Support\prepare_inherited_attributes($attributes->merge($getExtraAttributes(), escape: false)->class([
'filament-forms-radio-component flex flex-wrap gap-3',
'flex-col' => ! $isInline,
]))"
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes->merge($getExtraAttributes(), escape: false)->class([
'filament-forms-radio-component flex flex-wrap gap-3',
'flex-col' => ! $isInline,
]))
"
>
@foreach ($getOptions() as $value => $label)
@php
@@ -80,7 +80,15 @@
return
}
setTimeout(() => $el.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }), 200)
setTimeout(
() =>
$el.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
}),
200,
)
"
class="filament-forms-repeater-component-item relative rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-800 dark:ring-white/20"
>
@@ -164,7 +172,10 @@
@endif
<div
x-bind:class="{ 'invisible h-0 !m-0 overflow-y-hidden': isCollapsed, 'p-6': ! isCollapsed}"
x-bind:class="{
'invisible h-0 !m-0 overflow-y-hidden': isCollapsed,
'p-6': ! isCollapsed,
}"
>
{{ $item }}
</div>
@@ -9,22 +9,28 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('rich-editor', 'filament/forms') }}"
x-data="richEditorFormComponent({
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
x-on:trix-change="state = $event.target.value"
x-on:trix-attachment-add="
if (! $event.attachment.file) return
let attachment = $event.attachment
$wire.upload(`componentFileAttachments.{{ $statePath }}`, attachment.file, () => {
$wire.getFormComponentFileAttachmentUrl('{{ $statePath }}').then((url) => {
attachment.setAttributes({
url: url,
href: url,
})
})
})
$wire.upload(
`componentFileAttachments.{{ $statePath }}`,
attachment.file,
() => {
$wire
.getFormComponentFileAttachmentUrl('{{ $statePath }}')
.then((url) => {
attachment.setAttributes({
url: url,
href: url,
})
})
},
)
"
x-on:trix-file-accept="
if ({{ $hasToolbarButton('attachFiles') ? 'true' : 'false' }}) return
@@ -116,10 +122,7 @@
tabindex="-1"
>
<svg
@class([
'h-4',
'dark:fill-current' => config('forms.dark_mode'),
])
class="h-4 dark:fill-current"
aria-hidden="true"
focusable="false"
data-prefix="fas"
@@ -465,8 +468,10 @@
dusk="filament.forms.{{ $statePath }}"
{{ $getExtraInputAttributeBag()->class(['prose block w-full max-w-none break-words rounded-lg bg-white shadow-sm outline-none transition duration-75 dark:prose-invert focus:ring-1 focus:ring-inset dark:bg-gray-700']) }}
x-bind:class="{
'border-gray-300 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500': ! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-danger-600 ring-danger-600 dark:border-danger-400 dark:ring-danger-400': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-gray-300 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500':
! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-danger-600 ring-danger-600 dark:border-danger-400 dark:ring-danger-400':
@js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
></trix-editor>
@else
@@ -30,10 +30,12 @@
:disabled="$isDisabled"
:id="$getId()"
dusk="filament.forms.{{ $statePath }}"
:required="$isRequired() && (! ! $isConcealed())"
:attributes="\Filament\Support\prepare_inherited_attributes($getExtraInputAttributeBag()->merge([
$applyStateBindingModifiers('wire:model') => $statePath,
], escape: false))"
:required="$isRequired() && ((bool) $isConcealed())"
:attributes="
\Filament\Support\prepare_inherited_attributes($getExtraInputAttributeBag()->merge([
$applyStateBindingModifiers('wire:model') => $statePath,
], escape: false))
"
:error="$errors->has($statePath)"
:prefix="$hasPrefix"
:suffix="$hasSuffix"
@@ -70,45 +72,45 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('select', 'filament/forms') }}"
x-data="selectFormComponent({
canSelectPlaceholder: @js($canSelectPlaceholder()),
isHtmlAllowed: @js($isHtmlAllowed()),
getOptionLabelUsing: async () => {
return await $wire.getFormSelectOptionLabel(@js($statePath))
},
getOptionLabelsUsing: async () => {
return await $wire.getFormSelectOptionLabels(@js($statePath))
},
getOptionsUsing: async () => {
return await $wire.getFormSelectOptions(@js($statePath))
},
getSearchResultsUsing: async (search) => {
return await $wire.getFormSelectSearchResults(@js($statePath), search)
},
isAutofocused: @js($isAutofocused()),
isDisabled: @js($isDisabled),
isMultiple: @js($isMultiple()),
livewireId: @js($this->getId()),
hasDynamicOptions: @js($hasDynamicOptions()),
hasDynamicSearchResults: @js($hasDynamicSearchResults()),
loadingMessage: @js($getLoadingMessage()),
maxItems: @js($getMaxItems()),
maxItemsMessage: @js($getMaxItemsMessage()),
noSearchResultsMessage: @js($getNoSearchResultsMessage()),
options: @js($getOptionsForJs()),
optionsLimit: @js($getOptionsLimit()),
placeholder: @js($getPlaceholder()),
position: @js($getPosition()),
searchDebounce: @js($getSearchDebounce()),
searchingMessage: @js($getSearchingMessage()),
searchPrompt: @js($getSearchPrompt()),
searchableOptionFields: @js($getSearchableOptionFields()),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
statePath: @js($statePath),
})"
canSelectPlaceholder: @js($canSelectPlaceholder()),
isHtmlAllowed: @js($isHtmlAllowed()),
getOptionLabelUsing: async () => {
return await $wire.getFormSelectOptionLabel(@js($statePath))
},
getOptionLabelsUsing: async () => {
return await $wire.getFormSelectOptionLabels(@js($statePath))
},
getOptionsUsing: async () => {
return await $wire.getFormSelectOptions(@js($statePath))
},
getSearchResultsUsing: async (search) => {
return await $wire.getFormSelectSearchResults(@js($statePath), search)
},
isAutofocused: @js($isAutofocused()),
isDisabled: @js($isDisabled),
isMultiple: @js($isMultiple()),
livewireId: @js($this->id),
hasDynamicOptions: @js($hasDynamicOptions()),
hasDynamicSearchResults: @js($hasDynamicSearchResults()),
loadingMessage: @js($getLoadingMessage()),
maxItems: @js($getMaxItems()),
maxItemsMessage: @js($getMaxItemsMessage()),
noSearchResultsMessage: @js($getNoSearchResultsMessage()),
options: @js($getOptionsForJs()),
optionsLimit: @js($getOptionsLimit()),
placeholder: @js($getPlaceholder()),
position: @js($getPosition()),
searchDebounce: @js($getSearchDebounce()),
searchingMessage: @js($getSearchingMessage()),
searchPrompt: @js($getSearchPrompt()),
searchableOptionFields: @js($getSearchableOptionFields()),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
statePath: @js($statePath),
})"
x-on:keydown.esc="select.dropdown.isActive && $event.stopPropagation()"
wire:ignore
x-bind:class="{
'choices--error': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'choices--error': @js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
{{
$attributes
@@ -1,6 +1,5 @@
<div
x-data="{
tab: null,
init: function () {
@@ -23,7 +22,6 @@
history.pushState(null, document.title, url.toString())
},
}"
x-cloak
wire:ignore.self
@@ -3,7 +3,10 @@
@endphp
<div
x-bind:class="{ 'invisible h-0 p-0 overflow-y-hidden': tab !== '{{ $id }}', 'p-6': tab === '{{ $id }}' }"
x-bind:class="{
'invisible h-0 p-0 overflow-y-hidden': tab !== '{{ $id }}',
'p-6': tab === '{{ $id }}',
}"
x-on:expand-concealing-component.window="
error = $el.querySelector('[data-validation-error]')
@@ -17,7 +20,15 @@
return
}
setTimeout(() => $el.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }), 200)
setTimeout(
() =>
$el.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
}),
200,
)
"
{{
$attributes
@@ -11,8 +11,8 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('tags-input', 'filament/forms') }}"
x-data="tagsInputFormComponent({
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
})"
{{
$attributes
->merge($getExtraAttributes(), escape: false)
@@ -38,28 +38,30 @@
@if ($placeholder = $getPlaceholder()) placeholder="{{ $placeholder }}" @endif
type="text"
dusk="filament.forms.{{ $statePath }}"
x-on:keydown="() => {
if (['Enter', ...@js($splitKeys)].includes($event.key)) {
$event.preventDefault()
$event.stopPropagation()
x-on:keydown="
() => {
if (['Enter', ...@js($splitKeys)].includes($event.key)) {
$event.preventDefault()
$event.stopPropagation()
createTag()
createTag()
}
}
}"
"
x-on:blur="createTag()"
x-on:paste="$nextTick(() => {
const pattern = @js($splitKeys)
.map((key) => key.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'))
.join('|')
x-on:paste="
$nextTick(() => {
const pattern = @js($splitKeys).map((key) =>
key.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'),
).join('|')
newTag
.split(new RegExp(pattern, 'g'))
.forEach((tag) => {
newTag.split(new RegExp(pattern, 'g')).forEach((tag) => {
newTag = tag
createTag()
})
})"
})
"
x-model="newTag"
{{ $getExtraInputAttributeBag()->class(['webkit-calendar-picker-indicator:opacity-0 block w-full border-0 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400 sm:text-sm']) }}
/>
@@ -28,9 +28,9 @@
ax-load
ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('text-input', 'filament/forms') }}"
x-data="textInputFormComponent({
getMaskOptionsUsing: (IMask) => ({{ $getJsonMaskConfiguration() }}),
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')", lazilyEntangledModifiers: ['defer']) }},
})"
getMaskOptionsUsing: (IMask) => {{ $getJsonMaskConfiguration() }},
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')", lazilyEntangledModifiers: ['defer']) }},
})"
wire:ignore
@if ($isDebounced()) x-on:input.debounce.{{ $getDebounce() }}="$wire.$refresh" @endif
@if ($isLazy()) x-on:blur="$wire.$refresh" @endif
@@ -38,8 +38,10 @@
x-data="{}"
@endif
x-bind:class="{
'border-gray-300 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500': ! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-danger-600 ring-danger-600 dark:border-danger-400 dark:ring-danger-400': (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-gray-300 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500':
! (@js($statePath) in $wire.__instance.snapshot.memo.errors),
'border-danger-600 ring-danger-600 dark:border-danger-400 dark:ring-danger-400':
@js($statePath) in $wire.__instance.snapshot.memo.errors,
}"
{{
$getExtraInputAttributeBag()
@@ -7,7 +7,9 @@
@capture($content)
<button
x-data="{ state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }} }"
x-data="{
state: $wire.{{ $applyStateBindingModifiers("entangle('{$statePath}')") }},
}"
x-bind:aria-checked="state?.toString()"
x-on:click="state = ! state"
x-bind:class="
@@ -66,10 +68,12 @@
<x-filament::icon
:name="$getOffIcon()"
alias="forms::components.toggle.off"
:color="match ($offColor) {
'gray' => 'text-gray-400 dark:text-gray-700',
default => 'text-custom-600',
}"
:color="
match ($offColor) {
'gray' => 'text-gray-400 dark:text-gray-700',
default => 'text-custom-600',
}
"
size="h-3 w-3"
/>
@endif
@@ -87,10 +91,12 @@
<x-filament::icon
:name="$getOnIcon()"
alias="forms::components.toggle.on"
:color="match ($onColor) {
'gray' => 'text-gray-400 dark:text-gray-700',
default => 'text-custom-600',
}"
:color="
match ($onColor) {
'gray' => 'text-gray-400 dark:text-gray-700',
default => 'text-custom-600',
}
"
size="h-3 w-3"
x-cloak="x-cloak"
/>
@@ -4,7 +4,6 @@
<div
x-data="{
step: null,
init: function () {
@@ -44,7 +43,11 @@
},
autofocusFields: function () {
$nextTick(() => this.$refs[`step-${this.step}`].querySelector('[autofocus]')?.focus())
$nextTick(() =>
this.$refs[`step-${this.step}`]
.querySelector('[autofocus]')
?.focus(),
)
},
getStepIndex: function (step) {
@@ -60,11 +63,11 @@
},
isLastStep: function () {
return (this.getStepIndex(this.step) + 1) >= this.getSteps().length
return this.getStepIndex(this.step) + 1 >= this.getSteps().length
},
isStepAccessible: function(step, index) {
return @js($isSkippable()) || (this.getStepIndex(step) > index)
isStepAccessible: function (step, index) {
return @js($isSkippable()) || this.getStepIndex(step) > index
},
updateQueryString: function () {
@@ -77,7 +80,6 @@
history.pushState(null, document.title, url.toString())
},
}"
x-on:next-wizard-step.window="if ($event.detail.statePath === '{{ $statePath }}') nextStep()"
x-cloak
@@ -125,7 +127,8 @@
<div
x-bind:class="{
'bg-primary-600': getStepIndex(step) === {{ $loop->index }},
'bg-transparent group-hover:bg-gray-200 dark:group-hover:bg-gray-600': getStepIndex(step) > {{ $loop->index }},
'bg-transparent group-hover:bg-gray-200 dark:group-hover:bg-gray-600':
getStepIndex(step) > {{ $loop->index }},
}"
class="absolute start-0 top-0 h-full w-1 md:bottom-0 md:top-auto md:h-1 md:w-full"
aria-hidden="true"
@@ -140,7 +143,8 @@
'bg-primary-600': getStepIndex(step) > {{ $loop->index }},
'border-2': getStepIndex(step) <= {{ $loop->index }},
'border-primary-500': getStepIndex(step) === {{ $loop->index }},
'border-gray-300 dark:border-gray-500': getStepIndex(step) < {{ $loop->index }},
'border-gray-300 dark:border-gray-500':
getStepIndex(step) < {{ $loop->index }},
}"
class="filament-forms-wizard-component-header-step-icon flex h-10 w-10 items-center justify-center rounded-full"
>
@@ -169,7 +173,8 @@
<span
x-show="getStepIndex(step) <= {{ $loop->index }}"
x-bind:class="{
'text-gray-500 dark:text-gray-400': getStepIndex(step) !== {{ $loop->index }},
'text-gray-500 dark:text-gray-400':
getStepIndex(step) !== {{ $loop->index }},
'text-primary-500': getStepIndex(step) === {{ $loop->index }},
}"
>
@@ -228,7 +233,11 @@
<div class="flex items-center justify-between">
<div>
<div x-on:click="previousStep" x-show="! isFirstStep()" x-cloak>
<div
x-on:click="previousStep"
x-show="! isFirstStep()"
x-cloak
>
{{ $getAction('previous') }}
</div>
@@ -239,7 +248,13 @@
<div>
<div
x-on:click="$wire.dispatchFormEvent('wizard::nextStep', '{{ $statePath }}', getStepIndex(step))"
x-on:click="
$wire.dispatchFormEvent(
'wizard::nextStep',
'{{ $statePath }}',
getStepIndex(step),
)
"
x-show="! isLastStep()"
x-cloak
>
@@ -22,7 +22,15 @@
return
}
setTimeout(() => $el.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }), 200)
setTimeout(
() =>
$el.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
}),
200,
)
"
{{
$attributes
@@ -170,9 +170,9 @@ trait HasFormComponentActions
return false;
}
return $action->getModalSubheading() ||
return $action->getModalDescription() ||
$action->getModalContent() ||
$action->getModalFooter() ||
$action->getModalContentFooter() ||
$action->getInfolist() ||
$this->mountedFormComponentActionHasForm();
}
+5 -4
View File
@@ -7,6 +7,7 @@ use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Contracts\HasForms;
use Illuminate\Support\Arr;
use Illuminate\Testing\Assert;
use Livewire\Features\SupportUnitTesting\Testable;
@@ -30,8 +31,8 @@ class TestsForms
$formStatePath = $form->getStatePath();
foreach ($state as $key => $value) {
$this->set((filled($formStatePath) ? "{$formStatePath}.{$key}" : $key), $value);
foreach (Arr::dot($state, prepend: filled($formStatePath) ? "{$formStatePath}." : '') as $key => $value) {
$this->set($key, $value);
}
return $this;
@@ -51,8 +52,8 @@ class TestsForms
$formStatePath = $form->getStatePath();
foreach ($state as $key => $value) {
$this->assertSet((filled($formStatePath) ? "{$formStatePath}.{$key}" : $key), $value);
foreach (Arr::dot($state, prepend: filled($formStatePath) ? "{$formStatePath}." : '') as $key => $value) {
$this->assertSet($key, $value);
}
return $this;
+7 -26
View File
@@ -46,40 +46,21 @@ First, use NPM to install Tailwind CSS and its `forms` and `typography` plugins:
npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss --save-dev
```
Create a new `tailwind.config.js` file. Ensure that you add Filament's `content` path, custom `colors`, and the `plugins` you installed:
Create a new `tailwind.config.js` file. Ensure that you add Filament's `preset` which configures colors and the plugins you installed:
```js
import colors from 'tailwindcss/colors'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import preset from './vendor/filament/support/tailwind.config.preset'
export default {
presets: [preset],
content: [
'./resources/**/*.blade.php',
'./app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
darkMode: 'class',
theme: {
extend: {
colors: {
danger: colors.red,
info: colors.blue,
primary: colors.amber,
secondary: colors.gray,
success: colors.green,
warning: colors.amber,
},
},
},
plugins: [
forms,
typography,
],
}
```
Of course, you may specify your own custom `danger`, `gray`, `info`, `primary`, `secondary`, `success` and `warning` colors, which will be used instead. But each color needs to be a [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference), or have all 50 - 950 variants specified - a single hex code or RGB value won't work here.
### Configuring styles
In `resources/css/app.css`, import Tailwind CSS, and the Filament forms CSS, which is used for action modal forms:
@@ -174,10 +155,10 @@ composer update
php artisan filament:upgrade
```
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-update-cmd` to run it automatically:
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-autoload-dump` to run it automatically:
```json
"post-update-cmd": [
"post-autoload-dump": [
// ...
"@php artisan filament:upgrade"
],
@@ -14,14 +14,16 @@
:name="$icon"
alias="infolists::entries.icon"
color="text-custom-500"
:size="match ($size = ($getSize($state) ?? 'lg')) {
'xs' => 'h-3 w-3 filament-infolists-icon-entry-icon-size-xs',
'sm' => 'h-4 w-4 filament-infolists-icon-entry-icon-size-sm',
'md' => 'h-5 w-5 filament-infolists-icon-entry-icon-size-md',
'lg' => 'h-6 w-6 filament-infolists-icon-entry-icon-size-lg',
'xl' => 'h-7 w-7 filament-infolists-icon-entry-icon-size-xl',
default => $size,
}"
:size="
match ($size = ($getSize($state) ?? 'lg')) {
'xs' => 'h-3 w-3 filament-infolists-icon-entry-icon-size-xs',
'sm' => 'h-4 w-4 filament-infolists-icon-entry-icon-size-sm',
'md' => 'h-5 w-5 filament-infolists-icon-entry-icon-size-md',
'lg' => 'h-6 w-6 filament-infolists-icon-entry-icon-size-lg',
'xl' => 'h-7 w-7 filament-infolists-icon-entry-icon-size-xl',
default => $size,
}
"
:style="\Filament\Support\get_color_css_variables($getColor($state), shades: [500])"
/>
@endif
@@ -1,15 +1,14 @@
<div
x-data="{
tab: null,
init: function () {
this.$watch('tab', () => this.updateQueryString())
this.tab = @js(collect($getChildComponentContainer()->getComponents())
->filter(static fn (\Filament\Infolists\Components\Tabs\Tab $tab): bool => $tab->isVisible())
->get($getActiveTab() - 1)
->getId())
->filter(static fn (\Filament\Infolists\Components\Tabs\Tab $tab): bool => $tab->isVisible())
->get($getActiveTab() - 1)
->getId())
},
updateQueryString: function () {
@@ -22,7 +21,6 @@
history.pushState(null, document.title, url.toString())
},
}"
x-cloak
{{
@@ -3,7 +3,10 @@
@endphp
<div
x-bind:class="{ 'invisible h-0 p-0 overflow-y-hidden': tab !== '{{ $id }}', 'p-6': tab === '{{ $id }}' }"
x-bind:class="{
'invisible h-0 p-0 overflow-y-hidden': tab !== '{{ $id }}',
'p-6': tab === '{{ $id }}',
}"
{{
$attributes
->merge([
@@ -81,14 +81,15 @@
default => 'text-custom-600',
} => ! ($isBadge || $url),
($isProse ? match ($size = $getSize($state)) {
'sm' => 'prose-sm',
'base', 'md', null => 'prose-base',
'xs' => 'prose-xs',
'sm', null => 'prose-sm',
'base', 'md' => 'prose-base',
'lg' => 'prose-lg',
default => $size,
} : match ($size = ($isBadge ? 'sm' : $getSize($state))) {
'xs' => 'text-xs',
'sm' => 'text-sm',
'base', 'md', null => 'text-base',
'sm', null => 'text-sm',
'base', 'md' => 'text-base',
'lg' => 'text-lg',
default => $size,
}),
@@ -137,6 +138,8 @@
@endif
@class([
'inline-block',
'[&>*:first-child]:mt-0 [&>*:last-child]:mb-0' => $isProse,
'pt-2' => $isProse && (! $isLabelHidden()),
'cursor-pointer' => $isCopyable,
])
>
@@ -198,9 +198,9 @@ trait InteractsWithInfolists
return false;
}
return $action->getModalSubheading() ||
return $action->getModalDescription() ||
$action->getModalContent() ||
$action->getModalFooter() ||
$action->getModalContentFooter() ||
$action->getInfolist() ||
$this->mountedInfolistActionHasForm();
}
+7 -26
View File
@@ -46,40 +46,21 @@ First, use NPM to install Tailwind CSS and its `forms` and `typography` plugins:
npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss --save-dev
```
Create a new `tailwind.config.js` file. Ensure that you add Filament's `content` path, custom `colors`, and the `plugins` you installed:
Create a new `tailwind.config.js` file. Ensure that you add Filament's `preset` which configures colors and the plugins you installed:
```js
import colors from 'tailwindcss/colors'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import preset from './vendor/filament/support/tailwind.config.preset'
export default {
presets: [preset],
content: [
'./resources/**/*.blade.php',
'./app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
darkMode: 'class',
theme: {
extend: {
colors: {
danger: colors.red,
info: colors.blue,
primary: colors.amber,
secondary: colors.gray,
success: colors.green,
warning: colors.amber,
},
},
},
plugins: [
forms,
typography,
],
}
```
Of course, you may specify your own custom `danger`, `gray`, `info`, `primary`, `secondary`, `success` and `warning` colors, which will be used instead. But each color needs to be a [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference), or have all 50 - 950 variants specified - a single hex code or RGB value won't work here.
### Configuring styles
In `resources/css/app.css`, import Tailwind CSS:
@@ -172,10 +153,10 @@ composer update
php artisan filament:upgrade
```
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-update-cmd` to run it automatically:
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-autoload-dump` to run it automatically:
```json
"post-update-cmd": [
"post-autoload-dump": [
// ...
"@php artisan filament:upgrade"
],
@@ -56,7 +56,7 @@ Markdown text will automatically be rendered if passed to the title.
## Setting an icon
Optionally, a notification can have an icon that's displayed in front of its content. You may also set a color for the icon, which defaults to the `gray` color specified in your `tailwind.config.js` file. The icon can be the name of any Blade component. By default, the [Blade Heroicons v1](https://github.com/blade-ui-kit/blade-heroicons/tree/1.3.1) package is installed, so you may use the name of any [Heroicons v1](https://v1.heroicons.com) out of the box. However, you may create your own custom icon components or install an alternative library if you wish.
Optionally, a notification can have an [icon](https://blade-ui-kit.com/blade-icons?set=1#search) that's displayed in front of its content. You may also set a color for the icon, which is gray by default:
```php
use Filament\Notifications\Notification;
@@ -6,10 +6,9 @@
x-data="{}"
x-init="
window.addEventListener('EchoLoaded', () => {
window.Echo.private(@js($channel))
.listen('.database-notifications.sent', () => {
setTimeout(() => $wire.call('$refresh'), 500)
})
window.Echo.private(@js($channel)).listen('.database-notifications.sent', () => {
setTimeout(() => $wire.call('$refresh'), 500)
})
})
"
{{ $attributes }}
@@ -3,37 +3,35 @@
'unreadNotificationsCount',
])
<div {{ $attributes }}>
@if ($notifications->count())
<div class="mt-2 text-sm">
@if ($unreadNotificationsCount)
<x-filament::link
wire:click="markAllNotificationsAsRead"
color="gray"
tag="button"
tabindex="-1"
wire:target="markAllNotificationsAsRead"
wire:loading.attr="disabled"
class="disabled:pointer-events-none disabled:opacity-70"
>
{{ __('filament-notifications::database.modal.buttons.mark_all_as_read.label') }}
</x-filament::link>
<span>&bull;</span>
@endif
<div {{ $attributes->class('mt-2') }}>
<div class="mt-2 text-sm">
@if ($unreadNotificationsCount)
<x-filament::link
wire:click="clearNotifications"
x-on:click="close()"
wire:click="markAllNotificationsAsRead"
color="gray"
tag="button"
tabindex="-1"
wire:target="clearNotifications"
wire:target="markAllNotificationsAsRead"
wire:loading.attr="disabled"
class="disabled:pointer-events-none disabled:opacity-70"
>
{{ __('filament-notifications::database.modal.buttons.clear.label') }}
{{ __('filament-notifications::database.modal.buttons.mark_all_as_read.label') }}
</x-filament::link>
</div>
@endif
<span>&bull;</span>
@endif
<x-filament::link
wire:click="clearNotifications"
x-on:click="close()"
color="gray"
tag="button"
tabindex="-1"
wire:target="clearNotifications"
wire:loading.attr="disabled"
class="disabled:pointer-events-none disabled:opacity-70"
>
{{ __('filament-notifications::database.modal.buttons.clear.label') }}
</x-filament::link>
</div>
</div>
@@ -11,19 +11,19 @@
>
@if ($notifications->count())
<x-slot name="header">
<x-filament-notifications::database.modal.heading
:unread-notifications-count="$unreadNotificationsCount"
/>
<div>
<x-filament-notifications::database.modal.heading
:unread-notifications-count="$unreadNotificationsCount"
/>
<x-filament-notifications::database.modal.actions
:notifications="$notifications"
:unread-notifications-count="$unreadNotificationsCount"
/>
<x-filament-notifications::database.modal.actions
:notifications="$notifications"
:unread-notifications-count="$unreadNotificationsCount"
/>
</div>
</x-slot>
<div
class="-mx-6 mt-[calc(-1rem-1px)] divide-y border-b dark:border-gray-700"
>
<div class="-mx-6 divide-y border-b dark:border-gray-700">
@foreach ($notifications as $notification)
<div
@class([
@@ -6,10 +6,9 @@
x-data="{}"
x-init="
window.addEventListener('EchoLoaded', () => {
window.Echo.private(@js($channel))
.notification((notification) => {
setTimeout(() => $wire.handleBroadcastNotification(notification), 500)
})
window.Echo.private(@js($channel)).notification((notification) => {
setTimeout(() => $wire.handleBroadcastNotification(notification), 500)
})
})
if (window.Echo) {
@@ -8,19 +8,19 @@
:name="$name"
alias="notifications::notification"
color="text-custom-400"
:size="match ($size) {
'sm' => 'h-4 w-4',
'md' => 'h-5 w-5',
'lg' => 'h-6 w-6',
default => $size,
}"
{{-- format-ignore-start --}}
{{
:size="
match ($size) {
'sm' => 'h-4 w-4',
'md' => 'h-5 w-5',
'lg' => 'h-6 w-6',
default => $size,
}
"
:attributes="
$attributes
->class(['filament-notifications-notification-icon'])
->style([
\Filament\Support\get_color_css_variables($color, shades: [400]),
])
}}
{{-- format-ignore-end --}}
"
/>
@@ -5,9 +5,10 @@
<x-filament-notifications::notification
:notification="$notification"
:x-transition:enter-start="\Illuminate\Support\Arr::toCssClasses([
'opacity-0',
($this instanceof \Filament\Notifications\Http\Livewire\Notifications)
:x-transition:enter-start="
\Illuminate\Support\Arr::toCssClasses([
'opacity-0',
($this instanceof \Filament\Notifications\Http\Livewire\Notifications)
? match (static::$horizontalAlignment) {
'left' => '-translate-x-12',
'right' => 'translate-x-12',
@@ -18,7 +19,8 @@
},
}
: null,
])"
])
"
x-transition:leave-end="scale-95 opacity-0"
@class([
'w-full transition duration-300',
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -74,10 +74,10 @@ composer update
php artisan filament:upgrade
```
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-update-cmd` to run it automatically:
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-autoload-dump` to run it automatically:
```json
"post-update-cmd": [
"post-autoload-dump": [
// ...
"@php artisan filament:upgrade"
],
@@ -267,7 +267,7 @@ protected static ?string $navigationIcon = 'heroicon-o-user-group';
Alternatively, you may set a dynamic navigation icon in the `getNavigationIcon()` method:
```php
public static function getNavigationIcon(): string
public static function getNavigationIcon(): ?string
{
return 'heroicon-o-user-group';
}
-12
View File
@@ -212,18 +212,6 @@ NavigationItem::make('Analytics')
->hidden(! auth()->user()->can('view-analytics')),
```
## Changing the color of a navigation item icon
By default, navigation item icons are gray. You may change this by calling `iconColor()` on the `NavigationItem` object. The color may be `danger`, `info`, `primary`, `secondary`, `success` or `warning`:
```php
use Filament\Navigation\NavigationItem;
NavigationItem::make('Analytics')
->icon('heroicon-o-presentation-chart-line')
->iconColor('primary')
```
## Disabling resource or page navigation items
To prevent resources or pages from showing up in navigation, you may use:
+43 -85
View File
@@ -14,13 +14,15 @@ public function panel(Panel $panel): Panel
{
return $panel
// ...
->dangerColor(Color::Rose)
->grayColor(Color::Gray)
->infoColor(Color::Blue)
->primaryColor(Color::Indigo)
->secondaryColor(Color::Sky)
->successColor(Color::Emerald)
->warningColor(Color::Orange);
->colors([
'danger' => Color::Rose,
'gray' => Color::Gray,
'info' => Color::Blue,
'primary' => Color::Indigo,
'secondary' => Color::Sky,
'success' => Color::Emerald,
'warning' => Color::Orange,
]);
}
```
@@ -29,19 +31,22 @@ The `Filament\Support\Colors\Color` class contains color options for all [Tailwi
Alternatively, you may pass your own palette in as an array of RGB values:
```php
$panel->primaryColor([
50 => '238, 242, 255',
100 => '224, 231, 255',
200 => '199, 210, 254',
300 => '165, 180, 252',
400 => '129, 140, 248',
500 => '99, 102, 241',
600 => '79, 70, 229',
700 => '67, 56, 202',
800 => '55, 48, 163',
900 => '49, 46, 129',
950 => '30, 27, 75',
])
$panel
->colors([
'primary' => [
50 => '238, 242, 255',
100 => '224, 231, 255',
200 => '199, 210, 254',
300 => '165, 180, 252',
400 => '129, 140, 248',
500 => '99, 102, 241',
600 => '79, 70, 229',
700 => '67, 56, 202',
800 => '55, 48, 163',
900 => '49, 46, 129',
950 => '30, 27, 75',
],
])
```
### Generating a color palette
@@ -49,11 +54,15 @@ $panel->primaryColor([
If you want us to attempt to generate a palette for you based on a singular hex or RGB value, you can pass that in:
```php
use Filament\Support\Colors\Color;
$panel->primaryColor('#6366f1')
$panel->primaryColor('rgb(99, 102, 241)')
$panel
->colors([
'primary' => '#6366f1',
])
$panel
->colors([
'primary' => 'rgb(99, 102, 241)',
])
```
## Changing the font
@@ -97,74 +106,23 @@ $panel->font(
## Creating a custom theme
Filament allows you to change the fonts and color scheme used in the UI, by compiling a custom stylesheet to replace the default one. This custom stylesheet is called a "theme".
Filament allows you to change the CSS used to render the UI, by compiling a custom stylesheet to replace the default one. This custom stylesheet is called a "theme".
Themes use [Tailwind CSS](https://tailwindcss.com), the Tailwind Forms plugin, and the Tailwind Typography plugin, [Autoprefixer](https://github.com/postcss/autoprefixer), and [Tippy.js](https://atomiks.github.io/tippyjs). You may install these through NPM:
Themes use [Tailwind CSS](https://tailwindcss.com), the Tailwind Forms plugin, and the Tailwind Typography plugin, [Autoprefixer](https://github.com/postcss/autoprefixer), and [Tippy.js](https://atomiks.github.io/tippyjs).
To create a custom theme for a panel, you can use the `php artisan make:filament-theme` command:
```bash
npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss autoprefixer tippy.js --save-dev
php artisan make:filament-theme
```
To finish installing Tailwind, you must create a new `tailwind.config.js` file in the root of your project. The easiest way to do this is by running `npx tailwindcss init`.
If you have more than one panel, you can specify the panel you want to create a theme for:
In `tailwind.config.js`, require Filament's "preset", which gives you a working Tailwind configuration that you can override however you wish:
```js
import preset from './vendor/filament/filament/tailwind.config.preset'
export default {
presets: [preset],
}
```bash
php artisan make:filament-theme admin
```
If you use Vite to compile assets, in your `vite.config.js` file, register the `filament.css` theme file:
```js
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: [
laravel({
input: [
// ...
'resources/css/filament.css',
],
// ...
}),
],
})
```
And add Tailwind to the `postcss.config.js` file:
```js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
```
In `/resources/css/filament.css`, import Filament's base theme CSS:
```css
@import '../../vendor/filament/filament/resources/css/index.css';
```
Now, you may register the theme file in Filament's [configuration](configuration):
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->viteTheme('resources/css/filament.css')
}
```
The command will create a CSS file and Tailwind Configuration file in the `/resources/css/filament` directory. You can then customize the theme by editing these files. It will also give you instructions on how to compile the theme and register it in Filament. **Please follow the instructions in the command to complete the setup process.**
## Non-sticky topbar
@@ -8,18 +8,19 @@
<div
@if ($this->areFormActionsSticky())
x-data="{
isSticky: false,
evaluatePageScrollPosition: function() {
this.isSticky = (window.scrollY + (window.innerHeight * 2)) <= document.body.scrollHeight
evaluatePageScrollPosition: function () {
this.isSticky =
window.scrollY + window.innerHeight * 2 <=
document.body.scrollHeight
},
}"
x-init="evaluatePageScrollPosition"
x-on:scroll.window="evaluatePageScrollPosition"
x-bind:class="{
'filament-form-actions-sticky-panel sticky bottom-0 -mx-4 transform bg-white p-4 shadow-lg transition ring-1 ring-gray-950/5 md:-translate-y-4 md:rounded-xl dark:bg-gray-800 dark:ring-white/20': isSticky,
'filament-form-actions-sticky-panel sticky bottom-0 -mx-4 transform bg-white p-4 shadow-lg transition ring-1 ring-gray-950/5 md:-translate-y-4 md:rounded-xl dark:bg-gray-800 dark:ring-white/20':
isSticky,
}"
@endif
>
@@ -11,14 +11,10 @@
{{
$attributes
->class([
'min-h-4 ms-auto inline-flex items-center justify-center whitespace-normal rounded-xl px-2 py-0.5 text-xs font-medium tracking-tight',
match ($active) {
true => 'bg-gray-900/10 text-white',
false => 'bg-custom-500/10 text-custom-700 dark:text-custom-500',
},
'inline-flex whitespace-normal rounded-full px-1.5 py-0.5 text-xs font-medium tracking-tighter bg-custom-500/20 text-custom-700 dark:text-custom-500',
])
->style([
\Filament\Support\get_color_css_variables($badgeColor, shades: [500, 700]) => ! $active,
\Filament\Support\get_color_css_variables($badgeColor, shades: [500, 700]),
])
}}
>
@@ -1,98 +1,89 @@
@props([
'parentGroup' => null,
'collapsible' => true,
'hasItemIcons' => false,
'icon' => null,
'items' => [],
'label' => null,
])
<li
x-data="{ label: {{ \Illuminate\Support\Js::from((filled($parentGroup) ? "{$parentGroup}." : null) . $label) }} }"
class="filament-sidebar-group"
@if (filled($parentGroup))
x-bind:class="{{ config('filament.layout.sidebar.is_collapsible_on_desktop') ? '$store.sidebar.isOpen' : 'true' }} ? 'ms-11 pe-3 pt-3' : 'hidden'"
@endif
x-data="{ label: @js($label) }"
@class([
'filament-sidebar-group grid gap-y-1',
])
>
@if ($label)
<button
<div
@if ($collapsible)
x-on:click.prevent="$store.sidebar.toggleCollapsedGroup(label)"
x-on:click="$store.sidebar.toggleCollapsedGroup(label)"
@endif
@if (filament()->isSidebarCollapsibleOnDesktop())
x-show="$store.sidebar.isOpen"
x-transition:enter="lg:transition delay-100"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
class="flex w-full items-center justify-between"
@class([
'flex items-center gap-x-3 px-3 py-2',
'cursor-pointer' => $collapsible,
])
>
<div class="flex items-center gap-4">
@if ($icon)
<x-filament::icon
:name="$icon"
alias="panels::sidebar.group"
color="text-gray-600 dark:text-gray-300"
size="h-3 w-3"
class="ms-1 shrink-0"
/>
@endif
<p
class="flex-1 text-xs font-bold uppercase tracking-wider text-primary-600 dark:text-primary-500"
>
{{ $label }}
</p>
</div>
@if ($collapsible)
@if ($icon)
<x-filament::icon
name="heroicon-m-chevron-up"
alias="panels::sidebar.group.collapse"
size="h-5 w-5"
class="text-gray-600 transition dark:text-gray-300"
x-bind:class="! $store.sidebar.groupIsCollapsed(label) || 'rotate-180'"
:name="$icon"
alias="panels::sidebar.group"
color="text-gray-600 dark:text-gray-400"
size="h-6 w-6"
/>
@endif
</button>
<span
class="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ $label }}
</span>
@if ($collapsible)
<x-filament::icon-button
color="gray"
icon="heroicon-m-chevron-down"
icon-alias="panels::sidebar.group.collapse"
x-on:click.stop="$store.sidebar.toggleCollapsedGroup(label)"
x-bind:class="{ 'rotate-180': ! $store.sidebar.groupIsCollapsed(label) }"
class="-my-2.5 -me-2.5"
/>
@endif
</div>
@endif
<ul
x-show="! ($store.sidebar.groupIsCollapsed(label) && {{ filament()->isSidebarCollapsibleOnDesktop() ? '$store.sidebar.isOpen' : 'true' }})"
x-show="! ($store.sidebar.groupIsCollapsed(label) && ($store.sidebar.isOpen || @js(! filament()->isSidebarCollapsibleOnDesktop())))"
@if (filament()->isSidebarCollapsibleOnDesktop())
x-transition:enter="lg:transition delay-100"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
x-collapse.duration.200ms
@class([
'-mx-3 space-y-1 text-sm',
'mt-2' => $label,
'grid gap-y-1',
])
>
@foreach ($items as $item)
@if ($item instanceof \Filament\Navigation\NavigationItem)
@if ($item->isVisible())
<x-filament::layouts.app.sidebar.item
:active="$item->isActive()"
:icon="$item->getIcon()"
:iconColor="$item->getIconColor()"
:active-icon="$item->getActiveIcon()"
:url="$item->getUrl()"
:badge="$item->getBadge()"
:badgeColor="$item->getBadgeColor()"
:shouldOpenUrlInNewTab="$item->shouldOpenUrlInNewTab()"
>
{{ $item->getLabel() }}
</x-filament::layouts.app.sidebar.item>
@endif
@elseif ($item instanceof \Filament\Navigation\NavigationGroup)
<x-filament::layouts.app.sidebar.group
:label="$item->getLabel()"
@if ($item->isVisible())
<x-filament::layouts.app.sidebar.item
:active="$item->isActive()"
:has-grouped-border="! $hasItemIcons"
:icon="$item->getIcon()"
:collapsible="$item->isCollapsible()"
:items="$item->getItems()"
:parentGroup="(filled($parentGroup) ? ('$parentGroup' . '.') : null) . $label"
/>
:first="$loop->first"
:last="$loop->last"
:active-icon="$item->getActiveIcon()"
:url="$item->getUrl()"
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
>
{{ $item->getLabel() }}
</x-filament::layouts.app.sidebar.item>
@endif
@endforeach
</ul>
@@ -6,23 +6,31 @@
x-data="{}"
@if (filament()->isSidebarCollapsibleOnDesktop())
x-cloak
x-bind:class="$store.sidebar.isOpen ? 'filament-sidebar-open translate-x-0 max-w-[20em] lg:max-w-[--sidebar-width]' : '-translate-x-full lg:translate-x-0 lg:max-w-[--collapsed-sidebar-width] rtl:lg:-translate-x-0 rtl:translate-x-full'"
x-bind:class="
$store.sidebar.isOpen
? 'filament-sidebar-open translate-x-0 max-w-[20em] lg:max-w-[--sidebar-width]'
: '-translate-x-full lg:translate-x-0 lg:max-w-[--collapsed-sidebar-width] rtl:lg:-translate-x-0 rtl:translate-x-full'
"
@else
@if (filament()->hasTopNavigation() || filament()->isSidebarFullyCollapsibleOnDesktop())
x-cloak
@else
x-cloak="-lg"
@endif
x-bind:class="$store.sidebar.isOpen ? 'filament-sidebar-open translate-x-0' : '-translate-x-full rtl:translate-x-full'"
x-bind:class="
$store.sidebar.isOpen
? 'filament-sidebar-open translate-x-0'
: '-translate-x-full rtl:translate-x-full'
"
@endif
@class([
'filament-sidebar fixed inset-y-0 start-0 z-20 flex h-screen w-[--sidebar-width] flex-col bg-white transition-all dark:bg-gray-800 lg:z-0 lg:bg-transparent lg:dark:bg-transparent',
'filament-sidebar fixed inset-y-0 start-0 z-20 flex h-screen w-[--sidebar-width] flex-col bg-white transition-all dark:bg-gray-800 lg:z-0 lg:bg-transparent dark:lg:bg-transparent',
'lg:translate-x-0 rtl:lg:-translate-x-0' => ! (filament()->isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop() || filament()->hasTopNavigation()),
'lg:-translate-x-full rtl:lg:translate-x-full' => filament()->hasTopNavigation(),
])
>
<header
class="filament-sidebar-header relative flex h-[4rem] shrink-0 items-center justify-center border-b bg-white dark:border-gray-700 dark:bg-gray-800"
class="filament-sidebar-header relative flex h-[4rem] shrink-0 items-center justify-center bg-white shadow-[0_1px_0_0_theme(colors.gray.950_/_5%)] dark:bg-gray-800 dark:shadow-[0_1px_0_0_theme(colors.white_/_20%)]"
>
<div
@class([
@@ -30,7 +38,7 @@
'lg:px-4' => filament()->isSidebarCollapsibleOnDesktop() && (! filament()->isSidebarFullyCollapsibleOnDesktop()),
])
x-show="$store.sidebar.isOpen || @js(! filament()->isSidebarCollapsibleOnDesktop()) || @js(filament()->isSidebarFullyCollapsibleOnDesktop())"
x-transition:enter="lg:transition delay-100"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
>
@@ -38,9 +46,13 @@
<button
type="button"
class="filament-sidebar-collapse-button hidden h-10 w-10 shrink-0 items-center justify-center rounded-full text-primary-500 outline-none hover:bg-gray-500/5 focus:bg-primary-500/10 lg:flex"
x-bind:aria-label="$store.sidebar.isOpen ? '{{ __('filament::layout.buttons.sidebar.collapse.label') }}' : '{{ __('filament::layout.buttons.sidebar.expand.label') }}'"
x-bind:aria-label="
$store.sidebar.isOpen
? '{{ __('filament::layout.buttons.sidebar.collapse.label') }}'
: '{{ __('filament::layout.buttons.sidebar.expand.label') }}'
"
x-on:click.stop="$store.sidebar.isOpen ? $store.sidebar.close() : $store.sidebar.open()"
x-transition:enter="lg:transition delay-100"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
>
@@ -90,10 +102,14 @@
<button
type="button"
class="filament-sidebar-close-button flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-primary-500 outline-none hover:bg-gray-500/5 focus:bg-primary-500/10"
x-bind:aria-label="$store.sidebar.isOpen ? '{{ __('filament::layout.buttons.sidebar.collapse.label') }}' : '{{ __('filament::layout.buttons.sidebar.expand.label') }}'"
x-bind:aria-label="
$store.sidebar.isOpen
? '{{ __('filament::layout.buttons.sidebar.collapse.label') }}'
: '{{ __('filament::layout.buttons.sidebar.expand.label') }}'
"
x-on:click.stop="$store.sidebar.isOpen ? $store.sidebar.close() : $store.sidebar.open()"
x-show="(! $store.sidebar.isOpen) && @js(! filament()->isSidebarFullyCollapsibleOnDesktop())"
x-transition:enter="lg:transition delay-100"
x-show="! $store.sidebar.isOpen && @js(! filament()->isSidebarFullyCollapsibleOnDesktop())"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
>
@@ -107,7 +123,7 @@
</header>
<nav
class="filament-sidebar-nav flex-1 overflow-y-auto overflow-x-hidden py-6"
class="filament-sidebar-nav flex-1 overflow-y-auto overflow-x-hidden py-6 shadow-lg lg:shadow-none"
>
{{ filament()->renderHook('sidebar.start') }}
@@ -139,13 +155,14 @@
</script>
@if (filament()->hasNavigation())
<ul class="space-y-6 px-6">
<ul class="-mx-3 grid gap-y-3 px-6">
@foreach ($navigation as $group)
<x-filament::layouts.app.sidebar.group
:label="$group->getLabel()"
:icon="$group->getIcon()"
:collapsible="$group->isCollapsible()"
:items="$group->getItems()"
:has-item-icons="$group->hasItemIcons()"
/>
@endforeach
</ul>
@@ -3,15 +3,17 @@
'activeIcon' => null,
'badge' => null,
'badgeColor' => null,
'icon',
'iconColor' => 'gray',
'hasGroupedBorder' => false,
'last' => false,
'first' => false,
'icon' => null,
'shouldOpenUrlInNewTab' => false,
'url',
])
<li
@class([
'filament-sidebar-item overflow-hidden',
'filament-sidebar-item',
'filament-sidebar-item-active' => $active,
])
>
@@ -37,32 +39,55 @@
x-tooltip.html="tooltip"
@endif
@class([
'flex items-center justify-center gap-3 rounded-lg px-3 py-2 font-medium transition',
'hover:bg-gray-500/5 focus:bg-gray-500/5 dark:text-gray-300 dark:hover:bg-gray-700' => ! $active,
'bg-primary-500 text-white' => $active,
'relative flex items-center justify-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 outline-none transition hover:bg-gray-950/5 focus:bg-gray-950/5 dark:text-gray-300 dark:hover:bg-white/5 dark:focus:bg-white/5',
'bg-gray-950/5 text-primary-600 dark:bg-white/5 dark:text-primary-400' => $active,
])
>
<x-filament::icon
:name="($active && $activeIcon) ? $activeIcon : $icon"
alias="panels::sidebar.item"
:color="$active ? null : 'text-custom-600 dark:text-custom-500'"
size="h-6 w-6"
@style([
\Filament\Support\get_color_css_variables($iconColor, shades: [500, 600]) => ! $active,
])
/>
@if ($icon)
<x-filament::icon
:name="($active && $activeIcon) ? $activeIcon : $icon"
alias="panels::sidebar.item"
color="text-custom-600 dark:text-custom-400"
size="h-6 w-6"
:style="\Filament\Support\get_color_css_variables(($active ? 'primary' : 'gray'), shades: [400, 600])"
/>
@elseif ($hasGroupedBorder)
<div
class="filament-sidebar-item-grouped-border relative flex h-6 w-6 items-center justify-center"
>
@if (! $first)
<div
class="absolute -top-1/2 bottom-1/2 w-px bg-gray-300 dark:bg-gray-600"
></div>
@endif
<div
@if (! $last)
<div
class="absolute -bottom-1/2 top-1/2 w-px bg-gray-300 dark:bg-gray-600"
></div>
@endif
<div
@class([
'relative h-1.5 w-1.5 rounded-full',
'bg-gray-400' => ! $active,
'bg-primary-600 dark:bg-primary-400' => $active,
])
></div>
</div>
@endif
<span
@if (filament()->isSidebarCollapsibleOnDesktop())
x-show="$store.sidebar.isOpen"
x-transition:enter="lg:transition delay-100"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
class="flex flex-1"
class="flex-1"
>
{{ $slot }}
</div>
</span>
@if (filled($badge))
<x-filament::layouts.app.sidebar.badge
@@ -4,10 +4,10 @@
])
<header
{{ $attributes->class(['filament-main-topbar sticky top-0 z-10 border-b bg-white dark:border-gray-700 dark:bg-gray-800']) }}
{{ $attributes->class(['filament-main-topbar sticky top-0 z-10 bg-white shadow-[0_1px_0_0_theme(colors.gray.950_/_5%)] dark:border-gray-700 dark:bg-gray-800 dark:shadow-[0_1px_0_0_theme(colors.white_/_20%)]']) }}
>
<div
class="-mt-px flex h-16 items-center justify-between px-2 sm:px-4 md:px-6 lg:px-8"
class="flex h-16 items-center justify-between px-2 sm:px-4 md:px-6 lg:px-8"
>
<div class="flex flex-1 items-center">
{{ filament()->renderHook('topbar.start') }}
@@ -15,7 +15,11 @@
<button
x-cloak
x-data="{}"
x-bind:aria-label="$store.sidebar.isOpen ? '{{ __('filament::layout.buttons.sidebar.collapse.label') }}' : '{{ __('filament::layout.buttons.sidebar.expand.label') }}'"
x-bind:aria-label="
$store.sidebar.isOpen
? '{{ __('filament::layout.buttons.sidebar.collapse.label') }}'
: '{{ __('filament::layout.buttons.sidebar.expand.label') }}'
"
x-on:click="$store.sidebar.isOpen ? $store.sidebar.close() : $store.sidebar.open()"
@class([
'filament-sidebar-open-button flex h-10 w-10 shrink-0 items-center justify-center rounded-full outline-none hover:bg-gray-500/5 focus:bg-primary-500/10',
@@ -43,7 +47,7 @@
</div>
@if (filament()->hasNavigation())
<ul class="hidden flex-wrap items-center gap-3 lg:flex">
<ul class="hidden flex-wrap items-center gap-x-1 lg:flex">
@foreach ($navigation as $group)
@if ($groupLabel = $group->getLabel())
<x-filament::dropdown placement="bottom-start">
@@ -8,26 +8,35 @@
'url' => null,
])
@php
$tag = $url ? 'a' : 'button';
@endphp
<li
@class(['filament-topbar-item overflow-hidden', 'filament-topbar-item-active' => $active])
@class([
'filament-topbar-item overflow-hidden',
'filament-topbar-item-active' => $active,
])
>
<a
<{{ $tag }}
@if ($url)
href="{{ $url }}"
@if ($shouldOpenUrlInNewTab) target="_blank" @endif
@else
type="button"
@endif
@class([
'flex items-center justify-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition',
'hover:bg-gray-500/5 focus:bg-gray-500/5 dark:text-gray-300 dark:hover:bg-gray-700' => ! $active,
'bg-primary-500 text-white' => $active,
'flex items-center justify-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 outline-none transition hover:bg-gray-950/5 focus:bg-gray-950/5 dark:text-gray-300 dark:hover:bg-white/5 dark:focus:bg-white/5',
'bg-gray-950/5 text-primary-600 dark:bg-white/5 dark:text-primary-400' => $active,
])
>
@if ($icon || $activeIcon)
<x-filament::icon
:name="($active && $activeIcon) ? $activeIcon : $icon"
alias="panels::topbar.item"
:color="(! $active) ? 'text-gray-600 dark:text-gray-500' : null"
size="h-5 w-5"
color="text-custom-600 dark:text-custom-400"
size="h-6 w-6"
:style="\Filament\Support\get_color_css_variables(($active ? 'primary' : 'gray'), shades: [400, 600])"
/>
@endif
@@ -42,5 +51,5 @@
:active="$active"
/>
@endif
</a>
</{{ $tag }}>
</li>
@@ -24,14 +24,25 @@
{{ filament()->renderHook('user-menu.account.before') }}
<x-filament::dropdown.header
:color="$accountItem?->getColor() ?? 'gray'"
:icon="$accountItem?->getIcon() ?? 'heroicon-m-user-circle'"
:href="$accountItemUrl"
:tag="filled($accountItemUrl) ? 'a' : 'div'"
>
{{ $accountItem?->getLabel() ?? filament()->getUserName($user) }}
</x-filament::dropdown.header>
@if (filled($accountItemUrl))
<x-filament::dropdown.list>
<x-filament::dropdown.list.item
:color="$accountItem->getColor() ?? 'gray'"
:icon="$accountItem->getIcon() ?? 'heroicon-m-user-circle'"
:href="$accountItemUrl"
tag="a"
>
{{ $accountItem->getLabel() ?? filament()->getUserName($user) }}
</x-filament::dropdown.list.item>
</x-filament::dropdown.list>
@else
<x-filament::dropdown.header
:color="$accountItem?->getColor() ?? 'gray'"
:icon="$accountItem?->getIcon() ?? 'heroicon-m-user-circle'"
>
{{ $accountItem?->getLabel() ?? filament()->getUserName($user) }}
</x-filament::dropdown.header>
@endif
{{ filament()->renderHook('user-menu.account.after') }}
@@ -43,29 +54,49 @@
init: function () {
this.theme = localStorage.getItem('theme') || 'system'
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
if (this.theme !== 'system') {
return
}
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (event) => {
if (this.theme !== 'system') {
return
}
if (event.matches && (! document.documentElement.classList.contains('dark'))) {
document.documentElement.classList.add('dark')
} else if ((! event.matches) && document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark')
}
})
if (
event.matches &&
! document.documentElement.classList.contains('dark')
) {
document.documentElement.classList.add('dark')
} else if (
! event.matches &&
document.documentElement.classList.contains('dark')
) {
document.documentElement.classList.remove('dark')
}
})
$watch('theme', () => {
localStorage.setItem('theme', this.theme)
if (this.theme === 'dark' && (! document.documentElement.classList.contains('dark'))) {
if (
this.theme === 'dark' &&
! document.documentElement.classList.contains('dark')
) {
document.documentElement.classList.add('dark')
} else if (this.theme === 'light' && document.documentElement.classList.contains('dark')) {
} else if (
this.theme === 'light' &&
document.documentElement.classList.contains('dark')
) {
document.documentElement.classList.remove('dark')
} else if (this.theme === 'system') {
if (this.isSystemDark() && (! document.documentElement.classList.contains('dark'))) {
if (
this.isSystemDark() &&
! document.documentElement.classList.contains('dark')
) {
document.documentElement.classList.add('dark')
} else if ((! this.isSystemDark()) && document.documentElement.classList.contains('dark')) {
} else if (
! this.isSystemDark() &&
document.documentElement.classList.contains('dark')
) {
document.documentElement.classList.remove('dark')
}
}
@@ -1,12 +0,0 @@
<?php
namespace Filament\Commands\Aliases;
use Filament\Commands;
class CompileThemeCommand extends Commands\CompileThemeCommand
{
protected $hidden = true;
protected $signature = 'filament:theme {panel?} {--W|watch}';
}
@@ -0,0 +1,12 @@
<?php
namespace Filament\Commands\Aliases;
use Filament\Commands;
class MakeThemeCommand extends Commands\MakeThemeCommand
{
protected $hidden = true;
protected $signature = 'filament:theme {panel?} {--F|force}';
}
@@ -1,66 +0,0 @@
<?php
namespace Filament\Commands;
use Filament\Facades\Filament;
use Filament\Panel;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
class CompileThemeCommand extends Command
{
use CanManipulateFiles;
protected $description = 'Compiles a Filament theme.';
protected $signature = 'filament:compile-theme {panel?} {--W|watch}';
public function handle(): int
{
exec('npm -v', $npmVersion, $npmVersionExistCode);
if ($npmVersionExistCode !== 0) {
$this->error('Node.js is not installed. Please install before continuing.');
return static::FAILURE;
}
$this->info("Using Node.js v{$npmVersion[0]}");
$panelId = $this->argument('panel');
if (! $panelId) {
$panelIds = collect(Filament::getPanels())
->map(fn (Panel $panel): string => $panel->getId())
->filter(fn (string $panelId): bool => (
file_exists(resource_path("css/filament/{$panelId}/theme.css")) &&
file_exists(resource_path("css/filament/{$panelId}/tailwind.config.js"))
))
->all();
if (empty($panelIds)) {
$this->error('No themes found. Please create a theme using `php artisan make:filament-theme` before continuing.');
return static::FAILURE;
}
$defaultPanelId = in_array(Filament::getDefaultPanel()->getId(), $panelIds) ?
Filament::getDefaultPanel()->getId() :
Arr::first($panelIds);
$panelId = (count($panelIds) > 1) ? $this->choice(
'Which panel\'s theme would you like to compile?',
$panelIds,
$defaultPanelId,
) : Arr::first($panelIds);
}
exec('npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss autoprefixer tippy.js --save-dev');
exec("npx tailwindcss --input ./resources/css/filament/{$panelId}/theme.css --output ./public/css/filament/{$panelId}/theme.css --config ./resources/css/filament/{$panelId}/tailwind.config.js --minify" . ($this->option('watch') ? ' --watch' : ''));
$this->components->info("Compiled resources/css/filament/{$panelId}/theme.css!");
return static::SUCCESS;
}
}
@@ -19,6 +19,18 @@ class MakeThemeCommand extends Command
public function handle(): int
{
exec('npm -v', $npmVersion, $npmVersionExistCode);
if ($npmVersionExistCode !== 0) {
$this->error('Node.js is not installed. Please install before continuing.');
return static::FAILURE;
}
$this->info("Using Node.js v{$npmVersion[0]}");
exec('npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss autoprefixer tippy.js --save-dev');
$panel = $this->argument('panel');
if ($panel) {
@@ -68,11 +80,34 @@ class MakeThemeCommand extends Command
'viewPathPrefix' => $viewPathPrefix,
]);
$this->call('filament:compile-theme', ['panel' => $panelId]);
$this->components->info("Successfully created resources/css/filament/{$panelId}/theme.css and resources/css/filament/{$panelId}/tailwind.config.js!");
$this->components->info("Make sure to register the theme in the {$panelId} panel provider using `theme(asset('css/filament/{$panelId}/theme.css'))`");
if (! file_exists(base_path('vite.config.js'))) {
$this->components->warn('Action is required to complete the theme setup:');
$this->components->bulletList([
"It looks like you don't have Vite installed. Please use your asset bundling system of choice to compile `resources/css/filament/{$panelId}/theme.css` into `public/css/filament/{$panelId}/theme.css`.",
"If you're not currently using a bundler, we recommend using Vite. Alternatively, you can use the Tailwind CLI with the following command:",
"npx tailwindcss --input ./resources/css/filament/{$panelId}/theme.css --output ./public/css/filament/{$panelId}/theme.css --config ./resources/css/filament/{$panelId}/tailwind.config.js --minify",
"Make sure to register the theme in the {$panelId} panel provider using `->theme(asset('css/filament/{$panelId}/theme.css'))`",
]);
return static::SUCCESS;
}
$postcssConfigPath = base_path('postcss.config.js');
if (! file_exists($postcssConfigPath)) {
$this->copyStubToApp('ThemePostcssConfig', $postcssConfigPath);
$this->components->info('Successfully created postcss.config.js!');
}
$this->components->warn('Action is required to complete the theme setup:');
$this->components->bulletList([
"First, add a new item to the `input` array of `vite.config.js`: `resources/css/filament/{$panelId}/theme.css`.",
"Next, register the theme in the {$panelId} panel provider using `->viteTheme('resources/css/filament/{$panelId}/theme.css')`",
'Finally, run `npm run build` to compile the theme.',
]);
return static::SUCCESS;
}
@@ -89,7 +89,6 @@ class FilamentServiceProvider extends PackageServiceProvider
protected function getCommands(): array
{
$commands = [
Commands\CompileThemeCommand::class,
Commands\MakePageCommand::class,
Commands\MakePanelCommand::class,
Commands\MakeRelationManagerCommand::class,
@@ -2,6 +2,7 @@
namespace Filament\Navigation;
use Exception;
use Illuminate\Contracts\Support\Arrayable;
class NavigationGroup
@@ -13,7 +14,7 @@ class NavigationGroup
protected ?string $icon = null;
/**
* @var array<NavigationItem | NavigationGroup> | Arrayable
* @var array<NavigationItem> | Arrayable
*/
protected array | Arrayable $items = [];
@@ -53,10 +54,18 @@ class NavigationGroup
}
/**
* @param array<NavigationItem | NavigationGroup> | Arrayable $items
* @param array<NavigationItem> | Arrayable $items
*/
public function items(array | Arrayable $items): static
{
foreach ($items as $item) {
if ($item instanceof NavigationItem) {
continue;
}
throw new Exception("Navigation group [{$this->getLabel()}] has a nested group, which is not supported.");
}
$this->items = $items;
return $this;
@@ -71,11 +80,15 @@ class NavigationGroup
public function getIcon(): ?string
{
if (filled($this->icon) && $this->hasItemIcons()) {
throw new Exception("Navigation group [{$this->getLabel()}] has an icon but one or more of its items also have icons. Either the group or its items can have icons, but not both. This is to ensure a proper user experience.");
}
return $this->icon;
}
/**
* @return array<NavigationItem | NavigationGroup> | Arrayable
* @return array<NavigationItem> | Arrayable
*/
public function getItems(): array | Arrayable
{
@@ -109,4 +122,30 @@ class NavigationGroup
return false;
}
public function hasItemIcons(): bool
{
$hasIconCount = 0;
$hasNoIconCount = 0;
foreach ($this->getItems() as $item) {
if (! $item instanceof NavigationItem) {
continue;
}
if (blank($item->getIcon())) {
$hasNoIconCount++;
continue;
}
$hasIconCount++;
}
if (($hasIconCount > 0) && ($hasNoIconCount > 0) && filled($label = $this->getLabel())) {
throw new Exception("Navigation group [{$label}] has items with and without icons. All items must have icons or none of them can have icons. This is to ensure a proper user experience.");
}
return $hasIconCount > 0;
}
}
@@ -14,11 +14,6 @@ class NavigationItem
protected ?string $activeIcon = null;
/**
* @var string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
protected string | array | null $iconColor = null;
protected string $label;
protected ?string $badge = null;
@@ -89,23 +84,13 @@ class NavigationItem
return $this;
}
public function activeIcon(string $activeIcon): static
public function activeIcon(?string $activeIcon): static
{
$this->activeIcon = $activeIcon;
return $this;
}
/**
* @param string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null $color
*/
public function iconColor(string | array | null $color): static
{
$this->iconColor = $color;
return $this;
}
public function isActiveWhen(Closure $callback): static
{
$this->isActiveWhen = $callback;
@@ -184,14 +169,6 @@ class NavigationItem
return $this->activeIcon;
}
/**
* @return string | array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string} | null
*/
public function getIconColor(): string | array | null
{
return $this->iconColor;
}
public function getLabel(): string
{
return $this->label;
+1 -1
View File
@@ -24,7 +24,7 @@ class Dashboard extends Page
__('filament::pages/dashboard.title');
}
public static function getNavigationIcon(): string
public static function getNavigationIcon(): ?string
{
return static::$navigationIcon ??
FilamentIcon::resolve('panels::pages.dashboard.navigation')?->name ??
+2 -3
View File
@@ -92,15 +92,14 @@ abstract class Page extends BasePage
return static::$navigationGroup;
}
public static function getActiveNavigationIcon(): string
public static function getActiveNavigationIcon(): ?string
{
return static::$activeNavigationIcon ?? static::getNavigationIcon();
}
public static function getNavigationIcon(): ?string
{
return static::$navigationIcon ??
(filament()->hasTopNavigation() ? null : 'heroicon-o-document-text');
return static::$navigationIcon;
}
public static function getNavigationLabel(): string
@@ -4,7 +4,7 @@ namespace Filament\Panel\Concerns;
trait HasSidebar
{
protected string $sidebarWidth = '18rem';
protected string $sidebarWidth = '20rem';
protected string $collapsedSidebarWidth = '5.4rem';
+2 -3
View File
@@ -609,8 +609,7 @@ abstract class Resource
public static function getNavigationIcon(): ?string
{
return static::$navigationIcon ??
(filament()->hasTopNavigation() ? null : 'heroicon-o-rectangle-stack');
return static::$navigationIcon;
}
public static function navigationIcon(?string $icon): void
@@ -618,7 +617,7 @@ abstract class Resource
static::$navigationIcon = $icon;
}
public static function getActiveNavigationIcon(): string
public static function getActiveNavigationIcon(): ?string
{
return static::$activeNavigationIcon ?? static::getNavigationIcon();
}
@@ -1,73 +0,0 @@
<?php
namespace Filament\Testing;
use Closure;
use Filament\Pages\Page;
use Livewire\Features\SupportUnitTesting\Testable;
/**
* @method Page instance()
*
* @mixin Testable
*/
class TestsPages
{
public function fillForm(): Closure
{
return function (array $state = []): static {
$this->set('data', $state);
return $this;
};
}
public function assertFormSet(): Closure
{
return function (array $state): static {
foreach ($state as $key => $value) {
$this->assertSet("data.{$key}", $value);
}
return $this;
};
}
public function assertHasFormErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasErrors(
collect($keys)
->mapWithKeys(function ($value, $key): array {
if (is_int($key)) {
return [$key => "data.{$value}"];
}
return ["data.{$key}" => $value];
})
->all(),
);
return $this;
};
}
public function assertHasNoFormErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasNoErrors(
collect($keys)
->mapWithKeys(function ($value, $key): array {
if (is_int($key)) {
return [$key => "data.{$value}"];
}
return ["data.{$key}" => $value];
})
->all(),
);
return $this;
};
}
}
+2
View File
@@ -1 +1,3 @@
@import '../../../../vendor/filament/filament/resources/css/theme.css';
@config './tailwind.config.js';
+5 -119
View File
@@ -1,122 +1,8 @@
import defaultPreset from '../support/tailwind.config.preset.js'
import defaultTheme from 'tailwindcss/defaultTheme'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
export default {
content: ['./vendor/filament/**/*.blade.php'],
darkMode: 'class',
theme: {
extend: {
colors: {
custom: {
50: 'rgba(var(--c-50), <alpha-value>)',
100: 'rgba(var(--c-100), <alpha-value>)',
200: 'rgba(var(--c-200), <alpha-value>)',
300: 'rgba(var(--c-300), <alpha-value>)',
400: 'rgba(var(--c-400), <alpha-value>)',
500: 'rgba(var(--c-500), <alpha-value>)',
600: 'rgba(var(--c-600), <alpha-value>)',
700: 'rgba(var(--c-700), <alpha-value>)',
800: 'rgba(var(--c-800), <alpha-value>)',
900: 'rgba(var(--c-900), <alpha-value>)',
950: 'rgba(var(--c-950), <alpha-value>)',
},
danger: {
50: 'rgba(var(--danger-50), <alpha-value>)',
100: 'rgba(var(--danger-100), <alpha-value>)',
200: 'rgba(var(--danger-200), <alpha-value>)',
300: 'rgba(var(--danger-300), <alpha-value>)',
400: 'rgba(var(--danger-400), <alpha-value>)',
500: 'rgba(var(--danger-500), <alpha-value>)',
600: 'rgba(var(--danger-600), <alpha-value>)',
700: 'rgba(var(--danger-700), <alpha-value>)',
800: 'rgba(var(--danger-800), <alpha-value>)',
900: 'rgba(var(--danger-900), <alpha-value>)',
950: 'rgba(var(--danger-950), <alpha-value>)',
},
gray: {
50: 'rgba(var(--gray-50), <alpha-value>)',
100: 'rgba(var(--gray-100), <alpha-value>)',
200: 'rgba(var(--gray-200), <alpha-value>)',
300: 'rgba(var(--gray-300), <alpha-value>)',
400: 'rgba(var(--gray-400), <alpha-value>)',
500: 'rgba(var(--gray-500), <alpha-value>)',
600: 'rgba(var(--gray-600), <alpha-value>)',
700: 'rgba(var(--gray-700), <alpha-value>)',
800: 'rgba(var(--gray-800), <alpha-value>)',
900: 'rgba(var(--gray-900), <alpha-value>)',
950: 'rgba(var(--gray-950), <alpha-value>)',
},
info: {
50: 'rgba(var(--info-50), <alpha-value>)',
100: 'rgba(var(--info-100), <alpha-value>)',
200: 'rgba(var(--info-200), <alpha-value>)',
300: 'rgba(var(--info-300), <alpha-value>)',
400: 'rgba(var(--info-400), <alpha-value>)',
500: 'rgba(var(--info-500), <alpha-value>)',
600: 'rgba(var(--info-600), <alpha-value>)',
700: 'rgba(var(--info-700), <alpha-value>)',
800: 'rgba(var(--info-800), <alpha-value>)',
900: 'rgba(var(--info-900), <alpha-value>)',
950: 'rgba(var(--info-950), <alpha-value>)',
},
primary: {
50: 'rgba(var(--primary-50), <alpha-value>)',
100: 'rgba(var(--primary-100), <alpha-value>)',
200: 'rgba(var(--primary-200), <alpha-value>)',
300: 'rgba(var(--primary-300), <alpha-value>)',
400: 'rgba(var(--primary-400), <alpha-value>)',
500: 'rgba(var(--primary-500), <alpha-value>)',
600: 'rgba(var(--primary-600), <alpha-value>)',
700: 'rgba(var(--primary-700), <alpha-value>)',
800: 'rgba(var(--primary-800), <alpha-value>)',
900: 'rgba(var(--primary-900), <alpha-value>)',
950: 'rgba(var(--primary-950), <alpha-value>)',
},
secondary: {
50: 'rgba(var(--secondary-50), <alpha-value>)',
100: 'rgba(var(--secondary-100), <alpha-value>)',
200: 'rgba(var(--secondary-200), <alpha-value>)',
300: 'rgba(var(--secondary-300), <alpha-value>)',
400: 'rgba(var(--secondary-400), <alpha-value>)',
500: 'rgba(var(--secondary-500), <alpha-value>)',
600: 'rgba(var(--secondary-600), <alpha-value>)',
700: 'rgba(var(--secondary-700), <alpha-value>)',
800: 'rgba(var(--secondary-800), <alpha-value>)',
900: 'rgba(var(--secondary-900), <alpha-value>)',
950: 'rgba(var(--secondary-950), <alpha-value>)',
},
success: {
50: 'rgba(var(--success-50), <alpha-value>)',
100: 'rgba(var(--success-100), <alpha-value>)',
200: 'rgba(var(--success-200), <alpha-value>)',
300: 'rgba(var(--success-300), <alpha-value>)',
400: 'rgba(var(--success-400), <alpha-value>)',
500: 'rgba(var(--success-500), <alpha-value>)',
600: 'rgba(var(--success-600), <alpha-value>)',
700: 'rgba(var(--success-700), <alpha-value>)',
800: 'rgba(var(--success-800), <alpha-value>)',
900: 'rgba(var(--success-900), <alpha-value>)',
950: 'rgba(var(--success-950), <alpha-value>)',
},
warning: {
50: 'rgba(var(--warning-50), <alpha-value>)',
100: 'rgba(var(--warning-100), <alpha-value>)',
200: 'rgba(var(--warning-200), <alpha-value>)',
300: 'rgba(var(--warning-300), <alpha-value>)',
400: 'rgba(var(--warning-400), <alpha-value>)',
500: 'rgba(var(--warning-500), <alpha-value>)',
600: 'rgba(var(--warning-600), <alpha-value>)',
700: 'rgba(var(--warning-700), <alpha-value>)',
800: 'rgba(var(--warning-800), <alpha-value>)',
900: 'rgba(var(--warning-900), <alpha-value>)',
950: 'rgba(var(--warning-950), <alpha-value>)',
},
},
fontFamily: {
sans: ['var(--font-family)', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [forms, typography],
defaultPreset.theme.extend.fontFamily = {
sans: ['var(--font-family)', ...defaultTheme.fontFamily.sans],
}
export default defaultPreset
+95
View File
@@ -0,0 +1,95 @@
---
title: Colors
---
## Overview
Filament uses CSS variables to define its color palette. These CSS variables are mapped to Tailwind classes in the preset file that you load when installing Filament.
## Customizing the default colors
From a service provider's `boot()` method, or middleware, you can call the `FilamentColor::register()` method, which you can use to customize which colors Filament uses for UI elements.
There are 7 default colors that are used throughout Filament that you are able to customize:
```php
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
FilamentColor::register([
'danger' => Color::Red,
'gray' => Color::Gray,
'info' => Color::Blue,
'primary' => Color::Amber,
'secondary' => Color::Gray,
'success' => Color::Green,
'warning' => Color::Amber,
]);
```
The `Color` class contains every [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) to choose from.
## Using a non-Tailwind color
You can use custom colors that are not included in the [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) palette by passing an array of color shades from `50` to `900` in RGB format:
```php
use Filament\Support\Facades\FilamentColor;
FilamentColor::register([
'danger' => [
50 => '254, 242, 242',
100 => '254, 226, 226',
200 => '254, 202, 202',
300 => '252, 165, 165',
400 => '248, 113, 113',
500 => '239, 68, 68',
600 => '220, 38, 38',
700 => '185, 28, 28',
800 => '153, 27, 27',
900 => '127, 29, 29',
950 => '69, 10, 10',
];
]);
```
### Generating a custom color from a hex code
You can use the `Color::hex()` method to generate a custom color palette from a hex code:
```php
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
FilamentColor::register([
'danger' => Color::hex('#ff0000'),
]);
```
### Generating a custom color from an RGB value
You can use the `Color::rgb()` method to generate a custom color palette from an RGB value:
```php
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
FilamentColor::register([
'danger' => Color::rgb('rgb(255, 0, 0)'),
]);
```
## Registering extra colors
You can register extra colors that you can use throughout Filament:
```php
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
FilamentColor::register([
'indigo' => Color::Indigo,
]);
```
Now, you can use this color anywhere you would normally add `primary`, `secondary`, `danger`, etc.
@@ -20,14 +20,14 @@
@php
$buttonClasses = \Illuminate\Support\Arr::toCssClasses([
...[
'filament-button relative grid-flow-col items-center justify-center rounded-lg border font-medium outline-none transition-colors focus:ring-2 disabled:pointer-events-none disabled:opacity-70',
"filament-button filament-button-size-{$size} relative grid-flow-col items-center justify-center rounded-lg font-medium outline-none transition focus:ring-2 disabled:pointer-events-none disabled:opacity-70",
is_string($color) ? "filament-button-color-{$color}" : null,
match ($size) {
'xs' => 'filament-button-size-xs gap-1.5 px-[calc(theme(spacing.3)-1px)] py-[calc(theme(spacing.2)-1px)] text-xs',
'sm' => 'filament-button-size-sm gap-1.5 px-[calc(theme(spacing.[3.5])-1px)] py-[calc(theme(spacing.2)-1px)] text-sm',
'md' => 'filament-button-size-md gap-2 px-[calc(theme(spacing.4)-1px)] py-[calc(theme(spacing[2.5])-1px)] text-sm',
'lg' => 'filament-button-size-lg gap-2 px-[calc(theme(spacing.5)-1px)] py-[calc(theme(spacing.3)-1px)] text-sm',
'xl' => 'filament-button-size-xl gap-2 px-[calc(theme(spacing.6)-1px)] py-[calc(theme(spacing.3)-1px)] text-base',
'xs' => 'gap-1 px-2 py-1.5 text-xs',
'sm' => 'gap-1 px-2.5 py-1.5 text-sm',
'md' => 'gap-1.5 px-3 py-2 text-sm',
'lg' => 'gap-1.5 px-3.5 py-2.5 text-sm',
'xl' => 'gap-1.5 px-4 py-3 text-sm',
},
'hidden' => $labeledFrom,
match ($labeledFrom) {
@@ -42,17 +42,17 @@
...(
$outlined
? [
'filament-button-outlined',
'filament-button-outlined ring-1 ',
match ($color) {
'gray' => 'border-gray-300 text-gray-700 hover:bg-gray-500/10 focus:bg-gray-500/10 focus:ring-primary-500/50 dark:border-gray-600 dark:text-gray-200',
default => 'border-custom-600 text-custom-600 hover:bg-custom-500/10 focus:bg-custom-500/10 focus:ring-custom-500/50 dark:border-custom-400 dark:text-custom-400',
'gray' => 'ring-gray-600 text-gray-700 hover:bg-gray-500/10 focus:bg-gray-500/10 focus:ring-gray-500/50 dark:ring-gray-300/70 dark:text-gray-200',
default => 'ring-custom-600 text-custom-600 hover:bg-custom-500/10 focus:bg-custom-500/10 focus:ring-custom-500/50 dark:ring-custom-400 dark:text-custom-400 dark:focus:ring-custom-300/70',
},
]
: [
'shadow',
match ($color) {
'gray' => 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 focus:border-transparent focus:bg-gray-50 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:focus:bg-gray-700',
default => 'border-transparent bg-custom-600 text-white hover:bg-custom-500 focus:bg-custom-500 focus:ring-custom-500/50',
'gray' => 'ring-gray-950/10 ring-1 bg-white text-gray-700 hover:bg-gray-50 focus:bg-gray-50 focus:ring-2 dark:ring-white/20 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:focus:bg-gray-700 dark:hover:ring-white/30 dark:focus:ring-white/30',
default => 'bg-custom-600 text-white hover:bg-custom-500 focus:bg-custom-500 focus:ring-custom-500/50',
},
]
),
@@ -61,13 +61,12 @@
$buttonStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: $outlined ? [400, 500, 600] : [500, 600],
shades: $outlined ? [300, 400, 500, 600] : [500, 600],
) => $color !== 'gray',
]);
$iconSize ??= match ($size) {
'xs', 'sm' => 'sm',
'md', 'lg', 'xl' => 'md',
default => 'md',
};
@@ -78,28 +77,7 @@
default => $iconSize,
};
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'filament-button-icon',
match ($iconPosition) {
'before' => match ($size) {
'xs' => '-ms-0.5',
'sm' => '-ms-0.5',
'md' => '-ms-1',
'lg' => '-ms-1',
'xl' => '-ms-1',
default => null,
},
'after' => match ($size) {
'xs' => '-me-0.5',
'sm' => '-me-0.5',
'md' => '-me-1',
'lg' => '-me-1',
'xl' => '-me-1',
default => null,
},
default => null,
} => ! $labelSrOnly,
]);
$iconClasses = 'filament-button-icon';
$indicatorClasses = 'filament-button-indicator absolute -end-1 -top-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-custom-600 text-[0.5rem] font-medium text-white';
@@ -135,14 +113,16 @@
:tag="$tag"
:tooltip="$tooltip"
:type="$type"
:class="match ($labeledFrom) {
'sm' => 'sm:hidden',
'md' => 'md:hidden',
'lg' => 'lg:hidden',
'xl' => 'xl:hidden',
'2xl' => '2xl:hidden',
default => 'hidden',
}"
:class="
match ($labeledFrom) {
'sm' => 'sm:hidden',
'md' => 'md:hidden',
'lg' => 'lg:hidden',
'xl' => 'xl:hidden',
'2xl' => '2xl:hidden',
default => 'hidden',
}
"
:attributes="\Filament\Support\prepare_inherited_attributes($attributes)"
/>
@endif
@@ -0,0 +1,5 @@
<p
{{ $attributes->class(['filament-card-description mt-1 text-sm text-gray-500 dark:text-gray-400']) }}
>
{{ $slot }}
</p>
@@ -1,5 +1,5 @@
<h2
{{ $attributes->class(['filament-card-heading text-xl font-medium tracking-tight']) }}
<h3
{{ $attributes->class(['filament-card-heading text-base font-semibold leading-6']) }}
>
{{ $slot }}
</h2>
</h3>
@@ -1,5 +0,0 @@
<h3
{{ $attributes->class(['filament-card-subheading text-sm text-gray-600 dark:text-gray-400']) }}
>
{{ $slot }}
</h3>
@@ -33,8 +33,8 @@
x-ref="panel"
x-float{{ $placement ? ".placement.{$placement}" : '' }}.flip{{ $shift ? '.shift' : '' }}{{ $teleport ? '.teleport' : '' }}{{ $offset ? '.offset' : '' }}="{ offset: {{ $offset }} }"
x-cloak
x-transition:enter-start="opacity-0 scale-95"
x-transition:leave-end="opacity-0 scale-95"
x-transition:enter-start="scale-95 opacity-0"
x-transition:leave-end="scale-95 opacity-0"
@if ($attributes->has('wire:key'))
wire:ignore.self
wire:key="{{ $attributes->get('wire:key') }}.panel"
@@ -18,24 +18,31 @@
$iconSize ??= $size;
$linkClasses = \Illuminate\Support\Arr::toCssClasses([
'filament-link relative inline-flex items-center justify-center gap-0.5 font-medium outline-none hover:underline focus:underline disabled:pointer-events-none disabled:opacity-70',
"filament-link filament-link-size-{$size} relative inline-flex items-center justify-center font-medium outline-none hover:underline focus:underline disabled:pointer-events-none disabled:opacity-70",
'pe-4' => $indicator,
'pointer-events-none opacity-70' => $disabled,
match ($size) {
'xs' => 'gap-1 text-xs',
'sm' => 'gap-1 text-sm',
'md' => 'gap-1.5 text-sm',
'lg' => 'gap-1.5 text-sm',
'xl' => 'gap-1.5 text-sm',
},
match ($color) {
'gray' => 'text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200',
default => 'text-custom-600 hover:text-custom-500 dark:text-custom-500 dark:hover:text-custom-400',
},
match ($size) {
'sm' => 'text-sm',
'md' => 'text-sm',
'lg' => 'text-base',
},
]);
$linkStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables($color, shades: [400, 500, 600]) => $color !== 'gray',
]);
$iconSize ??= match ($size) {
'xs', 'sm' => 'sm',
default => 'md',
};
$iconSize = match ($iconSize) {
'sm' => 'h-4 w-4',
'md' => 'h-5 w-5',
@@ -43,11 +50,7 @@
default => $iconSize,
};
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'filament-link-icon',
'me-1' => $iconPosition === 'before',
'ms-1' => $iconPosition === 'after',
]);
$iconClasses = 'filament-link-icon';
$indicatorClasses = 'filament-link-indicator absolute -end-1 -top-1 inline-flex h-4 w-4 items-center justify-center rounded-full bg-custom-600 text-[0.5rem] font-medium text-white';
@@ -1,18 +0,0 @@
@props([
'alignment' => \Filament\Actions\MountableAction::getModalActionsAlignment(),
'fullWidth' => false,
])
<div
{{
$attributes->class([
'filament-modal-actions',
'flex flex-wrap items-center gap-4 rtl:space-x-reverse' => ! $fullWidth,
'flex-row-reverse space-x-reverse' => (! $fullWidth) && ($alignment === 'right'),
'justify-center' => (! $fullWidth) && ($alignment === 'center'),
'grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-2' => $fullWidth,
])
}}
>
{{ $slot }}
</div>
@@ -1,5 +1,5 @@
<h2
{{ $attributes->class(['filament-modal-heading text-xl font-medium tracking-tight']) }}
{{ $attributes->class(['filament-modal-heading text-base font-semibold leading-6']) }}
>
{{ $slot }}
</h2>
@@ -1,20 +1,23 @@
@props([
'actions' => null,
'alignment' => 'start',
'ariaLabelledby' => null,
'closeButton' => true,
'closeButton' => \Filament\Support\View\Components\Modal::$hasCloseButton,
'closeByClickingAway' => \Filament\Support\View\Components\Modal::$isClosedByClickingAway,
'closeEventName' => 'close-modal',
'displayClasses' => 'inline-block',
'footer' => null,
'footerActions' => [],
'footerActionsAlignment' => 'start',
'header' => null,
'heading' => null,
'headingComponent' => 'filament::modal.heading',
'hrComponent' => 'filament::hr',
'icon' => null,
'iconColor' => 'primary',
'id' => null,
'openEventName' => 'open-modal',
'slideOver' => false,
'subheading' => null,
'subheadingComponent' => 'filament::modal.subheading',
'stickyFooter' => false,
'description' => null,
'trigger' => null,
'visible' => true,
'width' => 'sm',
@@ -22,7 +25,6 @@
<div
x-data="{
isOpen: false,
livewire: null,
@@ -30,15 +32,18 @@
close: function () {
this.isOpen = false
this.$refs.modalContainer.dispatchEvent(new CustomEvent('modal-closed', { id: '{{ $id }}' }))
this.$refs.modalContainer.dispatchEvent(
new CustomEvent('modal-closed', { id: '{{ $id }}' }),
)
},
open: function () {
this.isOpen = true
this.$refs.modalContainer.dispatchEvent(new CustomEvent('modal-opened', { id: '{{ $id }}' }))
this.$refs.modalContainer.dispatchEvent(
new CustomEvent('modal-opened', { id: '{{ $id }}' }),
)
},
}"
x-trap.noscroll="isOpen"
@if ($id)
@@ -52,8 +57,8 @@
@endif
role="dialog"
aria-modal="true"
class="filament-modal {{ $displayClasses }}"
wire:ignore.self
class="filament-modal {{ $displayClasses }}"
>
{{ $trigger }}
@@ -94,10 +99,12 @@
>
<div
x-data="{ isShown: false }"
x-init="$nextTick(() => {
isShown = isOpen
$watch('isOpen', () => isShown = isOpen)
})"
x-init="
$nextTick(() => {
isShown = isOpen
$watch('isOpen', () => (isShown = isOpen))
})
"
x-show="isShown"
x-cloak
@if (filled($id))
@@ -119,7 +126,7 @@
x-transition:leave-end="translate-y-8"
@endif
@class([
'filament-modal-window pointer-events-auto w-full cursor-default bg-white py-2 dark:bg-gray-800',
'filament-modal-window pointer-events-auto w-full cursor-default bg-white dark:bg-gray-800',
'relative' => $width !== 'screen',
'ms-auto h-screen overflow-y-auto' => $slideOver,
'mx-auto rounded-xl' => ! ($slideOver || ($width === 'screen')),
@@ -138,106 +145,134 @@
'fixed inset-0' => $width === 'screen',
])
>
@if ($closeButton)
<button
tabindex="-1"
type="button"
class="absolute end-2 top-2"
@if (filled($id))
x-on:click="$dispatch('{{ $closeEventName }}', { id: '{{ $id }}' })"
@else
x-on:click="close()"
@endif
>
<x-filament::icon
name="heroicon-m-x-mark"
alias="support::modal.close-button"
color="text-gray-400"
size="h-5 w-5"
class="filament-modal-close-button cursor-pointer"
:title="__('filament-support::components/modal.actions.close.label')"
x-on:click="close()"
tabindex="-1"
/>
<span class="sr-only">
{{ __('filament-support::components/modal.actions.close.label') }}
</span>
</button>
@endif
<div
@class([
'flex h-full flex-col' => ($width === 'screen') || $slideOver,
'relative flex h-full flex-col' => ($width === 'screen') || $slideOver,
])
>
<div class="space-y-2">
@if ($header)
<div class="filament-modal-header px-6 py-2">
@if ($heading || $header)
<div
@class([
'filament-modal-header flex px-6 pt-6',
'mb-6' => $slot->isEmpty(),
match ($alignment) {
'left', 'start' => 'gap-x-5',
'center' => 'flex-col',
},
])
>
@if ($header)
{{ $header }}
</div>
@endif
@else
@if ($icon)
<div
@class([
'mb-5 flex items-center justify-center' => $alignment === 'center',
])
>
<div
@class([
'rounded-full bg-custom-100 dark:bg-custom-500/20',
match ($alignment) {
'left', 'start' => 'p-2',
'center' => 'p-3',
},
])
style="{{ \Filament\Support\get_color_css_variables($iconColor, shades: [100, 500]) }}"
>
<x-filament::icon
alias="filament::modal"
color="text-custom-600 dark:text-custom-400"
:name="$icon"
size="h-6 w-6"
:style="\Filament\Support\get_color_css_variables($iconColor, shades: [400, 600])"
/>
</div>
</div>
@endif
@if ($header && ($actions || $heading || $slot->isNotEmpty() || $subheading))
<x-dynamic-component
:component="$hrComponent"
class="px-2"
<div
@class([
'text-center' => $alignment === 'center',
])
>
<x-filament::modal.heading>
{{ $heading }}
</x-filament::modal.heading>
@if ($description)
<p
class="filament-modal-description mt-2 text-sm text-gray-500 dark:text-gray-400"
>
{{ $description }}
</p>
@endif
</div>
@endif
</div>
@endif
@if ($closeButton)
<div
@class([
'absolute',
'end-6 top-6' => $slideOver,
'end-4 top-4' => ! $slideOver,
])
>
<x-filament::icon-button
color="gray"
icon="heroicon-o-x-mark"
icon-alias="support::modal.close-button"
icon-size="lg"
:label="__('filament-support::components/modal.actions.close.label')"
tabindex="-1"
:x-on:click="filled($id) ? '$dispatch(' . \Illuminate\Support\Js::from($closeEventName) . ', { id: ' . \Illuminate\Support\Js::from($id) . ' })' : 'close()'"
class="filament-modal-close-button -m-2"
/>
@endif
</div>
</div>
@endif
@if ($slot->isNotEmpty())
<div
@class([
'filament-modal-content flex flex-col gap-y-4 py-6',
'flex-1' => ($width === 'screen') || $slideOver,
'pe-6 ps-[5.25rem]' => $icon && ($alignment === 'start'),
'px-6' => ! ($icon && ($alignment === 'start')),
])
>
{{ $slot }}
</div>
@endif
<div
@class([
'filament-modal-content space-y-2 p-2',
'flex-1 overflow-y-auto' => ($width === 'screen') || $slideOver,
'filament-modal-footer w-full',
'pe-6 ps-[5.25rem]' => $icon && ($alignment === 'start') && ($footerActionsAlignment !== 'center') && (! $stickyFooter),
'px-6' => ! ($icon && ($alignment === 'start') && ($footerActionsAlignment !== 'center') && (! $stickyFooter)),
'sticky bottom-0 rounded-b-xl border-t border-gray-200 bg-white py-5 dark:border-gray-700 dark:bg-gray-800' => $stickyFooter,
'pb-6' => ! $stickyFooter,
'mt-6' => (! $stickyFooter) && $slot->isEmpty() && (! $slideOver),
'mt-auto' => $slideOver,
])
>
@if ($heading || $subheading)
@if ($footer)
{{ $footer }}
@elseif (count($footerActions))
<div
@class([
'space-y-2 p-4 dark:text-white',
'text-center' => ! $slideOver,
'filament-modal-footer-actions gap-3',
match ($footerActionsAlignment) {
'center' => 'flex flex-col-reverse sm:grid sm:grid-cols-[repeat(auto-fit,minmax(0,1fr))]',
'end', 'right' => 'flex flex-row-reverse flex-wrap items-center',
'left', 'start' => 'flex flex-wrap items-center',
},
])
>
@if ($heading)
<x-dynamic-component
:component="$headingComponent"
:id="$id . '.heading'"
>
{{ $heading }}
</x-dynamic-component>
@endif
@if ($subheading)
<x-dynamic-component
:component="$subheadingComponent"
>
{{ $subheading }}
</x-dynamic-component>
@endif
</div>
@endif
@if ($slot->isNotEmpty())
<div class="space-y-4 px-4 py-2">
{{ $slot }}
</div>
@endif
{{ $actions }}
</div>
<div class="space-y-2">
@if ($footer && ($actions || $heading || $slot->isNotEmpty() || $subheading))
<x-dynamic-component
:component="$hrComponent"
class="px-2"
/>
@endif
@if ($footer)
<div class="filament-modal-footer px-6 py-2">
{{ $footer }}
@foreach ($footerActions as $action)
{{ $action }}
@endforeach
</div>
@endif
</div>
@@ -1,3 +0,0 @@
<p {{ $attributes->class(['filament-modal-subheading text-gray-500']) }}>
{{ $slot }}
</p>
@@ -32,7 +32,15 @@
return
}
setTimeout(() => $el.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' }), 200)
setTimeout(
() =>
$el.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start',
}),
200,
)
"
@endif
{{
@@ -58,7 +66,7 @@
} => ! $aside,
])
>
<div class="filament-section-component-header grid flex-1 gap-y-1">
<div class="filament-section-component-header flex-1">
<div
class="filament-section-component-header-heading-wrapper flex items-center gap-x-2"
>
@@ -67,12 +75,14 @@
:name="$icon"
alias="support::section.icon"
color="text-custom-400"
:size="match ($iconSize) {
'sm' => 'h-4 w-4',
'md' => 'h-5 w-5',
'lg' => 'h-6 w-6',
default => $iconSize,
}"
:size="
match ($iconSize) {
'sm' => 'h-4 w-4',
'md' => 'h-5 w-5',
'lg' => 'h-6 w-6',
default => $iconSize,
}
"
class="filament-section-component-header-icon"
:style="\Filament\Support\get_color_css_variables($iconColor, shades: [400])"
/>
@@ -87,7 +97,7 @@
@if ($description?->isNotEmpty())
<p
class="filament-section-component-header-description text-sm text-gray-500 dark:text-gray-400"
class="filament-section-component-header-description mt-1 text-sm text-gray-500 dark:text-gray-400"
>
{{ $description }}
</p>
@@ -25,8 +25,10 @@
@endif
@if ($alpineActive)
x-bind:class="{
'hover:text-gray-800 focus:text-primary-600 dark:text-gray-400 dark:hover:text-gray-300 dark:focus:text-gray-400': ! ({{ $alpineActive }}),
'text-primary-600 shadow bg-white dark:text-white dark:bg-primary-600': {{ $alpineActive }},
'hover:text-gray-800 focus:text-primary-600 dark:text-gray-400 dark:hover:text-gray-300 dark:focus:text-gray-400':
! {{ $alpineActive }},
'text-primary-600 shadow bg-white dark:text-white dark:bg-primary-600':
{{ $alpineActive }},
}"
@endif
{{
@@ -76,7 +78,7 @@
<span
@if ($alpineActive)
x-bind:class="{
'bg-gray-200 dark:bg-gray-600': ! ({{ $alpineActive }}),
'bg-gray-200 dark:bg-gray-600': ! {{ $alpineActive }},
'bg-white text-primary-600 font-medium': {{ $alpineActive }},
}"
@endif
@@ -2,6 +2,8 @@
namespace Filament\Support\Colors;
use Spatie\Color\Hex;
class ColorManager
{
/**
@@ -35,6 +37,24 @@ class ColorManager
return Color::rgb($color);
}
if (is_array($color)) {
return array_map(function (string $color): string {
if (str_starts_with($color, '#')) {
$color = Hex::fromString($color)->toRgb();
return "{$color->red()}, {$color->green()}, {$color->blue()}";
}
if (str_starts_with($color, 'rgb')) {
return (string) str($color)
->after('rgb(')
->before(')');
}
return $color;
}, $color);
}
return $color;
}
@@ -12,7 +12,7 @@ class InstallCommand extends Command
{
use CanManipulateFiles;
protected $signature = 'filament:install {--scaffold} {--actions} {--forms} {--notifications} {--panels} {--tables} {--widgets} {--F|force}';
protected $signature = 'filament:install {--scaffold} {--actions} {--forms} {--infolists} {--notifications} {--panels} {--tables} {--widgets} {--F|force}';
protected $description = 'Install Filament.';
@@ -80,6 +80,7 @@ class InstallCommand extends Command
if (
(! $this->option('actions')) &&
(! $this->option('forms')) &&
(! $this->option('infolists')) &&
(! $this->option('tables'))
) {
$css = $filesystem->get(resource_path('css/app.css'));
@@ -159,12 +160,12 @@ class InstallCommand extends Command
$command = '@php artisan filament:upgrade';
if (in_array($command, $configuration['scripts']['post-update-cmd'] ?? [])) {
if (in_array($command, $configuration['scripts']['post-autoload-dump'] ?? [])) {
return;
}
$configuration['scripts']['post-update-cmd'] ??= [];
$configuration['scripts']['post-update-cmd'][] = $command;
$configuration['scripts']['post-autoload-dump'] ??= [];
$configuration['scripts']['post-autoload-dump'][] = $command;
file_put_contents(
$path,
@@ -4,8 +4,15 @@ namespace Filament\Support\View\Components;
class Modal
{
public static bool $hasCloseButton = true;
public static bool $isClosedByClickingAway = true;
public static function closeButton(bool $condition = true): void
{
static::$hasCloseButton = $condition;
}
public static function closedByClickingAway(bool $condition = true): void
{
static::$isClosedByClickingAway = $condition;
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
@@ -1,21 +1,10 @@
import colors from 'tailwindcss/colors'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import preset from './vendor/filament/support/tailwind.config.preset'
export default {
content: ['./resources/**/*.blade.php', './vendor/filament/**/*.blade.php'],
darkMode: 'class',
theme: {
extend: {
colors: {
danger: colors.red,
info: colors.blue,
primary: colors.amber,
secondary: colors.gray,
success: colors.green,
warning: colors.amber,
},
},
},
plugins: [forms, typography],
presets: [preset],
content: [
'./app/Filament/**/*.php',
'./resources/views/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
}
+118
View File
@@ -0,0 +1,118 @@
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
export default {
content: ['./vendor/filament/**/*.blade.php'],
darkMode: 'class',
theme: {
extend: {
colors: {
custom: {
50: 'rgba(var(--c-50), <alpha-value>)',
100: 'rgba(var(--c-100), <alpha-value>)',
200: 'rgba(var(--c-200), <alpha-value>)',
300: 'rgba(var(--c-300), <alpha-value>)',
400: 'rgba(var(--c-400), <alpha-value>)',
500: 'rgba(var(--c-500), <alpha-value>)',
600: 'rgba(var(--c-600), <alpha-value>)',
700: 'rgba(var(--c-700), <alpha-value>)',
800: 'rgba(var(--c-800), <alpha-value>)',
900: 'rgba(var(--c-900), <alpha-value>)',
950: 'rgba(var(--c-950), <alpha-value>)',
},
danger: {
50: 'rgba(var(--danger-50), <alpha-value>)',
100: 'rgba(var(--danger-100), <alpha-value>)',
200: 'rgba(var(--danger-200), <alpha-value>)',
300: 'rgba(var(--danger-300), <alpha-value>)',
400: 'rgba(var(--danger-400), <alpha-value>)',
500: 'rgba(var(--danger-500), <alpha-value>)',
600: 'rgba(var(--danger-600), <alpha-value>)',
700: 'rgba(var(--danger-700), <alpha-value>)',
800: 'rgba(var(--danger-800), <alpha-value>)',
900: 'rgba(var(--danger-900), <alpha-value>)',
950: 'rgba(var(--danger-950), <alpha-value>)',
},
gray: {
50: 'rgba(var(--gray-50), <alpha-value>)',
100: 'rgba(var(--gray-100), <alpha-value>)',
200: 'rgba(var(--gray-200), <alpha-value>)',
300: 'rgba(var(--gray-300), <alpha-value>)',
400: 'rgba(var(--gray-400), <alpha-value>)',
500: 'rgba(var(--gray-500), <alpha-value>)',
600: 'rgba(var(--gray-600), <alpha-value>)',
700: 'rgba(var(--gray-700), <alpha-value>)',
800: 'rgba(var(--gray-800), <alpha-value>)',
900: 'rgba(var(--gray-900), <alpha-value>)',
950: 'rgba(var(--gray-950), <alpha-value>)',
},
info: {
50: 'rgba(var(--info-50), <alpha-value>)',
100: 'rgba(var(--info-100), <alpha-value>)',
200: 'rgba(var(--info-200), <alpha-value>)',
300: 'rgba(var(--info-300), <alpha-value>)',
400: 'rgba(var(--info-400), <alpha-value>)',
500: 'rgba(var(--info-500), <alpha-value>)',
600: 'rgba(var(--info-600), <alpha-value>)',
700: 'rgba(var(--info-700), <alpha-value>)',
800: 'rgba(var(--info-800), <alpha-value>)',
900: 'rgba(var(--info-900), <alpha-value>)',
950: 'rgba(var(--info-950), <alpha-value>)',
},
primary: {
50: 'rgba(var(--primary-50), <alpha-value>)',
100: 'rgba(var(--primary-100), <alpha-value>)',
200: 'rgba(var(--primary-200), <alpha-value>)',
300: 'rgba(var(--primary-300), <alpha-value>)',
400: 'rgba(var(--primary-400), <alpha-value>)',
500: 'rgba(var(--primary-500), <alpha-value>)',
600: 'rgba(var(--primary-600), <alpha-value>)',
700: 'rgba(var(--primary-700), <alpha-value>)',
800: 'rgba(var(--primary-800), <alpha-value>)',
900: 'rgba(var(--primary-900), <alpha-value>)',
950: 'rgba(var(--primary-950), <alpha-value>)',
},
secondary: {
50: 'rgba(var(--secondary-50), <alpha-value>)',
100: 'rgba(var(--secondary-100), <alpha-value>)',
200: 'rgba(var(--secondary-200), <alpha-value>)',
300: 'rgba(var(--secondary-300), <alpha-value>)',
400: 'rgba(var(--secondary-400), <alpha-value>)',
500: 'rgba(var(--secondary-500), <alpha-value>)',
600: 'rgba(var(--secondary-600), <alpha-value>)',
700: 'rgba(var(--secondary-700), <alpha-value>)',
800: 'rgba(var(--secondary-800), <alpha-value>)',
900: 'rgba(var(--secondary-900), <alpha-value>)',
950: 'rgba(var(--secondary-950), <alpha-value>)',
},
success: {
50: 'rgba(var(--success-50), <alpha-value>)',
100: 'rgba(var(--success-100), <alpha-value>)',
200: 'rgba(var(--success-200), <alpha-value>)',
300: 'rgba(var(--success-300), <alpha-value>)',
400: 'rgba(var(--success-400), <alpha-value>)',
500: 'rgba(var(--success-500), <alpha-value>)',
600: 'rgba(var(--success-600), <alpha-value>)',
700: 'rgba(var(--success-700), <alpha-value>)',
800: 'rgba(var(--success-800), <alpha-value>)',
900: 'rgba(var(--success-900), <alpha-value>)',
950: 'rgba(var(--success-950), <alpha-value>)',
},
warning: {
50: 'rgba(var(--warning-50), <alpha-value>)',
100: 'rgba(var(--warning-100), <alpha-value>)',
200: 'rgba(var(--warning-200), <alpha-value>)',
300: 'rgba(var(--warning-300), <alpha-value>)',
400: 'rgba(var(--warning-400), <alpha-value>)',
500: 'rgba(var(--warning-500), <alpha-value>)',
600: 'rgba(var(--warning-600), <alpha-value>)',
700: 'rgba(var(--warning-700), <alpha-value>)',
800: 'rgba(var(--warning-800), <alpha-value>)',
900: 'rgba(var(--warning-900), <alpha-value>)',
950: 'rgba(var(--warning-950), <alpha-value>)',
},
},
},
},
plugins: [forms, typography],
}
+7 -26
View File
@@ -46,40 +46,21 @@ First, use NPM to install Tailwind CSS and its `forms` and `typography` plugins:
npm install tailwindcss @tailwindcss/forms @tailwindcss/typography postcss --save-dev
```
Create a new `tailwind.config.js` file. Ensure that you add Filament's `content` path, custom `colors`, and the `plugins` you installed:
Create a new `tailwind.config.js` file. Ensure that you add Filament's `preset` which configures colors and the plugins you installed:
```js
import colors from 'tailwindcss/colors'
import forms from '@tailwindcss/forms'
import typography from '@tailwindcss/typography'
import preset from './vendor/filament/support/tailwind.config.preset'
export default {
presets: [preset],
content: [
'./resources/**/*.blade.php',
'./app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
darkMode: 'class',
theme: {
extend: {
colors: {
danger: colors.red,
info: colors.blue,
primary: colors.amber,
secondary: colors.gray,
success: colors.green,
warning: colors.amber,
},
},
},
plugins: [
forms,
typography,
],
}
```
Of course, you may specify your own custom `danger`, `gray`, `info`, `primary`, `secondary`, `success` and `warning` colors, which will be used instead. But each color needs to be a [Tailwind CSS color](https://tailwindcss.com/docs/customizing-colors#color-palette-reference), or have all 50 - 950 variants specified - a single hex code or RGB value won't work here.
### Configuring styles
In `resources/css/app.css`, import Tailwind CSS, and the Filament forms CSS, which is used for action modal forms:
@@ -174,10 +155,10 @@ composer update
php artisan filament:upgrade
```
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-update-cmd` to run it automatically:
We recommend adding the `filament:upgrade` command to your `composer.json`'s `post-autoload-dump` to run it automatically:
```json
"post-update-cmd": [
"post-autoload-dump": [
// ...
"@php artisan filament:upgrade"
],

Some files were not shown because too many files have changed in this diff Show More