修复暗色模式:class 变体、状态同步、FOUC 预防、系统偏好
- 配置 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) <noreply@anthropic.com>
This commit is contained in:
parent
3079f1a505
commit
61c1ec7282
@ -18,7 +18,7 @@ rand = { version = "0.8", features = ["getrandom"] }
|
|||||||
getrandom = { version = "0.2", features = ["js"] }
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[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"
|
wasm-bindgen = "0.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|||||||
@ -280,18 +280,12 @@
|
|||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
.h-6 {
|
|
||||||
height: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.h-\[60px\] {
|
.h-\[60px\] {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.min-h-screen {
|
.min-h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.w-6 {
|
|
||||||
width: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -307,6 +301,9 @@
|
|||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -644,117 +641,117 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:border-\[\#333\] {
|
.dark\:border-\[\#333\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
border-color: #333;
|
border-color: #333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:border-gray-600 {
|
.dark\:border-gray-600 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
border-color: var(--color-gray-600);
|
border-color: var(--color-gray-600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-\[\#1d1e20\] {
|
.dark\:bg-\[\#1d1e20\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: #1d1e20;
|
background-color: #1d1e20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-\[\#1d1e20\]\/80 {
|
.dark\:bg-\[\#1d1e20\]\/80 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: color-mix(in oklab, #1d1e20 80%, transparent);
|
background-color: color-mix(in oklab, #1d1e20 80%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-\[\#2e2e33\] {
|
.dark\:bg-\[\#2e2e33\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: #2e2e33;
|
background-color: #2e2e33;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-\[\#333\] {
|
.dark\:bg-\[\#333\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-\[\#dadadb\] {
|
.dark\:bg-\[\#dadadb\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: #dadadb;
|
background-color: #dadadb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-gray-700 {
|
.dark\:bg-gray-700 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-gray-700);
|
background-color: var(--color-gray-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-gray-800 {
|
.dark\:bg-gray-800 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-gray-800);
|
background-color: var(--color-gray-800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-gray-900 {
|
.dark\:bg-gray-900 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-gray-900);
|
background-color: var(--color-gray-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-green-900 {
|
.dark\:bg-green-900 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-green-900);
|
background-color: var(--color-green-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:bg-red-900 {
|
.dark\:bg-red-900 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
background-color: var(--color-red-900);
|
background-color: var(--color-red-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-\[\#9b9c9d\] {
|
.dark\:text-\[\#9b9c9d\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: #9b9c9d;
|
color: #9b9c9d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-\[\#dadadb\] {
|
.dark\:text-\[\#dadadb\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: #dadadb;
|
color: #dadadb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-blue-400 {
|
.dark\:text-blue-400 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-blue-400);
|
color: var(--color-blue-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-gray-300 {
|
.dark\:text-gray-300 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-gray-300);
|
color: var(--color-gray-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-gray-400 {
|
.dark\:text-gray-400 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-gray-400);
|
color: var(--color-gray-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-gray-900 {
|
.dark\:text-gray-900 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-gray-900);
|
color: var(--color-gray-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-green-300 {
|
.dark\:text-green-300 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-green-300);
|
color: var(--color-green-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-red-300 {
|
.dark\:text-red-300 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-red-300);
|
color: var(--color-red-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:text-white {
|
.dark\:text-white {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:decoration-\[\#dadadb\] {
|
.dark\:decoration-\[\#dadadb\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
text-decoration-color: #dadadb;
|
text-decoration-color: #dadadb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:hover\:border-gray-600 {
|
.dark\:hover\:border-gray-600 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
border-color: var(--color-gray-600);
|
border-color: var(--color-gray-600);
|
||||||
@ -763,7 +760,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:hover\:bg-\[\#444\] {
|
.dark\:hover\:bg-\[\#444\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
@ -772,7 +769,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:hover\:text-\[\#dadadb\] {
|
.dark\:hover\:text-\[\#dadadb\] {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
color: #dadadb;
|
color: #dadadb;
|
||||||
@ -781,7 +778,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark\:hover\:text-gray-200 {
|
.dark\:hover\:text-gray-200 {
|
||||||
@media (prefers-color-scheme: dark) {
|
&:where(.dark, .dark *) {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
color: var(--color-gray-200);
|
color: var(--color-gray-200);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use crate::pages::admin::AdminPage;
|
|||||||
use crate::pages::home::HomePage;
|
use crate::pages::home::HomePage;
|
||||||
use crate::pages::login::LoginPage;
|
use crate::pages::login::LoginPage;
|
||||||
use crate::pages::register::RegisterPage;
|
use crate::pages::register::RegisterPage;
|
||||||
use crate::theme::{Theme, use_theme};
|
use crate::theme::{Theme, ThemePreload, use_theme_provider};
|
||||||
|
|
||||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||||
pub enum Route {
|
pub enum Route {
|
||||||
@ -28,7 +28,7 @@ pub enum Route {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AppRouter() -> Element {
|
pub fn AppRouter() -> Element {
|
||||||
let theme = use_theme();
|
let theme = use_theme_provider();
|
||||||
let theme_class = match theme() {
|
let theme_class = match theme() {
|
||||||
Theme::Dark => "dark",
|
Theme::Dark => "dark",
|
||||||
Theme::Light => "",
|
Theme::Light => "",
|
||||||
@ -36,7 +36,8 @@ pub fn AppRouter() -> Element {
|
|||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: theme_class,
|
class: "{theme_class}",
|
||||||
|
ThemePreload {}
|
||||||
Router::<Route> {}
|
Router::<Route> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,4 +62,3 @@ pub fn SearchPage() -> Element {
|
|||||||
pub fn AboutPage() -> Element {
|
pub fn AboutPage() -> Element {
|
||||||
rsx! { "About" }
|
rsx! { "About" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
111
src/theme.rs
111
src/theme.rs
@ -10,13 +10,6 @@ pub enum Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Theme::Light => "light",
|
|
||||||
Theme::Dark => "dark",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle(&self) -> Self {
|
pub fn toggle(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Theme::Light => Theme::Dark,
|
Theme::Light => Theme::Dark,
|
||||||
@ -25,67 +18,121 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_theme() -> Signal<Theme> {
|
fn detect_initial_theme() -> Theme {
|
||||||
let theme = use_signal(|| {
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
let storage = web_sys::window()
|
let window = match web_sys::window() {
|
||||||
.and_then(|w| w.local_storage().ok())
|
Some(w) => w,
|
||||||
.flatten();
|
None => return Theme::Light,
|
||||||
if let Some(storage) = storage {
|
};
|
||||||
|
|
||||||
|
if let Ok(Some(storage)) = window.local_storage() {
|
||||||
if let Ok(Some(value)) = storage.get_item(THEME_KEY) {
|
if let Ok(Some(value)) = storage.get_item(THEME_KEY) {
|
||||||
if value == "dark" {
|
return if value == "dark" {
|
||||||
|
Theme::Dark
|
||||||
|
} else {
|
||||||
|
Theme::Light
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(Some(media)) = window.match_media("(prefers-color-scheme: dark)") {
|
||||||
|
if media.matches() {
|
||||||
return Theme::Dark;
|
return Theme::Dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Theme::Light
|
Theme::Light
|
||||||
});
|
}
|
||||||
|
|
||||||
|
pub fn use_theme_provider() -> Signal<Theme> {
|
||||||
|
let theme = use_signal(detect_initial_theme);
|
||||||
|
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
let current = theme();
|
|
||||||
let theme_str = current.as_str();
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
|
let current = theme();
|
||||||
if let Some(window) = web_sys::window() {
|
if let Some(window) = web_sys::window() {
|
||||||
if let Some(document) = window.document() {
|
if let Some(document) = window.document() {
|
||||||
if let Some(html) = document.document_element() {
|
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 _ = storage.set_item(THEME_KEY, theme_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = theme_str;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
use_context_provider(|| theme);
|
||||||
theme
|
theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn use_theme() -> Signal<Theme> {
|
||||||
|
use_context::<Signal<Theme>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
#[component]
|
||||||
pub fn ThemeToggle() -> Element {
|
pub fn ThemeToggle() -> Element {
|
||||||
let mut theme = use_theme();
|
let mut theme = use_theme();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
button {
|
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()),
|
onclick: move |_| theme.set(theme().toggle()),
|
||||||
if theme() == Theme::Dark {
|
if theme() == Theme::Dark {
|
||||||
img {
|
svg {
|
||||||
src: "/icons/bedtime_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
alt: "Dark mode",
|
height: "24px",
|
||||||
class: "w-6 h-6",
|
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 {
|
} else {
|
||||||
img {
|
svg {
|
||||||
src: "/icons/wb_sunny_24dp_E3E3E3_FILL0_wght400_GRAD0_opsz24.svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
alt: "Light mode",
|
height: "24px",
|
||||||
class: "w-6 h-6",
|
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",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user