Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to make Light/Dark mode using system setting. #4341

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions app/assets/images/icons/system.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 46 additions & 4 deletions app/components/theme/switcher_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,46 @@
<%= 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? %>
<% end %>
<div class="relative <% mobile? ? 'dark:hover:bg-gray-700/60 dark:hover:text-gray-200' : '' %>" data-controller="visibility" data-action="visibility:click:outside->visibility#off" data-visibility-visible-value="false">
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
<% if mobile? %>
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
<div>
<button data-action="click->visibility#toggle" class="text-gray-600 group flex items-center dark:text-gray-300 py-2 px-3 text-base font-medium dark:text-gray-300 hover:text-gray-900" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<%= inline_svg_tag "icons/#{current_theme.icon}.svg", class: yass(icon: type), aria: true, title: 'theme icon' %>
<%= current_theme.name.capitalize %>
</button>
</div>
<% else %>
<div>
<button type="button" data-action="click->visibility#toggle" class="text-gray-600 group flex items-center dark:text-gray-300 py-2 px-3" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<%= inline_svg_tag "icons/#{current_theme.icon}.svg", class: yass(icon: type), aria: true, title: 'theme icon' %>
</button>
</div>
<% end %>

<div
data-visibility-target="content"
data-transition-enter="transition ease-out duration-200"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-enter-end="transform opacity-100 scale-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-10 scale-95"
class="hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-50"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1">
<%= link_to themes_path(theme: 'dark'), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-3 py-2 text-sm', data: { turbo_method: :put } do %>
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
<%= inline_svg_tag "icons/moon.svg", class: 'icons/sign-out.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', aria: true, title: 'Sign out icon', aria: true, title: 'theme icon' %>
Dark Mode
<% end %>

<%= link_to themes_path(theme: 'light'), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-3 py-2 text-sm', data: { turbo_method: :put } do %>
<%= inline_svg_tag "icons/sun.svg", class: 'icons/sign-out.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', aria: true, title: 'Sign out icon', aria: true, title: 'theme icon' %>
Light Mode
<% end %>

<%= link_to themes_path(theme: 'system'), class: 'text-gray-700 dark:text-gray-300 group flex items-center px-3 py-2 text-sm', data: { turbo_method: :put, controller: 'handle-theme' } do %>
<%= inline_svg_tag "icons/system.svg", class: 'icons/sign-out.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', aria: true, title: 'Sign out icon', aria: true, title: 'theme icon' %>
System
<% end %>
</div>
</div>
4 changes: 0 additions & 4 deletions app/components/theme/switcher_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/components/theme/switcher_component.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
link:
base: 'text-gray-700 group flex items-center text-sm dark:text-gray-300'
base: 'text-gray-600 group flex items-center dark:text-gray-300'
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
default: 'py-2 px-3'
mobile: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 text-base font-medium py-2 px-2 dark:hover:bg-gray-700/60 dark:hover:text-gray-200'
icon_only: 'py-2 px-3'
Expand Down
40 changes: 40 additions & 0 deletions app/javascript/controllers/handle_theme_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Controller } from '@hotwired/stimulus';

export default class HandleThemeController extends Controller {
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
connect() {
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
function getCookie(name) {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const trimmedCookie = cookie.trim();
if (trimmedCookie.startsWith(`${name}=`)) {
return trimmedCookie.substring(name.length + 1);
}
}
return null;
}

const userThemePreference = getCookie('theme');
const rootElement = document.getElementById('root-element');

const setUserTheme = (theme) => {
rootElement.removeAttribute('class');
rootElement.classList.add(theme);
};

const updateTheme = () => {
if (!['light', 'dark'].includes(userThemePreference)) {
const userSystemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
setUserTheme(userSystemTheme);
}
};

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);

if (userThemePreference === 'system') {
const userSystemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
setUserTheme(userSystemTheme);
} else {
setUserTheme(userThemePreference);
}
}
}
25 changes: 13 additions & 12 deletions app/models/users/theme.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class Theme

DEFAULT_THEMES = [
%w[light sun],
%w[dark moon]
%w[dark moon],
%w[system system]
].freeze

def self.default_themes
Expand All @@ -19,23 +20,23 @@ 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
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
name
end

def dark_mode?
name == 'dark'
end

def system_mode?
sparshalc marked this conversation as resolved.
Show resolved Hide resolved
name == 'system'
end

attr_reader :name, :icon

def initialize(name:, icon:)
@name = name
@icon = icon
end
end
end
9 changes: 8 additions & 1 deletion spec/models/users/theme_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'system')
)
end
end
Expand Down Expand Up @@ -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
Expand Down