├── resources ├── views │ ├── .gitkeep │ └── components │ │ ├── partials │ │ ├── media.blade.php │ │ └── theme-toggle.blade.php │ │ └── layouts │ │ └── auth.blade.php └── css │ └── auth-designer.css ├── .husky └── pre-commit ├── config └── auth-designer.php ├── src ├── View │ └── AuthDesignerRenderHook.php ├── Pages │ └── Auth │ │ ├── EditProfile.php │ │ ├── Login.php │ │ ├── Register.php │ │ ├── ResetPassword.php │ │ ├── RequestPasswordReset.php │ │ └── EmailVerification.php ├── Concerns │ ├── HasRenderHooks.php │ ├── HasDefaults.php │ ├── HasAuthDesignerLayout.php │ ├── HasThemeSwitcher.php │ └── HasPages.php ├── Enums │ └── MediaPosition.php ├── AuthDesignerServiceProvider.php ├── Support │ └── MediaDetector.php ├── AuthDesignerConfigRepository.php ├── AuthDesignerPlugin.php └── Data │ ├── AuthDesignerConfig.php │ └── AuthPageConfig.php ├── package.json ├── rector.php ├── LICENSE.md ├── CHANGELOG.md ├── composer.json └── README.md /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /config/auth-designer.php: -------------------------------------------------------------------------------- 1 | renderHooks[$name][] = $hook; 14 | 15 | return $this; 16 | } 17 | 18 | public function getRenderHooks(): array 19 | { 20 | return $this->renderHooks; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Pages/Auth/Login.php: -------------------------------------------------------------------------------- 1 | '', 4 | 'videoClass' => '', 5 | ]) 6 | 7 | @if($config->isVideo()) 8 | 17 | @else 18 | {{ $config->mediaAlt ?? 'Authentication' }} 23 | @endif 24 | -------------------------------------------------------------------------------- /src/Pages/Auth/RequestPasswordReset.php: -------------------------------------------------------------------------------- 1 | $value) { 11 | $styles .= "--ad-theme-switcher-{$key}: {$value}; "; 12 | } 13 | $styles .= '"'; 14 | } 15 | 16 | $hasDarkMode = Filament::hasDarkMode(); 17 | $hasDarkModeForced = Filament::hasDarkModeForced(); 18 | @endphp 19 | 20 | @if ($hasDarkMode && !$hasDarkModeForced) 21 |
22 | 23 |
24 | @endif 25 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 11 | __DIR__.'/src', 12 | __DIR__.'/tests', 13 | ]) 14 | ->withSkip([ 15 | __DIR__.'/vendor', 16 | ]) 17 | ->withSets([ 18 | LevelSetList::UP_TO_PHP_84, 19 | SetList::CODE_QUALITY, 20 | SetList::DEAD_CODE, 21 | SetList::EARLY_RETURN, 22 | SetList::TYPE_DECLARATION, 23 | ]) 24 | ->withPreparedSets( 25 | deadCode: true, 26 | codeQuality: true, 27 | typeDeclarations: true, 28 | earlyReturn: true, 29 | ); 30 | -------------------------------------------------------------------------------- /src/Concerns/HasDefaults.php: -------------------------------------------------------------------------------- 1 | defaultsConfigurator = $configure; 15 | 16 | return $this; 17 | } 18 | 19 | public function getDefaultsConfigurator(): ?Closure 20 | { 21 | return $this->defaultsConfigurator; 22 | } 23 | 24 | protected function buildDefaultsConfig(): ?AuthPageConfig 25 | { 26 | if ($this->defaultsConfigurator === null) { 27 | return null; 28 | } 29 | 30 | $config = new AuthPageConfig; 31 | ($this->defaultsConfigurator)($config); 32 | 33 | return $config; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Concerns/HasAuthDesignerLayout.php: -------------------------------------------------------------------------------- 1 | shareAuthDesignerConfig(); 19 | } 20 | 21 | protected function shareAuthDesignerConfig(): void 22 | { 23 | $repository = app(AuthDesignerConfigRepository::class); 24 | $config = $repository->getConfig($this->getAuthDesignerPageKey()); 25 | 26 | View::share('authDesignerConfig', $config); 27 | } 28 | 29 | abstract protected function getAuthDesignerPageKey(): string; 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Caresome 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/AuthDesignerServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(static::$name) 19 | ->hasConfigFile() 20 | ->hasViews('filament-auth-designer'); 21 | } 22 | 23 | public function register(): void 24 | { 25 | parent::register(); 26 | 27 | $this->app->singleton(AuthDesignerConfigRepository::class); 28 | $this->app->singleton(MediaDetector::class); 29 | } 30 | 31 | public function packageBooted(): void 32 | { 33 | FilamentAsset::register([ 34 | Css::make('auth-designer', __DIR__.'/../resources/css/auth-designer.css'), 35 | ], package: 'caresome/filament-auth-designer'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Concerns/HasThemeSwitcher.php: -------------------------------------------------------------------------------- 1 | '1.5rem', 11 | 'right' => '1.5rem', 12 | 'bottom' => 'auto', 13 | 'left' => 'auto', 14 | ]; 15 | 16 | public function themeToggle(?string $top = null, ?string $right = null, ?string $bottom = null, ?string $left = null): static 17 | { 18 | $this->showThemeSwitcher = true; 19 | 20 | if ($top === null && $right === null && $bottom === null && $left === null) { 21 | $this->themePosition = ['top' => '1.5rem', 'right' => '1.5rem', 'bottom' => 'auto', 'left' => 'auto']; 22 | 23 | return $this; 24 | } 25 | 26 | $this->themePosition = [ 27 | 'top' => $top ?? 'auto', 28 | 'right' => $right ?? 'auto', 29 | 'bottom' => $bottom ?? 'auto', 30 | 'left' => $left ?? 'auto', 31 | ]; 32 | 33 | return $this; 34 | } 35 | 36 | public function hasThemeSwitcher(): bool 37 | { 38 | return $this->showThemeSwitcher; 39 | } 40 | 41 | public function getThemePosition(): array 42 | { 43 | return $this->themePosition; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Pages/Auth/EmailVerification.php: -------------------------------------------------------------------------------- 1 | link() 27 | ->label(__('filament-panels::layout.actions.logout.label')) 28 | ->color('danger') 29 | ->size('sm') 30 | ->url(Filament::getLogoutUrl()) 31 | ->postToUrl(); 32 | } 33 | 34 | public function content(Schema $schema): Schema 35 | { 36 | $parentSchema = parent::content($schema); 37 | $parentComponents = $parentSchema->getComponents(); 38 | 39 | return $parentSchema->components([ 40 | ...$parentComponents, 41 | Actions::make([$this->logoutAction()]), 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Support/MediaDetector.php: -------------------------------------------------------------------------------- 1 | getExtension($path); 14 | 15 | return in_array($extension, self::VIDEO_EXTENSIONS, true); 16 | } 17 | 18 | public function isImage(string $path): bool 19 | { 20 | $extension = $this->getExtension($path); 21 | 22 | return in_array($extension, self::IMAGE_EXTENSIONS, true); 23 | } 24 | 25 | public function getExtension(string $path): string 26 | { 27 | $path = strtok($path, '?'); 28 | $path = strtok($path, '#'); 29 | 30 | return strtolower(pathinfo($path, PATHINFO_EXTENSION)); 31 | } 32 | 33 | public function getMimeType(string $path): string 34 | { 35 | $extension = $this->getExtension($path); 36 | 37 | return match ($extension) { 38 | 'mp4' => 'video/mp4', 39 | 'webm' => 'video/webm', 40 | 'mov' => 'video/quicktime', 41 | 'ogg' => 'video/ogg', 42 | 'avi' => 'video/x-msvideo', 43 | 'mkv' => 'video/x-matroska', 44 | 'jpg', 'jpeg' => 'image/jpeg', 45 | 'png' => 'image/png', 46 | 'gif' => 'image/gif', 47 | 'webp' => 'image/webp', 48 | 'svg' => 'image/svg+xml', 49 | 'avif' => 'image/avif', 50 | default => 'application/octet-stream', 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AuthDesignerConfigRepository.php: -------------------------------------------------------------------------------- 1 | '1.5rem', 18 | 'right' => '1.5rem', 19 | 'bottom' => 'auto', 20 | 'left' => 'auto', 21 | ]; 22 | 23 | public function setDefaults(AuthPageConfig $config): void 24 | { 25 | $this->defaults = $config; 26 | } 27 | 28 | public function getDefaults(): ?AuthPageConfig 29 | { 30 | return $this->defaults; 31 | } 32 | 33 | public function setPageConfig(string $page, AuthPageConfig $config): void 34 | { 35 | $this->pageConfigs[$page] = $config; 36 | } 37 | 38 | public function getPageConfig(string $page): ?AuthPageConfig 39 | { 40 | return $this->pageConfigs[$page] ?? null; 41 | } 42 | 43 | public function hasPageConfig(string $page): bool 44 | { 45 | return isset($this->pageConfigs[$page]); 46 | } 47 | 48 | public function setThemeSwitcher(bool $enabled, array $position): void 49 | { 50 | $this->showThemeSwitcher = $enabled; 51 | $this->themePosition = $position; 52 | } 53 | 54 | public function getConfig(string $page): AuthDesignerConfig 55 | { 56 | $pageConfig = $this->getMergedPageConfig($page); 57 | 58 | return AuthDesignerConfig::fromPageConfig( 59 | config: $pageConfig, 60 | showThemeSwitcher: $pageConfig->getShowThemeSwitcher() ?? $this->showThemeSwitcher, 61 | themePosition: $pageConfig->getThemePosition() ?? $this->themePosition, 62 | ); 63 | } 64 | 65 | protected function getMergedPageConfig(string $page): AuthPageConfig 66 | { 67 | $pageConfig = $this->pageConfigs[$page] ?? new AuthPageConfig; 68 | 69 | if ($this->defaults instanceof AuthPageConfig) { 70 | return $pageConfig->mergeWith($this->defaults); 71 | } 72 | 73 | return $pageConfig; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v2.0.3 - 2025-12-06 6 | 7 | ### Added 8 | 9 | - **Profile Page Support** - Now supports `profile()` configuration with all auth designer features (media, position, blur, etc.) 10 | 11 | ## v2.0.2 - 2025-12-06 12 | 13 | ### Fixed 14 | 15 | - Ensured blur effect applies to all media positions (Left, Right, Top, Bottom, Cover) instead of just Cover 16 | 17 | ## v2.0.1 - 2025-12-06 18 | 19 | ### Fixed 20 | 21 | - Fixed inverted layout logic for `MediaPosition::Left` and `MediaPosition::Right` in CSS 22 | 23 | ## v2.0.0 - 2025-12-06 24 | 25 | ### Added 26 | 27 | - **Closure-based API** for flexible configuration 28 | - **Global defaults** via `->defaults()` 29 | - **Custom page classes** via `->usingPage()` 30 | - **Dynamic Media Positioning** - Replaces fixed layouts. Supports Left, Right, Top ("Stacked"), Bottom, and Cover 31 | - **Dynamic Theme Switcher** - Position the theme toggle button anywhere using CSS values, with support for per-page overrides 32 | - **Render Hooks** - Inject content into layouts 33 | - **Alt text support** for accessibility 34 | 35 | ### Changed 36 | 37 | - **BREAKING**: Replaced `AuthLayout` and `MediaDirection` enums with `MediaPosition` 38 | - **BREAKING**: Replaced `ThemePosition` enum with dynamic named arguments for `themeToggle()` 39 | - Refactored Blade views to use dynamic inline styles 40 | - Configuration now uses `AuthDesignerConfigRepository` singleton 41 | 42 | ### Removed 43 | 44 | - `AuthLayout`, `MediaDirection`, and `ThemePosition` enums 45 | - `config/auth-designer.php` file 46 | - `ConfigKeys` class 47 | 48 | ### Fixed 49 | 50 | - Custom auth page classes are no longer overridden (GitHub Issue #1) 51 | - Media detection handles URLs with query strings 52 | 53 | ## v1.0.0 - 2025-10-12 54 | 55 | ### Added 56 | 57 | - Initial release 58 | - Five layout types for auth pages (None, Split, Overlay, Top Banner, Side Panel) 59 | - Media background support for images and videos 60 | - Customizable media positioning (left/right) 61 | - Blur effects for overlay layouts with configurable intensity 62 | - Light/dark/system theme toggle 63 | - Theme toggle position control (top-right, top-left, bottom-right, bottom-left) 64 | - Logout button on email verification page for easy account switching 65 | - Comprehensive test suite with 19 tests and 75 assertions 66 | - Full integration with Filament v4 67 | - CSS variable-based styling for theme compatibility 68 | - Centered media positioning with object-fit cover 69 | -------------------------------------------------------------------------------- /resources/views/components/layouts/auth.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use Caresome\FilamentAuthDesigner\View\AuthDesignerRenderHook; 3 | 4 | $config = $authDesignerConfig; 5 | $hasMedia = $config->hasMedia(); 6 | $position = $config->position; 7 | $isCover = $config->isCover(); 8 | @endphp 9 | 10 | 11 | @if ($config->showThemeSwitcher) 12 | @include('filament-auth-designer::components.partials.theme-toggle', [ 13 | 'position' => $config->themePosition, 14 | ]) 15 | @endif 16 | 17 | @php 18 | $layoutStyles = []; 19 | 20 | if ($hasMedia && !$isCover && $config->mediaSize) { 21 | $layoutStyles[] = $config->getMediaSizeStyle(); 22 | } 23 | 24 | if ($config->blur > 0) { 25 | $layoutStyles[] = "--ad-blur: {$config->blur}px; --blur-overlay: {$config->getBlurOverlay()}; --blur-content: {$config->getBlurContent()}"; 26 | } 27 | @endphp 28 | 29 |
31 | @if ($hasMedia) 32 |
33 |
34 | @include('filament-auth-designer::components.partials.media', [ 35 | 'config' => $config, 36 | 'imageClass' => 'fi-auth-media', 37 | 'videoClass' => 'fi-auth-media', 38 | ]) 39 |
40 |
41 | @if ($config->hasRenderHook(AuthDesignerRenderHook::MediaOverlay)) 42 |
43 | {!! $config->renderHook(AuthDesignerRenderHook::MediaOverlay) !!} 44 |
45 | @endif 46 |
47 | @endif 48 | 49 |
50 | @if ($isCover) 51 | {!! $config->renderHook(AuthDesignerRenderHook::CardBefore) !!} 52 | 53 | {{ $slot }} 54 | 55 | {!! $config->renderHook(AuthDesignerRenderHook::CardAfter) !!} 56 | @else 57 |
58 | {{ $slot }} 59 |
60 | @endif 61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caresome/filament-auth-designer", 3 | "description": "Transform Filament's default auth pages into stunning, brand-ready experiences", 4 | "keywords": [ 5 | "caresome", 6 | "laravel", 7 | "filament", 8 | "filament-plugin", 9 | "auth-designer", 10 | "authentication", 11 | "auth-pages" 12 | ], 13 | "homepage": "https://github.com/caresome/filament-auth-designer", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Caresome", 18 | "email": "dev@caresome.app", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.2|^8.3|^8.4", 24 | "filament/filament": "^4.0", 25 | "spatie/laravel-package-tools": "^1.16", 26 | "illuminate/contracts": "^11.0||^12.0" 27 | }, 28 | "require-dev": { 29 | "larastan/larastan": "^3.0", 30 | "laravel/pint": "^1.14", 31 | "nunomaduro/collision": "^8.8", 32 | "orchestra/testbench": "^10.0.0||^9.0.0", 33 | "pestphp/pest": "^4.0", 34 | "pestphp/pest-plugin-arch": "^4.0", 35 | "pestphp/pest-plugin-laravel": "^4.0", 36 | "phpstan/extension-installer": "^1.4", 37 | "phpstan/phpstan-deprecation-rules": "^2.0", 38 | "phpstan/phpstan-phpunit": "^2.0", 39 | "rector/rector": "^2.2", 40 | "spatie/laravel-ray": "^1.35" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Caresome\\FilamentAuthDesigner\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Caresome\\FilamentAuthDesigner\\Tests\\": "tests/", 50 | "Workbench\\App\\": "workbench/app/" 51 | } 52 | }, 53 | "scripts": { 54 | "post-autoload-dump": "@composer run prepare", 55 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 56 | "analyse": "vendor/bin/phpstan analyse", 57 | "test": "vendor/bin/pest", 58 | "test-coverage": "vendor/bin/pest --coverage", 59 | "format": "vendor/bin/pint", 60 | "refactor": "vendor/bin/rector process", 61 | "refactor:dry": "vendor/bin/rector process --dry-run" 62 | }, 63 | "config": { 64 | "sort-packages": true, 65 | "allow-plugins": { 66 | "pestphp/pest-plugin": true, 67 | "phpstan/extension-installer": true 68 | } 69 | }, 70 | "extra": { 71 | "laravel": { 72 | "providers": [ 73 | "Caresome\\FilamentAuthDesigner\\AuthDesignerServiceProvider" 74 | ] 75 | } 76 | }, 77 | "minimum-stability": "stable", 78 | "prefer-stable": true 79 | } 80 | -------------------------------------------------------------------------------- /src/AuthDesignerPlugin.php: -------------------------------------------------------------------------------- 1 | hasLogin()) { 29 | $panel->login($this->getLoginPageClass()); 30 | } 31 | 32 | if ($this->hasRegistration()) { 33 | $panel->registration($this->getRegistrationPageClass()); 34 | } 35 | 36 | if ($this->hasPasswordReset()) { 37 | $panel->passwordReset( 38 | $this->getRequestPasswordResetPageClass(), 39 | $this->getResetPasswordPageClass() 40 | ); 41 | } 42 | 43 | if ($this->hasEmailVerification()) { 44 | $panel->emailVerification($this->getEmailVerificationPageClass()); 45 | } 46 | 47 | if ($this->hasProfile()) { 48 | $panel->profile($this->getProfilePageClass()); 49 | } 50 | } 51 | 52 | public function boot(Panel $panel): void 53 | { 54 | $this->configureRepository(); 55 | $this->registerRenderHooks(); 56 | } 57 | 58 | protected function registerRenderHooks(): void 59 | { 60 | foreach ($this->renderHooks as $name => $hooks) { 61 | foreach ($hooks as $hook) { 62 | FilamentView::registerRenderHook($name, $hook); 63 | } 64 | } 65 | } 66 | 67 | public function configureRepository(): void 68 | { 69 | $repository = app(AuthDesignerConfigRepository::class); 70 | 71 | $defaults = $this->buildDefaultsConfig(); 72 | if ($defaults instanceof AuthPageConfig) { 73 | $repository->setDefaults($defaults); 74 | } 75 | 76 | if ($this->hasLogin()) { 77 | $repository->setPageConfig('login', $this->buildPageConfig($this->loginConfigurator)); 78 | } 79 | 80 | if ($this->hasRegistration()) { 81 | $repository->setPageConfig('registration', $this->buildPageConfig($this->registrationConfigurator)); 82 | } 83 | 84 | if ($this->hasPasswordReset()) { 85 | $repository->setPageConfig('password-reset', $this->buildPageConfig($this->passwordResetConfigurator)); 86 | } 87 | 88 | if ($this->hasEmailVerification()) { 89 | $repository->setPageConfig('email-verification', $this->buildPageConfig($this->emailVerificationConfigurator)); 90 | } 91 | 92 | if ($this->hasProfile()) { 93 | $repository->setPageConfig('profile', $this->buildPageConfig($this->profileConfigurator)); 94 | } 95 | 96 | $repository->setThemeSwitcher($this->showThemeSwitcher, $this->themePosition); 97 | } 98 | 99 | public static function make(): static 100 | { 101 | return app(static::class); 102 | } 103 | 104 | public static function get(): static 105 | { 106 | /** @var static */ 107 | return filament(app(static::class)->getId()); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Data/AuthDesignerConfig.php: -------------------------------------------------------------------------------- 1 | media !== null && $this->media !== ''; 28 | } 29 | 30 | public function isVideo(): bool 31 | { 32 | return $this->isVideo; 33 | } 34 | 35 | public function isCover(): bool 36 | { 37 | return $this->position?->isCover() ?? false; 38 | } 39 | 40 | public function isHorizontal(): bool 41 | { 42 | return $this->position?->isHorizontal() ?? false; 43 | } 44 | 45 | private const BLUR_CONTENT_MULTIPLIER = 2.5; 46 | 47 | public function isVertical(): bool 48 | { 49 | return $this->position?->isVertical() ?? false; 50 | } 51 | 52 | public function getBlurOverlay(): string 53 | { 54 | return $this->blur.'px'; 55 | } 56 | 57 | public function getBlurContent(): string 58 | { 59 | return ($this->blur * self::BLUR_CONTENT_MULTIPLIER).'px'; 60 | } 61 | 62 | public function getMediaSizeStyle(): string 63 | { 64 | if (! $this->mediaSize || $this->isCover()) { 65 | return ''; 66 | } 67 | 68 | if ($this->isHorizontal()) { 69 | return '--media-size: '.$this->mediaSize; 70 | } 71 | 72 | if ($this->isVertical()) { 73 | return '--media-size: '.$this->mediaSize; 74 | } 75 | 76 | return ''; 77 | } 78 | 79 | public function hasRenderHook(string $name): bool 80 | { 81 | return isset($this->renderHooks[$name]) && count($this->renderHooks[$name]) > 0; 82 | } 83 | 84 | public function renderHook(string $name): string 85 | { 86 | if (! $this->hasRenderHook($name)) { 87 | return ''; 88 | } 89 | 90 | $output = ''; 91 | foreach ($this->renderHooks[$name] as $hook) { 92 | $result = $hook(); 93 | $output .= $result instanceof \Illuminate\Contracts\View\View ? $result->render() : $result; 94 | } 95 | 96 | return $output; 97 | } 98 | 99 | public static function fromPageConfig( 100 | AuthPageConfig $config, 101 | bool $showThemeSwitcher, 102 | array $themePosition, 103 | ): static { 104 | $media = $config->getMedia(); 105 | $hasMedia = $media !== null && $media !== ''; 106 | $mediaDetector = app(MediaDetector::class); 107 | 108 | return new self( 109 | position: $config->getEffectivePosition(), 110 | media: $media, 111 | mediaSize: $config->getMediaSize(), 112 | blur: $config->getBlur(), 113 | mediaAlt: $config->getMediaAlt(), 114 | showThemeSwitcher: $showThemeSwitcher, 115 | themePosition: $themePosition, 116 | isVideo: $hasMedia && $mediaDetector->isVideo($media), 117 | mediaMimeType: $hasMedia ? $mediaDetector->getMimeType($media) : null, 118 | renderHooks: $config->getRenderHooks(), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Concerns/HasPages.php: -------------------------------------------------------------------------------- 1 | hasLogin = true; 39 | $this->loginConfigurator = $configure; 40 | 41 | return $this; 42 | } 43 | 44 | public function registration(?Closure $configure = null): static 45 | { 46 | $this->hasRegistration = true; 47 | $this->registrationConfigurator = $configure; 48 | 49 | return $this; 50 | } 51 | 52 | public function passwordReset(?Closure $configure = null): static 53 | { 54 | $this->hasPasswordReset = true; 55 | $this->passwordResetConfigurator = $configure; 56 | 57 | return $this; 58 | } 59 | 60 | public function emailVerification(?Closure $configure = null): static 61 | { 62 | $this->hasEmailVerification = true; 63 | $this->emailVerificationConfigurator = $configure; 64 | 65 | return $this; 66 | } 67 | 68 | public function profile(?Closure $configure = null): static 69 | { 70 | $this->hasProfile = true; 71 | $this->profileConfigurator = $configure; 72 | 73 | return $this; 74 | } 75 | 76 | public function hasLogin(): bool 77 | { 78 | return $this->hasLogin; 79 | } 80 | 81 | public function hasRegistration(): bool 82 | { 83 | return $this->hasRegistration; 84 | } 85 | 86 | public function hasPasswordReset(): bool 87 | { 88 | return $this->hasPasswordReset; 89 | } 90 | 91 | public function hasEmailVerification(): bool 92 | { 93 | return $this->hasEmailVerification; 94 | } 95 | 96 | public function hasProfile(): bool 97 | { 98 | return $this->hasProfile; 99 | } 100 | 101 | protected function buildPageConfig(?Closure $configurator): AuthPageConfig 102 | { 103 | $config = new AuthPageConfig; 104 | 105 | if ($configurator instanceof \Closure) { 106 | $configurator($config); 107 | } 108 | 109 | return $config; 110 | } 111 | 112 | protected function getLoginPageClass(): string 113 | { 114 | $config = $this->buildPageConfig($this->loginConfigurator); 115 | 116 | return $config->getPageClass() ?? Login::class; 117 | } 118 | 119 | protected function getRegistrationPageClass(): string 120 | { 121 | $config = $this->buildPageConfig($this->registrationConfigurator); 122 | 123 | return $config->getPageClass() ?? Register::class; 124 | } 125 | 126 | protected function getRequestPasswordResetPageClass(): string 127 | { 128 | $config = $this->buildPageConfig($this->passwordResetConfigurator); 129 | 130 | return $config->getPageClass() ?? RequestPasswordReset::class; 131 | } 132 | 133 | protected function getResetPasswordPageClass(): string 134 | { 135 | return ResetPassword::class; 136 | } 137 | 138 | protected function getEmailVerificationPageClass(): string 139 | { 140 | $config = $this->buildPageConfig($this->emailVerificationConfigurator); 141 | 142 | return $config->getPageClass() ?? EmailVerification::class; 143 | } 144 | 145 | protected function getProfilePageClass(): string 146 | { 147 | $config = $this->buildPageConfig($this->profileConfigurator); 148 | 149 | return $config->getPageClass() ?? EditProfile::class; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Data/AuthPageConfig.php: -------------------------------------------------------------------------------- 1 | media = $media; 29 | $this->mediaAlt = $alt; 30 | 31 | return $this; 32 | } 33 | 34 | public function mediaPosition(?MediaPosition $position): static 35 | { 36 | $this->position = $position; 37 | 38 | return $this; 39 | } 40 | 41 | public function mediaSize(?string $size): static 42 | { 43 | $this->mediaSize = $size; 44 | 45 | return $this; 46 | } 47 | 48 | public function blur(int $blur): static 49 | { 50 | $this->blur = max(0, min(20, $blur)); 51 | 52 | return $this; 53 | } 54 | 55 | public function usingPage(string $pageClass): static 56 | { 57 | $this->pageClass = $pageClass; 58 | 59 | return $this; 60 | } 61 | 62 | public function renderHook(string $name, \Closure $hook): static 63 | { 64 | $this->renderHooks[$name][] = $hook; 65 | 66 | return $this; 67 | } 68 | 69 | public function getMedia(): ?string 70 | { 71 | return $this->media; 72 | } 73 | 74 | public function getPosition(): ?MediaPosition 75 | { 76 | return $this->position; 77 | } 78 | 79 | public function getEffectivePosition(): ?MediaPosition 80 | { 81 | if (! $this->hasMedia()) { 82 | return null; 83 | } 84 | 85 | return $this->position ?? MediaPosition::Cover; 86 | } 87 | 88 | public function getMediaSize(): ?string 89 | { 90 | return $this->mediaSize; 91 | } 92 | 93 | public function getBlur(): int 94 | { 95 | return $this->blur; 96 | } 97 | 98 | public function getMediaAlt(): ?string 99 | { 100 | return $this->mediaAlt; 101 | } 102 | 103 | public function getPageClass(): ?string 104 | { 105 | return $this->pageClass; 106 | } 107 | 108 | public function hasMedia(): bool 109 | { 110 | return $this->media !== null && $this->media !== ''; 111 | } 112 | 113 | public function isVideo(): bool 114 | { 115 | if (! $this->hasMedia()) { 116 | return false; 117 | } 118 | 119 | return app(MediaDetector::class)->isVideo($this->media); 120 | } 121 | 122 | public function hasCustomPage(): bool 123 | { 124 | return $this->pageClass !== null; 125 | } 126 | 127 | public function getRenderHooks(): array 128 | { 129 | return $this->renderHooks; 130 | } 131 | 132 | protected ?bool $showThemeSwitcher = null; 133 | 134 | protected ?array $themePosition = null; 135 | 136 | public function themeToggle(?string $top = null, ?string $right = null, ?string $bottom = null, ?string $left = null): static 137 | { 138 | $this->showThemeSwitcher = true; 139 | 140 | if ($top === null && $right === null && $bottom === null && $left === null) { 141 | $this->themePosition = ['top' => '1.5rem', 'right' => '1.5rem', 'bottom' => 'auto', 'left' => 'auto']; 142 | 143 | return $this; 144 | } 145 | 146 | $this->themePosition = [ 147 | 'top' => $top ?? 'auto', 148 | 'right' => $right ?? 'auto', 149 | 'bottom' => $bottom ?? 'auto', 150 | 'left' => $left ?? 'auto', 151 | ]; 152 | 153 | return $this; 154 | } 155 | 156 | public function getShowThemeSwitcher(): ?bool 157 | { 158 | return $this->showThemeSwitcher; 159 | } 160 | 161 | public function getThemePosition(): ?array 162 | { 163 | return $this->themePosition; 164 | } 165 | 166 | public function mergeWith(AuthPageConfig $defaults): static 167 | { 168 | $merged = new self; 169 | 170 | $merged->media = $this->media ?? $defaults->media; 171 | $merged->mediaAlt = $this->mediaAlt ?? $defaults->mediaAlt; 172 | $merged->position = $this->position ?? $defaults->position; 173 | $merged->mediaSize = $this->mediaSize ?? $defaults->mediaSize; 174 | $merged->blur = $this->blur !== 0 ? $this->blur : $defaults->blur; 175 | $merged->pageClass = $this->pageClass ?? $defaults->pageClass; 176 | $merged->renderHooks = $this->mergeRenderHooks($this->renderHooks, $defaults->renderHooks); 177 | $merged->showThemeSwitcher = $this->showThemeSwitcher ?? $defaults->showThemeSwitcher; 178 | $merged->themePosition = $this->themePosition ?? $defaults->themePosition; 179 | 180 | return $merged; 181 | } 182 | 183 | protected function mergeRenderHooks(array $pageHooks, array $defaultHooks): array 184 | { 185 | $merged = $defaultHooks; 186 | 187 | foreach ($pageHooks as $name => $hooks) { 188 | foreach ($hooks as $hook) { 189 | $merged[$name][] = $hook; 190 | } 191 | } 192 | 193 | return $merged; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /resources/css/auth-designer.css: -------------------------------------------------------------------------------- 1 | .fi-simple-header { 2 | align-items: start; 3 | } 4 | 5 | .fi-simple-header-heading { 6 | text-align: left; 7 | } 8 | 9 | /* ============================================ 10 | Unified Auth Layout System 11 | ============================================ */ 12 | 13 | .fi-auth-layout { 14 | display: flex; 15 | min-height: 100vh; 16 | } 17 | 18 | /* No media - centered form */ 19 | .fi-auth-layout.no-media { 20 | align-items: center; 21 | justify-content: center; 22 | padding: 1.5rem; 23 | } 24 | 25 | .fi-auth-layout.no-media .fi-auth-content-section { 26 | width: 100%; 27 | max-width: 28rem; 28 | } 29 | 30 | /* ============================================ 31 | Media Section - Common Styles 32 | ============================================ */ 33 | 34 | .fi-auth-media-wrapper { 35 | position: absolute; 36 | inset: 0; 37 | } 38 | 39 | .fi-auth-media { 40 | height: 100%; 41 | width: 100%; 42 | object-fit: cover; 43 | object-position: center; 44 | } 45 | 46 | .fi-auth-media-overlay { 47 | position: absolute; 48 | inset: 0; 49 | } 50 | 51 | .fi-auth-media-content { 52 | position: absolute; 53 | inset: 0; 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | z-index: 10; 58 | padding: 2rem; 59 | color: white; 60 | text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 61 | } 62 | 63 | /* ============================================ 64 | Horizontal Positions (Left / Right) 65 | ============================================ */ 66 | 67 | .fi-auth-layout.media-left, 68 | .fi-auth-layout.media-right { 69 | flex-direction: row; 70 | } 71 | 72 | .fi-auth-layout.media-left .fi-auth-media-section, 73 | .fi-auth-layout.media-right .fi-auth-media-section { 74 | position: relative; 75 | display: none; 76 | width: var(--media-size, 50%); 77 | flex-shrink: 0; 78 | overflow: hidden; 79 | } 80 | 81 | .fi-auth-layout.media-left .fi-auth-media-overlay, 82 | .fi-auth-layout.media-right .fi-auth-media-overlay { 83 | background: linear-gradient(to top, color-mix(in srgb, var(--gray-900) 50%, transparent), transparent); 84 | backdrop-filter: blur(var(--ad-blur, 0px)); 85 | } 86 | 87 | .fi-auth-layout.media-left .fi-auth-content-section, 88 | .fi-auth-layout.media-right .fi-auth-content-section { 89 | display: flex; 90 | flex: 1; 91 | flex-direction: column; 92 | align-items: center; 93 | justify-content: center; 94 | padding: 3rem 1.5rem; 95 | } 96 | 97 | .fi-auth-layout.media-left .fi-auth-form-container, 98 | .fi-auth-layout.media-right .fi-auth-form-container { 99 | width: 100%; 100 | max-width: 28rem; 101 | } 102 | 103 | .fi-auth-layout.media-right { 104 | flex-direction: row-reverse; 105 | } 106 | 107 | @media (min-width: 1024px) { 108 | .fi-auth-layout.media-left .fi-auth-media-section, 109 | .fi-auth-layout.media-right .fi-auth-media-section { 110 | display: block; 111 | } 112 | 113 | .fi-auth-layout.media-left .fi-auth-content-section, 114 | .fi-auth-layout.media-right .fi-auth-content-section { 115 | padding: 3rem; 116 | } 117 | } 118 | 119 | /* ============================================ 120 | Vertical Positions (Top / Bottom) 121 | ============================================ */ 122 | 123 | .fi-auth-layout.media-top, 124 | .fi-auth-layout.media-bottom { 125 | flex-direction: column; 126 | } 127 | 128 | .fi-auth-layout.media-bottom { 129 | flex-direction: column-reverse; 130 | } 131 | 132 | .fi-auth-layout.media-top .fi-auth-media-section, 133 | .fi-auth-layout.media-bottom .fi-auth-media-section { 134 | position: relative; 135 | width: 100%; 136 | flex: 0 0 var(--media-size, 250px); 137 | overflow: hidden; 138 | } 139 | 140 | .fi-auth-layout.media-top .fi-auth-media-overlay { 141 | background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--gray-900) 30%, transparent)); 142 | backdrop-filter: blur(var(--ad-blur, 0px)); 143 | } 144 | 145 | .fi-auth-layout.media-bottom .fi-auth-media-overlay { 146 | background: linear-gradient(to top, transparent, color-mix(in srgb, var(--gray-900) 30%, transparent)); 147 | backdrop-filter: blur(var(--ad-blur, 0px)); 148 | } 149 | 150 | .fi-auth-layout.media-top .fi-auth-content-section, 151 | .fi-auth-layout.media-bottom .fi-auth-content-section { 152 | display: flex; 153 | flex: 1 1 0; 154 | min-height: 0; 155 | align-items: center; 156 | justify-content: center; 157 | padding: 2rem 1.5rem; 158 | overflow: auto; 159 | } 160 | 161 | .fi-auth-layout.media-top .fi-auth-form-container, 162 | .fi-auth-layout.media-bottom .fi-auth-form-container { 163 | width: 100%; 164 | max-width: 28rem; 165 | } 166 | 167 | @media (min-width: 768px) { 168 | .fi-auth-layout.media-top .fi-auth-content-section, 169 | .fi-auth-layout.media-bottom .fi-auth-content-section { 170 | padding: 3rem 1.5rem; 171 | } 172 | } 173 | 174 | /* ============================================ 175 | Cover Position (Overlay / Fullscreen) 176 | ============================================ */ 177 | 178 | .fi-auth-layout.media-cover { 179 | position: relative; 180 | align-items: center; 181 | justify-content: center; 182 | padding: 1.5rem; 183 | } 184 | 185 | .fi-auth-layout.media-cover .fi-auth-media-section { 186 | position: fixed; 187 | inset: 0; 188 | z-index: 0; 189 | } 190 | 191 | .fi-auth-layout.media-cover .fi-auth-media-overlay { 192 | background: color-mix(in srgb, var(--gray-950) 40%, transparent); 193 | backdrop-filter: blur(var(--ad-blur, 0px)); 194 | } 195 | 196 | .fi-auth-layout.media-cover .fi-auth-content-section { 197 | position: relative; 198 | z-index: 10; 199 | width: 100%; 200 | max-width: 28rem; 201 | } 202 | 203 | .fi-auth-layout.media-cover .fi-auth-card { 204 | backdrop-filter: blur(calc(var(--ad-blur, 0px) * 2.5)); 205 | box-shadow: 0 20px 60px color-mix(in srgb, var(--gray-950) 30%, transparent); 206 | } 207 | 208 | /* ============================================ 209 | Theme Switcher 210 | ============================================ */ 211 | 212 | .fi-auth-theme-switcher-wrapper { 213 | position: fixed; 214 | z-index: 50; 215 | background-color: var(--gray-100); 216 | border-radius: var(--radius-lg); 217 | padding: 0.25rem; 218 | box-shadow: var(--ring-shadow); 219 | border: 1px solid var(--gray-200); 220 | } 221 | 222 | .dark .fi-auth-theme-switcher-wrapper { 223 | background-color: var(--gray-900); 224 | border: 1px solid var(--gray-800); 225 | } 226 | 227 | .fi-auth-theme-switcher-wrapper { 228 | top: var(--ad-theme-switcher-top, auto); 229 | right: var(--ad-theme-switcher-right, auto); 230 | bottom: var(--ad-theme-switcher-bottom, auto); 231 | left: var(--ad-theme-switcher-left, auto); 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filament Auth Designer 2 | 3 | [![Latest Version](https://img.shields.io/packagist/v/caresome/filament-auth-designer.svg?style=flat-square)](https://packagist.org/packages/caresome/filament-auth-designer) 4 | 5 | Transform Filament's default authentication pages into stunning, brand-ready experiences with customizable layouts, media backgrounds, and theme switching. 6 | 7 | > **Note:** This package is designed exclusively for **Filament v4**. For changes and updates, see the [CHANGELOG](CHANGELOG.md). 8 | 9 | ![filament-auth-designer-preview](https://github.com/user-attachments/assets/441dba74-3817-4f27-9e9c-99006b77aa36) 10 | 11 | ## Table of Contents 12 | 13 | - [Features](#features) 14 | - [Requirements](#requirements) 15 | - [Installation](#installation) 16 | - [Quick Start](#quick-start) 17 | - [Media Positioning](#media-positioning) 18 | - [Global Defaults](#global-defaults) 19 | - [Custom Page Classes](#custom-page-classes) 20 | - [Media Configuration](#media-configuration) 21 | - [Theme Toggle](#theme-toggle) 22 | - [Configuration Examples](#configuration-examples) 23 | - [Render Hooks](#render-hooks) 24 | - [Troubleshooting](#troubleshooting) 25 | - [Testing](#testing) 26 | - [License](#license) 27 | 28 | ## Features 29 | 30 | - 🎨 **Flexible Media Positioning** - Place media on any side (Left, Right, Top, Bottom) or as a fullscreen cover 31 | - 📐 **Custom Sizing** - Set media size with any CSS unit (%, px, vh, etc.) 32 | - 🖼️ **Media Backgrounds** - Support for both images and videos with auto-detection 33 | - 🌫️ **Blur Effects** - Configurable blur intensity (0-20) for Cover position 34 | - 🌓 **Theme Toggle** - Built-in light/dark/system theme switcher 35 | - 📍 **Positionable Theme Toggle** - Place theme switcher in any corner 36 | - 🔧 **Global Defaults** - Set defaults that apply to all auth pages 37 | - 🎯 **Per-Page Overrides** - Override defaults for specific pages 38 | - 🔌 **Custom Page Classes** - Use your own page classes with the plugin's layouts 39 | - 🪝 **Render Hooks** - Inject custom content at specific positions in layouts 40 | - ♿ **Accessibility** - Alt text support for media 41 | - 🚪 **Email Verification Logout** - Easy account switching from verification page 42 | - ⚡ **Zero Configuration** - Works out of the box with sensible defaults 43 | 44 | ## Requirements 45 | 46 | - PHP 8.2+ 47 | - Laravel 11.0 or 12.0 48 | - Filament 4.0 49 | 50 | ## Installation 51 | 52 | ```bash 53 | composer require caresome/filament-auth-designer 54 | ``` 55 | 56 | ## Quick Start 57 | 58 | Add to your Panel Provider (e.g., `app/Providers/Filament/AdminPanelProvider.php`): 59 | 60 | ```php 61 | use Caresome\FilamentAuthDesigner\AuthDesignerPlugin; 62 | use Caresome\FilamentAuthDesigner\Data\AuthPageConfig; 63 | use Caresome\FilamentAuthDesigner\Enums\MediaPosition; 64 | 65 | public function panel(Panel $panel): Panel 66 | { 67 | return $panel 68 | ->plugin( 69 | AuthDesignerPlugin::make() 70 | ->login(fn (AuthPageConfig $config) => $config 71 | ->media(asset('assets/background.jpg')) 72 | ->mediaPosition(MediaPosition::Cover) 73 | ->blur(8) 74 | ) 75 | ); 76 | } 77 | ``` 78 | 79 | ## Media Positioning 80 | 81 | Use `MediaPosition` to control where your media appears: 82 | 83 | | Position | Description | Size Applied As | 84 | | ---------- | ---------------------------------------- | --------------- | 85 | | **Left** | Media on left, form on right | Width | 86 | | **Right** | Media on right, form on left | Width | 87 | | **Top** | Media at top, form below | Height | 88 | | **Bottom** | Media at bottom, form above | Height | 89 | | **Cover** | Fullscreen background with centered form | Ignored | 90 | 91 | ### Default Behavior 92 | 93 | - **No media** → Minimal centered form 94 | - **Media without position** → Defaults to `Cover` 95 | - **Cover position** → `mediaSize()` is ignored (fullscreen) 96 | 97 | ### Position Examples 98 | 99 | #### Left Position (Split-style) 100 | 101 | ```php 102 | use Caresome\FilamentAuthDesigner\Enums\MediaPosition; 103 | 104 | ->login(fn ($config) => $config 105 | ->media(asset('assets/image.jpg')) 106 | ->mediaPosition(MediaPosition::Left) 107 | ->mediaSize('50%') // Media takes 50% width 108 | ) 109 | ``` 110 | 111 | #### Right Position 112 | 113 | ```php 114 | ->login(fn ($config) => $config 115 | ->media(asset('assets/image.jpg')) 116 | ->mediaPosition(MediaPosition::Right) 117 | ->mediaSize('40%') 118 | ) 119 | ``` 120 | 121 | #### Top Position (Banner-style) 122 | 123 | ```php 124 | ->login(fn ($config) => $config 125 | ->media(asset('assets/banner.jpg')) 126 | ->mediaPosition(MediaPosition::Top) 127 | ->mediaSize('250px') // Banner height 128 | ) 129 | ``` 130 | 131 | #### Bottom Position 132 | 133 | ```php 134 | ->login(fn ($config) => $config 135 | ->media(asset('assets/footer.jpg')) 136 | ->mediaPosition(MediaPosition::Bottom) 137 | ->mediaSize('200px') 138 | ) 139 | ``` 140 | 141 | #### Cover Position (Overlay-style) 142 | 143 | ```php 144 | ->login(fn ($config) => $config 145 | ->media(asset('assets/fullscreen.jpg')) 146 | ->mediaPosition(MediaPosition::Cover) 147 | ->blur(8) // Optional: 0-20 148 | ) 149 | ``` 150 | 151 | ### Size Units 152 | 153 | Use any valid CSS unit for `mediaSize()`: 154 | 155 | ```php 156 | ->mediaSize('400px') // Pixels 157 | ->mediaSize('30vh') // Viewport height 158 | ->mediaSize('20rem') // Rem units 159 | ``` 160 | 161 | ## Global Defaults 162 | 163 | Set defaults that apply to all auth pages, then override specific pages as needed: 164 | 165 | ```php 166 | AuthDesignerPlugin::make() 167 | ->defaults(fn ($config) => $config 168 | ->media(asset('assets/default-bg.jpg')) 169 | ->mediaPosition(MediaPosition::Cover) 170 | ->blur(8) 171 | ) 172 | ->login() // Uses defaults 173 | ->registration() // Uses defaults 174 | ->passwordReset(fn ($config) => $config 175 | ->mediaPosition(MediaPosition::Left) // Override position 176 | ->mediaSize('45%') 177 | ) 178 | ->emailVerification() // Uses defaults 179 | ->themeToggle() 180 | ``` 181 | 182 | ## Custom Page Classes 183 | 184 | Use your own custom auth page classes while keeping the plugin's layout features. This is useful when you need to customize the form (e.g., using username instead of email). 185 | 186 | ### Option 1: Extend the Plugin's Page 187 | 188 | ```php 189 | use Caresome\FilamentAuthDesigner\Pages\Auth\Login; 190 | 191 | class CustomLogin extends Login 192 | { 193 | public function form(Schema $schema): Schema 194 | { 195 | return $schema->components([ 196 | TextInput::make('username')->label('Username')->required(), 197 | $this->getPasswordFormComponent(), 198 | $this->getRememberFormComponent(), 199 | ]); 200 | } 201 | 202 | protected function getCredentialsFromFormData(array $data): array 203 | { 204 | return [ 205 | 'username' => $data['username'], 206 | 'password' => $data['password'], 207 | ]; 208 | } 209 | } 210 | 211 | // In your panel provider: 212 | AuthDesignerPlugin::make() 213 | ->login(fn ($config) => $config 214 | ->media(asset('assets/login-bg.jpg')) 215 | ->mediaPosition(MediaPosition::Cover) 216 | ->usingPage(CustomLogin::class) 217 | ) 218 | ``` 219 | 220 | ### Option 2: Use the Trait Directly 221 | 222 | ```php 223 | use Caresome\FilamentAuthDesigner\Concerns\HasAuthDesignerLayout; 224 | use Filament\Pages\Auth\Login; 225 | 226 | class CustomLogin extends Login 227 | { 228 | use HasAuthDesignerLayout; 229 | 230 | protected function getAuthDesignerPageKey(): string 231 | { 232 | return 'login'; 233 | } 234 | 235 | // Your customizations... 236 | } 237 | 238 | // In your panel provider: 239 | AuthDesignerPlugin::make() 240 | ->login(fn ($config) => $config 241 | ->usingPage(CustomLogin::class) 242 | ) 243 | ``` 244 | 245 | ## Media Configuration 246 | 247 | ### Images 248 | 249 | Supported formats: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.svg`, `.avif` 250 | 251 | ```php 252 | ->login(fn ($config) => $config 253 | ->media(asset('assets/background.jpg')) 254 | ) 255 | ``` 256 | 257 | ### Videos 258 | 259 | Supported formats: `.mp4`, `.webm`, `.mov`, `.ogg` 260 | 261 | Videos auto-play, loop continuously, and are muted by default. 262 | 263 | ```php 264 | ->login(fn ($config) => $config 265 | ->media(asset('assets/video.mp4')) 266 | ) 267 | ``` 268 | 269 | https://github.com/user-attachments/assets/154006f8-91b6-4e6e-9ed9-854442fe6a49 270 | 271 | ### Alt Text (Accessibility) 272 | 273 | ```php 274 | ->login(fn ($config) => $config 275 | ->media(asset('assets/background.jpg'), alt: 'Company branding image') 276 | ) 277 | ``` 278 | 279 | ## Theme Toggle 280 | 281 | Enable light/dark/system theme switcher: 282 | 283 | ```php 284 | ->themeToggle() // Default: Top Right (1.5rem) 285 | ->themeToggle(bottom: '2rem', right: '2rem') // Custom position 286 | ->themeToggle(top: '1rem', left: '1rem') // Top Left 287 | ``` 288 | 289 | You can position the theme switcher anywhere on the screen by passing `top`, `bottom`, `left`, or `right` CSS values. Defaults to `auto` if not specified. 290 | 291 | You can also override the theme switcher position for specific pages: 292 | 293 | ```php 294 | ->login(fn ($config) => $config 295 | ->themeToggle(bottom: '2rem', left: '2rem') 296 | ) 297 | ``` 298 | 299 | ![theme-position](https://github.com/user-attachments/assets/07be8080-9733-49d7-bef7-123be1d98997) 300 | 301 | ## Configuration Examples 302 | 303 | ### Simple - Same Layout for All Pages 304 | 305 | ```php 306 | AuthDesignerPlugin::make() 307 | ->defaults(fn ($config) => $config 308 | ->media(asset('assets/auth-bg.jpg')) 309 | ->mediaPosition(MediaPosition::Cover) 310 | ->blur(10) 311 | ) 312 | ->login() 313 | ->registration() 314 | ->passwordReset() 315 | ->emailVerification() 316 | ->themeToggle() 317 | ``` 318 | 319 | ### Advanced - Different Layout Per Page 320 | 321 | ```php 322 | AuthDesignerPlugin::make() 323 | ->defaults(fn ($config) => $config 324 | ->media(asset('assets/default-bg.jpg')) 325 | ->mediaPosition(MediaPosition::Right) 326 | ->mediaSize('50%') 327 | ) 328 | ->login(fn ($config) => $config 329 | ->media(asset('assets/login.jpg'), alt: 'Welcome back') 330 | ->mediaPosition(MediaPosition::Cover) 331 | ->blur(8) 332 | ) 333 | ->registration(fn ($config) => $config 334 | ->media(asset('assets/register.jpg')) 335 | ->mediaPosition(MediaPosition::Left) 336 | ->mediaSize('45%') 337 | ) 338 | ->passwordReset(fn ($config) => $config 339 | ->media(asset('assets/reset.jpg')) 340 | ->mediaPosition(MediaPosition::Top) 341 | ->mediaSize('200px') 342 | ) 343 | ->emailVerification() // Uses defaults 344 | ->profile(fn ($config) => $config 345 | ->media(asset('assets/profile-bg.jpg')) 346 | ->mediaPosition(MediaPosition::Right) 347 | ) 348 | ->themeToggle(bottom: '2rem', right: '2rem') 349 | ``` 350 | 351 | ### Available Methods 352 | 353 | | Method | Description | 354 | | ----------------------- | ----------------------------------------------------------- | 355 | | `->defaults()` | Set global defaults for all pages | 356 | | `->login()` | Configure login page | 357 | | `->registration()` | Configure registration page | 358 | | `->passwordReset()` | Configure password reset pages | 359 | | `->emailVerification()` | Configure email verification page | 360 | | `->profile()` | Configure profile page | 361 | | `->themeToggle()` | Enable theme switcher (defaults to top-right, customizable) | 362 | 363 | ### Configuration Options 364 | 365 | | Option | Description | Notes | 366 | | ------------------- | ------------------------------ | -------------------------------------- | 367 | | `->media()` | Set background image/video URL | First param is URL, second is alt text | 368 | | `->mediaPosition()` | Set media position | Left, Right, Top, Bottom, Cover | 369 | | `->mediaSize()` | Set media size | px/vh/rem; ignored for Cover | 370 | | `->blur()` | Blur intensity (0-20) | Applies to all positions | 371 | | `->usingPage()` | Use custom page class | For custom auth pages | 372 | | `->themeToggle()` | Set theme switcher position | Per-page override | 373 | 374 | ## Render Hooks 375 | 376 | Inject custom Blade content at specific positions within auth layouts: 377 | 378 | ```php 379 | use Caresome\FilamentAuthDesigner\View\AuthDesignerRenderHook; 380 | 381 | AuthDesignerPlugin::make() 382 | ->login(fn ($config) => $config 383 | ->media(asset('images/login-bg.jpg')) 384 | ->mediaPosition(MediaPosition::Cover) 385 | ->renderHook(AuthDesignerRenderHook::CardBefore, fn () => view('auth.branding')) 386 | ) 387 | ``` 388 | 389 | ### Available Hook Positions 390 | 391 | > **Note:** `CardBefore` and `CardAfter` are specific to the **Cover** layout where the form is inside a card. 392 | > For other layouts (Left, Right, etc.), where the form is not inside a card, use Filament's native render hooks: 393 | > 394 | > - `PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE` 395 | > - `PanelsRenderHook::AUTH_LOGIN_FORM_AFTER` 396 | 397 | | Hook | Description | Available In | 398 | | -------------- | ------------------------------- | ---------------------- | 399 | | `MediaOverlay` | Overlay content on top of media | All layouts with media | 400 | | `CardBefore` | Above the login card | Cover position only | 401 | | `CardAfter` | Below the login card | Cover position only | 402 | 403 | ### Hook Examples 404 | 405 | **Add branding above the login card (Cover position):** 406 | 407 | ```php 408 | ->login(fn ($config) => $config 409 | ->renderHook(AuthDesignerRenderHook::CardBefore, fn () => view('auth.branding')) 410 | ) 411 | ``` 412 | 413 | **Add company logo overlay on media:** 414 | 415 | ```php 416 | ->login(fn ($config) => $config 417 | ->renderHook(AuthDesignerRenderHook::MediaOverlay, fn () => view('auth.logo-overlay')) 418 | ) 419 | ``` 420 | 421 | **Multiple hooks at the same position:** 422 | 423 | ```php 424 | ->login(fn ($config) => $config 425 | ->renderHook(AuthDesignerRenderHook::CardBefore, fn () => view('auth.logo')) 426 | ->renderHook(AuthDesignerRenderHook::CardBefore, fn () => view('auth.welcome-message')) 427 | ) 428 | ``` 429 | 430 | ## Troubleshooting 431 | 432 | **Images not displaying:** 433 | 434 | - Verify asset path: `asset('path/to/image.jpg')` 435 | - Ensure files are in `public/` directory 436 | - Clear cache: `php artisan cache:clear` 437 | - Check browser console for 404 errors 438 | 439 | **Layout not applying:** 440 | 441 | - Clear view cache: `php artisan view:clear` 442 | - Verify enum usage: `MediaPosition::Cover` (not string) 443 | - Check plugin is registered in panel provider 444 | 445 | **Videos not auto-playing:** 446 | 447 | - Ensure format is supported (mp4, webm, mov, ogg) 448 | - Check browser autoplay policies 449 | - Test in different browsers 450 | 451 | **Blur effect not working:** 452 | 453 | - Value must be between 0-20 454 | - Some older browsers may not support backdrop-filter 455 | 456 | **Custom page not using layout:** 457 | 458 | - Ensure your custom page uses `HasAuthDesignerLayout` trait 459 | - Or extend the plugin's page class 460 | - Verify you're using `->usingPage()` in the config 461 | 462 | **Media size not applying:** 463 | 464 | - `mediaSize()` is ignored for `Cover` position 465 | - Ensure you're using a valid CSS unit 466 | 467 | ## Testing 468 | 469 | ```bash 470 | composer test # Run tests 471 | composer analyse # Run PHPStan 472 | composer format # Format code with Pint 473 | ``` 474 | 475 | ## License 476 | 477 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 478 | --------------------------------------------------------------------------------