1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-10 07:55:21 +02:00

User theme settings

This commit is contained in:
Josh Pigford 2025-04-10 10:08:28 -05:00
parent 52d170e36c
commit f1947a2dbe
7 changed files with 105 additions and 8 deletions

View file

@ -5,6 +5,8 @@
One-off styling (3rd party overrides, etc.) should be done in the application.css file.
*/
@custom-variant theme-dark (&:where([data-theme=dark], [data-theme=dark] *));
@theme {
/* Font families */
--font-sans: 'Geist', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@ -241,18 +243,34 @@
/* Design system color utilities */
@utility text-primary {
@apply text-gray-900;
@variant theme-dark {
@apply text-white;
}
}
@utility text-secondary {
@apply text-gray-500;
@variant theme-dark {
@apply text-gray-400;
}
}
@utility text-subdued {
@apply text-gray-400;
@variant theme-dark {
@apply text-gray-600;
}
}
@utility text-link {
@apply text-blue-600;
@variant theme-dark {
@apply text-blue-500;
}
}
@utility bg-surface {

View file

@ -74,7 +74,7 @@ class UsersController < ApplicationController
def user_params
params.require(:user).permit(
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled,
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :show_sidebar, :default_period, :show_ai_sidebar, :ai_enabled, :theme,
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id, :data_enrichment_enabled ]
)
end

View file

@ -0,0 +1,73 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { userPreference: String }
connect() {
this.applyTheme()
this.startSystemThemeListener()
}
disconnect() {
this.stopSystemThemeListener()
}
// Called automatically by Stimulus when the userPreferenceValue changes (e.g., after form submit/page reload)
userPreferenceValueChanged() {
this.applyTheme()
}
// Called when a theme radio button is clicked
updateTheme(event) {
const selectedTheme = event.currentTarget.value
if (selectedTheme === "system") {
this.setTheme(this.systemPrefersDark())
} else if (selectedTheme === "dark") {
this.setTheme(true)
} else {
this.setTheme(false)
}
}
// Applies theme based on the userPreferenceValue (from server)
applyTheme() {
if (this.userPreferenceValue === "system") {
this.setTheme(this.systemPrefersDark())
} else if (this.userPreferenceValue === "dark") {
this.setTheme(true)
} else {
this.setTheme(false)
}
}
// Sets or removes the data-theme attribute
setTheme(isDark) {
if (isDark) {
document.documentElement.setAttribute("data-theme", "dark")
} else {
document.documentElement.removeAttribute("data-theme")
}
}
systemPrefersDark() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
}
handleSystemThemeChange = (event) => {
// Only apply system theme changes if the user preference is currently 'system'
if (this.userPreferenceValue === "system") {
this.setTheme(event.matches)
}
}
startSystemThemeListener() {
this.darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
this.darkMediaQuery.addEventListener("change", this.handleSystemThemeChange)
}
stopSystemThemeListener() {
if (this.darkMediaQuery) {
this.darkMediaQuery.removeEventListener("change", this.handleSystemThemeChange)
}
}
}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="h-full text-primary overflow-hidden lg:overflow-auto font-sans <%= @os %>" lang="en">
<html class="h-full text-primary overflow-hidden lg:overflow-auto font-sans <%= @os %>" lang="en" data-controller="theme" data-theme-user-preference-value="<%= Current.user&.theme || 'system' %>">
<head>
<%= render "layouts/shared/head" %>
<%= yield :head %>

View file

@ -47,26 +47,26 @@
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
<div>
<%= styled_form_with model: @user, class: "flex justify-between items-center" do |form| %>
<%= styled_form_with model: @user, class: "flex justify-between items-center", data: { controller: "auto-submit-form" } do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<div class="text-center">
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">
<%= form.radio_button :theme, t(".theme_light"), checked: true %>
<%= form.radio_button :theme, "light", checked: @user.theme == "light", data: { auto_submit_form_target: "auto", action: "theme#updateTheme" } %>
<%= form.label :theme_light, t(".theme_light"), value: "light" %>
</div>
</div>
<div class="text-center">
<%= image_tag("dark-mode-preview.png", alt: "Dark Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">
<%= form.radio_button :theme, t(".theme_dark"), disabled: true, class: "cursor-not-allowed" %>
<%= form.radio_button :theme, "dark", checked: @user.theme == "dark", data: { auto_submit_form_target: "auto", action: "theme#updateTheme" } %>
<%= form.label :theme_dark, t(".theme_dark"), value: "dark" %>
</div>
</div>
<div class="text-center">
<%= image_tag("system-mode-preview.png", alt: "System Theme Preview", class: "h-44 mb-4") %>
<div class="flex items-center gap-2 justify-center">
<%= form.radio_button :theme, t(".theme_system"), disabled: true, class: "cursor-not-allowed" %>
<%= form.radio_button :theme, "system", checked: @user.theme == "system", data: { auto_submit_form_target: "auto", action: "theme#updateTheme" } %>
<%= form.label :theme_system, t(".theme_system"), value: "system" %>
</div>
</div>

View file

@ -0,0 +1,5 @@
class AddThemeToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :theme, :string, default: "system"
end
end

5
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_03_19_212839) do
ActiveRecord::Schema[7.2].define(version: 2025_04_10_144939) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -101,7 +101,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_19_212839) do
t.decimal "balance", precision: 19, scale: 4
t.string "currency"
t.boolean "is_active", default: true, null: false
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.uuid "import_id"
t.uuid "plaid_account_id"
t.boolean "scheduled_for_deletion", default: false
@ -614,6 +614,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_19_212839) do
t.uuid "last_viewed_chat_id"
t.boolean "show_ai_sidebar", default: true
t.boolean "ai_enabled", default: false, null: false
t.string "theme", default: "system"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["family_id"], name: "index_users_on_family_id"
t.index ["last_viewed_chat_id"], name: "index_users_on_last_viewed_chat_id"