├── stubs └── .gitkeep ├── resources ├── dist │ ├── .gitkeep │ ├── filament-tour.js │ └── filament-tour.css ├── views │ ├── .gitkeep │ ├── livewire │ │ └── filament-tour-widget.blade.php │ ├── tour │ │ └── step │ │ │ └── popover │ │ │ └── title.blade.php │ └── highlight │ │ └── button.blade.php ├── lang │ ├── de │ │ └── filament-tour.php │ ├── ar │ │ └── filament-tour.php │ ├── en │ │ └── filament-tour.php │ ├── fr │ │ └── filament-tour.php │ ├── pt_BR │ │ └── filament-tour.php │ └── pt_PT │ │ └── filament-tour.php ├── css │ └── index.css └── js │ ├── css-selector.js │ └── index.js ├── config └── filament-tour.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_tour_table.php.stub ├── LICENSE.md ├── bin └── build.js ├── src ├── FilamentTourServiceProvider.php ├── Tour │ ├── Traits │ │ ├── CanConstructRoute.php │ │ └── CanReadJson.php │ ├── Step │ │ └── StepEvent.php │ ├── HasTour.php │ ├── Step.php │ └── Tour.php ├── Highlight │ ├── HasHighlight.php │ └── Highlight.php ├── FilamentTourPlugin.php └── Livewire │ └── FilamentTourWidget.php ├── composer.json ├── CHANGELOG.md └── README.md /stubs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/lang/de/filament-tour.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'previous' => 'Zurück', 7 | 'next' => 'Weiter', 8 | 'done' => 'Fertig', 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /resources/lang/ar/filament-tour.php: -------------------------------------------------------------------------------- 1 | [ 7 | 8 | 'previous' => 'السابق', 9 | 'next' => 'التالي', 10 | 'done' => 'إنتهى', 11 | 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/en/filament-tour.php: -------------------------------------------------------------------------------- 1 | [ 7 | 8 | 'previous' => 'Previous', 9 | 'next' => 'Next', 10 | 'done' => 'Done', 11 | 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /config/filament-tour.php: -------------------------------------------------------------------------------- 1 | true, 7 | 'enable_css_selector' => false, 8 | 9 | 'tour_prefix_id' => 'tour_', 10 | 'highlight_prefix_id' => 'highlight_', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/fr/filament-tour.php: -------------------------------------------------------------------------------- 1 | [ 7 | 8 | 'previous' => 'Précédent', 9 | 'next' => 'Suivant', 10 | 'done' => 'Terminé', 11 | 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/pt_BR/filament-tour.php: -------------------------------------------------------------------------------- 1 | [ 7 | 8 | 'previous' => 'Anterior', 9 | 'next' => 'Próximo', 10 | 'done' => 'Concluir', 11 | 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/pt_PT/filament-tour.php: -------------------------------------------------------------------------------- 1 | [ 7 | 8 | 'previous' => 'Anterior', 9 | 'next' => 'Próximo', 10 | 'done' => 'Concluir', 11 | 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/views/livewire/filament-tour-widget.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 11 |
12 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /resources/views/tour/step/popover/title.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if(isset($icon)) 3 | 'text-gray-600 ring-gray-600/10 dark:text-gray-400 dark:ring-gray-400/20', 9 | default => 'text-custom-600 ring-custom-600/10 dark:text-custom-400 dark:ring-custom-400/30', 10 | }, 11 | ]) 12 | 13 | @style([ 14 | \Filament\Support\get_color_css_variables( 15 | $iconColor, 16 | shades: [ 17 | 50, 18 | 300, 19 | 400, 20 | ...$icon ? [500] : [], 21 | 600, 22 | 700, 23 | ] 24 | ) => $iconColor !== 'gray', 25 | 'margin-right:10px' 26 | ]) 27 | /> 28 | @endif 29 | {{$title}} 30 |
31 | -------------------------------------------------------------------------------- /resources/views/highlight/button.blade.php: -------------------------------------------------------------------------------- 1 |
9 | 'text-gray-600 ring-gray-600/10 dark:text-gray-400 dark:ring-gray-400/20', 14 | default => 'text-custom-600 ring-custom-600/10 dark:text-custom-400 dark:ring-custom-400/30', 15 | }, 16 | ]) 17 | 18 | @style([ 19 | \Filament\Support\get_color_css_variables( 20 | $iconColor, 21 | shades: [ 22 | 50, 23 | 300, 24 | 400, 25 | ...$icon ? [500] : [], 26 | 600, 27 | 700, 28 | ] 29 | ) => $iconColor !== 'gray', 30 | ]) 31 | /> 32 |
33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) JibayMcs 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 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const isDev = process.argv.includes('--dev') 4 | 5 | async function compile(options) { 6 | const context = await esbuild.context(options) 7 | 8 | if (isDev) { 9 | await context.watch() 10 | } else { 11 | await context.rebuild() 12 | await context.dispose() 13 | } 14 | } 15 | 16 | const defaultOptions = { 17 | define: { 18 | 'process.env.NODE_ENV': isDev ? `'development'` : `'production'`, 19 | }, 20 | bundle: true, 21 | mainFields: ['module', 'main'], 22 | platform: 'neutral', 23 | sourcemap: isDev ? 'inline' : false, 24 | sourcesContent: isDev, 25 | treeShaking: true, 26 | target: ['es2020'], 27 | minify: !isDev, 28 | plugins: [{ 29 | name: 'watchPlugin', 30 | setup: function (build) { 31 | build.onStart(() => { 32 | console.log(`Build started at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 33 | }) 34 | 35 | build.onEnd((result) => { 36 | if (result.errors.length > 0) { 37 | console.log(`Build failed at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`, result.errors) 38 | } else { 39 | console.log(`Build finished at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 40 | } 41 | }) 42 | } 43 | }], 44 | } 45 | 46 | compile({ 47 | ...defaultOptions, 48 | entryPoints: ['./resources/js/index.js'], 49 | outfile: './resources/dist/filament-tour.js', 50 | }) 51 | -------------------------------------------------------------------------------- /src/FilamentTourServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(static::$name) 23 | ->hasConfigFile(self::$name) 24 | ->hasTranslations() 25 | ->hasViews(static::$viewNamespace); 26 | } 27 | 28 | public function packageBooted(): void 29 | { 30 | FilamentAsset::register( 31 | $this->getAssets(), 32 | $this->getAssetPackageName() 33 | ); 34 | 35 | Livewire::component('filament-tour-widget', FilamentTourWidget::class); 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | protected function getAssets(): array 42 | { 43 | return [ 44 | Css::make('filament-tour-styles', __DIR__.'/../resources/dist/filament-tour.css'), 45 | Js::make('filament-tour-scripts', __DIR__.'/../resources/dist/filament-tour.js'), 46 | ]; 47 | } 48 | 49 | protected function getAssetPackageName(): ?string 50 | { 51 | return 'jibaymcs/filament-tour'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tour/Traits/CanConstructRoute.php: -------------------------------------------------------------------------------- 1 | route != null) { 16 | return $this->route; 17 | } 18 | 19 | if (! Filament::auth()->user()) { 20 | return '/'; 21 | } 22 | 23 | if (Filament::getCurrentPanel()->getTenantModel()) { 24 | 25 | $tenants = Filament::getCurrentPanel()->getTenantModel()::find(Filament::auth()->user()->getTenants(Filament::getCurrentPanel())); 26 | 27 | $tenant = $tenants->first(); 28 | 29 | $slug = $tenant->slug; 30 | if ($slug) { 31 | $this->route = parse_url($instance->getUrl(['tenant' => $slug]))['path']; 32 | } 33 | } else { 34 | if (method_exists($instance, 'getResource')) { 35 | $resource = new ($instance->getResource()); 36 | foreach ($resource->getPages() as $key => $page) { 37 | if ($class === $page->getPage()) { 38 | $this->route = parse_url($resource->getUrl($key))['path']; 39 | } 40 | } 41 | } else { 42 | $this->route = parse_url($instance->getUrl())['path'] ?? '/'; 43 | } 44 | 45 | } 46 | 47 | return $this->route; 48 | } 49 | 50 | public function setRoute(string $route) 51 | { 52 | $this->route = $route; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Highlight/HasHighlight.php: -------------------------------------------------------------------------------- 1 | highlights())->mapWithKeys(function (Highlight $highlight, $item) use ($class, $prefixId) { 16 | 17 | $data[$item] = [ 18 | 'route' => $this->getRoute($class), 19 | 20 | 'id' => "{$prefixId}{$highlight->getId()}", 21 | 22 | 'position' => $highlight->getPosition(), 23 | 24 | 'parent' => $highlight->getParent(), 25 | 26 | 'button' => view('filament-tour::highlight.button') 27 | ->with('id', "highlight_{$highlight->getId()}") 28 | ->with('icon', $highlight->getIcon()) 29 | ->with('iconColor', $highlight->getIcon()) 30 | ->render(), 31 | 32 | 'colors' => [ 33 | 'light' => $highlight->getColors()['light'], 34 | 'dark' => $highlight->getColors()['dark'], 35 | ], 36 | 37 | 'popover' => [ 38 | 39 | 'title' => view('filament-tour::tour.step.popover.title') 40 | ->with('title', $highlight->getTitle()) 41 | ->render(), 42 | 43 | 'description' => $highlight->getDescription(), 44 | ], 45 | ]; 46 | 47 | if ($highlight->getElement()) { 48 | $data[$item]['element'] = $highlight->getElement(); 49 | } 50 | 51 | return $data; 52 | 53 | })->toArray(); 54 | } 55 | 56 | /** 57 | * Define your highlights here. 58 | */ 59 | abstract public function highlights(): array; 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jibaymcs/filament-tour", 3 | "description": "Bring the power of DriverJs to your Filament panels and start a tour !", 4 | "keywords": [ 5 | "JibayMcs", 6 | "laravel", 7 | "filament-tour", 8 | "filament", 9 | "filamentphp", 10 | "driverjs" 11 | ], 12 | "homepage": "https://github.com/jibaymcs/filament-tour", 13 | "support": { 14 | "issues": "https://github.com/jibaymcs/filament-tour/issues", 15 | "source": "https://github.com/jibaymcs/filament-tour" 16 | }, 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "JibayMcs", 21 | "email": "jb@ss2i-services.fr", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": ">8.2", 27 | "filament/filament": "^4.0", 28 | "spatie/laravel-package-tools": "^1.15.0" 29 | }, 30 | "require-dev": { 31 | "nunomaduro/collision": "^8.0", 32 | "nunomaduro/larastan": "^3.0", 33 | "orchestra/testbench": "^10.0", 34 | "pestphp/pest": "^4.0", 35 | "pestphp/pest-plugin-arch": "^4.0", 36 | "pestphp/pest-plugin-laravel": "^4.0", 37 | "phpstan/extension-installer": "^1.4", 38 | "phpstan/phpstan-deprecation-rules": "^2.0", 39 | "phpstan/phpstan-phpunit": "^2.0" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "JibayMcs\\FilamentTour\\": "src/" 44 | } 45 | }, 46 | "scripts": { 47 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 48 | "analyse": "vendor/bin/phpstan analyse", 49 | "test": "vendor/bin/pest", 50 | "test-coverage": "vendor/bin/pest --coverage" 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "pestphp/pest-plugin": true, 56 | "phpstan/extension-installer": true 57 | } 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "JibayMcs\\FilamentTour\\FilamentTourServiceProvider" 63 | ] 64 | } 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true 68 | } 69 | -------------------------------------------------------------------------------- /resources/css/index.css: -------------------------------------------------------------------------------- 1 | @import '../../vendor/filament/filament/resources/css/theme.css'; 2 | @import "driver.js/dist/driver.css"; 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | @tailwind screens; 8 | 9 | .hidden { 10 | display: none !important; 11 | } 12 | 13 | #circle-cursor { 14 | pointer-events: none; 15 | cursor: crosshair; 16 | display: none; 17 | width: 20px; 18 | height: 20px; 19 | background-color: rgba(255, 255, 255, 0.5); 20 | border-radius: 50%; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | z-index: 10000; 25 | transition: width .3s, height .3s, left .1s, top .1s; 26 | -webkit-box-shadow: 0px 0px 5px 0px rgb(var(--gray-950)); 27 | -moz-box-shadow: 0px 0px 5px 0px rgb(var(--gray-950)); 28 | box-shadow: 0px 0px 5px 0px rgb(var(--gray-950)); 29 | } 30 | 31 | .driver-popover { 32 | box-sizing: border-box; 33 | margin: 0; 34 | padding: 15px; 35 | border-radius: 5px; 36 | min-width: 300px !important; 37 | max-width: 750px !important; 38 | box-shadow: 0 1px 10px #0006; 39 | z-index: 1000000000; 40 | position: fixed; 41 | top: 0; 42 | right: 0; 43 | } 44 | 45 | .driver-help-button { 46 | width: 15px; 47 | height: 15px; 48 | position: absolute; 49 | cursor: pointer; 50 | } 51 | 52 | .top-left { 53 | top: -14px; 54 | left: -16px; 55 | } 56 | 57 | .top-right { 58 | top: -14px; 59 | right: -16px; 60 | } 61 | 62 | .bottom-right { 63 | bottom: -14px; 64 | right: -16px; 65 | } 66 | 67 | .bottom-left { 68 | bottom: -14px; 69 | left: -16px; 70 | } 71 | 72 | @layer utilities { 73 | 74 | .dark .driver-popover-arrow { 75 | border: 5px solid rgb(var(--gray-900)) !important; 76 | border-right-color: #0000 !important; 77 | border-bottom-color: #0000 !important; 78 | border-top-color: #0000 !important; 79 | } 80 | 81 | .dark .driver-popover { 82 | box-sizing: border-box; 83 | margin: 0; 84 | padding: 15px; 85 | border-radius: 5px; 86 | box-shadow: 0 1px 10px #0006; 87 | z-index: 1000000000; 88 | position: fixed; 89 | top: 0; 90 | right: 0; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/FilamentTourPlugin.php: -------------------------------------------------------------------------------- 1 | getId()); 30 | 31 | return $plugin; 32 | } 33 | 34 | public function getId(): string 35 | { 36 | return 'filament-tour'; 37 | } 38 | 39 | public function register(Panel $panel): void 40 | { 41 | $panel->renderHook('panels::body.start', fn () => Blade::render('')); 42 | } 43 | 44 | public function boot(Panel $panel): void {} 45 | 46 | public function onlyVisibleOnce(bool $onlyVisibleOnce = true): self 47 | { 48 | $this->onlyVisibleOnce = $onlyVisibleOnce; 49 | 50 | return $this; 51 | } 52 | 53 | public function isOnlyVisibleOnce(): ?bool 54 | { 55 | return $this->onlyVisibleOnce; 56 | } 57 | 58 | // Generate documentation 59 | public function enableCssSelector(bool|Closure $enableCssSelector = true): self 60 | { 61 | if (is_callable($enableCssSelector)) { 62 | $this->enableCssSelector = $enableCssSelector(); 63 | } elseif (is_bool($enableCssSelector)) { 64 | $this->enableCssSelector = $enableCssSelector; 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | public function isCssSelectorEnabled(): ?bool 71 | { 72 | return $this->enableCssSelector; 73 | } 74 | 75 | public function historyType(string $type): self 76 | { 77 | $this->historyType = $type; 78 | 79 | return $this; 80 | } 81 | 82 | public function getHistoryType(): string 83 | { 84 | return $this->historyType; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Tour/Step/StepEvent.php: -------------------------------------------------------------------------------- 1 | clickOnNext = $selector; 28 | } else { 29 | $this->clickOnNext = $this->evaluate($selector); 30 | } 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Set the notification to be shown when the user clicks on the next button of your step. 37 | * 38 | * @return $this 39 | */ 40 | public function notifyOnNext(Notification|Closure $notification): self 41 | { 42 | if (is_callable($notification)) { 43 | $this->notifyOnNext = $this->evaluate($notification); 44 | } else { 45 | $this->notifyOnNext = $notification->toArray(); 46 | } 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Set the redirection to be done when the user clicks on the next button of your step. 53 | *
54 | * You can choose to open the redirection in a new tab or not with **$newTab**, default false. 55 | * 56 | * @return $this 57 | */ 58 | public function redirectOnNext(string $url, bool $newTab = false): self 59 | { 60 | $this->redirectOnNext = ['url' => $url, 'newTab' => $newTab]; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Set the livewire event to dispatch to, when the user clicks on the next button of your step. 67 | * 68 | * @param Step ...$args 69 | * @return $this 70 | */ 71 | public function dispatchOnNext(string $name, ...$params): self 72 | { 73 | $this->dispatchOnNext = ['name' => $name, 'params' => $params]; 74 | 75 | return $this; 76 | } 77 | 78 | public function getClickOnNext(): ?string 79 | { 80 | return $this->clickOnNext; 81 | } 82 | 83 | public function getNotifyOnNext(): ?array 84 | { 85 | return $this->notifyOnNext; 86 | } 87 | 88 | public function getDispatchOnNext(): ?array 89 | { 90 | return $this->dispatchOnNext; 91 | } 92 | 93 | public function getRedirectOnNext(): ?array 94 | { 95 | return $this->redirectOnNext; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Livewire/FilamentTourWidget.php: -------------------------------------------------------------------------------- 1 | getPages())->map(fn ($item) => $item->getPage()) 31 | ->flatten() 32 | ->each(function ($item) use (&$filamentClasses) { 33 | $filamentClasses[] = $item; 34 | }); 35 | } else { 36 | $filamentClasses[] = $class; 37 | } 38 | 39 | } 40 | 41 | foreach ($filamentClasses as $class) { 42 | $traits = class_uses($class); 43 | 44 | if (in_array(HasTour::class, $traits)) { 45 | $classesUsingHasTour[] = $class; 46 | } 47 | 48 | if (in_array(HasHighlight::class, $traits)) { 49 | $classesUsingHasHighlight[] = $class; 50 | } 51 | } 52 | 53 | foreach ($classesUsingHasTour as $class) { 54 | $this->tours = array_merge($this->tours, (new $class)->constructTours($class)); 55 | } 56 | 57 | foreach ($classesUsingHasHighlight as $class) { 58 | $this->highlights = array_merge($this->highlights, (new $class)->constructHighlights($class)); 59 | } 60 | 61 | $this->dispatch('filament-tour::loaded-elements', 62 | only_visible_once: FilamentTourPlugin::get()->getHistoryType() == 'local_storage' && (is_bool(FilamentTourPlugin::get()->isOnlyVisibleOnce()) ? FilamentTourPlugin::get()->isOnlyVisibleOnce() : config('filament-tour.only_visible_once')), 63 | tours: $this->tours, 64 | highlights: $this->highlights, 65 | ); 66 | 67 | if (config('app.env') != 'production') { 68 | $hasCssSelector = is_bool(FilamentTourPlugin::get()->isCssSelectorEnabled()) ? FilamentTourPlugin::get()->isCssSelectorEnabled() : config('filament-tour.enable_css_selector'); 69 | $this->dispatch('filament-tour::change-css-selector-status', enabled: $hasCssSelector); 70 | } 71 | } 72 | 73 | public function render() 74 | { 75 | return view('filament-tour::livewire.filament-tour-widget'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Tour/Traits/CanReadJson.php: -------------------------------------------------------------------------------- 1 | $tour['id'], 20 | 'colors' => [ 21 | 'dark' => '#fff', 22 | 'light' => 'rgb(0,0,0)', 23 | ], 24 | ]); 25 | 26 | $app->route($tour['route'] ?? null); 27 | 28 | if (isset($tour['colors'])) { 29 | $app->colors($tour['colors'][0] ?? 'rgb(0,0,0)', $tour['colors'][1] ?? '#fff'); 30 | } 31 | 32 | $app->alwaysShow($tour['alwaysShow'] ?? false); 33 | 34 | $app->visible($tour['visible'] ?? true); 35 | 36 | $app->uncloseable($tour['uncloseable'] ?? false); 37 | 38 | $app->disableEvents($tour['disableEvents'] ?? false); 39 | 40 | $app->ignoreRoutes($tour['ignoreRoutes'] ?? false); 41 | 42 | $app->nextButtonLabel($tour['nextButtonLabel'] ?? Lang::get('filament-tour::filament-tour.button.next')); 43 | 44 | $app->previousButtonLabel($tour['previousButtonLabel'] ?? Lang::get('filament-tour::filament-tour.button.previous')); 45 | 46 | $app->doneButtonLabel($tour['doneButtonLabel'] ?? Lang::get('filament-tour::filament-tour.button.done')); 47 | 48 | foreach ($tour['steps'] as $step) { 49 | $steps[] = Step::fromArray($step); 50 | } 51 | 52 | if (isset($tour['steps'])) { 53 | $app->steps(...$steps); 54 | } 55 | 56 | return $app; 57 | } 58 | 59 | private static function readJson(string $json): array 60 | { 61 | if (filter_var($json, FILTER_VALIDATE_URL)) { 62 | $jsonContent = file_get_contents($json); 63 | if ($jsonContent !== false) { 64 | $data = json_decode($jsonContent, true); 65 | if (json_last_error() === JSON_ERROR_NONE) { 66 | return $data; 67 | } else { 68 | Notification::make('error_parsing_json_from_url') 69 | ->title('Error parsing Tour from JSON as URL') 70 | ->danger() 71 | ->send(); 72 | } 73 | } else { 74 | Notification::make('error_parsing_url') 75 | ->title('Unable to parse URL for JSON Tour') 76 | ->body($json) 77 | ->danger() 78 | ->send(); 79 | } 80 | } else { 81 | $jsonData = json_decode($json, true); 82 | if (json_last_error() === JSON_ERROR_NONE) { 83 | return $jsonData; 84 | } else { 85 | Notification::make('error_parsing_json') 86 | ->title('Error parsing Tour from JSON') 87 | ->body('Verify if your JSON file is valid or exists') 88 | ->danger() 89 | ->send(); 90 | } 91 | } 92 | 93 | return []; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Tour/HasTour.php: -------------------------------------------------------------------------------- 1 | tours() as $tour) { 19 | 20 | if ($tour instanceof Tour) { 21 | 22 | if ($tour->getRoute() && Filament::auth()->user()) { 23 | $this->setRoute($tour->getRoute()); 24 | } 25 | 26 | $steps = json_encode(collect($tour->getSteps())->mapWithKeys(function (Step $step, $item) use ($tour) { 27 | 28 | $data[$item] = [ 29 | 'uncloseable' => $step->isUncloseable(), 30 | 31 | 'popover' => [ 32 | 'title' => view('filament-tour::tour.step.popover.title') 33 | ->with('title', $step->getTitle()) 34 | ->with('icon', $step->getIcon()) 35 | ->with('iconColor', $step->getIconColor()) 36 | ->render(), 37 | 'description' => $step->getDescription(), 38 | ], 39 | 40 | 'progress' => [ 41 | 'current' => $item, 42 | 'total' => count($tour->getSteps()), 43 | ], 44 | ]; 45 | 46 | if (! $tour->hasDisabledEvents()) { 47 | $data[$item]['events'] = [ 48 | 'redirectOnNext' => $step->getRedirectOnNext(), 49 | 'clickOnNext' => $step->getClickOnNext(), 50 | 'notifyOnNext' => $step->getNotifyOnNext(), 51 | 'dispatchOnNext' => $step->getDispatchOnNext(), 52 | ]; 53 | } 54 | 55 | if ($step->getElement()) { 56 | $data[$item]['element'] = $step->getElement(); 57 | } 58 | 59 | return $data; 60 | })->toArray()); 61 | 62 | if ($steps) { 63 | 64 | $route = $this->getRoute($class); 65 | 66 | $tours[] = [ 67 | 'routesIgnored' => $tour->isRoutesIgnored(), 68 | 69 | 'uncloseable' => $tour->isUncloseable(), 70 | 71 | 'route' => $route, 72 | 73 | 'id' => "{$prefixId}{$tour->getId()}", 74 | 75 | 'alwaysShow' => $tour->isAlwaysShow(), 76 | 77 | 'colors' => [ 78 | 'light' => $tour->getColors()['light'], 79 | 'dark' => $tour->getColors()['dark'], 80 | ], 81 | 82 | 'steps' => $steps, 83 | 84 | 'nextButtonLabel' => $tour->getNextButtonLabel(), 85 | 'previousButtonLabel' => $tour->getPreviousButtonLabel(), 86 | 'doneButtonLabel' => $tour->getDoneButtonLabel(), 87 | ]; 88 | } 89 | } 90 | } 91 | 92 | return $tours; 93 | } 94 | 95 | /** 96 | * Define your tours here. 97 | */ 98 | abstract public function tours(): array; 99 | } 100 | -------------------------------------------------------------------------------- /src/Highlight/Highlight.php: -------------------------------------------------------------------------------- 1 | id = $id; 33 | $this->colors = $colors; 34 | $this->parent = $parent; 35 | } 36 | 37 | /** 38 | * Create the instance of your highlight. 39 | *
40 | * Define a **$parent** to be able to view this highlight button next to it 41 | */ 42 | public static function make(string $parent): static 43 | { 44 | return app(static::class, 45 | [ 46 | 'id' => Str::slug($parent), 47 | 'colors' => [ 48 | 'dark' => '#fff', 49 | 'light' => 'rgb(0,0,0)', 50 | ], 51 | 'parent' => $parent, 52 | ]); 53 | } 54 | 55 | public function getId(): string 56 | { 57 | return $this->id; 58 | } 59 | 60 | /** 61 | * Set the element to highlight when you click on this highlight button. 62 | * 63 | * @return $this 64 | */ 65 | public function element(string $element): self 66 | { 67 | $this->element = $element; 68 | 69 | return $this; 70 | } 71 | 72 | public function getElement(): ?string 73 | { 74 | return $this->element; 75 | } 76 | 77 | /** 78 | * Set the title of your highlight. 79 | * 80 | * @return $this 81 | */ 82 | public function title(string|Closure $title): self 83 | { 84 | $this->title = is_string($title) ? $title : $title(); 85 | 86 | return $this; 87 | } 88 | 89 | public function getTitle(): string 90 | { 91 | return $this->title; 92 | } 93 | 94 | /** 95 | * Set the description of your highlight. 96 | * 97 | * @return $this 98 | */ 99 | public function description(string|Closure|HtmlString|View $description): self 100 | { 101 | $this->description = is_callable($description) ? $description() : ($description instanceof View ? $description->render() : $description); 102 | 103 | return $this; 104 | } 105 | 106 | public function getDescription(): HtmlString|string|View|null 107 | { 108 | return $this->description; 109 | } 110 | 111 | /** 112 | * Set the icon highlight button. 113 | *
114 | * - **heroicon-m-question-mark-circle** by default 115 | * 116 | * @return $this 117 | */ 118 | public function icon(string $icon): self 119 | { 120 | $this->icon = $icon; 121 | 122 | return $this; 123 | } 124 | 125 | public function getIcon(): string 126 | { 127 | return $this->icon; 128 | } 129 | 130 | /** 131 | * Set the icon color of your highlight button. 132 | *
133 | * - **gray** by default 134 | * 135 | * @return $this 136 | */ 137 | public function iconColor(string $color): self 138 | { 139 | $this->iconColor = $color; 140 | 141 | return $this; 142 | } 143 | 144 | public function getIconColor(): string 145 | { 146 | return $this->iconColor; 147 | } 148 | 149 | /** 150 | * Set the colors of your background highlighted elements, based on your current filament theme. 151 | *
152 | * - **rgb(0,0,0)** by default for **$light** 153 | *
154 | * - **rgb(var(--gray-600))** by default for **$dark** 155 | * 156 | * @return $this 157 | */ 158 | public function colors(string $light, string $dark): self 159 | { 160 | $this->colors = [ 161 | 'light' => $light, 162 | 'dark' => $dark, 163 | ]; 164 | 165 | return $this; 166 | } 167 | 168 | public function getColors(): array 169 | { 170 | return $this->colors; 171 | } 172 | 173 | /** 174 | * Set the position of your highlight button. 175 | *
176 | * - **top-left** by default 177 | *
178 | * - **top-right** 179 | *
180 | * - **bottom-left** 181 | *
182 | * - **bottom-right** 183 | * 184 | * @return $this 185 | */ 186 | public function position(string $position): self 187 | { 188 | match ($position) { 189 | 'top-right' => $this->position = 'top-right', 190 | 'bottom-left' => $this->position = 'bottom-left', 191 | 'bottom-right' => $this->position = 'bottom-right', 192 | default => $this->position = 'top-left', 193 | }; 194 | 195 | return $this; 196 | } 197 | 198 | public function getPosition(): string 199 | { 200 | return $this->position; 201 | } 202 | 203 | public function getParent(): string 204 | { 205 | return $this->parent; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Tour/Step.php: -------------------------------------------------------------------------------- 1 | element = $element; 34 | } 35 | 36 | public static function fromArray(array $array): static 37 | { 38 | $step = $array; 39 | 40 | $app = app(static::class, ['element' => $step['element'] ?? null]); 41 | 42 | $app->title($step['title']); 43 | $app->description($step['description']); 44 | $app->icon($step['icon'] ?? null); 45 | $app->iconColor($step['iconColor'] ?? null); 46 | $app->uncloseable($step['uncloseable'] ?? false); 47 | 48 | if ($step['events']['dispatchOnNext']) { 49 | $app->dispatchOnNext($step['events']['dispatchOnNext'][0], ...$step['events']['dispatchOnNext'][1]); 50 | } 51 | 52 | if ($step['events']['notifyOnNext']) { 53 | $app->notifyOnNext( 54 | Notification::make(uniqid()) 55 | ->title($step['events']['notifyOnNext']['title']) 56 | ->body($step['events']['notifyOnNext']['body'] ?? null) 57 | ->color($step['events']['notifyOnNext']['color'] ?? null) 58 | ->icon($step['events']['notifyOnNext']['icon'] ?? null) 59 | ->iconColor($step['events']['notifyOnNext']['iconColor'] ?? null) 60 | ->iconSize($step['events']['notifyOnNext']['iconSize'] ?? null) 61 | ->actions($step['events']['notifyOnNext']['actions'] ?? []) 62 | ->duration($step['events']['notifyOnNext']['duration'] ?? 6000) 63 | ); 64 | } 65 | 66 | if ($step['events']['clickOnNext']) { 67 | $app->clickOnNext($step['events']['clickOnNext']); 68 | } 69 | 70 | if ($step['events']['redirectOnNext']) { 71 | if (is_array($step['events']['redirectOnNext'])) { 72 | $app->redirectOnNext($step['events']['redirectOnNext']['url'], isset($step['events']['redirectOnNext']['newTab']) ? $step['events']['redirectOnNext']['newTab'] : true); 73 | } else { 74 | $app->redirectOnNext($step['events']['redirectOnNext']); 75 | } 76 | } 77 | 78 | return $app; 79 | } 80 | 81 | /** 82 | * Set the title of your step. 83 | * 84 | * @return $this 85 | */ 86 | public function title(string|Closure $title): self 87 | { 88 | $this->title = is_string($title) ? $title : $this->evaluate($title); 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Set the description of your step. 95 | * 96 | * @return $this 97 | */ 98 | public function description(string|Closure|HtmlString|View $description): self 99 | { 100 | try { 101 | if (is_callable($description)) { 102 | $this->description = $this->evaluate($description); 103 | } elseif (method_exists($description, 'toHtml')) { 104 | $this->description = $description->toHtml(); 105 | } elseif (method_exists($description, 'render')) { 106 | $this->description = $description->render(); 107 | } else { 108 | $this->description = $description; 109 | } 110 | } catch (Throwable $e) { 111 | throw new Exception("Unable to evaluate description.\n{$e->getMessage()}"); 112 | } 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Set the icon of your step, next to the title. 119 | * 120 | * @return $this 121 | */ 122 | public function icon(?string $icon): self 123 | { 124 | $this->icon = $icon; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Set the color of your icon. 131 | * 132 | * @return $this 133 | */ 134 | public function iconColor(?string $color): self 135 | { 136 | $this->iconColor = $color; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Set the step as uncloseable. 143 | * 144 | * @return $this 145 | */ 146 | public function uncloseable(bool|Closure $uncloseable = true): self 147 | { 148 | if (is_bool($uncloseable)) { 149 | $this->uncloseable = $uncloseable; 150 | } else { 151 | $this->uncloseable = $uncloseable(); 152 | } 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Create the instance of your step. 159 | *
160 | * If no **$element** defined, the step will be shown as a modal. 161 | */ 162 | public static function make(?string $element = null): static 163 | { 164 | return app(static::class, ['element' => $element]); 165 | } 166 | 167 | public function getElement(): ?string 168 | { 169 | return $this->element; 170 | } 171 | 172 | public function getTitle(): string 173 | { 174 | return $this->title; 175 | } 176 | 177 | public function getDescription(): ?string 178 | { 179 | return $this->description; 180 | } 181 | 182 | public function getIcon(): ?string 183 | { 184 | return $this->icon; 185 | } 186 | 187 | public function getIconColor(): ?string 188 | { 189 | return $this->iconColor; 190 | } 191 | 192 | public function isUncloseable(): bool 193 | { 194 | return $this->uncloseable; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filament-tour` will be documented in this file. 4 | 5 | ## Filament V4 support - 2025-10-10 6 | 7 | ### What's Changed 8 | 9 | * Adds Filament v4 support by @pixelsDev-Pim in https://github.com/JibayMcs/filament-tour/pull/41 10 | 11 | ### New Contributors 12 | 13 | * @pixelsDev-Pim made their first contribution in https://github.com/JibayMcs/filament-tour/pull/41 14 | 15 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.2...v4.0.1 16 | 17 | ## v3.1.2 - 2025-06-19 18 | 19 | ### What's Changed 20 | 21 | * Updates readme, adding route example and provides rendering tour on content example by @Mrkbingham in https://github.com/JibayMcs/filament-tour/pull/27 22 | * remove illuminate/contracts to support any laravel version by @atmonshi in https://github.com/JibayMcs/filament-tour/pull/32 23 | * Update composer.json to allow installation on PHP 8.3 and 8.4 by @acornforth in https://github.com/JibayMcs/filament-tour/pull/36 24 | * fix:added pt_PT by @FelicianoIvan in https://github.com/JibayMcs/filament-tour/pull/30 25 | 26 | ### New Contributors 27 | 28 | * @Mrkbingham made their first contribution in https://github.com/JibayMcs/filament-tour/pull/27 29 | * @atmonshi made their first contribution in https://github.com/JibayMcs/filament-tour/pull/32 30 | * @acornforth made their first contribution in https://github.com/JibayMcs/filament-tour/pull/36 31 | * @FelicianoIvan made their first contribution in https://github.com/JibayMcs/filament-tour/pull/30 32 | 33 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.1...v3.1.2 34 | 35 | ## v3.1.1 - 2024-11-05 36 | 37 | ### What's Changed 38 | 39 | * fixes bug #23 - with wrong routes for unauthenticated multinenancy users by @OccTherapist in https://github.com/JibayMcs/filament-tour/pull/24 40 | 41 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.9...v3.1.1 42 | 43 | ## v3.1.0.9 - 2024-09-11 44 | 45 | ### What's Changed 46 | 47 | * Fixing Typo in my last commit by @OccTherapist in https://github.com/JibayMcs/filament-tour/pull/22 48 | 49 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.8...v3.1.0.9 50 | 51 | ## v3.1.0.8 - 2024-09-10 52 | 53 | ### What's Changed 54 | 55 | * add german translations by @OccTherapist in https://github.com/JibayMcs/filament-tour/pull/21 56 | 57 | ### New Contributors 58 | 59 | * @OccTherapist made their first contribution in https://github.com/JibayMcs/filament-tour/pull/21 60 | 61 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.7...v3.1.0.8 62 | 63 | ## v3.1.0.7 - 2024-08-06 64 | 65 | ### What's Changed 66 | 67 | * suitable for what json format works | Update Step.php by @MrPowerUp82 in https://github.com/JibayMcs/filament-tour/pull/19 68 | 69 | ### New Contributors 70 | 71 | * @MrPowerUp82 made their first contribution in https://github.com/JibayMcs/filament-tour/pull/19 72 | 73 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.6...v3.1.0.7 74 | 75 | ## v3.1.0.6 - 2024-08-06 76 | 77 | ### Fixes 78 | 79 | - Fixed THE major issue regarding to the route system, basically caused by unauthentified user 80 | - Fixed opening highlights on click the highlight button 81 | - Fixed event to open a tour, ex: used in a custom Action: 82 | ```php 83 | protected function getHeaderActions(): array 84 | { 85 | return [ 86 | Action::make('Tour')->dispatch('filament-tour::open-tour', ['tour_dashboard']), 87 | ]; 88 | } 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ``` 97 | 98 | ``` 99 | 100 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.5...3.x 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ``` 109 | ## v3.1.0.5 - 2024-05-10 110 | 111 | ### What's Changed 112 | 113 | * chore(deps): upd illuminate/contracts to laravel 11 by @pepperfm in https://github.com/JibayMcs/filament-tour/pull/17 114 | 115 | ### New Contributors 116 | 117 | * @pepperfm made their first contribution in https://github.com/JibayMcs/filament-tour/pull/17 118 | 119 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.4...v3.1.0.5 120 | 121 | ## v3.1.0.4 - 2024-04-17 122 | 123 | ### What's Changed 124 | 125 | * Fixed translation by @theHocineSaad in https://github.com/JibayMcs/filament-tour/pull/10 126 | * Fix the filament-tour::open-tour and filament-tour::open-highlight problems by @wallacemaxters in https://github.com/JibayMcs/filament-tour/pull/14 127 | 128 | ### New Contributors 129 | 130 | * @theHocineSaad made their first contribution in https://github.com/JibayMcs/filament-tour/pull/10 131 | * @wallacemaxters made their first contribution in https://github.com/JibayMcs/filament-tour/pull/14 132 | 133 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.3...v3.1.0.4 134 | 135 | ## v3.1.0.3 - 2023-09-20 136 | 137 | ### What's Changed 138 | 139 | - add ar translations by @aymanalareqi in https://github.com/JibayMcs/filament-tour/pull/4 140 | 141 | ### New Contributors 142 | 143 | - @aymanalareqi made their first contribution in #4 144 | 145 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.2...v3.1.0.3 146 | 147 | ## What's new ? - 2023-09-14 148 | 149 | - Fixed error on parsing URL parameters from the tour routing system 150 | 151 | **Full Changelog**: https://github.com/JibayMcs/filament-tour/compare/v3.1.0.1...v3.1.0.2 152 | 153 | ## The JSON Update - 2023-09-06 154 | 155 | ### The JSON Update ᴠ3.1.0.1 156 | 157 | If creating your guided tours in PHP bores you or takes up too much space, play with JSON! 158 | 159 | You can now load your tours directly using a JSON file from a URL or your Storage! 160 | 161 | - Finished setup for multiple tours registration, now "goto" a next tour on finished the first one 162 | 163 | ## v3.1.0.0 - 2023-09-05 164 | 165 | ### The First Release ! 166 | 167 | #### Development Tool :eyes: 168 | 169 | [Check it here !](https://github.com/JibayMcs/filament-tour/blob/3.x/README.md#development-tool) 170 | 171 | #### Tour 172 | 173 | - Added to make all child steps uncloseable 174 | 175 | - `function uncloseable(bool|Closure $uncloseable = true)` 176 | 177 | - Added to disable all steps events 178 | 179 | - `function disableEvents(bool|Closure $disableEvents = true)` 180 | 181 | - Added to ignore routes check to launch Tour 182 | 183 | - `function ignoreRoutes(bool|Closure $ignoreRoutes = true)` 184 | 185 | 186 | ## 1.0.0 - 202X-XX-XX 187 | 188 | - initial release 189 | -------------------------------------------------------------------------------- /resources/js/css-selector.js: -------------------------------------------------------------------------------- 1 | let lastMouseX = 0; 2 | let lastMouseY = 0; 3 | let active = false; 4 | let hasNavigator = window.navigator.clipboard; 5 | let isInElement = false; 6 | let selected = null; 7 | 8 | let cursor = document.querySelector('#circle-cursor'); 9 | 10 | export function initCssSelector() { 11 | Livewire.on('filament-tour::change-css-selector-status', function ({enabled}) { 12 | 13 | if (enabled) { 14 | 15 | document.onmousemove = handleMouseMove; 16 | document.onkeyup = release; 17 | 18 | document.onmouseover = enterCursor; 19 | document.onmouseleave = leaveCursor; 20 | 21 | function release(event) { 22 | if (event.key !== 'Escape') return; 23 | active = false; 24 | selected = null; 25 | cursor.style.display = 'none'; 26 | } 27 | 28 | document.addEventListener('keydown', function (event) { 29 | 30 | if (event.ctrlKey && event.code === 'Space' && !active) { 31 | if (!hasNavigator) { 32 | new FilamentNotification() 33 | .title('Filament Tour - CSS Selector') 34 | .body("Your browser does not support the Clipboard API !
Don't forget to be in https:// protocol") 35 | .danger() 36 | .send(); 37 | } else { 38 | active = true; 39 | moveCursor(lastMouseX, lastMouseY); 40 | cursor.style.display = 'block'; 41 | 42 | new FilamentNotification() 43 | .title('Filament Tour - CSS Selector') 44 | .body('Activated !
Press Ctrl + C to copy the CSS Selector of the selected element !') 45 | .success() 46 | .send(); 47 | } 48 | } 49 | 50 | if (event.ctrlKey && event.code === 'KeyC' && active) { 51 | navigator.clipboard.writeText(getOptimizedSelector(selected) ?? 'Nothing selected !'); 52 | 53 | active = false; 54 | selected = null; 55 | cursor.style.display = 'none'; 56 | 57 | new FilamentNotification() 58 | .title('Filament Tour - CSS Selector') 59 | .body(`CSS Selector copied to clipboard !`) 60 | .success() 61 | .send(); 62 | } 63 | 64 | }); 65 | 66 | 67 | } 68 | }); 69 | } 70 | 71 | function escapeCssSelector(str) { 72 | return str.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1'); 73 | } 74 | 75 | function getOptimizedSelector(el) { 76 | let fullSelector = getCssSelector(el); 77 | 78 | return optimizeSelector(fullSelector); 79 | } 80 | 81 | function optimizeSelector(selector) { 82 | let parts = selector.split(' > '); 83 | 84 | for (let i = parts.length - 2; i >= 0; i--) { 85 | let testSelector = parts.slice(i).join(' > '); 86 | if (document.querySelectorAll(testSelector).length === 1) { 87 | return testSelector; 88 | } 89 | } 90 | 91 | return selector; 92 | } 93 | 94 | export function getCssSelector(el) { 95 | if (!el) { 96 | return ''; 97 | } 98 | 99 | if (el.id) { 100 | return '#' + escapeCssSelector(el.id); 101 | } 102 | 103 | if (el === document.body) { 104 | return 'body'; 105 | } 106 | 107 | let tag = el.tagName.toLowerCase(); 108 | 109 | let validClasses = el.className.split(/\s+/).filter(cls => cls && !cls.startsWith('--')); 110 | let classes = validClasses.length ? '.' + validClasses.map(escapeCssSelector).join('.') : ''; 111 | 112 | let selectorWithoutNthOfType = tag + classes; 113 | 114 | try { 115 | let siblingsWithSameSelector = Array.from(el.parentNode.querySelectorAll(selectorWithoutNthOfType)); 116 | if (siblingsWithSameSelector.length === 1 && siblingsWithSameSelector[0] === el) { 117 | return getCssSelector(el.parentNode) + ' > ' + selectorWithoutNthOfType; 118 | } 119 | 120 | let siblings = Array.from(el.parentNode.children); 121 | let sameTagAndClassSiblings = siblings.filter(sib => sib.tagName === el.tagName && sib.className === el.className); 122 | if (sameTagAndClassSiblings.length > 1) { 123 | let index = sameTagAndClassSiblings.indexOf(el) + 1; 124 | return getCssSelector(el.parentNode) + ' > ' + tag + classes + ':nth-of-type(' + index + ')'; 125 | } else { 126 | return getCssSelector(el.parentNode) + ' > ' + tag + classes; 127 | } 128 | } catch (e) { 129 | 130 | } 131 | 132 | } 133 | 134 | function handleMouseMove(event) { 135 | lastMouseX = event.clientX; 136 | lastMouseY = event.clientY; 137 | 138 | moveCursor(event.clientX, event.clientY); 139 | } 140 | 141 | function moveCursor(pX, pY) { 142 | if (!active) return; 143 | 144 | let diff = 10; 145 | if (!isInElement) { 146 | cursor.style.left = (pX - diff) + 'px'; 147 | cursor.style.top = (pY - diff) + 'px'; 148 | cursor.style.width = '20px'; 149 | cursor.style.height = '20px'; 150 | cursor.style.borderRadius = "50%"; 151 | } 152 | } 153 | 154 | 155 | function enterCursor(event) { 156 | event.stopPropagation(); 157 | 158 | if (!active) return; 159 | 160 | isInElement = true; 161 | 162 | let elem = event.target; 163 | 164 | while (elem.lastElementChild) { 165 | elem = elem.lastElementChild; 166 | } 167 | 168 | if (elem) { 169 | let eX = elem.offsetParent ? elem.offsetLeft + elem.offsetParent.offsetLeft : elem.offsetLeft 170 | let eY = elem.offsetParent ? elem.offsetTop + elem.offsetParent.offsetTop : elem.offsetTop; 171 | let eW = elem.offsetWidth; 172 | let eH = elem.offsetHeight; 173 | let diff = 6; 174 | selected = elem; 175 | cursor.style.left = eX - diff + 'px'; 176 | cursor.style.top = eY - diff + 'px'; 177 | cursor.style.width = (eW + diff * 2 - 1) + 'px'; 178 | cursor.style.height = (eH + diff * 2 - 1) + 'px'; 179 | cursor.style.borderRadius = "5px"; 180 | } 181 | } 182 | 183 | function leaveCursor(event) { 184 | if (!active) return; 185 | 186 | isInElement = false; 187 | } 188 | -------------------------------------------------------------------------------- /src/Tour/Tour.php: -------------------------------------------------------------------------------- 1 | id = $id; 42 | $this->colors = $colors; 43 | 44 | $this->nextButtonLabel = Lang::get('filament-tour::filament-tour.button.next'); 45 | $this->previousButtonLabel = Lang::get('filament-tour::filament-tour.button.previous'); 46 | $this->doneButtonLabel = Lang::get('filament-tour::filament-tour.button.done'); 47 | } 48 | 49 | /** 50 | * Create the instance of your tour. 51 | *
52 | * Define an **$id** to be able to call it later in a livewire event. 53 | */ 54 | public static function make(...$params): static 55 | { 56 | $params = collect($params); 57 | 58 | switch ($params->keys()->map(fn ($key) => $key)->toArray()[0]) { 59 | case 'url': 60 | case 'json': 61 | return self::fromJson($params->first()); 62 | default: 63 | return app(static::class, 64 | [ 65 | 'id' => $params->first(), 66 | 'colors' => [ 67 | 'dark' => '#fff', 68 | 'light' => 'rgb(0,0,0)', 69 | ], 70 | ]); 71 | break; 72 | } 73 | } 74 | 75 | /** 76 | * Set the route where the tour will be shown. 77 | * 78 | * @return $this 79 | */ 80 | public function route(string $route): self 81 | { 82 | $this->route = $route; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Set the colors of your background highlighted elements, based on your current filament theme. 89 | *
90 | * - **rgb(0,0,0)** by default for **$light** 91 | *
92 | * - **rgb(var(--gray-600))** by default for **$dark** 93 | * 94 | * @return $this 95 | */ 96 | public function colors(string $light, string $dark): self 97 | { 98 | $this->colors = [ 99 | 'light' => $light, 100 | 'dark' => $dark, 101 | ]; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Set the tour as always visible, even is already viewed by the user. 108 | * 109 | * @return $this 110 | */ 111 | public function alwaysShow(bool|Closure $alwaysShow = true): self 112 | { 113 | if (is_bool($alwaysShow)) { 114 | $this->alwaysShow = $alwaysShow; 115 | } else { 116 | $this->alwaysShow = $this->evaluate($alwaysShow); 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Set the tour as visible or not. 124 | * 125 | * @return $this 126 | */ 127 | public function visible(bool|Closure $visible = true): self 128 | { 129 | if (is_bool($visible)) { 130 | $this->visible = $visible; 131 | } else { 132 | $this->visible = $this->evaluate($visible); 133 | } 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Set the tour steps uncloseable. 140 | * 141 | * @return $this 142 | */ 143 | public function uncloseable(bool|Closure $uncloseable = true): self 144 | { 145 | if (is_bool($uncloseable)) { 146 | $this->uncloseable = $uncloseable; 147 | } else { 148 | $this->uncloseable = $this->evaluate($uncloseable); 149 | } 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Disable all events on the tour. 156 | * default: false 157 | * 158 | * @return $this 159 | */ 160 | public function disableEvents(bool|Closure $disableEvents = true): self 161 | { 162 | if (is_bool($disableEvents)) { 163 | $this->disableEvents = $disableEvents; 164 | } else { 165 | $this->disableEvents = $this->evaluate($disableEvents); 166 | } 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Bypass the route check to show your tour on any routes. 173 | * 174 | * @return $this 175 | */ 176 | public function ignoreRoutes(bool|Closure $ignoreRoutes = true): self 177 | { 178 | 179 | if (is_bool($ignoreRoutes)) { 180 | $this->ignoreRoutes = $ignoreRoutes; 181 | } else { 182 | $this->ignoreRoutes = $this->evaluate($ignoreRoutes); 183 | } 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * Set the label of the next button. 190 | * 191 | * @return $this 192 | */ 193 | public function nextButtonLabel(string $label): self 194 | { 195 | $this->nextButtonLabel = $label; 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Set the label of the previous button. 202 | * 203 | * @return $this 204 | */ 205 | public function previousButtonLabel(string $label): self 206 | { 207 | $this->previousButtonLabel = $label; 208 | 209 | return $this; 210 | } 211 | 212 | /** 213 | * Set the label of the done button. 214 | * 215 | * @return $this 216 | */ 217 | public function doneButtonLabel(string $label): self 218 | { 219 | $this->doneButtonLabel = $label; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * Set the steps of your tour. 226 | * 227 | * @return $this 228 | */ 229 | public function steps(Step ...$steps): self 230 | { 231 | $this->steps = $steps; 232 | 233 | return $this; 234 | } 235 | 236 | public function getId(): string 237 | { 238 | return $this->id; 239 | } 240 | 241 | public function getRoute(): ?string 242 | { 243 | return $this->route; 244 | } 245 | 246 | public function getSteps(): array 247 | { 248 | return $this->steps; 249 | } 250 | 251 | public function getColors(): array 252 | { 253 | return $this->colors; 254 | } 255 | 256 | public function isAlwaysShow(): bool 257 | { 258 | return $this->alwaysShow; 259 | } 260 | 261 | public function isVisible(): bool 262 | { 263 | return $this->visible; 264 | } 265 | 266 | public function getNextButtonLabel(): string 267 | { 268 | return $this->nextButtonLabel; 269 | } 270 | 271 | public function getPreviousButtonLabel(): string 272 | { 273 | return $this->previousButtonLabel; 274 | } 275 | 276 | public function getDoneButtonLabel(): string 277 | { 278 | return $this->doneButtonLabel; 279 | } 280 | 281 | public function isUncloseable(): bool 282 | { 283 | return $this->uncloseable; 284 | } 285 | 286 | public function hasDisabledEvents(): bool 287 | { 288 | return $this->disableEvents; 289 | } 290 | 291 | public function isRoutesIgnored(): bool 292 | { 293 | return $this->ignoreRoutes; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /resources/js/index.js: -------------------------------------------------------------------------------- 1 | import {driver} from "driver.js"; 2 | import {initCssSelector} from './css-selector.js'; 3 | 4 | document.addEventListener('livewire:initialized', async function () { 5 | 6 | initCssSelector(); 7 | 8 | let pluginData; 9 | 10 | let tours = []; 11 | let highlights = []; 12 | 13 | function waitForElement(selector, callback) { 14 | if (document.querySelector(selector)) { 15 | callback(document.querySelector(selector)); 16 | return; 17 | } 18 | 19 | const observer = new MutationObserver(function (mutations) { 20 | if (document.querySelector(selector)) { 21 | callback(document.querySelector(selector)); 22 | observer.disconnect(); 23 | } 24 | }); 25 | 26 | observer.observe(document.body, { 27 | childList: true, 28 | subtree: true 29 | }); 30 | } 31 | 32 | function parseId(params) { 33 | 34 | if (Array.isArray(params)) { 35 | return params[0]; 36 | } else if (typeof params === 'object') { 37 | return params.id; 38 | } 39 | 40 | return params; 41 | } 42 | 43 | Livewire.dispatch('filament-tour::load-elements', {request: window.location}) 44 | 45 | Livewire.on('filament-tour::loaded-elements', function (data) { 46 | 47 | pluginData = data; 48 | 49 | pluginData.tours.forEach((tour) => { 50 | tours.push(tour); 51 | 52 | if (!localStorage.getItem('tours')) { 53 | localStorage.setItem('tours', "[]"); 54 | } 55 | }); 56 | 57 | selectTour(tours); 58 | 59 | pluginData.highlights.forEach((highlight) => { 60 | 61 | if (highlight.route === window.location.pathname) { 62 | 63 | //TODO Add a more precise/efficient selector 64 | 65 | waitForElement(highlight.parent, function (selector) { 66 | selector.parentNode.style.position = 'relative'; 67 | 68 | let tempDiv = document.createElement('div'); 69 | tempDiv.innerHTML = highlight.button; 70 | 71 | tempDiv.firstChild.classList.add(highlight.position); 72 | 73 | selector.parentNode.insertBefore(tempDiv.firstChild, selector) 74 | }); 75 | 76 | highlights.push(highlight); 77 | } 78 | }); 79 | }); 80 | 81 | function selectTour(tours, startIndex = 0) { 82 | for (let i = startIndex; i < tours.length; i++) { 83 | let tour = tours[i]; 84 | let conditionAlwaysShow = tour.alwaysShow; 85 | let conditionRoutesIgnored = tour.routesIgnored; 86 | let conditionRouteMatches = tour.route === window.location.pathname; 87 | let conditionVisibleOnce = !pluginData.only_visible_once || 88 | (pluginData.only_visible_once && !localStorage.getItem('tours').includes(tour.id)); 89 | 90 | if ( 91 | (conditionAlwaysShow && conditionRoutesIgnored) || 92 | (conditionAlwaysShow && !conditionRoutesIgnored && conditionRouteMatches) || 93 | (conditionRoutesIgnored && conditionVisibleOnce) || 94 | (conditionRouteMatches && conditionVisibleOnce) 95 | ) { 96 | openTour(tour); 97 | break; 98 | } 99 | } 100 | } 101 | 102 | 103 | Livewire.on('filament-tour::open-highlight', function (params) { 104 | 105 | const id = parseId(params); 106 | 107 | console.log(highlights) 108 | 109 | let highlight = highlights.find(element => element.id === id); 110 | 111 | if (highlight) { 112 | driver({ 113 | overlayColor: localStorage.theme === 'light' ? highlight.colors.light : highlight.colors.dark, 114 | 115 | onPopoverRender: (popover, {config, state}) => { 116 | popover.title.innerHTML = ""; 117 | popover.title.innerHTML = state.activeStep.popover.title; 118 | 119 | if (!state.activeStep.popover.description) { 120 | popover.title.firstChild.style.justifyContent = 'center'; 121 | } 122 | 123 | let contentClasses = "dark:text-white fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 mb-4"; 124 | 125 | popover.footer.parentElement.classList.add(...contentClasses.split(" ")); 126 | }, 127 | }).highlight(highlight); 128 | 129 | } else { 130 | console.error(`Highlight with id '${id}' not found`); 131 | } 132 | }); 133 | 134 | Livewire.on('filament-tour::open-tour', function (params) { 135 | 136 | const id = parseId(params); 137 | 138 | let tour = tours.find(element => element.id === `tour_${id}`); 139 | 140 | if (tour) { 141 | openTour(tour); 142 | } else { 143 | console.error(`Tour with id '${id}' not found`); 144 | } 145 | }); 146 | 147 | function openTour(tour) { 148 | 149 | let steps = JSON.parse(tour.steps); 150 | 151 | if (steps.length > 0) { 152 | 153 | const driverObj = driver({ 154 | allowClose: true, 155 | disableActiveInteraction: true, 156 | overlayColor: localStorage.theme === 'light' ? tour.colors.light : tour.colors.dark, 157 | onDeselected: ((element, step, {config, state}) => { 158 | 159 | }), 160 | onCloseClick: ((element, step, {config, state}) => { 161 | if (state.activeStep && (!state.activeStep.uncloseable || tour.uncloseable)) 162 | driverObj.destroy(); 163 | 164 | if (!localStorage.getItem('tours').includes(tour.id)) { 165 | localStorage.setItem('tours', JSON.stringify([...JSON.parse(localStorage.getItem('tours')), tour.id])); 166 | } 167 | }), 168 | onDestroyStarted: ((element, step, {config, state}) => { 169 | if (state.activeStep && !state.activeStep.uncloseable && !tour.uncloseable) { 170 | driverObj.destroy(); 171 | } 172 | }), 173 | onDestroyed: ((element, step, {config, state}) => { 174 | 175 | }), 176 | onNextClick: ((element, step, {config, state}) => { 177 | 178 | 179 | if (tours.length > 1 && driverObj.isLastStep()) { 180 | let index = tours.findIndex(objet => objet.id === tour.id); 181 | 182 | if (index !== -1 && index < tours.length - 1) { 183 | let nextTourIndex = index + 1; 184 | selectTour(tours, nextTourIndex); 185 | } 186 | } 187 | 188 | 189 | if (driverObj.isLastStep()) { 190 | 191 | if (!localStorage.getItem('tours').includes(tour.id)) { 192 | localStorage.setItem('tours', JSON.stringify([...JSON.parse(localStorage.getItem('tours')), tour.id])); 193 | } 194 | 195 | driverObj.destroy(); 196 | } 197 | 198 | 199 | if (step.events) { 200 | 201 | if (step.events.notifyOnNext) { 202 | new FilamentNotification() 203 | .title(step.events.notifyOnNext.title) 204 | .body(step.events.notifyOnNext.body) 205 | .icon(step.events.notifyOnNext.icon) 206 | .iconColor(step.events.notifyOnNext.iconColor) 207 | .color(step.events.notifyOnNext.color) 208 | .duration(step.events.notifyOnNext.duration) 209 | .send(); 210 | } 211 | 212 | if (step.events.dispatchOnNext) { 213 | Livewire.dispatch(step.events.dispatchOnNext.name, step.events.dispatchOnNext.params); 214 | } 215 | 216 | if (step.events.clickOnNext) { 217 | document.querySelector(step.events.clickOnNext).click(); 218 | } 219 | 220 | if (step.events.redirectOnNext) { 221 | window.open(step.events.redirectOnNext.url, step.events.redirectOnNext.newTab ? '_blank' : '_self'); 222 | } 223 | } 224 | 225 | 226 | driverObj.moveNext(); 227 | }), 228 | onPopoverRender: (popover, {config, state}) => { 229 | 230 | if (state.activeStep.uncloseable || tour.uncloseable) 231 | document.querySelector(".driver-popover-close-btn").remove(); 232 | 233 | popover.title.innerHTML = ""; 234 | popover.title.innerHTML = state.activeStep.popover.title; 235 | 236 | if (!state.activeStep.popover.description) { 237 | popover.title.firstChild.style.justifyContent = 'center'; 238 | } 239 | 240 | let contentClasses = "dark:text-white fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 mb-4"; 241 | 242 | // popover.description.insertAdjacentHTML("beforeend", state.activeStep.popover.form); 243 | 244 | popover.footer.parentElement.classList.add(...contentClasses.split(" ")); 245 | 246 | popover.footer.innerHTML = ""; 247 | popover.footer.classList.add('flex', 'mt-3'); 248 | popover.footer.style.justifyContent = 'space-evenly'; 249 | 250 | popover.footer.classList.remove("driver-popover-footer"); 251 | 252 | 253 | const nextButton = document.createElement("button"); 254 | let nextClasses = "fi-btn fi-btn-size-md relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus:ring-2 disabled:pointer-events-none disabled:opacity-70 rounded-lg fi-btn-color-primary gap-1.5 px-3 py-2 text-sm inline-grid shadow-sm bg-custom-600 text-white hover:bg-custom-500 dark:bg-custom-500 dark:hover:bg-custom-400 focus:ring-custom-500/50 dark:focus:ring-custom-400/50 fi-ac-btn-action"; 255 | 256 | nextButton.classList.add(...nextClasses.split(" "), 'driver-popover-next-btn'); 257 | nextButton.innerText = driverObj.isLastStep() ? tour.doneButtonLabel : tour.nextButtonLabel; 258 | 259 | nextButton.style.setProperty('--c-400', 'var(--primary-400'); 260 | nextButton.style.setProperty('--c-500', 'var(--primary-500'); 261 | nextButton.style.setProperty('--c-600', 'var(--primary-600'); 262 | 263 | const prevButton = document.createElement("button"); 264 | let prevClasses = "fi-btn fi-btn-size-md relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus:ring-2 disabled:pointer-events-none disabled:opacity-70 rounded-lg fi-btn-color-gray gap-1.5 px-3 py-2 text-sm inline-grid shadow-sm bg-white text-gray-950 hover:bg-gray-50 dark:bg-white/5 dark:text-white dark:hover:bg-white/10 ring-1 ring-gray-950/10 dark:ring-white/20 fi-ac-btn-action"; 265 | prevButton.classList.add(...prevClasses.split(" "), 'driver-popover-prev-btn'); 266 | prevButton.innerText = tour.previousButtonLabel; 267 | 268 | if (!driverObj.isFirstStep()) { 269 | popover.footer.appendChild(prevButton); 270 | } 271 | popover.footer.appendChild(nextButton); 272 | }, 273 | steps: steps, 274 | }); 275 | 276 | driverObj.drive(); 277 | } 278 | } 279 | }); 280 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bring the power of DriverJs to your Filament panels and start a tour ! 2 | 3 | ## Due to a heavy workload, I'm unable to continue fixing and improving Filament-Tour. 4 | ## But if you feel like patching it, modifying it or rewriting it, don't hesitate to contribute to the project! 5 | 6 | With the power of [DriverJS](https://driverjs.com) bring to your users an elegant way to discover your panels ! 7 | 8 | ## Installation 9 | 10 | You can install this filament plugin via composer: 11 | 12 | For Filament V4.x 13 | 14 | ```bash 15 | composer require jibaymcs/filament-tour:"^4.0" 16 | ``` 17 | 18 | 19 | For Filament V3.x 20 | 21 | ```bash 22 | composer require jibaymcs/filament-tour:"^3.0" 23 | ``` 24 | 25 | For Filament V2.x 26 | 27 | ```bash 28 | composer require jibaymcs/filament-tour:"^2.0" 29 | ``` 30 | 31 | You can publish the config file with: 32 | 33 | ```bash 34 | php artisan vendor:publish --tag="filament-tour-config" 35 | ``` 36 | 37 | Optionally, you can publish the views using 38 | 39 | ```bash 40 | php artisan vendor:publish --tag="filament-tour-views" 41 | ``` 42 | 43 | This is the contents of the published config file: 44 | 45 | ```php 46 | return [ 47 | "only_visible_once" => true, 48 | ]; 49 | ``` 50 | 51 | ## Usage 52 | 53 | ```php 54 | use JibayMcs\FilamentTour\FilamentTourPlugin; 55 | 56 | public function panel(Panel $panel) { 57 | return $panel->default() 58 | ->[...] 59 | ->plugins([ FilamentTourPlugin::make() ]); 60 | } 61 | ``` 62 | 63 | You can also enable or disable the check on the local storage if the current user have already seen the tour. 64 | 65 | ```php 66 | // default : true 67 | FilamentTourPlugin::make()->onlyVisibleOnce(false) 68 | ``` 69 | 70 | # Start a tour ! 71 | 72 | Let's follow this example to add a tour to your dashboard page. 73 | 74 | If you don't already have a customized dashboard, please refer to the following tutorial: [FIlamentPHP - Dashboard - Customizing the dashboard page](https://filamentphp.com/docs/3.x/panels/dashboard#customizing-the-dashboard-page) 75 | 76 | ## Use the correct trait to registers your tours ! 77 | 78 | ```php 79 | steps( 106 | 107 | Step::make() 108 | ->title("Welcome to your Dashboard !") 109 | ->description(view('tutorial.dashboard.introduction')), 110 | 111 | Step::make('.fi-avatar') 112 | ->title('Woaw ! Here is your avatar !') 113 | ->description('You look nice !') 114 | ->icon('heroicon-o-user-circle') 115 | ->iconColor('primary') 116 | ), 117 | ]; 118 | } 119 | ``` 120 | 121 | ### Displaying your tour ! 122 | 123 | In order to display your tour, its important to remember to pass in the route for the given path you'd like to have the 124 | tour show up on - if you'd like to render the tour on the main admin panels dashboard, set the route to: 125 | 126 | ```php 127 | ->route('/admin') 128 | ``` 129 | 130 | Alternatively, you may want to show tours based on more complex logic, for example if a user hasn't created a specific 131 | type of content. In order to show a tour you will need to create an event listener in your livewire component. 132 | Continuing with the Dashboard example, let's show a tour only if the user hasn't created a new Post yet. We can write 133 | a query to see if the user has no posts, and if so, fire an `open-tour` event to show the Tour: 134 | 135 | ```php 136 | use App\Models\Post; 137 | use Illuminate\Support\Facades\Config; 138 | use Livewire\Attributes\On; 139 | 140 | /** 141 | * Renders the first tour. 142 | * 143 | * @param boolean $only_visible_once Whether the tour should only be visible once. 144 | * @param array $tours Tours to render. 145 | * @param array $highlights Highlights to render. 146 | * 147 | * @return void 148 | */ 149 | #[On('filament-tour::loaded-elements')] 150 | public function renderPostTour(bool $only_visible_once, array $tours, array $highlights): void 151 | { 152 | // If there are posts, don't show this tour 153 | if (Post::count() > 0) { 154 | return; 155 | } 156 | 157 | // Get the tour prefix value 158 | $prefix = Config::get('filament-tour.tour_prefix_id'); 159 | 160 | // Remove the prefix 161 | $firstTourID = substr($tours[0]['id'], strlen($prefix)); 162 | 163 | // Dispatch the event to open the tour 164 | $this->dispatch('filament-tour::open-tour', $firstTourID); 165 | } 166 | ``` 167 | 168 | You can also bring up tours for users when they click on a button. See more in the (Event)[#events] section. 169 | 170 | ### Create a JSON tour ! 171 | 172 | #### - From a direct URL 173 | ```php 174 | use JibayMcs\FilamentTour\Tour\Tour; 175 | 176 | public function tours(): array { 177 | return [ 178 | Tour::make(url: "https://gist.githubusercontent.com/JibayMcs/cc06efddadcfc0a0ff59e116533ee727/raw/8c5c86a3a4b92e4d0586d7a344d0e41f0c175659/TourDashboard.json") 179 | ]; 180 | } 181 | ``` 182 | 183 | #### - From your Storage 184 | 185 | ```php 186 | use JibayMcs\FilamentTour\Tour\Tour; 187 | use Illuminate\Support\Facades\Storage; 188 | 189 | public function tours(): array { 190 | return [ 191 | Tour::make(json: Storage::disk('local')->get("TourDashboard.json")) 192 | ]; 193 | } 194 | ``` 195 | 196 | > [!IMPORTANT] 197 | > Using `Tour::make(url: "")` or `Tour::make(json: "")` is the same thing, so don't worry about the name of your parameter if you've got the wrong type.
198 | > BUT
199 | > If you use `Tour::make('my-tour')` it's equal to `Tour::make(id: 'my-tour')` 200 | > And here you need to construct all your steps. No JSON reading here. 201 | 202 |
203 | JSON Example file (click to expand) or Github Gist Link 204 | 205 | ```json 206 | { 207 | "id": "dashboard", 208 | "route": "/admin/test-team", 209 | "colors": [ 210 | "", 211 | "" 212 | ], 213 | "alwaysShow": true, 214 | "visible": true, 215 | "uncloseable": true, 216 | "ignoreRoutes": false, 217 | "disableEvents": true, 218 | "nextButtonLabel": "Next", 219 | "previousButtonLabel": "Previous", 220 | "doneButtonLabel": "Done", 221 | "steps": [ 222 | { 223 | "title": "Woaw ! First Step !", 224 | "description": "Yeah ! And I'm from a json file !", 225 | "uncloseable": false, 226 | "events": { 227 | "clickOnNext": "body", 228 | "notifyOnNext": { 229 | "title": "Hello World !", 230 | "body": "Woaw ! I'm from a Json file !", 231 | "color": "success" 232 | }, 233 | "redirectOnNext": { 234 | "url": "https://filamentphp.com", 235 | "newTab": true 236 | }, 237 | "dispatchOnNext": [ 238 | "open-modal", 239 | { 240 | "id": "edit-user" 241 | } 242 | ] 243 | } 244 | }, 245 | { 246 | "title": "An other one !", 247 | "description": "Yeah ! And I'm from the same json file !", 248 | "uncloseable": false, 249 | "events": { 250 | "clickOnNext": "body", 251 | "notifyOnNext": { 252 | "title": "Hello World !", 253 | "body": "Woaw ! I'm from a Json file !", 254 | "color": "success" 255 | }, 256 | "redirectOnNext": { 257 | "url": "https://filamentphp.com", 258 | "newTab": true 259 | }, 260 | "dispatchOnNext": [ 261 | "open-modal", 262 | { 263 | "id": "edit-user" 264 | } 265 | ] 266 | } 267 | } 268 | ] 269 | } 270 | ``` 271 |
272 | 273 | # Tour.php 274 | 275 | ### Tour methods reference 276 | 277 | ```php 278 | use JibayMcs\FilamentTour\Tour\Tour; 279 | 280 | // Instanciate a tour, and provide an id, to trigger it later 281 | Tour::make(string $id) 282 | 283 | // Since 3.1.0.1, JSON Support update 284 | Tour::make(... $params) 285 | 286 | // Define a custom url to trigger your tour 287 | ->route(string $route) 288 | 289 | //Register the steps of your tour 290 | ->steps(Step ...$steps) 291 | 292 | // Define a color of your highlight overlay for the dark and light theme of your filament panel 293 | ->colors(string $light, string $dark) 294 | 295 | //Set the tour as always visible, even is already viewed by the user. 296 | ->alwaysShow(bool|Closure $alwaysShow = true) 297 | 298 | // Set the tour visible or not 299 | ->visible(bool|Closure $visible = true) 300 | 301 | // Set the 'Next' button label 302 | ->nextButtonLabel(string $label) 303 | 304 | // Set the 'Previous' button label 305 | ->previousButtonLabel(string $label) 306 | 307 | // Set the 'Done' button label 308 | ->doneButtonLabel(string $label) 309 | 310 | // Set the whole steps of the tour as uncloseable 311 | ->uncloseable(bool|Closure $uncloseable = true) 312 | 313 | // Disable all tour steps events 314 | ->disableEvents(bool|Closure $disableEvents = true) 315 | 316 | // Bypass route check to show the tour on all pages 317 | // Maybe useless, but who knows ? 318 | ->ignoreRoutes(bool|Closure $ignoreRoutes = true) 319 | ``` 320 | 321 | # Step.php 322 | 323 | ### Step methods reference 324 | 325 | ```php 326 | use JibayMcs\FilamentTour\Tour\Step; 327 | 328 | // If no element provided, the step act like a modal 329 | Step::make(string $element = null) 330 | 331 | // Define the title of your step 332 | // Mandatory 333 | ->title(string|Closure $title) 334 | 335 | // Define the description of your step 336 | // Also accept HTML 337 | // Mandatory 338 | ->description(string|Closure|HtmlString|View $description) 339 | 340 | // Define an icon next to your step title 341 | ->icon(string $icon) 342 | 343 | // Define the color of the title icon 344 | ->iconColor(string $color) 345 | 346 | // Step your step closeable or not 347 | // Default: true 348 | ->uncloseable(bool|Closure $uncloseable = true) 349 | 350 | //Simulate a click on a CSS selected element when you press the next button 351 | ->clickOnNext(string|Closure $selector) 352 | 353 | // Send a notification when you press the next button 354 | ->notifyOnNext(Notification $notification) 355 | 356 | //Redirect you to a custom url or a route() when you press the next button 357 | ->redirectOnNext(string $url, bool $newTab = false) 358 | 359 | // Dispatch an event like `$dispatch()` when you press the next button 360 | ->dispatchOnNext(string $name, ...$args) 361 | ``` 362 | 363 | # Highlights 364 | 365 | Same as tour, use the correct trait ! 366 | 367 | - Use the correct trait to registers your highlights ! 368 | 369 | ```php 370 | element('.fi-header-heading') 398 | ->title('Whoaw ! You highlighted the title of the page !') 399 | ->description('"Dashboard"'), 400 | 401 | Highlight::make('.fi-avatar') 402 | ->element('.fi-avatar') 403 | ->title("Pssst ! That's your avatar") 404 | ->icon('heroicon-o-user-circle') 405 | ->iconColor('primary'), 406 | 407 | ]; 408 | } 409 | ``` 410 | 411 | ___ 412 | 413 | # Highlight.php 414 | 415 | ### Highlight methods reference 416 | 417 | ```php 418 | use JibayMcs\FilamentTour\Highlight\Highlight; 419 | 420 | // Instantiate a highlight with a CSS select of the element where the icon button is next to 421 | Highlight::make(string $parent) 422 | 423 | // Define the element to be highlighted 424 | ->element(string $element) 425 | 426 | // Set the title of your highlight 427 | ->title(string|Closure $title) 428 | 429 | // Set the description of your highlight 430 | ->description(string|Closure|HtmlString|View $description) 431 | 432 | // Define a custom icon for your highlight button 433 | // Default: heroicon-m-question-mark-circle 434 | ->icon(string $icon) 435 | 436 | // Define the color of the highlight icon button 437 | // Default: gray 438 | ->iconColor(string $color) 439 | 440 | // Define a color of your highlight overlay for the dark and light theme of your filament panel 441 | ->colors(string $light, string $dark) 442 | 443 | // Set the position of your icon button around the parent 444 | // Default: top-left 445 | // Available: top-left, top-right, bottom-left, bottom-right 446 | ->position(string $position) 447 | ``` 448 | 449 | ___ 450 | 451 | # Events 452 | 453 | ### Available events: 454 | 455 | - `filament-tour::open-highlight` **string** id 456 | Open a specific highlight by its id. 457 |
458 |
459 | - `filament-tour::open-tour` **string** id 460 | Open a specific tour by its id. 461 | 462 | ___ 463 | 464 | Filament Tour, dispatch some event to show tours and highlights. 465 | So you can trigger them from your own code. 466 | 467 | Basically, if you want a custom button to trigger a tour or a highlight, you can do something like this: 468 | 469 | ```html 470 | // ======== Highlights 471 | // AlpineJS 472 | 473 | 474 | // Livewire 475 | 476 | 477 | // ======== Tours 478 | //AlpineJS 479 | 480 | 481 | // Livewire 482 | 483 | ``` 484 | 485 | > **ℹ️** 486 | > Don't forget to prefix your event with `filament-tour::` to trigger the correct event. 487 | 488 | # Development Tool 489 | 490 | > [!IMPORTANT] 491 | > This tool is always disabled in production mode. `APP_ENV=production` 492 | 493 | Filament Tour embed a simple tool to help you to develop your tours and highlights. 494 | 495 | Let me show you how to use it ! 496 | 497 | ### Enable the tool 498 | 499 | To enable the tool, simply use `FilamentTourPlugin::make()->enableCssSelector()` in your plugin declaration. 500 | 501 | ### Keyboard shortcuts 502 | 503 | **Ctrl**|**Cmd** + **Space** To open the tool. 504 |
505 |
506 | **Escape** To exit the tool. 507 |
508 |
509 | **Ctrl**|**Cmd** + **C** To copy the CSS Selector of the highlighted element. 510 | 511 | [CSS Selector Tool Utilisation Preview](https://github.com/JibayMcs/filament-tour/assets/7621593/162db2a3-1f46-4493-ae0d-cffcb2f00462) 512 | 513 | # Extra Resources 514 | 515 | ### DriverJS 516 | 517 | - [DriverJS Website](https://driverjs.com) 518 | - [DriverJS GitHub](https://github.com/kamranahmedse/driver.js) (Give some 🩵 to the author !) 519 | 520 | The core of this plugin ! 521 | Don't hesitate to check the documentation to learn more about the possibilities of this plugin. 522 | _I don't implemented all the features of DriverJS, at this time, but I'm working on it !_ 523 | 524 | ## Changelog 525 | 526 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 527 | 528 | ## Contributing 529 | 530 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 531 | 532 | ## Security Vulnerabilities 533 | 534 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 535 | 536 | ## Credits 537 | 538 | - [JibayMcs](https://github.com/JibayMcs) 539 | - [DriverJS](https://driverjs.com) 540 | - [All Contributors](../../contributors) 541 | 542 | ## License 543 | 544 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 545 | 546 | -------------------------------------------------------------------------------- /resources/dist/filament-tour.js: -------------------------------------------------------------------------------- 1 | var Y={};function j(e={}){Y={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function p(e){return e?Y[e]:Y}function O(e,t,o,r){return(e/=r/2)<1?o/2*e*e+t:-o/2*(--e*(e-2)-1)+t}function U(e){let t='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(o=>{let r=o.matches(t),n=Array.from(o.querySelectorAll(t));return[...r?[o]:[],...n]}).filter(o=>getComputedStyle(o).pointerEvents!=="none"&&ge(o))}function ee(e){if(!e||he(e))return;let t=p("smoothScroll");e.scrollIntoView({behavior:!t||me(e)?"auto":"smooth",inline:"center",block:"center"})}function me(e){if(!e||!e.parentElement)return;let t=e.parentElement;return t.scrollHeight>t.clientHeight}function he(e){let t=e.getBoundingClientRect();return t.top>=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)}function ge(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}var R={};function x(e,t){R[e]=t}function v(e){return e?R[e]:R}function X(){R={}}var q={};function W(e,t){q[e]=t}function A(e){var t;(t=q[e])==null||t.call(q)}function we(){q={}}function ye(e,t,o,r){let n=v("__activeStagePosition"),c=n||o.getBoundingClientRect(),m=r.getBoundingClientRect(),l=O(e,c.x,m.x-c.x,t),s=O(e,c.y,m.y-c.y,t),d=O(e,c.width,m.width-c.width,t),i=O(e,c.height,m.height-c.height,t);n={x:l,y:s,width:d,height:i},oe(n),x("__activeStagePosition",n)}function te(e){if(!e)return;let t=e.getBoundingClientRect(),o={x:t.x,y:t.y,width:t.width,height:t.height};x("__activeStagePosition",o),oe(o)}function be(){let e=v("__activeStagePosition"),t=v("__overlaySvg");if(!e)return;if(!t){console.warn("No stage svg found.");return}let o=window.innerWidth,r=window.innerHeight;t.setAttribute("viewBox",`0 0 ${o} ${r}`)}function xe(e){let t=Ce(e);document.body.appendChild(t),re(t,o=>{o.target.tagName==="path"&&A("overlayClick")}),x("__overlaySvg",t)}function oe(e){let t=v("__overlaySvg");if(!t){xe(e);return}let o=t.firstElementChild;if(o?.tagName!=="path")throw new Error("no path element found in stage svg");o.setAttribute("d",ne(e))}function Ce(e){let t=window.innerWidth,o=window.innerHeight,r=document.createElementNS("http://www.w3.org/2000/svg","svg");r.classList.add("driver-overlay","driver-overlay-animated"),r.setAttribute("viewBox",`0 0 ${t} ${o}`),r.setAttribute("xmlSpace","preserve"),r.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),r.setAttribute("version","1.1"),r.setAttribute("preserveAspectRatio","xMinYMin slice"),r.style.fillRule="evenodd",r.style.clipRule="evenodd",r.style.strokeLinejoin="round",r.style.strokeMiterlimit="2",r.style.zIndex="10000",r.style.position="fixed",r.style.top="0",r.style.left="0",r.style.width="100%",r.style.height="100%";let n=document.createElementNS("http://www.w3.org/2000/svg","path");return n.setAttribute("d",ne(e)),n.style.fill=p("overlayColor")||"rgb(0,0,0)",n.style.opacity=`${p("overlayOpacity")}`,n.style.pointerEvents="auto",n.style.cursor="auto",r.appendChild(n),r}function ne(e){let t=window.innerWidth,o=window.innerHeight,r=p("stagePadding")||0,n=p("stageRadius")||0,c=e.width+r*2,m=e.height+r*2,l=Math.min(n,c/2,m/2),s=Math.floor(Math.max(l,0)),d=e.x-r+s,i=e.y-r,a=c-s*2,u=m-s*2;return`M${t},0L0,0L0,${o}L${t},${o}L${t},0Z 2 | M${d},${i} h${a} a${s},${s} 0 0 1 ${s},${s} v${u} a${s},${s} 0 0 1 -${s},${s} h-${a} a${s},${s} 0 0 1 -${s},-${s} v-${u} a${s},${s} 0 0 1 ${s},-${s} z`}function Se(){let e=v("__overlaySvg");e&&e.remove()}function Le(){let e=document.getElementById("driver-dummy-element");if(e)return e;let t=document.createElement("div");return t.id="driver-dummy-element",t.style.width="0",t.style.height="0",t.style.pointerEvents="none",t.style.opacity="0",t.style.position="fixed",t.style.top="50%",t.style.left="50%",document.body.appendChild(t),t}function V(e){let{element:t}=e,o=typeof t=="string"?document.querySelector(t):t;o||(o=Le()),_e(o,e)}function ke(){let e=v("__activeElement"),t=v("__activeStep");e&&(te(e),be(),le(e,t))}function _e(e,t){let o=Date.now(),r=v("__activeStep"),n=v("__activeElement")||e,c=!n||n===e,m=e.id==="driver-dummy-element",l=n.id==="driver-dummy-element",s=p("animate"),d=t.onHighlightStarted||p("onHighlightStarted"),i=t?.onHighlighted||p("onHighlighted"),a=r?.onDeselected||p("onDeselected"),u=p(),f=v();!c&&a&&a(l?void 0:n,r,{config:u,state:f}),d&&d(m?void 0:e,t,{config:u,state:f});let g=!c&&s,w=!1;$e(),x("previousStep",r),x("previousElement",n),x("activeStep",t),x("activeElement",e);let h=()=>{if(v("__transitionCallback")!==h)return;let y=Date.now()-o,L=400-y<=400/2;t.popover&&L&&!w&&g&&(Z(e,t),w=!0),p("animate")&&y<400?ye(y,400,n,e):(te(e),i&&i(m?void 0:e,t,{config:p(),state:v()}),x("__transitionCallback",void 0),x("__previousStep",r),x("__previousElement",n),x("__activeStep",t),x("__activeElement",e)),window.requestAnimationFrame(h)};x("__transitionCallback",h),window.requestAnimationFrame(h),ee(e),!g&&t.popover&&Z(e,t),n.classList.remove("driver-active-element","driver-no-interaction"),n.removeAttribute("aria-haspopup"),n.removeAttribute("aria-expanded"),n.removeAttribute("aria-controls"),p("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function Ee(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(t=>{t.classList.remove("driver-active-element","driver-no-interaction"),t.removeAttribute("aria-haspopup"),t.removeAttribute("aria-expanded"),t.removeAttribute("aria-controls")})}function $(){let e=v("__resizeTimeout");e&&window.cancelAnimationFrame(e),x("__resizeTimeout",window.requestAnimationFrame(ke))}function Pe(e){var t;if(!v("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;let o=v("__activeElement"),r=(t=v("popover"))==null?void 0:t.wrapper,n=U([...r?[r]:[],...o?[o]:[]]),c=n[0],m=n[n.length-1];if(e.preventDefault(),e.shiftKey){let l=n[n.indexOf(document.activeElement)-1]||m;l?.focus()}else{let l=n[n.indexOf(document.activeElement)+1]||c;l?.focus()}}function ie(e){(p("allowKeyboardControl")??!0)&&(e.key==="Escape"?A("escapePress"):e.key==="ArrowRight"?A("arrowRightPress"):e.key==="ArrowLeft"&&A("arrowLeftPress"))}function re(e,t,o){let r=(n,c)=>{let m=n.target;e.contains(m)&&((!o||o(m))&&(n.preventDefault(),n.stopPropagation(),n.stopImmediatePropagation()),c?.(n))};document.addEventListener("pointerdown",r,!0),document.addEventListener("mousedown",r,!0),document.addEventListener("pointerup",r,!0),document.addEventListener("mouseup",r,!0),document.addEventListener("click",n=>{r(n,t)},!0)}function Ne(){window.addEventListener("keyup",ie,!1),window.addEventListener("keydown",Pe,!1),window.addEventListener("resize",$),window.addEventListener("scroll",$)}function Ae(){window.removeEventListener("keyup",ie),window.removeEventListener("resize",$),window.removeEventListener("scroll",$)}function $e(){let e=v("popover");e&&(e.wrapper.style.display="none")}function Z(e,t){var o,r;let n=v("popover");n&&document.body.removeChild(n.wrapper),n=Te(),document.body.appendChild(n.wrapper);let{title:c,description:m,showButtons:l,disableButtons:s,showProgress:d,nextBtnText:i=p("nextBtnText")||"Next →",prevBtnText:a=p("prevBtnText")||"← Previous",progressText:u=p("progressText")||"{current} of {total}"}=t.popover||{};n.nextButton.innerHTML=i,n.previousButton.innerHTML=a,n.progress.innerHTML=u,c?(n.title.innerText=c,n.title.style.display="block"):n.title.style.display="none",m?(n.description.innerHTML=m,n.description.style.display="block"):n.description.style.display="none";let f=l||p("showButtons"),g=d||p("showProgress")||!1,w=f?.includes("next")||f?.includes("previous")||g;n.closeButton.style.display=f.includes("close")?"block":"none",w?(n.footer.style.display="flex",n.progress.style.display=g?"block":"none",n.nextButton.style.display=f.includes("next")?"block":"none",n.previousButton.style.display=f.includes("previous")?"block":"none"):n.footer.style.display="none";let h=s||p("disableButtons")||[];h!=null&&h.includes("next")&&(n.nextButton.disabled=!0,n.nextButton.classList.add("driver-popover-btn-disabled")),h!=null&&h.includes("previous")&&(n.previousButton.disabled=!0,n.previousButton.classList.add("driver-popover-btn-disabled")),h!=null&&h.includes("close")&&(n.closeButton.disabled=!0,n.closeButton.classList.add("driver-popover-btn-disabled"));let y=n.wrapper;y.style.display="block",y.style.left="",y.style.top="",y.style.bottom="",y.style.right="",y.id="driver-popover-content",y.setAttribute("role","dialog"),y.setAttribute("aria-labelledby","driver-popover-title"),y.setAttribute("aria-describedby","driver-popover-description");let L=n.arrow;L.className="driver-popover-arrow";let _=((o=t.popover)==null?void 0:o.popoverClass)||p("popoverClass")||"";y.className=`driver-popover ${_}`.trim(),re(n.wrapper,P=>{var B,T,H;let N=P.target,M=((B=t.popover)==null?void 0:B.onNextClick)||p("onNextClick"),I=((T=t.popover)==null?void 0:T.onPrevClick)||p("onPrevClick"),D=((H=t.popover)==null?void 0:H.onCloseClick)||p("onCloseClick");if(N.classList.contains("driver-popover-next-btn"))return M?M(e,t,{config:p(),state:v()}):A("nextClick");if(N.classList.contains("driver-popover-prev-btn"))return I?I(e,t,{config:p(),state:v()}):A("prevClick");if(N.classList.contains("driver-popover-close-btn"))return D?D(e,t,{config:p(),state:v()}):A("closeClick")},P=>!(n!=null&&n.description.contains(P))&&!(n!=null&&n.title.contains(P))&&P.className.includes("driver-popover")),x("popover",n);let b=((r=t.popover)==null?void 0:r.onPopoverRender)||p("onPopoverRender");b&&b(n,{config:p(),state:v()}),le(e,t),ee(y);let C=e.classList.contains("driver-dummy-element"),S=U([y,...C?[]:[e]]);S.length>0&&S[0].focus()}function se(){let e=v("popover");if(!(e!=null&&e.wrapper))return;let t=e.wrapper.getBoundingClientRect(),o=p("stagePadding")||0,r=p("popoverOffset")||0;return{width:t.width+o+r,height:t.height+o+r,realWidth:t.width,realHeight:t.height}}function G(e,t){let{elementDimensions:o,popoverDimensions:r,popoverPadding:n,popoverArrowDimensions:c}=t;return e==="start"?Math.max(Math.min(o.top-n,window.innerHeight-r.realHeight-c.width),c.width):e==="end"?Math.max(Math.min(o.top-r?.realHeight+o.height+n,window.innerHeight-r?.realHeight-c.width),c.width):e==="center"?Math.max(Math.min(o.top+o.height/2-r?.realHeight/2,window.innerHeight-r?.realHeight-c.width),c.width):0}function Q(e,t){let{elementDimensions:o,popoverDimensions:r,popoverPadding:n,popoverArrowDimensions:c}=t;return e==="start"?Math.max(Math.min(o.left-n,window.innerWidth-r.realWidth-c.width),c.width):e==="end"?Math.max(Math.min(o.left-r?.realWidth+o.width+n,window.innerWidth-r?.realWidth-c.width),c.width):e==="center"?Math.max(Math.min(o.left+o.width/2-r?.realWidth/2,window.innerWidth-r?.realWidth-c.width),c.width):0}function le(e,t){let o=v("popover");if(!o)return;let{align:r="start",side:n="left"}=t?.popover||{},c=r,m=e.id==="driver-dummy-element"?"over":n,l=p("stagePadding")||0,s=se(),d=o.arrow.getBoundingClientRect(),i=e.getBoundingClientRect(),a=i.top-s.height,u=a>=0,f=window.innerHeight-(i.bottom+s.height),g=f>=0,w=i.left-s.width,h=w>=0,y=window.innerWidth-(i.right+s.width),L=y>=0,_=!u&&!g&&!h&&!L,b=m;if(m==="top"&&u?L=h=g=!1:m==="bottom"&&g?L=h=u=!1:m==="left"&&h?L=u=g=!1:m==="right"&&L&&(h=u=g=!1),m==="over"){let C=window.innerWidth/2-s.realWidth/2,S=window.innerHeight/2-s.realHeight/2;o.wrapper.style.left=`${C}px`,o.wrapper.style.right="auto",o.wrapper.style.top=`${S}px`,o.wrapper.style.bottom="auto"}else if(_){let C=window.innerWidth/2-s?.realWidth/2,S=10;o.wrapper.style.left=`${C}px`,o.wrapper.style.right="auto",o.wrapper.style.bottom=`${S}px`,o.wrapper.style.top="auto"}else if(h){let C=Math.min(w,window.innerWidth-s?.realWidth-d.width),S=G(c,{elementDimensions:i,popoverDimensions:s,popoverPadding:l,popoverArrowDimensions:d});o.wrapper.style.left=`${C}px`,o.wrapper.style.top=`${S}px`,o.wrapper.style.bottom="auto",o.wrapper.style.right="auto",b="left"}else if(L){let C=Math.min(y,window.innerWidth-s?.realWidth-d.width),S=G(c,{elementDimensions:i,popoverDimensions:s,popoverPadding:l,popoverArrowDimensions:d});o.wrapper.style.right=`${C}px`,o.wrapper.style.top=`${S}px`,o.wrapper.style.bottom="auto",o.wrapper.style.left="auto",b="right"}else if(u){let C=Math.min(a,window.innerHeight-s.realHeight-d.width),S=Q(c,{elementDimensions:i,popoverDimensions:s,popoverPadding:l,popoverArrowDimensions:d});o.wrapper.style.top=`${C}px`,o.wrapper.style.left=`${S}px`,o.wrapper.style.bottom="auto",o.wrapper.style.right="auto",b="top"}else if(g){let C=Math.min(f,window.innerHeight-s?.realHeight-d.width),S=Q(c,{elementDimensions:i,popoverDimensions:s,popoverPadding:l,popoverArrowDimensions:d});o.wrapper.style.left=`${S}px`,o.wrapper.style.bottom=`${C}px`,o.wrapper.style.top="auto",o.wrapper.style.right="auto",b="bottom"}_?o.arrow.classList.add("driver-popover-arrow-none"):Be(c,b,e)}function Be(e,t,o){let r=v("popover");if(!r)return;let n=o.getBoundingClientRect(),c=se(),m=r.arrow,l=c.width,s=window.innerWidth,d=n.width,i=n.left,a=c.height,u=window.innerHeight,f=n.top,g=n.height;m.className="driver-popover-arrow";let w=t,h=e;t==="top"?(i+d<=0?(w="right",h="end"):i+d-l<=0&&(w="top",h="start"),i>=s?(w="left",h="end"):i+l>=s&&(w="top",h="end")):t==="bottom"?(i+d<=0?(w="right",h="start"):i+d-l<=0&&(w="bottom",h="start"),i>=s?(w="left",h="start"):i+l>=s&&(w="bottom",h="end")):t==="left"?(f+g<=0?(w="bottom",h="end"):f+g-a<=0&&(w="left",h="start"),f>=u?(w="top",h="end"):f+a>=u&&(w="left",h="end")):t==="right"&&(f+g<=0?(w="bottom",h="start"):f+g-a<=0&&(w="right",h="start"),f>=u?(w="top",h="start"):f+a>=u&&(w="right",h="end")),w?(m.classList.add(`driver-popover-arrow-side-${w}`),m.classList.add(`driver-popover-arrow-align-${h}`)):m.classList.add("driver-popover-arrow-none")}function Te(){let e=document.createElement("div");e.classList.add("driver-popover");let t=document.createElement("div");t.classList.add("driver-popover-arrow");let o=document.createElement("header");o.id="driver-popover-title",o.classList.add("driver-popover-title"),o.style.display="none",o.innerText="Popover Title";let r=document.createElement("div");r.id="driver-popover-description",r.classList.add("driver-popover-description"),r.style.display="none",r.innerText="Popover description is here";let n=document.createElement("button");n.type="button",n.classList.add("driver-popover-close-btn"),n.setAttribute("aria-label","Close"),n.innerHTML="×";let c=document.createElement("footer");c.classList.add("driver-popover-footer");let m=document.createElement("span");m.classList.add("driver-popover-progress-text"),m.innerText="";let l=document.createElement("span");l.classList.add("driver-popover-navigation-btns");let s=document.createElement("button");s.type="button",s.classList.add("driver-popover-prev-btn"),s.innerHTML="← Previous";let d=document.createElement("button");return d.type="button",d.classList.add("driver-popover-next-btn"),d.innerHTML="Next →",l.appendChild(s),l.appendChild(d),c.appendChild(m),c.appendChild(l),e.appendChild(n),e.appendChild(t),e.appendChild(o),e.appendChild(r),e.appendChild(c),{wrapper:e,arrow:t,title:o,description:r,footer:c,previousButton:s,nextButton:d,closeButton:n,footerButtons:l,progress:m}}function He(){var e;let t=v("popover");t&&((e=t.wrapper.parentElement)==null||e.removeChild(t.wrapper))}function J(e={}){j(e);function t(){p("allowClose")&&d()}function o(){let i=v("activeIndex"),a=p("steps")||[];if(typeof i>"u")return;let u=i+1;a[u]?s(u):d()}function r(){let i=v("activeIndex"),a=p("steps")||[];if(typeof i>"u")return;let u=i-1;a[u]?s(u):d()}function n(i){(p("steps")||[])[i]?s(i):d()}function c(){var i;if(v("__transitionCallback"))return;let a=v("activeIndex"),u=v("__activeStep"),f=v("__activeElement");if(typeof a>"u"||typeof u>"u"||typeof v("activeIndex")>"u")return;let g=((i=u.popover)==null?void 0:i.onPrevClick)||p("onPrevClick");if(g)return g(f,u,{config:p(),state:v()});r()}function m(){var i;if(v("__transitionCallback"))return;let a=v("activeIndex"),u=v("__activeStep"),f=v("__activeElement");if(typeof a>"u"||typeof u>"u")return;let g=((i=u.popover)==null?void 0:i.onNextClick)||p("onNextClick");if(g)return g(f,u,{config:p(),state:v()});o()}function l(){v("isInitialized")||(x("isInitialized",!0),document.body.classList.add("driver-active",p("animate")?"driver-fade":"driver-simple"),Ne(),W("overlayClick",t),W("escapePress",t),W("arrowLeftPress",c),W("arrowRightPress",m))}function s(i=0){var a,u,f,g,w,h,y,L;let _=p("steps");if(!_){console.error("No steps to drive through"),d();return}if(!_[i]){d();return}x("__activeOnDestroyed",document.activeElement),x("activeIndex",i);let b=_[i],C=_[i+1],S=_[i-1],P=((a=b.popover)==null?void 0:a.doneBtnText)||p("doneBtnText")||"Done",B=p("allowClose"),T=typeof((u=b.popover)==null?void 0:u.showProgress)<"u"?(f=b.popover)==null?void 0:f.showProgress:p("showProgress"),H=(((g=b.popover)==null?void 0:g.progressText)||p("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${i+1}`).replace("{{total}}",`${_.length}`),N=((w=b.popover)==null?void 0:w.showButtons)||p("showButtons"),M=["next","previous",...B?["close"]:[]].filter(fe=>!(N!=null&&N.length)||N.includes(fe)),I=((h=b.popover)==null?void 0:h.onNextClick)||p("onNextClick"),D=((y=b.popover)==null?void 0:y.onPrevClick)||p("onPrevClick"),ve=((L=b.popover)==null?void 0:L.onCloseClick)||p("onCloseClick");V({...b,popover:{showButtons:M,nextBtnText:C?void 0:P,disableButtons:[...S?[]:["previous"]],showProgress:T,progressText:H,onNextClick:I||(()=>{C?s(i+1):d()}),onPrevClick:D||(()=>{s(i-1)}),onCloseClick:ve||(()=>{d()}),...b?.popover||{}}})}function d(i=!0){let a=v("__activeElement"),u=v("__activeStep"),f=v("__activeOnDestroyed"),g=p("onDestroyStarted");if(i&&g){let y=!a||a?.id==="driver-dummy-element";g(y?void 0:a,u,{config:p(),state:v()});return}let w=u?.onDeselected||p("onDeselected"),h=p("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),Ae(),He(),Ee(),Se(),we(),X(),a&&u){let y=a.id==="driver-dummy-element";w&&w(y?void 0:a,u,{config:p(),state:v()}),h&&h(y?void 0:a,u,{config:p(),state:v()})}f&&f.focus()}return{isActive:()=>v("isInitialized")||!1,refresh:$,drive:(i=0)=>{l(),s(i)},setConfig:j,setSteps:i=>{X(),j({...p(),steps:i})},getConfig:p,getState:v,getActiveIndex:()=>v("activeIndex"),isFirstStep:()=>v("activeIndex")===0,isLastStep:()=>{let i=p("steps")||[],a=v("activeIndex");return a!==void 0&&a===i.length-1},getActiveStep:()=>v("activeStep"),getActiveElement:()=>v("activeElement"),getPreviousElement:()=>v("previousElement"),getPreviousStep:()=>v("previousStep"),moveNext:o,movePrevious:r,moveTo:n,hasNextStep:()=>{let i=p("steps")||[],a=v("activeIndex");return a!==void 0&&i[a+1]},hasPreviousStep:()=>{let i=p("steps")||[],a=v("activeIndex");return a!==void 0&&i[a-1]},highlight:i=>{l(),V({...i,popover:i.popover?{showButtons:[],showProgress:!1,progressText:"",...i.popover}:void 0})},destroy:()=>{d(!1)}}}var de=0,ce=0,E=!1,Me=window.navigator.clipboard,K=!1,z=null,k=document.querySelector("#circle-cursor");function pe(){Livewire.on("filament-tour::change-css-selector-status",function({enabled:e}){if(e){let t=function(o){o.key==="Escape"&&(E=!1,z=null,k.style.display="none")};document.onmousemove=Oe,document.onkeyup=t,document.onmouseover=We,document.onmouseleave=Re,document.addEventListener("keydown",function(o){o.ctrlKey&&o.code==="Space"&&!E&&(Me?(E=!0,ue(de,ce),k.style.display="block",new FilamentNotification().title("Filament Tour - CSS Selector").body("Activated !
Press Ctrl + C to copy the CSS Selector of the selected element !").success().send()):new FilamentNotification().title("Filament Tour - CSS Selector").body("Your browser does not support the Clipboard API !
Don't forget to be in https:// protocol").danger().send()),o.ctrlKey&&o.code==="KeyC"&&E&&(navigator.clipboard.writeText(Ie(z)??"Nothing selected !"),E=!1,z=null,k.style.display="none",new FilamentNotification().title("Filament Tour - CSS Selector").body("CSS Selector copied to clipboard !").success().send())})}})}function ae(e){return e.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,"\\$1")}function Ie(e){let t=F(e);return De(t)}function De(e){let t=e.split(" > ");for(let o=t.length-2;o>=0;o--){let r=t.slice(o).join(" > ");if(document.querySelectorAll(r).length===1)return r}return e}function F(e){if(!e)return"";if(e.id)return"#"+ae(e.id);if(e===document.body)return"body";let t=e.tagName.toLowerCase(),o=e.className.split(/\s+/).filter(c=>c&&!c.startsWith("--")),r=o.length?"."+o.map(ae).join("."):"",n=t+r;try{let c=Array.from(e.parentNode.querySelectorAll(n));if(c.length===1&&c[0]===e)return F(e.parentNode)+" > "+n;let l=Array.from(e.parentNode.children).filter(s=>s.tagName===e.tagName&&s.className===e.className);if(l.length>1){let s=l.indexOf(e)+1;return F(e.parentNode)+" > "+t+r+":nth-of-type("+s+")"}else return F(e.parentNode)+" > "+t+r}catch{}}function Oe(e){de=e.clientX,ce=e.clientY,ue(e.clientX,e.clientY)}function ue(e,t){if(!E)return;let o=10;K||(k.style.left=e-o+"px",k.style.top=t-o+"px",k.style.width="20px",k.style.height="20px",k.style.borderRadius="50%")}function We(e){if(e.stopPropagation(),!E)return;K=!0;let t=e.target;for(;t.lastElementChild;)t=t.lastElementChild;if(t){let o=t.offsetParent?t.offsetLeft+t.offsetParent.offsetLeft:t.offsetLeft,r=t.offsetParent?t.offsetTop+t.offsetParent.offsetTop:t.offsetTop,n=t.offsetWidth,c=t.offsetHeight,m=6;z=t,k.style.left=o-m+"px",k.style.top=r-m+"px",k.style.width=n+m*2-1+"px",k.style.height=c+m*2-1+"px",k.style.borderRadius="5px"}}function Re(e){E&&(K=!1)}document.addEventListener("livewire:initialized",async function(){pe();let e,t=[],o=[];function r(l,s){if(document.querySelector(l)){s(document.querySelector(l));return}let d=new MutationObserver(function(i){document.querySelector(l)&&(s(document.querySelector(l)),d.disconnect())});d.observe(document.body,{childList:!0,subtree:!0})}function n(l){return Array.isArray(l)?l[0]:typeof l=="object"?l.id:l}Livewire.dispatch("filament-tour::load-elements",{request:window.location}),Livewire.on("filament-tour::loaded-elements",function(l){e=l,e.tours.forEach(s=>{t.push(s),localStorage.getItem("tours")||localStorage.setItem("tours","[]")}),c(t),e.highlights.forEach(s=>{s.route===window.location.pathname&&(r(s.parent,function(d){d.parentNode.style.position="relative";let i=document.createElement("div");i.innerHTML=s.button,i.firstChild.classList.add(s.position),d.parentNode.insertBefore(i.firstChild,d)}),o.push(s))})});function c(l,s=0){for(let d=s;di.id===s);d?J({overlayColor:localStorage.theme==="light"?d.colors.light:d.colors.dark,onPopoverRender:(i,{config:a,state:u})=>{i.title.innerHTML="",i.title.innerHTML=u.activeStep.popover.title,u.activeStep.popover.description||(i.title.firstChild.style.justifyContent="center");let f="dark:text-white fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 mb-4";i.footer.parentElement.classList.add(...f.split(" "))}}).highlight(d):console.error(`Highlight with id '${s}' not found`)}),Livewire.on("filament-tour::open-tour",function(l){let s=n(l),d=t.find(i=>i.id===`tour_${s}`);d?m(d):console.error(`Tour with id '${s}' not found`)});function m(l){let s=JSON.parse(l.steps);if(s.length>0){let d=J({allowClose:!0,disableActiveInteraction:!0,overlayColor:localStorage.theme==="light"?l.colors.light:l.colors.dark,onDeselected:(i,a,{config:u,state:f})=>{},onCloseClick:(i,a,{config:u,state:f})=>{f.activeStep&&(!f.activeStep.uncloseable||l.uncloseable)&&d.destroy(),localStorage.getItem("tours").includes(l.id)||localStorage.setItem("tours",JSON.stringify([...JSON.parse(localStorage.getItem("tours")),l.id]))},onDestroyStarted:(i,a,{config:u,state:f})=>{f.activeStep&&!f.activeStep.uncloseable&&!l.uncloseable&&d.destroy()},onDestroyed:(i,a,{config:u,state:f})=>{},onNextClick:(i,a,{config:u,state:f})=>{if(t.length>1&&d.isLastStep()){let g=t.findIndex(w=>w.id===l.id);if(g!==-1&&g{(u.activeStep.uncloseable||l.uncloseable)&&document.querySelector(".driver-popover-close-btn").remove(),i.title.innerHTML="",i.title.innerHTML=u.activeStep.popover.title,u.activeStep.popover.description||(i.title.firstChild.style.justifyContent="center");let f="dark:text-white fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 mb-4";i.footer.parentElement.classList.add(...f.split(" ")),i.footer.innerHTML="",i.footer.classList.add("flex","mt-3"),i.footer.style.justifyContent="space-evenly",i.footer.classList.remove("driver-popover-footer");let g=document.createElement("button"),w="fi-btn fi-btn-size-md relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus:ring-2 disabled:pointer-events-none disabled:opacity-70 rounded-lg fi-btn-color-primary gap-1.5 px-3 py-2 text-sm inline-grid shadow-sm bg-custom-600 text-white hover:bg-custom-500 dark:bg-custom-500 dark:hover:bg-custom-400 focus:ring-custom-500/50 dark:focus:ring-custom-400/50 fi-ac-btn-action";g.classList.add(...w.split(" "),"driver-popover-next-btn"),g.innerText=d.isLastStep()?l.doneButtonLabel:l.nextButtonLabel,g.style.setProperty("--c-400","var(--primary-400"),g.style.setProperty("--c-500","var(--primary-500"),g.style.setProperty("--c-600","var(--primary-600");let h=document.createElement("button"),y="fi-btn fi-btn-size-md relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus:ring-2 disabled:pointer-events-none disabled:opacity-70 rounded-lg fi-btn-color-gray gap-1.5 px-3 py-2 text-sm inline-grid shadow-sm bg-white text-gray-950 hover:bg-gray-50 dark:bg-white/5 dark:text-white dark:hover:bg-white/10 ring-1 ring-gray-950/10 dark:ring-white/20 fi-ac-btn-action";h.classList.add(...y.split(" "),"driver-popover-prev-btn"),h.innerText=l.previousButtonLabel,d.isFirstStep()||i.footer.appendChild(h),i.footer.appendChild(g)},steps:s});d.drive()}}}); 3 | -------------------------------------------------------------------------------- /resources/dist/filament-tour.css: -------------------------------------------------------------------------------- 1 | @tailwind base;@tailwind components;@tailwind utilities;.driver-active *,.driver-active .driver-overlay{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in .2s ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in .2s}.driver-popover{all:unset;color:#2d2d2d;min-width:250px;max-width:300px;background-color:#fff}.driver-popover *{font-family:Helvetica Neue,Inter,ui-sans-serif,Apple Color Emoji,Helvetica,Arial,sans-serif}.driver-popover-title{font:19px/normal sans-serif;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1;margin:0}.driver-popover-close-btn{all:unset;position:absolute;top:0;right:0;width:32px;height:28px;cursor:pointer;font-size:18px;font-weight:500;color:#d2d2d2;z-index:1;text-align:center;transition:color;transition-duration:.2s}.driver-popover-close-btn:focus,.driver-popover-close-btn:hover{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;font-weight:400;zoom:1}.driver-popover-footer{margin-top:15px;text-align:right;zoom:1;display:flex;align-items:center;justify-content:space-between}.driver-popover-progress-text{font-size:13px;font-weight:400;color:#727272;zoom:1}.driver-popover-footer button{all:unset;display:inline-block;box-sizing:border-box;padding:3px 7px;text-decoration:none;text-shadow:1px 1px 0 #fff;background-color:#fff;color:#2d2d2d;font:12px/normal sans-serif;cursor:pointer;outline:0;zoom:1;line-height:1.3;border:1px solid #ccc;border-radius:3px}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}:not(body):has(>.driver-active-element){overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}.driver-popover-footer button:focus,.driver-popover-footer button:hover{background-color:#f7f7f7}.driver-popover-navigation-btns{display:flex;flex-grow:1;justify-content:flex-end}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";position:absolute;border:5px solid #fff}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{left:100%;border-right-color:#0000;border-bottom-color:#0000;border-top-color:#0000}.driver-popover-arrow-side-right{right:100%;border-left-color:#0000;border-bottom-color:#0000;border-top-color:#0000}.driver-popover-arrow-side-top{top:100%;border-right-color:#0000;border-bottom-color:#0000;border-left-color:#0000}.driver-popover-arrow-side-bottom{bottom:100%;border-left-color:#0000;border-top-color:#0000;border-right-color:#0000}.driver-popover-arrow-side-center{display:none}.driver-popover-arrow-side-left.driver-popover-arrow-align-start,.driver-popover-arrow-side-right.driver-popover-arrow-align-start{top:15px}.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start,.driver-popover-arrow-side-top.driver-popover-arrow-align-start{left:15px}.driver-popover-arrow-align-end.driver-popover-arrow-side-left,.driver-popover-arrow-align-end.driver-popover-arrow-side-right{bottom:15px}.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end,.driver-popover-arrow-side-top.driver-popover-arrow-align-end{right:15px}.driver-popover-arrow-side-left.driver-popover-arrow-align-center,.driver-popover-arrow-side-right.driver-popover-arrow-align-center{top:50%;margin-top:-5px}.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center,.driver-popover-arrow-side-top.driver-popover-arrow-align-center{left:50%;margin-left:-5px}.driver-popover-arrow-none{display:none}/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:rgba(var(--gray-200),1)}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family),ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:rgba(var(--gray-400),1)}input::placeholder,textarea::placeholder{opacity:1;color:rgba(var(--gray-400),1)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}input::placeholder,textarea::placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='rgba(var(--gray-500), var(--tw-stroke-opacity, 1))' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:#0000;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}:root.dark{color-scheme:dark}[data-field-wrapper]{scroll-margin-top:8rem}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.prose :where(p):not(:where([class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(hr):not(:where([class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-left-width:.25rem;border-left-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure>*):not(:where([class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose :where(code):not(:where([class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose] *)){background-color:initial;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose] *)){width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose] *)){vertical-align:initial}.prose :where(tfoot):not(:where([class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose] *)){vertical-align:top}.prose :where(video):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(figure):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(li):not(:where([class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose :where(.prose>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-sm :where(p):not(:where([class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where([class~=lead]):not(:where([class~=not-prose] *)){font-size:1.2857143em;line-height:1.5555556;margin-top:.8888889em;margin-bottom:.8888889em}.prose-sm :where(blockquote):not(:where([class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em;padding-left:1.1111111em}.prose-sm :where(h1):not(:where([class~=not-prose] *)){font-size:2.1428571em;margin-top:0;margin-bottom:.8em;line-height:1.2}.prose-sm :where(h2):not(:where([class~=not-prose] *)){font-size:1.4285714em;margin-top:1.6em;margin-bottom:.8em;line-height:1.4}.prose-sm :where(h3):not(:where([class~=not-prose] *)){font-size:1.2857143em;margin-top:1.5555556em;margin-bottom:.4444444em;line-height:1.5555556}.prose-sm :where(h4):not(:where([class~=not-prose] *)){margin-top:1.4285714em;margin-bottom:.5714286em;line-height:1.4285714}.prose-sm :where(img):not(:where([class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(video):not(:where([class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(figure):not(:where([class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(figure>*):not(:where([class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(figcaption):not(:where([class~=not-prose] *)){font-size:.8571429em;line-height:1.3333333;margin-top:.6666667em}.prose-sm :where(code):not(:where([class~=not-prose] *)){font-size:.8571429em}.prose-sm :where(h2 code):not(:where([class~=not-prose] *)){font-size:.9em}.prose-sm :where(h3 code):not(:where([class~=not-prose] *)){font-size:.8888889em}.prose-sm :where(pre):not(:where([class~=not-prose] *)){font-size:.8571429em;line-height:1.6666667;margin-top:1.6666667em;margin-bottom:1.6666667em;border-radius:.25rem;padding:.6666667em 1em}.prose-sm :where(ol):not(:where([class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-left:1.5714286em}.prose-sm :where(ul):not(:where([class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-left:1.5714286em}.prose-sm :where(li):not(:where([class~=not-prose] *)){margin-top:.2857143em;margin-bottom:.2857143em}.prose-sm :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.4285714em}.prose-sm :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.4285714em}.prose-sm :where(.prose-sm>ul>li p):not(:where([class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(.prose-sm>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose-sm>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(hr):not(:where([class~=not-prose] *)){margin-top:2.8571429em;margin-bottom:2.8571429em}.prose-sm :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(table):not(:where([class~=not-prose] *)){font-size:.8571429em;line-height:1.5}.prose-sm :where(thead th):not(:where([class~=not-prose] *)){padding-right:1em;padding-bottom:.6666667em;padding-left:1em}.prose-sm :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose-sm :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose-sm :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.6666667em 1em}.prose-sm :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose-sm :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose-sm :where(.prose-sm>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose-sm>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-base :where(p):not(:where([class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose-base :where([class~=lead]):not(:where([class~=not-prose] *)){font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose-base :where(blockquote):not(:where([class~=not-prose] *)){margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose-base :where(h1):not(:where([class~=not-prose] *)){font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose-base :where(h2):not(:where([class~=not-prose] *)){font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose-base :where(h3):not(:where([class~=not-prose] *)){font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose-base :where(h4):not(:where([class~=not-prose] *)){margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose-base :where(img):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose-base :where(video):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose-base :where(figure):not(:where([class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose-base :where(figure>*):not(:where([class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-base :where(figcaption):not(:where([class~=not-prose] *)){font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose-base :where(code):not(:where([class~=not-prose] *)){font-size:.875em}.prose-base :where(h2 code):not(:where([class~=not-prose] *)){font-size:.875em}.prose-base :where(h3 code):not(:where([class~=not-prose] *)){font-size:.9em}.prose-base :where(pre):not(:where([class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose-base :where(ol):not(:where([class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose-base :where(ul):not(:where([class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em}.prose-base :where(li):not(:where([class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose-base :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose-base :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.375em}.prose-base :where(.prose-base>ul>li p):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose-base :where(.prose-base>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose-base :where(.prose-base>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose-base :where(.prose-base>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.25em}.prose-base :where(.prose-base>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.25em}.prose-base :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose-base :where(hr):not(:where([class~=not-prose] *)){margin-top:3em;margin-bottom:3em}.prose-base :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(table):not(:where([class~=not-prose] *)){font-size:.875em;line-height:1.7142857}.prose-base :where(thead th):not(:where([class~=not-prose] *)){padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose-base :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose-base :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose-base :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.5714286em}.prose-base :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose-base :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose-base :where(.prose-base>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-base :where(.prose-base>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.prose-lg :where(p):not(:where([class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em}.prose-lg :where([class~=lead]):not(:where([class~=not-prose] *)){font-size:1.2222222em;line-height:1.4545455;margin-top:1.0909091em;margin-bottom:1.0909091em}.prose-lg :where(blockquote):not(:where([class~=not-prose] *)){margin-top:1.6666667em;margin-bottom:1.6666667em;padding-left:1em}.prose-lg :where(h1):not(:where([class~=not-prose] *)){font-size:2.6666667em;margin-top:0;margin-bottom:.8333333em;line-height:1}.prose-lg :where(h2):not(:where([class~=not-prose] *)){font-size:1.6666667em;margin-top:1.8666667em;margin-bottom:1.0666667em;line-height:1.3333333}.prose-lg :where(h3):not(:where([class~=not-prose] *)){font-size:1.3333333em;margin-top:1.6666667em;margin-bottom:.6666667em;line-height:1.5}.prose-lg :where(h4):not(:where([class~=not-prose] *)){margin-top:1.7777778em;margin-bottom:.4444444em;line-height:1.5555556}.prose-lg :where(img):not(:where([class~=not-prose] *)){margin-top:1.7777778em;margin-bottom:1.7777778em}.prose-lg :where(video):not(:where([class~=not-prose] *)){margin-top:1.7777778em;margin-bottom:1.7777778em}.prose-lg :where(figure):not(:where([class~=not-prose] *)){margin-top:1.7777778em;margin-bottom:1.7777778em}.prose-lg :where(figure>*):not(:where([class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-lg :where(figcaption):not(:where([class~=not-prose] *)){font-size:.8888889em;line-height:1.5;margin-top:1em}.prose-lg :where(code):not(:where([class~=not-prose] *)){font-size:.8888889em}.prose-lg :where(h2 code):not(:where([class~=not-prose] *)){font-size:.8666667em}.prose-lg :where(h3 code):not(:where([class~=not-prose] *)){font-size:.875em}.prose-lg :where(pre):not(:where([class~=not-prose] *)){font-size:.8888889em;line-height:1.75;margin-top:2em;margin-bottom:2em;border-radius:.375rem;padding:1em 1.5em}.prose-lg :where(ol):not(:where([class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em;padding-left:1.5555556em}.prose-lg :where(ul):not(:where([class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em;padding-left:1.5555556em}.prose-lg :where(li):not(:where([class~=not-prose] *)){margin-top:.6666667em;margin-bottom:.6666667em}.prose-lg :where(ol>li):not(:where([class~=not-prose] *)){padding-left:.4444444em}.prose-lg :where(ul>li):not(:where([class~=not-prose] *)){padding-left:.4444444em}.prose-lg :where(.prose-lg>ul>li p):not(:where([class~=not-prose] *)){margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg :where(.prose-lg>ul>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-lg :where(.prose-lg>ul>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-lg :where(.prose-lg>ol>li>:first-child):not(:where([class~=not-prose] *)){margin-top:1.3333333em}.prose-lg :where(.prose-lg>ol>li>:last-child):not(:where([class~=not-prose] *)){margin-bottom:1.3333333em}.prose-lg :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose] *)){margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg :where(hr):not(:where([class~=not-prose] *)){margin-top:3.1111111em;margin-bottom:3.1111111em}.prose-lg :where(hr+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(h2+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(h3+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(h4+*):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(table):not(:where([class~=not-prose] *)){font-size:.8888889em;line-height:1.5}.prose-lg :where(thead th):not(:where([class~=not-prose] *)){padding-right:.75em;padding-bottom:.75em;padding-left:.75em}.prose-lg :where(thead th:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose-lg :where(thead th:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose-lg :where(tbody td,tfoot td):not(:where([class~=not-prose] *)){padding:.75em}.prose-lg :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose] *)){padding-left:0}.prose-lg :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose] *)){padding-right:0}.prose-lg :where(.prose-lg>:first-child):not(:where([class~=not-prose] *)){margin-top:0}.prose-lg :where(.prose-lg>:last-child):not(:where([class~=not-prose] *)){margin-bottom:0}.h-\[100dvh\]{height:100dvh}.dark .driver-popover-arrow{border:5px solid #0000!important;border-left:5px solid rgb(var(--gray-900))!important}.dark .driver-popover{box-sizing:border-box;margin:0;padding:15px;border-radius:5px;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0}:is(.dark .dark\:prose-invert){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.disabled\:placeholder\:\[-webkit-text-fill-color\:theme\(colors\.gray\.400\)\]:disabled::-moz-placeholder,.disabled\:placeholder\:\[-webkit-text-fill-color\:theme\(colors\.gray\.400\)\]:disabled::placeholder{-webkit-text-fill-color:rgba(var(--gray-400),1)}.group\/item:focus-visible .group-focus-visible\/item\:underline,.group\/link:focus-visible .group-focus-visible\/link\:underline{text-decoration-line:underline}:is([dir=ltr] .ltr\:hidden),:is([dir=rtl] .rtl\:hidden){display:none}:is([dir=rtl] .rtl\:-translate-x-0){--tw-translate-x:-0px}:is([dir=rtl] .rtl\:-translate-x-0),:is([dir=rtl] .rtl\:-translate-x-5){transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:-translate-x-5){--tw-translate-x:-1.25rem}:is([dir=rtl] .rtl\:-translate-x-full){--tw-translate-x:-100%}:is([dir=rtl] .rtl\:-translate-x-full),:is([dir=rtl] .rtl\:translate-x-1\/2){transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:translate-x-1\/2){--tw-translate-x:50%}:is([dir=rtl] .rtl\:translate-x-1\/4){--tw-translate-x:25%}:is([dir=rtl] .rtl\:translate-x-1\/4),:is([dir=rtl] .rtl\:translate-x-full){transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:translate-x-full){--tw-translate-x:100%}:is([dir=rtl] .rtl\:rotate-180){--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:flex-row-reverse){flex-direction:row-reverse}:is([dir=rtl] .rtl\:divide-x-reverse)>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:1}:is(.dark .dark\:flex){display:flex}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:divide-white\/10)>:not([hidden])~:not([hidden]){border-color:#ffffff1a}:is(.dark .dark\:divide-white\/5)>:not([hidden])~:not([hidden]){border-color:#ffffff0d}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgba(var(--gray-600),var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgba(var(--gray-700),var(--tw-border-opacity))}:is(.dark .dark\:border-primary-500){--tw-border-opacity:1;border-color:rgba(var(--primary-500),var(--tw-border-opacity))}:is(.dark .dark\:border-white\/10){border-color:#ffffff1a}:is(.dark .dark\:border-white\/5){border-color:#ffffff0d}:is(.dark .dark\:border-t-white\/10){border-top-color:#ffffff1a}:is(.dark .dark\:\!bg-gray-700){--tw-bg-opacity:1!important;background-color:rgba(var(--gray-700),var(--tw-bg-opacity))!important}:is(.dark .dark\:bg-custom-400\/10){background-color:rgba(var(--c-400),.1)}:is(.dark .dark\:bg-custom-500){--tw-bg-opacity:1;background-color:rgba(var(--c-500),var(--tw-bg-opacity))}:is(.dark .dark\:bg-custom-500\/20){background-color:rgba(var(--c-500),.2)}:is(.dark .dark\:bg-gray-400\/10){background-color:rgba(var(--gray-400),.1)}:is(.dark .dark\:bg-gray-500){--tw-bg-opacity:1;background-color:rgba(var(--gray-500),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-500\/20){background-color:rgba(var(--gray-500),.2)}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgba(var(--gray-600),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgba(var(--gray-700),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgba(var(--gray-800),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-900\/30){background-color:rgba(var(--gray-900),.3)}:is(.dark .dark\:bg-gray-950){--tw-bg-opacity:1;background-color:rgba(var(--gray-950),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-950\/75){background-color:rgba(var(--gray-950),.75)}:is(.dark .dark\:bg-primary-400){--tw-bg-opacity:1;background-color:rgba(var(--primary-400),var(--tw-bg-opacity))}:is(.dark .dark\:bg-primary-500){--tw-bg-opacity:1;background-color:rgba(var(--primary-500),var(--tw-bg-opacity))}:is(.dark .dark\:bg-transparent){background-color:initial}:is(.dark .dark\:bg-white\/10){background-color:#ffffff1a}:is(.dark .dark\:bg-white\/5){background-color:#ffffff0d}:is(.dark .dark\:fill-current){fill:currentColor}:is(.dark .dark\:text-custom-300\/50){color:rgba(var(--c-300),.5)}:is(.dark .dark\:text-custom-400){--tw-text-opacity:1;color:rgba(var(--c-400),var(--tw-text-opacity))}:is(.dark .dark\:text-custom-400\/10){color:rgba(var(--c-400),.1)}:is(.dark .dark\:text-danger-400){--tw-text-opacity:1;color:rgba(var(--danger-400),var(--tw-text-opacity))}:is(.dark .dark\:text-danger-500){--tw-text-opacity:1;color:rgba(var(--danger-500),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300\/50){color:rgba(var(--gray-300),.5)}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-700){--tw-text-opacity:1;color:rgba(var(--gray-700),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-800){--tw-text-opacity:1;color:rgba(var(--gray-800),var(--tw-text-opacity))}:is(.dark .dark\:text-primary-400){--tw-text-opacity:1;color:rgba(var(--primary-400),var(--tw-text-opacity))}:is(.dark .dark\:text-primary-500){--tw-text-opacity:1;color:rgba(var(--primary-500),var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-white\/5){color:#ffffff0d}:is(.dark .dark\:ring-custom-400\/30){--tw-ring-color:rgba(var(--c-400),0.3)}:is(.dark .dark\:ring-custom-500){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--c-500),var(--tw-ring-opacity))}:is(.dark .dark\:ring-danger-500){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--danger-500),var(--tw-ring-opacity))}:is(.dark .dark\:ring-gray-400\/20){--tw-ring-color:rgba(var(--gray-400),0.2)}:is(.dark .dark\:ring-gray-50\/10){--tw-ring-color:rgba(var(--gray-50),0.1)}:is(.dark .dark\:ring-gray-700){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-700),var(--tw-ring-opacity))}:is(.dark .dark\:ring-gray-900){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-900),var(--tw-ring-opacity))}:is(.dark .dark\:ring-white\/10){--tw-ring-color:#ffffff1a}:is(.dark .dark\:ring-white\/20){--tw-ring-color:#fff3}:is(.dark .dark\:placeholder\:text-gray-500)::-moz-placeholder{--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}:is(.dark .dark\:placeholder\:text-gray-500)::placeholder{--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}:is(.dark .dark\:before\:bg-primary-500):before{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgba(var(--primary-500),var(--tw-bg-opacity))}:is(.dark .dark\:checked\:bg-danger-500:checked){--tw-bg-opacity:1;background-color:rgba(var(--danger-500),var(--tw-bg-opacity))}:is(.dark .dark\:checked\:bg-primary-500:checked){--tw-bg-opacity:1;background-color:rgba(var(--primary-500),var(--tw-bg-opacity))}:is(.dark .dark\:focus-within\:bg-white\/5:focus-within){background-color:#ffffff0d}:is(.dark .dark\:hover\:bg-custom-400:hover){--tw-bg-opacity:1;background-color:rgba(var(--c-400),var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-custom-400\/10:hover){background-color:rgba(var(--c-400),.1)}:is(.dark .dark\:hover\:bg-white\/10:hover){background-color:#ffffff1a}:is(.dark .dark\:hover\:bg-white\/5:hover){background-color:#ffffff0d}:is(.dark .dark\:hover\:text-custom-300:hover){--tw-text-opacity:1;color:rgba(var(--c-300),var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-custom-300\/75:hover){color:rgba(var(--c-300),.75)}:is(.dark .dark\:hover\:text-gray-200:hover){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300\/75:hover){color:rgba(var(--gray-300),.75)}:is(.dark .dark\:hover\:text-gray-400:hover){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .dark\:hover\:ring-white\/20:hover){--tw-ring-color:#fff3}:is(.dark .dark\:focus\:ring-danger-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--danger-500),var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--primary-500),var(--tw-ring-opacity))}:is(.dark .dark\:checked\:focus\:ring-danger-400\/50:focus:checked){--tw-ring-color:rgba(var(--danger-400),0.5)}:is(.dark .dark\:checked\:focus\:ring-primary-400\/50:focus:checked){--tw-ring-color:rgba(var(--primary-400),0.5)}:is(.dark .dark\:focus-visible\:border-primary-500:focus-visible){--tw-border-opacity:1;border-color:rgba(var(--primary-500),var(--tw-border-opacity))}:is(.dark .dark\:focus-visible\:bg-custom-400\/10:focus-visible){background-color:rgba(var(--c-400),.1)}:is(.dark .dark\:focus-visible\:bg-white\/5:focus-visible){background-color:#ffffff0d}:is(.dark .dark\:focus-visible\:text-custom-300\/75:focus-visible){color:rgba(var(--c-300),.75)}:is(.dark .dark\:focus-visible\:text-gray-300\/75:focus-visible){color:rgba(var(--gray-300),.75)}:is(.dark .dark\:focus-visible\:text-gray-400:focus-visible){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .dark\:focus-visible\:ring-custom-400\/50:focus-visible){--tw-ring-color:rgba(var(--c-400),0.5)}:is(.dark .dark\:focus-visible\:ring-custom-500:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--c-500),var(--tw-ring-opacity))}:is(.dark .dark\:focus-visible\:ring-primary-500:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--primary-500),var(--tw-ring-opacity))}:is(.dark .dark\:disabled\:bg-transparent:disabled){background-color:initial}:is(.dark .dark\:disabled\:text-gray-400:disabled){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .dark\:disabled\:ring-white\/10:disabled){--tw-ring-color:#ffffff1a}:is(.dark .dark\:disabled\:\[-webkit-text-fill-color\:theme\(colors\.gray\.400\)\]:disabled){-webkit-text-fill-color:rgba(var(--gray-400),1)}:is(.dark .dark\:disabled\:placeholder\:\[-webkit-text-fill-color\:theme\(colors\.gray\.500\)\]:disabled)::-moz-placeholder,:is(.dark .dark\:disabled\:placeholder\:\[-webkit-text-fill-color\:theme\(colors\.gray\.500\)\]:disabled)::placeholder{-webkit-text-fill-color:rgba(var(--gray-500),1)}:is(.dark .dark\:disabled\:checked\:bg-gray-600:checked:disabled){--tw-bg-opacity:1;background-color:rgba(var(--gray-600),var(--tw-bg-opacity))}:is(.dark .group\/button:hover .dark\:group-hover\/button\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .group:hover .dark\:group-hover\:text-gray-200){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(.dark .group:hover .dark\:group-hover\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .group:focus-visible .dark\:group-focus-visible\:text-gray-200){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(.dark .group:focus-visible .dark\:group-focus-visible\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}@media (min-width:1024px){:is([dir=rtl] .rtl\:lg\:-translate-x-0){--tw-translate-x:-0px}:is([dir=rtl] .rtl\:lg\:-translate-x-0),:is([dir=rtl] .rtl\:lg\:translate-x-full){transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:lg\:translate-x-full){--tw-translate-x:100%}:is(.dark .dark\:lg\:bg-transparent){background-color:initial}}:is(.dark .dark\:\[\&\.trix-active\]\:bg-white\/5.trix-active){background-color:#ffffff0d}:is(.dark .dark\:\[\&\.trix-active\]\:text-primary-400.trix-active){--tw-text-opacity:1;color:rgba(var(--primary-400),var(--tw-text-opacity))}:is(.dark .dark\:\[\&\:not\(\:has\(\.fi-ac-action\:focus\)\)\]\:focus-within\:ring-danger-500:focus-within:not(:has(.fi-ac-action:focus))){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--danger-500),var(--tw-ring-opacity))}:is(.dark .dark\:\[\&\:not\(\:has\(\.fi-ac-action\:focus\)\)\]\:focus-within\:ring-primary-500:focus-within:not(:has(.fi-ac-action:focus))){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--primary-500),var(--tw-ring-opacity))}:is(.dark .dark\:\[\&\:not\(\:nth-child\(1_of_\.fi-btn\)\)\]\:shadow-\[-1px_0_0_0_theme\(colors\.white\/20\%\)\]:not(:nth-child(1 of .fi-btn))){--tw-shadow:-1px 0 0 0 #fff3;--tw-shadow-colored:-1px 0 0 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}:is(.dark .\[\&\>\*\:first-child\]\:dark\:before\:bg-primary-500)>:first-child:before{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgba(var(--primary-500),var(--tw-bg-opacity))}:is(.dark .\[\&_optgroup\]\:dark\:bg-gray-900) optgroup{--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}:is(.dark .\[\&_option\]\:dark\:bg-gray-900) option{--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}:checked+*>.\[\:checked\+\*\>\&\]\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}input:checked+.\[input\:checked\+\&\]\:bg-custom-600{--tw-bg-opacity:1;background-color:rgba(var(--c-600),var(--tw-bg-opacity))}input:checked+.\[input\:checked\+\&\]\:bg-gray-400{--tw-bg-opacity:1;background-color:rgba(var(--gray-400),var(--tw-bg-opacity))}input:checked+.\[input\:checked\+\&\]\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}input:checked+.\[input\:checked\+\&\]\:ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}input:checked+.\[input\:checked\+\&\]\:hover\:bg-custom-500:hover{--tw-bg-opacity:1;background-color:rgba(var(--c-500),var(--tw-bg-opacity))}input:checked+.\[input\:checked\+\&\]\:hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgba(var(--gray-300),var(--tw-bg-opacity))}:is(.dark input:checked+.dark\:\[input\:checked\+\&\]\:bg-custom-500){--tw-bg-opacity:1;background-color:rgba(var(--c-500),var(--tw-bg-opacity))}:is(.dark input:checked+.dark\:\[input\:checked\+\&\]\:bg-gray-600){--tw-bg-opacity:1;background-color:rgba(var(--gray-600),var(--tw-bg-opacity))}:is(.dark input:checked+.dark\:\[input\:checked\+\&\]\:hover\:bg-custom-400:hover){--tw-bg-opacity:1;background-color:rgba(var(--c-400),var(--tw-bg-opacity))}:is(.dark input:checked+.dark\:\[input\:checked\+\&\]\:hover\:bg-gray-500:hover){--tw-bg-opacity:1;background-color:rgba(var(--gray-500),var(--tw-bg-opacity))}input:checked:focus-visible+.\[input\:checked\:focus-visible\+\&\]\:ring-custom-500\/50{--tw-ring-color:rgba(var(--c-500),0.5)}:is(.dark input:checked:focus-visible+.dark\:\[input\:checked\:focus-visible\+\&\]\:ring-custom-400\/50){--tw-ring-color:rgba(var(--c-400),0.5)}input:focus-visible+.\[input\:focus-visible\+\&\]\:z-10{z-index:10}input:focus-visible+.\[input\:focus-visible\+\&\]\:ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}input:focus-visible+.\[input\:focus-visible\+\&\]\:ring-gray-950\/10{--tw-ring-color:rgba(var(--gray-950),0.1)}:is(.dark input:focus-visible+.dark\:\[input\:focus-visible\+\&\]\:ring-white\/20){--tw-ring-color:#fff3}#circle-cursor{pointer-events:none;cursor:crosshair;display:none;width:20px;height:20px;background-color:#ffffff80;border-radius:50%;position:absolute;top:0;left:0;z-index:10000;transition:width .3s,height .3s,left .1s,top .1s;box-shadow:0 0 5px 0 rgb(var(--gray-950))}.driver-popover{box-sizing:border-box;margin:0;padding:15px;border-radius:5px;min-width:300px!important;max-width:750px!important;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0}.driver-help-button{width:15px;height:15px;position:absolute;cursor:pointer}.top-left{top:-14px;left:-16px}.top-right{top:-14px;right:-16px}.bottom-right{bottom:-14px;right:-16px}.bottom-left{bottom:-14px;left:-16px} --------------------------------------------------------------------------------