diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 13975ae62d..e5f7a6f739 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -370,111 +370,3 @@ html[data-theme="dark"] { 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); - - --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 .2em 1.6em -0.8em oklch(var(--lch-black) / 0.6), - 0 .4em 2.4em -1em oklch(var(--lch-black) / 0.7), - 0 .4em .8em -1.2em oklch(var(--lch-black) / 0.8), - 0 .8em 1.2em -1.6em oklch(var(--lch-black) / 0.9), - 0 1.2em 1.6em -2em oklch(var(--lch-black) / 1); - } -} diff --git a/app/assets/stylesheets/bar.css b/app/assets/stylesheets/bar.css index e13badb7bc..8a3be18313 100644 --- a/app/assets/stylesheets/bar.css +++ b/app/assets/stylesheets/bar.css @@ -23,12 +23,6 @@ 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 0c76ee544c..9fe53e0633 100644 --- a/app/assets/stylesheets/base.css +++ b/app/assets/stylesheets/base.css @@ -58,12 +58,6 @@ 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 a4e5001393..e76e844071 100644 --- a/app/assets/stylesheets/buttons.css +++ b/app/assets/stylesheets/buttons.css @@ -32,12 +32,6 @@ --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 871e61a59b..5c39f31212 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -249,12 +249,6 @@ 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)); - } - } } &:has(.card) { @@ -851,12 +845,6 @@ html[data-theme="dark"] & { --bubble-opacity: 100%; } - - @media (prefers-color-scheme: dark) { - html:not([data-theme]) & { - --bubble-opacity: 100%; - } - } } /* Card column indicators diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 76f08fdc5d..8ba9707a89 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -31,12 +31,6 @@ 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 dcb971d516..302f74343c 100644 --- a/app/assets/stylesheets/circled-text.css +++ b/app/assets/stylesheets/circled-text.css @@ -15,12 +15,6 @@ 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 827312842b..88f5c65f0d 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -141,12 +141,6 @@ --caret-icon: var(--caret-icon-dark); } - @media (prefers-color-scheme: dark) { - html:not([data-theme]) & { - --caret-icon: var(--caret-icon-dark); - } - } - 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 f6aef0e394..f0c9d1a2a9 100644 --- a/app/assets/stylesheets/markdown.css +++ b/app/assets/stylesheets/markdown.css @@ -28,11 +28,5 @@ filter: invert(1); } - @media (prefers-color-scheme: dark) { - html:not([data-theme]) & { - filter: invert(1); - } - } - } } diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index ca7a058f43..c03123605a 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -20,7 +20,7 @@ } .card { - @media (prefers-color-scheme: dark) { + html[data-theme="dark"] & { box-shadow: 0 0 0 1px var(--color-ink-lighter); } } diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index 5088092734..aeca1d3e2d 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -96,12 +96,6 @@ --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 7da03d67b4..67e637514d 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 */ - html[data-theme="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); @@ -25,23 +25,7 @@ --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); - } - - @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); - } + --markup-deleted: lch(73.8 65 29.18); } color: var(--color-ink); @@ -60,7 +44,11 @@ .gd { color: var(--markup-deleted); - background-color: light-dark(lch(39.64 68.17 31.45 / 0.15), lch(39.64 68.17 31.45 / 0.2)); + background-color: lch(39.64 68.17 31.45 / 0.15); + + html[data-theme="dark"] & { + background-color: lch(39.64 68.17 31.45 / 0.2); + } } .nb, .nc, .no, .nn { @@ -73,7 +61,11 @@ .gi { color: var(--markup-inserted); - background-color: light-dark(lch(49.14 52.75 142.85 / 0.15), lch(83.65 59.31 141.61 / 0.15)); + background-color: lch(49.14 52.75 142.85 / 0.15); + + html[data-theme="dark"] & { + background-color: lch(83.65 59.31 141.61 / 0.15); + } } .kc, .l, .ld, .m, .mb, .mf, .mh, .mi, .il, .mo, .mx, .sb, .bp, .ne, .nl, .py, .nv, .vc, .vg, .vi, .vm, .o, .ow { diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 0e47857abb..df7e10f3c7 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -303,13 +303,6 @@ 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 { diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 89cf586921..4f96e87a2c 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -293,23 +293,11 @@ html[data-theme="dark"] & { display: none; } - - html:not([data-theme]) & { - @media (prefers-color-scheme: dark) { - display: none; - } - } } .hide-on-light-mode { html[data-theme="light"] & { display: none; } - - html:not([data-theme]) & { - @media (prefers-color-scheme: light) { - display: none; - } - } } } diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index cf64df5794..002a1949a5 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -3,10 +3,20 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["lightButton", "darkButton", "autoButton"] + #mediaQuery + #handleSystemThemeChange + connect() { + this.#mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + this.#handleSystemThemeChange = () => this.#applyStoredTheme() + this.#mediaQuery.addEventListener("change", this.#handleSystemThemeChange) this.#applyStoredTheme() } + disconnect() { + this.#mediaQuery.removeEventListener("change", this.#handleSystemThemeChange) + } + setLight() { this.#theme = "light" } @@ -23,22 +33,24 @@ export default class extends Controller { return localStorage.getItem("theme") || "auto" } + get #resolvedTheme() { + const stored = this.#storedTheme + if (stored === "light" || stored === "dark") return stored + return this.#mediaQuery.matches ? "dark" : "light" + } + set #theme(theme) { localStorage.setItem("theme", theme) - const currentTheme = document.documentElement.getAttribute("data-theme") || "auto" - const hasChanged = currentTheme !== theme + const resolved = this.#resolvedTheme + const currentTheme = document.documentElement.dataset.theme + const hasChanged = currentTheme !== resolved const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches const animate = hasChanged && !prefersReducedMotion const applyTheme = () => { - if (theme === "auto") { - document.documentElement.removeAttribute("data-theme") - } else { - document.documentElement.setAttribute("data-theme", theme) - } - + document.documentElement.dataset.theme = resolved this.#updateButtons() } diff --git a/app/views/layouts/_theme_preference.html.erb b/app/views/layouts/_theme_preference.html.erb index 0c33750bd8..1015cefed9 100644 --- a/app/views/layouts/_theme_preference.html.erb +++ b/app/views/layouts/_theme_preference.html.erb @@ -1,6 +1,10 @@ <%= javascript_tag nonce: true do %> const theme = localStorage.getItem("theme") - if (theme && theme !== "auto") { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches + + if (theme === "light" || theme === "dark") { document.documentElement.dataset.theme = theme + } else { + document.documentElement.dataset.theme = prefersDark ? "dark" : "light" } <% end %> diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb index 9f34c17068..b4522f85dc 100644 --- a/app/views/layouts/public.html.erb +++ b/app/views/layouts/public.html.erb @@ -2,7 +2,7 @@ <%= render "layouts/shared/head" %> - +