aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-07-10 01:30:05 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-07-10 23:53:02 +0300
commit1c524efbf7da91cb99bb4516feb514071e938495 (patch)
treee1fced0104749014db154f5d3b29881e705bfafc
parent8979719a1af4bc0712407db7f95704f645f261a3 (diff)
dark theme support
-rwxr-xr-xbuild_static.php96
-rwxr-xr-xdark-theme-diff.js14
-rwxr-xr-xdeploy.sh7
-rw-r--r--engine/RequestDispatcher.php7
-rw-r--r--engine/Skin.php5
-rw-r--r--engine/SkinContext.php8
-rw-r--r--htdocs/img/enter.svg1
-rw-r--r--htdocs/js/common.js293
-rw-r--r--htdocs/sass.php9
-rw-r--r--htdocs/scss/app/blog.scss (renamed from htdocs/scss/blog.scss)20
-rw-r--r--htdocs/scss/app/common.scss (renamed from htdocs/scss/common.scss)172
-rw-r--r--htdocs/scss/app/form.scss (renamed from htdocs/scss/form.scss)4
-rw-r--r--htdocs/scss/app/head.scss157
-rw-r--r--htdocs/scss/app/hljs.scss1
-rw-r--r--htdocs/scss/app/mobile.scss (renamed from htdocs/scss/mobile.scss)2
-rw-r--r--htdocs/scss/app/pages.scss (renamed from htdocs/scss/pages.scss)0
-rw-r--r--htdocs/scss/bundle_admin.scss3
-rw-r--r--htdocs/scss/bundle_common.scss10
-rw-r--r--htdocs/scss/colors/dark.scss46
-rw-r--r--htdocs/scss/colors/light.scss46
-rw-r--r--htdocs/scss/common-bundle.scss9
-rw-r--r--htdocs/scss/entries/admin/dark.scss2
-rw-r--r--htdocs/scss/entries/admin/light.scss2
-rw-r--r--htdocs/scss/entries/common/dark.scss2
-rw-r--r--htdocs/scss/entries/common/light.scss2
-rw-r--r--htdocs/scss/hljs.scss1
-rw-r--r--htdocs/scss/hljs/github.scss36
-rw-r--r--htdocs/scss/vars.scss39
-rw-r--r--lang/en.php5
-rw-r--r--model/Upload.php2
-rw-r--r--package-lock.json647
-rw-r--r--package.json7
-rwxr-xr-xprepare_static.php48
-rw-r--r--skin/base.skin.php120
34 files changed, 1504 insertions, 319 deletions
diff --git a/build_static.php b/build_static.php
new file mode 100755
index 0000000..a9ec027
--- /dev/null
+++ b/build_static.php
@@ -0,0 +1,96 @@
+#!/usr/bin/env php8.1
+<?php
+
+function gethash(string $path): string {
+ return substr(sha1(file_get_contents($path)), 0, 8);
+}
+
+function sassc(string $src_file, string $dst_file): int {
+ $cmd = 'sassc -t compressed '.escapeshellarg($src_file).' '.escapeshellarg($dst_file);
+ exec($cmd, $output, $code);
+ return $code;
+}
+
+function clean_css(string $file) {
+ $output = $file.'.out';
+ if (file_exists($output))
+ unlink($output);
+
+ $cmd = ROOT.'/node_modules/clean-css-cli/bin/cleancss -O2 "all:on;mergeSemantically:on;restructureRules:on" '.escapeshellarg($file).' > '.escapeshellarg($output);
+ system($cmd);
+
+ if (file_exists($output)) {
+ unlink($file);
+ rename($output, $file);
+ } else {
+ fwrite(STDERR, "error: could not cleancss $file\n");
+ }
+}
+
+function dark_diff(string $light_file, string $dark_file): void {
+ $temp_output = $dark_file.'.diff';
+ $cmd = ROOT.'/dark-theme-diff.js '.escapeshellarg($light_file).' '.$dark_file.' > '.$temp_output;
+ exec($cmd, $output, $code);
+ if ($code != 0) {
+ fwrite(STDERR, "dark_diff failed with code $code\n");
+ return;
+ }
+
+ unlink($dark_file);
+ rename($temp_output, $dark_file);
+}
+
+require __DIR__.'/init.php';
+
+function build_static(): void {
+ $css_dir = ROOT.'/htdocs/css';
+ $hashes = [];
+
+ if (!file_exists($css_dir))
+ mkdir($css_dir);
+
+ // 1. scss -> css
+ $themes = ['light', 'dark'];
+ $entries = ['common', 'admin'];
+ foreach ($themes as $theme) {
+ foreach ($entries as $entry) {
+ $input = ROOT.'/htdocs/scss/entries/'.$entry.'/'.$theme.'.scss';
+ $output = $css_dir.'/'.$entry.($theme == 'dark' ? '_dark' : '').'.css';
+ if (sassc($input, $output) != 0) {
+ fwrite(STDERR, "error: could not compile entries/$entry/$theme.scss\n");
+ continue;
+ }
+
+ // 1.1. apply clean-css optimizations and transformations
+ clean_css($output);
+ }
+ }
+
+ // 2. generate dark theme diff
+ foreach ($entries as $entry) {
+ $light_file = $css_dir.'/'.$entry.'.css';
+ $dark_file = str_replace('.css', '_dark.css', $light_file);
+ dark_diff($light_file, $dark_file);
+ }
+
+ // 3. calculate hashes
+ foreach (['css', 'js'] as $type) {
+ $reldir = ROOT.'/htdocs/';
+ $entries = glob_recursive($reldir.$type.'/*.'.$type);
+ if (empty($entries)) {
+ continue;
+ }
+ foreach ($entries as $file) {
+ $name = preg_replace('/^'.preg_quote($reldir, '/').'/', '', $file);
+ $hashes[$name] = gethash($file);
+ }
+ }
+
+ // 4. write config-static.php
+ $scfg = "<?php\n\n";
+ $scfg .= "return ".var_export($hashes, true).";\n";
+
+ file_put_contents(ROOT.'/config-static.php', $scfg);
+}
+
+build_static(); \ No newline at end of file
diff --git a/dark-theme-diff.js b/dark-theme-diff.js
new file mode 100755
index 0000000..5ca1945
--- /dev/null
+++ b/dark-theme-diff.js
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+const {generateCSSPatch} = require('css-patch')
+const fs = require('fs')
+
+const files = process.argv.slice(2)
+if (files.length !== 2) {
+ console.log(`usage: ${process.argv[0]} file1 file2`)
+ process.exit()
+}
+
+const css1 = fs.readFileSync(files[0], 'utf-8')
+const css2 = fs.readFileSync(files[1], 'utf-8')
+
+console.log(generateCSSPatch(css1, css2)) \ No newline at end of file
diff --git a/deploy.sh b/deploy.sh
index a64c174..96240f6 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -25,7 +25,12 @@ git reset --hard
git pull origin master
composer8.1 install --no-dev --optimize-autoloader --ignore-platform-reqs
-$PHP prepare_static.php
+
+if [ ! -d node_modules ]; then
+ npm i
+fi
+
+$PHP build_static.php
cp "$DEV_DIR/config-local.php" .
cat config-local.php | grep -v is_dev | tee config-local.php >/dev/null
diff --git a/engine/RequestDispatcher.php b/engine/RequestDispatcher.php
index 38b965d..3c3f684 100644
--- a/engine/RequestDispatcher.php
+++ b/engine/RequestDispatcher.php
@@ -39,11 +39,14 @@ class RequestDispatcher {
}
$skin = new Skin();
- $skin->static[] = '/css/common-bundle.css';
+ $skin->static[] = '/css/common.css';
$skin->static[] = '/js/common.js';
+ $lang = LangData::getInstance();
+ $skin->addLangKeys($lang->search('/^theme_/'));
+
/** @var RequestHandler $handler */
- $handler = new $handler_class($skin, LangData::getInstance(), $router_input);
+ $handler = new $handler_class($skin, $lang, $router_input);
$resp = $handler->beforeDispatch();
if ($resp instanceof Response) {
$resp->send();
diff --git a/engine/Skin.php b/engine/Skin.php
index 57f8b90..917eef7 100644
--- a/engine/Skin.php
+++ b/engine/Skin.php
@@ -23,11 +23,16 @@ class Skin {
else
$js = null;
+ $theme = ($_COOKIE['theme'] ?? 'auto');
+ if (!in_array($theme, ['auto', 'dark', 'light']))
+ $theme = 'auto';
+
$layout_ctx = new SkinContext('\\skin\\base');
$lang = $this->getLang();
$lang = !empty($lang) ? json_encode($lang, JSON_UNESCAPED_UNICODE) : '';
return new Response(200, $layout_ctx->layout(
static: $this->static,
+ theme: $theme,
title: $this->title,
opts: $this->options,
js: $js,
diff --git a/engine/SkinContext.php b/engine/SkinContext.php
index cfb5068..c395453 100644
--- a/engine/SkinContext.php
+++ b/engine/SkinContext.php
@@ -53,10 +53,12 @@ class SkinContext extends SkinBase {
return call_user_func_array($fn, $arguments);
}
- public function __get(string $name) {
+ public function &__get(string $name) {
$fn = $this->ns.'\\'.$name;
- if (function_exists($fn))
- return [$this, $name];
+ if (function_exists($fn)) {
+ $f = [$this, $name];
+ return $f;
+ }
if (array_key_exists($name, $this->data))
return $this->data[$name];
diff --git a/htdocs/img/enter.svg b/htdocs/img/enter.svg
deleted file mode 100644
index 6fe49ed..0000000
--- a/htdocs/img/enter.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9.474"><path d="M0 4.737l4.737 4.737 1.105-1.106L3 5.526h12V.79h-1.579v3.158H3l2.842-2.842L4.737 0z"/></svg>
diff --git a/htdocs/js/common.js b/htdocs/js/common.js
index 4e4199c..959b672 100644
--- a/htdocs/js/common.js
+++ b/htdocs/js/common.js
@@ -14,6 +14,39 @@ if (!String.prototype.endsWith) {
};
}
+if (!Object.assign) {
+ Object.defineProperty(Object, 'assign', {
+ enumerable: false,
+ configurable: true,
+ writable: true,
+ value: function(target, firstSource) {
+ 'use strict';
+ if (target === undefined || target === null) {
+ throw new TypeError('Cannot convert first argument to object');
+ }
+
+ var to = Object(target);
+ for (var i = 1; i < arguments.length; i++) {
+ var nextSource = arguments[i];
+ if (nextSource === undefined || nextSource === null) {
+ continue;
+ }
+
+ var keysArray = Object.keys(Object(nextSource));
+ for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
+ var nextKey = keysArray[nextIndex];
+ var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
+ if (desc !== undefined && desc.enumerable) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ return to;
+ }
+ });
+}
+
+
//
// AJAX
//
@@ -87,7 +120,7 @@ if (!String.prototype.endsWith) {
break;
}
- opts = extend({}, defaultOpts, opts);
+ opts = Object.assign({}, defaultOpts, opts);
var xhr = createXMLHttpRequest();
xhr.open(method, url);
@@ -244,11 +277,11 @@ function setCookie(name, value, days) {
date.setTime(date.getTime() + (days*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
}
- document.cookie = name + "=" + (value || "") + expires + "; path=/";
+ document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/";
}
function unsetCookie(name) {
- document.cookie = name+'=; Max-Age=-99999999;';
+ document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/";
}
function getCookie(name) {
@@ -312,6 +345,34 @@ function escape(str) {
return pre.innerHTML;
}
+function parseUrl(uri) {
+ var parser = document.createElement('a');
+ parser.href = uri;
+
+ return {
+ protocol: parser.protocol, // => "http:"
+ host: parser.host, // => "example.com:3000"
+ hostname: parser.hostname, // => "example.com"
+ port: parser.port, // => "3000"
+ pathname: parser.pathname, // => "/pathname/"
+ hash: parser.hash, // => "#hash"
+ search: parser.search, // => "?search=test"
+ origin: parser.origin, // => "http://example.com:3000"
+ path: (parser.pathname || '') + (parser.search || '')
+ }
+}
+
+function once(fn, context) {
+ var result;
+ return function() {
+ if (fn) {
+ result = fn.apply(context || this, arguments);
+ fn = null;
+ }
+ return result;
+ };
+}
+
//
//
@@ -390,3 +451,229 @@ window.__lang = {};
unsetCookie('is_retina');
}
})();
+
+var StaticManager = {
+ loadedStyles: [],
+ versions: {},
+
+ setStyles: function(list, versions) {
+ this.loadedStyles = list;
+ this.versions = versions;
+ },
+
+ loadStyle: function(name, theme, callback) {
+ var url;
+ if (!window.appConfig.devMode) {
+ var filename = name + (theme === 'dark' ? '_dark' : '') + '.css';
+ url = '/css/'+filename+'?'+this.versions[filename];
+ } else {
+ url = '/sass.php?name='+name+'&theme='+theme;
+ }
+
+ var el = document.createElement('link');
+ el.onerror = callback
+ el.onload = callback
+ el.setAttribute('rel', 'stylesheet');
+ el.setAttribute('type', 'text/css');
+ el.setAttribute('id', 'style_'+name+'_dark');
+ el.setAttribute('href', url);
+
+ document.getElementsByTagName('head')[0].appendChild(el);
+ }
+};
+
+var ThemeSwitcher = (function() {
+ /**
+ * @type {string[]}
+ */
+ var modes = ['auto', 'dark', 'light'];
+
+ /**
+ * @type {number}
+ */
+ var currentModeIndex = -1;
+
+ /**
+ * @type {boolean|null}
+ */
+ var systemState = null;
+
+ /**
+ * @returns {boolean}
+ */
+ function isSystemModeSupported() {
+ try {
+ // crashes on:
+ // Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 630 Dual SIM) like Gecko
+ // Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1
+ // Mozilla/5.0 (iPad; CPU OS 12_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1
+ //
+ // error examples:
+ // - window.matchMedia("(prefers-color-scheme: dark)").addEventListener is not a function. (In 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",this.onSystemSettingChange.bind(this))', 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener' is undefined)
+ // - Object [object MediaQueryList] has no method 'addEventListener'
+ return !!window['matchMedia']
+ && typeof window.matchMedia("(prefers-color-scheme: dark)").addEventListener === 'function';
+ } catch (e) {
+ return false
+ }
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ function isDarkModeApplied() {
+ var st = StaticManager.loadedStyles;
+ for (var i = 0; i < st.length; i++) {
+ var name = st[i];
+ if (ge('style_'+name+'_dark'))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @returns {string}
+ */
+ function getSavedMode() {
+ var val = getCookie('theme');
+ if (!val)
+ return modes[0];
+ if (modes.indexOf(val) === -1) {
+ console.error('[ThemeSwitcher getSavedMode] invalid cookie value')
+ unsetCookie('theme')
+ return modes[0]
+ }
+ return val
+ }
+
+ /**
+ * @param {boolean} dark
+ */
+ function changeTheme(dark) {
+ addClass(document.body, 'theme-changing');
+
+ var onDone = function() {
+ window.requestAnimationFrame(function() {
+ removeClass(document.body, 'theme-changing');
+ })
+ };
+
+ window.requestAnimationFrame(function() {
+ if (dark)
+ enableDark(onDone);
+ else
+ disableDark(onDone);
+ })
+ }
+
+ /**
+ * @param {function} callback
+ */
+ function enableDark(callback) {
+ var names = [];
+ StaticManager.loadedStyles.forEach(function(name) {
+ var el = ge('style_'+name+'_dark');
+ if (el)
+ return;
+ names.push(name);
+ });
+
+ var left = names.length;
+ names.forEach(function(name) {
+ StaticManager.loadStyle(name, 'dark', once(function(e) {
+ left--;
+ if (left === 0)
+ callback();
+ }));
+ })
+ }
+
+ /**
+ * @param {function} callback
+ */
+ function disableDark(callback) {
+ StaticManager.loadedStyles.forEach(function(name) {
+ var el = ge('style_'+name+'_dark');
+ if (el)
+ el.remove();
+ })
+ callback();
+ }
+
+ /**
+ * @param {string} mode
+ */
+ function setLabel(mode) {
+ var labelEl = ge('theme-switcher-label');
+ labelEl.innerHTML = escape(lang('theme_'+mode));
+ }
+
+ return {
+ init: function() {
+ var cur = getSavedMode();
+ currentModeIndex = modes.indexOf(cur);
+
+ var systemSupported = isSystemModeSupported();
+ if (!systemSupported) {
+ if (currentModeIndex === 0) {
+ modes.shift(); // remove 'auto' from the list
+ currentModeIndex = 1; // set to 'light'
+ if (isDarkModeApplied())
+ disableDark();
+ }
+ } else {
+ /**
+ * @param {boolean} dark
+ */
+ var onSystemChange = function(dark) {
+ var prevSystemState = systemState;
+ systemState = dark;
+
+ if (modes[currentModeIndex] !== 'auto')
+ return;
+
+ if (systemState !== prevSystemState)
+ changeTheme(systemState);
+ };
+
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
+ onSystemChange(e.matches === true)
+ });
+
+ onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true);
+ }
+
+ setLabel(modes[currentModeIndex]);
+ },
+
+ next: function(e) {
+ if (hasClass(document.body, 'theme-changing')) {
+ console.log('next: theme changing is in progress, ignoring...')
+ return;
+ }
+
+ currentModeIndex = (currentModeIndex + 1) % modes.length;
+ switch (modes[currentModeIndex]) {
+ case 'auto':
+ if (systemState !== null)
+ changeTheme(systemState);
+ break;
+
+ case 'light':
+ if (isDarkModeApplied())
+ changeTheme(false);
+ break;
+
+ case 'dark':
+ if (!isDarkModeApplied())
+ changeTheme(true);
+ break;
+ }
+
+ setLabel(modes[currentModeIndex]);
+ setCookie('theme', modes[currentModeIndex]);
+
+ return cancelEvent(e);
+ }
+ };
+})(); \ No newline at end of file
diff --git a/htdocs/sass.php b/htdocs/sass.php
index eb24962..186b706 100644
--- a/htdocs/sass.php
+++ b/htdocs/sass.php
@@ -4,7 +4,14 @@ require __DIR__.'/../init.php';
global $config;
$name = $_REQUEST['name'] ?? '';
-if (!$config['is_dev'] || !$name || !file_exists($path = ROOT.'/htdocs/scss/'.$name.'.scss')) {
+$theme = $_REQUEST['theme'] ?? '';
+
+if ($theme != 'light' && $theme != 'dark') {
+ http_response_code(403);
+ exit;
+}
+
+if (!$config['is_dev'] || !$name || !file_exists($path = ROOT.'/htdocs/scss/entries/'.$name.'/'.$theme.'.scss')) {
// logError(__FILE__.': access denied');
http_response_code(403);
exit;
diff --git a/htdocs/scss/blog.scss b/htdocs/scss/app/blog.scss
index 7641683..53ec7e4 100644
--- a/htdocs/scss/blog.scss
+++ b/htdocs/scss/app/blog.scss
@@ -1,4 +1,4 @@
-@import 'vars';
+@import '../vars';
.blog-write-link-wrap {
margin-bottom: $base-padding;
@@ -81,7 +81,7 @@
margin-bottom: 2px;
}
.blog-upload-item-info {
- color: #888;
+ color: $grey;
font-size: $fs - 2px;
}
.blog-upload-item-note {
@@ -102,7 +102,7 @@
}
.blog-post-date {
- color: #888;
+ color: $grey;
margin-top: 5px;
font-size: $fs - 1px;
> a {
@@ -168,7 +168,7 @@
background-color: $code-block-bg;
span.term-prompt {
- color: #999;
+ color: $light-grey;
@include user-select(none);
}
}
@@ -177,7 +177,7 @@
border-left: 3px #e0e0e0 solid;
margin-left: 0;
padding: 5px 0 5px 12px;
- color: #888;
+ color: $grey;
}
table.table-100 {
@@ -266,7 +266,7 @@
.blog-post-comments {
margin-top: $base-padding;
padding: 12px 15px;
- border: 1px #e0e0e0 solid;
+ border: 1px $border-color solid;
@include radius(3px);
}
.blog-post-comments img {
@@ -317,7 +317,7 @@ td.blog-item-date-cell {
padding-right: 10px;
}
.blog-item-date {
- color: #777;
+ color: $grey;
//text-transform: lowercase;
}
td.blog-item-title-cell {
@@ -344,6 +344,8 @@ td.blog-item-title-cell {
padding-top: 0;
}
}
+
+/*
a.blog-item-view-all-link {
display: inline-block;
padding: 4px 17px;
@@ -356,7 +358,7 @@ a.blog-item-view-all-link:hover {
text-decoration: none;
background-color: #ededed;
}
-
+*/
.blog-tags {
float: right;
@@ -374,7 +376,7 @@ a.blog-item-view-all-link:hover {
font-size: $fs - 1px;
}
.blog-tag-item > a {
- color: #222;
+ color: $fg;
}
.blog-tag-item-count {
color: #aaa;
diff --git a/htdocs/scss/common.scss b/htdocs/scss/app/common.scss
index 644a080..b9c5959 100644
--- a/htdocs/scss/common.scss
+++ b/htdocs/scss/app/common.scss
@@ -1,4 +1,4 @@
-@import "vars";
+@import "../vars";
.clearfix:after {
content: ".";
@@ -13,7 +13,7 @@ html, body {
padding: 0;
margin: 0;
border: 0;
- //background-color: $bg;
+ background-color: $bg;
color: $fg;
height: 100%;
min-height: 100%;
@@ -35,13 +35,15 @@ body.full-width .base-width {
margin-right: auto;
}
-input[type="text"], textarea {
+input[type="text"],
+input[type="password"],
+textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
box-sizing: border-box;
border: 1px $input-border solid;
- border-radius: 0px;
+ border-radius: 0;
background-color: $input-bg;
color: $fg;
font-family: $ff;
@@ -49,14 +51,16 @@ input[type="text"], textarea {
padding: 6px;
outline: none;
@include radius(3px);
+
+ &:focus {
+ border-color: $input-border-focused;
+ }
}
+
textarea {
resize: vertical;
}
-input[type="text"]:focus,
-textarea:focus {
- border-color: $input-border-focused;
-}
+
//input[type="checkbox"] {
// margin-left: 0;
//}
@@ -110,137 +114,6 @@ p, p code {
padding: $base-padding 0;
}
-.head {
- padding: 0 $side-padding;
-}
-.head-inner {
- //padding: 13px 0;
- position: relative;
- border-bottom: 2px $border-color solid;
-}
-.head-logo {
- padding: 4px 0;
- font-family: $ffMono;
- font-size: 15px;
- display: inline-block;
- position: absolute;
- left: 0;
- background-color: transparent;
- @include transition(background-color, 0.03s);
-}
-.head-logo {
- padding: 16px 0;
- background-color: #fff;
-}
-.head-logo:after {
- content: '';
- display: block;
- width: 40px;
- position: absolute;
- right: -40px;
- top: 0;
- bottom: 0;
- border-left: 8px #fff solid;
- box-sizing: border-box;
- background: linear-gradient(to left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); /* W3C */
-}
-.head-logo > a {
- color: $fg;
- font-size: 14px;
-}
-.head-logo > a:hover {
- text-decoration: none;
-}
-.head-logo-enter {
- display: inline;
- opacity: 0;
- font-size: 11px;
- position: relative;
- background: #eee;
- padding: 2px 5px;
- color: #333;
- font-weight: normal;
- vertical-align: middle;
- top: -1px;
- @include transition(opacity, 0.03s);
-}
-.head-logo-enter-icon {
- width: 12px;
- height: 7px;
- display: inline-block;
- background: url(/img/enter.svg) 0 0 no-repeat;
- background-size: 12px 7px;
- margin-right: 5px;
-}
-.head-logo > a:hover .head-logo-enter {
- opacity: 1;
-}
-.head-logo-path {
- color: $fg;
- font-weight: bold;
- -webkit-font-smoothing: antialiased;
- @include transition(color, 0.03s);
-}
-.head-logo > a:hover .head-logo-path:not(.alwayshover) {
- color: #aaa;
-}
-.head-logo-path:not(.neverhover):hover {
- color: #000 !important;
-}
-.head-logo-dolsign {
- color: $head-green-color;
- font-weight: normal;
- &.is_root {
- color: $head-red-color;
- }
-}
-.head-logo-cd {
- display: none;
-}
-.head-logo > a:hover .head-logo-cd {
- display: inline;
-}
-.head-logo-path-mapped {
- padding: 3px 5px;
- background: #f1f1f1;
- pointer-events: none;
- @include radius(3px);
- margin: 0 2px;
-}
-
-.head-items {
- float: right;
- color: #777; // color of separators
- //padding: 8px 0;
-}
-a.head-item {
- color: $fg;
- font-size: $fs - 1px;
- display: block;
- float: left;
- padding: 16px 0;
-}
-a.head-item > span {
- padding: 0 12px;
- border-right: 1px #d0d0d0 solid;
-}
-a.head-item > span > span {
- padding: 2px 0;
-}
-a.head-item:last-child > span {
- border-right: 0;
- padding-right: 1px;
-}
-/*a.head-item:first-child > span {
- padding-left: 2px;
-}*/
-a.head-item:hover {
- //color: $link-color;
- text-decoration: none;
-}
-a.head-item:hover > span > span {
- border-bottom: 1px #d0d0d0 solid;
-}
table.contacts {
border: 0;
@@ -257,17 +130,17 @@ table.contacts td {
table.contacts td.label {
text-align: right;
width: 30%;
- color: #777;
+ color: $dark-grey;
}
table.contacts td.value {
text-align: left;
padding-left: 8px;
}
table.contacts td.value span {
- background: #eee;
+ background: $inline-code-block-bg;
padding: 3px 7px 4px;
border-radius: 3px;
- color: #333;
+ color: $fg;
font-family: $ffMono;
font-size: $fs - 1px;
}
@@ -283,13 +156,14 @@ table.contacts td pre {
table.contacts div.note {
font-size: $fs - 3px;
padding-top: 2px;
- color: #777;
+ color: $dark-grey;
> a {
- color: #777;
- border-bottom: 1px #ccc solid;
+ color: $dark-grey;
+ border-bottom: 1px $border-color solid;
&:hover {
text-decoration: none;
- border-bottom-color: #999;
+ color: $link-color;
+ border-bottom-color: $link-color;
}
}
}
@@ -360,11 +234,11 @@ table.contacts div.note {
//font-weight: bold;
}
.md-file-attach-size {
- color: #888;
+ color: $grey;
margin-left: 2px;
}
.md-file-attach-note {
- color: #000;
+ color: $fg;
margin-left: 2px;
}
@@ -402,7 +276,7 @@ table.contacts div.note {
}
.md-image-note {
line-height: 150%;
- color: #777;
+ color: $dark-grey;
padding: 2px 0 4px;
}
diff --git a/htdocs/scss/form.scss b/htdocs/scss/app/form.scss
index 4faa4d1..197732c 100644
--- a/htdocs/scss/form.scss
+++ b/htdocs/scss/app/form.scss
@@ -1,4 +1,4 @@
-@import 'vars';
+@import '../vars';
$form-field-label-width: 120px;
@@ -35,7 +35,7 @@ form { display: block; margin: 0; }
font-size: 12px;
letter-spacing: 0.5px;
text-transform: uppercase;
- color: #888;
+ color: $grey;
}
.form-field {
//margin-left: $form-field-label-width + 10px;
diff --git a/htdocs/scss/app/head.scss b/htdocs/scss/app/head.scss
new file mode 100644
index 0000000..fed30d1
--- /dev/null
+++ b/htdocs/scss/app/head.scss
@@ -0,0 +1,157 @@
+.head {
+ padding: 0 $side-padding;
+}
+.head-inner {
+ //padding: 13px 0;
+ position: relative;
+ border-bottom: 2px $border-color solid;
+}
+.head-logo {
+ padding: 4px 0;
+ font-family: $ffMono;
+ font-size: 15px;
+ display: inline-block;
+ position: absolute;
+ left: 0;
+ background-color: transparent;
+}
+body:not(.theme-changing) .head-logo {
+ @include transition(background-color, 0.03s);
+}
+.head-logo {
+ padding: 16px 0;
+ background-color: $bg;
+}
+.head-logo:after {
+ content: '';
+ display: block;
+ width: 40px;
+ position: absolute;
+ right: -40px;
+ top: 0;
+ bottom: 0;
+ border-left: 8px $bg solid;
+ box-sizing: border-box;
+ background: linear-gradient(to left, rgba($bg, 0) 0%, rgba($bg, 1) 100%); /* W3C */
+}
+.head-logo > a {
+ color: $fg;
+ font-size: 14px;
+}
+.head-logo > a:hover {
+ text-decoration: none;
+}
+.head-logo-enter {
+ background: $code-block-bg;
+ color: $hljs_fg;
+ display: inline;
+ opacity: 0;
+ font-size: 11px;
+ position: relative;
+ padding: 2px 5px;
+ font-weight: normal;
+ vertical-align: middle;
+ top: -1px;
+}
+body:not(.theme-changing) .head-logo-enter {
+ @include transition(opacity, 0.03s);
+}
+.head-logo-enter-icon {
+ width: 12px;
+ height: 7px;
+ display: inline-block;
+ margin-right: 5px;
+ position: relative;
+ top: 1px;
+
+ > svg {
+ path {
+ fill: $hljs_fg;
+ }
+ }
+}
+.head-logo > a:hover .head-logo-enter {
+ opacity: 1;
+}
+.head-logo-path {
+ color: $fg;
+ font-weight: bold;
+ -webkit-font-smoothing: antialiased;
+}
+body:not(.theme-changing) .head-logo-path {
+ @include transition(color, 0.03s);
+}
+.head-logo > a:hover .head-logo-path:not(.alwayshover) {
+ color: $light_grey;
+}
+.head-logo-path:not(.neverhover):hover {
+ color: $fg !important;
+}
+.head-logo-dolsign {
+ color: $head_green_color;
+ font-weight: normal;
+ &.is_root {
+ color: $head_red_color;
+ }
+}
+.head-logo-cd {
+ display: none;
+}
+.head-logo > a:hover .head-logo-cd {
+ display: inline;
+}
+.head-logo-path-mapped {
+ padding: 3px 5px;
+ background: #f1f1f1;
+ pointer-events: none;
+ @include radius(3px);
+ margin: 0 2px;
+}
+
+.head-items {
+ float: right;
+ color: #777; // color of separators
+ //padding: 8px 0;
+}
+a.head-item {
+ color: $fg;
+ font-size: $fs - 1px;
+ display: block;
+ float: left;
+ padding: 16px 0;
+
+ > span {
+ position: relative;
+ padding: 0 12px;
+ border-right: 1px $head-items-separator solid;
+
+ > span {
+ padding: 2px 0;
+
+ > span.moon-icon {
+ position: absolute;
+ left: 0;
+
+ > svg path {
+ fill: $fg;
+ }
+ }
+ }
+ }
+
+ &.is-theme-switcher > span {
+ padding-left: 20px;
+ }
+
+ &:last-child > span {
+ border-right: 0;
+ padding-right: 1px;
+ }
+}
+a.head-item:hover {
+ //color: $link-color;
+ text-decoration: none;
+}
+a.head-item:hover > span > span {
+ border-bottom: 1px $head-items-separator solid;
+} \ No newline at end of file
diff --git a/htdocs/scss/app/hljs.scss b/htdocs/scss/app/hljs.scss
new file mode 100644
index 0000000..913c45e
--- /dev/null
+++ b/htdocs/scss/app/hljs.scss
@@ -0,0 +1 @@
+@import "../hljs/github.css";
diff --git a/htdocs/scss/mobile.scss b/htdocs/scss/app/mobile.scss
index d4d0d25..a765e25 100644
--- a/htdocs/scss/mobile.scss
+++ b/htdocs/scss/app/mobile.scss
@@ -1,4 +1,4 @@
-@import 'vars';
+@import '../vars';
textarea {
-webkit-overflow-scrolling: touch;
diff --git a/htdocs/scss/pages.scss b/htdocs/scss/app/pages.scss
index 873a6ae..873a6ae 100644
--- a/htdocs/scss/pages.scss
+++ b/htdocs/scss/app/pages.scss
diff --git a/htdocs/scss/bundle_admin.scss b/htdocs/scss/bundle_admin.scss
new file mode 100644
index 0000000..06808d0
--- /dev/null
+++ b/htdocs/scss/bundle_admin.scss
@@ -0,0 +1,3 @@
+.admin-page {
+ line-height: 155%;
+}
diff --git a/htdocs/scss/bundle_common.scss b/htdocs/scss/bundle_common.scss
new file mode 100644
index 0000000..dd0bd6a
--- /dev/null
+++ b/htdocs/scss/bundle_common.scss
@@ -0,0 +1,10 @@
+@import "./app/common";
+@import "./app/head";
+@import "./app/blog";
+@import "./app/form";
+@import "./app/pages";
+@import "./hljs/github.scss";
+
+@media screen and (max-width: 600px) {
+ @import "./app/mobile";
+}
diff --git a/htdocs/scss/colors/dark.scss b/htdocs/scss/colors/dark.scss
new file mode 100644
index 0000000..c7c25cf
--- /dev/null
+++ b/htdocs/scss/colors/dark.scss
@@ -0,0 +1,46 @@
+$head_green_color: #0bad19;
+$head_red_color: #e23636;
+$link-color: #71abe5;
+
+$grey: #798086;
+$dark-grey: $grey;
+$light-grey: $grey;
+$fg: #eee;
+$bg: #222;
+
+$code-block-bg: #394146;
+$inline-code-block-bg: #394146;
+
+$light-bg: #464c4e;
+$dark-bg: #272C2D;
+$dark-fg: #999;
+
+$input-border: #48535a;
+$input-border-focused: #48535a;
+$input-bg: #30373b;
+$border-color: #48535a;
+
+$error-block-bg: #f9eeee;
+$error-block-fg: #d13d3d;
+
+$success-block-bg: #eff5f0;
+$success-block-fg: #2a6f34;
+
+$head-items-separator: #5e6264;
+
+// colors from https://github.com/Kelbster/highlightjs-material-dark-theme/blob/master/css/materialdark.css
+$hljs_fg: #CDD3D8;
+$hljs_bg: #2B2B2D;
+$hljs_quote: #6272a4;
+$hljs_string: #f1fa8c;
+$hljs_literal: #bd93f9;
+$hljs_title: #75A5FF;
+$hljs_keyword: #C792EA;
+$hljs_type: #da4939;
+$hljs_tag: #abb2bf;
+$hljs_regexp: #F77669;
+$hljs_symbol: #C792EA;
+$hljs_builtin: #C792EA;
+$hljs_meta: #75A5FF;
+$hljs_deletion: #e6e1dc;
+$hljs_addition: #144212; \ No newline at end of file
diff --git a/htdocs/scss/colors/light.scss b/htdocs/scss/colors/light.scss
new file mode 100644
index 0000000..e6c14ba
--- /dev/null
+++ b/htdocs/scss/colors/light.scss
@@ -0,0 +1,46 @@
+$head_green_color: #0bad19;
+$head_red_color: #ce1a1a;
+$link-color: #116fd4;
+
+$grey: #888;
+$dark-grey: #777;
+$light-grey: #999;
+$fg: #222;
+$bg: #fff;
+
+$code-block-bg: #f3f3f3;
+$inline-code-block-bg: #f1f1f1;
+
+$light-bg: #464c4e;
+$dark-bg: #272C2D;
+$dark-fg: #999;
+
+$input-border: #e0e0e0;
+$input-border-focused: #e0e0e0;
+$input-bg: #f7f7f7;
+$border-color: #e0e0e0;
+
+$error-block-bg: #f9eeee;
+$error-block-fg: #d13d3d;
+
+$success-block-bg: #eff5f0;
+$success-block-fg: #2a6f34;
+
+$head-items-separator: #d0d0d0;
+
+// github.com style (c) Vasily Polovnyov <vast@whiteants.net>
+$hljs_fg: #333;
+$hljs_bg: #f8f8f8;
+$hljs_quote: #998;
+$hljs_string: #d14;
+$hljs_literal: #008080;
+$hljs_title: #900;
+$hljs_keyword: $hljs_fg;
+$hljs_type: #458;
+$hljs_tag: #000080;
+$hljs_regexp: #009926;
+$hljs_symbol: #990073;
+$hljs_builtin: #0086b3;
+$hljs_meta: #999;
+$hljs_deletion: #fdd;
+$hljs_addition: #dfd; \ No newline at end of file
diff --git a/htdocs/scss/common-bundle.scss b/htdocs/scss/common-bundle.scss
deleted file mode 100644
index 397f0c3..0000000
--- a/htdocs/scss/common-bundle.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-@import "./common.scss";
-@import "./blog.scss";
-@import "./form.scss";
-@import "./hljs/github.scss";
-@import "./pages.scss";
-
-@media screen and (max-width: 600px) {
- @import "./mobile.scss";
-}
diff --git a/htdocs/scss/entries/admin/dark.scss b/htdocs/scss/entries/admin/dark.scss
new file mode 100644
index 0000000..d704436
--- /dev/null
+++ b/htdocs/scss/entries/admin/dark.scss
@@ -0,0 +1,2 @@
+@import '../../colors/dark';
+@import '../../bundle_admin'; \ No newline at end of file
diff --git a/htdocs/scss/entries/admin/light.scss b/htdocs/scss/entries/admin/light.scss
new file mode 100644
index 0000000..4ac8bf0
--- /dev/null
+++ b/htdocs/scss/entries/admin/light.scss
@@ -0,0 +1,2 @@
+@import '../../colors/light';
+@import '../../bundle_admin'; \ No newline at end of file
diff --git a/htdocs/scss/entries/common/dark.scss b/htdocs/scss/entries/common/dark.scss
new file mode 100644
index 0000000..f983214
--- /dev/null
+++ b/htdocs/scss/entries/common/dark.scss
@@ -0,0 +1,2 @@
+@import '../../colors/dark';
+@import '../../bundle_common'; \ No newline at end of file
diff --git a/htdocs/scss/entries/common/light.scss b/htdocs/scss/entries/common/light.scss
new file mode 100644
index 0000000..a3044bd
--- /dev/null
+++ b/htdocs/scss/entries/common/light.scss
@@ -0,0 +1,2 @@
+@import '../../colors/light';
+@import '../../bundle_common'; \ No newline at end of file
diff --git a/htdocs/scss/hljs.scss b/htdocs/scss/hljs.scss
deleted file mode 100644
index 36f52de..0000000
--- a/htdocs/scss/hljs.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import "./hljs/github.css";
diff --git a/htdocs/scss/hljs/github.scss b/htdocs/scss/hljs/github.scss
index 791932b..ddbfc0a 100644
--- a/htdocs/scss/hljs/github.scss
+++ b/htdocs/scss/hljs/github.scss
@@ -1,27 +1,21 @@
-/*
-
-github.com style (c) Vasily Polovnyov <vast@whiteants.net>
-
-*/
-
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
- color: #333;
- background: #f8f8f8;
+ color: $hljs_fg;
+ background: $hljs_bg;
}
.hljs-comment,
.hljs-quote {
- color: #998;
+ color: $hljs_quote;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
- color: #333;
+ color: $hljs_fg;
font-weight: bold;
}
@@ -30,18 +24,18 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
- color: #008080;
+ color: $hljs_literal;
}
.hljs-string,
.hljs-doctag {
- color: #d14;
+ color: $hljs_string;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
- color: #900;
+ color: $hljs_title;
font-weight: bold;
}
@@ -51,43 +45,43 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
.hljs-type,
.hljs-class .hljs-title {
- color: #458;
+ color: $hljs_type;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
- color: #000080;
+ color: $hljs_tag;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
- color: #009926;
+ color: $hljs_regexp;
}
.hljs-symbol,
.hljs-bullet {
- color: #990073;
+ color: $hljs_symbol;
}
.hljs-built_in,
.hljs-builtin-name {
- color: #0086b3;
+ color: $hljs_builtin;
}
.hljs-meta {
- color: #999;
+ color: $hljs_meta;
font-weight: bold;
}
.hljs-deletion {
- background: #fdd;
+ background: $hljs_deletion;
}
.hljs-addition {
- background: #dfd;
+ background: $hljs_addition;
}
.hljs-emphasis {
diff --git a/htdocs/scss/vars.scss b/htdocs/scss/vars.scss
index e056740..cc67f04 100644
--- a/htdocs/scss/vars.scss
+++ b/htdocs/scss/vars.scss
@@ -4,42 +4,9 @@ $ff: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
$ffMono: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
$base-width: 900px;
-//$sb-width: 120px;
$side-padding: 25px;
$base-padding: 18px;
-
$footer-height: 64px;
-$head-green-color: #0bad19;
-$head-red-color: #ce1a1a;
-$link-color: #116fd4;
-
-$bg: #f7f7f7;
-$content-bg: #fff;
-$code-block-bg: #f3f3f3;
-$inline-code-block-bg: #f1f1f1;
-
-$fg: #222;
-$blue1: #729fcf;
-$blue2: #3465a4;
-$blue3: #204a87;
-$orange1: #fcaf3e;
-$orange2: #f57900;
-$orange3: #ce5c00;
-
-$light-bg: #464c4e;
-$dark-bg: #272C2D;
-$dark-fg: #999;
-
-$input-border: #e0e0e0;
-$input-border-focused: #e0e0e0;
-$input-bg: #f7f7f7;
-$border-color: #e0e0e0;
-
-$error-block-bg: #f9eeee;
-$error-block-fg: #d13d3d;
-
-$success-block-bg: #eff5f0;
-$success-block-fg: #2a6f34;
@mixin radius($radius) {
-o-border-radius: $radius;
@@ -50,9 +17,9 @@ $success-block-fg: #2a6f34;
}
@mixin transition($property, $duration, $easing: linear) {
- transition: $property $duration $easing;
- -webkit-transition: $property $duration $easing;
- -moz-transition: $property $duration $easing;
+ transition: $property $duration $easing;
+ -webkit-transition: $property $duration $easing;
+ -moz-transition: $property $duration $easing;
}
@mixin linearGradient($top, $bottom){
diff --git a/lang/en.php b/lang/en.php
index cf2af6c..5736104 100644
--- a/lang/en.php
+++ b/lang/en.php
@@ -20,6 +20,11 @@ return [
'delete' => 'delete',
'info_saved' => 'Information saved.',
+ // theme switcher
+ 'theme_auto' => 'auto',
+ 'theme_dark' => 'dark',
+ 'theme_light' => 'light',
+
// contacts
'contacts_email' => 'email',
'contacts_pgp' => 'OpenPGP public key',
diff --git a/model/Upload.php b/model/Upload.php
index 586be24..441a14d 100644
--- a/model/Upload.php
+++ b/model/Upload.php
@@ -142,7 +142,7 @@ class Upload extends Model {
if (is_file($dir.'/'.$f))
unlink($dir.'/'.$f);
else
- logError('deleteAllImagePreviews: '.$dir.'/'.$f.' is not a file!');
+ logError(__METHOD__.': '.$dir.'/'.$f.' is not a file!');
$deleted++;
}
}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..578f958
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,647 @@
+{
+ "name": "www",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "clean-css": "^5.3.0",
+ "clean-css-cli": "^5.6.0",
+ "css-patch": "^1.2.0"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/clean-css": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz",
+ "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==",
+ "dev": true,
+ "dependencies": {
+ "source-map": "~0.6.0"
+ },
+ "engines": {
+ "node": ">= 10.0"
+ }
+ },
+ "node_modules/clean-css-cli": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/clean-css-cli/-/clean-css-cli-5.6.0.tgz",
+ "integrity": "sha512-68vorNEG808D1QzeerO9AlwQVTuaR8YSK4aqwIsjJq0wDSyPH11ApHY0O+EQrdEGUZcN+d72v+Nn/gpxjAFewQ==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "clean-css": "^5.3.0",
+ "commander": "7.x",
+ "glob": "^7.1.6"
+ },
+ "bin": {
+ "cleancss": "bin/cleancss"
+ },
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/css-patch": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/css-patch/-/css-patch-1.2.0.tgz",
+ "integrity": "sha512-wCIyPGugTmf10KO39QLD3N+qIum0ljrj/8pJdULjjuXQ6oEeYd5+quMF7jIdnEL5Ftp0wmbvO8qPvAmzrw0EaA==",
+ "dev": true,
+ "dependencies": {
+ "diff": "^5.0.0",
+ "stylis": "^4.0.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.1.tgz",
+ "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==",
+ "dev": true
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ }
+ },
+ "dependencies": {
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "clean-css": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz",
+ "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==",
+ "dev": true,
+ "requires": {
+ "source-map": "~0.6.0"
+ }
+ },
+ "clean-css-cli": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/clean-css-cli/-/clean-css-cli-5.6.0.tgz",
+ "integrity": "sha512-68vorNEG808D1QzeerO9AlwQVTuaR8YSK4aqwIsjJq0wDSyPH11ApHY0O+EQrdEGUZcN+d72v+Nn/gpxjAFewQ==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^3.5.2",
+ "clean-css": "^5.3.0",
+ "commander": "7.x",
+ "glob": "^7.1.6"
+ }
+ },
+ "commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "css-patch": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/css-patch/-/css-patch-1.2.0.tgz",
+ "integrity": "sha512-wCIyPGugTmf10KO39QLD3N+qIum0ljrj/8pJdULjjuXQ6oEeYd5+quMF7jIdnEL5Ftp0wmbvO8qPvAmzrw0EaA==",
+ "dev": true,
+ "requires": {
+ "diff": "^5.0.0",
+ "stylis": "^4.0.13"
+ }
+ },
+ "diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "dev": true
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "stylis": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.1.tgz",
+ "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3abc792
--- /dev/null
+++ b/package.json
@@ -0,0 +1,7 @@
+{
+ "devDependencies": {
+ "clean-css": "^5.3.0",
+ "clean-css-cli": "^5.6.0",
+ "css-patch": "^1.2.0"
+ }
+}
diff --git a/prepare_static.php b/prepare_static.php
deleted file mode 100755
index e3663f8..0000000
--- a/prepare_static.php
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env php8.1
-<?php
-
-function gethash(string $path): string {
- return substr(sha1(file_get_contents($path)), 0, 8);
-}
-
-function sassc(string $src_dir, string $dst_dir, string $file): int {
- $cmd = 'sassc -t expanded '.escapeshellarg($src_dir.'/'.$file).' '.escapeshellarg($dst_dir.'/'.preg_replace('/\.scss$/', '.css', $file));
- exec($cmd, $output, $code);
- return $code;
-}
-
-require __DIR__.'/init.php';
-global $config;
-
-function build_static(): void {
- $css_dir = ROOT.'/htdocs/css';
- $hashes = [];
-
- if (!file_exists($css_dir))
- mkdir($css_dir);
-
- $files = ['common-bundle.scss', 'admin.scss'];
- foreach ($files as $file) {
- if (sassc(ROOT.'/htdocs/scss', $css_dir, $file) != 0)
- fwrite(STDERR, "error: could not compile $file\n");
- }
-
- foreach (['css', 'js'] as $type) {
- $reldir = ROOT.'/htdocs/';
- $files = glob_recursive($reldir.$type.'/*.'.$type);
- if (empty($files)) {
- continue;
- }
- foreach ($files as $file) {
- $name = preg_replace('/^'.preg_quote($reldir, '/').'/', '', $file);
- $hashes[$name] = gethash($file);
- }
- }
-
- $scfg = "<?php\n\n";
- $scfg .= "return ".var_export($hashes, true).";\n";
-
- file_put_contents(ROOT.'/config-static.php', $scfg);
-}
-
-build_static(); \ No newline at end of file
diff --git a/skin/base.skin.php b/skin/base.skin.php
index ab7d689..2be71dc 100644
--- a/skin/base.skin.php
+++ b/skin/base.skin.php
@@ -2,7 +2,17 @@
namespace skin\base;
-function layout($ctx, $title, $unsafe_body, $static, $meta, $js, $opts, $exec_time, $unsafe_lang) {
+use admin;
+use RequestDispatcher;
+
+function layout($ctx, $title, $unsafe_body, $static, $meta, $js, $opts, $exec_time, $unsafe_lang, $theme) {
+global $config;
+$app_config = json_encode([
+ 'domain' => $config['domain'],
+ 'devMode' => $config['is_dev'],
+ 'cookieHost' => $config['cookie_host'],
+]);
+
return <<<HTML
<!doctype html>
<html lang="en">
@@ -12,16 +22,16 @@ return <<<HTML
<link rel="shortcut icon" href="/favicon.ico?4" type="image/x-icon">
<link rel="alternate" type="application/rss+xml" href="/feed.rss">
<title>{$title}</title>
+ <script type="text/javascript">window.appConfig = {$app_config};</script>
{$ctx->renderMeta($meta)}
- {$ctx->renderStatic($static)}
+ {$ctx->renderStatic($static, $theme)}
</head>
<body{$ctx->if_true($opts['full_width'], ' class="full-width"')}>
- {$ctx->renderHeader(renderLogo($ctx, $opts['logo_path_map'], $opts['logo_link_map']))}
+ {$ctx->renderHeader($theme, renderLogo($ctx, $opts['logo_path_map'], $opts['logo_link_map']))}
<div class="page-content base-width">
<div class="page-content-inner">{$unsafe_body}</div>
</div>
- {$ctx->if_true($js != '' || !empty($lang) || $opts['dynlogo_enabled'],
- $ctx->renderScript, $js, $unsafe_lang, $opts['dynlogo_enabled'])}
+ {$ctx->renderScript($js, $unsafe_lang, $opts['dynlogo_enabled'])}
</body>
</html>
<!--
@@ -32,11 +42,18 @@ HTML;
}
function renderScript($ctx, $unsafe_js, $unsafe_lang, $enable_dynlogo) {
+global $config;
+
+$styles = json_encode($ctx->styleNames);
+$versions = !$config['is_dev'] ? json_encode($config['static']) : '{}';
+
return <<<HTML
<script type="text/javascript">
+StaticManager.setStyles({$styles}, {$versions});
{$ctx->if_true($unsafe_js, '(function(){'.$unsafe_js.'})();')}
{$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')}
{$ctx->if_true($enable_dynlogo, 'DynamicLogo.init();')}
+ThemeSwitcher.init();
</script>
HTML;
}
@@ -53,26 +70,76 @@ function renderMeta($ctx, $meta) {
}, $meta));
}
-function renderStatic($ctx, $static) {
+function renderStatic($ctx, $static, $theme) {
global $config;
$html = [];
+ $dark = $theme == 'dark';
+ $ctx->styleNames = [];
foreach ($static as $name) {
// list($name, $options) = $item;
$version = $config['is_dev'] ? time() : $config['static'][substr($name, 1)] ?? 'notfound';
if (str_ends_with($name, '.js'))
$html[] = jsLink($name, $version);
- else if (str_ends_with($name, '.css'))
- $html[] = cssLink($name, $version/*, $options*/);
+ else if (str_ends_with($name, '.css')) {
+ $html[] = cssLink($name, 'light', $version, $style_name);
+ $ctx->styleNames[] = $style_name;
+
+ if ($dark)
+ $html[] = cssLink($name, 'dark', $version, $style_name);
+ else if (!$config['is_dev'])
+ $html[] = cssPrefetchLink(str_replace('.css', '_dark.css', $name), $version);
+ }
}
return implode("\n", $html);
}
-function renderHeader($ctx, $unsafe_logo_html) {
+function jsLink(string $name, $version = null): string {
+ if ($version !== null)
+ $name .= '?'.$version;
+ return '<script src="'.$name.'" type="text/javascript"></script>';
+}
+
+function cssLink(string $name, string $theme, $version = null, &$bname = null): string {
+ global $config;
+
+ $dname = dirname($name);
+ $bname = basename($name);
+ if (($pos = strrpos($bname, '.')))
+ $bname = substr($bname, 0, $pos);
+
+ if ($config['is_dev']) {
+ $href = '/sass.php?name='.urlencode($bname).'&amp;theme='.$theme;
+ } else {
+ $href = $dname.'/'.$bname.($theme == 'dark' ? '_dark' : '').'.css'.($version !== null ? '?'.$version : '');
+ }
+ $id = 'style_'.$bname;
+ if ($theme == 'dark')
+ $id .= '_dark';
+ return '<link rel="stylesheet" id="'.$id.'" type="text/css" href="'.$href.'">';
+}
+
+function cssPrefetchLink(string $name, $verison = null): string {
+$url = $name;
+if ($verison)
+ $url .= '?'.$verison;
+return <<<HTML
+<link rel="prefetch" href="{$url}" />
+HTML;
+}
+
+function renderHeader($ctx, $theme, $unsafe_logo_html) {
return <<<HTML
<div class="head base-width">
<div class="head-inner clearfix">
<div class="head-logo">{$unsafe_logo_html}</div>
<div class="head-items clearfix">
+ <a class="head-item is-theme-switcher" href="javascript:void(0)" onclick="return ThemeSwitcher.next(event)">
+ <span>
+ <span>
+ <span class="moon-icon">{$ctx->renderMoonIcon()}</span><span id="theme-switcher-label">{$theme}</span>
+ </span>
+ </span>
+ </a>
<a class="head-item" href="/"><span><span>blog</span></span></a>
<a class="head-item" href="/projects/"><span><span>projects</span></span></a>
<a class="head-item" href="https://git.ch1p.io/?s=idle"><span><span>git</span></span></a>
@@ -87,9 +154,9 @@ HTML;
// TODO rewrite this fcking crap
function renderLogo($ctx, array $path_map = [], array $link_map = []): string {
- $uri = \RequestDispatcher::path();
+ $uri = RequestDispatcher::path();
- if (!\admin::isAdmin()) {
+ if (!admin::isAdmin()) {
$prompt_sign = '<span class="head-logo-dolsign">$</span>';
} else {
$prompt_sign = '<span class="head-logo-dolsign is_root">#</span>';
@@ -143,7 +210,7 @@ function renderLogo($ctx, array $path_map = [], array $link_map = []): string {
$last_pos = $pos + 1;
$close_tags++;
}
- $html .= str_repeat('</span>', $close_tags).' '.$prompt_sign.' <span class="head-logo-cd">cd <span id="head_cd_text">~</span> <span class="head-logo-enter"><span class="head-logo-enter-icon"></span>Enter</span></span></a>';
+ $html .= str_repeat('</span>', $close_tags).' '.$prompt_sign.' <span class="head-logo-cd">cd <span id="head_cd_text">~</span> <span class="head-logo-enter"><span class="head-logo-enter-icon">'.enterIcon().'</span>Enter</span></span></a>';
for ($i = count($path_parts)-1, $j = 0; $i >= 0; $i--, $j++) {
if (isset($path_map[$j])) {
@@ -169,25 +236,16 @@ function renderLogo($ctx, array $path_map = [], array $link_map = []): string {
return $html;
}
-function jsLink(string $name, $version = null): string {
- if ($version !== null)
- $name .= '?'.$version;
- return '<script src="'.$name.'" type="text/javascript"></script>';
+function enterIcon() {
+return <<<SVG
+<svg width="12" height="7" viewBox="0 0 9.6 5.172" xmlns="http://www.w3.org/2000/svg">
+ <path d="M.4 2.586l2.779 2.8.648-.654-1.667-1.68H9.2V.253h-.926V2.12H2.16L3.827.44 3.18-.214z"/>
+</svg>
+SVG;
}
-function cssLink(string $name, $version = null/*, $options = null*/): string {
- global $config;
- if ($config['is_dev']) {
- $bname = basename($name);
- if (($pos = strrpos($bname, '.')))
- $bname = substr($bname, 0, $pos);
- $href = '/sass.php?name='.urlencode($bname);
- } else {
- $href = $name.($version !== null ? '?'.$version : '');
- }
- $s = '<link rel="stylesheet" type="text/css" href="'.$href.'"';
- // if (!is_null($options))
- // $s .= ' media="'.$options.'"';
- $s .= '>';
- return $s;
+function renderMoonIcon($ctx) {
+return <<<SVG
+<svg width="18" height="18" xmlns="http://www.w3.org/2000/svg"><path d="M14.54 10.37a5.4 5.4 0 01-6.91-6.91.59.59 0 00-.74-.75 6.66 6.66 0 00-2.47 1.54 6.6 6.6 0 1010.87 6.86.59.59 0 00-.75-.74zm-1.61 2.39a5.44 5.44 0 01-7.69-7.69 5.58 5.58 0 011-.76 6.55 6.55 0 007.47 7.47 5.15 5.15 0 01-.78.98z" fill-rule="evenodd" /></svg>
+SVG;
} \ No newline at end of file