diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md
new file mode 100644
index 0000000..607b2c2
--- /dev/null
+++ b/plugins/devtools/CHANGELOG.md
@@ -0,0 +1,197 @@
+# v1.5.4
+## 10/26/2021
+
+1. [](#improved)
+ * Moved offline check to CLI flag [#70](https://github.com/getgrav/grav-plugin-devtools/issues/70)
+ * Updated footer copyrights for Pure Blank
+
+# v1.5.3
+## 06/16/2021
+
+1. [](#bugfix)
+ * Fixes over-zealous regex that caused duplication in copy tasks [#69](https://github.com/getgrav/grav-plugin-devtools/issues/69)
+
+# v1.5.2
+## 05/19/2021
+
+1. [](#new)
+ * Added basic TailwindCSS theme [#65](https://github.com/getgrav/grav-plugin-devtools/pull/65)
+1. [](#improved)
+ * Fixed typo [#67](https://github.com/getgrav/grav-plugin-devtools/pull/67)
+ * Use canonical URLs [#58](https://github.com/getgrav/grav-plugin-devtools/pull/58)
+ * Replace `theme_config` with `config.theme` [#60](https://github.com/getgrav/grav-plugin-devtools/pull/60)
+1. [](#bugfix)
+ * Fixed a bad path regarding composer install after plugin creation
+
+# v1.5.1
+## 03/17/2021
+
+1. [](#improved)
+ * Clearer instructions for composer initialization [#62](https://github.com/getgrav/grav-plugin-devtools/pull/62)
+ * Comment out autoload subscription event by default now that Grav 1.7 is out [#62](https://github.com/getgrav/grav-plugin-devtools/pull/62)
+
+# v1.5.0
+## 02/18/2021
+
+1. [](#new)
+ * Updated CLI commands for latest standards
+ * Pass phpstan level 8 tests
+1. [](#improved)
+ * Add default configuration to an inherited theme's YAML file [getgrav/grav-premium-issues#50](https://github.com/getgrav/grav-premium-issues/issues/50)
+1. [](#bugfix)
+ * Output cmd does not correctly show colors [#56](https://github.com/getgrav/grav-plugin-devtools/issues/56)
+
+# v1.4.2
+## 12/02/2020
+
+1. [](#improved)
+ * User return typehints in plugin.php
+ * Add proper twig escapes into a new theme
+
+# v1.4.1
+## 05/20/2020
+
+1. [](#improved)
+ * Make name key Composer 2.0 compatible [#48](https://github.com/getgrav/grav-plugin-devtools/pull/48)
+1. [](#bugfix)
+ * Correct type for themes [#49](https://github.com/getgrav/grav-plugin-devtools/pull/49)
+
+# v1.4.0
+## 04/27/2020
+
+1. [](#new)
+ * Added new required `slug:` and `type:` attributes to blueprints
+1. [](#improved)
+ * Fixed plugin autoload
+
+# v1.3.1
+## 02/24/2020
+
+1. [](#improved)
+ * Set `validation: loose` in plugin blueprints by default
+ * Add Grav 1.6 dependency to all new plugins and themes
+
+# v1.3.0
+## 02/13/2020
+
+1. [](#improved)
+ * Added composer-based autoloader to the `new-plugin` command
+
+# v1.2.4
+## 11/06/2019
+
+1. [](#improved)
+ * Added the ability to use devtools without an online connection to GPM
+1. [](#bugfix)
+ * Regression fix for missing `theme_config` in pure-blank [#45](https://github.com/getgrav/grav-plugin-devtools/issues/45)
+
+# v1.2.3
+## 06/20/2019
+
+1. [](#improved)
+ * pure-blank: Use new 'deferred' blocks for header
+ * pure-blank: Use `home_url` variable
+ * pure-blank: Improved `README.md.twig`
+
+# v1.2.2
+## 04/21/2019
+
+1. [](#bugfix)
+ * Add Github username field to new-theme template [#39](https://github.com/getgrav/grav-plugin-devtools/pull/39)
+
+# v1.2.1
+## 08/04/2018
+
+1. [](#bugfix)
+ * Fixed incorrect folder name as a result of renaming typo of `inheritence` to `inheritance` [#32](https://github.com/getgrav/grav-plugin-devtools/issues/32)
+
+# v1.2.0
+## 07/25/2018
+
+1. [](#new)
+ * Internationalization for blank plugin component [#30](https://github.com/getgrav/grav-plugin-devtools/issues/30)
+1. [](#improved)
+ * Added a new check for reserved PHP words [#7](https://github.com/getgrav/grav-plugin-devtools/issues/7)
+ * Improved regex for valid emails [#21](https://github.com/getgrav/grav-plugin-devtools/issues/21)
+1. [](#bugfix)
+ * Fix broken renaming when doing a theme 'copy'
+ * Typos [#31](https://github.com/getgrav/grav-plugin-devtools/pull/31)
+
+# v1.1.1
+## 03/29/2018
+
+1. [](#bugfix)
+ * Fixed theme inheritance bug [#25](https://github.com/getgrav/grav-plugin-devtools/pull/25)
+
+# v1.1.0
+## 03/29/2018
+
+1. [](#new)
+ * Added new Theme `copy` option to create a new theme from another
+1. [](#improved)
+ * Stop flushing GPM cache on each call to speed things up considerably!
+1. [](#bugfix)
+ * Updated README.md [#23](https://github.com/getgrav/grav-plugin-devtools/pull/23)
+ * Properly extend Theme or Plugin [#24](https://github.com/getgrav/grav-plugin-devtools/pull/24)
+
+# v1.0.8
+## 10/02/2017
+
+1. [](#bugfix)
+ inherited theme is after new theme [#9](https://github.com/getgrav/grav-plugin-devtools/issues/9)
+
+# v1.0.7
+## 10/02/2017
+
+1. [](#bugfix)
+ * Various fixes for things that broke with the blueprint generation PR [#20](https://github.com/getgrav/grav-plugin-devtools/issues/20)
+
+# v1.0.6
+## 09/28/2017
+
+1. [](#new)
+ * Added blueprint generation [#17](https://github.com/getgrav/grav-plugin-devtools/pull/17)
+1. [](#improved)
+ * changed Pure CDN location [#19](https://github.com/getgrav/grav-plugin-devtools/pull/19)
+1. [](#bugfix)
+ * Fixed readme referencing `githubid` [#13](https://github.com/getgrav/grav-plugin-devtools/pull/13)
+
+# v1.0.5
+## 02/26/2017
+
+1. [](#improved)
+ * Added GitHub ID prompt [#5](https://github.com/getgrav/grav-plugin-devtools/pull/5)
+1. [](#bugfix)
+ * Added missing closing html tag [#12](https://github.com/getgrav/grav-plugin-devtools/pull/12)
+
+# v1.0.4
+## 10/19/2016
+
+1. [](#improved)
+ * More complete README.md
+ * Typo in Error template
+
+# v1.0.3
+## 09/16/2016
+
+1. [](#bugfix)
+ * Removed `Theme` from theme's class causing events to not process - https://github.com/getgrav/grav/issues/1047
+ * Typo in README.md
+
+# v1.0.2
+## 07/20/2016
+
+1. [](#bugfix)
+ * Removed old `header.html.twig`
+
+# v1.0.1
+## 05/06/2016
+
+1. [](#bugfix)
+ * Fix for Grav 1.0.x
+
+# v1.0.0
+## 04/19/2016
+
+1. [](#new)
+ * ChangeLog started...
diff --git a/plugins/devtools/LICENSE b/plugins/devtools/LICENSE
new file mode 100644
index 0000000..4bb7092
--- /dev/null
+++ b/plugins/devtools/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Grav
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md
new file mode 100644
index 0000000..c785a2b
--- /dev/null
+++ b/plugins/devtools/README.md
@@ -0,0 +1,79 @@
+# Grav Devtools Plugin
+
+The `devtools` is a [Grav](http://github.com/getgrav/grav) Plugin that lets you quickly create a scaffolding for your new plugins and themes. The plugin provides CLI commands that allow for the quick and easy deployment of a sample scaffolding for your new plugin.
+
+# Installation
+
+## GPM Installation (Preferred)
+
+The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm). From the root of your Grav install type:
+
+ bin/gpm install devtools
+
+## Manual Installation
+
+If for some reason you can't use GPM you can manually install this plugin. Download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `devtools`.
+
+You should now have all the plugin files under
+
+ /your/site/grav/user/plugins/devtools
+
+## Configuration
+
+By default, devtools will perform a check with the online gpm repository to ensure name-collision avoidance. If you wish to not perform this online check, change the devtools.yaml at `user/config/plugins` from `collision_check: true` to `collision_check: false`.
+
+# Usage
+
+## Plugin Scaffolding
+
+To create a new plugin you simply need to run: `bin/plugin devtools new-plugin` and fill in the few questions at the prompts:
+
+```
+> bin/plugin devtools new-plugin
+Enter Plugin Name: MyPlugin
+Enter Plugin Description: My New Custom Plugin
+Enter Developer Name: Johnny Rotten
+Enter GitHub ID (can be blank): pretty-vacant
+Enter Developer Email: johnny@rotten.com
+
+SUCCESS plugin myplugin -> Created Successfully
+
+Path: /home/johnnyr/webroot/grav-installation/user/plugins/myplugin
+```
+
+## Theme Scaffolding
+
+To create a new theme you simply need to run: `bin/plugin devtools new-theme` and fill in the few questions at the prompts:
+
+```
+> bin/plugin devtools new-theme
+Enter Theme Name: MyTheme
+Enter Theme Description: My New Custom Theme
+Enter Developer Name: Johnny Rotten
+Enter GitHub ID (can be blank): pretty-vacant
+Enter Developer Email: johnny@rotten.com
+Please choose a template type
+ [pure-blank ] Basic Theme using Pure.css
+ [inheritance] Inherit from another theme
+ [copy ] Copy another theme
+ > pure-blank
+
+SUCCESS theme mytheme -> Created Successfully
+
+Path: /home/johnnyr/webroot/grav-installation/user/themes/mytheme
+```
+
+There are **three template creation options**
+
+1. `pure-blank` - This is a very basic blank theme that uses the [Pure CSS framework](http://purecss.io/)
+2. `inheritance` - This creates a very basic template with minimal files that inherits a base theme. To find out more about theme inheritance, [check out the subject in more details on the Grav Learn site](https://learn.getgrav.org/themes/customization#theme-inheritance).
+3. `copy` - This allows you to create a new theme based on an existing theme. This is the simplest way to get started with a new theme by using another theme as the basis.
+
+## Skipping Online Project Name Collision Checking
+
+By default, devtools will check your project's name with the existing gpm ecosystem to ensure no collisions. In order to skip this check, add an `--offline` or `-o` to your command:
+
+ `bin/plugin devtools new-theme --offline`
+or
+
+ `bin/plugin devtools new-theme -o`
diff --git a/plugins/devtools/blueprints.yaml b/plugins/devtools/blueprints.yaml
new file mode 100644
index 0000000..33f8c40
--- /dev/null
+++ b/plugins/devtools/blueprints.yaml
@@ -0,0 +1,41 @@
+name: DevTools
+slug: devtools
+type: plugin
+version: 1.5.4
+description: Plugin and Theme scaffolding utilities
+icon: cogs
+author:
+ name: Team Grav
+ email: devs@getgrav.org
+ url: http://getgrav.org
+homepage: https://github.com/getgrav/grav-plugin-devtools
+keywords: devtools, plugin, theme
+bugs: https://github.com/getgrav/grav-plugin-devtools/issues
+license: MIT
+
+dependencies:
+ - { name: grav, version: '>=1.7.0' }
+
+form:
+ validation: strict
+ fields:
+ enabled:
+ type: toggle
+ label: PLUGIN_ADMIN.PLUGIN_STATUS
+ highlight: 1
+ default: 0
+ options:
+ 1: PLUGIN_ADMIN.ENABLED
+ 0: PLUGIN_ADMIN.DISABLED
+ validate:
+ type: bool
+ collision_check:
+ type: toggle
+ label: PLUGIN_DEVTOOLS.COLLISION_CHECK
+ highlight: 1
+ default: 1
+ options:
+ 1: PLUGIN_ADMIN.ENABLED
+ 0: PLUGIN_ADMIN.DISABLED
+ validate:
+ type: bool
diff --git a/plugins/devtools/classes/DevToolsCommand.php b/plugins/devtools/classes/DevToolsCommand.php
new file mode 100644
index 0000000..1271f06
--- /dev/null
+++ b/plugins/devtools/classes/DevToolsCommand.php
@@ -0,0 +1,362 @@
+init();
+ $grav['uri']->init();
+
+ $this->inflector = $grav['inflector'];
+ $this->locator = $grav['locator'];
+ $this->twig = $grav['twig'];
+ $this->gpm = new GPM();
+
+ //Add `theme://` to prevent fail
+ $this->locator->addPath('theme', '', []);
+ $this->locator->addPath('plugin', '', []);
+ $this->locator->addPath('blueprint', '', []);
+ // $this->config->set('theme', $config->get('themes.' . $name));
+ }
+
+ /**
+ * Backwards compatibility to Grav 1.6.
+ *
+ * @return InputInterface
+ */
+ public function getInput(): InputInterface
+ {
+ return $this->input;
+ }
+
+ /**
+ * Backwards compatibility to Grav 1.6.
+ *
+ * @return SymfonyStyle
+ */
+ public function getIO(): SymfonyStyle
+ {
+ $output = $this->output;
+ if (!$output instanceof SymfonyStyle) {
+ $this->output = $output = new SymfonyStyle($this->input, $this->output);
+ }
+
+ return $this->output;
+ }
+
+ /**
+ * Copies the component type and renames accordingly
+ *
+ * @return bool
+ */
+ protected function createComponent(): bool
+ {
+ $name = $this->component['name'];
+ $folder_name = strtolower($this->inflector::hyphenize($name));
+ $new_theme = $folder_name;
+ $type = $this->component['type'];
+ $grav = Grav::instance();
+ $config = $grav['config'];
+ $current_theme = $config->get('system.pages.theme');
+ $template = $this->component['template'];
+ $source_theme = null;
+
+ if (isset($this->component['copy'])) {
+ $current_theme = $this->component['copy'];
+ $source_theme = $this->locator->findResource('themes://' . $current_theme);
+ $template_folder = $source_theme;
+ } else {
+ $template_folder = __DIR__ . "/../components/{$type}/{$template}";
+ }
+
+ if ($type === 'blueprint') {
+ $component_folder = $this->locator->findResource('themes://' . $current_theme) . '/blueprints';
+ } else {
+ $component_folder = $this->locator->findResource($type . 's://') . DS . $folder_name;
+ }
+
+ if (false === $template_folder) {
+ $this->output->writeln("Theme {$current_theme} does not exist");
+ return false;
+ }
+
+ if ($template === 'inheritance') {
+ $parent_theme = $this->component['extends'];
+ $yaml_file = $this->locator->findResource('themes://' . $parent_theme) . '/' . $parent_theme . '.yaml';
+ $this->component['config'] = file_get_contents($yaml_file);;
+ }
+
+ if (isset($source_theme)) {
+ /**
+ * Copy existing theme and regex-replace old stuff with new
+ */
+
+ // Get source if a symlink
+ if (is_link($template_folder)) {
+ $template_folder = readlink($template_folder);
+ if (false === $template_folder) {
+ $this->output->writeln("Theme {$current_theme} is a bad symlink");
+ return false;
+ }
+ }
+
+ //Copy All files to component folder
+ try {
+ Folder::copy($template_folder, $component_folder, '/.git|node_modules/');
+ } catch (\Exception $e) {
+ $this->output->writeln("" . $e->getMessage() . "");
+ return false;
+ }
+
+ // Do some filename renaming
+ $base_old_filename = $component_folder . '/' . $current_theme;
+ $base_new_filename = $component_folder . '/' . $new_theme;
+ @rename( $base_old_filename . '.php', $base_new_filename . '.php');
+ @rename( $base_old_filename . '.yaml', $base_new_filename . '.yaml');
+
+ $camelized_current = $this->inflector::camelize($current_theme);
+ $camelized_new = $this->inflector::camelize($name);
+
+ $hyphenized_current = $this->inflector::hyphenize($current_theme);
+ $hyphenized_new = $this->inflector::hyphenize($name);
+
+ $titleized_current = $this->inflector::titleize($current_theme);
+ $titleized_new = $this->inflector::titleize($name);
+
+ $underscoreized_current = $this->inflector::underscorize($current_theme);
+ $underscoreized_new = $this->inflector::underscorize($name);
+
+ $variations_regex = [
+ ["/$camelized_current/", "/$hyphenized_current/"],
+ [$camelized_new, $hyphenized_new]
+ ];
+
+ if (!in_array("/$titleized_current/", array_values($variations_regex[0]))) {
+ $current_regex = $variations_regex[0];
+ $new_regex = $variations_regex[1];
+ $current_regex[] = "/$titleized_current/";
+ $new_regex[] = $titleized_new;
+ $variations_regex = [$current_regex, $new_regex];
+ }
+
+ if (!in_array("/$underscoreized_current/", array_values($variations_regex[0]))) {
+ $current_regex = $variations_regex[0];
+ $new_regex = $variations_regex[1];
+ $current_regex[] = "/$underscoreized_current/";
+ $new_regex[] = $underscoreized_new;
+ $variations_regex = [$current_regex, $new_regex];
+ }
+
+ $regex_array = [
+ $new_theme . '.php' => $variations_regex,
+ 'blueprints.yaml' => $variations_regex,
+ 'README.md' => $variations_regex,
+ ];
+
+ foreach ($regex_array as $filename => $data) {
+ $filename = $component_folder . '/' . $filename;
+ if (!file_exists($filename)) {
+ continue;
+ }
+ $file = file_get_contents($filename);
+ if ($file) {
+ $file = preg_replace($data[0], $data[1], $file);
+ }
+ file_put_contents($filename, $file);
+ }
+
+ echo $source_theme;
+
+ } else {
+ /**
+ * Use components folder and twig processing
+ */
+ //Copy All files to component folder
+ try {
+ Folder::copy($template_folder, $component_folder);
+ } catch (\Exception $e) {
+ $this->output->writeln("" . $e->getMessage() . "");
+ return false;
+ }
+
+ //Add Twig vars and templates then initialize
+ $this->twig->twig_vars['component'] = $this->component;
+ $this->twig->twig_paths[] = $template_folder;
+ $this->twig->init();
+
+ //Get all templates of component then process each with twig and save
+ $templates = Folder::all($component_folder);
+
+ try {
+ foreach($templates as $templateFile) {
+ if (Utils::endsWith($templateFile, '.twig') && !Utils::endsWith($templateFile, '.html.twig')) {
+ $content = $this->twig->processTemplate($templateFile);
+ $file = File::instance($component_folder . DS . str_replace('.twig', '', $templateFile));
+ $file->content($content);
+ $file->save();
+
+ //Delete twig template
+ $file = File::instance($component_folder . DS . $templateFile);
+ $file->delete();
+ }
+ }
+ } catch (\Exception $e) {
+ $this->output->writeln("" . $e->getMessage() . "");
+ $this->output->writeln("Rolling back...");
+ Folder::delete($component_folder);
+ $this->output->writeln($type . "creation failed!");
+ return false;
+ }
+ if ($type !== 'blueprint') {
+ rename($component_folder . DS . $type . '.php', $component_folder . DS . $folder_name . '.php');
+ rename($component_folder . DS . $type . '.yaml', $component_folder . DS . $folder_name . '.yaml');
+ } else {
+ $bpname = $this->inflector::hyphenize($this->component['bpname']);
+ rename($component_folder . DS . $type . '.yaml', $component_folder . DS . $bpname . '.yaml');
+ }
+ }
+
+ $this->output->writeln('');
+ $this->output->writeln('SUCCESS ' . $type . ' ' . $name . ' -> Created Successfully');
+ $this->output->writeln('');
+ $this->output->writeln('Path: ' . $component_folder . '');
+ $this->output->writeln('');
+ if ($type === 'plugin') {
+ $this->output->writeln('Please run `cd ' . $component_folder . '` and `composer update` to initialize the autoloader');
+ $this->output->writeln('');
+ }
+
+ return true;
+ }
+
+ /**
+ * Iterate through all options and validate
+ *
+ * @return void
+ */
+ protected function validateOptions(): void
+ {
+ foreach (array_filter($this->options) as $type => $value) {
+ $this->validate($type, $value);
+ }
+ }
+
+ /**
+ * @param string $type
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function validate(string $type, $value)
+ {
+ switch ($type) {
+ case 'name':
+ // Check If name
+ if ($value === null || trim($value) === '') {
+ throw new \RuntimeException('Name cannot be empty');
+ }
+
+ if (!$this->options['offline']) {
+ // Check for name collision with online gpm.
+ if (false !== $this->gpm->findPackage($value)) {
+ throw new \RuntimeException('Package name exists in GPM');
+ }
+ } else {
+ $this->output->writeln('');
+ $this->output->writeln(' Warning: Please note that by skipping the online check, your project\'s plugin or theme name may conflict with an existing plugin or theme.');
+ }
+
+ // Check if it's reserved
+ if ($this->isReservedWord(strtolower($value))) {
+ throw new \RuntimeException("\"" . $value . "\" is a reserved word and cannot be used as the name");
+ }
+
+ break;
+
+ case 'description':
+ if($value === null || trim($value) === '') {
+ throw new \RuntimeException('Description cannot be empty');
+ }
+
+ break;
+ case 'themename':
+ if($value === null || trim($value) === '') {
+ throw new \RuntimeException('Theme Name cannot be empty');
+ }
+
+ break;
+ case 'developer':
+ if ($value === null || trim($value) === '') {
+ throw new \RuntimeException('Developer\'s Name cannot be empty');
+ }
+
+ break;
+
+ case 'githubid':
+ // GitHubID can be blank, so nothing here
+ break;
+
+ case 'email':
+ if (!preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $value)) {
+ throw new \RuntimeException('Not a valid email address');
+ }
+
+ break;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param string $word
+ * @return bool
+ */
+ public function isReservedWord(string $word): bool
+ {
+ return in_array($word, $this->reserved_keywords, true);
+ }
+}
diff --git a/plugins/devtools/cli/NewBlueprintCommand.php b/plugins/devtools/cli/NewBlueprintCommand.php
new file mode 100644
index 0000000..f1dee47
--- /dev/null
+++ b/plugins/devtools/cli/NewBlueprintCommand.php
@@ -0,0 +1,88 @@
+setName('new-blueprint')
+ ->setAliases(['newblueprint','blueprint'])
+ ->addOption(
+ 'bpname',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name of your new Grav theme'
+ )
+ ->addOption(
+ 'name',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name of your new Grav theme'
+ )
+ ->addOption(
+ 'template',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name/username of the developer'
+ )
+ ->setDescription('Create a blueprint that extend the default.yaml blueprint files')
+ ->setHelp('The new-blueprint command creates a new blueprint file.');
+ }
+
+ /**
+ * @return int
+ */
+ protected function serve(): int
+ {
+ $this->init();
+
+ $input = $this->getInput();
+ $io = $this->getIO();
+
+ $this->component['type'] = 'blueprint';
+ $this->component['template'] = 'modular';
+ $this->component['version'] = '0.1.0';
+ $this->component['themename'] = 'bonjour';
+
+ $this->options = [
+ 'name' => $input->getOption('name'),
+ 'bpname' => $input->getOption('bpname'),
+ 'template' => $input->getOption('template'),
+
+ ];
+
+ $this->validateOptions();
+
+ $this->component = array_replace($this->component, $this->options);
+
+ if (!$this->options['template']) {
+ $question = new ChoiceQuestion('Please choose a template type', ['newtab', 'append']);
+
+ $this->component['template'] = $io->askQuestion($question);
+ }
+ if (!$this->options['bpname']) {
+ $question = new Question('Enter Blueprint Name');
+
+ $this->component['bpname'] = $io->askQuestion($question);
+ }
+
+ $this->createComponent();
+
+ return 0;
+ }
+}
diff --git a/plugins/devtools/cli/NewPluginCommand.php b/plugins/devtools/cli/NewPluginCommand.php
new file mode 100644
index 0000000..f57ee23
--- /dev/null
+++ b/plugins/devtools/cli/NewPluginCommand.php
@@ -0,0 +1,146 @@
+setName('new-plugin')
+ ->setAliases(['newplugin'])
+ ->addOption(
+ 'name',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name of your new Grav plugin'
+ )
+ ->addOption(
+ 'desc',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'A description of your new Grav plugin'
+ )
+ ->addOption(
+ 'dev',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name/username of the developer'
+ )
+ ->addOption(
+ 'github',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The developer\'s GitHub ID'
+ )
+ ->addOption(
+ 'email',
+ 'e',
+ InputOption::VALUE_OPTIONAL,
+ 'The developer\'s email'
+ )
+ ->addOption(
+ 'offline',
+ 'o',
+ InputOption::VALUE_NONE,
+ 'Skip online name collision check'
+ )
+ ->setDescription('Creates a new Grav plugin with the basic required files')
+ ->setHelp('The new-plugin command creates a new Grav instance and performs the creation of a plugin.');
+ }
+
+ /**
+ * @return int
+ */
+ protected function serve(): int
+ {
+ $this->init();
+
+ $input = $this->getInput();
+ $io = $this->getIO();
+
+ $this->component['type'] = 'plugin';
+ $this->component['template'] = 'blank';
+ $this->component['version'] = '0.1.0';
+
+ $this->options = [
+ 'name' => $input->getOption('name'),
+ 'description' => $input->getOption('desc'),
+ 'author' => [
+ 'name' => $input->getOption('dev'),
+ 'email' => $input->getOption('email'),
+ 'githubid' => $input->getOption('github')
+ ],
+ 'offline' => $input->getOption('offline'),
+ ];
+
+ $this->validateOptions();
+
+ $this->component = array_replace($this->component, $this->options);
+
+ if (!$this->options['name']) {
+ $question = new Question('Enter Plugin Name');
+ $question->setValidator(function ($value) {
+ return $this->validate('name', $value);
+ });
+
+ $this->component['name'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['description']) {
+ $question = new Question('Enter Plugin Description');
+ $question->setValidator(function ($value) {
+ return $this->validate('description', $value);
+ });
+
+ $this->component['description'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['author']['name']) {
+ $question = new Question('Enter Developer Name');
+ $question->setValidator(function ($value) {
+ return $this->validate('developer', $value);
+ });
+
+ $this->component['author']['name'] = $io->askQuestion($question);
+ }
+
+
+ if (!$this->options['author']['githubid']) {
+ $question = new Question('Enter GitHub ID (can be blank)');
+ $question->setValidator(function ($value) {
+ return $this->validate('githubid', $value);
+ });
+
+ $this->component['author']['githubid'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['author']['email']) {
+ $question = new Question('Enter Developer Email');
+ $question->setValidator(function ($value) {
+ return $this->validate('email', $value);
+ });
+
+ $this->component['author']['email'] = $io->askQuestion($question);
+ }
+
+ $this->component['template'] = 'blank';
+
+ $this->createComponent();
+
+ return 0;
+ }
+
+}
diff --git a/plugins/devtools/cli/NewThemeCommand.php b/plugins/devtools/cli/NewThemeCommand.php
new file mode 100644
index 0000000..fb479ef
--- /dev/null
+++ b/plugins/devtools/cli/NewThemeCommand.php
@@ -0,0 +1,171 @@
+setName('new-theme')
+ ->setAliases(['newtheme'])
+ ->addOption(
+ 'name',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name of your new Grav theme'
+ )
+ ->addOption(
+ 'desc',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'A description of your new Grav theme'
+ )
+ ->addOption(
+ 'dev',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The name/username of the developer'
+ )
+ ->addOption(
+ 'github',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The developer\'s GitHub ID'
+ )
+ ->addOption(
+ 'email',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'The developer\'s email'
+ )
+ ->addOption(
+ 'offline',
+ 'o',
+ InputOption::VALUE_NONE,
+ 'Skip online name collision check'
+ )
+ ->setDescription('Creates a new Grav theme with the basic required files')
+ ->setHelp('The new-theme command creates a new Grav instance and performs the creation of a theme.');
+ }
+
+ /**
+ * @return int
+ */
+ protected function serve(): int
+ {
+ $this->init();
+
+ $input = $this->getInput();
+ $io = $this->getIO();
+
+ $this->component['type'] = 'theme';
+ $this->component['template'] = 'blank';
+ $this->component['version'] = '0.1.0';
+
+ $this->options = [
+ 'name' => $input->getOption('name'),
+ 'description' => $input->getOption('desc'),
+ 'author' => [
+ 'name' => $input->getOption('dev'),
+ 'email' => $input->getOption('email'),
+ 'githubid' => $input->getOption('github'),
+ ],
+ 'offline' => $input->getOption('offline'),
+ ];
+
+ $this->validateOptions();
+
+ $this->component = array_replace($this->component, $this->options);
+
+ if (!$this->options['name']) {
+ $question = new Question('Enter Theme Name');
+ $question->setValidator(function ($value) {
+ return $this->validate('name', $value);
+ });
+
+ $this->component['name'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['description']) {
+ $question = new Question('Enter Theme Description');
+ $question->setValidator(function ($value) {
+ return $this->validate('description', $value);
+ });
+
+ $this->component['description'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['author']['name']) {
+ $question = new Question('Enter Developer Name');
+ $question->setValidator(function ($value) {
+ return $this->validate('developer', $value);
+ });
+
+ $this->component['author']['name'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['author']['githubid']) {
+ $question = new Question('Enter GitHub ID (can be blank)');
+ $question->setValidator(function ($value) {
+ return $this->validate('githubid', $value);
+ });
+
+ $this->component['author']['githubid'] = $io->askQuestion($question);
+ }
+
+ if (!$this->options['author']['email']) {
+ $question = new Question('Enter Developer Email');
+ $question->setValidator(function ($value) {
+ return $this->validate('email', $value);
+ });
+
+ $this->component['author']['email'] = $io->askQuestion($question);
+ }
+
+ $question = new ChoiceQuestion(
+ 'Please choose an option',
+ ['pure-blank' => 'Basic Theme using Pure.css', 'tailwind' => 'Basic Theme using tailwind.css', 'inheritance' => 'Inherit from another theme', 'copy' => 'Copy another theme']
+ );
+ $this->component['template'] = $io->askQuestion($question);
+
+ if ($this->component['template'] === 'inheritance') {
+ $themes = $this->gpm->getInstalledThemes();
+ $installedThemes = [];
+ foreach ($themes as $key => $theme) {
+ $installedThemes[] = $key;
+ }
+
+ $question = new ChoiceQuestion('Please choose a theme to extend', $installedThemes);
+ $this->component['extends'] = $io->askQuestion($question);
+ } elseif ($this->component['template'] === 'copy') {
+ $themes = $this->gpm->getInstalledThemes();
+ $installedThemes = [];
+ foreach ($themes as $key => $theme) {
+ $installedThemes[] = $key;
+ }
+
+ $question = new ChoiceQuestion(
+ 'Please choose a theme to copy',
+ $installedThemes
+ );
+ $this->component['copy'] = $io->askQuestion($question);
+ }
+ $this->createComponent();
+
+ return 0;
+ }
+}
diff --git a/plugins/devtools/components/blueprint/append/blueprint.yaml.twig b/plugins/devtools/components/blueprint/append/blueprint.yaml.twig
new file mode 100644
index 0000000..8db427f
--- /dev/null
+++ b/plugins/devtools/components/blueprint/append/blueprint.yaml.twig
@@ -0,0 +1,21 @@
+title: {{ component.bpname }}
+extends@:
+ type: default
+ context: blueprints://pages
+
+form:
+ fields:
+ tabs:
+ type: tabs
+ active: 1
+
+ fields:
+ content:
+ fields:
+ header.an_example_text_field:
+ type: text
+ label: Add a number
+ default: 5
+ validate:
+ required: true
+ type: int
\ No newline at end of file
diff --git a/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig b/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig
new file mode 100644
index 0000000..d110556
--- /dev/null
+++ b/plugins/devtools/components/blueprint/newtab/blueprint.yaml.twig
@@ -0,0 +1,15 @@
+title: Item
+extends@:
+ type: default
+ context: blueprints://pages
+
+form:
+ fields:
+ tabs:
+ fields:
+ blog:
+ type: tab
+ title: {{ component.bpname }}
+
+ fields:
+ header.mytextfield:
\ No newline at end of file
diff --git a/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig b/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig
new file mode 100644
index 0000000..973fb27
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/CHANGELOG.md.twig
@@ -0,0 +1,5 @@
+# v0.1.0
+## {{ "now"|date("m/d/Y") }}
+
+1. [](#new)
+ * ChangeLog started...
diff --git a/plugins/devtools/components/plugin/blank/LICENSE.twig b/plugins/devtools/components/plugin/blank/LICENSE.twig
new file mode 100644
index 0000000..6f88097
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/LICENSE.twig
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) {{ "now"|date("Y") }} {{ component.author.name }}
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/devtools/components/plugin/blank/README.md.twig b/plugins/devtools/components/plugin/blank/README.md.twig
new file mode 100644
index 0000000..e9cae52
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/README.md.twig
@@ -0,0 +1,59 @@
+{% set component_title = (component.name|titleize) %}
+{% set component_hyphenated = (component.name|hyphenize) %}
+{% set developer_hyphenated = (component.author.githubid|hyphenize) %}
+# {{ component_title }} Plugin
+
+**This README.md file should be modified to describe the features, installation, configuration, and general usage of the plugin.**
+
+The **{{ component_title }}** Plugin is an extension for [Grav CMS](http://github.com/getgrav/grav). {{ component.description }}
+
+## Installation
+
+Installing the {{ component_title }} plugin can be done in one of three ways: The GPM (Grav Package Manager) installation method lets you quickly install the plugin with a simple terminal command, the manual method lets you do so via a zip file, and the admin method lets you do so via the Admin Plugin.
+
+### GPM Installation (Preferred)
+
+To install the plugin via the [GPM](http://learn.getgrav.org/advanced/grav-gpm), through your system's terminal (also called the command line), navigate to the root of your Grav-installation, and enter:
+
+ bin/gpm install {{ component_hyphenated }}
+
+This will install the {{ component_title }} plugin into your `/user/plugins`-directory within Grav. Its files can be found under `/your/site/grav/user/plugins/{{ component_hyphenated }}`.
+
+### Manual Installation
+
+To install the plugin manually, download the zip-version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `{{ component_hyphenated }}`. You can find these files on [GitHub](https://github.com/{{ developer_hyphenated }}/grav-plugin-{{ component_hyphenated }}) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras).
+
+You should now have all the plugin files under
+
+ /your/site/grav/user/plugins/{{ component_hyphenated }}
+
+> NOTE: This plugin is a modular component for Grav which may require other plugins to operate, please see its [blueprints.yaml-file on GitHub](https://github.com/{{ developer_hyphenated }}/grav-plugin-{{ component_hyphenated }}/blob/master/blueprints.yaml).
+
+### Admin Plugin
+
+If you use the Admin Plugin, you can install the plugin directly by browsing the `Plugins`-menu and clicking on the `Add` button.
+
+## Configuration
+
+Before configuring this plugin, you should copy the `user/plugins/{{ component_hyphenated }}/{{ component_hyphenated }}.yaml` to `user/config/plugins/{{ component_hyphenated }}.yaml` and only edit that copy.
+
+Here is the default configuration and an explanation of available options:
+
+```yaml
+enabled: true
+```
+
+Note that if you use the Admin Plugin, a file with your configuration named {{component_hyphenated}}.yaml will be saved in the `user/config/plugins/`-folder once the configuration is saved in the Admin.
+
+## Usage
+
+**Describe how to use the plugin.**
+
+## Credits
+
+**Did you incorporate third-party code? Want to thank somebody?**
+
+## To Do
+
+- [ ] Future plans, if any
+
diff --git a/plugins/devtools/components/plugin/blank/blueprints.yaml.twig b/plugins/devtools/components/plugin/blank/blueprints.yaml.twig
new file mode 100644
index 0000000..bd0defd
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/blueprints.yaml.twig
@@ -0,0 +1,37 @@
+{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%}
+name: {{ component.name|titleize }}
+slug: {{ component.name|hyphenize }}
+type: plugin
+version: 0.1.0
+description: {{ component.description }}
+icon: plug
+author:
+ name: {{ component.author.name }}
+ email: {{ component.author.email }}
+homepage: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}
+demo: http://demo.yoursite.com
+keywords: grav, plugin, etc
+bugs: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}/issues
+docs: https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}/blob/develop/README.md
+license: MIT
+
+dependencies:
+ - { name: grav, version: '>=1.6.0' }
+
+form:
+ validation: loose
+ fields:
+ enabled:
+ type: toggle
+ label: PLUGIN_ADMIN.PLUGIN_STATUS
+ highlight: 1
+ default: 0
+ options:
+ 1: PLUGIN_ADMIN.ENABLED
+ 0: PLUGIN_ADMIN.DISABLED
+ validate:
+ type: bool
+ text_var:
+ type: text
+ label: PLUGIN_{{ component.name|underscorize|upper }}.TEXT_VARIABLE
+ help: PLUGIN_{{ component.name|underscorize|upper }}.TEXT_VARIABLE_HELP
diff --git a/plugins/devtools/components/plugin/blank/classes/.gitkeep b/plugins/devtools/components/plugin/blank/classes/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/plugin/blank/composer.json.twig b/plugins/devtools/components/plugin/blank/composer.json.twig
new file mode 100644
index 0000000..14cfe4f
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/composer.json.twig
@@ -0,0 +1,30 @@
+{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%}
+{
+ "name": "{{ githubid|lower }}/{{ component.name|hyphenize }}",
+ "type": "grav-plugin",
+ "description": "{{ component.description }}",
+ "keywords": ["plugin"],
+ "homepage": "https://github.com/{{ githubid }}/grav-plugin-{{ component.name|hyphenize }}",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "{{ component.author.name }}",
+ "email": "{{ component.author.email }}",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=7.1.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "Grav\\Plugin\\{{ component.name|camelize }}\\": "classes/"
+ },
+ "classmap": ["{{ component.name|hyphenize }}.php"]
+ },
+ "config": {
+ "platform": {
+ "php": "7.1.3"
+ }
+ }
+}
diff --git a/plugins/devtools/components/plugin/blank/languages.yaml.twig b/plugins/devtools/components/plugin/blank/languages.yaml.twig
new file mode 100644
index 0000000..855cd0e
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/languages.yaml.twig
@@ -0,0 +1,4 @@
+en:
+ PLUGIN_{{ component.name|underscorize|upper }}:
+ TEXT_VARIABLE: Text Variable
+ TEXT_VARIABLE_HELP: Text to add to the top of a page
diff --git a/plugins/devtools/components/plugin/blank/plugin.php.twig b/plugins/devtools/components/plugin/blank/plugin.php.twig
new file mode 100644
index 0000000..0daecc4
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/plugin.php.twig
@@ -0,0 +1,59 @@
+ [
+ // Uncomment following line when plugin requires Grav < 1.7
+ // ['autoload', 100000],
+ ['onPluginsInitialized', 0]
+ ]
+ ];
+ }
+
+ /**
+ * Composer autoload
+ *
+ * @return ClassLoader
+ */
+ public function autoload(): ClassLoader
+ {
+ return require __DIR__ . '/vendor/autoload.php';
+ }
+
+ /**
+ * Initialize the plugin
+ */
+ public function onPluginsInitialized(): void
+ {
+ // Don't proceed if we are in the admin plugin
+ if ($this->isAdmin()) {
+ return;
+ }
+
+ // Enable the main events we are interested in
+ $this->enable([
+ // Put your main events here
+ ]);
+ }
+}
diff --git a/plugins/devtools/components/plugin/blank/plugin.yaml.twig b/plugins/devtools/components/plugin/blank/plugin.yaml.twig
new file mode 100644
index 0000000..55e2f2e
--- /dev/null
+++ b/plugins/devtools/components/plugin/blank/plugin.yaml.twig
@@ -0,0 +1,2 @@
+enabled: true
+text_var: Custom Text added by the **{{ component.name|titleize }}** plugin (disable plugin to remove)
diff --git a/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig b/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig
new file mode 100644
index 0000000..973fb27
--- /dev/null
+++ b/plugins/devtools/components/theme/inheritance/CHANGELOG.md.twig
@@ -0,0 +1,5 @@
+# v0.1.0
+## {{ "now"|date("m/d/Y") }}
+
+1. [](#new)
+ * ChangeLog started...
diff --git a/plugins/devtools/components/theme/inheritance/LICENSE.twig b/plugins/devtools/components/theme/inheritance/LICENSE.twig
new file mode 100644
index 0000000..6f88097
--- /dev/null
+++ b/plugins/devtools/components/theme/inheritance/LICENSE.twig
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) {{ "now"|date("Y") }} {{ component.author.name }}
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/devtools/components/theme/inheritance/README.md.twig b/plugins/devtools/components/theme/inheritance/README.md.twig
new file mode 100644
index 0000000..ee5c0b7
--- /dev/null
+++ b/plugins/devtools/components/theme/inheritance/README.md.twig
@@ -0,0 +1,7 @@
+# {{ component.name|titleize }} Theme
+
+The **{{ component.name|titleize }}** Theme is for [Grav CMS](http://github.com/getgrav/grav). This README.md file should be modified to describe the features, installation, configuration, and general usage of this theme.
+
+## Description
+
+{{ component.description }}
diff --git a/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig b/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig
new file mode 100644
index 0000000..91700a3
--- /dev/null
+++ b/plugins/devtools/components/theme/inheritance/blueprints.yaml.twig
@@ -0,0 +1,19 @@
+{% set githubid = component.author.githubid ?: component.author.name|hyphenize -%}
+name: {{ component.name|titleize }}
+slug: {{ component.name|hyphenize }}
+type: theme
+version: 0.1.0
+description: {{ component.description }}
+icon: rebel
+author:
+ name: {{ component.author.name }}
+ email: {{ component.author.email }}
+homepage: https://github.com/{{ githubid }}/grav-theme-{{ component.name|hyphenize }}
+demo: http://demo.yoursite.com
+keywords: grav, theme, etc
+bugs: https://github.com/{{ githubid }}/grav-theme-{{ component.name|hyphenize }}/issues
+readme: https://github.com/{{ githubid }}/grav-theme-{{ component.name|hyphenize }}/blob/develop/README.md
+license: MIT
+
+dependencies:
+ - { name: grav, version: '>=1.6.0' }
diff --git a/plugins/devtools/components/theme/inheritance/css/.gitkeep b/plugins/devtools/components/theme/inheritance/css/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/inheritance/js/.gitkeep b/plugins/devtools/components/theme/inheritance/js/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/inheritance/screenshot.jpg b/plugins/devtools/components/theme/inheritance/screenshot.jpg
new file mode 100644
index 0000000..5205ca5
Binary files /dev/null and b/plugins/devtools/components/theme/inheritance/screenshot.jpg differ
diff --git a/plugins/devtools/components/theme/inheritance/templates/.gitkeep b/plugins/devtools/components/theme/inheritance/templates/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/inheritance/theme.php.twig b/plugins/devtools/components/theme/inheritance/theme.php.twig
new file mode 100644
index 0000000..c05016c
--- /dev/null
+++ b/plugins/devtools/components/theme/inheritance/theme.php.twig
@@ -0,0 +1,9 @@
+=1.6.0' }
+
+form:
+ validation: loose
+ fields:
+ dropdown.enabled:
+ type: toggle
+ label: Dropdown in Menu
+ highlight: 1
+ default: 1
+ options:
+ 1: PLUGIN_ADMIN.ENABLED
+ 0: PLUGIN_ADMIN.DISABLED
+ validate:
+ type: bool
diff --git a/plugins/devtools/components/theme/pure-blank/css/custom.css b/plugins/devtools/components/theme/pure-blank/css/custom.css
new file mode 100644
index 0000000..e796328
--- /dev/null
+++ b/plugins/devtools/components/theme/pure-blank/css/custom.css
@@ -0,0 +1,175 @@
+/* Core Stuff */
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+body {
+ font-size: 1rem;
+ line-height: 1.7;
+ color: #606d6e;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: #454B4D;
+}
+
+a {
+ color: #1F8CD6;
+ text-decoration: none;
+}
+
+a:hover {
+ color: #175E91;
+}
+
+pre {
+ background: #F0F0F0;
+ margin: 1rem 0;
+ border-radius: 2px;
+}
+
+blockquote {
+ border-left: 10px solid #eee;
+ margin: 0;
+ padding: 0 2rem;
+}
+
+/* Utility Classes */
+.wrapper {
+ margin: 0 3rem;
+}
+
+.padding {
+ padding: 3rem 1rem;
+}
+
+.left {
+ float: left;
+}
+
+.right {
+ float: right
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.text-left {
+ text-align: left;
+}
+
+/* Content Styling */
+.header .padding {
+ padding: 1rem 0;
+}
+
+.header {
+ background-color: #1F8DD6;
+ color: #eee;
+}
+
+.header a {
+ color: #fff;
+}
+
+.header .logo {
+ font-size: 1.7rem;
+ text-transform: uppercase;
+}
+
+.footer {
+ background-color: #eee;
+}
+
+/* Menu Settings */
+.main-nav ul {
+ text-align: center;
+ letter-spacing: -1em;
+ margin: 0;
+ padding: 0;
+}
+
+.main-nav ul li {
+ display: inline-block;
+ letter-spacing: normal;
+}
+
+.main-nav ul li a {
+ position: relative;
+ display: block;
+ line-height: 45px;
+ color: #fff;
+ padding: 0 20px;
+ white-space: nowrap;
+}
+
+.main-nav > ul > li > a {
+ border-radius: 2px;
+}
+
+/*Active dropdown nav item */
+.main-nav ul li:hover > a {
+ background-color: #175E91;
+}
+
+/* Selected Dropdown nav item */
+.main-nav ul li.selected > a {
+ background-color: #fff;
+ color: #175E91;
+}
+
+/* Dropdown CSS */
+.main-nav ul li {position: relative;}
+
+.main-nav ul li ul {
+ position: absolute;
+ background-color: #1F8DD6;
+ min-width: 100%;
+ text-align: left;
+ z-index: 999;
+
+ display: none;
+}
+.main-nav ul li ul li {
+ display: block;
+}
+
+/* Dropdown CSS */
+.main-nav ul li ul ul {
+ left: 100%;
+ top: 0;
+}
+
+/* Active on Hover */
+.main-nav li:hover > ul {
+ display: block;
+}
+
+/* Child Indicator */
+.main-nav .has-children > a {
+ padding-right: 30px;
+}
+.main-nav .has-children > a:after {
+ font-family: FontAwesome;
+ content: '\f107';
+ position: absolute;
+ display: inline-block;
+ right: 8px;
+ top: 0;
+}
+
+.main-nav .has-children .has-children > a:after {
+ content: '\f105';
+}
diff --git a/plugins/devtools/components/theme/pure-blank/fonts/.gitkeep b/plugins/devtools/components/theme/pure-blank/fonts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/pure-blank/images/logo.png b/plugins/devtools/components/theme/pure-blank/images/logo.png
new file mode 100644
index 0000000..64be1a9
Binary files /dev/null and b/plugins/devtools/components/theme/pure-blank/images/logo.png differ
diff --git a/plugins/devtools/components/theme/pure-blank/js/.gitkeep b/plugins/devtools/components/theme/pure-blank/js/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/pure-blank/screenshot.jpg b/plugins/devtools/components/theme/pure-blank/screenshot.jpg
new file mode 100644
index 0000000..5205ca5
Binary files /dev/null and b/plugins/devtools/components/theme/pure-blank/screenshot.jpg differ
diff --git a/plugins/devtools/components/theme/pure-blank/templates/default.html.twig b/plugins/devtools/components/theme/pure-blank/templates/default.html.twig
new file mode 100644
index 0000000..1e97738
--- /dev/null
+++ b/plugins/devtools/components/theme/pure-blank/templates/default.html.twig
@@ -0,0 +1,5 @@
+{% extends 'partials/base.html.twig' %}
+
+{% block content %}
+ {{ page.content|raw }}
+{% endblock %}
diff --git a/plugins/devtools/components/theme/pure-blank/templates/error.html.twig b/plugins/devtools/components/theme/pure-blank/templates/error.html.twig
new file mode 100644
index 0000000..c945464
--- /dev/null
+++ b/plugins/devtools/components/theme/pure-blank/templates/error.html.twig
@@ -0,0 +1,8 @@
+{% extends 'partials/base.html.twig' %}
+
+{% block content %}
+
+
Error!
+ {{ page.content|raw }}
+
+{% endblock %}
diff --git a/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig b/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig
new file mode 100644
index 0000000..c4e7e60
--- /dev/null
+++ b/plugins/devtools/components/theme/pure-blank/templates/partials/base.html.twig
@@ -0,0 +1,70 @@
+
+
+
+{% block head %}
+
+ {% if header.title %}{{ header.title|e }} | {% endif %}{{ site.title|e }}
+
+
+
+ {% include 'partials/metadata.html.twig' %}
+
+
+
+{% endblock head %}
+
+{% block stylesheets %}
+ {% do assets.addCss('https://unpkg.com/purecss@1.0.0/build/pure-min.css', 100) %}
+ {% do assets.addCss('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', 99) %}
+ {% do assets.addCss('theme://css/custom.css', 98) %}
+{% endblock %}
+
+{% block javascripts %}
+ {% do assets.addJs('jquery', 100) %}
+{% endblock %}
+
+{% block assets deferred %}
+ {{ assets.css()|raw }}
+ {{ assets.js()|raw }}
+{% endblock %}
+
+
+
+{% block header %}
+
+{% endblock %}
+
+{% block body %}
+
+
+ {% block content %}{% endblock %}
+
+
+{% endblock %}
+
+{% block footer %}
+
+{% endblock %}
+
+{% block bottom %}
+ {{ assets.js('bottom')|raw }}
+{% endblock %}
+
+
+
diff --git a/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig b/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig
new file mode 100644
index 0000000..28df6aa
--- /dev/null
+++ b/plugins/devtools/components/theme/pure-blank/templates/partials/navigation.html.twig
@@ -0,0 +1,47 @@
+{% macro loop(page) %}
+ {% for p in page.children.visible %}
+ {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %}
+ {% if p.children.visible.count > 0 %}
+
+
+ {% if p.header.icon %}{% endif %}
+ {{ p.menu|e }}
+
+
+
+ {% else %}
+
+
+ {% if p.header.icon %}{% endif %}
+ {{ p.menu|e }}
+
+
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+
+
diff --git a/plugins/devtools/components/theme/pure-blank/theme.php.twig b/plugins/devtools/components/theme/pure-blank/theme.php.twig
new file mode 100644
index 0000000..332b164
--- /dev/null
+++ b/plugins/devtools/components/theme/pure-blank/theme.php.twig
@@ -0,0 +1,9 @@
+=1.6.0' }
+
+form:
+ validation: loose
+ fields:
+ dropdown.enabled:
+ type: toggle
+ label: Dropdown in Menu
+ highlight: 1
+ default: 1
+ options:
+ 1: PLUGIN_ADMIN.ENABLED
+ 0: PLUGIN_ADMIN.DISABLED
+ validate:
+ type: bool
+ production:
+ type: toggle
+ label: Production Mode
+ highlight: 1
+ default: 1
+ options:
+ 1: PLUGIN_ADMIN.ENABLED
+ 0: PLUGIN_ADMIN.DISABLED
+ validate:
+ type: bool
diff --git a/plugins/devtools/components/theme/tailwind/css/site.css b/plugins/devtools/components/theme/tailwind/css/site.css
new file mode 100644
index 0000000..f578962
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/css/site.css
@@ -0,0 +1,5 @@
+/*@import 'yourcssfile.css';*/
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/plugins/devtools/components/theme/tailwind/fonts/.gitkeep b/plugins/devtools/components/theme/tailwind/fonts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/tailwind/images/logo.png b/plugins/devtools/components/theme/tailwind/images/logo.png
new file mode 100644
index 0000000..64be1a9
Binary files /dev/null and b/plugins/devtools/components/theme/tailwind/images/logo.png differ
diff --git a/plugins/devtools/components/theme/tailwind/js/.gitkeep b/plugins/devtools/components/theme/tailwind/js/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig b/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig
new file mode 100644
index 0000000..2b48ba2
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/languages/en.yaml.twig
@@ -0,0 +1,2 @@
+THEME_{{ component.name|hyphenize|replace({'-': '_'})|upper }}:
+ ERROR: 'Error!'
diff --git a/plugins/devtools/components/theme/tailwind/package.json.twig b/plugins/devtools/components/theme/tailwind/package.json.twig
new file mode 100644
index 0000000..902fac2
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/package.json.twig
@@ -0,0 +1,30 @@
+{
+ "name": "{{ component.name }}",
+ "repository": "",
+ "private": true,
+ "version": "0.1.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "postcss css/site.css -o dist/css/site.css --verbose",
+ "watch": "postcss css/site.css -o dist/css/site.css --watch --verbose",
+ "prod" : "postcss css/site.css -o dist/css/site.min.css --env production --verbose"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "alpinejs": "^2.8.2",
+ "tailwindcss": "^2.1.2",
+ "@tailwindcss/forms": "^0.3.2",
+ "@tailwindcss/typography": "^0.4.0",
+ "tailwindcss-debug-screens": "^2.0.0",
+ "autoprefixer": "^10.2.5",
+ "precss": "^4.0.0",
+ "cssnano": "^4.1.11",
+ "postcss": "^8.2.9",
+ "postcss-cli": "^8.3.1",
+ "postcss-import": "^14.0.1",
+ "postcss-nested": "^5.0.5",
+ "postcss-hexrgba": "^2.0.1",
+ "postcss-color-function": "^4.1.0"
+ }
+}
diff --git a/plugins/devtools/components/theme/tailwind/postcss.config.js b/plugins/devtools/components/theme/tailwind/postcss.config.js
new file mode 100644
index 0000000..655e577
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/postcss.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ plugins: {
+ 'postcss-import': {},
+ 'precss': {},
+ 'tailwindcss': {},
+ 'postcss-nested': {},
+ 'autoprefixer': {},
+ ...process.env.NODE_ENV === 'production'
+ ? {'cssnano': {}} : {}
+ },
+}
diff --git a/plugins/devtools/components/theme/tailwind/screenshot.jpg b/plugins/devtools/components/theme/tailwind/screenshot.jpg
new file mode 100644
index 0000000..5205ca5
Binary files /dev/null and b/plugins/devtools/components/theme/tailwind/screenshot.jpg differ
diff --git a/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig b/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig
new file mode 100644
index 0000000..27a4484
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/tailwind.config.js.twig
@@ -0,0 +1,70 @@
+const { colors } = require('tailwindcss/defaultTheme');
+
+module.exports = {
+ purge: [
+ '../../config/**/*.yaml',
+ '../../pages/**/*.md',
+ './blueprints/**/*.yaml',
+ './js/**/*.js',
+ './templates/**/*.twig',
+ './{{ component.name|hyphenize }}.yaml',
+ './{{ component.name|hyphenize }}.php'
+ ],
+ darkMode: 'class', //false or 'media' or 'class'
+ theme: {
+ extend: {
+ screens: {
+ sm: '640px',
+ md: '768px',
+ lg: '1024px',
+ xl: '1280px',
+ '2xl': '1536px'
+ }
+ },
+ colors: {
+ 'primary': {
+ 'lighter': colors.yellow['300'],
+ DEFAULT: colors.yellow['400'],
+ 'darker' : colors.yellow['500'],
+ },
+ black: colors.black,
+ white: colors.white,
+ red: colors.red,
+ green: colors.green,
+ blue: colors.blue,
+ orange: colors.orange,
+ indigo: colors.indigo,
+ transparent: 'transparent',
+ 'inherit': 'inherit',
+ },
+ typography: (theme) => ({
+ DEFAULT: {
+ css: {
+ color: 'inherit',
+ lineHeight: 'inherit',
+ maxWidth: 'inherit',
+ a: {
+ transition: 'all 500ms',
+ color: theme('colors.primary.DEFAULT'),
+ '&:hover': {
+ color: theme('colors.primary.darker')
+ },
+ textDecoration: 'none'
+ },
+ strong: {
+ color: 'inherit'
+ },
+ }
+ }
+ }),
+ },
+ variants: {
+ extend: {},
+ },
+ plugins: [
+ require('@tailwindcss/typography'),
+ require('@tailwindcss/forms'),
+ require('tailwindcss-debug-screens'),
+ ],
+ important: false,
+}
diff --git a/plugins/devtools/components/theme/tailwind/templates/default.html.twig b/plugins/devtools/components/theme/tailwind/templates/default.html.twig
new file mode 100644
index 0000000..1e97738
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/templates/default.html.twig
@@ -0,0 +1,5 @@
+{% extends 'partials/base.html.twig' %}
+
+{% block content %}
+ {{ page.content|raw }}
+{% endblock %}
diff --git a/plugins/devtools/components/theme/tailwind/templates/error.html.twig b/plugins/devtools/components/theme/tailwind/templates/error.html.twig
new file mode 100644
index 0000000..c25efaa
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/templates/error.html.twig
@@ -0,0 +1,8 @@
+{% extends 'partials/base.html.twig' %}
+
+{% block content %}
+
+
{{ 'THEME_TAILWIND.ERROR'|t }}
+ {{ page.content|raw }}
+
+{% endblock %}
diff --git a/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig b/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig
new file mode 100644
index 0000000..04509fd
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/templates/partials/base.html.twig
@@ -0,0 +1,67 @@
+{% set extension = config.theme.production ? '.min' : '' %}
+
+
+
+ {% block head %}
+
+ {% if header.title %}{{ header.title|e }} | {% endif %}{{ site.title|e }}
+
+
+
+ {% include 'partials/metadata.html.twig' %}
+
+
+
+ {% endblock head %}
+
+ {% block stylesheets %}
+ {% do assets.addCss('theme://dist/css/app' ~ extension ~ '.css', 98) %}
+ {% endblock %}
+
+ {% block javascripts %}
+ {% endblock %}
+
+ {% block assets deferred %}
+ {{ assets.css()|raw }}
+ {{ assets.js()|raw }}
+ {% endblock %}
+
+
+
+{% block header %}
+
+{% endblock %}
+
+{% block body %}
+
+
+ {% block content %}{% endblock %}
+
+
+{% endblock %}
+
+{% block footer %}
+
+{% endblock %}
+
+{% block bottom %}
+ {{ assets.js('bottom')|raw }}
+{% endblock %}
+
+
+
diff --git a/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig b/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig
new file mode 100644
index 0000000..28df6aa
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/templates/partials/navigation.html.twig
@@ -0,0 +1,47 @@
+{% macro loop(page) %}
+ {% for p in page.children.visible %}
+ {% set current_page = (p.active or p.activeChild) ? 'selected' : '' %}
+ {% if p.children.visible.count > 0 %}
+
+
+ {% if p.header.icon %}{% endif %}
+ {{ p.menu|e }}
+
+
+
+ {% else %}
+
+
+ {% if p.header.icon %}{% endif %}
+ {{ p.menu|e }}
+
+
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+
+
diff --git a/plugins/devtools/components/theme/tailwind/theme.php.twig b/plugins/devtools/components/theme/tailwind/theme.php.twig
new file mode 100644
index 0000000..332b164
--- /dev/null
+++ b/plugins/devtools/components/theme/tailwind/theme.php.twig
@@ -0,0 +1,9 @@
+