FEATURE: Dark mode alternative logos (#10441)

This commit is contained in:
Penar Musaraj 2020-08-17 15:43:20 -04:00 committed by GitHub
parent d5a4318ac1
commit 3745f2bb86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 266 additions and 28 deletions

View File

@ -82,6 +82,20 @@ export default {
Session.currentProp("safe_mode", setupData.safeMode); 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; app.HighlightJSPath = setupData.highlightJsPath;
Session.currentProp("svgSpritePath", setupData.svgSpritePath); Session.currentProp("svgSpritePath", setupData.svgSpritePath);

View File

@ -4,6 +4,7 @@ import { h } from "virtual-dom";
import { iconNode } from "discourse-common/lib/icon-library"; import { iconNode } from "discourse-common/lib/icon-library";
import { wantsNewWindow } from "discourse/lib/intercept-click"; import { wantsNewWindow } from "discourse/lib/intercept-click";
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import Session from "discourse/models/session";
export default createWidget("home-logo", { export default createWidget("home-logo", {
tagName: "div.title", tagName: "div.title",
@ -17,57 +18,110 @@ export default createWidget("home-logo", {
return typeof href === "function" ? href() : href; return typeof href === "function" ? href() : href;
}, },
logoUrl() { logoUrl(opts = {}) {
return this.siteSettings.site_logo_url || ""; return this.logoResolver("logo", opts);
}, },
mobileLogoUrl() { mobileLogoUrl(opts = {}) {
return this.siteSettings.site_mobile_logo_url || ""; return this.logoResolver("mobile_logo", opts);
}, },
smallLogoUrl() { smallLogoUrl(opts = {}) {
return this.siteSettings.site_logo_small_url || ""; return this.logoResolver("logo_small", opts);
}, },
logo() { logo() {
const { siteSettings } = this; const { siteSettings } = this,
const mobileView = this.site.mobileView; 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 showMobileLogo = mobileView && mobileLogoUrl.length > 0;
const logoUrl = this.logoUrl(); const logoUrl = this.logoUrl(),
logoUrlDark = this.logoUrl(darkModeOptions);
const title = siteSettings.title; const title = siteSettings.title;
if (this.attrs.minimized) { if (this.attrs.minimized) {
const logoSmallUrl = this.smallLogoUrl(); const logoSmallUrl = this.smallLogoUrl(),
logoSmallUrlDark = this.smallLogoUrl(darkModeOptions);
if (logoSmallUrl.length) { if (logoSmallUrl.length) {
return h("img#site-logo.logo-small", { return this.logoElement(
key: "logo-small", "logo-small",
attributes: { logoSmallUrl,
src: getURL(logoSmallUrl), title,
width: 36, logoSmallUrlDark
alt: title );
}
});
} else { } else {
return iconNode("home"); return iconNode("home");
} }
} else if (showMobileLogo) { } else if (showMobileLogo) {
return h("img#site-logo.logo-big", { return this.logoElement(
key: "logo-mobile", "logo-mobile",
attributes: { src: getURL(mobileLogoUrl), alt: title } mobileLogoUrl,
}); title,
mobileLogoUrlDark
);
} else if (logoUrl.length) { } else if (logoUrl.length) {
return h("img#site-logo.logo-big", { return this.logoElement("logo-big", logoUrl, title, logoUrlDark);
key: "logo-big",
attributes: { src: getURL(logoUrl), alt: title }
});
} else { } else {
return h("h1#site-text-logo.text-logo", { key: "logo-text" }, title); 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() { html() {
return h( return h(
"a", "a",

View File

@ -14,7 +14,16 @@
@return red($hex), green($hex), blue($hex); @return red($hex), green($hex), blue($hex);
} }
@function schemeType() {
@if is-light-color-scheme() {
@return "light";
} @else {
@return "dark";
}
}
:root { :root {
--scheme-type: #{schemeType()};
--primary: #{$primary}; --primary: #{$primary};
--secondary: #{$secondary}; --secondary: #{$secondary};
--tertiary: #{$tertiary}; --tertiary: #{$tertiary};

View File

@ -183,6 +183,9 @@ class SiteSetting < ActiveRecord::Base
site_logo_small_url site_logo_small_url
site_mobile_logo_url site_mobile_logo_url
site_favicon_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 } }.each { |client_setting| client_settings << client_setting }
%i{ %i{
@ -190,6 +193,9 @@ class SiteSetting < ActiveRecord::Base
logo_small logo_small
digest_logo digest_logo
mobile_logo mobile_logo
logo_dark
logo_small_dark
mobile_logo_dark
large_icon large_icon
manifest_icon manifest_icon
favicon favicon

View File

@ -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." 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." 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." 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." 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." 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 <a href='https://en.wikipedia.org/wiki/Favicon' target='_blank'>https://en.wikipedia.org/wiki/Favicon</a>. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, large_icon will be used." favicon: "A favicon for your site, see <a href='https://en.wikipedia.org/wiki/Favicon' target='_blank'>https://en.wikipedia.org/wiki/Favicon</a>. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, large_icon will be used."

View File

@ -74,6 +74,18 @@ branding:
default: "" default: ""
client: true client: true
type: upload 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: large_icon:
default: "" default: ""
client: true client: true

View File

@ -1,10 +1,13 @@
import { moduleForWidget, widgetTest } from "helpers/widget-test"; import { moduleForWidget, widgetTest } from "helpers/widget-test";
import Session from "discourse/models/session";
moduleForWidget("home-logo"); moduleForWidget("home-logo");
const bigLogo = "/images/d-logo-sketch.png?test"; const bigLogo = "/images/d-logo-sketch.png?test";
const smallLogo = "/images/d-logo-sketch-small.png?test"; const smallLogo = "/images/d-logo-sketch-small.png?test";
const mobileLogo = "/images/d-logo-sketch.png?mobile"; const mobileLogo = "/images/d-logo-sketch.png?mobile";
const darkLogo = "/images/d-logo-sketch.png?dark";
const title = "Cool Forum"; const title = "Cool Forum";
const prefersDark = "(prefers-color-scheme: dark)";
widgetTest("basics", { widgetTest("basics", {
template: '{{mount-widget widget="home-logo" args=args}}', template: '{{mount-widget widget="home-logo" args=args}}',
@ -38,6 +41,7 @@ widgetTest("basics - minimized", {
assert.ok(find("img.logo-small").length === 1); assert.ok(find("img.logo-small").length === 1);
assert.equal(find("img.logo-small").attr("src"), smallLogo); 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("alt"), title);
assert.equal(find("img.logo-small").attr("width"), 36);
} }
}); });
@ -79,7 +83,7 @@ widgetTest("mobile logo", {
}, },
test(assert) { 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); assert.equal(find("#site-logo").attr("src"), mobileLogo);
} }
}); });
@ -96,3 +100,139 @@ widgetTest("mobile without logo", {
assert.equal(find("#site-logo").attr("src"), bigLogo); 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"
);
}
});