diff --git a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js index 4e44e13b67a..647cd38cf59 100644 --- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js +++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js @@ -82,6 +82,20 @@ export default { Session.currentProp("safe_mode", setupData.safeMode); } + Session.currentProp( + "darkModeAvailable", + document.head.querySelectorAll( + 'link[media="(prefers-color-scheme: dark)"]' + ).length > 0 + ); + + Session.currentProp( + "darkColorScheme", + getComputedStyle(document.documentElement) + .getPropertyValue("--scheme-type") + .trim() === "dark" + ); + app.HighlightJSPath = setupData.highlightJsPath; Session.currentProp("svgSpritePath", setupData.svgSpritePath); diff --git a/app/assets/javascripts/discourse/app/widgets/home-logo.js b/app/assets/javascripts/discourse/app/widgets/home-logo.js index 9af8f2b2739..31a10fd4151 100644 --- a/app/assets/javascripts/discourse/app/widgets/home-logo.js +++ b/app/assets/javascripts/discourse/app/widgets/home-logo.js @@ -4,6 +4,7 @@ import { h } from "virtual-dom"; import { iconNode } from "discourse-common/lib/icon-library"; import { wantsNewWindow } from "discourse/lib/intercept-click"; import DiscourseURL from "discourse/lib/url"; +import Session from "discourse/models/session"; export default createWidget("home-logo", { tagName: "div.title", @@ -17,57 +18,110 @@ export default createWidget("home-logo", { return typeof href === "function" ? href() : href; }, - logoUrl() { - return this.siteSettings.site_logo_url || ""; + logoUrl(opts = {}) { + return this.logoResolver("logo", opts); }, - mobileLogoUrl() { - return this.siteSettings.site_mobile_logo_url || ""; + mobileLogoUrl(opts = {}) { + return this.logoResolver("mobile_logo", opts); }, - smallLogoUrl() { - return this.siteSettings.site_logo_small_url || ""; + smallLogoUrl(opts = {}) { + return this.logoResolver("logo_small", opts); }, logo() { - const { siteSettings } = this; - const mobileView = this.site.mobileView; + const { siteSettings } = this, + mobileView = this.site.mobileView; + + const darkModeOptions = Session.currentProp("darkModeAvailable") + ? { dark: true } + : {}; + + const mobileLogoUrl = this.mobileLogoUrl(), + mobileLogoUrlDark = this.mobileLogoUrl(darkModeOptions); - const mobileLogoUrl = this.mobileLogoUrl(); const showMobileLogo = mobileView && mobileLogoUrl.length > 0; - const logoUrl = this.logoUrl(); + const logoUrl = this.logoUrl(), + logoUrlDark = this.logoUrl(darkModeOptions); const title = siteSettings.title; if (this.attrs.minimized) { - const logoSmallUrl = this.smallLogoUrl(); + const logoSmallUrl = this.smallLogoUrl(), + logoSmallUrlDark = this.smallLogoUrl(darkModeOptions); if (logoSmallUrl.length) { - return h("img#site-logo.logo-small", { - key: "logo-small", - attributes: { - src: getURL(logoSmallUrl), - width: 36, - alt: title - } - }); + return this.logoElement( + "logo-small", + logoSmallUrl, + title, + logoSmallUrlDark + ); } else { return iconNode("home"); } } else if (showMobileLogo) { - return h("img#site-logo.logo-big", { - key: "logo-mobile", - attributes: { src: getURL(mobileLogoUrl), alt: title } - }); + return this.logoElement( + "logo-mobile", + mobileLogoUrl, + title, + mobileLogoUrlDark + ); } else if (logoUrl.length) { - return h("img#site-logo.logo-big", { - key: "logo-big", - attributes: { src: getURL(logoUrl), alt: title } - }); + return this.logoElement("logo-big", logoUrl, title, logoUrlDark); } else { return h("h1#site-text-logo.text-logo", { key: "logo-text" }, title); } }, + logoResolver(name, opts = {}) { + const { siteSettings } = this; + + // get alternative logos for browser dark dark mode switching + if (opts.dark) { + return siteSettings[`site_${name}_dark_url`]; + } + + // try dark logos first when color scheme is dark + // this is independent of browser dark mode + // hence the fallback to normal logos + if (Session.currentProp("darkColorScheme")) { + return ( + siteSettings[`site_${name}_dark_url`] || + siteSettings[`site_${name}_url`] || + "" + ); + } + + return siteSettings[`site_${name}_url`] || ""; + }, + + logoElement(key, url, title, darkUrl = null) { + const attributes = + key === "logo-small" + ? { src: getURL(url), width: 36, alt: title } + : { src: getURL(url), alt: title }; + + const imgElement = h(`img#site-logo.${key}`, { + key: key, + attributes + }); + + if (darkUrl && url !== darkUrl) { + return h("picture", [ + h("source", { + attributes: { + srcset: getURL(darkUrl), + media: "(prefers-color-scheme: dark)" + } + }), + imgElement + ]); + } + + return imgElement; + }, + html() { return h( "a", diff --git a/app/assets/stylesheets/color_definitions.scss b/app/assets/stylesheets/color_definitions.scss index 047dcd0dd46..72104d80b11 100644 --- a/app/assets/stylesheets/color_definitions.scss +++ b/app/assets/stylesheets/color_definitions.scss @@ -14,7 +14,16 @@ @return red($hex), green($hex), blue($hex); } +@function schemeType() { + @if is-light-color-scheme() { + @return "light"; + } @else { + @return "dark"; + } +} + :root { + --scheme-type: #{schemeType()}; --primary: #{$primary}; --secondary: #{$secondary}; --tertiary: #{$tertiary}; diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index a4f6f809778..daef5fa3c96 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -183,6 +183,9 @@ class SiteSetting < ActiveRecord::Base site_logo_small_url site_mobile_logo_url site_favicon_url + site_logo_dark_url + site_logo_small_dark_url + site_mobile_logo_dark_url }.each { |client_setting| client_settings << client_setting } %i{ @@ -190,6 +193,9 @@ class SiteSetting < ActiveRecord::Base logo_small digest_logo mobile_logo + logo_dark + logo_small_dark + mobile_logo_dark large_icon manifest_icon favicon diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e2f69d37f42..3219e5300d5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1481,6 +1481,9 @@ en: logo_small: "The small logo image at the top left of your site, seen when scrolling down. Use a square 120 × 120 image. If left blank, a home glyph will be shown." digest_logo: "The alternate logo image used at the top of your site's email summary. Use a wide rectangle image. Don't use an SVG image. If left blank, the image from the `logo` setting will be used." mobile_logo: "The logo used on mobile version of your site. Use a wide rectangular image with a height of 120 and an aspect ratio greater than 3:1. If left blank, the image from the `logo` setting will be used." + logo_dark: "Dark scheme alternative for the 'logo' site setting." + logo_small_dark: "Dark scheme alternative for the 'logo small' site setting." + mobile_logo_dark: "Dark scheme alternative for the 'mobile logo' site setting." large_icon: "Image used as the base for other metadata icons. Should ideally be larger than 512 x 512. If left blank, logo_small will be used." manifest_icon: "Image used as logo/splash image on Android. Will be automatically resized to 512 × 512. If left blank, large_icon will be used." favicon: "A favicon for your site, see https://en.wikipedia.org/wiki/Favicon. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, large_icon will be used." diff --git a/config/site_settings.yml b/config/site_settings.yml index 17ff664358b..23153613b03 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -74,6 +74,18 @@ branding: default: "" client: true type: upload + logo_dark: + default: "" + client: true + type: upload + logo_small_dark: + default: "" + client: true + type: upload + mobile_logo_dark: + default: "" + client: true + type: upload large_icon: default: "" client: true diff --git a/test/javascripts/widgets/home-logo-test.js b/test/javascripts/widgets/home-logo-test.js index 14b32a3e2c7..130ff8b7e5e 100644 --- a/test/javascripts/widgets/home-logo-test.js +++ b/test/javascripts/widgets/home-logo-test.js @@ -1,10 +1,13 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test"; +import Session from "discourse/models/session"; moduleForWidget("home-logo"); const bigLogo = "/images/d-logo-sketch.png?test"; const smallLogo = "/images/d-logo-sketch-small.png?test"; const mobileLogo = "/images/d-logo-sketch.png?mobile"; +const darkLogo = "/images/d-logo-sketch.png?dark"; const title = "Cool Forum"; +const prefersDark = "(prefers-color-scheme: dark)"; widgetTest("basics", { template: '{{mount-widget widget="home-logo" args=args}}', @@ -38,6 +41,7 @@ widgetTest("basics - minimized", { assert.ok(find("img.logo-small").length === 1); assert.equal(find("img.logo-small").attr("src"), smallLogo); assert.equal(find("img.logo-small").attr("alt"), title); + assert.equal(find("img.logo-small").attr("width"), 36); } }); @@ -79,7 +83,7 @@ widgetTest("mobile logo", { }, test(assert) { - assert.ok(find("img#site-logo.logo-big").length === 1); + assert.ok(find("img#site-logo.logo-mobile").length === 1); assert.equal(find("#site-logo").attr("src"), mobileLogo); } }); @@ -96,3 +100,139 @@ widgetTest("mobile without logo", { assert.equal(find("#site-logo").attr("src"), bigLogo); } }); + +widgetTest("logo with dark mode alternative", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = darkLogo; + Session.currentProp("darkModeAvailable", true); + }, + afterEach() { + Session.currentProp("darkModeAvailable", null); + }, + + test(assert) { + assert.ok(find("img#site-logo.logo-big").length === 1); + assert.equal(find("#site-logo").attr("src"), bigLogo); + + assert.equal( + find("picture source").attr("media"), + prefersDark, + "includes dark mode media attribute" + ); + assert.equal( + find("picture source").attr("srcset"), + darkLogo, + "includes dark mode alternative logo source" + ); + } +}); + +widgetTest("mobile logo with dark mode alternative", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_mobile_logo_url = mobileLogo; + this.siteSettings.site_mobile_logo_dark_url = darkLogo; + Session.currentProp("darkModeAvailable", true); + + this.site.mobileView = true; + }, + afterEach() { + Session.currentProp("darkModeAvailable", null); + }, + + test(assert) { + assert.equal(find("#site-logo").attr("src"), mobileLogo); + + assert.equal( + find("picture source").attr("media"), + prefersDark, + "includes dark mode media attribute" + ); + assert.equal( + find("picture source").attr("srcset"), + darkLogo, + "includes dark mode alternative logo source" + ); + } +}); + +widgetTest("dark mode enabled but no dark logo set", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = ""; + Session.currentProp("darkModeAvailable", true); + }, + afterEach() { + Session.currentProp("darkModeAvailable", null); + }, + + test(assert) { + assert.ok(find("img#site-logo.logo-big").length === 1); + assert.equal(find("#site-logo").attr("src"), bigLogo); + assert.ok( + find("picture").length === 0, + "does not include alternative logo" + ); + } +}); + +widgetTest("dark logo set but no dark mode", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = darkLogo; + }, + + test(assert) { + assert.ok(find("img#site-logo.logo-big").length === 1); + assert.equal(find("#site-logo").attr("src"), bigLogo); + assert.ok( + find("picture").length === 0, + "does not include alternative logo" + ); + } +}); + +widgetTest("dark color scheme and dark logo set", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = darkLogo; + Session.currentProp("darkColorScheme", true); + }, + afterEach() { + Session.currentProp("darkColorScheme", null); + }, + test(assert) { + assert.ok(find("img#site-logo.logo-big").length === 1); + assert.equal(find("#site-logo").attr("src"), darkLogo, "uses dark logo"); + assert.ok( + find("picture").length === 0, + "does not add dark mode alternative" + ); + } +}); + +widgetTest("dark color scheme and dark logo not set", { + template: '{{mount-widget widget="home-logo" args=args}}', + beforeEach() { + this.siteSettings.site_logo_url = bigLogo; + this.siteSettings.site_logo_dark_url = ""; + Session.currentProp("darkColorScheme", true); + }, + afterEach() { + Session.currentProp("darkColorScheme", null); + }, + test(assert) { + assert.ok(find("img#site-logo.logo-big").length === 1); + assert.equal( + find("#site-logo").attr("src"), + bigLogo, + "uses regular logo on dark scheme if no dark logo" + ); + } +});