From 2120d001868e107a9e088261b6d61751b30e4f25 Mon Sep 17 00:00:00 2001 From: Bruno Canepa <8711973+bruncanepa@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:36:16 -0300 Subject: [PATCH 01/17] Add Theme Switcher --- app/assets/stylesheets/_global.css | 111 +++++++++++++++++- .../controllers/theme_controller.js | 48 ++++++++ app/views/layouts/application.html.erb | 18 ++- 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 app/javascript/controllers/theme_controller.js diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 414d4c696..865be918c 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -234,8 +234,117 @@ --color-gradient-2: oklch(var(--lch-pink-lighter)); --color-gradient-3: oklch(var(--lch-purple-lighter)); --color-gradient-4: var(--color-canvas); +} - @media (prefers-color-scheme: dark) { +/* Dark mode - explicit theme choice overrides system preference */ +html[data-theme="dark"] { + --lch-canvas: 20% 0.0195 232.58; + --lch-ink-inverted: var(--lch-black); + + --lch-ink-darkest: 96.02% 0.0034 260; + --lch-ink-darker: 86% 0.0061 260; + --lch-ink-dark: 73.97% 0.009 260; + --lch-ink-medium: 62% 0.0122 260; + --lch-ink-light: 40% 0.0148 260; + --lch-ink-lighter: 30% 0.0178 260; + --lch-ink-lightest: 25% 0.0204 260; + + --lch-uncolor-darkest: 96.09% 0.0076 100; + --lch-uncolor-darker: 86% 0.021 90; + --lch-uncolor-dark: 73.93% 0.041 80; + --lch-uncolor-medium: 62% 0.0552 70; + --lch-uncolor-light: 40% 0.0387 60; + --lch-uncolor-lighter: 30% 0.012 50; + --lch-uncolor-lightest: 25% 0.0017 40; + + --lch-red-darkest: 95.85% 0.0218 46; + --lch-red-darker: 86% 0.086 44; + --lch-red-dark: 73.95% 0.139 42; + --lch-red-medium: 62% 0.154 40; + --lch-red-light: 40% 0.088 38; + --lch-red-lighter: 30% 0.032 36; + --lch-red-lightest: 25% 0.011 34; + + --lch-yellow-darkest: 96% 0.056 100; + --lch-yellow-darker: 86% 0.103 90; + --lch-yellow-dark: 74.06% 0.136 80; + --lch-yellow-medium: 62.1% 0.146 70; + --lch-yellow-light: 40% 0.0736 60; + --lch-yellow-lighter: 30% 0.026 50; + --lch-yellow-lightest: 25% 0.01 40; + + --lch-lime-darkest: 96.04% 0.066 115; + --lch-lime-darker: 86% 0.098 114; + --lch-lime-dark: 73.97% 0.121 113; + --lch-lime-medium: 62% 0.128 112; + --lch-lime-light: 40% 0.0637 111; + --lch-lime-lighter: 30% 0.024 110; + --lch-lime-lightest: 25% 0.012 109; + + --lch-green-darkest: 96.12% 0.035 143; + --lch-green-darker: 86% 0.082 144; + --lch-green-dark: 73.99% 0.117 145; + --lch-green-medium: 62% 0.1261 146; + --lch-green-light: 40% 0.065 147; + --lch-green-lighter: 30% 0.03 148; + --lch-green-lightest: 25% 0.018 149; + + --lch-aqua-darkest: 96.15% 0.0244 202; + --lch-aqua-darker: 86% 0.06 204; + --lch-aqua-dark: 73.92% 0.095 206; + --lch-aqua-medium: 62% 0.106 208; + --lch-aqua-light: 40% 0.0594 210; + --lch-aqua-lighter: 30% 0.028 212; + --lch-aqua-lightest: 25% 0.017 214; + + --lch-blue-darkest: 95.93% 0.0217 252; + --lch-blue-darker: 86% 0.068 254; + --lch-blue-dark: 74% 0.1293 256; + --lch-blue-medium: 62% 0.159 258; + --lch-blue-light: 40% 0.094 260; + --lch-blue-lighter: 30% 0.0452 262; + --lch-blue-lightest: 25% 0.0318 264; + + --lch-violet-darkest: 95.97% 0.019 280; + --lch-violet-darker: 86% 0.068 282; + --lch-violet-dark: 74.08% 0.142 284; + --lch-violet-medium: 62% 0.184 286; + --lch-violet-light: 40% 0.108 288; + --lch-violet-lighter: 30% 0.048 290; + --lch-violet-lightest: 25% 0.025 292; + + --lch-purple-darkest: 95.99% 0.0217 302; + --lch-purple-darker: 86% 0.068 304; + --lch-purple-dark: 73.98% 0.141 306; + --lch-purple-medium: 62% 0.177 308; + --lch-purple-light: 40% 0.099 310; + --lch-purple-lighter: 30% 0.04 312; + --lch-purple-lightest: 25% 0.017 314; + + --lch-pink-darkest: 95.84% 0.0308 336; + --lch-pink-darker: 86% 0.074 338; + --lch-pink-dark: 74.04% 0.1294 340; + --lch-pink-medium: 62% 0.166 342; + --lch-pink-light: 40% 0.085 344; + --lch-pink-lighter: 30% 0.03 346; + --lch-pink-lightest: 25% 0.011 348; + + --color-terminal-bg: var(--color-canvas); + --color-terminal-text-light: oklch(var(--lch-green-dark)); + --color-golden: oklch(var(--lch-blue-medium)); + --color-highlight: oklch(var(--lch-blue-lighter)); + + --shadow: 0 0 0 1px oklch(var(--lch-black) / 0.42), + 0 0.2em 1.6em -0.8em oklch(var(--lch-black) / 0.6), + 0 0.4em 2.4em -1em oklch(var(--lch-black) / 0.7), + 0 0.4em 0.8em -1.2em oklch(var(--lch-black) / 0.8), + 0 0.8em 1.2em -1.6em oklch(var(--lch-black) / 0.9), + 0 1.2em 1.6em -2em oklch(var(--lch-black) / 1); +} + +/* Fallback to system preference when no explicit theme is set */ +@media (prefers-color-scheme: dark) { + html:not([data-theme]) { --lch-canvas: 20% 0.0195 232.58; --lch-ink-inverted: var(--lch-black); diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js new file mode 100644 index 000000000..c5476c06c --- /dev/null +++ b/app/javascript/controllers/theme_controller.js @@ -0,0 +1,48 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["sunIcon", "moonIcon"] + + connect() { + this.applyStoredTheme() + this.updateIcon() + } + + toggle() { + const currentTheme = document.documentElement.getAttribute("data-theme") + // If no theme is set, check system preference or default to light + let newTheme + if (!currentTheme) { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches + newTheme = prefersDark ? "light" : "dark" + } else { + newTheme = currentTheme === "dark" ? "light" : "dark" + } + this.setTheme(newTheme) + this.updateIcon() + } + + setTheme(theme) { + document.documentElement.setAttribute("data-theme", theme) + localStorage.setItem("theme", theme) + } + + applyStoredTheme() { + const storedTheme = localStorage.getItem("theme") + if (storedTheme) { + document.documentElement.setAttribute("data-theme", storedTheme) + } + } + + updateIcon() { + const currentTheme = document.documentElement.getAttribute("data-theme") + const isDark = currentTheme === "dark" || + (!currentTheme && window.matchMedia("(prefers-color-scheme: dark)").matches) + + // Show moon icon in light mode, sun icon in dark mode + if (this.hasSunIconTarget && this.hasMoonIconTarget) { + this.sunIconTarget.style.display = isDark ? "block" : "none" + this.moonIconTarget.style.display = isDark ? "none" : "block" + } + } +} diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d2af3e959..a71c3fc49 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -2,11 +2,27 @@ <%= render "layouts/shared/head" %> - + <%= render "layouts/shared/flash" %> From 2864e32efa3f9a192728d42b636259c6cdb6ccc4 Mon Sep 17 00:00:00 2001 From: Bruno Canepa <8711973+bruncanepa@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:01:14 -0300 Subject: [PATCH 02/17] Move theme button into the user profile page --- app/views/layouts/application.html.erb | 16 ---------------- app/views/users/_theme.html.erb | 16 ++++++++++++++++ app/views/users/show.html.erb | 4 ++++ 3 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 app/views/users/_theme.html.erb diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index a71c3fc49..6aef1f1bc 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,22 +7,6 @@ Skip to main content <%= render "my/menu" if Current.user %> <%= yield :header %> - <%= render "layouts/shared/flash" %> diff --git a/app/views/users/_theme.html.erb b/app/views/users/_theme.html.erb new file mode 100644 index 000000000..6ba153326 --- /dev/null +++ b/app/views/users/_theme.html.erb @@ -0,0 +1,16 @@ + diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index eec6444a0..cb95a38fd 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,6 +1,10 @@ <% @page_title = @user.name %> <% me_or_you = Current.user == @user ? "me" : @user.first_name %> +<% if Current.user == @user %> + <%= render "users/theme" %> +<% end %> +
From 6ae9486b4d0ba8a048e297287cf1d372b85cc61b Mon Sep 17 00:00:00 2001 From: Bruno Canepa <8711973+bruncanepa@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:29:49 -0300 Subject: [PATCH 03/17] add 3 toggle botons --- .../controllers/theme_controller.js | 67 ++++++++++++------- app/views/users/_theme.html.erb | 52 +++++++++----- app/views/users/show.html.erb | 6 +- 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index c5476c06c..150864622 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -1,48 +1,63 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { - static targets = ["sunIcon", "moonIcon"] + static targets = ["lightBtn", "darkBtn", "autoBtn"] connect() { this.applyStoredTheme() - this.updateIcon() + this.updateButtons() } - toggle() { - const currentTheme = document.documentElement.getAttribute("data-theme") - // If no theme is set, check system preference or default to light - let newTheme - if (!currentTheme) { - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches - newTheme = prefersDark ? "light" : "dark" - } else { - newTheme = currentTheme === "dark" ? "light" : "dark" - } - this.setTheme(newTheme) - this.updateIcon() + setLight() { + this.setTheme("light") + document.documentElement.setAttribute("data-theme", "light") + this.updateButtons() + } + + setDark() { + this.setTheme("dark") + document.documentElement.setAttribute("data-theme", "dark") + this.updateButtons() + } + + setAuto() { + this.setTheme("auto") + document.documentElement.removeAttribute("data-theme") + this.updateButtons() } setTheme(theme) { - document.documentElement.setAttribute("data-theme", theme) localStorage.setItem("theme", theme) } applyStoredTheme() { - const storedTheme = localStorage.getItem("theme") - if (storedTheme) { - document.documentElement.setAttribute("data-theme", storedTheme) + const storedTheme = localStorage.getItem("theme") || "auto" + + if (storedTheme === "light") { + document.documentElement.setAttribute("data-theme", "light") + } else if (storedTheme === "dark") { + document.documentElement.setAttribute("data-theme", "dark") + } else { + // auto - don't set data-theme, let CSS media query handle it + document.documentElement.removeAttribute("data-theme") } } - updateIcon() { - const currentTheme = document.documentElement.getAttribute("data-theme") - const isDark = currentTheme === "dark" || - (!currentTheme && window.matchMedia("(prefers-color-scheme: dark)").matches) + updateButtons() { + const storedTheme = localStorage.getItem("theme") || "auto" - // Show moon icon in light mode, sun icon in dark mode - if (this.hasSunIconTarget && this.hasMoonIconTarget) { - this.sunIconTarget.style.display = isDark ? "block" : "none" - this.moonIconTarget.style.display = isDark ? "none" : "block" + // Reset all buttons + if (this.hasLightBtnTarget) this.lightBtnTarget.removeAttribute("aria-selected") + if (this.hasDarkBtnTarget) this.darkBtnTarget.removeAttribute("aria-selected") + if (this.hasAutoBtnTarget) this.autoBtnTarget.removeAttribute("aria-selected") + + // Highlight active button + if (storedTheme === "light" && this.hasLightBtnTarget) { + this.lightBtnTarget.setAttribute("aria-selected", "true") + } else if (storedTheme === "dark" && this.hasDarkBtnTarget) { + this.darkBtnTarget.setAttribute("aria-selected", "true") + } else if (this.hasAutoBtnTarget) { + this.autoBtnTarget.setAttribute("aria-selected", "true") } } } diff --git a/app/views/users/_theme.html.erb b/app/views/users/_theme.html.erb index 6ba153326..86cf694e2 100644 --- a/app/views/users/_theme.html.erb +++ b/app/views/users/_theme.html.erb @@ -1,16 +1,36 @@ - +
+ Appearance + + +
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index cb95a38fd..e59e9cad8 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,10 +1,6 @@ <% @page_title = @user.name %> <% me_or_you = Current.user == @user ? "me" : @user.first_name %> -<% if Current.user == @user %> - <%= render "users/theme" %> -<% end %> -
@@ -43,6 +39,8 @@
<%= render "users/transfer", user: @user %> + <%= render "users/theme" %> +
<%= button_to session_url(script_name: nil), method: :delete, class: "btn btn--plain txt-link txt-small", data: { turbo: false } do %> Sign out From f419ba5a13b2cdbd83ef433aa10241fe937f6e11 Mon Sep 17 00:00:00 2001 From: Bruno Canepa <8711973+bruncanepa@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:07:06 -0300 Subject: [PATCH 04/17] refactor from PR feedback: update theme controller to use private methods for theme management --- .../controllers/theme_controller.js | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index 150864622..26ab67817 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -4,34 +4,38 @@ export default class extends Controller { static targets = ["lightBtn", "darkBtn", "autoBtn"] connect() { - this.applyStoredTheme() - this.updateButtons() + this.#applyStoredTheme() + this.#updateButtons() } setLight() { - this.setTheme("light") + this.#setTheme("light") document.documentElement.setAttribute("data-theme", "light") - this.updateButtons() + this.#updateButtons() } setDark() { - this.setTheme("dark") + this.#setTheme("dark") document.documentElement.setAttribute("data-theme", "dark") - this.updateButtons() + this.#updateButtons() } setAuto() { - this.setTheme("auto") + this.#setTheme("auto") document.documentElement.removeAttribute("data-theme") - this.updateButtons() + this.#updateButtons() } - setTheme(theme) { + get #storedTheme() { + return localStorage.getItem("theme") || "auto" + } + + #setTheme(theme) { localStorage.setItem("theme", theme) } - applyStoredTheme() { - const storedTheme = localStorage.getItem("theme") || "auto" + #applyStoredTheme() { + const storedTheme = this.#storedTheme if (storedTheme === "light") { document.documentElement.setAttribute("data-theme", "light") @@ -43,8 +47,8 @@ export default class extends Controller { } } - updateButtons() { - const storedTheme = localStorage.getItem("theme") || "auto" + #updateButtons() { + const storedTheme = this.#storedTheme // Reset all buttons if (this.hasLightBtnTarget) this.lightBtnTarget.removeAttribute("aria-selected") From 5175353979aa6c7c82759e10f292bfbc7b5086be Mon Sep 17 00:00:00 2001 From: Bruno Canepa <8711973+bruncanepa@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:40:27 -0300 Subject: [PATCH 05/17] Split svg into files and add a css file --- app/assets/images/monitor.svg | 1 + app/assets/images/moon.svg | 1 + app/assets/images/sun.svg | 1 + app/assets/stylesheets/icons.css | 3 + app/assets/stylesheets/theme-switcher.css | 67 +++++++++++++++++++++++ app/views/users/_theme.html.erb | 30 +++------- 6 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 app/assets/images/monitor.svg create mode 100644 app/assets/images/moon.svg create mode 100644 app/assets/images/sun.svg create mode 100644 app/assets/stylesheets/theme-switcher.css diff --git a/app/assets/images/monitor.svg b/app/assets/images/monitor.svg new file mode 100644 index 000000000..a1140a2a8 --- /dev/null +++ b/app/assets/images/monitor.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/moon.svg b/app/assets/images/moon.svg new file mode 100644 index 000000000..8db6c1b09 --- /dev/null +++ b/app/assets/images/moon.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/sun.svg b/app/assets/images/sun.svg new file mode 100644 index 000000000..6dd6e54c8 --- /dev/null +++ b/app/assets/images/sun.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index 8e9f19e89..2e269b9d0 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -72,6 +72,8 @@ .icon--menu-dots-horizontal { --svg: url("menu-dots-horizontal.svg "); } .icon--menu-dots-vertical { --svg: url("menu-dots-vertical.svg "); } .icon--minus { --svg: url("minus.svg "); } + .icon--monitor { --svg: url("monitor.svg "); } + .icon--moon { --svg: url("moon.svg "); } .icon--move { --svg: url("move.svg "); } .icon--notification-bell-access-only { --svg: url("bell.svg "); } .icon--notification-bell-watching { --svg: url("bell-off.svg "); } @@ -96,6 +98,7 @@ .icon--settings { --svg: url("settings.svg "); } .icon--share { --svg: url("share.svg "); } .icon--sliders { --svg: url("sliders.svg "); } + .icon--sun { --svg: url("sun.svg "); } .icon--switch { --svg: url("switch.svg "); } .icon--tag { --svg: url("tag.svg "); } .icon--tag-outline { --svg: url("tag-outline.svg "); } diff --git a/app/assets/stylesheets/theme-switcher.css b/app/assets/stylesheets/theme-switcher.css new file mode 100644 index 000000000..1e35ca990 --- /dev/null +++ b/app/assets/stylesheets/theme-switcher.css @@ -0,0 +1,67 @@ +@layer components { + .theme-switcher { + --gap: 8px; + + align-items: center; + display: flex; + flex-wrap: wrap; + gap: var(--gap); + inline-size: 100%; + justify-content: center; + margin: 1rem auto; + max-inline-size: 100%; + } + + .theme-switcher__btn { + --btn-border-radius: 0.4em; + + align-items: center; + aspect-ratio: 5/3; + background-color: var(--color-ink-lightest); + border: none; + border-radius: 0.5em; + color: inherit; + cursor: pointer; + display: flex; + flex-basis: calc((100% - var(--gap) * 2) / 3); + flex-direction: column; + font-size: var(--text-small); + line-height: 1; + justify-content: center; + overflow: hidden; + padding: 0.5em; + position: relative; + row-gap: 0.3lh; + text-align: center; + + .icon { + --icon-size: 2em; + } + + span { + display: block; + text-wrap: nowrap; + } + + @media (max-width: 639px) { + font-size: var(--text-x-small); + font-weight: 500; + } + + @media (any-hover: hover) { + &:hover { + background-color: var(--color-ink-lighter); + } + } + + &[aria-selected="true"] { + background-color: var(--color-selected); + + @media (any-hover: hover) { + &:hover { + background-color: var(--color-selected); + } + } + } + } +} diff --git a/app/views/users/_theme.html.erb b/app/views/users/_theme.html.erb index 86cf694e2..ce4e91429 100644 --- a/app/views/users/_theme.html.erb +++ b/app/views/users/_theme.html.erb @@ -1,35 +1,19 @@
Appearance - From 6e19e17ca475d4653d26f03ec875193e9c5bbf48 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 8 Dec 2025 17:46:10 -0600 Subject: [PATCH 06/17] Need to account for theme setting when using `prefers-color-scheme: dark` --- app/assets/stylesheets/bar.css | 8 +++++++- app/assets/stylesheets/base.css | 8 +++++++- app/assets/stylesheets/buttons.css | 8 +++++++- app/assets/stylesheets/card-columns.css | 8 +++++++- app/assets/stylesheets/cards.css | 8 +++++++- app/assets/stylesheets/circled-text.css | 8 +++++++- app/assets/stylesheets/inputs.css | 8 +++++++- app/assets/stylesheets/lexxy.css | 9 ++++++++- app/assets/stylesheets/markdown.css | 9 ++++++++- app/assets/stylesheets/nav.css | 8 +++++++- app/assets/stylesheets/reactions.css | 9 ++++++++- app/assets/stylesheets/syntax.css | 18 +++++++++++++++++- app/assets/stylesheets/trays.css | 9 ++++++++- 13 files changed, 105 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/bar.css b/app/assets/stylesheets/bar.css index 86183ccb0..e13badb7b 100644 --- a/app/assets/stylesheets/bar.css +++ b/app/assets/stylesheets/bar.css @@ -19,10 +19,16 @@ view-transition-name: bar; z-index: var(--z-bar); - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { border-block: 1px solid var(--color-ink-lighter); } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + border-block: 1px solid var(--color-ink-lighter); + } + } + &:has(.bar__placeholder[hidden]) { padding-inline: 1ch; } diff --git a/app/assets/stylesheets/base.css b/app/assets/stylesheets/base.css index 31c4180e1..05d2b016e 100644 --- a/app/assets/stylesheets/base.css +++ b/app/assets/stylesheets/base.css @@ -55,9 +55,15 @@ ::selection { background: var(--color-selected); - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { background-color: var(--color-selected-dark); } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + background-color: var(--color-selected-dark); + } + } } :where(ul, ol):where([role="list"]) { diff --git a/app/assets/stylesheets/buttons.css b/app/assets/stylesheets/buttons.css index 274d80ceb..4ee5f199e 100644 --- a/app/assets/stylesheets/buttons.css +++ b/app/assets/stylesheets/buttons.css @@ -27,10 +27,16 @@ } } - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { --btn-hover-brightness: 1.25; } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + --btn-hover-brightness: 1.25; + } + } + &[disabled], &:has([disabled]), [disabled] &[type=submit], diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index ac690ac30..19084f65c 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -179,9 +179,15 @@ outline: var(--focus-ring-size) solid var(--color-selected-dark); outline-offset: var(--focus-ring-offset); - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { outline-color: oklch(var(--lch-blue-medium)); } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + outline-color: oklch(var(--lch-blue-medium)); + } + } } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 9fe9c6601..3fb0a7f4d 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -27,10 +27,16 @@ text-align: start; z-index: 1; - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { box-shadow: 0 0 0 1px var(--color-ink-lighter); } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + box-shadow: 0 0 0 1px var(--color-ink-lighter); + } + } + .popup { inline-size: 260px; } diff --git a/app/assets/stylesheets/circled-text.css b/app/assets/stylesheets/circled-text.css index 9c3e87f48..dcb971d51 100644 --- a/app/assets/stylesheets/circled-text.css +++ b/app/assets/stylesheets/circled-text.css @@ -12,9 +12,15 @@ opacity: 0.5; mix-blend-mode: multiply; - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { mix-blend-mode: screen; } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + mix-blend-mode: screen; + } + } } span::before, diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index 109bfbb26..366983d00 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -113,10 +113,16 @@ background-repeat: no-repeat; text-align: start; - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + } + } + option { background-color: var(--color-canvas); color: var(--color-ink); diff --git a/app/assets/stylesheets/lexxy.css b/app/assets/stylesheets/lexxy.css index e3f8f8cbd..a0100ca1b 100644 --- a/app/assets/stylesheets/lexxy.css +++ b/app/assets/stylesheets/lexxy.css @@ -188,10 +188,17 @@ padding: 0.5em 1.8em 0.5em 1.2em; text-align: start; - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + } + } + + option { background-color: var(--color-canvas); color: var(--color-ink); diff --git a/app/assets/stylesheets/markdown.css b/app/assets/stylesheets/markdown.css index 5dfac832c..f6aef0e39 100644 --- a/app/assets/stylesheets/markdown.css +++ b/app/assets/stylesheets/markdown.css @@ -24,8 +24,15 @@ } } - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { filter: invert(1); } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + filter: invert(1); + } + } + } } diff --git a/app/assets/stylesheets/nav.css b/app/assets/stylesheets/nav.css index bcbe83088..bcc60c85e 100644 --- a/app/assets/stylesheets/nav.css +++ b/app/assets/stylesheets/nav.css @@ -23,9 +23,15 @@ background: var(--color-ink-lighter); } - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { --input-background: var(--color-ink-lighter); } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + --input-background: var(--color-ink-lighter); + } + } } } diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index ed6187bae..797e9a7cb 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -95,9 +95,16 @@ &:not(.expanded):hover { filter: brightness(var(--reaction-hover-brightness)); - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { --reaction-hover-brightness: 1.25; } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + --reaction-hover-brightness: 1.25; + } + } + } } } diff --git a/app/assets/stylesheets/syntax.css b/app/assets/stylesheets/syntax.css index 7ddc2cf42..7da03d67b 100644 --- a/app/assets/stylesheets/syntax.css +++ b/app/assets/stylesheets/syntax.css @@ -14,7 +14,7 @@ --markup-deleted: lch(39.64 68.17 31.45); /* Redefine named color values for dark mode */ - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] { --keyword: lch(67.63 58.99 30.64); --entity: lch(75.13 46.73 306.74); --constant: lch(74.9 39.71 255.53); @@ -28,6 +28,22 @@ --markup-deleted: lch(73.8% 65 29.18); } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) { + --keyword: lch(67.63 58.99 30.64); + --entity: lch(75.13 46.73 306.74); + --constant: lch(74.9 39.71 255.53); + --string: lch(74.9 39.71 255.53); + --variable: lch(76.17 61.1 61.97); + --comment: lch(60.83 6.66 254.46); + --entity-tag: lch(83.65 59.31 141.61); + --markup-heading: lch(47.93 71.67 280.72); + --markup-list: lch(83.84 57.9 85.03); + --markup-inserted: lch(83.65 59.31 141.61); + --markup-deleted: lch(73.8% 65 29.18); + } + } + color: var(--color-ink); .w { diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 46c4b74b7..3cefc1ecc 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -300,9 +300,16 @@ border-radius: 0; } - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { box-shadow: 0 0 0 1px var(--color-ink-lighter); } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + box-shadow: 0 0 0 1px var(--color-ink-lighter); + } + } + } .card__background { From 18c0a7875999be01d4e11883ea6bdab2c37b06a7 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 8 Dec 2025 17:53:40 -0600 Subject: [PATCH 07/17] Prefer component and utility classes when possible --- app/assets/stylesheets/theme-switcher.css | 62 +++++------------------ app/views/users/_theme.html.erb | 20 ++++---- 2 files changed, 22 insertions(+), 60 deletions(-) diff --git a/app/assets/stylesheets/theme-switcher.css b/app/assets/stylesheets/theme-switcher.css index 1e35ca990..2b35308d1 100644 --- a/app/assets/stylesheets/theme-switcher.css +++ b/app/assets/stylesheets/theme-switcher.css @@ -1,58 +1,20 @@ @layer components { .theme-switcher { - --gap: 8px; - - align-items: center; - display: flex; - flex-wrap: wrap; - gap: var(--gap); - inline-size: 100%; - justify-content: center; - margin: 1rem auto; - max-inline-size: 100%; - } - - .theme-switcher__btn { - --btn-border-radius: 0.4em; - - align-items: center; - aspect-ratio: 5/3; - background-color: var(--color-ink-lightest); - border: none; - border-radius: 0.5em; - color: inherit; - cursor: pointer; - display: flex; - flex-basis: calc((100% - var(--gap) * 2) / 3); - flex-direction: column; - font-size: var(--text-small); - line-height: 1; - justify-content: center; - overflow: hidden; - padding: 0.5em; - position: relative; - row-gap: 0.3lh; - text-align: center; - - .icon { - --icon-size: 2em; - } - - span { - display: block; - text-wrap: nowrap; - } - @media (max-width: 639px) { - font-size: var(--text-x-small); - font-weight: 500; + flex-direction: column; } + } + .theme-switcher__btn { + --btn-background: var(--color-ink-lightest); + --btn-border-radius: 0.4em; + --btn-border-size: 0; + --btn-gap: 0.3lh; + --icon-size: 2em; - @media (any-hover: hover) { - &:hover { - background-color: var(--color-ink-lighter); - } - } + flex: 1 0 33%; + flex-direction: column; + position: relative; + white-space: nowrap; &[aria-selected="true"] { background-color: var(--color-selected); diff --git a/app/views/users/_theme.html.erb b/app/views/users/_theme.html.erb index ce4e91429..01d2c479e 100644 --- a/app/views/users/_theme.html.erb +++ b/app/views/users/_theme.html.erb @@ -1,20 +1,20 @@ -
- Appearance +
+ Appearance -
- - -
-
+
From 2b99c09089e23d041d74d3fbbd132ecbde053ad0 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 8 Dec 2025 17:54:06 -0600 Subject: [PATCH 08/17] Rework settings for clarity, separation, and consistency --- app/views/users/_transfer.html.erb | 9 +++++---- app/views/users/show.html.erb | 9 ++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/users/_transfer.html.erb b/app/views/users/_transfer.html.erb index 8befea9ce..489d9765a 100644 --- a/app/views/users/_transfer.html.erb +++ b/app/views/users/_transfer.html.erb @@ -1,10 +1,11 @@ -
+
<% url = session_transfer_url(user.identity.transfer_id, script_name: nil) %>
From 3f312897800f0ebeb6c6f6531c1b14616e3deae9 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 8 Dec 2025 18:03:26 -0600 Subject: [PATCH 09/17] Account for user color theme preference --- app/assets/stylesheets/lexxy.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/lexxy.css b/app/assets/stylesheets/lexxy.css index cc79ca76a..1b00668dd 100644 --- a/app/assets/stylesheets/lexxy.css +++ b/app/assets/stylesheets/lexxy.css @@ -75,10 +75,16 @@ padding-inline: 1.5ch 1.8em; text-align: start; - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); } + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + } + } + option { background-color: var(--color-canvas); color: var(--color-ink); From aae9d38046f935c5f944887d5536713ed5dd751c Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 10 Dec 2025 11:18:27 -0600 Subject: [PATCH 10/17] Make vars for select input BG image --- app/assets/stylesheets/inputs.css | 8 +++++--- app/assets/stylesheets/theme-switcher.css | 11 +++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index 366983d00..252ef2835 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -104,22 +104,24 @@ .input--select { --input-border-radius: 2em; --input-padding: 0.5em 1.8em 0.5em 1.2em; + --caret-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23000'/%3E%3C/svg%3E"); + --caret-icon-dark: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); -webkit-appearance: none; appearance: none; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23000'/%3E%3C/svg%3E"); + background-image: var(--caret-icon); background-size: 0.5em; background-position: center right 0.9em; background-repeat: no-repeat; text-align: start; html[data-theme="dark"] & { - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + --caret-icon: var(--caret-icon-dark); } @media (prefers-color-scheme: dark) { html:not([data-theme]) & { - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + --caret-icon: var(--caret-icon-dark); } } diff --git a/app/assets/stylesheets/theme-switcher.css b/app/assets/stylesheets/theme-switcher.css index 2b35308d1..8f25c31f9 100644 --- a/app/assets/stylesheets/theme-switcher.css +++ b/app/assets/stylesheets/theme-switcher.css @@ -1,9 +1,12 @@ @layer components { .theme-switcher { - @media (max-width: 639px) { + @media (max-width: 479px) { + --row-gap: 1ch; + flex-direction: column; } } + .theme-switcher__btn { --btn-background: var(--color-ink-lightest); --btn-border-radius: 0.4em; @@ -11,7 +14,7 @@ --btn-gap: 0.3lh; --icon-size: 2em; - flex: 1 0 33%; + flex: 1; flex-direction: column; position: relative; white-space: nowrap; @@ -25,5 +28,9 @@ } } } + + @media (max-width: 479px) { + flex-direction: row; + } } } From c90d5cd65ed82cd1b1abf90528a2ed6de626a5bc Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 10 Dec 2025 11:21:00 -0600 Subject: [PATCH 11/17] Make vars for lexxy syntax dropdown icons --- app/assets/stylesheets/lexxy.css | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/lexxy.css b/app/assets/stylesheets/lexxy.css index 1b00668dd..82d5626f6 100644 --- a/app/assets/stylesheets/lexxy.css +++ b/app/assets/stylesheets/lexxy.css @@ -55,10 +55,13 @@ } .lexxy-code-language-picker { + --caret-icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23000'/%3E%3C/svg%3E"); + --caret-icon-dark: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + -webkit-appearance: none; appearance: none; background-color: var(--color-canvas); - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23000'/%3E%3C/svg%3E"); + background-image: var(--caret-icon); background-position: center right 0.9em; background-repeat: no-repeat; background-size: 0.5em; @@ -76,12 +79,12 @@ text-align: start; html[data-theme="dark"] & { - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + --caret-icon: var(--caret-icon-dark); } @media (prefers-color-scheme: dark) { html:not([data-theme]) & { - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); + --caret-icon: var(--caret-icon-dark); } } From dc6696940c1b34c81887e3ffaf189fa5707414da Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Wed, 10 Dec 2025 11:21:31 -0600 Subject: [PATCH 12/17] Don't use abbreviations --- app/javascript/controllers/theme_controller.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index 26ab67817..ce2d4695b 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -1,7 +1,7 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { - static targets = ["lightBtn", "darkBtn", "autoBtn"] + static targets = ["lightButton", "darkButton", "autoButton"] connect() { this.#applyStoredTheme() @@ -51,17 +51,17 @@ export default class extends Controller { const storedTheme = this.#storedTheme // Reset all buttons - if (this.hasLightBtnTarget) this.lightBtnTarget.removeAttribute("aria-selected") - if (this.hasDarkBtnTarget) this.darkBtnTarget.removeAttribute("aria-selected") - if (this.hasAutoBtnTarget) this.autoBtnTarget.removeAttribute("aria-selected") + if (this.hasLightBtnTarget) this.lightButtonTarget.removeAttribute("aria-selected") + if (this.hasDarkBtnTarget) this.darkButtonTarget.removeAttribute("aria-selected") + if (this.hasAutoBtnTarget) this.autoButtonTarget.removeAttribute("aria-selected") // Highlight active button if (storedTheme === "light" && this.hasLightBtnTarget) { - this.lightBtnTarget.setAttribute("aria-selected", "true") + this.lightButtonTarget.setAttribute("aria-selected", "true") } else if (storedTheme === "dark" && this.hasDarkBtnTarget) { - this.darkBtnTarget.setAttribute("aria-selected", "true") + this.darkButtonTarget.setAttribute("aria-selected", "true") } else if (this.hasAutoBtnTarget) { - this.autoBtnTarget.setAttribute("aria-selected", "true") + this.autoButtonTarget.setAttribute("aria-selected", "true") } } } From 2d359fa23067a0eda489f21c16048c697399bd95 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 10 Dec 2025 16:15:45 -0600 Subject: [PATCH 13/17] Clean-up styles and JS controller, use radios --- app/assets/stylesheets/theme-switcher.css | 15 +++----- .../controllers/theme_controller.js | 38 ++++--------------- app/views/users/_theme.html.erb | 15 +++++--- 3 files changed, 23 insertions(+), 45 deletions(-) diff --git a/app/assets/stylesheets/theme-switcher.css b/app/assets/stylesheets/theme-switcher.css index 8f25c31f9..4b3b1bae2 100644 --- a/app/assets/stylesheets/theme-switcher.css +++ b/app/assets/stylesheets/theme-switcher.css @@ -11,22 +11,19 @@ --btn-background: var(--color-ink-lightest); --btn-border-radius: 0.4em; --btn-border-size: 0; - --btn-gap: 0.3lh; + --btn-gap: 0.1lh; + --btn-padding: 1em; --icon-size: 2em; + column-gap: var(--inline-space); flex: 1; flex-direction: column; position: relative; white-space: nowrap; - &[aria-selected="true"] { - background-color: var(--color-selected); - - @media (any-hover: hover) { - &:hover { - background-color: var(--color-selected); - } - } + &:has(input:checked) { + --btn-background: var(--color-selected); + --btn-color: var(--color-ink); } @media (max-width: 479px) { diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index ce2d4695b..9c3c9841f 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -5,25 +5,18 @@ export default class extends Controller { connect() { this.#applyStoredTheme() - this.#updateButtons() } setLight() { this.#setTheme("light") - document.documentElement.setAttribute("data-theme", "light") - this.#updateButtons() } setDark() { this.#setTheme("dark") - document.documentElement.setAttribute("data-theme", "dark") - this.#updateButtons() } setAuto() { this.#setTheme("auto") - document.documentElement.removeAttribute("data-theme") - this.#updateButtons() } get #storedTheme() { @@ -32,36 +25,21 @@ export default class extends Controller { #setTheme(theme) { localStorage.setItem("theme", theme) + + theme === "auto" ? document.documentElement.removeAttribute("data-theme") : document.documentElement.setAttribute("data-theme", theme) + + this.#updateButtons() } #applyStoredTheme() { - const storedTheme = this.#storedTheme - - if (storedTheme === "light") { - document.documentElement.setAttribute("data-theme", "light") - } else if (storedTheme === "dark") { - document.documentElement.setAttribute("data-theme", "dark") - } else { - // auto - don't set data-theme, let CSS media query handle it - document.documentElement.removeAttribute("data-theme") - } + this.#setTheme(this.#storedTheme) } #updateButtons() { const storedTheme = this.#storedTheme - // Reset all buttons - if (this.hasLightBtnTarget) this.lightButtonTarget.removeAttribute("aria-selected") - if (this.hasDarkBtnTarget) this.darkButtonTarget.removeAttribute("aria-selected") - if (this.hasAutoBtnTarget) this.autoButtonTarget.removeAttribute("aria-selected") - - // Highlight active button - if (storedTheme === "light" && this.hasLightBtnTarget) { - this.lightButtonTarget.setAttribute("aria-selected", "true") - } else if (storedTheme === "dark" && this.hasDarkBtnTarget) { - this.darkButtonTarget.setAttribute("aria-selected", "true") - } else if (this.hasAutoBtnTarget) { - this.autoButtonTarget.setAttribute("aria-selected", "true") - } + if (this.lightButtonTarget) { this.lightButtonTarget.checked = (storedTheme === "light") } + if (this.darkButtonTarget) { this.darkButtonTarget.checked = (storedTheme === "dark") } + if (this.autoButtonTarget) { this.autoButtonTarget.checked = (storedTheme !== "light" && storedTheme !== "dark") } } } diff --git a/app/views/users/_theme.html.erb b/app/views/users/_theme.html.erb index 01d2c479e..0f7db9547 100644 --- a/app/views/users/_theme.html.erb +++ b/app/views/users/_theme.html.erb @@ -2,19 +2,22 @@ Appearance
- + + - + + - + +
From 04d3ac50b21c811ef9dc41f079af64480380e164 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 10 Dec 2025 16:46:58 -0600 Subject: [PATCH 14/17] Clean-up post merge and rework layout to accomodate API and Appearance blocks --- app/views/users/_access_tokens.html.erb | 4 ++++ app/views/users/_transfer.html.erb | 7 +------ app/views/users/show.html.erb | 28 ++++++++----------------- 3 files changed, 14 insertions(+), 25 deletions(-) create mode 100644 app/views/users/_access_tokens.html.erb diff --git a/app/views/users/_access_tokens.html.erb b/app/views/users/_access_tokens.html.erb new file mode 100644 index 000000000..285f87149 --- /dev/null +++ b/app/views/users/_access_tokens.html.erb @@ -0,0 +1,4 @@ +
+

Developer

+

Manage <%= link_to "personal access tokens", my_access_tokens_path, class: "btn btn--plain txt-link" %> used with the Fizzy developer API.

+
\ No newline at end of file diff --git a/app/views/users/_transfer.html.erb b/app/views/users/_transfer.html.erb index 533693ea8..13f895ebc 100644 --- a/app/views/users/_transfer.html.erb +++ b/app/views/users/_transfer.html.erb @@ -1,15 +1,10 @@
<% url = session_transfer_url(user.identity.transfer_id, script_name: nil) %> -
-

Link a device

-

Use this link to sign-in on another device

-
-
<% end %> + + <% if Current.user == @user %> + + + <%= button_to session_url(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %> + Sign out of Fizzy on this device + <% end %> + <% end %>
@@ -44,25 +52,7 @@
<%= render "users/theme" %> <%= render "users/transfer", user: @user %> - - - - <%= button_to session_url(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %> - Sign out of Fizzy on this device -
-
-

API

-
- -
- <%= link_to "Personal access tokens", my_access_tokens_path, class: "btn" %> -
-
- -
- <%= button_to session_url(script_name: nil), method: :delete, class: "btn btn--plain txt-link txt-small", data: { turbo: false } do %> - Sign out of Fizzy - <% end %> + <%= render "users/access_tokens" %>
<% end %>
From 21586291facfdcd660ae86f4b2fcb270de4389f4 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 11 Dec 2025 08:59:13 -0600 Subject: [PATCH 15/17] Prefer normal if/else conditional --- app/javascript/controllers/theme_controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index 9c3c9841f..fbc93a44f 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -26,7 +26,11 @@ export default class extends Controller { #setTheme(theme) { localStorage.setItem("theme", theme) - theme === "auto" ? document.documentElement.removeAttribute("data-theme") : document.documentElement.setAttribute("data-theme", theme) + if (theme === "auto") { + document.documentElement.removeAttribute("data-theme") + } else { + document.documentElement.setAttribute("data-theme", theme) + } this.#updateButtons() } From 2a0cf4efe9f56a626cd5bdb5c37865b8fda1c5b8 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 11 Dec 2025 09:31:12 -0600 Subject: [PATCH 16/17] Turn `#theme` into a proper setter --- app/javascript/controllers/theme_controller.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index fbc93a44f..30e296dca 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -8,22 +8,22 @@ export default class extends Controller { } setLight() { - this.#setTheme("light") + this.#theme = "light" } setDark() { - this.#setTheme("dark") + this.#theme = "dark" } setAuto() { - this.#setTheme("auto") + this.#theme = "auto" } get #storedTheme() { return localStorage.getItem("theme") || "auto" } - #setTheme(theme) { + set #theme(theme) { localStorage.setItem("theme", theme) if (theme === "auto") { @@ -36,7 +36,7 @@ export default class extends Controller { } #applyStoredTheme() { - this.#setTheme(this.#storedTheme) + this.#theme = this.#storedTheme } #updateButtons() { From 941ce1f775bd63797a403334f62eeb89121f9a14 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 11 Dec 2025 09:38:34 -0600 Subject: [PATCH 17/17] Need to use `has*Target` to check for optional static targets to prevent console errors --- app/javascript/controllers/theme_controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index 30e296dca..154bf4fc0 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -42,8 +42,8 @@ export default class extends Controller { #updateButtons() { const storedTheme = this.#storedTheme - if (this.lightButtonTarget) { this.lightButtonTarget.checked = (storedTheme === "light") } - if (this.darkButtonTarget) { this.darkButtonTarget.checked = (storedTheme === "dark") } - if (this.autoButtonTarget) { this.autoButtonTarget.checked = (storedTheme !== "light" && storedTheme !== "dark") } + if (this.hasLightButtonTarget) { this.lightButtonTarget.checked = (storedTheme === "light") } + if (this.hasDarkButtonTarget) { this.darkButtonTarget.checked = (storedTheme === "dark") } + if (this.hasAutoButtonTarget) { this.autoButtonTarget.checked = (storedTheme !== "light" && storedTheme !== "dark") } } }