From c06b1aafdad85de0f84e6e72b05e49215b15e493 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 10 Jul 2024 15:48:50 -0600 Subject: [PATCH] Kdots dark mode --- api/src/Framework.php | 6 +- kdots/assets/styles/framework.css | 27 +++++++ kdots/assets/styles/framework.less | 14 +++- kdots/assets/styles/framework_dark.less | 28 ++++++++ kdots/head.tpl | 2 +- kdots/inc/class.kdots_framework.inc.php | 16 +++++ kdots/js/EgwDarkmodeToggle.ts | 94 +++++++++++++++++++++++++ kdots/js/EgwFramework.ts | 23 ++++++ kdots/js/EgwFrameworkApp.styles.ts | 6 +- kdots/js/app.ts | 7 +- 10 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 kdots/assets/styles/framework_dark.less create mode 100644 kdots/js/EgwDarkmodeToggle.ts diff --git a/api/src/Framework.php b/api/src/Framework.php index 8b7afc152b..3715879e2d 100644 --- a/api/src/Framework.php +++ b/api/src/Framework.php @@ -1229,11 +1229,11 @@ abstract class Framework extends Framework\Extra $topmenu_info_items = [ 'user_avatar' => $this->_user_avatar_menu(), 'update' => ($update = Framework\Updates::notification()) ? $update : null, - 'logout' => (Header\UserAgent::mobile()) ? self::_logout_menu() : null, - 'notifications' => ($GLOBALS['egw_info']['user']['apps']['notifications']) ? self::_get_notification_bell() : null, + 'logout' => (Header\UserAgent::mobile()) ? static::_logout_menu() : null, + 'notifications' => ($GLOBALS['egw_info']['user']['apps']['notifications']) ? static::_get_notification_bell() : null, 'quick_add' => $vars['quick_add'], 'print_title' => $this->_print_menu(), - 'darkmode' => self::_darkmode_menu() + 'darkmode' => static::_darkmode_menu() ]; // array of topmenu items (orders of the items matter) diff --git a/kdots/assets/styles/framework.css b/kdots/assets/styles/framework.css index 86b885e64d..4caed4c30d 100644 --- a/kdots/assets/styles/framework.css +++ b/kdots/assets/styles/framework.css @@ -1,3 +1,29 @@ +/** + * kDots main styles + * + * Note that light / dark colors should go in framework_light.less & framework_dark.less + */ +/** Theme customisations **/ +html[data-darkmode="true"] body { + background-color: black; + color: var(--sl-color-neutral-700); + /*** HEADER ***/ + /*** APPLICATION ***/ + /*** End APPLICATION ***/ + /*** WIDGETS ***/ + /* This should mostly go away with webcomponents */ + /** End WIDGETS **/ +} +html[data-darkmode="true"] body #egw_fw_topmenu_info_items #topmenu_info_timer:before { + filter: initial; +} +html[data-darkmode="true"] body egw-app { + --application-header-text-color: var(--sl-color-neutral-700); +} +html[data-darkmode="true"] body .nextmatch_sortheader { + color: #96bcd9; +} +/** End theme customisations **/ html, body { width: 100vw; @@ -118,6 +144,7 @@ egw-framework#egw_fw_basecontainer .egw_fw_ui_sidemenu_entry_header { #egw_fw_topmenu_info_items #topmenu_info_timer #topmenu_timer { display: block; width: 100%; + z-index: 2; } #egw_fw_topmenu_info_items #topmenu_info_timer:hover { cursor: pointer; diff --git a/kdots/assets/styles/framework.less b/kdots/assets/styles/framework.less index b5b7b3c54e..c7307f85ff 100644 --- a/kdots/assets/styles/framework.less +++ b/kdots/assets/styles/framework.less @@ -1,3 +1,14 @@ +/** + * kDots main styles + * + * Note that light / dark colors should go in framework_light.less & framework_dark.less + */ + + +/** Theme customisations **/ +@import "./framework_dark.less"; +/** End theme customisations **/ + html, body { width: 100vw; height: 100vh; @@ -6,7 +17,6 @@ html, body { margin: 0px; /** Messages **/ - .sl-toast-stack { top: auto; bottom: 0px; @@ -140,6 +150,7 @@ egw-framework#egw_fw_basecontainer { #topmenu_timer { display: block; width: 100%; + z-index: 2; } &:hover { @@ -397,5 +408,4 @@ div.et2_nextmatch { } - /*** END WIDGETS ***/ \ No newline at end of file diff --git a/kdots/assets/styles/framework_dark.less b/kdots/assets/styles/framework_dark.less new file mode 100644 index 0000000000..4628fbe388 --- /dev/null +++ b/kdots/assets/styles/framework_dark.less @@ -0,0 +1,28 @@ +html[data-darkmode="true"] body { + background-color: black; + color: var(--sl-color-neutral-700); + + /*** HEADER ***/ + #egw_fw_topmenu_info_items { + #topmenu_info_timer:before { + filter:initial; + } + } + + /*** APPLICATION ***/ + + egw-app { + --application-header-text-color: var(--sl-color-neutral-700); + } + + /*** End APPLICATION ***/ + + /*** WIDGETS ***/ + /* This should mostly go away with webcomponents */ + + .nextmatch_sortheader { + color: #96bcd9; + } + + /** End WIDGETS **/ +} \ No newline at end of file diff --git a/kdots/head.tpl b/kdots/head.tpl index ab6a686420..9ad8222ba8 100644 --- a/kdots/head.tpl +++ b/kdots/head.tpl @@ -34,7 +34,7 @@ {include_wz_tooltip} - Site logo diff --git a/kdots/inc/class.kdots_framework.inc.php b/kdots/inc/class.kdots_framework.inc.php index 581d5720ae..9e879162b3 100644 --- a/kdots/inc/class.kdots_framework.inc.php +++ b/kdots/inc/class.kdots_framework.inc.php @@ -76,6 +76,9 @@ class kdots_framework extends Api\Framework\Ajax case 'user_avatar': $vars['topmenu_info_items'] .= "
$item
{$vars['topmenu_items']}
"; break; + case 'darkmode': + $vars['topmenu_info_items'] .= $item; + break; default: $vars['topmenu_info_items'] .= '\n"; @@ -174,4 +177,17 @@ class kdots_framework extends Api\Framework\Ajax return $ret; } + + + /** + * Returns darkmode menu + * + * @return string + */ + protected static function _darkmode_menu() + { + $mode = $GLOBALS['egw_info']['user']['preferences']['common']['darkmode'] == 1 ? 'dark' : 'light'; + return ' '; + } } diff --git a/kdots/js/EgwDarkmodeToggle.ts b/kdots/js/EgwDarkmodeToggle.ts new file mode 100644 index 0000000000..4ec175bd2a --- /dev/null +++ b/kdots/js/EgwDarkmodeToggle.ts @@ -0,0 +1,94 @@ +import {css, html, LitElement} from "lit"; +import {customElement} from "lit/decorators/custom-element.js"; +import {property} from "lit/decorators/property.js"; + +/** + * @summary System message + * + * @dependency sl-alert + * @dependency sl-icon + * + * @slot - Content + * @slot icon - An icon to show in the message + * + * @csspart base - Wraps it all. + * @csspart icon - + */ +@customElement('egw-darkmode-toggle') +export class EgwDarkmodeToggle extends LitElement +{ + static get styles() + { + return [ + css` + sl-icon-button::part(base) { + padding: 0; + } + ` + ]; + } + + @property({type: Boolean}) + darkmode = false; + + private _initialValue = false; + + constructor() + { + super(); + this._initialValue = window.matchMedia("(prefers-color-scheme: dark)").matches; + this.handleDarkmodeChange = this.handleDarkmodeChange.bind(this); + } + + connectedCallback() + { + super.connectedCallback(); + this.toggleDarkmode(this.hasAttribute("darkmode") ? this.darkmode : this._initialValue); + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", this.handleDarkmodeChange); + } + + disconnectedCallback() + { + super.disconnectedCallback(); + window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", this.handleDarkmodeChange); + } + + public toggleDarkmode(force = null) + { + if(force == null) + { + force = !(document.documentElement.getAttribute("data-darkmode") == "true"); + } + this.darkmode = force; + if(force) + { + document.documentElement.setAttribute("data-darkmode", "true"); + } + else + { + document.documentElement.setAttribute("data-darkmode", "0"); + } + // Set class for Shoelace + document.documentElement.classList.toggle("sl-theme-dark", this.darkmode); + this.requestUpdate("darkmode") + this.updateComplete.then(() => + { + this.dispatchEvent(new CustomEvent("egw-darkmode-change", {bubbles: true})); + }); + } + + handleDarkmodeChange(e) + { + this.toggleDarkmode(e.matches ? "dark" : "light"); + } + + render() : unknown + { + return html` + {this.toggleDarkmode()}} + > + `; + } + +} \ No newline at end of file diff --git a/kdots/js/EgwFramework.ts b/kdots/js/EgwFramework.ts index 6ac7dc3369..dca714a7ac 100644 --- a/kdots/js/EgwFramework.ts +++ b/kdots/js/EgwFramework.ts @@ -111,6 +111,11 @@ export class EgwFramework extends LitElement private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");} + constructor() + { + super(); + this.handleDarkmodeChange = this.handleDarkmodeChange.bind(this); + } connectedCallback() { super.connectedCallback(); @@ -124,6 +129,15 @@ export class EgwFramework extends LitElement // Override framework setSidebox, use arrow function to force context this.egw.framework.setSidebox = (applicationName, sideboxData, hash?) => this.setSidebox(applicationName, sideboxData, hash); } + + document.body.addEventListener("egw-darkmode-change", this.handleDarkmodeChange); + } + + disconnectedCallback() + { + super.disconnectedCallback(); + + document.body.removeEventListener("egw-darkmode-change", this.handleDarkmodeChange); } protected firstUpdated(_changedProperties : PropertyValues) @@ -572,6 +586,15 @@ export class EgwFramework extends LitElement protected getBaseUrl() {return "";} + protected handleDarkmodeChange(event) + { + // Update CSS classes + this.classList.toggle("sl-theme-light", !event.target.darkmode); + this.classList.toggle("sl-theme-dark", event.target.darkmode); + + // Update preference + this.egw.set_preference("common", "darkmode", (event.target.darkmode ? "1" : "0")); + } /** * An application tab is chosen, show the app * diff --git a/kdots/js/EgwFrameworkApp.styles.ts b/kdots/js/EgwFrameworkApp.styles.ts index e82c304f91..9dbb7dc2b4 100644 --- a/kdots/js/EgwFrameworkApp.styles.ts +++ b/kdots/js/EgwFrameworkApp.styles.ts @@ -39,13 +39,13 @@ export default css` max-height: 3em; background-color: var(--application-color, --primary-background-color); - color: var(--application-header-text-color, white); + color: var(--application-header-text-color, var(--sl-color-neutral-0)); font-size: 1.8em; } .egw_fw_app__header sl-icon-button::part(base), .egw_fw_app__header et2-button-icon { font-size: inherit; - color: var(--application-header-text-color, white); + color: var(--application-header-text-color, var(--sl-color-neutral-0)); } .egw_fw_app__header et2-button-icon { @@ -53,7 +53,7 @@ export default css` } .egw_fw_app__header sl-icon-button::part(base):hover, .egw_fw_app__header et2-button-icon::part(base):hover { - color: var(--application-header-text-color, white); + color: var(--application-header-text-color, var(--sl-color-neutral-0)); filter: brightness(70%); } diff --git a/kdots/js/app.ts b/kdots/js/app.ts index 5131ff337f..37ffea0c77 100644 --- a/kdots/js/app.ts +++ b/kdots/js/app.ts @@ -4,11 +4,12 @@ import {EgwFramework} from "./EgwFramework"; import {EgwFrameworkApp} from "./EgwFrameworkApp"; +import {EgwDarkmodeToggle} from "./EgwDarkmodeToggle"; document.addEventListener('DOMContentLoaded', () => { - // Not sure what's up here + // Not sure what's up here, but it makes sure everything is loaded if(!window.customElements.get("egw-framework")) { window.customElements.define("egw-framework", EgwFramework); @@ -17,6 +18,10 @@ document.addEventListener('DOMContentLoaded', () => { window.customElements.define("egw-app", EgwFrameworkApp); } + if(!window.customElements.get("egw-darkmode-toggle")) + { + window.customElements.define("egw-darkmode-toggle", EgwDarkmodeToggle); + } /* Set up listener on avatar menu */ const avatarMenu = document.querySelector("#topmenu_info_user_avatar"); if(avatarMenu)