├── resources ├── sass │ └── tool.scss ├── css │ └── app.css ├── js │ ├── components │ │ ├── icons │ │ │ ├── IconSuccess.vue │ │ │ └── IconFailed.vue │ │ ├── NotificationsCard.vue │ │ ├── NotificationModalFooter.vue │ │ ├── NotificationParamForm.vue │ │ ├── NotificationParamInput.vue │ │ ├── NotificationsOverview.vue │ │ ├── NotificationsTable.vue │ │ ├── NotificationsSend.vue │ │ └── NotificationsParamModal.vue │ └── tool.js ├── lang │ ├── en.json │ ├── fr.json │ └── it.json └── views │ └── navigation.blade.php ├── dist ├── css │ └── tool.css └── js │ └── tool.js ├── .styleci.yml ├── images ├── screenshot_send.png ├── screenshot_overview.png └── screenshot_parameters.png ├── mix-manifest.json ├── .gitignore ├── tests ├── Models │ └── TestModel.php ├── 2018_09_04_000000_create_test_models_table.php ├── Controllers │ ├── NotificationStatsControllerTest.php │ ├── NotificationClassesControllerTest.php │ ├── NotifiableControllerTest.php │ └── NotificationControllerTest.php ├── ClassFinderTest.php ├── Notifications │ └── TestNotification.php └── TestCase.php ├── webpack.mix.js ├── src ├── NovaNotification.php ├── Http │ ├── Middleware │ │ └── Authorize.php │ └── Controllers │ │ ├── ApiController.php │ │ ├── NotificationStatsController.php │ │ ├── NotificationController.php │ │ ├── NotifiableController.php │ │ └── NotificationClassesController.php ├── NovaNotifications.php ├── ClassFinder.php └── ToolServiceProvider.php ├── config └── nova-notifications.php ├── .travis.yml ├── database ├── factories │ ├── TestModelFactory.php │ └── NovaNotificationFactory.php └── migrations │ └── 2018_09_04_000000_create_nova_notifications_table.php ├── phpunit.xml.dist ├── CHANGELOG.md ├── package.json ├── LICENSE.md ├── routes └── api.php ├── composer.json ├── README.md ├── coverage.clover └── tailwind.js /resources/sass/tool.scss: -------------------------------------------------------------------------------- 1 | // Nova Tool CSS 2 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | /*@tailwind components;*/ 2 | /*@tailwind utilities;*/ -------------------------------------------------------------------------------- /dist/css/tool.css: -------------------------------------------------------------------------------- 1 | /*@tailwind components;*/ 2 | 3 | /*@tailwind utilities;*/ 4 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement -------------------------------------------------------------------------------- /images/screenshot_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christophrumpel/nova-notifications/HEAD/images/screenshot_send.png -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dist/js/tool.js": "/dist/js/tool.js", 3 | "/dist/css/tool.css": "/dist/css/tool.css" 4 | } -------------------------------------------------------------------------------- /images/screenshot_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christophrumpel/nova-notifications/HEAD/images/screenshot_overview.png -------------------------------------------------------------------------------- /images/screenshot_parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christophrumpel/nova-notifications/HEAD/images/screenshot_parameters.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | -------------------------------------------------------------------------------- /tests/Models/TestModel.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /resources/js/components/icons/IconFailed.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/nova-notifications.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'App', 9 | ], 10 | /* 11 | * The namespaces you want to check for Notifiable classes. 12 | */ 13 | 'notificationNamespaces' => [ 14 | 'App\Notifications', 15 | 'Illuminate', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | 2 | Nova.booting((Vue, router) => { 3 | router.addRoutes([ 4 | { 5 | name: 'nova-notifications', 6 | path: '/nova-notifications', 7 | component: require('./components/NotificationsOverview'), 8 | }, 9 | { 10 | name: 'nova-notifications-send', 11 | path: '/nova-notifications-send', 12 | component: require('./components/NotificationsSend'), 13 | }, 14 | ]) 15 | 16 | }) 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.3 6 | 7 | env: 8 | matrix: 9 | - COMPOSER_FLAGS="--prefer-lowest" 10 | - COMPOSER_FLAGS="" 11 | 12 | before_script: 13 | - travis_retry composer self-update 14 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source 15 | 16 | script: 17 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 18 | 19 | after_script: 20 | - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover -------------------------------------------------------------------------------- /src/Http/Middleware/Authorize.php: -------------------------------------------------------------------------------- 1 | authorize($request) ? $next($request) : abort(403); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/factories/TestModelFactory.php: -------------------------------------------------------------------------------- 1 | define(TestModel::class, function (Faker $faker) { 18 | return [ 19 | ]; 20 | }); 21 | -------------------------------------------------------------------------------- /src/Http/Controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | isSubclassOf('Illuminate\Database\Eloquent\Model'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/js/components/NotificationsCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/2018_09_04_000000_create_test_models_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('nova_notifications'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/Controllers/NotificationStatsController.php: -------------------------------------------------------------------------------- 1 | $this->getNotificationsCount(), 13 | 'failed' => $this->getFailedNotificationsCount(), 14 | ]; 15 | } 16 | 17 | private function getNotificationsCount() 18 | { 19 | return DB::table('nova_notifications') 20 | ->count(); 21 | } 22 | 23 | private function getFailedNotificationsCount() 24 | { 25 | return DB::table('nova_notifications') 26 | ->where('failed', true) 27 | ->count(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/NovaNotifications.php: -------------------------------------------------------------------------------- 1 | define(NovaNotification::class, function (Faker $faker) { 18 | return [ 19 | 'notification' => $faker->name, 20 | 'notifiable_type' => $faker->unique()->safeEmail, 21 | 'notifiable_id' => $faker->numberBetween(1, 100), 22 | 'channel' => collect(['twitter', 'email', 'Database'])->random(), 23 | 'failed' => false, 24 | ]; 25 | }); 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `nova-notifications` will be documented in this file. 4 | 5 | ## 1.0.6 - 09.10.2018 6 | 7 | * Remove custom styles to avoid Nova styles overriding 8 | 9 | ## 1.0.5 10 | 11 | * Fix bug where notifications were not shown if there was only one 12 | * Change how notifications and notifiables are found. Works not automatically. So the config is not necessary anymore. 13 | 14 | ## 1.0.4 15 | 16 | * Remove TailwindCSS preflight to prevent overriding styles in Nova itself 17 | * Make notificationClasses per default an array to prevent problem with checking length 18 | 19 | ## 1.0.3 20 | 21 | * Added translation files for EN and FR 22 | * Style CI fixes 23 | 24 | ## 1.0.2 - 2018-09-10 25 | 26 | - The path to the migration directory was wrong. This was fixed. 27 | 28 | ## 1.0.1 - 2018-09-10 29 | 30 | - The error message was not being shown on the send page when there was no notification class. This was fixed. 31 | 32 | ## 1.0.0 - 2018-09-10 33 | 34 | - Initial release -------------------------------------------------------------------------------- /tests/Controllers/NotificationStatsControllerTest.php: -------------------------------------------------------------------------------- 1 | get('nova-vendor/nova-notifications/notifications/stats') 13 | ->assertSuccessful(); 14 | } 15 | 16 | /** @test * */ 17 | public function it_returns_stats_for_nova_notifications() 18 | { 19 | factory(NovaNotification::class) 20 | ->times(2) 21 | ->create(); 22 | 23 | factory(NovaNotification::class)->create([ 24 | 'failed' => true, 25 | ]); 26 | 27 | $this->get('nova-vendor/nova-notifications/notifications/stats') 28 | ->assertSuccessful() 29 | ->assertJson([ 30 | 'all' => 3, 31 | 'failed' => 1, 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/ClassFinderTest.php: -------------------------------------------------------------------------------- 1 | app->setBasePath(__DIR__.'/..'); 15 | 16 | $classFinder = app(ClassFinder::class); 17 | 18 | $response = $classFinder->find('Illuminate\Database\Eloquent\Model', ['App']); 19 | 20 | $this->assertInstanceOf('Illuminate\Support\Collection', $response); 21 | } 22 | 23 | /** 24 | * @test 25 | **/ 26 | public function it_find_classes_which_are_extending_a_specific_class() 27 | { 28 | $this->app->setBasePath(__DIR__.'/..'); 29 | 30 | $classFinder = app(ClassFinder::class); 31 | 32 | $response = $classFinder->findByExtending('Illuminate\Database\Eloquent\Model', ['App']); 33 | 34 | $this->assertInstanceOf('Illuminate\Support\Collection', $response); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2018_09_04_000000_create_nova_notifications_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('notification'); 19 | $table->string('notifiable_type'); 20 | $table->string('notifiable_id'); 21 | $table->string('channel'); 22 | $table->boolean('failed')->default(false); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('nova_notifications'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/js/components/NotificationModalFooter.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "^5.0.0", 14 | "laravel-mix": "^1.0", 15 | "tailwindcss": "^0.6.0" 16 | }, 17 | "dependencies": { 18 | "vue": "^2.5.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Christoph Rumpel 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Nova Notifications": "Nova Notifications", 3 | "Define Notification Parameters": "Define Notification Parameters", 4 | "Cancel": "Cancel", 5 | "Channel": "Channel", 6 | "Date": "Date", 7 | "Name": "Name", 8 | "Notifiable": "Notifiable", 9 | "Notifiables": "Notifiables", 10 | "Notifiable ID": "Notifiable ID", 11 | "Notifiable Item": "Notifiable Item", 12 | "Notifiable Type": "Notifiable Type", 13 | "Notification": "Notification", 14 | "Notification has not been chosen.": "Notification has not been chosen.", 15 | "Notification has been sent!": "Notification has been sent!", 16 | "Overview": "Overview", 17 | "Parameters": "Parameters", 18 | "Select": "Select", 19 | "Sent": "Sent", 20 | "Send": "Send", 21 | "Send Notification": "Send Notification", 22 | "Send Notification To": "Send Notification To", 23 | "The notification could not be created with the provided information": "The notification could not be created with the provided information", 24 | "There has been an error!": "There has been an error!", 25 | "You don't have any notification classes yet.": "You don't have any notification classes yet." 26 | } -------------------------------------------------------------------------------- /resources/lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Nova Notifications": "Notifications", 3 | "Define Notification Parameters": "Paramètres de la notification", 4 | "Cancel": "Annuler", 5 | "Channel": "Channel", 6 | "Date": "Date", 7 | "Name": "Nom", 8 | "Notifiable": "Destinataire", 9 | "Notifiables": "Destinataires", 10 | "Notifiable ID": "ID destinataire", 11 | "Notifiable Item": "Destinataire", 12 | "Notifiable Type": "Type destinataire", 13 | "Notification": "Notification", 14 | "Notification has not been chosen.": "Aucune notification choisie.", 15 | "Notification has been sent!": "La notification a été envoyée !", 16 | "Overview": "Vue d'ensemble", 17 | "Parameters": "Paramètres", 18 | "Select": "Selectionner", 19 | "Sent": "Envoyé", 20 | "Send": "Envoyer", 21 | "Send Notification": "Envoyer la notification", 22 | "Send Notification To": "Destinataire", 23 | "The notification could not be created with the provided information": "La notification ne peut être créée avec le paramètres fournis", 24 | "There has been an error!": "Il y'a eu une erreur !", 25 | "You don't have any notification classes yet.": "Aucune notification n'est disponible." 26 | } -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | =7.2.0", 12 | "laravel/nova": "*" 13 | }, 14 | "require-dev": { 15 | "orchestra/testbench": "~3.0", 16 | "mockery/mockery": "^1.0", 17 | "phpunit/phpunit": "8.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Christophrumpel\\NovaNotifications\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Christophrumpel\\NovaNotifications\\Tests\\": "tests/" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Christophrumpel\\NovaNotifications\\ToolServiceProvider" 33 | ] 34 | } 35 | }, 36 | "repositories": [ 37 | { 38 | "type": "composer", 39 | "url": "https://nova.laravel.com" 40 | } 41 | ], 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/navigation.blade.php: -------------------------------------------------------------------------------- 1 |

