diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js
index dd80b409d29..1ec0dd64337 100644
--- a/app/assets/javascripts/discourse/app/controllers/preferences/interface.js
+++ b/app/assets/javascripts/discourse/app/controllers/preferences/interface.js
@@ -4,6 +4,11 @@ import Controller from "@ember/controller";
import { setDefaultHomepage } from "discourse/lib/utilities";
import discourseComputed from "discourse-common/utils/decorators";
import { listThemes, setLocalTheme } from "discourse/lib/theme-selector";
+import {
+ listColorSchemes,
+ loadColorSchemeStylesheet,
+ updateColorSchemeCookie
+} from "discourse/lib/color-scheme-picker";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { reload } from "discourse/helpers/page-reloader";
import {
@@ -12,6 +17,7 @@ import {
iOSWithVisualViewport
} from "discourse/lib/utilities";
import { computed } from "@ember/object";
+import { reads } from "@ember/object/computed";
const USER_HOMES = {
1: "latest",
@@ -26,14 +32,25 @@ const TITLE_COUNT_MODES = ["notifications", "contextual"];
export default Controller.extend({
currentThemeId: -1,
+ previewingColorScheme: false,
+ selectedColorSchemeId: null,
+ selectedDarkColorSchemeId: null,
preferencesController: inject("preferences"),
- @discourseComputed("makeThemeDefault")
- saveAttrNames(makeDefault) {
+ init() {
+ this._super(...arguments);
+
+ this.setProperties({
+ selectedColorSchemeId: this.session.userColorSchemeId,
+ selectedDarkColorSchemeId: this.session.userDarkSchemeId
+ });
+ },
+
+ @discourseComputed("makeThemeDefault", "makeColorSchemeDefault")
+ saveAttrNames(makeThemeDefault, makeColorSchemeDefault) {
let attrs = [
"locale",
"external_links_in_new_tab",
- "dark_scheme_id",
"dynamic_favicon",
"enable_quoting",
"enable_defer",
@@ -47,10 +64,14 @@ export default Controller.extend({
"skip_new_user_tips"
];
- if (makeDefault) {
+ if (makeThemeDefault) {
attrs.push("theme_ids");
}
+ if (makeColorSchemeDefault) {
+ attrs.push("color_scheme_id");
+ attrs.push("dark_scheme_id");
+ }
return attrs;
},
@@ -71,6 +92,11 @@ export default Controller.extend({
return JSON.parse(this.siteSettings.available_locales);
},
+ @discourseComputed
+ defaultDarkSchemeId() {
+ return this.siteSettings.default_dark_mode_color_scheme_id;
+ },
+
@discourseComputed
textSizes() {
return TEXT_SIZES.map(value => {
@@ -116,6 +142,16 @@ export default Controller.extend({
}
},
+ @discourseComputed
+ userSelectableColorSchemes() {
+ return listColorSchemes(this.site);
+ },
+
+ showColorSchemeSelector: reads("userSelectableColorSchemes.length"),
+ selectedColorSchemeNoneLabel: I18n.t(
+ "user.color_schemes.default_description"
+ ),
+
@discourseComputed("model.user_option.theme_ids", "themeId")
showThemeSetDefault(userOptionThemes, selectedTheme) {
return !userOptionThemes || userOptionThemes[0] !== selectedTheme;
@@ -153,7 +189,23 @@ export default Controller.extend({
@discourseComputed
showDarkModeToggle() {
- return this.siteSettings.default_dark_mode_color_scheme_id > 0;
+ return this.defaultDarkSchemeId > 0 && !this.showDarkColorSchemeSelector;
+ },
+
+ @discourseComputed
+ userSelectableDarkColorSchemes() {
+ return listColorSchemes(this.site, {
+ darkOnly: true
+ });
+ },
+
+ @discourseComputed("userSelectableDarkColorSchemes")
+ showDarkColorSchemeSelector(darkSchemes) {
+ // when a default dark scheme is set
+ // dropdown has two items (disable / use site default)
+ // but we show a checkbox in that case
+ const minToShow = this.defaultDarkSchemeId > 0 ? 2 : 1;
+ return darkSchemes && darkSchemes.length > minToShow;
},
enableDarkMode: computed({
@@ -178,10 +230,32 @@ export default Controller.extend({
this.set("model.user_option.text_size", this.textSize);
}
- this.set(
- "model.user_option.dark_scheme_id",
- this.enableDarkMode ? null : -1
- );
+ if (this.makeColorSchemeDefault) {
+ this.set(
+ "model.user_option.color_scheme_id",
+ this.selectedColorSchemeId
+ );
+ }
+
+ if (this.showDarkModeToggle) {
+ this.set(
+ "model.user_option.dark_scheme_id",
+ this.enableDarkMode ? null : -1
+ );
+ } else {
+ // if chosen dark scheme matches site dark scheme, no need to store
+ if (
+ this.defaultDarkSchemeId > 0 &&
+ this.selectedDarkColorSchemeId === this.defaultDarkSchemeId
+ ) {
+ this.set("model.user_option.dark_scheme_id", null);
+ } else {
+ this.set(
+ "model.user_option.dark_scheme_id",
+ this.selectedDarkColorSchemeId
+ );
+ }
+ }
return this.model
.save(this.saveAttrNames)
@@ -202,6 +276,24 @@ export default Controller.extend({
this.model.updateTextSizeCookie(this.textSize);
}
+ if (this.makeColorSchemeDefault) {
+ updateColorSchemeCookie(null);
+ updateColorSchemeCookie(null, { dark: true });
+ } else {
+ updateColorSchemeCookie(this.selectedColorSchemeId);
+
+ if (
+ this.defaultDarkSchemeId > 0 &&
+ this.selectedDarkColorSchemeId === this.defaultDarkSchemeId
+ ) {
+ updateColorSchemeCookie(null, { dark: true });
+ } else {
+ updateColorSchemeCookie(this.selectedDarkColorSchemeId, {
+ dark: true
+ });
+ }
+ }
+
this.homeChanged();
if (this.isiPad) {
@@ -236,6 +328,60 @@ export default Controller.extend({
// Force refresh when leaving this screen
this.session.requiresRefresh = true;
this.set("textSize", newSize);
+ },
+
+ loadColorScheme(colorSchemeId) {
+ this.setProperties({
+ selectedColorSchemeId: colorSchemeId,
+ previewingColorScheme: true
+ });
+
+ if (colorSchemeId < 0) {
+ const defaultTheme = this.userSelectableThemes.findBy(
+ "id",
+ this.themeId
+ );
+
+ if (defaultTheme && defaultTheme.color_scheme_id) {
+ colorSchemeId = defaultTheme.color_scheme_id;
+ }
+ }
+ loadColorSchemeStylesheet(colorSchemeId, this.themeId);
+ if (this.selectedDarkColorSchemeId === -1) {
+ // set this same scheme for dark mode preview when dark scheme is disabled
+ loadColorSchemeStylesheet(colorSchemeId, this.themeId, true);
+ }
+ },
+
+ loadDarkColorScheme(colorSchemeId) {
+ this.setProperties({
+ selectedDarkColorSchemeId: colorSchemeId,
+ previewingColorScheme: true
+ });
+
+ if (colorSchemeId === -1) {
+ // load preview of regular scheme when dark scheme is disabled
+ loadColorSchemeStylesheet(
+ this.selectedColorSchemeId,
+ this.themeId,
+ true
+ );
+ } else {
+ loadColorSchemeStylesheet(colorSchemeId, this.themeId, true);
+ }
+ },
+
+ undoColorSchemePreview() {
+ this.setProperties({
+ selectedColorSchemeId: this.session.userColorSchemeId,
+ selectedDarkColorSchemeId: this.session.userDarkSchemeId,
+ previewingColorScheme: false
+ });
+ const darkStylesheet = document.querySelector("link#cs-preview-dark"),
+ lightStylesheet = document.querySelector("link#cs-preview-light");
+ if (darkStylesheet) darkStylesheet.remove();
+
+ if (lightStylesheet) lightStylesheet.remove();
}
}
});
diff --git a/app/assets/javascripts/discourse/app/lib/color-scheme-picker.js b/app/assets/javascripts/discourse/app/lib/color-scheme-picker.js
new file mode 100644
index 00000000000..f1bca621789
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/color-scheme-picker.js
@@ -0,0 +1,88 @@
+import I18n from "I18n";
+import { ajax } from "discourse/lib/ajax";
+import cookie, { removeCookie } from "discourse/lib/cookie";
+
+export function listColorSchemes(site, options = {}) {
+ let schemes = site.get("user_color_schemes");
+
+ if (!schemes || !Array.isArray(schemes)) {
+ return null;
+ }
+
+ let results = [];
+
+ if (!options.darkOnly) {
+ schemes = schemes.sort((a, b) => Number(a.is_dark) - Number(b.is_dark));
+ }
+ schemes.forEach(s => {
+ if ((options.darkOnly && s.is_dark) || !options.darkOnly) {
+ results.push({
+ name: s.name,
+ id: s.id
+ });
+ }
+ });
+
+ if (options.darkOnly) {
+ const defaultDarkColorScheme = site.get("default_dark_color_scheme");
+ if (defaultDarkColorScheme) {
+ const existing = schemes.findBy("id", defaultDarkColorScheme.id);
+ if (!existing) {
+ results.unshift({
+ id: defaultDarkColorScheme.id,
+ name: `${defaultDarkColorScheme.name} ${I18n.t(
+ "user.color_schemes.default_dark_scheme"
+ )}`
+ });
+ }
+ }
+
+ results.unshift({
+ id: -1,
+ name: I18n.t("user.color_schemes.disable_dark_scheme")
+ });
+ }
+
+ return results.length === 0 ? null : results;
+}
+
+export function loadColorSchemeStylesheet(
+ colorSchemeId,
+ theme_id,
+ dark = false
+) {
+ const themeId = theme_id ? `/${theme_id}` : "";
+ ajax(`/color-scheme-stylesheet/${colorSchemeId}${themeId}.json`).then(
+ result => {
+ if (result && result.new_href) {
+ const elementId = dark ? "cs-preview-dark" : "cs-preview-light";
+ const existingElement = document.querySelector(`link#${elementId}`);
+ if (existingElement) {
+ existingElement.href = result.new_href;
+ } else {
+ let link = document.createElement("link");
+ link.href = result.new_href;
+ link.media = dark
+ ? "(prefers-color-scheme: dark)"
+ : "(prefers-color-scheme: light)";
+ link.rel = "stylesheet";
+ link.id = elementId;
+
+ document.body.appendChild(link);
+ }
+ }
+ }
+ );
+}
+
+export function updateColorSchemeCookie(id, options = {}) {
+ const cookieName = options.dark ? "dark_scheme_id" : "color_scheme_id";
+ if (id) {
+ cookie(cookieName, id, {
+ path: "/",
+ expires: 9999
+ });
+ } else {
+ removeCookie(cookieName, { path: "/", expires: 1 });
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/lib/theme-selector.js b/app/assets/javascripts/discourse/app/lib/theme-selector.js
index a5b46c8ac1e..82d838fd766 100644
--- a/app/assets/javascripts/discourse/app/lib/theme-selector.js
+++ b/app/assets/javascripts/discourse/app/lib/theme-selector.js
@@ -94,7 +94,11 @@ export function listThemes(site) {
}
themes.forEach(t => {
- results.push({ name: t.name, id: t.theme_id });
+ results.push({
+ name: t.name,
+ id: t.theme_id,
+ color_scheme_id: t.color_scheme_id
+ });
});
return results.length === 0 ? null : results;
diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js
index 940c5678194..15b7315e2d4 100644
--- a/app/assets/javascripts/discourse/app/models/user.js
+++ b/app/assets/javascripts/discourse/app/models/user.js
@@ -302,6 +302,7 @@ const User = RestModel.extend({
"email_messages_level",
"email_level",
"email_previous_replies",
+ "color_scheme_id",
"dark_scheme_id",
"dynamic_favicon",
"enable_quoting",
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 20c10a81e8d..15b9ef82922 100644
--- a/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js
+++ b/app/assets/javascripts/discourse/app/pre-initializers/discourse-bootstrap.js
@@ -90,6 +90,9 @@ export default {
session.highlightJsPath = setupData.highlightJsPath;
session.svgSpritePath = setupData.svgSpritePath;
+ session.userColorSchemeId =
+ parseInt(setupData.userColorSchemeId, 10) || null;
+ session.userDarkSchemeId = parseInt(setupData.userDarkSchemeId, 10) || -1;
if (isDevelopment()) {
setIconList(setupData.svgIconList);
diff --git a/app/assets/javascripts/discourse/app/routes/preferences-interface.js b/app/assets/javascripts/discourse/app/routes/preferences-interface.js
index 34976bbd825..bf483fc9871 100644
--- a/app/assets/javascripts/discourse/app/routes/preferences-interface.js
+++ b/app/assets/javascripts/discourse/app/routes/preferences-interface.js
@@ -12,6 +12,7 @@ export default RestrictedUserRoute.extend({
makeThemeDefault:
!user.get("user_option.theme_ids") ||
currentThemeId() === user.get("user_option.theme_ids")[0],
+ makeColorSchemeDefault: !user.get("user_option.color_scheme_id"),
makeTextSizeDefault:
user.get("currentTextSize") === user.get("user_option.text_size")
});
diff --git a/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs
index d4cd222b96f..e53cd172c2c 100644
--- a/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs
+++ b/app/assets/javascripts/discourse/app/templates/preferences/interface.hbs
@@ -20,6 +20,53 @@
{{/if}}
+{{#if showColorSchemeSelector}}
+
+
+
+ {{#if showDarkColorSchemeSelector}}
+
{{i18n "user.color_schemes.regular" }}
+ {{/if}}
+
+ {{combo-box
+ content=userSelectableColorSchemes
+ value=selectedColorSchemeId
+ onChange=(action "loadColorScheme")
+ options=(hash
+ translatedNone=selectedColorSchemeNoneLabel
+ )
+
+ }}
+
+
+ {{#if showDarkColorSchemeSelector}}
+
+
{{i18n "user.color_schemes.dark" }}
+
+ {{combo-box
+ content=userSelectableDarkColorSchemes
+ value=selectedDarkColorSchemeId
+ onChange=(action "loadDarkColorScheme")}}
+
+
+
+
+ {{i18n "user.color_schemes.dark_instructions" }}
+
+ {{/if}}
+
+ {{#if previewingColorScheme}}
+ {{#if previewingColorScheme}}
+ {{d-button action=(action "undoColorSchemePreview") label="user.color_schemes.undo" icon="undo" class="btn-default btn-small undo-preview"}}
+ {{/if}}
+
+
+ {{preference-checkbox labelKey="user.color_scheme_default_on_all_devices" checked=makeColorSchemeDefault}}
+
+ {{/if}}
+
+{{/if}}
+
{{#if showDarkModeToggle}}
diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss
index e5617aa0124..0cc24a534be 100644
--- a/app/assets/stylesheets/common/admin/customize.scss
+++ b/app/assets/stylesheets/common/admin/customize.scss
@@ -179,6 +179,10 @@
margin: 0 0.5em 0.5em 0;
}
}
+
+ &.color-scheme .admin-controls {
+ padding: 0.5em;
+ }
}
.add-component-button {
vertical-align: middle;
@@ -486,10 +490,16 @@
}
.color-scheme {
.controls {
+ display: flex;
+ align-items: center;
button,
a {
margin-right: 10px;
}
+
+ button.btn-danger {
+ margin-left: auto;
+ }
}
}
.colors {
@@ -507,8 +517,8 @@
margin-bottom: 0;
margin-right: 6px;
}
- .hex {
- text-align: center;
+ th.overriden {
+ text-align: right;
}
.color-input {
display: flex;
diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss
index 4aabed9c24c..275444ab7ea 100644
--- a/app/assets/stylesheets/common/base/user.scss
+++ b/app/assets/stylesheets/common/base/user.scss
@@ -656,6 +656,46 @@
.save-theme-alert {
font-size: $font-down-1;
}
+
+ .control-subgroup {
+ float: left;
+ + .controls {
+ clear: both;
+ padding-top: 1em;
+ }
+ }
+
+ .light-color-scheme {
+ margin-right: 1em;
+ }
+
+ .instructions {
+ clear: both;
+ display: inline-block;
+ margin-top: 4px;
+ }
+
+ @mixin inactiveMode() {
+ color: var(--primary-medium);
+ .select-kit.combo-box .select-kit-header {
+ border-color: var(--primary-medium);
+ }
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .light-color-scheme {
+ @include inactiveMode;
+ }
+ }
+ @media (prefers-color-scheme: light) {
+ .dark-color-scheme {
+ @include inactiveMode;
+ }
+ }
+
+ .undo-preview {
+ margin-bottom: 1em;
+ }
}
.paginated-topics-list {
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 9432a82ff98..aed20e9962e 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -247,11 +247,6 @@
.user-chooser {
width: 100%;
}
-
- .instructions {
- display: inline-block;
- margin-top: 4px;
- }
}
.user-crawler {
diff --git a/app/controllers/admin/color_schemes_controller.rb b/app/controllers/admin/color_schemes_controller.rb
index 79292350908..4b6407158ab 100644
--- a/app/controllers/admin/color_schemes_controller.rb
+++ b/app/controllers/admin/color_schemes_controller.rb
@@ -38,6 +38,6 @@ class Admin::ColorSchemesController < Admin::AdminController
end
def color_scheme_params
- params.permit(color_scheme: [:base_scheme_id, :name, colors: [:name, :hex]])[:color_scheme]
+ params.permit(color_scheme: [:base_scheme_id, :name, :user_selectable, colors: [:name, :hex]])[:color_scheme]
end
end
diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb
index 3391456cda7..163ed9e430f 100644
--- a/app/controllers/stylesheets_controller.rb
+++ b/app/controllers/stylesheets_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class StylesheetsController < ApplicationController
- skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map]
+ skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map, :color_scheme]
def show_source_map
show_resource(source_map: true)
@@ -13,6 +13,13 @@ class StylesheetsController < ApplicationController
show_resource
end
+ def color_scheme
+ params.require("id")
+ params.permit("theme_id")
+
+ stylesheet = Stylesheet::Manager.color_scheme_stylesheet_details(params[:id], 'all', params[:theme_id])
+ render json: stylesheet
+ end
protected
def show_resource(source_map: false)
@@ -28,7 +35,7 @@ class StylesheetsController < ApplicationController
if !Rails.env.production?
# TODO add theme
# calling this method ensures we have a cache for said target
- # we hold of re-compilation till someone asks for asset
+ # we hold off re-compilation till someone asks for asset
if target.include?("color_definitions")
split_target, color_scheme_id = target.split(/_(-?[0-9]+)/)
Stylesheet::Manager.color_scheme_stylesheet_link_tag(color_scheme_id)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d4fe2de92b2..c0dec694b17 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -402,6 +402,11 @@ module ApplicationHelper
end
def scheme_id
+ custom_user_scheme_id = cookies[:color_scheme_id] || current_user&.user_option&.color_scheme_id
+ if custom_user_scheme_id && ColorScheme.find_by_id(custom_user_scheme_id)
+ return custom_user_scheme_id
+ end
+
return if theme_ids.blank?
Theme
.where(id: theme_ids.first)
@@ -409,6 +414,10 @@ module ApplicationHelper
.first
end
+ def dark_scheme_id
+ cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id || SiteSetting.default_dark_mode_color_scheme_id
+ end
+
def current_homepage
current_user&.user_option&.homepage || SiteSetting.anonymous_homepage
end
@@ -454,9 +463,6 @@ module ApplicationHelper
result = +""
result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(scheme_id, 'all', theme_ids)
- user_dark_scheme_id = current_user&.user_option&.dark_scheme_id
- dark_scheme_id = user_dark_scheme_id || SiteSetting.default_dark_mode_color_scheme_id
-
if dark_scheme_id != -1
result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)', theme_ids)
end
@@ -489,7 +495,9 @@ module ApplicationHelper
highlight_js_path: HighlightJs.path,
svg_sprite_path: SvgSprite.path(theme_ids),
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
- color_scheme_is_dark: dark_color_scheme?
+ color_scheme_is_dark: dark_color_scheme?,
+ user_color_scheme_id: scheme_id,
+ user_dark_scheme_id: dark_scheme_id
}
if Rails.env.development?
diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb
index 4dbdde5061b..ab82c117a92 100644
--- a/app/models/color_scheme.rb
+++ b/app/models/color_scheme.rb
@@ -131,8 +131,8 @@ class ColorScheme < ActiveRecord::Base
before_save :bump_version
after_save :publish_discourse_stylesheet
- after_save :dump_hex_cache
- after_destroy :dump_hex_cache
+ after_save :dump_caches
+ after_destroy :dump_caches
belongs_to :theme
validates_associated :color_scheme_colors
@@ -286,8 +286,9 @@ class ColorScheme < ActiveRecord::Base
end
end
- def dump_hex_cache
+ def dump_caches
self.class.hex_cache.clear
+ ApplicationSerializer.expire_cache_fragment!("user_color_schemes")
end
def bump_version
@@ -297,6 +298,8 @@ class ColorScheme < ActiveRecord::Base
end
def is_dark?
+ return if colors.empty?
+
primary_b = brightness(colors_by_name["primary"].hex)
secondary_b = brightness(colors_by_name["secondary"].hex)
@@ -314,12 +317,13 @@ end
#
# Table name: color_schemes
#
-# id :integer not null, primary key
-# name :string not null
-# version :integer default(1), not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# via_wizard :boolean default(FALSE), not null
-# base_scheme_id :string
-# theme_id :integer
+# id :integer not null, primary key
+# name :string not null
+# version :integer default(1), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# via_wizard :boolean default(FALSE), not null
+# base_scheme_id :string
+# theme_id :integer
+# user_selectable :boolean default(FALSE), not null
#
diff --git a/app/serializers/color_scheme_selectable_serializer.rb b/app/serializers/color_scheme_selectable_serializer.rb
new file mode 100644
index 00000000000..10ad373e6aa
--- /dev/null
+++ b/app/serializers/color_scheme_selectable_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ColorSchemeSelectableSerializer < ApplicationSerializer
+ attributes :id, :name, :is_dark
+
+ def is_dark
+ object.is_dark?
+ end
+end
diff --git a/app/serializers/color_scheme_serializer.rb b/app/serializers/color_scheme_serializer.rb
index 7c5237f97fa..f8ae507a86c 100644
--- a/app/serializers/color_scheme_serializer.rb
+++ b/app/serializers/color_scheme_serializer.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ColorSchemeSerializer < ApplicationSerializer
- attributes :id, :name, :is_base, :base_scheme_id, :theme_id, :theme_name
+ attributes :id, :name, :is_base, :base_scheme_id, :theme_id, :theme_name, :user_selectable
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
def theme_name
diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb
index 96833f398e7..f7915050aa9 100644
--- a/app/serializers/site_serializer.rb
+++ b/app/serializers/site_serializer.rb
@@ -24,6 +24,8 @@ class SiteSerializer < ApplicationSerializer
:wizard_required,
:topic_featured_link_allowed_category_ids,
:user_themes,
+ :user_color_schemes,
+ :default_dark_color_scheme,
:censored_regexp,
:shared_drafts_category_id,
:custom_emoji_translation
@@ -40,12 +42,23 @@ class SiteSerializer < ApplicationSerializer
Theme.where('id = :default OR user_selectable',
default: SiteSetting.default_theme_id)
.order(:name)
- .pluck(:id, :name)
- .map { |id, n| { theme_id: id, name: n, default: id == SiteSetting.default_theme_id } }
+ .pluck(:id, :name, :color_scheme_id)
+ .map { |id, n, cs| { theme_id: id, name: n, default: id == SiteSetting.default_theme_id, color_scheme_id: cs } }
.as_json
end
end
+ def user_color_schemes
+ cache_fragment("user_color_schemes") do
+ schemes = ColorScheme.where('user_selectable').order(:name)
+ ActiveModel::ArraySerializer.new(schemes, each_serializer: ColorSchemeSelectableSerializer).as_json
+ end
+ end
+
+ def default_dark_color_scheme
+ ColorScheme.find_by_id(SiteSetting.default_dark_mode_color_scheme_id).as_json
+ end
+
def groups
cache_anon_fragment("group_names") do
object.groups.order(:name).pluck(:id, :name).map { |id, name| { id: id, name: name } }.as_json
diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb
index fb6091abd9d..ed47d0e6dbe 100644
--- a/app/serializers/user_option_serializer.rb
+++ b/app/serializers/user_option_serializer.rb
@@ -8,6 +8,7 @@ class UserOptionSerializer < ApplicationSerializer
:email_level,
:email_messages_level,
:external_links_in_new_tab,
+ :color_scheme_id,
:dark_scheme_id,
:dynamic_favicon,
:enable_quoting,
diff --git a/app/services/color_scheme_revisor.rb b/app/services/color_scheme_revisor.rb
index e76f3d6f36b..7515d513d3a 100644
--- a/app/services/color_scheme_revisor.rb
+++ b/app/services/color_scheme_revisor.rb
@@ -13,8 +13,8 @@ class ColorSchemeRevisor
def revise
ColorScheme.transaction do
-
- @color_scheme.name = @params[:name] if @params.has_key?(:name)
+ @color_scheme.name = @params[:name] if @params.has_key?(:name)
+ @color_scheme.user_selectable = @params[:user_selectable] if @params.has_key?(:user_selectable)
@color_scheme.base_scheme_id = @params[:base_scheme_id] if @params.has_key?(:base_scheme_id)
has_colors = @params[:colors]
@@ -31,6 +31,7 @@ class ColorSchemeRevisor
if has_colors ||
@color_scheme.saved_change_to_name? ||
+ @color_scheme.will_save_change_to_user_selectable? ||
@color_scheme.saved_change_to_base_scheme_id?
@color_scheme.save
diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb
index 913e5ee7cd0..713fc2b9999 100644
--- a/app/services/user_updater.rb
+++ b/app/services/user_updater.rb
@@ -26,6 +26,7 @@ class UserUpdater
:external_links_in_new_tab,
:enable_quoting,
:enable_defer,
+ :color_scheme_id,
:dark_scheme_id,
:dynamic_favicon,
:automatically_unpin_topics,
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 072638439e5..c8d2915cdba 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -935,6 +935,16 @@ en:
not_first_time: "Not your first time?"
skip_link: "Skip these tips"
theme_default_on_all_devices: "Make this the default theme on all my devices"
+ color_scheme_default_on_all_devices: "Set default color scheme(s) on all my devices"
+ color_scheme: "Color Scheme"
+ color_schemes:
+ default_description: "Default"
+ disable_dark_scheme: "Same as regular"
+ dark_instructions: "You can preview the dark mode color scheme by toggling your device's dark mode."
+ undo: "Reset"
+ regular: "Regular"
+ dark: "Dark mode"
+ default_dark_scheme: "(site default)"
dark_mode: "Dark Mode"
dark_mode_enable: "Enable automatic dark mode color scheme"
text_size_default_on_all_devices: "Make this the default text size on all my devices"
@@ -3886,6 +3896,7 @@ en:
hide_unused_fields: "Hide unused fields"
is_default: "Theme is enabled by default"
user_selectable: "Theme can be selected by users"
+ color_scheme_user_selectable: "Color scheme can be selected by users"
color_scheme: "Color Palette"
default_light_scheme: "Light (default)"
color_scheme_select: "Select colors to be used by theme"
diff --git a/config/routes.rb b/config/routes.rb
index bdf42bc0c30..47bc641eb5f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -518,6 +518,7 @@ Discourse::Application.routes.draw do
get "stylesheets/:name.css.map" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/ }
get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ }
+ get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json }
get "theme-javascripts/:digest.js" => "theme_javascripts#show", constraints: { digest: /\h{40}/ }
post "uploads/lookup-metadata" => "uploads#metadata"
diff --git a/db/migrate/20200819021210_add_user_selectable_column_to_color_schemes.rb b/db/migrate/20200819021210_add_user_selectable_column_to_color_schemes.rb
new file mode 100644
index 00000000000..e9add3e262c
--- /dev/null
+++ b/db/migrate/20200819021210_add_user_selectable_column_to_color_schemes.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddUserSelectableColumnToColorSchemes < ActiveRecord::Migration[6.0]
+ def change
+ add_column :color_schemes, :user_selectable, :bool, null: false, default: false
+ end
+end
diff --git a/db/migrate/20200819203846_add_color_scheme_id_to_user_options.rb b/db/migrate/20200819203846_add_color_scheme_id_to_user_options.rb
new file mode 100644
index 00000000000..fc700e5aa69
--- /dev/null
+++ b/db/migrate/20200819203846_add_color_scheme_id_to_user_options.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddColorSchemeIdToUserOptions < ActiveRecord::Migration[6.0]
+ def change
+ add_column :user_options, :color_scheme_id, :integer
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 257acb53c37..ad3a7a3239c 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -374,11 +374,44 @@ describe ApplicationHelper do
expect(cs_stylesheets).not_to include("(prefers-color-scheme: dark)")
end
- context "with a user option" do
+ context "custom light scheme" do
+ before do
+ @new_cs = Fabricate(:color_scheme, name: 'Flamboyant')
+ user.user_option.color_scheme_id = @new_cs.id
+ user.user_option.save!
+ helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
+ end
+
+ it "returns color scheme from user option value" do
+ color_stylesheets = helper.discourse_color_scheme_stylesheets
+ expect(color_stylesheets).to include("color_definitions_flamboyant")
+ end
+
+ it "returns color scheme from cookie value" do
+ cs = ColorScheme.where(name: "Dark").first
+ helper.request.cookies["color_scheme_id"] = cs.id
+
+ color_stylesheets = helper.discourse_color_scheme_stylesheets
+
+ expect(color_stylesheets).to include("color_definitions_dark")
+ expect(color_stylesheets).not_to include("color_definitions_flamboyant")
+ end
+
+ it "falls back to base scheme with invalid cookie value" do
+ helper.request.cookies["color_scheme_id"] = -50
+
+ color_stylesheets = helper.discourse_color_scheme_stylesheets
+ expect(color_stylesheets).not_to include("color_definitions_flamboyant")
+ expect(color_stylesheets).to include("color_definitions_base")
+ end
+ end
+
+ context "dark scheme with user option and/or cookies" do
before do
user.user_option.dark_scheme_id = -1
user.user_option.save!
helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user
+ @new_cs = Fabricate(:color_scheme, name: 'Custom Color Scheme')
SiteSetting.default_dark_mode_color_scheme_id = ColorScheme.where(name: "Dark").pluck(:id).first
end
@@ -391,14 +424,28 @@ describe ApplicationHelper do
end
it "returns user-selected dark color scheme stylesheet" do
- new_cs = Fabricate(:color_scheme, name: 'Custom Color Scheme')
- user.user_option.update!(dark_scheme_id: new_cs.id)
+ user.user_option.update!(dark_scheme_id: @new_cs.id)
color_stylesheets = helper.discourse_color_scheme_stylesheets
expect(color_stylesheets).to include("(prefers-color-scheme: dark)")
expect(color_stylesheets).to include("custom-color-scheme")
end
+ it "respects cookie value over user option for dark color scheme" do
+ helper.request.cookies["dark_scheme_id"] = @new_cs.id
+
+ color_stylesheets = helper.discourse_color_scheme_stylesheets
+ expect(color_stylesheets).to include("(prefers-color-scheme: dark)")
+ expect(color_stylesheets).to include("custom-color-scheme")
+ end
+
+ it "returns no dark scheme with invalid cookie value" do
+ helper.request.cookies["dark_scheme_id"] = -10
+
+ color_stylesheets = helper.discourse_color_scheme_stylesheets
+ expect(color_stylesheets).not_to include("(prefers-color-scheme: dark)")
+ end
+
end
end
diff --git a/spec/models/color_scheme_spec.rb b/spec/models/color_scheme_spec.rb
index 7035f06b44c..1add1fa77ad 100644
--- a/spec/models/color_scheme_spec.rb
+++ b/spec/models/color_scheme_spec.rb
@@ -96,5 +96,10 @@ describe ColorScheme do
ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'F8F8F8' }, { name: 'secondary', hex: '232323' }])
expect(scheme.is_dark?).to eq(true)
end
+
+ it "does not break in scheme without colors" do
+ scheme = ColorScheme.create(name: "No Bueno")
+ expect(scheme.is_dark?).to eq(nil)
+ end
end
end
diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb
index c97aafe10b6..cf8a8ba9cc5 100644
--- a/spec/models/site_spec.rb
+++ b/spec/models/site_spec.rb
@@ -11,8 +11,8 @@ describe Site do
expected = Theme.where('id = :default OR user_selectable',
default: SiteSetting.default_theme_id)
.order(:name)
- .pluck(:id, :name)
- .map { |id, n| { "theme_id" => id, "name" => n, "default" => id == SiteSetting.default_theme_id } }
+ .pluck(:id, :name, :color_scheme_id)
+ .map { |id, n, cs| { "theme_id" => id, "name" => n, "default" => id == SiteSetting.default_theme_id, "color_scheme_id" => cs } }
expect(parsed["user_themes"]).to eq(expected)
end
diff --git a/spec/requests/stylesheets_controller_spec.rb b/spec/requests/stylesheets_controller_spec.rb
index 38f06152877..04926798acf 100644
--- a/spec/requests/stylesheets_controller_spec.rb
+++ b/spec/requests/stylesheets_controller_spec.rb
@@ -57,4 +57,26 @@ describe StylesheetsController do
expect(response.status).to eq(200)
end
+
+ context "#color_scheme" do
+ it 'works as expected' do
+ scheme = ColorScheme.last
+ get "/color-scheme-stylesheet/#{scheme.id}.json"
+
+ expect(response.status).to eq(200)
+ json = JSON.parse(response.body)
+ expect(json["color_scheme_id"]).to eq(scheme.id)
+ end
+
+ it 'works with a theme parameter' do
+ scheme = ColorScheme.last
+ theme = Theme.last
+ get "/color-scheme-stylesheet/#{scheme.id}/#{theme.id}.json"
+
+ expect(response.status).to eq(200)
+ json = JSON.parse(response.body)
+ expect(json["color_scheme_id"]).to eq(scheme.id)
+ end
+
+ end
end
diff --git a/spec/serializers/site_serializer_spec.rb b/spec/serializers/site_serializer_spec.rb
index 22a55ced834..bc9787a267f 100644
--- a/spec/serializers/site_serializer_spec.rb
+++ b/spec/serializers/site_serializer_spec.rb
@@ -28,4 +28,33 @@ describe SiteSerializer do
expect(categories[0][:notification_level]).to eq(0)
expect(categories[-1][:notification_level]).to eq(1)
end
+
+ it "includes user-selectable color schemes" do
+ scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
+ scheme.user_selectable = true
+ scheme.save!
+
+ serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
+ expect(serialized[:user_color_schemes].count).to eq (1)
+
+ dark_scheme = ColorScheme.create_from_base(name: "ADarkScheme", base_scheme_id: "Dark")
+ dark_scheme.user_selectable = true
+ dark_scheme.save!
+
+ serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
+ expect(serialized[:user_color_schemes].count).to eq(2)
+ expect(serialized[:user_color_schemes][0][:is_dark]).to eq(true)
+ end
+
+ it "includes default dark mode scheme" do
+ scheme = ColorScheme.last
+ SiteSetting.default_dark_mode_color_scheme_id = scheme.id
+ serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
+ default_dark_scheme =
+ expect(serialized[:default_dark_color_scheme]["name"]).to eq(scheme.name)
+
+ SiteSetting.default_dark_mode_color_scheme_id = -1
+ serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
+ expect(serialized[:default_dark_color_scheme]).to eq(nil)
+ end
end
diff --git a/spec/services/color_scheme_revisor_spec.rb b/spec/services/color_scheme_revisor_spec.rb
index 2c7d8590d75..c078481ce7e 100644
--- a/spec/services/color_scheme_revisor_spec.rb
+++ b/spec/services/color_scheme_revisor_spec.rb
@@ -48,6 +48,14 @@ describe ColorSchemeRevisor do
}.to_not change { color_scheme.reload.version }
expect(color_scheme.colors.first.hex).to eq(color.hex)
end
+
+ it "can change the user_selectable column" do
+ expect(color_scheme.user_selectable).to eq(false)
+
+ ColorSchemeRevisor.revise(color_scheme, { user_selectable: true })
+ expect(color_scheme.reload.user_selectable).to eq(true)
+ end
+
end
end
diff --git a/test/javascripts/acceptance/preferences-test.js b/test/javascripts/acceptance/preferences-test.js
index e0e3ceaf0c8..9b5b3b3c6ee 100644
--- a/test/javascripts/acceptance/preferences-test.js
+++ b/test/javascripts/acceptance/preferences-test.js
@@ -2,7 +2,6 @@ import I18n from "I18n";
import { acceptance, updateCurrentUser } from "helpers/qunit-helpers";
import selectKit from "helpers/select-kit-helper";
import User from "discourse/models/user";
-import cookie, { removeCookie } from "discourse/lib/cookie";
function preferencesPretender(server, helper) {
server.post("/u/second_factors.json", () => {
@@ -122,51 +121,6 @@ QUnit.test("update some fields", async assert => {
);
});
-QUnit.test("font size change", async assert => {
- removeCookie("text_size");
-
- const savePreferences = async () => {
- assert.ok(!exists(".saved"), "it hasn't been saved yet");
- await click(".save-changes");
- assert.ok(exists(".saved"), "it displays the saved message");
- find(".saved").remove();
- };
-
- await visit("/u/eviltrout/preferences/interface");
-
- // Live changes without reload
- await selectKit(".text-size .combobox").expand();
- await selectKit(".text-size .combobox").selectRowByValue("larger");
- assert.ok(document.documentElement.classList.contains("text-size-larger"));
-
- await selectKit(".text-size .combobox").expand();
- await selectKit(".text-size .combobox").selectRowByValue("largest");
- assert.ok(document.documentElement.classList.contains("text-size-largest"));
-
- assert.equal(cookie("text_size"), null, "cookie is not set");
-
- // Click save (by default this sets for all browsers, no cookie)
- await savePreferences();
-
- assert.equal(cookie("text_size"), null, "cookie is not set");
-
- await selectKit(".text-size .combobox").expand();
- await selectKit(".text-size .combobox").selectRowByValue("larger");
- await click(".text-size input[type=checkbox]");
-
- await savePreferences();
-
- assert.equal(cookie("text_size"), "larger|1", "cookie is set");
- await click(".text-size input[type=checkbox]");
- await selectKit(".text-size .combobox").expand();
- await selectKit(".text-size .combobox").selectRowByValue("largest");
-
- await savePreferences();
- assert.equal(cookie("text_size"), null, "cookie is removed");
-
- removeCookie("text_size");
-});
-
QUnit.test("username", async assert => {
await visit("/u/eviltrout/preferences/username");
assert.ok(exists("#change_username"), "it has the input element");
@@ -483,16 +437,3 @@ QUnit.test("can select an option from a dropdown", async assert => {
await field.selectRowByValue("Cat");
assert.equal(field.header().value(), "Cat", "it sets the value of the field");
});
-
-acceptance("User Preferences disabling dark mode", {
- loggedIn: true,
- settings: { default_dark_mode_color_scheme_id: 1 }
-});
-
-QUnit.test("shows option to disable dark mode", async assert => {
- await visit("/u/eviltrout/preferences/interface");
- assert.ok(
- $(".control-group.dark-mode").length,
- "it has the option to disable dark mode"
- );
-});
diff --git a/test/javascripts/acceptance/user-preferences-interface-test.js b/test/javascripts/acceptance/user-preferences-interface-test.js
new file mode 100644
index 00000000000..1fe2586832d
--- /dev/null
+++ b/test/javascripts/acceptance/user-preferences-interface-test.js
@@ -0,0 +1,206 @@
+import { acceptance } from "helpers/qunit-helpers";
+import selectKit from "helpers/select-kit-helper";
+import Site from "discourse/models/site";
+import Session from "discourse/models/session";
+import cookie, { removeCookie } from "discourse/lib/cookie";
+
+acceptance("User Preferences - Interface", {
+ loggedIn: true
+});
+
+QUnit.test("font size change", async assert => {
+ removeCookie("text_size");
+
+ const savePreferences = async () => {
+ assert.ok(!exists(".saved"), "it hasn't been saved yet");
+ await click(".save-changes");
+ assert.ok(exists(".saved"), "it displays the saved message");
+ find(".saved").remove();
+ };
+
+ await visit("/u/eviltrout/preferences/interface");
+
+ // Live changes without reload
+ await selectKit(".text-size .combobox").expand();
+ await selectKit(".text-size .combobox").selectRowByValue("larger");
+ assert.ok(document.documentElement.classList.contains("text-size-larger"));
+
+ await selectKit(".text-size .combobox").expand();
+ await selectKit(".text-size .combobox").selectRowByValue("largest");
+ assert.ok(document.documentElement.classList.contains("text-size-largest"));
+
+ assert.equal(cookie("text_size"), null, "cookie is not set");
+
+ // Click save (by default this sets for all browsers, no cookie)
+ await savePreferences();
+
+ assert.equal(cookie("text_size"), null, "cookie is not set");
+
+ await selectKit(".text-size .combobox").expand();
+ await selectKit(".text-size .combobox").selectRowByValue("larger");
+ await click(".text-size input[type=checkbox]");
+
+ await savePreferences();
+
+ assert.equal(cookie("text_size"), "larger|1", "cookie is set");
+ await click(".text-size input[type=checkbox]");
+ await selectKit(".text-size .combobox").expand();
+ await selectKit(".text-size .combobox").selectRowByValue("largest");
+
+ await savePreferences();
+ assert.equal(cookie("text_size"), null, "cookie is removed");
+
+ removeCookie("text_size");
+});
+
+QUnit.test(
+ "does not show option to disable dark mode by default",
+ async assert => {
+ await visit("/u/eviltrout/preferences/interface");
+ assert.equal($(".control-group.dark-mode").length, 0);
+ }
+);
+
+QUnit.test("shows light/dark color scheme pickers", async assert => {
+ let site = Site.current();
+ site.set("user_color_schemes", [
+ { id: 2, name: "Cool Breeze" },
+ { id: 3, name: "Dark Night", is_dark: true }
+ ]);
+
+ await visit("/u/eviltrout/preferences/interface");
+ assert.ok($(".light-color-scheme").length, "has regular dropdown");
+ assert.ok($(".dark-color-scheme").length, "has dark color scheme dropdown");
+});
+
+function interfacePretender(server, helper) {
+ server.get("/color-scheme-stylesheet/2.json", () => {
+ return helper.response({
+ success: "OK"
+ });
+ });
+}
+
+acceptance("User Preferences Color Schemes (with default dark scheme)", {
+ loggedIn: true,
+ settings: { default_dark_mode_color_scheme_id: 1 },
+ pretend: interfacePretender
+});
+
+QUnit.test("show option to disable dark mode", async assert => {
+ await visit("/u/eviltrout/preferences/interface");
+
+ assert.ok(
+ $(".control-group.dark-mode").length,
+ "it has the option to disable dark mode"
+ );
+});
+
+QUnit.test("no color scheme picker by default", async assert => {
+ let site = Site.current();
+ site.set("user_color_schemes", []);
+
+ await visit("/u/eviltrout/preferences/interface");
+ assert.equal($(".control-group.color-scheme").length, 0);
+});
+
+QUnit.test("light color scheme picker", async assert => {
+ let site = Site.current();
+ site.set("user_color_schemes", [{ id: 2, name: "Cool Breeze" }]);
+
+ await visit("/u/eviltrout/preferences/interface");
+ assert.ok($(".light-color-scheme").length, "has regular picker dropdown");
+ assert.equal(
+ $(".dark-color-scheme").length,
+ 0,
+ "does not have a dark color scheme picker"
+ );
+});
+
+QUnit.test("light and dark color scheme pickers", async assert => {
+ let site = Site.current();
+ let session = Session.current();
+ session.userDarkSchemeId = 1; // same as default set in site settings
+
+ site.set("default_dark_color_scheme", { id: 1, name: "Dark" });
+ site.set("user_color_schemes", [
+ { id: 2, name: "Cool Breeze" },
+ { id: 3, name: "Dark Night", is_dark: true }
+ ]);
+
+ const savePreferences = async () => {
+ assert.ok(!exists(".saved"), "it hasn't been saved yet");
+ await click(".save-changes");
+ assert.ok(exists(".saved"), "it displays the saved message");
+ find(".saved").remove();
+ };
+
+ await visit("/u/eviltrout/preferences/interface");
+ assert.ok($(".light-color-scheme").length, "has regular dropdown");
+ assert.ok($(".dark-color-scheme").length, "has dark color scheme dropdown");
+ assert.equal(
+ $(".dark-color-scheme .selected-name").data("value"),
+ session.userDarkSchemeId,
+ "sets site default as selected dark scheme"
+ );
+ assert.equal(
+ $(".control-group.dark-mode").length,
+ 0,
+ "it does not show disable dark mode checkbox"
+ );
+
+ removeCookie("color_scheme_id");
+ removeCookie("dark_scheme_id");
+
+ await selectKit(".light-color-scheme .combobox").expand();
+ await selectKit(".light-color-scheme .combobox").selectRowByValue(2);
+ assert.equal(cookie("color_scheme_id"), null, "cookie is not set");
+ assert.ok(
+ exists(".color-scheme-checkbox input:checked"),
+ "defaults to storing values in user options"
+ );
+
+ await savePreferences();
+ assert.equal(cookie("color_scheme_id"), null, "cookie is unchanged");
+
+ // Switch to saving changes in cookies
+ await click(".color-scheme-checkbox input[type=checkbox]");
+ await savePreferences();
+ assert.equal(cookie("color_scheme_id"), 2, "cookie is set");
+
+ // dark scheme
+ await selectKit(".dark-color-scheme .combobox").expand();
+ assert.ok(
+ selectKit(".dark-color-scheme .combobox")
+ .rowByValue(1)
+ .exists(),
+ "default dark scheme is included"
+ );
+
+ await selectKit(".dark-color-scheme .combobox").selectRowByValue(-1);
+ assert.equal(
+ cookie("dark_scheme_id"),
+ null,
+ "cookie is not set before saving"
+ );
+
+ await savePreferences();
+ assert.equal(cookie("dark_scheme_id"), -1, "cookie is set");
+
+ await click("button.undo-preview");
+ assert.equal(
+ selectKit(".light-color-scheme .combobox")
+ .header()
+ .value(),
+ null,
+ "resets light scheme dropdown"
+ );
+
+ assert.equal(
+ selectKit(".dark-color-scheme .combobox")
+ .header()
+ .value(),
+ session.userDarkSchemeId,
+ "resets dark scheme dropdown"
+ );
+});