diff --git a/app/configs/options.json b/app/configs/options.json new file mode 100644 index 0000000..5a4d599 --- /dev/null +++ b/app/configs/options.json @@ -0,0 +1,204 @@ +{ + "basic.site.title": { + "namespace": "basic.site.title", + "public": true, + "title": "Site title", + "desc": "The site title", + "type": "string", + "default": "Theme Sakura" + }, + "basic.site.logo": { + "namespace": "basic.site.logo", + "public": true, + "title": "Site logo", + "desc": "The site's Logo image, will display on navigation bar.", + "type": "mediaPicker", + "default": [ + { + "id": 0, + "url": "https://v3.vuejs.org/logo.png" + } + ], + "binds": { + "title": "Select image for site logo.", + "button": "Use this image", + "type": "image", + "multiple": false + } + }, + "social.github": { + "namespace": "social.github", + "public": true, + "title": "Github username", + "desc": "Your Github username", + "type": "string", + "default": "" + }, + "thirdParty.reCaptcha.enable": { + "namespace": "thirdParty.reCaptcha.enable", + "public": true, + "title": "Enable reCAPTCHA", + "desc": "Use reCAPTCHA for anti-spam check.", + "type": "switcher", + "default": false, + "binds": { + "positiveLabel": "Enabled", + "negativeLabel": "Disabled", + "disabled": false + } + }, + "thirdParty.reCaptcha.version": { + "namespace": "thirdParty.reCaptcha.version", + "public": true, + "title": "reCAPTCHA version", + "desc": "Register your reCAPTCHA app 'here', and choose a version.", + "type": "choose", + "default": null, + "binds": { + "options": [ + { + "label": "reCAPTCHA version 3", + "disabled": false + }, + { + "label": "reCAPTCHA version 2", + "disabled": false + } + ] + } + }, + "thirdParty.reCaptcha.siteKey": { + "namespace": "thirdParty.reCaptcha.siteKey", + "public": true, + "title": "reCAPTCHA site key", + "type": "string", + "default": "" + }, + "thirdParty.reCaptcha.secretKey": { + "namespace": "thirdParty.reCaptcha.secretKey", + "public": false, + "title": "reCAPTCHA secret key", + "type": "string", + "default": "" + }, + "other.hello": { + "namespace": "other.hello", + "public": true, + "title": "Hello world", + "type": "string", + "default": "world" + }, + "demo.string": { + "namespace": "demo.string", + "public": true, + "title": "String", + "desc": "One line string input.", + "type": "string", + "default": "Hello world!" + }, + "demo.longString": { + "namespace": "demo.longString", + "public": true, + "title": "Long string", + "desc": "Textarea for long string input.", + "type": "longString", + "default": "\"It is the unknown we fear when we look upon death and darkness, nothing more.\"\n-- Albus Dumbledore" + }, + "demo.switcher": { + "namespace": "demo.switcher", + "public": true, + "title": "Switcher", + "type": "switcher", + "desc": "True/False switcher.", + "default": true, + "binds": { + "positiveLabel": "current on", + "negativeLabel": "current off", + "disabled": false + } + }, + "demo.choose": { + "namespace": "demo.choose", + "public": true, + "title": "Choose", + "desc": "Choose one from options.", + "type": "choose", + "default": null, + "binds": { + "options": [ + { + "label": "op 1", + "disabled": false + }, + { + "label": "op 2", + "disabled": false + }, + { + "label": "op 3", + "disabled": false + }, + { + "label": "op 4", + "disabled": true + } + ] + } + }, + "demo.selection": { + "namespace": "demo.selection", + "public": true, + "title": "Selection", + "desc": "Selection multiple items from options. max: {0: no limit, >0: limit}", + "type": "selection", + "default": [ + true, + false, + true + ], + "binds": { + "options": [ + { + "label": "op 1", + "disabled": false + }, + { + "label": "op 2", + "disabled": false + }, + { + "label": "op 3", + "disabled": false + }, + { + "label": "op 4", + "disabled": true + } + ], + "max": 2 + } + }, + "demo.mediaPicker": { + "namespace": "demo.mediaPicker", + "public": true, + "title": "Media picker", + "desc": "type=\"image\"|\"video\"|\"audio?\", the object must include id, id=0 for remote media.", + "type": "mediaPicker", + "default": [ + { + "id": 0, + "url": "https://view.moezx.cc/images/2021/07/02/d5ab73174d18652d890e2f4d1b9bef8f.gif" + }, + { + "id": 0, + "url": "https://view.moezx.cc/images/2021/07/02/a90553bf5b67770e87a89b2ce204eaa7.gif" + } + ], + "binds": { + "title": "Select Media", + "button": "Use this media", + "type": "image", + "multiple": true + } + } +} \ No newline at end of file diff --git a/app/controllers/init-state-controller.php b/app/controllers/init-state-controller.php index 3f7e976..fdce521 100644 --- a/app/controllers/init-state-controller.php +++ b/app/controllers/init-state-controller.php @@ -31,9 +31,8 @@ class InitStateController extends BaseController 'menus' => (new MenuController)->get_menus(), // 'rewrite_rules' => (new \WP_Rewrite())->rewrite_rules(), 'index' => (new WP_Rewrite())->index, - 'config' => (new ConfigurationController)->public_options(), - 'recaptcha_site_key' => '6LfHEoEbAAAAAI5p_XBlr1WxEvrsOSNQFCQNcT79', // v2 secret key: 6LfHEoEbAAAAAIh0w2I9PCcVoa0j71mO6t7fipsj - // 'recaptcha_site_key' => '6LdKhX8bAAAAAF5HJprXtKvg3nfBJMfgd2o007PN' // v3 secret key: 6LdKhX8bAAAAAA010EXlQ32FWoYD1J2sLb8SaYLR + 'config' => (new OptionController)->get_public_display_options(), + // 'recaptcha_site_key' => sakura_options('thirdParty.reCaptcha.siteKey', ''), // use thirdParty.reCaptcha.siteKey ); } diff --git a/app/controllers/configuration-controller.php b/app/controllers/option-controller.php similarity index 59% rename from app/controllers/configuration-controller.php rename to app/controllers/option-controller.php index 285e503..1cf9374 100644 --- a/app/controllers/configuration-controller.php +++ b/app/controllers/option-controller.php @@ -5,24 +5,10 @@ namespace Sakura\Controllers; use WP_REST_Server; use WP_REST_Request; use WP_Error; -use Sakura\Lib\Exception; use Sakura\Models\OptionModel; -class ConfigurationController extends BaseController +class OptionController extends BaseController { - public function public_options() - { - $keys = [ - // key => default value - 'title' => 'Theme Sakura', - ]; - $res = []; - foreach ($keys as $key => $default) { - $res[$key] = $this->sakura_options($key, $default); - } - return $res; - } - /** * Constructor. * @@ -49,8 +35,8 @@ class ConfigurationController extends BaseController array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array($this, 'get_config'), - 'permission_callback' => array($this, 'get_config_permissions_check'), + 'callback' => array($this, 'get_public_config'), + 'permission_callback' => array($this, 'get_public_config_permissions_check'), // 'args' => $this->get_collection_params(), ), array( @@ -64,6 +50,16 @@ class ConfigurationController extends BaseController ); } + public function get_public_config(WP_REST_Request $request) + { + return $this->get_public_display_options(); + } + + public function get_public_config_permissions_check(WP_REST_Request $request) + { + return true; + } + public function get_config(WP_REST_Request $request) { $config = (array) OptionModel::get($this->rest_base); @@ -85,13 +81,30 @@ class ConfigurationController extends BaseController public function update_config(WP_REST_Request $request) { - $original = (array) $this->get_config($request); + $db = (array) $this->get_config($request); + $cache = $db; $json = (array) self::json_validate($request->get_body()); - if (empty(array_diff($original, $json))) { - return $original; + $hasNoDiff = true; + + foreach ($json as $key => $value) { + if (array_key_exists($key, $cache)) { + $nv = json_encode($value); + $ov = json_encode($cache[$key]); + if ($hasNoDiff) $hasNoDiff = $nv === $ov; + } else { + if ($hasNoDiff) $hasNoDiff = false; + } + $db[$key] = $value; } - $config = OptionModel::update($this->rest_base, $json); + if ($hasNoDiff) { + return [ + 'code' => 'save_config_succeed', + 'message' => __('Configurations already up to date.', self::$text_domain), + ]; + } + + $config = OptionModel::update($this->rest_base, $db); if (!$config) { return new WP_Error( 'save_config_failure', @@ -99,7 +112,10 @@ class ConfigurationController extends BaseController array('status' => 500) ); } else { - return $this->get_config($request); + return [ + 'code' => 'save_config_succeed', + 'message' => __('Configurations saved successfully.', self::$text_domain), + ]; } } @@ -108,10 +124,10 @@ class ConfigurationController extends BaseController return true; } - public function inite_theme() - { - $config = OptionModel::create($this->rest_base, (array)[]); - } + // public function inite_theme() + // { + // $config = OptionModel::create($this->rest_base, (array)[]); + // } public static function json_validate(string $string) { @@ -150,4 +166,38 @@ class ConfigurationController extends BaseController // sprintf(__("No existing database saving value or default value for option '%s'.", self::$text_domain), $namespace) // ); } + + public static function get_option_json() + { + $options = file_get_contents(__DIR__ . "/../configs/options.json"); + return json_decode($options, true); + } + + public function get_public_display_options() + { + $output = []; + $defaults = (array) self::get_option_json(); + // return $defaults; + foreach ($defaults as $key => $value) { + if ($value['public']) { + $output[$value['namespace']] = $this->sakura_options($value['namespace'], $value['default']); + } + } + return $output; + } + + /** + * Use in admin page only + * @return array + */ + public function get_all_options() + { + $output = []; + $defaults = (array) self::get_option_json(); + // return $defaults; + foreach ($defaults as $key => $value) { + $output[$value['namespace']] = $this->sakura_options($value['namespace'], $value['default']); + } + return $output; + } } diff --git a/app/functions.php b/app/functions.php index 3936f29..6167c2f 100644 --- a/app/functions.php +++ b/app/functions.php @@ -24,6 +24,6 @@ new Sakura\Routers\PagesRouter(); function sakura_options(string $namespace, $default) { - $CF = new Sakura\Controllers\ConfigurationController(); + $CF = new Sakura\Controllers\OptionController(); return $CF->sakura_options($namespace, $default); } diff --git a/app/helpers/admin-page-helper.php b/app/helpers/admin-page-helper.php index daac685..7e540e8 100644 --- a/app/helpers/admin-page-helper.php +++ b/app/helpers/admin-page-helper.php @@ -4,6 +4,7 @@ namespace Sakura\Helpers; use Sakura\Helpers\ViteHelper; use Sakura\Controllers\InitStateController; +use Sakura\Controllers\OptionController; class AdminPageHelper extends ViteHelper { @@ -46,7 +47,11 @@ class AdminPageHelper extends ViteHelper wp_enqueue_script('[type:module]dev-main', self::$development_host . '/src/admin/main.ts', array(), null, true); + wp_localize_script('[type:module]dev-main', 'AdminColors', $this->get_admin_color_css()); + wp_localize_script('[type:module]dev-main', 'InitState', (new InitStateController())->get_initial_state()); + + wp_localize_script('[type:module]dev-main', 'SakuraOptions', ['data' => (new OptionController())->get_all_options()]); } public function enqueue_production_scripts() @@ -56,9 +61,13 @@ class AdminPageHelper extends ViteHelper $manifest = self::get_manifest_file('admin'); // - wp_enqueue_script('[type:module]chunk-vendors.js', $assets_base_path . $manifest[$entry_key]['file'], array(), null, false); + wp_enqueue_script('[type:module]chunk-entrance.js', $assets_base_path . $manifest[$entry_key]['file'], array(), null, false); - wp_localize_script('[type:module]chunk-vendors.js', 'InitState', (new InitStateController())->get_initial_state()); + wp_localize_script('[type:module]chunk-entrance.js', 'AdminColors', $this->get_admin_color_css()); + + wp_localize_script('[type:module]chunk-entrance.js', 'InitState', (new InitStateController())->get_initial_state()); + + wp_localize_script('[type:module]chunk-entrance.js', 'SakuraOptions', (new OptionController())->get_all_options()); // foreach ($manifest[$entry_key]['imports'] as $index => $import) { @@ -85,4 +94,26 @@ class AdminPageHelper extends ViteHelper wp_enqueue_style('fontawesome-free', 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.3/css/all.min.css'); } + + public function get_admin_color_css() + { + // {"name":"Default","url":false,"colors":["#1d2327","#2c3338","#2271b1","#72aee6"],"icon_colors":{"base":"#a7aaad","focus":"#72aee6","current":"#fff"}} + global $_wp_admin_css_colors; + $theme = (array) $_wp_admin_css_colors[get_user_option('admin_color')]; + // $scheme = [ + // 'dark-primary' => $theme['colors'][0], + // 'dark-secondary' => $theme['colors'][1], + // 'light-primary' => $theme['colors'][2], + // 'light-secondary' => $theme['colors'][3], + // 'icon-base' => $theme['icon_colors']['base'], + // 'icon-focus' => $theme['icon_colors']['focus'], + // 'icon-current' => $theme['icon_colors']['current'], + // ]; + return $theme; + // $css = ''; + // foreach ($scheme as $key => $value) { + // $css .= "--{$key}:{$value};"; + // } + // return $css; + } } diff --git a/app/helpers/setup-helper.php b/app/helpers/setup-helper.php index c7db468..cd66d39 100644 --- a/app/helpers/setup-helper.php +++ b/app/helpers/setup-helper.php @@ -2,7 +2,7 @@ namespace Sakura\Helpers; -use Sakura\Controllers\ConfigurationController; +use Sakura\Controllers\OptionController; class SetupHelper { @@ -22,7 +22,7 @@ class SetupHelper // count post views add_action('get_header', [$this, 'set_post_views']); // Inite config options - add_action('after_switch_theme', [new ConfigurationController(), 'inite_theme'], 1, 2); + add_action('after_switch_theme', [new OptionController(), 'inite_theme'], 1, 2); } public function setup() diff --git a/app/helpers/vite-helper.php b/app/helpers/vite-helper.php index 418ee62..7c0070f 100644 --- a/app/helpers/vite-helper.php +++ b/app/helpers/vite-helper.php @@ -67,12 +67,7 @@ class ViteHelper extends BaseClass wp_enqueue_style('normalize.css', 'https://cdn.jsdelivr.net/npm/normalize.css/normalize.css'); - // TODO: don't use vue.js as handler - // wp_enqueue_script('vue.js', 'https://unpkg.com/vue@next', array(), false, false); - - // wp_localize_script('vue.js', 'InitState', (new InitStateController())->get_initial_state()); - - wp_enqueue_script('recaptcha', 'https://www.recaptcha.net/recaptcha/api.js?render=6LdKhX8bAAAAAF5HJprXtKvg3nfBJMfgd2o007PN', array(), false, true); + wp_enqueue_script('recaptcha', 'https://www.recaptcha.net/recaptcha/api.js', array(), false, true); } public static function script_tag_filter($tag, $handle, $src) diff --git a/app/routers/api-router.php b/app/routers/api-router.php index 9dc322f..69f6e9b 100644 --- a/app/routers/api-router.php +++ b/app/routers/api-router.php @@ -4,7 +4,7 @@ namespace Sakura\Routers; use WP_REST_Controller; use WP_REST_Server; -use Sakura\Controllers\ConfigurationController; +use Sakura\Controllers\OptionController; use Sakura\Controllers\InitStateController; use Sakura\Controllers\MenuController; use Sakura\Controllers\PostController; @@ -33,7 +33,9 @@ class ApiRouter extends WP_REST_Controller */ public function register_rest_routes() { - add_action('rest_api_init', [new ConfigurationController(), 'register_routes']); + // add options support + add_action('rest_api_init', [new OptionController(), 'register_routes']); + add_action('rest_api_init', function () { // theme's initial states register_rest_route( diff --git a/app/utils/tools.php b/app/utils/tools.php index cad2eb6..f196149 100644 --- a/app/utils/tools.php +++ b/app/utils/tools.php @@ -2,6 +2,8 @@ namespace Sakura\Utils; +use Rogervila\ArrayDiffMultidimensional; + class Tools { public static function echo_interceptor(callable $callback, ...$args) @@ -13,15 +15,46 @@ class Tools return $output; } - // public function get_text_from_dom($node, $text) { - // if (!is_null($node->childNodes)) { - // foreach ($node->childNodes as $node) { - // $text = get_text_from_dom($node, $text); - // } - // } - // else { - // return $text . $node->textContent . ' '; - // } - // return $text; - // } + public static function get_text_from_dom($node, $text) + { + if (!is_null($node->childNodes)) { + foreach ($node->childNodes as $node) { + $text = self::get_text_from_dom($node, $text); + } + } else { + return $text . $node->textContent . ' '; + } + return $text; + } + + /** + * https://stackoverflow.com/a/3877494/8083009 + * + * @param array $aArray1 + * @param array $aArray2 + * + * @return array + */ + public static function array_recursive_diff(array $aArray1, array $aArray2) + { + $aReturn = array(); + + foreach ($aArray1 as $mKey => $mValue) { + if (array_key_exists($mKey, $aArray2)) { + if (is_array($mValue)) { + $aRecursiveDiff = self::array_recursive_diff($mValue, $aArray2[$mKey]); + if (count($aRecursiveDiff)) { + $aReturn[$mKey] = $aRecursiveDiff; + } + } else { + if ($mValue != $aArray2[$mKey]) { + $aReturn[$mKey] = $mValue; + } + } + } else { + $aReturn[$mKey] = $mValue; + } + } + return $aReturn; + } } diff --git a/app/views/helpers/admin-page-helper.twig b/app/views/helpers/admin-page-helper.twig index 8cd3b65..26f73dc 100644 --- a/app/views/helpers/admin-page-helper.twig +++ b/app/views/helpers/admin-page-helper.twig @@ -1,5 +1,5 @@ {% block admin_app %} -
+
Loading
{% endblock %} diff --git a/package.json b/package.json index 7d6a2e7..404d8b5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "rsync": "nodemon -e '*' --watch ./app --ignore ./app/vendor scripts/rsync.mjs", "rsync:composer": "nodemon --watch './composer.json' --watch './composer.lock' scripts/rsync.mjs --composer", "gen:icon": "node scripts/import-svg-icons.mjs && eslint \"src/components/icon/**/*.{ts,js,json,vue}\" --fix && prettier \"src/components/icon/**/*.{ts,js,json,vue}\" --write", - "mdc": "node scripts/mdc-upgrade.mjs" + "mdc": "node scripts/mdc-upgrade.mjs", + "options": "node scripts/options-export/copy-options.mjs && yarn tsc scripts/options-export/dump-options.ts && node scripts/options-export/dump-options.js" }, "dependencies": { "@formatjs/intl": "^1.13.2", @@ -49,6 +50,7 @@ "@yzfe/vue3-svgicon": "^1.0.1", "axios": "^0.21.1", "camelcase-keys": "^7.0.0", + "chroma-js": "^2.1.2", "crypto-js": "^4.0.0", "gsap": "^3.7.0", "highlight.js": "^11.1.0", @@ -67,6 +69,7 @@ }, "devDependencies": { "@formatjs/cli": "^4.2.27", + "@types/chroma-js": "^2.1.3", "@types/crypto-js": "^4.0.1", "@types/jest": "^26.0.24", "@types/marked": "^2.0.4", @@ -87,7 +90,6 @@ "eslint-plugin-formatjs": "^2.17.1", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-vue": "^7.13.0", - "foreman": "^3.0.1", "jest": "^27.0.6", "nodemon": "^2.0.12", "postcss-import": "^14.0.2", diff --git a/scripts/import-svg-icons.mjs b/scripts/import-svg-icons.mjs index 3f57a20..1906f22 100644 --- a/scripts/import-svg-icons.mjs +++ b/scripts/import-svg-icons.mjs @@ -50,4 +50,4 @@ readdirSync(iconDir).forEach((file) => { const vueContent = template(importContent, dataContent) -writeFileSync(targetDir, vueContent) +writeFileSync(targetDir, vueContent, { flag: 'w+' }) diff --git a/scripts/options-export/.gitignore b/scripts/options-export/.gitignore new file mode 100644 index 0000000..53b4525 --- /dev/null +++ b/scripts/options-export/.gitignore @@ -0,0 +1,2 @@ +*.js +options.ts diff --git a/scripts/options-export/copy-options.mjs b/scripts/options-export/copy-options.mjs new file mode 100644 index 0000000..77258e8 --- /dev/null +++ b/scripts/options-export/copy-options.mjs @@ -0,0 +1,5 @@ +import { readFileSync, writeFileSync } from 'fs' + +let file = readFileSync('./src/admin/options.ts', { flag: 'r' }).toString() +file = file.replace('@/locales', './locales') +writeFileSync('./scripts/options-export/options.ts', file, { flag: 'w+' }) diff --git a/scripts/options-export/dump-options.ts b/scripts/options-export/dump-options.ts new file mode 100644 index 0000000..669381b --- /dev/null +++ b/scripts/options-export/dump-options.ts @@ -0,0 +1,15 @@ +import options from './options' +import { writeFileSync } from 'fs' + +const exportOptions: { [key: string]: any } = {} + +Object.keys(options).forEach((tab) => { + options[tab].options.forEach((option) => { + if (option.depends) delete option.depends // remove function + exportOptions[option.namespace] = option + }) +}) + +console.dir(exportOptions) + +writeFileSync('./app/configs/options.json', JSON.stringify(exportOptions, null, 2), { flag: 'w+' }) diff --git a/scripts/options-export/locales.ts b/scripts/options-export/locales.ts new file mode 100644 index 0000000..2a4d63d --- /dev/null +++ b/scripts/options-export/locales.ts @@ -0,0 +1,10 @@ +// intl.formatMessage({ +// id: 'options.basic.siteTitle', +// defaultMessage: 'The site title', +// }) + +export default { + formatMessage({ id, defaultMessage }: { id: string; defaultMessage: string }) { + return defaultMessage + }, +} diff --git a/src/admin/App.vue b/src/admin/App.vue index 78e38d4..954db9d 100644 --- a/src/admin/App.vue +++ b/src/admin/App.vue @@ -1,29 +1,49 @@ diff --git a/src/admin/Core.vue b/src/admin/Core.vue index 4635ac0..cb44825 100644 --- a/src/admin/Core.vue +++ b/src/admin/Core.vue @@ -18,19 +18,28 @@ >

{{ options[tabKey].title }}

-

{{ options[tabKey].desc }}

-
- -
+

+

+ +
+ +
+
- +
@@ -38,21 +47,14 @@ @@ -110,6 +165,44 @@ export default defineComponent({ .tab-page__content { width: calc(100% - 24px); padding: 12px; + .row__wrapper { + &--options { + &-enter-active { + transform-origin: top; + animation: from 0.3s forwards; + } + &-leave-active { + transform-origin: top; + animation: to 0.3s forwards; + } + &-move { + transition: transform 0.3s ease; + } + } + } + @keyframes from { + 0% { + transform: scaleY(0); + opacity: 0; + } + 100% { + transform: scaleY(1); + opacity: 1; + } + } + + @keyframes to { + 0% { + height: 56px; + transform: scaleY(1); + opacity: 1; + } + 100% { + height: 0; + transform: scaleY(0); + opacity: 0; + } + } } } } diff --git a/src/admin/OptionItem.vue b/src/admin/OptionItem.vue index 5cb65c2..3aac70b 100644 --- a/src/admin/OptionItem.vue +++ b/src/admin/OptionItem.vue @@ -80,11 +80,17 @@ export default defineComponent({ diff --git a/src/components/messages/Messages.vue b/src/components/messages/Messages.vue new file mode 100644 index 0000000..b041563 --- /dev/null +++ b/src/components/messages/Messages.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/components/radio/RadioButton.vue b/src/components/radio/RadioButton.vue index a97ff92..dd4943e 100644 --- a/src/components/radio/RadioButton.vue +++ b/src/components/radio/RadioButton.vue @@ -1,11 +1,11 @@ @@ -32,7 +32,7 @@ export default defineComponent({ }, emits: ['update:checked'], setup(props, { emit }) { - const id = uniqueHash() + const id = `radio-${uniqueHash()}` const [elRef, setElRef] = useElementRef() @@ -84,6 +84,12 @@ export default defineComponent({ flex-direction: row; justify-content: flex-start; align-items: center; + &.disabled { + cursor: not-allowed; + .label { + cursor: not-allowed; + } + } .label { user-select: none; } diff --git a/src/components/switcher/Switcher.vue b/src/components/switcher/Switcher.vue index 4607585..ac12447 100644 --- a/src/components/switcher/Switcher.vue +++ b/src/components/switcher/Switcher.vue @@ -1,5 +1,5 @@