diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0296b676eb5..d10380f0d1c 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -555,8 +555,12 @@ module ApplicationHelper
end
def dark_scheme_id
- cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id ||
- SiteSetting.default_dark_mode_color_scheme_id
+ if SiteSetting.use_overhauled_theme_color_palette
+ scheme_id
+ else
+ cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id ||
+ SiteSetting.default_dark_mode_color_scheme_id
+ end
end
def current_homepage
@@ -638,6 +642,7 @@ module ApplicationHelper
result << stylesheet_manager.color_scheme_stylesheet_preload_tag(
dark_scheme_id,
"(prefers-color-scheme: dark)",
+ dark: SiteSetting.use_overhauled_theme_color_palette,
)
end
@@ -657,6 +662,7 @@ module ApplicationHelper
dark_scheme_id,
"(prefers-color-scheme: dark)",
self.method(:add_resource_preload_list),
+ dark: SiteSetting.use_overhauled_theme_color_palette,
)
end
@@ -668,7 +674,7 @@ module ApplicationHelper
if dark_scheme_id != -1
result << <<~HTML
-
+
HTML
else
result << <<~HTML
diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb
index 30d3f97ce0c..921762957e4 100644
--- a/app/models/color_scheme.rb
+++ b/app/models/color_scheme.rb
@@ -409,16 +409,18 @@ class ColorScheme < ActiveRecord::Base
new_color_scheme
end
- def self.lookup_hex_for_name(name, scheme_id = nil)
+ def self.lookup_hex_for_name(name, scheme_id = nil, dark: false)
enabled_color_scheme = find_by(id: scheme_id) if scheme_id
enabled_color_scheme ||= Theme.where(id: SiteSetting.default_theme_id).first&.color_scheme
- (enabled_color_scheme || base).colors.find { |c| c.name == name }.try(:hex)
+ color_record = (enabled_color_scheme || base).colors.find { |c| c.name == name }
+ return if !color_record
+ dark ? color_record.dark_hex || color_record.hex : color_record.hex
end
- def self.hex_for_name(name, scheme_id = nil)
- hex_cache.defer_get_set(scheme_id ? name + "_#{scheme_id}" : name) do
- lookup_hex_for_name(name, scheme_id)
- end
+ def self.hex_for_name(name, scheme_id = nil, dark: false)
+ cache_key = scheme_id ? "#{name}_#{scheme_id}" : name
+ cache_key += "_dark" if dark
+ hex_cache.defer_get_set(cache_key) { lookup_hex_for_name(name, scheme_id, dark:) }
end
def colors=(arr)
@@ -450,10 +452,16 @@ class ColorScheme < ActiveRecord::Base
colors || ColorScheme.base_colors
end
- def resolved_colors
+ def resolved_colors(dark: false)
from_base = ColorScheme.base_colors
from_custom_scheme = base_colors
- from_db = colors.map { |c| [c.name, c.hex] }.to_h
+ from_db =
+ colors
+ .map do |c|
+ hex = dark ? (c.dark_hex || c.hex) : c.hex
+ [c.name, hex]
+ end
+ .to_h
resolved = from_base.merge(from_custom_scheme).except("hover", "selected").merge(from_db)
diff --git a/app/models/color_scheme_color.rb b/app/models/color_scheme_color.rb
index aa24a13bf5c..ce74d38ee26 100644
--- a/app/models/color_scheme_color.rb
+++ b/app/models/color_scheme_color.rb
@@ -4,6 +4,7 @@ class ColorSchemeColor < ActiveRecord::Base
belongs_to :color_scheme
validates :hex, format: { with: /\A([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ }
+ validates :dark_hex, format: { with: /\A([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ }, allow_nil: true
def hex_with_hash
"##{hex}"
@@ -20,6 +21,7 @@ end
# color_scheme_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
+# dark_hex :string(6)
#
# Indexes
#
diff --git a/config/site_settings.yml b/config/site_settings.yml
index f636835e720..a8904196ebe 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -3522,3 +3522,6 @@ experimental:
type: group_list
list_type: compact
area: "group_permissions"
+ use_overhauled_theme_color_palette:
+ default: false
+ hidden: true
diff --git a/db/migrate/20250120115539_add_dark_hex_to_color_scheme_color.rb b/db/migrate/20250120115539_add_dark_hex_to_color_scheme_color.rb
new file mode 100644
index 00000000000..cad3f7ef462
--- /dev/null
+++ b/db/migrate/20250120115539_add_dark_hex_to_color_scheme_color.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddDarkHexToColorSchemeColor < ActiveRecord::Migration[7.2]
+ def change
+ add_column :color_scheme_colors, :dark_hex, :string, limit: 6
+ end
+end
diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb
index 5d53844b295..601bfbe12d8 100644
--- a/lib/stylesheet/importer.rb
+++ b/lib/stylesheet/importer.rb
@@ -145,19 +145,21 @@ module Stylesheet
if @color_scheme_id
colors =
begin
- ColorScheme.find(@color_scheme_id).resolved_colors
+ ColorScheme.find(@color_scheme_id).resolved_colors(dark: @dark)
rescue StandardError
ColorScheme.base_colors
end
elsif (@theme_id && !theme.component)
- colors = theme&.color_scheme&.resolved_colors || ColorScheme.base_colors
+ colors = theme&.color_scheme&.resolved_colors(dark: @dark) || ColorScheme.base_colors
else
# this is a slightly ugly backwards compatibility fix,
# we shouldn't be using the default theme color scheme for components
# (most components use CSS custom properties which work fine without this)
colors =
- Theme.find_by_id(SiteSetting.default_theme_id)&.color_scheme&.resolved_colors ||
- ColorScheme.base_colors
+ Theme
+ .find_by_id(SiteSetting.default_theme_id)
+ &.color_scheme
+ &.resolved_colors(dark: @dark) || ColorScheme.base_colors
end
colors.each { |n, hex| contents << "$#{n}: ##{hex} !default; " }
@@ -178,6 +180,7 @@ module Stylesheet
@theme = options[:theme]
@theme_id = options[:theme_id]
@color_scheme_id = options[:color_scheme_id]
+ @dark = options[:dark]
if @theme && !@theme_id
# make up an id so other stuff does not bail out
diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb
index ce66bd44d38..0c2c0b107ec 100644
--- a/lib/stylesheet/manager.rb
+++ b/lib/stylesheet/manager.rb
@@ -42,10 +42,11 @@ class Stylesheet::Manager
cache.clear_regex(/#{plugin}/)
end
- def self.color_scheme_cache_key(color_scheme, theme_id = nil)
+ def self.color_scheme_cache_key(color_scheme, theme_id = nil, dark: false)
color_scheme_name = Slug.for(color_scheme.name) + color_scheme&.id.to_s
theme_string = theme_id ? "_theme#{theme_id}" : ""
- "#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}_#{theme_string}_#{Discourse.current_hostname}"
+ dark_string = dark ? "_dark" : ""
+ "#{COLOR_SCHEME_STYLESHEET}_#{color_scheme_name}_#{theme_string}_#{Discourse.current_hostname}#{dark_string}"
end
def self.precompile_css
@@ -114,14 +115,17 @@ class Stylesheet::Manager
theme = manager.get_theme(theme_id)
[theme_color_scheme, *color_schemes].compact.uniq.each do |scheme|
- $stderr.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{theme.name} (#{scheme.name})"
-
- Stylesheet::Manager::Builder.new(
- target: COLOR_SCHEME_STYLESHEET,
- theme: theme,
- color_scheme: scheme,
- manager: manager,
- ).compile(force: true)
+ [true, false].each do |dark|
+ mode = dark ? "dark" : "light"
+ $stderr.puts "precompile target: #{COLOR_SCHEME_STYLESHEET} #{theme.name} (#{scheme.name}) (#{mode})"
+ Stylesheet::Manager::Builder.new(
+ target: COLOR_SCHEME_STYLESHEET,
+ theme: theme,
+ color_scheme: scheme,
+ manager: manager,
+ dark:,
+ ).compile(force: true)
+ end
end
clear_color_scheme_cache!
@@ -336,7 +340,7 @@ class Stylesheet::Manager
end
end
- def color_scheme_stylesheet_details(color_scheme_id = nil, media)
+ def color_scheme_stylesheet_details(color_scheme_id = nil, media, dark: false)
theme_id = @theme_id || SiteSetting.default_theme_id
color_scheme =
@@ -353,10 +357,10 @@ class Stylesheet::Manager
target = COLOR_SCHEME_STYLESHEET.to_sym
current_hostname = Discourse.current_hostname
- cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id)
+ cache_key = self.class.color_scheme_cache_key(color_scheme, theme_id, dark:)
cache.defer_get_set(cache_key) do
- stylesheet = { color_scheme_id: color_scheme.id }
+ stylesheet = { color_scheme_id: color_scheme.id, dark: }
theme = get_theme(theme_id)
@@ -366,6 +370,7 @@ class Stylesheet::Manager
theme: get_theme(theme_id),
color_scheme: color_scheme,
manager: self,
+ dark:,
)
builder.compile unless File.exist?(builder.stylesheet_fullpath)
@@ -376,8 +381,8 @@ class Stylesheet::Manager
end
end
- def color_scheme_stylesheet_preload_tag(color_scheme_id = nil, media = "all")
- stylesheet = color_scheme_stylesheet_details(color_scheme_id, media)
+ def color_scheme_stylesheet_preload_tag(color_scheme_id = nil, media = "all", dark: false)
+ stylesheet = color_scheme_stylesheet_details(color_scheme_id, media, dark:)
return "" if !stylesheet
@@ -386,8 +391,13 @@ class Stylesheet::Manager
%[].html_safe
end
- def color_scheme_stylesheet_link_tag(color_scheme_id = nil, media = "all", preload_callback = nil)
- stylesheet = color_scheme_stylesheet_details(color_scheme_id, media)
+ def color_scheme_stylesheet_link_tag(
+ color_scheme_id = nil,
+ media = "all",
+ preload_callback = nil,
+ dark: false
+ )
+ stylesheet = color_scheme_stylesheet_details(color_scheme_id, media, dark:)
return "" if !stylesheet
diff --git a/lib/stylesheet/manager/builder.rb b/lib/stylesheet/manager/builder.rb
index 738770de3ce..005dabb11ae 100644
--- a/lib/stylesheet/manager/builder.rb
+++ b/lib/stylesheet/manager/builder.rb
@@ -3,11 +3,12 @@
class Stylesheet::Manager::Builder
attr_reader :theme
- def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:)
+ def initialize(target: :desktop, theme: nil, color_scheme: nil, manager:, dark: false)
@target = target
@theme = theme
@color_scheme = color_scheme
@manager = manager
+ @dark = dark
end
def compile(opts = {})
@@ -46,6 +47,7 @@ class Stylesheet::Manager::Builder
source_map_file: source_map_url_relative_from_stylesheet,
color_scheme_id: @color_scheme&.id,
load_paths: load_paths,
+ dark: @dark,
)
rescue SassC::SyntaxError, SassC::NotRenderedError => e
if Stylesheet::Importer::THEME_TARGETS.include?(@target.to_s)
@@ -119,13 +121,14 @@ class Stylesheet::Manager::Builder
end
def qualified_target
+ dark_string = @dark ? "_dark" : ""
if is_theme?
"#{@target}_#{theme&.id}"
elsif @color_scheme
- "#{@target}_#{scheme_slug}_#{@color_scheme&.id}_#{@theme&.id}"
+ "#{@target}_#{scheme_slug}_#{@color_scheme&.id}_#{@theme&.id}#{dark_string}"
else
scheme_string = theme&.color_scheme ? "_#{theme.color_scheme.id}" : ""
- "#{@target}#{scheme_string}"
+ "#{@target}#{scheme_string}#{dark_string}"
end
end
@@ -245,8 +248,9 @@ class Stylesheet::Manager::Builder
digest_string = "#{current_hostname}-"
if cs
theme_color_defs = resolve_baked_field(:common, :color_definitions)
+ dark_string = @dark ? "-dark" : ""
digest_string +=
- "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
+ "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}#{dark_string}"
else
digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 568e0248986..89e1e8de879 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -877,12 +877,20 @@ RSpec.describe ApplicationHelper do
describe "#discourse_theme_color_meta_tags" do
before do
light = Fabricate(:color_scheme)
- light.color_scheme_colors << ColorSchemeColor.new(name: "header_background", hex: "abcdef")
+ light.color_scheme_colors << ColorSchemeColor.new(
+ name: "header_background",
+ hex: "abcdef",
+ dark_hex: "fedcba",
+ )
light.save!
helper.request.cookies["color_scheme_id"] = light.id
dark = Fabricate(:color_scheme)
- dark.color_scheme_colors << ColorSchemeColor.new(name: "header_background", hex: "defabc")
+ dark.color_scheme_colors << ColorSchemeColor.new(
+ name: "header_background",
+ hex: "defabc",
+ dark_hex: "cbafed",
+ )
dark.save!
helper.request.cookies["dark_scheme_id"] = dark.id
end
@@ -902,6 +910,17 @@ RSpec.describe ApplicationHelper do
HTML
end
+
+ context "when use_overhauled_theme_color_palette setting is true" do
+ before { SiteSetting.use_overhauled_theme_color_palette = true }
+
+ it "renders a light and dark theme-color meta tag using the light and dark palettes of the same color scheme record" do
+ expect(helper.discourse_theme_color_meta_tags).to eq(<<~HTML)
+
+
+ HTML
+ end
+ end
end
describe "#discourse_color_scheme_meta_tag" do
@@ -944,4 +963,27 @@ RSpec.describe ApplicationHelper do
HTML
end
end
+
+ describe "#dark_scheme_id" do
+ fab!(:dark_scheme) { Fabricate(:color_scheme) }
+ fab!(:light_scheme) { Fabricate(:color_scheme) }
+
+ before do
+ helper.request.cookies["color_scheme_id"] = light_scheme.id
+ helper.request.cookies["dark_scheme_id"] = dark_scheme.id
+ end
+
+ it "returns the value set in the dark_scheme_id cookie" do
+ expect(helper.dark_scheme_id).to eq(dark_scheme.id)
+ end
+
+ context "when use_overhauled_theme_color_palette is true" do
+ before { SiteSetting.use_overhauled_theme_color_palette = true }
+
+ it "returns the same value as #scheme_id" do
+ expect(helper.dark_scheme_id).to eq(helper.scheme_id)
+ expect(helper.scheme_id).to eq(light_scheme.id)
+ end
+ end
+ end
end
diff --git a/spec/lib/stylesheet/manager_spec.rb b/spec/lib/stylesheet/manager_spec.rb
index b779c02b5d7..ca3359cdbbc 100644
--- a/spec/lib/stylesheet/manager_spec.rb
+++ b/spec/lib/stylesheet/manager_spec.rb
@@ -676,6 +676,73 @@ RSpec.describe Stylesheet::Manager do
expect(link).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_")
end
+ it "generates the dark mode of a color scheme when the dark option is specified" do
+ scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
+ ColorSchemeRevisor.revise(
+ scheme,
+ colors: [{ name: "primary", hex: "CABFAF", dark_hex: "FAFCAB" }],
+ )
+ theme = Fabricate(:theme)
+ manager = manager(theme.id)
+
+ dark_stylesheet =
+ Stylesheet::Manager::Builder.new(
+ target: :color_definitions,
+ theme: theme,
+ color_scheme: scheme,
+ manager: manager,
+ dark: true,
+ ).compile
+ light_stylesheet =
+ Stylesheet::Manager::Builder.new(
+ target: :color_definitions,
+ theme: theme,
+ color_scheme: scheme,
+ manager: manager,
+ ).compile
+
+ expect(light_stylesheet).to include("--primary: #CABFAF;")
+ expect(light_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}")
+ expect(light_stylesheet).not_to include(
+ "color_definitions_neutral_#{scheme.id}_#{theme.id}_dark",
+ )
+
+ expect(dark_stylesheet).to include("--primary: #FAFCAB;")
+ expect(dark_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}_dark")
+ end
+
+ it "uses the light colors as fallback if the dark scheme doesn't define them" do
+ scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
+ ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "BACFAB", dark_hex: nil }])
+ theme = Fabricate(:theme)
+ manager = manager(theme.id)
+
+ dark_stylesheet =
+ Stylesheet::Manager::Builder.new(
+ target: :color_definitions,
+ theme: theme,
+ color_scheme: scheme,
+ manager: manager,
+ dark: true,
+ ).compile
+ light_stylesheet =
+ Stylesheet::Manager::Builder.new(
+ target: :color_definitions,
+ theme: theme,
+ color_scheme: scheme,
+ manager: manager,
+ ).compile
+
+ expect(light_stylesheet).to include("--primary: #BACFAB;")
+ expect(light_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}")
+ expect(light_stylesheet).not_to include(
+ "color_definitions_neutral_#{scheme.id}_#{theme.id}_dark",
+ )
+
+ expect(dark_stylesheet).to include("--primary: #BACFAB;")
+ expect(dark_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}_dark")
+ end
+
it "updates outputted colors when updating a color scheme" do
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
theme = Fabricate(:theme)
@@ -905,7 +972,7 @@ RSpec.describe Stylesheet::Manager do
# Ensure we force compile each theme only once
expect(output.scan(/#{child_theme_with_css.name}/).length).to eq(2)
- expect(StylesheetCache.count).to eq(22) # (3 themes * 2 targets) + 16 color schemes (2 themes * 8 color schemes (7 defaults + 1 theme scheme))
+ expect(StylesheetCache.count).to eq(38) # (3 themes * 2 targets) + 32 color schemes (2 themes * 8 color schemes (7 defaults + 1 theme scheme) * 2 (light and dark mode per scheme))
end
it "generates precompiled CSS - core and themes" do
@@ -913,7 +980,7 @@ RSpec.describe Stylesheet::Manager do
Stylesheet::Manager.precompile_theme_css
results = StylesheetCache.pluck(:target)
- expect(results.size).to eq(30) # 11 core targets + 9 theme + 10 color schemes
+ expect(results.size).to eq(46) # 8 core targets + 6 theme + 32 color schemes (light and dark mode per scheme)
theme_targets.each do |tar|
expect(
@@ -929,7 +996,7 @@ RSpec.describe Stylesheet::Manager do
Stylesheet::Manager.precompile_theme_css
results = StylesheetCache.pluck(:target)
- expect(results.size).to eq(30) # 11 core targets + 9 theme + 10 color schemes
+ expect(results.size).to eq(46) # 8 core targets + 6 theme + 32 color schemes (light and dark mode per scheme)
expect(results).to include("color_definitions_#{scheme1.name}_#{scheme1.id}_#{user_theme.id}")
expect(results).to include(