From bf3400b878ab20f3fa04bba55218a4f90eeba952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gir=C3=B3n?= Date: Fri, 7 Jun 2024 13:19:40 +0200 Subject: [PATCH] web: improve style for mobile view (#393) * move stylecss to beautified file * beware! remove unused code * tabs to space * add responsive classes * fix td border * add to advanced web * enlarge checkboxes * use common header func for confirmation html * fix checkbox center align * update const css code * move css to resources folder --- resources/README.md | 4 + resources/style.css | 233 ++++++++++++++++++++++++++++++++++++ src/WebCfgServer.cpp | 35 +++--- src/WebCfgServerConstants.h | 2 +- 4 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 resources/README.md create mode 100644 resources/style.css diff --git a/resources/README.md b/resources/README.md new file mode 100644 index 0000000..4a583a4 --- /dev/null +++ b/resources/README.md @@ -0,0 +1,4 @@ +## Resources + +Any files used along with application source code, but not to be mixed with C/C++ code. +This can include CSS, JS, favicon, etc. diff --git a/resources/style.css b/resources/style.css new file mode 100644 index 0000000..6f1cce1 --- /dev/null +++ b/resources/style.css @@ -0,0 +1,233 @@ +/* +escaped by https://www.cescaper.com/ +source: https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css +*/ + +/* + * Usage: + * Compact / minify this code with any tool + * Copy one-line string to src/WebCfgServerConstants.h as stylecss + * TODO: automate this process upon building :) +*/ + +:root { + --nc-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --nc-font-mono: Consolas, monaco, 'Ubuntu Mono', 'Liberation Mono', 'Courier New', Courier, monospace; + --nc-tx-1: #000000; + --nc-tx-2: #1A1A1A; + --nc-bg-1: #FFFFFF; + --nc-bg-2: #F6F8FA; + --nc-bg-3: #E5E7EB; + --nc-lk-1: #0070F3; + --nc-lk-2: #0366D6; + --nc-lk-tx: #FFFFFF; + --nc-ac-1: #79FFE1; + --nc-ac-tx: #0C4047 +} + +@media (prefers-color-scheme:dark) { + :root { + --nc-tx-1: #ffffff; + --nc-tx-2: #eeeeee; + --nc-bg-1: #000000; + --nc-bg-2: #111111; + --nc-bg-3: #222222; + --nc-lk-1: #3291FF; + --nc-lk-2: #0070F3; + --nc-lk-tx: #FFFFFF; + --nc-ac-1: #7928CA; + --nc-ac-tx: #FFFFFF + } +} + +* { + margin: 0; + padding: 0 +} + +img, +input, +option, +p, +table, +textarea, +ul { + margin-bottom: 1rem +} + +button, +html, +input, +select { + font-family: var(--nc-font-sans) +} + +body { + margin: 0 auto; + max-width: 750px; + padding: 2rem; + border-radius: 6px; + overflow-x: hidden; + word-break: normal; + overflow-wrap: anywhere; + background: var(--nc-bg-1); + color: var(--nc-tx-2); + font-size: 1.03rem; + line-height: 1.5 +} + +::selection { + background: var(--nc-ac-1); + color: var(--nc-ac-tx) +} + +h1, h2, h3, h4, h5, h6 { + line-height: 1; + color: var(--nc-tx-1); + padding-top: .875rem +} + +h1, h2, h3 { + color: var(--nc-tx-1); + padding-bottom: 2px; + margin-bottom: 8px; + border-bottom: 1px solid var(--nc-bg-2) +} + +h4, h5, h6 { + margin-bottom: .3rem +} + +h1 { font-size: 2.25rem } +h2 { font-size: 1.85rem } +h3 { font-size: 1.55rem } +h4 { font-size: 1.25rem } +h5 { font-size: 1rem } +h6 { font-size: .875rem } +a { color: var(--nc-lk-1) } +a:hover { color: var(--nc-lk-2) } +abbr { cursor: help } +abbr:hover { cursor: help } + +a button, +button, +input[type=button], +input[type=reset], +input[type=submit] { + font-size: 1rem; + display: inline-block; + padding: 6px 12px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background: var(--nc-lk-1); + color: var(--nc-lk-tx); + border: 0; + border-radius: 4px; + box-sizing: border-box; + cursor: pointer; + color: var(--nc-lk-tx) +} + +a button[disabled], +button[disabled], +input[type=button][disabled], +input[type=reset][disabled], +input[type=submit][disabled] { + cursor: default; + opacity: .5; + cursor: not-allowed +} + +.button:focus, +.button:hover, +button:focus, +button:hover, +input[type=button]:focus, +input[type=button]:hover, +input[type=reset]:focus, +input[type=reset]:hover, +input[type=submit]:focus, +input[type=submit]:hover { + background: var(--nc-lk-2) +} + +table { + border-collapse: collapse; + width: 100% +} + +td, th { + border: 1px solid var(--nc-bg-3); + text-align: left; + padding: .5rem +} + +th { background: var(--nc-bg-2) } +tr:nth-child(even) { background: var(--nc-bg-2) } + +textarea { max-width: 100% } + +input, select, textarea { + padding: 6px 12px; + margin-bottom: .5rem; + background: var(--nc-bg-2); + color: var(--nc-tx-2); + border: 1px solid var(--nc-bg-3); + border-radius: 4px; + box-shadow: none; + box-sizing: border-box +} + +img { max-width: 100% } + +td>input { + margin-top: 0px; + margin-bottom: 0px +} + +td>textarea { + margin-top: 0px; + margin-bottom: 0px +} + +td>select { + margin-top: 0px; + margin-bottom: 0px +} + +#tblnav td, th { + border: 0; + border-bottom: 1px solid; +} + +.tdbtn { + text-align: center; + vertical-align: middle; +} + +.warning { + color: #f00; +} + +@media only screen and (max-width: 600px) { + .adapt td { display: block; } + + .adapt input[type=text], + .adapt input[type=password], + .adapt input[type=submit], + .adapt textarea, + .adapt select { width: 100%; } + + .adapt td:has(input[type=checkbox]) { + text-align: center; + } + + .adapt input[type=checkbox] { + width: 1.5em; + height: 1.5em; + } + + .adapt table td:first-child { border-bottom: 0; } + .adapt table td:last-child { border-top: 0; } +} diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index b2e180b..1cb7e7c 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -1174,7 +1174,7 @@ void WebCfgServer::buildCredHtml(String &response) { buildHtmlHeader(response); - response.concat("
"); + response.concat(""); response.concat("

Credentials

"); response.concat(""); printInputField(response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, false, true); @@ -1186,7 +1186,7 @@ void WebCfgServer::buildCredHtml(String &response) if(_nuki != nullptr) { - response.concat("

"); + response.concat("

"); response.concat("

Nuki Lock PIN

"); response.concat("
"); printInputField(response, "NUKIPIN", "PIN Code (# to clear)", "*", 20, true); @@ -1197,7 +1197,7 @@ void WebCfgServer::buildCredHtml(String &response) if(_nukiOpener != nullptr) { - response.concat("

"); + response.concat("

"); response.concat("

Nuki Opener PIN

"); response.concat("
"); printInputField(response, "NUKIOPPIN", "PIN Code (# to clear)", "*", 20, true); @@ -1210,7 +1210,7 @@ void WebCfgServer::buildCredHtml(String &response) if(_nuki != nullptr) { response.concat("

Unpair Nuki Lock

"); - response.concat(""); + response.concat(""); response.concat("
"); String message = "Type "; message.concat(_confirmCode); @@ -1223,7 +1223,7 @@ void WebCfgServer::buildCredHtml(String &response) if(_nukiOpener != nullptr) { response.concat("

Unpair Nuki Opener

"); - response.concat(""); + response.concat(""); response.concat("
"); String message = "Type "; message.concat(_confirmCode); @@ -1234,8 +1234,8 @@ void WebCfgServer::buildCredHtml(String &response) } response.concat("

Factory reset Nuki Hub

"); - response.concat("

This will reset all settings to default and unpair Nuki Lock and/or Opener. Optionally will also reset WiFi settings and reopen WiFi manager portal.

"); - response.concat(""); + response.concat("

This will reset all settings to default and unpair Nuki Lock and/or Opener. Optionally will also reset WiFi settings and reopen WiFi manager portal.

"); + response.concat(""); response.concat("
"); String message = "Type "; message.concat(_confirmCode); @@ -1301,7 +1301,7 @@ void WebCfgServer::buildOtaCompletedHtml(String &response) void WebCfgServer::buildMqttConfigHtml(String &response) { buildHtmlHeader(response); - response.concat(""); + response.concat(""); response.concat("

Basic MQTT and Network Configuration

"); response.concat("
"); printInputField(response, "HOSTNAME", "Host name", _preferences->getString(preference_hostname).c_str(), 100); @@ -1348,9 +1348,9 @@ void WebCfgServer::buildMqttConfigHtml(String &response) void WebCfgServer::buildAdvancedConfigHtml(String &response) { buildHtmlHeader(response); - response.concat(""); + response.concat(""); response.concat("

Advanced Configuration

"); - response.concat("

Warning: Changing these settings can lead to bootloops that might require you to erase the ESP32 and reflash nukihub using USB/serial

"); + response.concat("

Warning: Changing these settings can lead to bootloops that might require you to erase the ESP32 and reflash nukihub using USB/serial

"); response.concat("
"); response.concat("
Current bootloop prevention state"); response.concat(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled"); @@ -1466,7 +1466,7 @@ void WebCfgServer::buildAccLvlHtml(String &response) if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) { printCheckBox(response, "KPPUB", "Publish keypad entries information", _preferences->getBool(preference_keypad_info_enabled), ""); - printCheckBox(response, "KPCODE", "Also publish keypad codes (Disadvised for security reasons)", _preferences->getBool(preference_keypad_publish_code, false), ""); + printCheckBox(response, "KPCODE", "Also publish keypad codes (Disadvised for security reasons)", _preferences->getBool(preference_keypad_publish_code, false), ""); printCheckBox(response, "KPENA", "Add, modify and delete keypad codes", _preferences->getBool(preference_keypad_control_enabled), ""); } printCheckBox(response, "TCPUB", "Publish time control entries information", _preferences->getBool(preference_timecontrol_info_enabled), ""); @@ -1623,7 +1623,7 @@ void WebCfgServer::buildNukiConfigHtml(String &response) { buildHtmlHeader(response); - response.concat(""); + response.concat(""); response.concat("

Basic Nuki Configuration

"); response.concat(""); printCheckBox(response, "LOCKENA", "Nuki Smartlock enabled", _preferences->getBool(preference_lock_enabled), ""); @@ -1688,17 +1688,10 @@ void WebCfgServer::buildGpioConfigHtml(String &response) void WebCfgServer::buildConfirmHtml(String &response, const String &message, uint32_t redirectDelay) { String delay(redirectDelay); + String header = ""; - response.concat("\n"); - response.concat("\n"); - response.concat("Nuki Hub\n"); - response.concat(""); - response.concat("\n\n"); - response.concat("\n"); + buildHtmlHeader(response, header); response.concat(message); - response.concat(""); } diff --git a/src/WebCfgServerConstants.h b/src/WebCfgServerConstants.h index 6b2785d..a9031ac 100644 --- a/src/WebCfgServerConstants.h +++ b/src/WebCfgServerConstants.h @@ -3,7 +3,7 @@ // escaped by https://www.cescaper.com/ // source: https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css -const char stylecss[] = ":root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000000;--nc-tx-2:#1A1A1A;--nc-bg-1:#FFFFFF;--nc-bg-2:#F6F8FA;--nc-bg-3:#E5E7EB;--nc-lk-1:#0070F3;--nc-lk-2:#0366D6;--nc-lk-tx:#FFFFFF;--nc-ac-1:#79FFE1;--nc-ac-tx:#0C4047}@media (prefers-color-scheme:dark){:root{--nc-tx-1:#ffffff;--nc-tx-2:#eeeeee;--nc-bg-1:#000000;--nc-bg-2:#111111;--nc-bg-3:#222222;--nc-lk-1:#3291FF;--nc-lk-2:#0070F3;--nc-lk-tx:#FFFFFF;--nc-ac-1:#7928CA;--nc-ac-tx:#FFFFFF}}*{margin:0;padding:0}address,area,article,aside,audio,blockquote,datalist,details,dl,fieldset,figure,iframe,img,input,meter,nav,ol,optgroup,option,output,p,pre,progress,ruby,section,table,textarea,ul,video{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:normal;overflow-wrap:anywhere;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr:hover{cursor:help}blockquote{padding:1.5rem;background:var(--nc-bg-2);border-left:5px solid var(--nc-bg-3)}abbr{cursor:help}blockquote :last-child{padding-bottom:0;margin-bottom:0}header{background:var(--nc-bg-2);border-bottom:1px solid var(--nc-bg-3);padding:2rem 1.5rem;margin:-2rem calc(0px - (50vw - 50%)) 2rem;padding-left:calc(50vw - 50%);padding-right:calc(50vw - 50%)}header h1,header h2,header h3{padding-bottom:0;border-bottom:0}header>:first-child{margin-top:0;padding-top:0}header>:last-child{margin-bottom:0}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}code,kbd,pre,samp{font-family:var(--nc-font-mono)}code,kbd,pre,samp{background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px;padding:3px 6px;font-size:.9rem}kbd{border-bottom:3px solid var(--nc-bg-3)}pre{padding:1rem 1.4rem;max-width:100%;overflow:auto}pre code{background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}code pre{display:inline;background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}details{padding:.6rem 1rem;background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px}summary{cursor:pointer;font-weight:700}details[open]{padding-bottom:.75rem}details[open] summary{margin-bottom:6px}details[open]>:last-child{margin-bottom:0}dt{font-weight:700}dd::before{content:'→ '}hr{border:0;border-bottom:1px solid var(--nc-bg-3);margin:1rem auto}fieldset{margin-top:1rem;padding:2rem;border:1px solid var(--nc-bg-3);border-radius:4px}legend{padding:0.5rem}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}table caption{font-weight:700;margin-bottom:.5rem}textarea{max-width:100%}ol,ul{padding-left:2rem}li{margin-top:.4rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}mark{padding:3px 6px;background:var(--nc-ac-1);color:var(--nc-ac-tx)}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}td>input{margin-top:0px;margin-bottom:0px}td>textarea{margin-top:0px;margin-bottom:0px}td>select{margin-top:0px;margin-bottom:0px}#tblnav td,th{border:0;border-bottom:1px solid;}.tdbtn{text-align:center;vertical-align: middle;}"; +const char stylecss[] = ":root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000;--nc-tx-2:#1a1a1a;--nc-bg-1:#fff;--nc-bg-2:#f6f8fa;--nc-bg-3:#e5e7eb;--nc-lk-1:#0070f3;--nc-lk-2:#0366d6;--nc-lk-tx:#fff;--nc-ac-1:#79ffe1;--nc-ac-tx:#0c4047}@media(prefers-color-scheme:dark){:root{--nc-tx-1:#fff;--nc-tx-2:#eee;--nc-bg-1:#000;--nc-bg-2:#111;--nc-bg-3:#222;--nc-lk-1:#3291ff;--nc-lk-2:#0070f3;--nc-lk-tx:#fff;--nc-ac-1:#7928ca;--nc-ac-tx:#fff}}*{margin:0;padding:0}img,input,option,p,table,textarea,ul{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:normal;overflow-wrap:anywhere;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr{cursor:help}abbr:hover{cursor:help}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}textarea{max-width:100%}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}td>input{margin-top:0;margin-bottom:0}td>textarea{margin-top:0;margin-bottom:0}td>select{margin-top:0;margin-bottom:0}#tblnav td,th{border:0;border-bottom:1px solid}.tdbtn{text-align:center;vertical-align:middle}.warning{color:red}@media only screen and (max-width:600px){.adapt td{display:block}.adapt input[type=text],.adapt input[type=password],.adapt input[type=submit],.adapt textarea,.adapt select{width:100%}.adapt td:has(input[type=checkbox]){text-align:center}.adapt input[type=checkbox]{width:1.5em;height:1.5em}.adapt table td:first-child{border-bottom:0}.adapt table td:last-child{border-top:0}}"; // converted to char array by https://notisrac.github.io/FileToCArray/ const unsigned char favicon_32x32[] = {