From 284e708e6783854fb1297e56ae289d9c689182d3 Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Fri, 7 Feb 2025 03:28:34 +0300 Subject: [PATCH] FEATURE: Dark/light mode selector (#31086) This commit makes the [color-scheme-toggle](https://github.com/discourse/discourse-color-scheme-toggle) theme component a core feature with improvements and bug fixes. The theme component will be updated to become a no-op if the core feature is enabled. Noteworthy changes: * the color mode selector has a new "Auto" option that makes the site render in the same color mode as the user's system preference * the splash screen respects the color mode selected by the user * dark/light variants of category logos and background images are now picked correctly based on the selected color mode * a new `interface_color_selector` site setting to disable the selector or choose its location between the sidebar footer or header Internal topic: t/139465. --------- Co-authored-by: Ella --- .../discourse/app/components/d-styles.gjs | 9 +- .../discourse/app/components/header/icons.gjs | 9 + .../discourse/app/components/header/logo.gjs | 48 +-- .../components/interface-color-selector.gjs | 85 ++++++ .../app/components/light-dark-img.gjs | 13 +- .../app/components/sidebar/footer.gjs | 6 + .../app/initializers/discourse-bootstrap.js | 3 +- .../instance-initializers/interface-color.js | 8 + .../discourse/app/services/interface-color.js | 112 +++++++ .../stylesheets/common/base/header.scss | 13 +- app/controllers/stylesheets_controller.rb | 2 +- app/helpers/application_helper.rb | 99 ++++-- .../interface_color_selector_setting.rb | 25 ++ app/views/common/_discourse_splash.html.erb | 30 +- app/views/static/login.html.erb | 2 +- config/locales/client.en.yml | 9 + config/locales/server.en.yml | 1 + config/site_settings.yml | 4 + lib/middleware/anonymous_cache.rb | 6 + lib/stylesheet/manager.rb | 43 ++- lib/svg_sprite.rb | 1 + .../discourse/initializers/chat-setup.js | 4 +- spec/lib/middleware/anonymous_cache_spec.rb | 39 +++ spec/lib/stylesheet/manager_spec.rb | 58 ++-- spec/models/color_scheme_spec.rb | 4 +- spec/requests/application_controller_spec.rb | 288 ++++++++++++++++++ spec/system/interface_color_selector_spec.rb | 167 ++++++++++ .../page_objects/components/home_logo.rb | 19 ++ .../components/interface_color_mode.rb | 30 ++ .../components/interface_color_selector.rb | 39 +++ 30 files changed, 1065 insertions(+), 111 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/interface-color-selector.gjs create mode 100644 app/assets/javascripts/discourse/app/instance-initializers/interface-color.js create mode 100644 app/assets/javascripts/discourse/app/services/interface-color.js create mode 100644 app/models/interface_color_selector_setting.rb create mode 100644 spec/system/interface_color_selector_spec.rb create mode 100644 spec/system/page_objects/components/home_logo.rb create mode 100644 spec/system/page_objects/components/interface_color_mode.rb create mode 100644 spec/system/page_objects/components/interface_color_selector.rb diff --git a/app/assets/javascripts/discourse/app/components/d-styles.gjs b/app/assets/javascripts/discourse/app/components/d-styles.gjs index c8e0085cb36..efc3cca12d9 100644 --- a/app/assets/javascripts/discourse/app/components/d-styles.gjs +++ b/app/assets/javascripts/discourse/app/components/d-styles.gjs @@ -5,6 +5,7 @@ import { getURLWithCDN } from "discourse/lib/get-url"; export default class DStyles extends Component { @service session; @service site; + @service interfaceColor; get categoryColors() { return [ @@ -17,7 +18,7 @@ export default class DStyles extends Component { } get categoryBackgrounds() { - const css = []; + let css = []; const darkCss = []; this.site.categories.forEach((category) => { @@ -45,7 +46,11 @@ export default class DStyles extends Component { }); if (darkCss.length > 0) { - css.push("@media (prefers-color-scheme: dark) {", ...darkCss, "}"); + if (this.interfaceColor.darkModeForced) { + css = darkCss; + } else if (!this.interfaceColor.lightModeForced) { + css.push("@media (prefers-color-scheme: dark) {", ...darkCss, "}"); + } } return css.join("\n"); diff --git a/app/assets/javascripts/discourse/app/components/header/icons.gjs b/app/assets/javascripts/discourse/app/components/header/icons.gjs index a49b61afc03..62cc9a735f9 100644 --- a/app/assets/javascripts/discourse/app/components/header/icons.gjs +++ b/app/assets/javascripts/discourse/app/components/header/icons.gjs @@ -2,6 +2,7 @@ import Component from "@glimmer/component"; import { action } from "@ember/object"; import { service } from "@ember/service"; import { eq } from "truth-helpers"; +import InterfaceColorSelector from "discourse/components/interface-color-selector"; import DAG from "discourse/lib/dag"; import getURL from "discourse/lib/get-url"; import Dropdown from "./dropdown"; @@ -15,6 +16,7 @@ function resetHeaderIcons() { headerIcons.add("search"); headerIcons.add("hamburger", undefined, { after: "search" }); headerIcons.add("user-menu", undefined, { after: "hamburger" }); + headerIcons.add("interface-color-selector", undefined, { before: "search" }); } export function headerIconsDAG() { @@ -32,6 +34,7 @@ export default class Icons extends Component { @service sidebarState; @service header; @service search; + @service interfaceColor; get showHamburger() { // NOTE: In this scenario, we are forcing the sidebar on admin users, @@ -98,6 +101,12 @@ export default class Icons extends Component { @toggleUserMenu={{@toggleUserMenu}} /> {{/if}} + {{else if (eq entry.key "interface-color-selector")}} + {{#if this.interfaceColor.selectorAvailableInHeader}} +
  • + +
  • + {{/if}} {{else if entry.value}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/header/logo.gjs b/app/assets/javascripts/discourse/app/components/header/logo.gjs index f801a82e4de..86b07c0aa2d 100644 --- a/app/assets/javascripts/discourse/app/components/header/logo.gjs +++ b/app/assets/javascripts/discourse/app/components/header/logo.gjs @@ -1,10 +1,34 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; import { and, eq, notEq } from "truth-helpers"; import getURL from "discourse/lib/get-url"; -const Logo = +} diff --git a/app/assets/javascripts/discourse/app/components/interface-color-selector.gjs b/app/assets/javascripts/discourse/app/components/interface-color-selector.gjs new file mode 100644 index 00000000000..60733f8611d --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/interface-color-selector.gjs @@ -0,0 +1,85 @@ +import Component from "@glimmer/component"; +import { fn } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import DropdownMenu from "discourse/components/dropdown-menu"; +import { i18n } from "discourse-i18n"; +import DMenu from "float-kit/components/d-menu"; + +export default class InterfaceColorSelector extends Component { + @service interfaceColor; + + get selectorIcon() { + if (this.interfaceColor.lightModeForced) { + return "sun"; + } else if (this.interfaceColor.darkModeForced) { + return "moon"; + } else { + return "circle-half-stroke"; + } + } + + @action + switchToLight(dMenu) { + this.interfaceColor.forceLightMode(); + dMenu.close(); + } + + @action + switchToDark(dMenu) { + this.interfaceColor.forceDarkMode(); + dMenu.close(); + } + + @action + switchToAuto(dMenu) { + this.interfaceColor.removeColorModeOverride(); + dMenu.close(); + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/light-dark-img.gjs b/app/assets/javascripts/discourse/app/components/light-dark-img.gjs index ddfc16c0272..f405497c183 100644 --- a/app/assets/javascripts/discourse/app/components/light-dark-img.gjs +++ b/app/assets/javascripts/discourse/app/components/light-dark-img.gjs @@ -5,6 +5,7 @@ import { getURLWithCDN } from "discourse/lib/get-url"; export default class LightDarkImg extends Component { @service session; + @service interfaceColor; get isDarkImageAvailable() { return ( @@ -28,6 +29,16 @@ export default class LightDarkImg extends Component { return getURLWithCDN(this.args.darkImg.url); } + get darkMediaQuery() { + if (this.interfaceColor.darkModeForced) { + return "all"; + } else if (this.interfaceColor.lightModeForced) { + return "none"; + } else { + return "(prefers-color-scheme: dark)"; + } + } +