From 61c1ec728233c339c899f7d37e67da2e95534e5b Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 26 May 2026 11:31:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9A=97=E8=89=B2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=9Aclass=20=E5=8F=98=E4=BD=93=E3=80=81=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=90=8C=E6=AD=A5=E3=80=81FOUC=20=E9=A2=84=E9=98=B2?= =?UTF-8?q?=E3=80=81=E7=B3=BB=E7=BB=9F=E5=81=8F=E5=A5=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置 Tailwind v4 @custom-variant dark 使用 class 模式 - 用 use_context_provider 共享主题状态,修复 AppRouter/ThemeToggle 不同步 - 默认跟随系统偏好(matchMedia),无 localStorage 时自动检测 - 统一暗色标记为 .dark class,移除 data-theme 冗余 - ThemePreload 内联脚本在 DOM 解析前设置 class,消除首屏闪烁 - SVG 内联 + currentColor,图标颜色随主题切换 - Cargo.toml 补充 web-sys MediaQueryList/DomTokenList feature Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.toml | 2 +- input.css | 2 + public/style.css | 61 +++++++++++------------ src/router.rs | 8 +-- src/theme.rs | 123 ++++++++++++++++++++++++++++++++--------------- 5 files changed, 121 insertions(+), 75 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5924072..aee9135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ rand = { version = "0.8", features = ["getrandom"] } getrandom = { version = "0.2", features = ["js"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element"] } +web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList"] } wasm-bindgen = "0.2" [profile.release] diff --git a/input.css b/input.css index f1d8c73..9c0f814 100644 --- a/input.css +++ b/input.css @@ -1 +1,3 @@ @import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); diff --git a/public/style.css b/public/style.css index 18c74f3..f255781 100644 --- a/public/style.css +++ b/public/style.css @@ -280,18 +280,12 @@ .table { display: table; } - .h-6 { - height: calc(var(--spacing) * 6); - } .h-\[60px\] { height: 60px; } .min-h-screen { min-height: 100vh; } - .w-6 { - width: calc(var(--spacing) * 6); - } .w-full { width: 100%; } @@ -307,6 +301,9 @@ .flex-1 { flex: 1; } + .cursor-pointer { + cursor: pointer; + } .flex-col { flex-direction: column; } @@ -644,117 +641,117 @@ } } .dark\:border-\[\#333\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { border-color: #333; } } .dark\:border-gray-600 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { border-color: var(--color-gray-600); } } .dark\:bg-\[\#1d1e20\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: #1d1e20; } } .dark\:bg-\[\#1d1e20\]\/80 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: color-mix(in oklab, #1d1e20 80%, transparent); } } .dark\:bg-\[\#2e2e33\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: #2e2e33; } } .dark\:bg-\[\#333\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: #333; } } .dark\:bg-\[\#dadadb\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: #dadadb; } } .dark\:bg-gray-700 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: var(--color-gray-700); } } .dark\:bg-gray-800 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: var(--color-gray-800); } } .dark\:bg-gray-900 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: var(--color-gray-900); } } .dark\:bg-green-900 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: var(--color-green-900); } } .dark\:bg-red-900 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { background-color: var(--color-red-900); } } .dark\:text-\[\#9b9c9d\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: #9b9c9d; } } .dark\:text-\[\#dadadb\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: #dadadb; } } .dark\:text-blue-400 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-blue-400); } } .dark\:text-gray-300 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-gray-300); } } .dark\:text-gray-400 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-gray-400); } } .dark\:text-gray-900 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-gray-900); } } .dark\:text-green-300 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-green-300); } } .dark\:text-red-300 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-red-300); } } .dark\:text-white { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { color: var(--color-white); } } .dark\:decoration-\[\#dadadb\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { text-decoration-color: #dadadb; } } .dark\:hover\:border-gray-600 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { &:hover { @media (hover: hover) { border-color: var(--color-gray-600); @@ -763,7 +760,7 @@ } } .dark\:hover\:bg-\[\#444\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { &:hover { @media (hover: hover) { background-color: #444; @@ -772,7 +769,7 @@ } } .dark\:hover\:text-\[\#dadadb\] { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { &:hover { @media (hover: hover) { color: #dadadb; @@ -781,7 +778,7 @@ } } .dark\:hover\:text-gray-200 { - @media (prefers-color-scheme: dark) { + &:where(.dark, .dark *) { &:hover { @media (hover: hover) { color: var(--color-gray-200); diff --git a/src/router.rs b/src/router.rs index be1acb3..76f5999 100644 --- a/src/router.rs +++ b/src/router.rs @@ -4,7 +4,7 @@ use crate::pages::admin::AdminPage; use crate::pages::home::HomePage; use crate::pages::login::LoginPage; use crate::pages::register::RegisterPage; -use crate::theme::{Theme, use_theme}; +use crate::theme::{Theme, ThemePreload, use_theme_provider}; #[derive(Clone, Routable, Debug, PartialEq)] pub enum Route { @@ -28,7 +28,7 @@ pub enum Route { #[component] pub fn AppRouter() -> Element { - let theme = use_theme(); + let theme = use_theme_provider(); let theme_class = match theme() { Theme::Dark => "dark", Theme::Light => "", @@ -36,7 +36,8 @@ pub fn AppRouter() -> Element { rsx! { div { - class: theme_class, + class: "{theme_class}", + ThemePreload {} Router:: {} } } @@ -61,4 +62,3 @@ pub fn SearchPage() -> Element { pub fn AboutPage() -> Element { rsx! { "About" } } - diff --git a/src/theme.rs b/src/theme.rs index 5d871b2..4953368 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -10,13 +10,6 @@ pub enum Theme { } impl Theme { - pub fn as_str(&self) -> &'static str { - match self { - Theme::Light => "light", - Theme::Dark => "dark", - } - } - pub fn toggle(&self) -> Self { match self { Theme::Light => Theme::Dark, @@ -25,67 +18,121 @@ impl Theme { } } -pub fn use_theme() -> Signal { - let theme = use_signal(|| { - #[cfg(target_arch = "wasm32")] - { - let storage = web_sys::window() - .and_then(|w| w.local_storage().ok()) - .flatten(); - if let Some(storage) = storage { - if let Ok(Some(value)) = storage.get_item(THEME_KEY) { - if value == "dark" { - return Theme::Dark; - } - } +fn detect_initial_theme() -> Theme { + #[cfg(target_arch = "wasm32")] + { + let window = match web_sys::window() { + Some(w) => w, + None => return Theme::Light, + }; + + if let Ok(Some(storage)) = window.local_storage() { + if let Ok(Some(value)) = storage.get_item(THEME_KEY) { + return if value == "dark" { + Theme::Dark + } else { + Theme::Light + }; } } - Theme::Light - }); + + if let Ok(Some(media)) = window.match_media("(prefers-color-scheme: dark)") { + if media.matches() { + return Theme::Dark; + } + } + } + Theme::Light +} + +pub fn use_theme_provider() -> Signal { + let theme = use_signal(detect_initial_theme); use_effect(move || { - let current = theme(); - let theme_str = current.as_str(); - #[cfg(target_arch = "wasm32")] { + let current = theme(); if let Some(window) = web_sys::window() { if let Some(document) = window.document() { if let Some(html) = document.document_element() { - let _ = html.set_attribute("data-theme", theme_str); + match current { + Theme::Dark => { + let _ = html.class_list().add_1("dark"); + } + Theme::Light => { + let _ = html.class_list().remove_1("dark"); + } + } } } - if let Some(storage) = window.local_storage().ok().flatten() { + if let Ok(Some(storage)) = window.local_storage() { + let theme_str = match current { + Theme::Dark => "dark", + Theme::Light => "light", + }; let _ = storage.set_item(THEME_KEY, theme_str); } } } - - let _ = theme_str; }); + use_context_provider(|| theme); theme } +pub fn use_theme() -> Signal { + use_context::>() +} + +const THEME_PRELOAD_SCRIPT: &str = r#" +(function() { + try { + var theme = localStorage.getItem('yggdrasil-theme'); + if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); + } + } catch (e) {} +})(); +"#; + +#[component] +pub fn ThemePreload() -> Element { + rsx! { + script { + dangerous_inner_html: "{THEME_PRELOAD_SCRIPT}", + } + } +} + #[component] pub fn ThemeToggle() -> Element { let mut theme = use_theme(); rsx! { button { - class: "theme-toggle p-2 rounded-full hover:opacity-80 transition-opacity", + class: "theme-toggle p-2 rounded-full cursor-pointer hover:opacity-80 transition-opacity text-gray-600 dark:text-gray-300", onclick: move |_| theme.set(theme().toggle()), if theme() == Theme::Dark { - img { - src: "/icons/bedtime_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg", - alt: "Dark mode", - class: "w-6 h-6", + svg { + xmlns: "http://www.w3.org/2000/svg", + height: "24px", + view_box: "0 -960 960 960", + width: "24px", + fill: "currentColor", + path { + d: "M484-80q-84 0-157.5-32t-128-86.5Q144-253 112-326.5T80-484q0-146 93-257.5T410-880q-18 99 11 193.5T521-521q71 71 165.5 100T880-410q-26 144-138 237T484-80Zm0-80q88 0 163-44t118-121q-86-8-163-43.5T464-465q-61-61-97-138t-43-163q-77 43-120.5 118.5T160-484q0 135 94.5 229.5T484-160Zm-20-305Z", + } } } else { - img { - src: "/icons/wb_sunny_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg", - alt: "Light mode", - class: "w-6 h-6", + svg { + xmlns: "http://www.w3.org/2000/svg", + height: "24px", + view_box: "0 -960 960 960", + width: "24px", + fill: "currentColor", + path { + d: "M440-800v-120h80v120h-80Zm0 760v-120h80v120h-80Zm360-400v-80h120v80H800Zm-760 0v-80h120v80H40Zm708-252-56-56 70-72 58 58-72 70ZM198-140l-58-58 72-70 56 56-70 72Zm564 0-70-72 56-56 72 70-58 58ZM212-692l-72-70 58-58 70 72-56 56Zm98 382q-70-70-70-170t70-170q70-70 170-70t170 70q70 70 70 170t-70 170q-70 70-170 70t-170-70Zm283.5-56.5Q640-413 640-480t-46.5-113.5Q547-640 480-640t-113.5 46.5Q320-547 320-480t46.5 113.5Q413-320 480-320t113.5-46.5ZM480-480Z", + } } } }