mirror of
https://github.com/discourse/discourse.git
synced 2025-03-20 06:57:55 +08:00
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:
parent
16e7744ab5
commit
882b0aac19
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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"],
|
||||
|
@ -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}}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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" }),
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}"
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user