├── 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 |
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}
--------------------------------------------------------------------------------