DEV: Let themes extend color definitions (#10429)

Themes can now declare custom colors that get compiled in core's color definitions stylesheet, thus allowing themes to better support dark/light color schemes. 

For example, if you need your theme to use tertiary for an element in a light color scheme and quaternary in a dark scheme, you can add the following SCSS to your theme's `color_definitions.scss` file: 

```
:root {
  --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};
}
```

And then use the `--mytheme-tertiary-or-quaternary` variable as the color property of that element. You can also use this file to add color variables that use SCSS color transformation functions (lighten, darken, saturate, etc.) without compromising your theme's compatibility with different color schemes.
This commit is contained in:
Penar Musaraj 2020-08-18 13:02:13 -04:00 committed by GitHub
parent 16e7744ab5
commit 882b0aac19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 101 additions and 37 deletions

View File

@ -33,6 +33,15 @@ export default Component.extend({
}
},
@observes("placeholder")
placeholderChanged() {
if (this._editor) {
this._editor.setOptions({
placeholder: this.placeholder
});
}
},
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
@ -72,7 +81,6 @@ export default Component.extend({
didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], loadedAce => {
loadedAce.config.set("loadWorkerFromBlob", false);
@ -85,7 +93,7 @@ export default Component.extend({
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px" });
editor.setOptions({ fontSize: "14px", placeholder: this.placeholder });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;

View File

@ -1,3 +1,4 @@
import I18n from "I18n";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
@ -30,9 +31,17 @@ export default Component.extend({
activeSectionMode(targetName, fieldName) {
if (["settings", "translations"].includes(targetName)) return "yaml";
if (["extra_scss"].includes(targetName)) return "scss";
if (["color_definitions"].includes(fieldName)) return "scss";
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@discourseComputed("currentTargetName", "fieldName")
placeholder(targetName, fieldName) {
return fieldName && fieldName === "color_definitions"
? I18n.t("admin.customize.theme.color_definitions.placeholder")
: "";
},
@discourseComputed("fieldName", "currentTargetName", "theme")
activeSection: {
get(fieldName, target, model) {

View File

@ -72,7 +72,7 @@ const Theme = RestModel.extend({
}
return {
common: [...common, "embedded_scss"],
common: [...common, "embedded_scss", "color_definitions"],
desktop: common,
mobile: common,
settings: ["yaml"],

View File

@ -87,4 +87,4 @@
<pre class="field-error">{{error}}</pre>
{{/if}}
{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}}
{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true" placeholder=placeholder}}

View File

@ -447,6 +447,12 @@
bottom: 0;
}
.ace_placeholder {
font-family: inherit;
font-size: $font-up-1;
color: $primary-high;
}
.status-actions {
float: right;
margin-top: 7px;

View File

@ -458,7 +458,7 @@ module ApplicationHelper
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)')
result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)', theme_ids)
end
result.html_safe
end

View File

@ -271,7 +271,7 @@ class ColorScheme < ActiveRecord::Base
def publish_discourse_stylesheet
if self.id
Stylesheet::Manager.color_scheme_cache_clear(self)
Stylesheet::Manager.clear_color_scheme_cache!
theme_ids = Theme.where(color_scheme_id: self.id).pluck(:id)
if theme_ids.present?

View File

@ -265,7 +265,7 @@ class ThemeField < ActiveRecord::Base
end
def self.scss_fields
@scss_fields ||= %w(scss embedded_scss)
@scss_fields ||= %w(scss embedded_scss color_definitions)
end
def self.basic_targets
@ -424,6 +424,9 @@ class ThemeField < ActiveRecord::Base
ThemeFileMatcher.new(regex: /^common\/embedded\.scss$/,
targets: :common, names: "embedded_scss", types: :scss,
canonical: -> (h) { "common/embedded.scss" }),
ThemeFileMatcher.new(regex: /^common\/color_definitions\.scss$/,
targets: :common, names: "color_definitions", types: :scss,
canonical: -> (h) { "common/color_definitions.scss" }),
ThemeFileMatcher.new(regex: /^(?:scss|stylesheets)\/(?<name>.+)\.scss$/,
targets: :extra_scss, names: nil, types: :scss,
canonical: -> (h) { "stylesheets/#{h[:name]}.scss" }),

View File

@ -3994,6 +3994,10 @@ en:
embedded_scss:
text: "Embedded CSS"
title: "Enter custom CSS to deliver with embedded version of comments"
color_definitions:
text: "Color Definitions"
title: "Enter custom color definitions (advanced users only)"
placeholder: "\r\nUse this stylesheet to add custom colors to the list of CSS custom properties.\r\n\r\nExample: \r\n\r\n:root {\r\n --mytheme-tertiary-or-quaternary: #{dark-light-choose($tertiary, $quaternary)};\r\n}\r\n\r\nPrefixing the property names is highly recommended to avoid conflicts with plugins and/or core."
head_tag:
text: "</head>"
title: "HTML that will be inserted before the </head> tag"

View File

@ -21,7 +21,7 @@ module Stylesheet
file = File.read path
if asset.to_s == Stylesheet::Manager::COLOR_SCHEME_STYLESHEET
file += Stylesheet::Importer.import_color_definitions
file += Stylesheet::Importer.import_color_definitions(options[:theme_id])
end
end

View File

@ -115,14 +115,20 @@ module Stylesheet
register_imports!
def self.import_color_definitions
return "" unless DiscoursePluginRegistry.color_definition_stylesheets.length
def self.import_color_definitions(theme_id)
contents = +""
DiscoursePluginRegistry.color_definition_stylesheets.each do |name, path|
contents << "// Color definitions from #{name}\n\n"
contents << File.read(path.to_s)
contents << "\n\n"
end
if theme_id
Theme.list_baked_fields([theme_id], :common, :color_definitions).each do |row|
contents << "// Color definitions from #{Theme.find_by_id(theme_id)&.name}\n\n"
contents << row.value
end
end
contents
end

View File

@ -23,6 +23,10 @@ class Stylesheet::Manager
cache.hash.keys.select { |k| k =~ /theme/ }.each { |k| cache.delete(k) }
end
def self.clear_color_scheme_cache!
cache.hash.keys.select { |k| k =~ /color_definitions/ }.each { |k| cache.delete(k) }
end
def self.clear_core_cache!(targets)
cache.hash.keys.select { |k| k =~ /#{targets.join('|')}/ }.each { |k| cache.delete(k) }
end
@ -94,13 +98,14 @@ class Stylesheet::Manager
end
def self.color_scheme_stylesheet_details(color_scheme_id = nil, media, theme_id)
theme_id = theme_id || SiteSetting.default_theme_id
color_scheme = begin
ColorScheme.find(color_scheme_id)
rescue
# don't load fallback when requesting dark color scheme
return false if media != "all"
theme_id = theme_id || SiteSetting.default_theme_id
Theme.find_by_id(theme_id)&.color_scheme || ColorScheme.base
end
@ -108,13 +113,14 @@ class Stylesheet::Manager
target = COLOR_SCHEME_STYLESHEET.to_sym
current_hostname = Discourse.current_hostname
cache_key = color_scheme_cache_key(color_scheme)
cache_key = color_scheme_cache_key(color_scheme, theme_id)
stylesheets = cache[cache_key]
return stylesheets if stylesheets.present?
stylesheet = { color_scheme_id: color_scheme&.id }
builder = self.new(target, nil, color_scheme)
builder = self.new(target, theme_id, color_scheme)
builder.compile unless File.exists?(builder.stylesheet_fullpath)
href = builder.stylesheet_path(current_hostname)
@ -132,20 +138,16 @@ class Stylesheet::Manager
%[<link href="#{href}" media="#{media}" rel="stylesheet"/>].html_safe
end
def self.color_scheme_cache_key(color_scheme)
def self.color_scheme_cache_key(color_scheme, theme_id = nil)
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
"#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}_#{Discourse.current_hostname}"
end
def self.color_scheme_cache_clear(color_scheme)
cache_key = color_scheme_cache_key(color_scheme)
cache[cache_key] = nil
theme_string = theme_id ? "_theme#{theme_id}" : ""
"#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}#{theme_string}_#{Discourse.current_hostname}"
end
def self.precompile_css
themes = Theme.where('user_selectable OR id = ?', SiteSetting.default_theme_id).pluck(:id, :name)
themes = Theme.where('user_selectable OR id = ?', SiteSetting.default_theme_id).pluck(:id, :name, :color_scheme_id)
themes << nil
themes.each do |id, name|
themes.each do |id, name, color_scheme_id|
[:desktop, :mobile, :desktop_rtl, :mobile_rtl, :desktop_theme, :mobile_theme, :admin].each do |target|
theme_id = id || SiteSetting.default_theme_id
next if target =~ THEME_REGEX && theme_id == -1
@ -156,17 +158,13 @@ class Stylesheet::Manager
builder.compile(force: true)
cache[cache_key] = nil
end
end
cs_ids = Theme.where('user_selectable OR id = ?', SiteSetting.default_theme_id).pluck(:color_scheme_id)
ColorScheme.where(id: cs_ids).each do |cs|
target = COLOR_SCHEME_STYLESHEET
STDERR.puts "precompile target: #{target} #{cs.name}"
scheme = ColorScheme.find_by_id(color_scheme_id) || ColorScheme.base
STDERR.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{name} (#{scheme.name})"
builder = self.new(target, nil, cs)
builder = self.new(COLOR_SCHEME_STYLESHEET, id, scheme)
builder.compile(force: true)
cache_key = color_scheme_cache_key(cs)
cache[cache_key] = nil
clear_color_scheme_cache!
end
nil
@ -349,8 +347,6 @@ class Stylesheet::Manager
end
def theme_digest
scss = ""
if [:mobile_theme, :desktop_theme].include?(@target)
scss_digest = theme.resolve_baked_field(:common, :scss)
scss_digest += theme.resolve_baked_field(@target.to_s.sub("_theme", ""), :scss)
@ -401,7 +397,8 @@ class Stylesheet::Manager
category_updated = Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum
if cs || category_updated > 0
Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}"
theme_color_defs = theme&.resolve_baked_field(:common, :color_definitions)
Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}"
else
digest_string = "defaults-#{Stylesheet::Manager.last_file_updated}"

View File

@ -175,6 +175,21 @@ describe Stylesheet::Manager do
expect(digest1).to_not eq(digest2)
end
it "updates digest when updating a theme's color definitions" do
scheme = ColorScheme.base
theme = Fabricate(:theme)
manager = Stylesheet::Manager.new(:color_definitions, theme.id, scheme)
digest1 = manager.color_scheme_digest
theme.set_field(target: :common, name: :color_definitions, value: 'body {color: brown}')
theme.save!
digest2 = manager.color_scheme_digest
expect(digest1).to_not eq(digest2)
end
end
describe 'color_scheme_stylesheets' do
@ -248,6 +263,16 @@ describe Stylesheet::Manager do
expect(stylesheet2).to include("--primary: #c00;")
end
it "includes theme color definitions in color scheme" do
theme = Fabricate(:theme)
theme.set_field(target: :common, name: :color_definitions, value: ':root {--special: rebeccapurple;}')
theme.save!
scheme = ColorScheme.base
stylesheet = Stylesheet::Manager.new(:color_definitions, theme.id, scheme).compile
expect(stylesheet).to include("--special: rebeccapurple")
end
end
# this test takes too long, we don't run it by default
@ -286,7 +311,7 @@ describe Stylesheet::Manager do
Stylesheet::Manager.precompile_css
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(16) # (2 themes x 7 targets) + (2 themes x 1 color scheme)
expect(results.size).to eq(17) # (2 themes x 7 targets) + 3 color schemes (2 themes, 1 base)
core_targets.each do |tar|
expect(results.count { |target| target =~ /^#{tar}_(#{scheme1.id}|#{scheme2.id})$/ }).to eq(2)
end
@ -301,7 +326,7 @@ describe Stylesheet::Manager do
Stylesheet::Manager.precompile_css
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(21) # (2 themes x 7 targets) + (1 no/default/core theme x 5 core targets) + (2 themes x 1 color scheme)
expect(results.size).to eq(22) # (2 themes x 7 targets) + (1 no/default/core theme x 5 core targets) + 3 color schemes (2 themes, 1 base)
core_targets.each do |tar|
expect(results.count { |target| target =~ /^(#{tar}_(#{scheme1.id}|#{scheme2.id})|#{tar})$/ }).to eq(3)

View File

@ -21,12 +21,15 @@ describe ColorScheme do
theme.save!
href = Stylesheet::Manager.stylesheet_data(:desktop_theme, theme.id)[0][:new_href]
colors_href = Stylesheet::Manager.color_scheme_stylesheet_details(scheme.id, "all", nil)
ColorSchemeRevisor.revise(scheme, colors: [{ name: 'primary', hex: 'bbb' }])
href2 = Stylesheet::Manager.stylesheet_data(:desktop_theme, theme.id)[0][:new_href]
colors_href2 = Stylesheet::Manager.color_scheme_stylesheet_details(scheme.id, "all", nil)
expect(href).not_to eq(href2)
expect(colors_href).not_to eq(colors_href2)
end
describe "new" do

View File

@ -60,6 +60,7 @@ describe RemoteTheme do
"common/header.html" => "I AM HEADER",
"common/random.html" => "I AM SILLY",
"common/embedded.scss" => "EMBED",
"common/color_definitions.scss" => ":root{--color-var: red}",
"assets/font.woff2" => "FAKE FONT",
"settings.yaml" => "boolean_setting: true",
"locales/en.yml" => "sometranslations"
@ -90,12 +91,14 @@ describe RemoteTheme do
expect(@theme.theme_modifier_set.serialize_topic_excerpts).to eq(true)
expect(@theme.theme_fields.length).to eq(9)
expect(@theme.theme_fields.length).to eq(10)
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
expect(mapped["0-header"]).to eq("I AM HEADER")
expect(mapped["1-scss"]).to eq(scss_data)
expect(mapped["0-embedded_scss"]).to eq("EMBED")
expect(mapped["0-color_definitions"]).to eq(":root{--color-var: red}")
expect(mapped["0-font"]).to eq("")
@ -103,7 +106,7 @@ describe RemoteTheme do
expect(mapped["4-en"]).to eq("sometranslations")
expect(mapped.length).to eq(9)
expect(mapped.length).to eq(10)
expect(@theme.settings.length).to eq(1)
expect(@theme.settings.first.value).to eq(true)