diff --git a/app/assets/images/icons/computer-desktop.svg b/app/assets/images/icons/computer-desktop.svg
new file mode 100644
index 0000000000..0836863e6b
--- /dev/null
+++ b/app/assets/images/icons/computer-desktop.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/components/theme/mobile_switcher_component.html.erb b/app/components/theme/mobile_switcher_component.html.erb
new file mode 100644
index 0000000000..e725c0c5a4
--- /dev/null
+++ b/app/components/theme/mobile_switcher_component.html.erb
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ <% Users::Theme.default_themes.each do |theme| %>
+ -
+ <%= inline_svg_tag "icons/#{theme.icon}.svg", class: 'mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-gray-300 dark:group-hover:text-gray-400', title: "#{theme.name} mode option", aria: true %>
+ <%= "#{theme.name.capitalize} mode" %>
+
+ <% end %>
+
+
+
+
diff --git a/app/components/theme/mobile_switcher_component.rb b/app/components/theme/mobile_switcher_component.rb
new file mode 100644
index 0000000000..20d56aa883
--- /dev/null
+++ b/app/components/theme/mobile_switcher_component.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Theme::MobileSwitcherComponent < ApplicationComponent
+ def initialize(current_theme:)
+ @current_theme = current_theme
+ end
+
+ private
+
+ attr_reader :current_theme
+end
diff --git a/app/components/theme/switcher_component.html.erb b/app/components/theme/switcher_component.html.erb
index ee8f34aedb..571ee69839 100644
--- a/app/components/theme/switcher_component.html.erb
+++ b/app/components/theme/switcher_component.html.erb
@@ -1,4 +1,36 @@
-<%= link_to themes_path(theme: other_theme.name), class: yass(link: type), data: { turbo_method: :put } do %>
- <%= inline_svg_tag "icons/#{current_theme.icon}.svg", class: yass(icon: type), aria: true, title: 'theme icon' %>
- <%= text unless icon_only? %>
+<% if mobile? %>
+ <%= render Theme::MobileSwitcherComponent.new(current_theme:) %>
+<% else %>
+
+
+
+
+
+
+
+
+ <% Users::Theme.default_themes.each do |theme| %>
+ -
+ <%= inline_svg_tag "icons/#{theme.icon}.svg", class: 'mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-gray-300 dark:group-hover:text-gray-400', title: "#{theme.name} mode option", aria: true %>
+ <%= "#{theme.name.capitalize} mode" %>
+
+ <% end %>
+
+
+
<% end %>
diff --git a/app/components/theme/switcher_component.rb b/app/components/theme/switcher_component.rb
index 4e05d3c36d..19c0d4ac4b 100644
--- a/app/components/theme/switcher_component.rb
+++ b/app/components/theme/switcher_component.rb
@@ -8,10 +8,6 @@ def text
"#{current_theme.name.capitalize} mode"
end
- def other_theme
- Users::Theme.default_themes.find { |other_theme| other_theme.name != current_theme.name }
- end
-
def icon_only?
type == :icon_only
end
diff --git a/app/javascript/controllers/theme_switcher_controller.js b/app/javascript/controllers/theme_switcher_controller.js
new file mode 100644
index 0000000000..589f45b6f8
--- /dev/null
+++ b/app/javascript/controllers/theme_switcher_controller.js
@@ -0,0 +1,49 @@
+import { Controller } from '@hotwired/stimulus';
+import { put } from '@rails/request.js';
+
+export default class ThemeSwitcherController extends Controller {
+ static values = {
+ currentTheme: String,
+ url: String,
+ };
+
+ connect() {
+ const userThemePreference = this.currentThemeValue;
+
+ this.updateTheme(userThemePreference);
+ this.addThemeChangeListener(userThemePreference);
+ }
+
+ addThemeChangeListener(userThemePreference) {
+ const themeChangeHandler = () => this.updateTheme(userThemePreference);
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', themeChangeHandler);
+ }
+
+ async handleThemeChangeRequest(event) {
+ const { theme } = event.currentTarget.dataset;
+
+ await this.updateThemeAndSendRequest(theme);
+ }
+
+ async updateThemeAndSendRequest(theme) {
+ this.updateTheme(theme);
+ await put(this.urlValue, { body: JSON.stringify({ theme }) });
+ }
+
+ updateTheme(theme) {
+ const validThemes = ['light', 'dark'];
+ const userSystemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ const selectedTheme = validThemes.includes(theme) ? theme : userSystemTheme;
+
+ this.setUserTheme(selectedTheme);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ setUserTheme(theme) {
+ const rootElement = document.getElementById('root-element');
+ const availableThemes = ['system', 'light', 'dark'];
+
+ availableThemes.forEach((t) => rootElement.classList.remove(t));
+ rootElement.classList.add(theme);
+ }
+}
diff --git a/app/models/users/theme.rb b/app/models/users/theme.rb
index d8f0a94e7c..a95b410368 100644
--- a/app/models/users/theme.rb
+++ b/app/models/users/theme.rb
@@ -4,9 +4,15 @@ class Theme
DEFAULT_THEMES = [
%w[light sun],
- %w[dark moon]
+ %w[dark moon],
+ %w[system computer-desktop]
].freeze
+ def initialize(name:, icon:)
+ @name = name
+ @icon = icon
+ end
+
def self.default_themes
DEFAULT_THEMES.map { |name, icon| new(name:, icon:) }
end
@@ -19,17 +25,6 @@ def self.for(value)
default_themes.find { |theme| theme.name == value }
end
- attr_reader :name, :icon
-
- def initialize(name:, icon:)
- @name = name
- @icon = icon
- end
-
- def <=>(other)
- name <=> other.name
- end
-
def to_s
name
end
@@ -37,5 +32,11 @@ def to_s
def dark_mode?
name == 'dark'
end
+
+ def system_mode?
+ name == 'system'
+ end
+
+ attr_reader :name, :icon
end
end
diff --git a/spec/components/theme/switcher_component_spec.rb b/spec/components/theme/switcher_component_spec.rb
index 56c0b8ef79..a25eb0081e 100644
--- a/spec/components/theme/switcher_component_spec.rb
+++ b/spec/components/theme/switcher_component_spec.rb
@@ -2,22 +2,22 @@
RSpec.describe Theme::SwitcherComponent, type: :component do
context 'when dark mode is enabled' do
- it 'renders the light mode button' do
+ it 'renders the moon icon' do
component = described_class.new(current_theme: Users::Theme.for('dark'))
render_inline(component)
- expect(page).to have_link(href: '/themes?theme=light')
+ expect(page).to have_css('title', text: 'moon')
end
end
- context 'when dark mode is not enabled' do
- it 'renders the dark mode button' do
- component = described_class.new(current_theme: Users::Theme.for('light'))
+ context 'when system mode is not enabled' do
+ it 'renders the computer desktop icon' do
+ component = described_class.new(current_theme: Users::Theme.for('system'))
render_inline(component)
- expect(page).to have_link(href: '/themes?theme=dark')
+ expect(page).to have_css('title', text: 'computer-desktop')
end
end
end
diff --git a/spec/models/users/theme_spec.rb b/spec/models/users/theme_spec.rb
index 594c19a4cc..e7e32aca0f 100644
--- a/spec/models/users/theme_spec.rb
+++ b/spec/models/users/theme_spec.rb
@@ -5,7 +5,8 @@
it 'returns the default themes' do
expect(described_class.default_themes).to contain_exactly(
an_object_having_attributes(name: 'light', icon: 'sun'),
- an_object_having_attributes(name: 'dark', icon: 'moon')
+ an_object_having_attributes(name: 'dark', icon: 'moon'),
+ an_object_having_attributes(name: 'system', icon: 'computer-desktop')
)
end
end
@@ -43,6 +44,12 @@
end
end
+ context 'when the theme is system default' do
+ it 'returns true' do
+ expect(described_class.new(name: 'system', icon: 'system')).to be_system_mode
+ end
+ end
+
context 'when the theme is light' do
it 'returns false' do
expect(described_class.new(name: 'light', icon: 'sun')).not_to be_dark_mode