2 | 3 | 6 | 7 | {{__('Notifications')}} 8 |

9 | 10 | 20 | -------------------------------------------------------------------------------- /tests/Notifications/TestNotification.php: -------------------------------------------------------------------------------- 1 | testParam = $testParam; 26 | } 27 | 28 | /** 29 | * Get the notification's delivery channels. 30 | * 31 | * @param mixed $notifiable 32 | * @return array 33 | */ 34 | public function via($notifiable) 35 | { 36 | return ['mail']; 37 | } 38 | 39 | /** 40 | * Get the mail representation of the notification. 41 | * 42 | * @param mixed $notifiable 43 | * @return \Illuminate\Notifications\Messages\MailMessage 44 | */ 45 | public function toMail($notifiable) 46 | { 47 | return (new MailMessage) 48 | ->line('The introduction to the notification.') 49 | ->action('Notification Action', url('/')) 50 | ->line('Thank you for using our application!'); 51 | } 52 | 53 | /** 54 | * Get the array representation of the notification. 55 | * 56 | * @param mixed $notifiable 57 | * @return array 58 | */ 59 | public function toArray($notifiable) 60 | { 61 | return [ 62 | // 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Http/Controllers/NotificationController.php: -------------------------------------------------------------------------------- 1 | get(); 15 | } 16 | 17 | /** 18 | * @param Request $request 19 | * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response 20 | */ 21 | public function send(Request $request) 22 | { 23 | $notificationClass = $request->get('notification')['name']; 24 | $params = collect($request->get('notificationParameters'))->map(function ($param) { 25 | if ($this->isEloquentModelClass($param['type'])) { 26 | return $param['type']::findOrFail($param['value']); 27 | } 28 | 29 | return $param['value']; 30 | }); 31 | 32 | if (! class_exists($notificationClass)) { 33 | return response('', 400); 34 | } 35 | 36 | try { 37 | $notification = $params ? new $notificationClass(...$params) : new $notificationClass(); 38 | } catch (\Throwable $e) { 39 | return response(__('The notification could not be created with the provided information'), 422); 40 | } 41 | 42 | $notifiable = str_replace('.', '\\', $request->get('notifiable')['name']); 43 | 44 | Notification::send($notifiable::findOrFail($request->get('notifiable')['value']), $notification); 45 | 46 | $this->respondSuccess(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Controllers/NotificationClassesControllerTest.php: -------------------------------------------------------------------------------- 1 | disableExceptionHandling(); 19 | 20 | $this->testNotificationClassName = 'Christophrumpel\NovaNotifications\Tests\Notifications\TestNotification'; 21 | 22 | $this->classFinder = Mockery::mock(ClassFinder::class); 23 | $this->app->instance(ClassFinder::class, $this->classFinder); 24 | } 25 | 26 | /** 27 | * @test 28 | **/ 29 | public function it_returns_given_notification_classes() 30 | { 31 | $this->classFinder->shouldReceive('findByExtending') 32 | ->withArgs(['Illuminate\Notifications\Notification', config('nova-notifications.notificationNamespaces')]) 33 | ->andReturn(collect([$this->testNotificationClassName])); 34 | 35 | $this->get('nova-vendor/nova-notifications/notifications/classes') 36 | ->assertSuccessful() 37 | ->assertJson([ 38 | [ 39 | 'name' => $this->testNotificationClassName, 40 | 'parameters' => [ 41 | [ 42 | 'name' => 'testParam', 43 | 'type' => 'int', 44 | 'options' => '', 45 | ], 46 | ], 47 | ], 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Http/Controllers/NotifiableController.php: -------------------------------------------------------------------------------- 1 | classFinder = $classFinder; 23 | } 24 | 25 | public function index() 26 | { 27 | $modelClasses = $this->classFinder->findByExtending('Illuminate\Database\Eloquent\Model', config('nova-notifications.notifiableNamespaces')) 28 | ->filter(function ($className) { 29 | $classInfo = new ReflectionClass($className); 30 | 31 | return in_array('Illuminate\Notifications\Notifiable', $classInfo->getTraitNames()); 32 | }) 33 | ->map(function ($className) { 34 | return [ 35 | 'name' => str_replace('\\', '.', $className), 36 | 'options' => $className::all(), 37 | ]; 38 | }); 39 | 40 | $options = $modelClasses->map(function ($notifiable) { 41 | return [ 42 | 'name' => $notifiable['name'], 43 | ]; 44 | }) 45 | ->toArray(); 46 | 47 | return [ 48 | 'data' => $modelClasses->values(), 49 | 'filter' => [ 50 | 'name' => __('Notifiables'), 51 | 'options' => $options, 52 | ], 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /resources/js/components/NotificationParamForm.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 54 | 55 | -------------------------------------------------------------------------------- /resources/js/components/NotificationParamInput.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 44 | -------------------------------------------------------------------------------- /resources/js/components/NotificationsOverview.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 45 | 46 | 49 | -------------------------------------------------------------------------------- /src/ClassFinder.php: -------------------------------------------------------------------------------- 1 | getAppNamespace(); 17 | } 18 | 19 | /** 20 | * @param string $nameSpace 21 | * @return Collection 22 | */ 23 | public function find(string $nameSpace): Collection 24 | { 25 | $composer = require base_path('vendor/autoload.php'); 26 | 27 | return collect($composer->getClassMap())->filter(function ($value, $key) use ($nameSpace) { 28 | return Str::startsWith($key, $nameSpace); 29 | }); 30 | } 31 | 32 | /** 33 | * Find classes which are extending a specific class. 34 | * 35 | * @param string $classNameToFind 36 | * @param array $namespacesToSearch 37 | * @return Collection 38 | */ 39 | public function findByExtending(string $classNameToFind, array $namespacesToSearch): Collection 40 | { 41 | $composer = require base_path('vendor/autoload.php'); 42 | 43 | return collect($composer->getClassMap()) 44 | ->keys() 45 | ->filter(function ($className) { 46 | return $className !== 'Illuminate\Filesystem\Cache'; 47 | }) 48 | ->filter(function ($className) use ($namespacesToSearch) { 49 | return collect($namespacesToSearch) 50 | ->filter(function ($namespace) use ($className) { 51 | return Str::startsWith($className, $namespace); 52 | }) 53 | ->count(); 54 | }) 55 | ->filter(function ($className) use ($classNameToFind) { 56 | try { 57 | $classInfo = new ReflectionClass($className); 58 | } catch (\Exception $e) { 59 | return false; 60 | } 61 | 62 | return $classInfo->isSubclassOf($classNameToFind); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Controllers/NotifiableControllerTest.php: -------------------------------------------------------------------------------- 1 | app->instance(ClassFinder::class, $classFinder); 17 | 18 | $classFinder->shouldReceive('findByExtending') 19 | ->withArgs(['Illuminate\Database\Eloquent\Model', ['App']]) 20 | ->andReturn(collect([])); 21 | 22 | $response = $this->get('nova-vendor/nova-notifications/notifiables') 23 | ->assertSuccessful(); 24 | 25 | $output = json_decode($response->getContent()); 26 | $this->assertEmpty($output->data); 27 | } 28 | 29 | /** 30 | * @test 31 | **/ 32 | public function it_returns_given_notifiable() 33 | { 34 | $testModelClassName = 'Christophrumpel\NovaNotifications\Tests\Models\TestModel'; 35 | $testModelClassNameDots = str_replace('\\', '.', $testModelClassName); 36 | $classFinder = Mockery::mock(ClassFinder::class); 37 | $this->app->instance(ClassFinder::class, $classFinder); 38 | 39 | $classFinder->shouldReceive('findByExtending') 40 | ->withArgs(['Illuminate\Database\Eloquent\Model', ['App']]) 41 | ->andReturn(collect([$testModelClassName])); 42 | 43 | $this->get('nova-vendor/nova-notifications/notifiables') 44 | ->assertSuccessful() 45 | ->assertJson([ 46 | 'data' => [ 47 | [ 48 | 'name' => $testModelClassNameDots, 49 | 'options' => [], 50 | ], 51 | ], 52 | 'filter' => [ 53 | 'name' => 'Notifiables', 54 | 'options' => [ 55 | [ 56 | 'name' => $testModelClassNameDots, 57 | ], 58 | ], 59 | ], 60 | ]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/../database/migrations'); 19 | $this->loadMigrationsFrom(__DIR__.'/../tests/'); 20 | $this->withFactories(__DIR__.'/../database/factories'); 21 | } 22 | 23 | protected function getPackageProviders($app) 24 | { 25 | return [ 26 | ToolServiceProvider::class, 27 | ]; 28 | } 29 | 30 | /** 31 | * Define environment setup. 32 | * 33 | * @param \Illuminate\Foundation\Application $app 34 | * @return void 35 | */ 36 | protected function getEnvironmentSetUp($app) 37 | { 38 | // Setup default database to use sqlite :memory: 39 | $app['config']->set('database.default', 'testbench'); 40 | $app['config']->set('database.connections.testbench', [ 41 | 'driver' => 'sqlite', 42 | 'database' => ':memory:', 43 | 'prefix' => '', 44 | ]); 45 | 46 | $app['config']->set('nova-notifications.modelNamespace', 'Christophrumpel\NovaNotifications\Tests\Models'); 47 | 48 | $app['config']->set('nova-notifications.notificationNamespace', 49 | 'Christophrumpel\NovaNotifications\Tests\Notifications'); 50 | } 51 | 52 | // Framework-supplied test case methods snipped for brevity 53 | // Use this version if you're on PHP 7 54 | protected function disableExceptionHandling() 55 | { 56 | $this->app->instance(ExceptionHandler::class, new class extends Handler { 57 | public function __construct() 58 | { 59 | } 60 | 61 | public function report(Exception $e) 62 | { 63 | // no-op 64 | } 65 | 66 | public function render($request, Exception $e) 67 | { 68 | throw $e; 69 | } 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /resources/js/components/NotificationsTable.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 74 | 75 | 78 | -------------------------------------------------------------------------------- /src/Http/Controllers/NotificationClassesController.php: -------------------------------------------------------------------------------- 1 | classFinder = $classFinder; 26 | } 27 | 28 | public function index() 29 | { 30 | return $this->classFinder->findByExtending('Illuminate\Notifications\Notification', config('nova-notifications.notificationNamespaces')) 31 | ->map(function ($className) { 32 | try { 33 | $classInfo = new ReflectionMethod($className, '__construct'); 34 | } catch (\ReflectionException $e) { 35 | return [ 36 | 'name' => $className, 37 | 'parameters' => [], 38 | ]; 39 | } 40 | 41 | $notificationClassInfo = new stdClass(); 42 | $notificationClassInfo->name = $classInfo->class; 43 | 44 | $params = collect($classInfo->getParameters())->map(function (ReflectionParameter $param) { 45 | $paramTypeName = is_null($param->getType()) ? 'unknown' : $param->getType() 46 | ->getName(); 47 | 48 | if (class_exists($paramTypeName)) { 49 | $class = new ReflectionClass($paramTypeName); 50 | $fullyClassName = $class->getName(); 51 | 52 | if ($this->isEloquentModelClass($fullyClassName)) { 53 | $options = collect($fullyClassName::all())->map(function ($item) { 54 | return [ 55 | 'id' => $item->id, 56 | 'name' => $item->name ?? $item->id, 57 | ]; 58 | }); 59 | } 60 | } 61 | 62 | return [ 63 | 'name' => $param->getName(), 64 | 'type' => $paramTypeName, 65 | 'options' => $options ?? '', 66 | ]; 67 | }); 68 | 69 | $notificationClassInfo->parameters = $params; 70 | 71 | return $notificationClassInfo; 72 | })->values(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ToolServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views', 'nova-notifications'); 20 | 21 | $this->publishes([ 22 | __DIR__.'/../config/nova-notifications.php' => config_path('nova-notifications.php'), 23 | ], 'config'); 24 | 25 | $this->publishes([ 26 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/nova-notifications'), 27 | ], 'nova-notifications-lang'); 28 | 29 | $this->loadJsonTranslationsFrom(resource_path('lang/vendor/nova-notifications')); 30 | 31 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 32 | 33 | $this->app->booted(function () { 34 | $this->routes(); 35 | }); 36 | 37 | Event::listen('Illuminate\Notifications\Events\NotificationSent', function ($event) { 38 | NovaNotification::create([ 39 | 'notification' => get_class($event->notification), 40 | 'notifiable_type' => get_class($event->notifiable), 41 | 'notifiable_id' => $event->notifiable->id ?? '?', 42 | 'channel' => $event->channel, 43 | 'failed' => false, 44 | ]); 45 | }); 46 | 47 | Event::listen('Illuminate\Notifications\Events\NotificationFailed', function ($event) { 48 | NovaNotification::create([ 49 | 'notification' => get_class($event->notification), 50 | 'notifiable_type' => get_class($event->notifiable), 51 | 'notifiable_id' => $event->notifiable->id ?? '', 52 | 'channel' => $event->channel, 53 | 'failed' => true, 54 | ]); 55 | }); 56 | } 57 | 58 | /** 59 | * Register the tool's routes. 60 | * 61 | * @return void 62 | */ 63 | protected function routes() 64 | { 65 | if ($this->app->routesAreCached()) { 66 | return; 67 | } 68 | 69 | Route::middleware(['nova', Authorize::class]) 70 | ->prefix('nova-vendor/nova-notifications') 71 | ->group(__DIR__.'/../routes/api.php'); 72 | } 73 | 74 | /** 75 | * Register any application services. 76 | * 77 | * @return void 78 | */ 79 | public function register() 80 | { 81 | $this->mergeConfigFrom(__DIR__.'/../config/nova-notifications.php', 'nova-notifications'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > :warning: **Note**: This repo is not maintained right now. 2 | 3 | # A Laravel Nova Tool for Handling Laravel Notifications 4 | 5 | With this [Nova](https://nova.laravel.com) tool: 6 | - You can overview all sent and failed notifications. 7 | - You can send out notifications. (depening on the notification's dependencies) 8 | - All sent or failed notifications will be stored in the database. 9 | 10 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/christophrumpel/nova-notifications.svg?style=flat-square)](https://packagist.org/packages/christophrumpel/nova-notifications) 11 | 12 | ![screenshot of nova notifications overview](/images/screenshot_overview.png) 13 | ![screenshot of nova notifications send](/images/screenshot_send.png) 14 | 15 | ## Requirements 16 | 17 | In order to use this package, you need to have an installation of Laravel with Laravel nova setup. 18 | 19 | ## Installation 20 | 21 | First install the [Nova](https://nova.laravel.com) package via composer: 22 | 23 | ```bash 24 | composer require christophrumpel/nova-notifications 25 | ``` 26 | Then publish the config file(optional): 27 | ``` bash 28 | php artisan vendor:publish --provider="Christophrumpel\NovaNotifications\ToolServiceProvider" 29 | ``` 30 | In there, you can define where to look for the Notification classes, as well as the notifiable models. 31 | ```php 32 | [ 38 | 'App', 39 | ], 40 | /* 41 | * The namespaces you want to check for Notifiable classes. 42 | */ 43 | 'notificationNamespaces' => [ 44 | 'App\Notifications', 45 | 'Illuminate', 46 | ], 47 | ]; 48 | ``` 49 | 50 | 51 | Next up, you must register the tool via the `tools` method of the `NovaServiceProvider`. 52 | 53 | ```php 54 | // inside app/Providers/NovaServiceProvder.php 55 | 56 | // ... 57 | 58 | public function tools() 59 | { 60 | return [ 61 | // ... 62 | new \Christophrumpel\NovaNotifications\NovaNotifications() 63 | ]; 64 | } 65 | ``` 66 | 67 | You also need to run `php artisan migrate` on your Laravel application so that the new notifications table will be created. 68 | 69 | # Usage 70 | 71 | After installing the tool, you should see the new sidebar navigation item for `Notifications`. 72 | 73 | ## Overview 74 | 75 | On the `Overview` page you can see all of the sent and failed notifications. This only works for notifications sent after installing this package. 76 | 77 | Usually, only the notifications sent through the `database` channel will be stored in the database. This package comes with a new `nova_notifications` table, where `all` of them get stored. 78 | 79 | ## Send 80 | 81 | On the `Send` page you can see all of your created notification classes. 82 | 83 | If you don't see a newly created notification class, try running `composer dump-autoload`. 84 | 85 | ### Parameters 86 | 87 | Since you notifications often depend on parameters, this package tries to help you with that. All found constructor parameters will be shown when you try to send a notification. If one of the dependencies is an Eloquent Model, you will get a list with all of the items to choose from. 88 | 89 | ![screenshot of nova notifications send](/images/screenshot_parameters.png) 90 | 91 | If you want to create a new notification with custom objects, then this approach will not work for now. If you have a custom use-case, let me know about it, and we can think of a solution. 92 | 93 | ### Testing 94 | 95 | ``` bash 96 | phpunit tests 97 | ``` 98 | 99 | ### Changelog 100 | 101 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 102 | 103 | ### Security 104 | 105 | If you discover any security-related issues, please email me at c.rumpel@kabsi.at instead of using the issue tracker. 106 | 107 | ## License 108 | 109 | The MIT License (MIT). 110 | -------------------------------------------------------------------------------- /tests/Controllers/NotificationControllerTest.php: -------------------------------------------------------------------------------- 1 | notificationClassName = 'Christophrumpel\NovaNotifications\Tests\Notifications\TestNotification'; 19 | factory(TestModel::class) 20 | ->times(4) 21 | ->create(); 22 | } 23 | 24 | /** @test * */ 25 | public function it_returns_no_nova_notifications() 26 | { 27 | $this->get('nova-vendor/nova-notifications/notifications') 28 | ->assertSuccessful() 29 | ->assertJsonCount(0); 30 | } 31 | 32 | /** @test * */ 33 | public function it_returns_given_nova_notifications() 34 | { 35 | $notifications = factory(NovaNotification::class) 36 | ->times(2) 37 | ->create(); 38 | 39 | $this->get('nova-vendor/nova-notifications/notifications') 40 | ->assertSuccessful() 41 | ->assertJsonCount(2) 42 | ->assertJson([ 43 | $notifications[0]->toArray(), 44 | $notifications[1]->toArray(), 45 | ]); 46 | } 47 | 48 | /** @test * */ 49 | public function it_does_not_find_the_provided_notification_class() 50 | { 51 | $notificationData = [ 52 | 'notification' => ['name' => 'NotGivenNotification'], 53 | 'notificationParameters' => [], 54 | 'notifiable' => [ 55 | 'name' => 'App\User', 56 | 'value' => '2', 57 | ], 58 | ]; 59 | 60 | $this->post('nova-vendor/nova-notifications/notifications/send', $notificationData) 61 | ->assertStatus(400); 62 | } 63 | 64 | /** @test * */ 65 | public function it_sends_a_notification() 66 | { 67 | $this->disableExceptionHandling(); 68 | 69 | $notificationData = [ 70 | 'notification' => ['name' => $this->notificationClassName], 71 | 'notificationParameters' => [], 72 | 'notifiable' => [ 73 | 'name' => 'Christophrumpel\NovaNotifications\Tests\Models\TestModel', 74 | 'value' => '2', 75 | ], 76 | ]; 77 | 78 | $this->post('nova-vendor/nova-notifications/notifications/send', $notificationData) 79 | ->assertSuccessful(); 80 | 81 | Notification::assertSentTo(TestModel::find(2), $this->notificationClassName); 82 | } 83 | 84 | /** @test * */ 85 | public function it_sends_a_notification_with_wrong_notifiable_id() 86 | { 87 | $notificationData = [ 88 | 'notification' => ['name' => $this->notificationClassName], 89 | 'notificationParameters' => [], 90 | 'notifiable' => [ 91 | 'name' => 'Christophrumpel\NovaNotifications\Tests\Models\TestModel', 92 | 'value' => '99', 93 | ], 94 | ]; 95 | 96 | $this->post('nova-vendor/nova-notifications/notifications/send', $notificationData) 97 | ->assertStatus(404); 98 | } 99 | 100 | /** @test * */ 101 | public function it_sends_a_notification_with_wrong_params() 102 | { 103 | $this->disableExceptionHandling(); 104 | $notificationData = [ 105 | 'notification' => ['name' => $this->notificationClassName], 106 | 'notificationParameters' => [ 107 | [ 108 | 'type' => 'int', 109 | 'value' => 'wrongInputType', 110 | ], 111 | ], 112 | 'notifiable' => [ 113 | 'name' => 'Christophrumpel\NovaNotifications\Tests\TestModel', 114 | 'value' => '99', 115 | ], 116 | ]; 117 | 118 | $this->post('nova-vendor/nova-notifications/notifications/send', $notificationData) 119 | ->assertStatus(422); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /resources/js/components/NotificationsSend.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 129 | 130 | 133 | -------------------------------------------------------------------------------- /resources/js/components/NotificationsParamModal.vue: -------------------------------------------------------------------------------- 1 | 117 | 118 | 152 | 153 | 156 | -------------------------------------------------------------------------------- /coverage.clover: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /tailwind.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Tailwind - The Utility-First CSS Framework 4 | 5 | A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink), 6 | David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger). 7 | 8 | Welcome to the Tailwind config file. This is where you can customize 9 | Tailwind specifically for your project. Don't be intimidated by the 10 | length of this file. It's really just a big JavaScript object and 11 | we've done our very best to explain each section. 12 | 13 | View the full documentation at https://tailwindcss.com. 14 | 15 | 16 | |------------------------------------------------------------------------------- 17 | | The default config 18 | |------------------------------------------------------------------------------- 19 | | 20 | | This variable contains the default Tailwind config. You don't have 21 | | to use it, but it can sometimes be helpful to have available. For 22 | | example, you may choose to merge your custom configuration 23 | | values with some of the Tailwind defaults. 24 | | 25 | */ 26 | 27 | // let defaultConfig = require('tailwindcss/defaultConfig')() 28 | 29 | 30 | /* 31 | |------------------------------------------------------------------------------- 32 | | Colors https://tailwindcss.com/docs/colors 33 | |------------------------------------------------------------------------------- 34 | | 35 | | Here you can specify the colors used in your project. To get you started, 36 | | we've provided a generous palette of great looking colors that are perfect 37 | | for prototyping, but don't hesitate to change them for your project. You 38 | | own these colors, nothing will break if you change everything about them. 39 | | 40 | | We've used literal color names ("red", "blue", etc.) for the default 41 | | palette, but if you'd rather use functional names like "primary" and 42 | | "secondary", or even a numeric scale like "100" and "200", go for it. 43 | | 44 | */ 45 | 46 | let colors = { 47 | 'transparent': 'transparent', 48 | 49 | 'black': '#22292f', 50 | 'grey-darkest': '#3d4852', 51 | 'grey-darker': '#606f7b', 52 | 'grey-dark': '#8795a1', 53 | 'grey': '#b8c2cc', 54 | 'grey-light': '#dae1e7', 55 | 'grey-lighter': '#f1f5f8', 56 | 'grey-lightest': '#f8fafc', 57 | 'white': '#ffffff', 58 | 59 | 'red-darkest': '#3b0d0c', 60 | 'red-darker': '#621b18', 61 | 'red-dark': '#cc1f1a', 62 | 'red': '#e3342f', 63 | 'red-light': '#ef5753', 64 | 'red-lighter': '#f9acaa', 65 | 'red-lightest': '#fcebea', 66 | 67 | 'orange-darkest': '#462a16', 68 | 'orange-darker': '#613b1f', 69 | 'orange-dark': '#de751f', 70 | 'orange': '#f6993f', 71 | 'orange-light': '#faad63', 72 | 'orange-lighter': '#fcd9b6', 73 | 'orange-lightest': '#fff5eb', 74 | 75 | 'yellow-darkest': '#453411', 76 | 'yellow-darker': '#684f1d', 77 | 'yellow-dark': '#f2d024', 78 | 'yellow': '#ffed4a', 79 | 'yellow-light': '#fff382', 80 | 'yellow-lighter': '#fff9c2', 81 | 'yellow-lightest': '#fcfbeb', 82 | 83 | 'green-darkest': '#0f2f21', 84 | 'green-darker': '#1a4731', 85 | 'green-dark': '#1f9d55', 86 | 'green': '#38c172', 87 | 'green-light': '#51d88a', 88 | 'green-lighter': '#a2f5bf', 89 | 'green-lightest': '#e3fcec', 90 | 91 | 'teal-darkest': '#0d3331', 92 | 'teal-darker': '#20504f', 93 | 'teal-dark': '#38a89d', 94 | 'teal': '#4dc0b5', 95 | 'teal-light': '#64d5ca', 96 | 'teal-lighter': '#a0f0ed', 97 | 'teal-lightest': '#e8fffe', 98 | 99 | 'blue-darkest': '#12283a', 100 | 'blue-darker': '#1c3d5a', 101 | 'blue-dark': '#2779bd', 102 | 'blue': '#3490dc', 103 | 'blue-light': '#6cb2eb', 104 | 'blue-lighter': '#bcdefa', 105 | 'blue-lightest': '#eff8ff', 106 | 107 | 'indigo-darkest': '#191e38', 108 | 'indigo-darker': '#2f365f', 109 | 'indigo-dark': '#5661b3', 110 | 'indigo': '#6574cd', 111 | 'indigo-light': '#7886d7', 112 | 'indigo-lighter': '#b2b7ff', 113 | 'indigo-lightest': '#e6e8ff', 114 | 115 | 'purple-darkest': '#21183c', 116 | 'purple-darker': '#382b5f', 117 | 'purple-dark': '#794acf', 118 | 'purple': '#9561e2', 119 | 'purple-light': '#a779e9', 120 | 'purple-lighter': '#d6bbfc', 121 | 'purple-lightest': '#f3ebff', 122 | 123 | 'pink-darkest': '#451225', 124 | 'pink-darker': '#6f213f', 125 | 'pink-dark': '#eb5286', 126 | 'pink': '#f66d9b', 127 | 'pink-light': '#fa7ea8', 128 | 'pink-lighter': '#ffbbca', 129 | 'pink-lightest': '#ffebef', 130 | } 131 | 132 | module.exports = { 133 | 134 | /* 135 | |----------------------------------------------------------------------------- 136 | | Colors https://tailwindcss.com/docs/colors 137 | |----------------------------------------------------------------------------- 138 | | 139 | | The color palette defined above is also assigned to the "colors" key of 140 | | your Tailwind config. This makes it easy to access them in your CSS 141 | | using Tailwind's config helper. For example: 142 | | 143 | | .error { color: config('colors.red') } 144 | | 145 | */ 146 | 147 | colors: colors, 148 | 149 | 150 | /* 151 | |----------------------------------------------------------------------------- 152 | | Screens https://tailwindcss.com/docs/responsive-design 153 | |----------------------------------------------------------------------------- 154 | | 155 | | Screens in Tailwind are translated to CSS media queries. They define the 156 | | responsive breakpoints for your project. By default Tailwind takes a 157 | | "mobile first" approach, where each screen size represents a minimum 158 | | viewport width. Feel free to have as few or as many screens as you 159 | | want, naming them in whatever way you'd prefer for your project. 160 | | 161 | | Tailwind also allows for more complex screen definitions, which can be 162 | | useful in certain situations. Be sure to see the full responsive 163 | | documentation for a complete list of options. 164 | | 165 | | Class name: .{screen}:{utility} 166 | | 167 | */ 168 | 169 | screens: { 170 | 'sm': '576px', 171 | 'md': '768px', 172 | 'lg': '992px', 173 | 'xl': '1200px', 174 | }, 175 | 176 | 177 | /* 178 | |----------------------------------------------------------------------------- 179 | | Fonts https://tailwindcss.com/docs/fonts 180 | |----------------------------------------------------------------------------- 181 | | 182 | | Here is where you define your project's font stack, or font families. 183 | | Keep in mind that Tailwind doesn't actually load any fonts for you. 184 | | If you're using custom fonts you'll need to import them prior to 185 | | defining them here. 186 | | 187 | | By default we provide a native font stack that works remarkably well on 188 | | any device or OS you're using, since it just uses the default fonts 189 | | provided by the platform. 190 | | 191 | | Class name: .font-{name} 192 | | 193 | */ 194 | 195 | fonts: { 196 | 'sans': [ 197 | 'system-ui', 198 | 'BlinkMacSystemFont', 199 | '-apple-system', 200 | 'Segoe UI', 201 | 'Roboto', 202 | 'Oxygen', 203 | 'Ubuntu', 204 | 'Cantarell', 205 | 'Fira Sans', 206 | 'Droid Sans', 207 | 'Helvetica Neue', 208 | 'sans-serif', 209 | ], 210 | 'serif': [ 211 | 'Constantia', 212 | 'Lucida Bright', 213 | 'Lucidabright', 214 | 'Lucida Serif', 215 | 'Lucida', 216 | 'DejaVu Serif', 217 | 'Bitstream Vera Serif', 218 | 'Liberation Serif', 219 | 'Georgia', 220 | 'serif', 221 | ], 222 | 'mono': [ 223 | 'Menlo', 224 | 'Monaco', 225 | 'Consolas', 226 | 'Liberation Mono', 227 | 'Courier New', 228 | 'monospace', 229 | ] 230 | }, 231 | 232 | 233 | /* 234 | |----------------------------------------------------------------------------- 235 | | Text sizes https://tailwindcss.com/docs/text-sizing 236 | |----------------------------------------------------------------------------- 237 | | 238 | | Here is where you define your text sizes. Name these in whatever way 239 | | makes the most sense to you. We use size names by default, but 240 | | you're welcome to use a numeric scale or even something else 241 | | entirely. 242 | | 243 | | By default Tailwind uses the "rem" unit type for most measurements. 244 | | This allows you to set a root font size which all other sizes are 245 | | then based on. That said, you are free to use whatever units you 246 | | prefer, be it rems, ems, pixels or other. 247 | | 248 | | Class name: .text-{size} 249 | | 250 | */ 251 | 252 | textSizes: { 253 | 'xs': '.75rem', // 12px 254 | 'sm': '.875rem', // 14px 255 | 'base': '1rem', // 16px 256 | 'lg': '1.125rem', // 18px 257 | 'xl': '1.25rem', // 20px 258 | '2xl': '1.5rem', // 24px 259 | '3xl': '1.875rem', // 30px 260 | '4xl': '2.25rem', // 36px 261 | '5xl': '3rem', // 48px 262 | }, 263 | 264 | 265 | /* 266 | |----------------------------------------------------------------------------- 267 | | Font weights https://tailwindcss.com/docs/font-weight 268 | |----------------------------------------------------------------------------- 269 | | 270 | | Here is where you define your font weights. We've provided a list of 271 | | common font weight names with their respective numeric scale values 272 | | to get you started. It's unlikely that your project will require 273 | | all of these, so we recommend removing those you don't need. 274 | | 275 | | Class name: .font-{weight} 276 | | 277 | */ 278 | 279 | fontWeights: { 280 | 'hairline': 100, 281 | 'thin': 200, 282 | 'light': 300, 283 | 'normal': 400, 284 | 'medium': 500, 285 | 'semibold': 600, 286 | 'bold': 700, 287 | 'extrabold': 800, 288 | 'black': 900, 289 | }, 290 | 291 | 292 | /* 293 | |----------------------------------------------------------------------------- 294 | | Leading (line height) https://tailwindcss.com/docs/line-height 295 | |----------------------------------------------------------------------------- 296 | | 297 | | Here is where you define your line height values, or as we call 298 | | them in Tailwind, leadings. 299 | | 300 | | Class name: .leading-{size} 301 | | 302 | */ 303 | 304 | leading: { 305 | 'none': 1, 306 | 'tight': 1.25, 307 | 'normal': 1.5, 308 | 'loose': 2, 309 | }, 310 | 311 | 312 | /* 313 | |----------------------------------------------------------------------------- 314 | | Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing 315 | |----------------------------------------------------------------------------- 316 | | 317 | | Here is where you define your letter spacing values, or as we call 318 | | them in Tailwind, tracking. 319 | | 320 | | Class name: .tracking-{size} 321 | | 322 | */ 323 | 324 | tracking: { 325 | 'tight': '-0.05em', 326 | 'normal': '0', 327 | 'wide': '0.05em', 328 | }, 329 | 330 | 331 | /* 332 | |----------------------------------------------------------------------------- 333 | | Text colors https://tailwindcss.com/docs/text-color 334 | |----------------------------------------------------------------------------- 335 | | 336 | | Here is where you define your text colors. By default these use the 337 | | color palette we defined above, however you're welcome to set these 338 | | independently if that makes sense for your project. 339 | | 340 | | Class name: .text-{color} 341 | | 342 | */ 343 | 344 | textColors: colors, 345 | 346 | 347 | /* 348 | |----------------------------------------------------------------------------- 349 | | Background colors https://tailwindcss.com/docs/background-color 350 | |----------------------------------------------------------------------------- 351 | | 352 | | Here is where you define your background colors. By default these use 353 | | the color palette we defined above, however you're welcome to set 354 | | these independently if that makes sense for your project. 355 | | 356 | | Class name: .bg-{color} 357 | | 358 | */ 359 | 360 | backgroundColors: colors, 361 | 362 | 363 | /* 364 | |----------------------------------------------------------------------------- 365 | | Background sizes https://tailwindcss.com/docs/background-size 366 | |----------------------------------------------------------------------------- 367 | | 368 | | Here is where you define your background sizes. We provide some common 369 | | values that are useful in most projects, but feel free to add other sizes 370 | | that are specific to your project here as well. 371 | | 372 | | Class name: .bg-{size} 373 | | 374 | */ 375 | 376 | backgroundSize: { 377 | 'auto': 'auto', 378 | 'cover': 'cover', 379 | 'contain': 'contain', 380 | }, 381 | 382 | 383 | /* 384 | |----------------------------------------------------------------------------- 385 | | Border widths https://tailwindcss.com/docs/border-width 386 | |----------------------------------------------------------------------------- 387 | | 388 | | Here is where you define your border widths. Take note that border 389 | | widths require a special "default" value set as well. This is the 390 | | width that will be used when you do not specify a border width. 391 | | 392 | | Class name: .border{-side?}{-width?} 393 | | 394 | */ 395 | 396 | borderWidths: { 397 | default: '1px', 398 | '0': '0', 399 | '2': '2px', 400 | '4': '4px', 401 | '8': '8px', 402 | }, 403 | 404 | 405 | /* 406 | |----------------------------------------------------------------------------- 407 | | Border colors https://tailwindcss.com/docs/border-color 408 | |----------------------------------------------------------------------------- 409 | | 410 | | Here is where you define your border colors. By default these use the 411 | | color palette we defined above, however you're welcome to set these 412 | | independently if that makes sense for your project. 413 | | 414 | | Take note that border colors require a special "default" value set 415 | | as well. This is the color that will be used when you do not 416 | | specify a border color. 417 | | 418 | | Class name: .border-{color} 419 | | 420 | */ 421 | 422 | borderColors: global.Object.assign({ default: colors['grey-light'] }, colors), 423 | 424 | 425 | /* 426 | |----------------------------------------------------------------------------- 427 | | Border radius https://tailwindcss.com/docs/border-radius 428 | |----------------------------------------------------------------------------- 429 | | 430 | | Here is where you define your border radius values. If a `default` radius 431 | | is provided, it will be made available as the non-suffixed `.rounded` 432 | | utility. 433 | | 434 | | If your scale includes a `0` value to reset already rounded corners, it's 435 | | a good idea to put it first so other values are able to override it. 436 | | 437 | | Class name: .rounded{-side?}{-size?} 438 | | 439 | */ 440 | 441 | borderRadius: { 442 | 'none': '0', 443 | 'sm': '.125rem', 444 | default: '.25rem', 445 | 'lg': '.5rem', 446 | 'full': '9999px', 447 | }, 448 | 449 | 450 | /* 451 | |----------------------------------------------------------------------------- 452 | | Width https://tailwindcss.com/docs/width 453 | |----------------------------------------------------------------------------- 454 | | 455 | | Here is where you define your width utility sizes. These can be 456 | | percentage based, pixels, rems, or any other units. By default 457 | | we provide a sensible rem based numeric scale, a percentage 458 | | based fraction scale, plus some other common use-cases. You 459 | | can, of course, modify these values as needed. 460 | | 461 | | 462 | | It's also worth mentioning that Tailwind automatically escapes 463 | | invalid CSS class name characters, which allows you to have 464 | | awesome classes like .w-2/3. 465 | | 466 | | Class name: .w-{size} 467 | | 468 | */ 469 | 470 | width: { 471 | 'auto': 'auto', 472 | 'px': '1px', 473 | '1': '0.25rem', 474 | '2': '0.5rem', 475 | '3': '0.75rem', 476 | '4': '1rem', 477 | '5': '1.25rem', 478 | '6': '1.5rem', 479 | '8': '2rem', 480 | '10': '2.5rem', 481 | '12': '3rem', 482 | '16': '4rem', 483 | '24': '6rem', 484 | '32': '8rem', 485 | '48': '12rem', 486 | '64': '16rem', 487 | '1/2': '50%', 488 | '1/3': '33.33333%', 489 | '2/3': '66.66667%', 490 | '1/4': '25%', 491 | '3/4': '75%', 492 | '1/5': '20%', 493 | '2/5': '40%', 494 | '3/5': '60%', 495 | '4/5': '80%', 496 | '1/6': '16.66667%', 497 | '5/6': '83.33333%', 498 | 'full': '100%', 499 | 'screen': '100vw' 500 | }, 501 | 502 | 503 | /* 504 | |----------------------------------------------------------------------------- 505 | | Height https://tailwindcss.com/docs/height 506 | |----------------------------------------------------------------------------- 507 | | 508 | | Here is where you define your height utility sizes. These can be 509 | | percentage based, pixels, rems, or any other units. By default 510 | | we provide a sensible rem based numeric scale plus some other 511 | | common use-cases. You can, of course, modify these values as 512 | | needed. 513 | | 514 | | Class name: .h-{size} 515 | | 516 | */ 517 | 518 | height: { 519 | 'auto': 'auto', 520 | 'px': '1px', 521 | '1': '0.25rem', 522 | '2': '0.5rem', 523 | '3': '0.75rem', 524 | '4': '1rem', 525 | '5': '1.25rem', 526 | '6': '1.5rem', 527 | '8': '2rem', 528 | '10': '2.5rem', 529 | '12': '3rem', 530 | '16': '4rem', 531 | '24': '6rem', 532 | '32': '8rem', 533 | '48': '12rem', 534 | '64': '16rem', 535 | 'full': '100%', 536 | 'screen': '100vh' 537 | }, 538 | 539 | 540 | /* 541 | |----------------------------------------------------------------------------- 542 | | Minimum width https://tailwindcss.com/docs/min-width 543 | |----------------------------------------------------------------------------- 544 | | 545 | | Here is where you define your minimum width utility sizes. These can 546 | | be percentage based, pixels, rems, or any other units. We provide a 547 | | couple common use-cases by default. You can, of course, modify 548 | | these values as needed. 549 | | 550 | | Class name: .min-w-{size} 551 | | 552 | */ 553 | 554 | minWidth: { 555 | '0': '0', 556 | 'full': '100%', 557 | }, 558 | 559 | 560 | /* 561 | |----------------------------------------------------------------------------- 562 | | Minimum height https://tailwindcss.com/docs/min-height 563 | |----------------------------------------------------------------------------- 564 | | 565 | | Here is where you define your minimum height utility sizes. These can 566 | | be percentage based, pixels, rems, or any other units. We provide a 567 | | few common use-cases by default. You can, of course, modify these 568 | | values as needed. 569 | | 570 | | Class name: .min-h-{size} 571 | | 572 | */ 573 | 574 | minHeight: { 575 | '0': '0', 576 | 'full': '100%', 577 | 'screen': '100vh' 578 | }, 579 | 580 | 581 | /* 582 | |----------------------------------------------------------------------------- 583 | | Maximum width https://tailwindcss.com/docs/max-width 584 | |----------------------------------------------------------------------------- 585 | | 586 | | Here is where you define your maximum width utility sizes. These can 587 | | be percentage based, pixels, rems, or any other units. By default 588 | | we provide a sensible rem based scale and a "full width" size, 589 | | which is basically a reset utility. You can, of course, 590 | | modify these values as needed. 591 | | 592 | | Class name: .max-w-{size} 593 | | 594 | */ 595 | 596 | maxWidth: { 597 | 'xs': '20rem', 598 | 'sm': '30rem', 599 | 'md': '40rem', 600 | 'lg': '50rem', 601 | 'xl': '60rem', 602 | '2xl': '70rem', 603 | '3xl': '80rem', 604 | '4xl': '90rem', 605 | '5xl': '100rem', 606 | 'full': '100%', 607 | }, 608 | 609 | 610 | /* 611 | |----------------------------------------------------------------------------- 612 | | Maximum height https://tailwindcss.com/docs/max-height 613 | |----------------------------------------------------------------------------- 614 | | 615 | | Here is where you define your maximum height utility sizes. These can 616 | | be percentage based, pixels, rems, or any other units. We provide a 617 | | couple common use-cases by default. You can, of course, modify 618 | | these values as needed. 619 | | 620 | | Class name: .max-h-{size} 621 | | 622 | */ 623 | 624 | maxHeight: { 625 | 'full': '100%', 626 | 'screen': '100vh', 627 | }, 628 | 629 | 630 | /* 631 | |----------------------------------------------------------------------------- 632 | | Padding https://tailwindcss.com/docs/padding 633 | |----------------------------------------------------------------------------- 634 | | 635 | | Here is where you define your padding utility sizes. These can be 636 | | percentage based, pixels, rems, or any other units. By default we 637 | | provide a sensible rem based numeric scale plus a couple other 638 | | common use-cases like "1px". You can, of course, modify these 639 | | values as needed. 640 | | 641 | | Class name: .p{side?}-{size} 642 | | 643 | */ 644 | 645 | padding: { 646 | 'px': '1px', 647 | '0': '0', 648 | '1': '0.25rem', 649 | '2': '0.5rem', 650 | '3': '0.75rem', 651 | '4': '1rem', 652 | '5': '1.25rem', 653 | '6': '1.5rem', 654 | '8': '2rem', 655 | '10': '2.5rem', 656 | '12': '3rem', 657 | '16': '4rem', 658 | '20': '5rem', 659 | '24': '6rem', 660 | '32': '8rem', 661 | }, 662 | 663 | 664 | /* 665 | |----------------------------------------------------------------------------- 666 | | Margin https://tailwindcss.com/docs/margin 667 | |----------------------------------------------------------------------------- 668 | | 669 | | Here is where you define your margin utility sizes. These can be 670 | | percentage based, pixels, rems, or any other units. By default we 671 | | provide a sensible rem based numeric scale plus a couple other 672 | | common use-cases like "1px". You can, of course, modify these 673 | | values as needed. 674 | | 675 | | Class name: .m{side?}-{size} 676 | | 677 | */ 678 | 679 | margin: { 680 | 'auto': 'auto', 681 | 'px': '1px', 682 | '0': '0', 683 | '1': '0.25rem', 684 | '2': '0.5rem', 685 | '3': '0.75rem', 686 | '4': '1rem', 687 | '5': '1.25rem', 688 | '6': '1.5rem', 689 | '8': '2rem', 690 | '10': '2.5rem', 691 | '12': '3rem', 692 | '16': '4rem', 693 | '20': '5rem', 694 | '24': '6rem', 695 | '32': '8rem', 696 | }, 697 | 698 | 699 | /* 700 | |----------------------------------------------------------------------------- 701 | | Negative margin https://tailwindcss.com/docs/negative-margin 702 | |----------------------------------------------------------------------------- 703 | | 704 | | Here is where you define your negative margin utility sizes. These can 705 | | be percentage based, pixels, rems, or any other units. By default we 706 | | provide matching values to the padding scale since these utilities 707 | | generally get used together. You can, of course, modify these 708 | | values as needed. 709 | | 710 | | Class name: .-m{side?}-{size} 711 | | 712 | */ 713 | 714 | negativeMargin: { 715 | 'px': '1px', 716 | '0': '0', 717 | '1': '0.25rem', 718 | '2': '0.5rem', 719 | '3': '0.75rem', 720 | '4': '1rem', 721 | '5': '1.25rem', 722 | '6': '1.5rem', 723 | '8': '2rem', 724 | '10': '2.5rem', 725 | '12': '3rem', 726 | '16': '4rem', 727 | '20': '5rem', 728 | '24': '6rem', 729 | '32': '8rem', 730 | }, 731 | 732 | 733 | /* 734 | |----------------------------------------------------------------------------- 735 | | Shadows https://tailwindcss.com/docs/shadows 736 | |----------------------------------------------------------------------------- 737 | | 738 | | Here is where you define your shadow utilities. As you can see from 739 | | the defaults we provide, it's possible to apply multiple shadows 740 | | per utility using comma separation. 741 | | 742 | | If a `default` shadow is provided, it will be made available as the non- 743 | | suffixed `.shadow` utility. 744 | | 745 | | Class name: .shadow-{size?} 746 | | 747 | */ 748 | 749 | shadows: { 750 | default: '0 2px 4px 0 rgba(0,0,0,0.10)', 751 | 'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)', 752 | 'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)', 753 | 'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)', 754 | 'outline': '0 0 0 3px rgba(52,144,220,0.5)', 755 | 'none': 'none', 756 | }, 757 | 758 | 759 | /* 760 | |----------------------------------------------------------------------------- 761 | | Z-index https://tailwindcss.com/docs/z-index 762 | |----------------------------------------------------------------------------- 763 | | 764 | | Here is where you define your z-index utility values. By default we 765 | | provide a sensible numeric scale. You can, of course, modify these 766 | | values as needed. 767 | | 768 | | Class name: .z-{index} 769 | | 770 | */ 771 | 772 | zIndex: { 773 | 'auto': 'auto', 774 | '0': 0, 775 | '10': 10, 776 | '20': 20, 777 | '30': 30, 778 | '40': 40, 779 | '50': 50, 780 | }, 781 | 782 | 783 | /* 784 | |----------------------------------------------------------------------------- 785 | | Opacity https://tailwindcss.com/docs/opacity 786 | |----------------------------------------------------------------------------- 787 | | 788 | | Here is where you define your opacity utility values. By default we 789 | | provide a sensible numeric scale. You can, of course, modify these 790 | | values as needed. 791 | | 792 | | Class name: .opacity-{name} 793 | | 794 | */ 795 | 796 | opacity: { 797 | '0': '0', 798 | '25': '.25', 799 | '50': '.5', 800 | '75': '.75', 801 | '100': '1', 802 | }, 803 | 804 | 805 | /* 806 | |----------------------------------------------------------------------------- 807 | | SVG fill https://tailwindcss.com/docs/svg 808 | |----------------------------------------------------------------------------- 809 | | 810 | | Here is where you define your SVG fill colors. By default we just provide 811 | | `fill-current` which sets the fill to the current text color. This lets you 812 | | specify a fill color using existing text color utilities and helps keep the 813 | | generated CSS file size down. 814 | | 815 | | Class name: .fill-{name} 816 | | 817 | */ 818 | 819 | svgFill: { 820 | 'current': 'currentColor', 821 | }, 822 | 823 | 824 | /* 825 | |----------------------------------------------------------------------------- 826 | | SVG stroke https://tailwindcss.com/docs/svg 827 | |----------------------------------------------------------------------------- 828 | | 829 | | Here is where you define your SVG stroke colors. By default we just provide 830 | | `stroke-current` which sets the stroke to the current text color. This lets 831 | | you specify a stroke color using existing text color utilities and helps 832 | | keep the generated CSS file size down. 833 | | 834 | | Class name: .stroke-{name} 835 | | 836 | */ 837 | 838 | svgStroke: { 839 | 'current': 'currentColor', 840 | }, 841 | 842 | 843 | /* 844 | |----------------------------------------------------------------------------- 845 | | Modules https://tailwindcss.com/docs/configuration#modules 846 | |----------------------------------------------------------------------------- 847 | | 848 | | Here is where you control which modules are generated and what variants are 849 | | generated for each of those modules. 850 | | 851 | | Currently supported variants: 852 | | - responsive 853 | | - hover 854 | | - focus 855 | | - active 856 | | - group-hover 857 | | 858 | | To disable a module completely, use `false` instead of an array. 859 | | 860 | */ 861 | 862 | modules: { 863 | appearance: ['responsive'], 864 | backgroundAttachment: ['responsive'], 865 | backgroundColors: ['responsive', 'hover', 'focus'], 866 | backgroundPosition: ['responsive'], 867 | backgroundRepeat: ['responsive'], 868 | backgroundSize: ['responsive'], 869 | borderCollapse: [], 870 | borderColors: ['responsive', 'hover', 'focus'], 871 | borderRadius: ['responsive'], 872 | borderStyle: ['responsive'], 873 | borderWidths: ['responsive'], 874 | cursor: ['responsive'], 875 | display: ['responsive'], 876 | flexbox: ['responsive'], 877 | float: ['responsive'], 878 | fonts: ['responsive'], 879 | fontWeights: ['responsive', 'hover', 'focus'], 880 | height: ['responsive'], 881 | leading: ['responsive'], 882 | lists: ['responsive'], 883 | margin: ['responsive'], 884 | maxHeight: ['responsive'], 885 | maxWidth: ['responsive'], 886 | minHeight: ['responsive'], 887 | minWidth: ['responsive'], 888 | negativeMargin: ['responsive'], 889 | opacity: ['responsive'], 890 | outline: ['focus'], 891 | overflow: ['responsive'], 892 | padding: ['responsive'], 893 | pointerEvents: ['responsive'], 894 | position: ['responsive'], 895 | resize: ['responsive'], 896 | shadows: ['responsive', 'hover', 'focus'], 897 | svgFill: [], 898 | svgStroke: [], 899 | tableLayout: ['responsive'], 900 | textAlign: ['responsive'], 901 | textColors: ['responsive', 'hover', 'focus'], 902 | textSizes: ['responsive'], 903 | textStyle: ['responsive', 'hover', 'focus'], 904 | tracking: ['responsive'], 905 | userSelect: ['responsive'], 906 | verticalAlign: ['responsive'], 907 | visibility: ['responsive'], 908 | whitespace: ['responsive'], 909 | width: ['responsive'], 910 | zIndex: ['responsive'], 911 | }, 912 | 913 | 914 | /* 915 | |----------------------------------------------------------------------------- 916 | | Plugins https://tailwindcss.com/docs/plugins 917 | |----------------------------------------------------------------------------- 918 | | 919 | | Here is where you can register any plugins you'd like to use in your 920 | | project. Tailwind's built-in `container` plugin is enabled by default to 921 | | give you a Bootstrap-style responsive container component out of the box. 922 | | 923 | | Be sure to view the complete plugin documentation to learn more about how 924 | | the plugin system works. 925 | | 926 | */ 927 | 928 | plugins: [ 929 | require('tailwindcss/plugins/container')({ 930 | // center: true, 931 | // padding: '1rem', 932 | }), 933 | ], 934 | 935 | 936 | /* 937 | |----------------------------------------------------------------------------- 938 | | Advanced Options https://tailwindcss.com/docs/configuration#options 939 | |----------------------------------------------------------------------------- 940 | | 941 | | Here is where you can tweak advanced configuration options. We recommend 942 | | leaving these options alone unless you absolutely need to change them. 943 | | 944 | */ 945 | 946 | options: { 947 | prefix: '', 948 | important: false, 949 | separator: ':', 950 | }, 951 | 952 | } 953 | -------------------------------------------------------------------------------- /dist/js/tool.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 3); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports) { 69 | 70 | /* globals __VUE_SSR_CONTEXT__ */ 71 | 72 | // IMPORTANT: Do NOT use ES2015 features in this file. 73 | // This module is a runtime utility for cleaner component module output and will 74 | // be included in the final webpack user bundle. 75 | 76 | module.exports = function normalizeComponent ( 77 | rawScriptExports, 78 | compiledTemplate, 79 | functionalTemplate, 80 | injectStyles, 81 | scopeId, 82 | moduleIdentifier /* server only */ 83 | ) { 84 | var esModule 85 | var scriptExports = rawScriptExports = rawScriptExports || {} 86 | 87 | // ES6 modules interop 88 | var type = typeof rawScriptExports.default 89 | if (type === 'object' || type === 'function') { 90 | esModule = rawScriptExports 91 | scriptExports = rawScriptExports.default 92 | } 93 | 94 | // Vue.extend constructor export interop 95 | var options = typeof scriptExports === 'function' 96 | ? scriptExports.options 97 | : scriptExports 98 | 99 | // render functions 100 | if (compiledTemplate) { 101 | options.render = compiledTemplate.render 102 | options.staticRenderFns = compiledTemplate.staticRenderFns 103 | options._compiled = true 104 | } 105 | 106 | // functional template 107 | if (functionalTemplate) { 108 | options.functional = true 109 | } 110 | 111 | // scopedId 112 | if (scopeId) { 113 | options._scopeId = scopeId 114 | } 115 | 116 | var hook 117 | if (moduleIdentifier) { // server build 118 | hook = function (context) { 119 | // 2.3 injection 120 | context = 121 | context || // cached call 122 | (this.$vnode && this.$vnode.ssrContext) || // stateful 123 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional 124 | // 2.2 with runInNewContext: true 125 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 126 | context = __VUE_SSR_CONTEXT__ 127 | } 128 | // inject component styles 129 | if (injectStyles) { 130 | injectStyles.call(this, context) 131 | } 132 | // register component module identifier for async chunk inferrence 133 | if (context && context._registeredComponents) { 134 | context._registeredComponents.add(moduleIdentifier) 135 | } 136 | } 137 | // used by ssr in case component is cached and beforeCreate 138 | // never gets called 139 | options._ssrRegister = hook 140 | } else if (injectStyles) { 141 | hook = injectStyles 142 | } 143 | 144 | if (hook) { 145 | var functional = options.functional 146 | var existing = functional 147 | ? options.render 148 | : options.beforeCreate 149 | 150 | if (!functional) { 151 | // inject component registration as beforeCreate hook 152 | options.beforeCreate = existing 153 | ? [].concat(existing, hook) 154 | : [hook] 155 | } else { 156 | // for template-only hot-reload because in that case the render fn doesn't 157 | // go through the normalizer 158 | options._injectStyles = hook 159 | // register for functioal component in vue file 160 | options.render = function renderWithStyleInjection (h, context) { 161 | hook.call(context) 162 | return existing(h, context) 163 | } 164 | } 165 | } 166 | 167 | return { 168 | esModule: esModule, 169 | exports: scriptExports, 170 | options: options 171 | } 172 | } 173 | 174 | 175 | /***/ }), 176 | /* 1 */ 177 | /***/ (function(module, exports) { 178 | 179 | /* 180 | MIT License http://www.opensource.org/licenses/mit-license.php 181 | Author Tobias Koppers @sokra 182 | */ 183 | // css base code, injected by the css-loader 184 | module.exports = function(useSourceMap) { 185 | var list = []; 186 | 187 | // return the list of modules as css string 188 | list.toString = function toString() { 189 | return this.map(function (item) { 190 | var content = cssWithMappingToString(item, useSourceMap); 191 | if(item[2]) { 192 | return "@media " + item[2] + "{" + content + "}"; 193 | } else { 194 | return content; 195 | } 196 | }).join(""); 197 | }; 198 | 199 | // import a list of modules into the list 200 | list.i = function(modules, mediaQuery) { 201 | if(typeof modules === "string") 202 | modules = [[null, modules, ""]]; 203 | var alreadyImportedModules = {}; 204 | for(var i = 0; i < this.length; i++) { 205 | var id = this[i][0]; 206 | if(typeof id === "number") 207 | alreadyImportedModules[id] = true; 208 | } 209 | for(i = 0; i < modules.length; i++) { 210 | var item = modules[i]; 211 | // skip already imported module 212 | // this implementation is not 100% perfect for weird media query combinations 213 | // when a module is imported multiple times with different media queries. 214 | // I hope this will never occur (Hey this way we have smaller bundles) 215 | if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { 216 | if(mediaQuery && !item[2]) { 217 | item[2] = mediaQuery; 218 | } else if(mediaQuery) { 219 | item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; 220 | } 221 | list.push(item); 222 | } 223 | } 224 | }; 225 | return list; 226 | }; 227 | 228 | function cssWithMappingToString(item, useSourceMap) { 229 | var content = item[1] || ''; 230 | var cssMapping = item[3]; 231 | if (!cssMapping) { 232 | return content; 233 | } 234 | 235 | if (useSourceMap && typeof btoa === 'function') { 236 | var sourceMapping = toComment(cssMapping); 237 | var sourceURLs = cssMapping.sources.map(function (source) { 238 | return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */' 239 | }); 240 | 241 | return [content].concat(sourceURLs).concat([sourceMapping]).join('\n'); 242 | } 243 | 244 | return [content].join('\n'); 245 | } 246 | 247 | // Adapted from convert-source-map (MIT) 248 | function toComment(sourceMap) { 249 | // eslint-disable-next-line no-undef 250 | var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); 251 | var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64; 252 | 253 | return '/*# ' + data + ' */'; 254 | } 255 | 256 | 257 | /***/ }), 258 | /* 2 */ 259 | /***/ (function(module, exports, __webpack_require__) { 260 | 261 | /* 262 | MIT License http://www.opensource.org/licenses/mit-license.php 263 | Author Tobias Koppers @sokra 264 | Modified by Evan You @yyx990803 265 | */ 266 | 267 | var hasDocument = typeof document !== 'undefined' 268 | 269 | if (typeof DEBUG !== 'undefined' && DEBUG) { 270 | if (!hasDocument) { 271 | throw new Error( 272 | 'vue-style-loader cannot be used in a non-browser environment. ' + 273 | "Use { target: 'node' } in your Webpack config to indicate a server-rendering environment." 274 | ) } 275 | } 276 | 277 | var listToStyles = __webpack_require__(8) 278 | 279 | /* 280 | type StyleObject = { 281 | id: number; 282 | parts: Array 283 | } 284 | 285 | type StyleObjectPart = { 286 | css: string; 287 | media: string; 288 | sourceMap: ?string 289 | } 290 | */ 291 | 292 | var stylesInDom = {/* 293 | [id: number]: { 294 | id: number, 295 | refs: number, 296 | parts: Array<(obj?: StyleObjectPart) => void> 297 | } 298 | */} 299 | 300 | var head = hasDocument && (document.head || document.getElementsByTagName('head')[0]) 301 | var singletonElement = null 302 | var singletonCounter = 0 303 | var isProduction = false 304 | var noop = function () {} 305 | var options = null 306 | var ssrIdKey = 'data-vue-ssr-id' 307 | 308 | // Force single-tag solution on IE6-9, which has a hard limit on the # of