mirror of
https://github.com/kmvan/x-prober.git
synced 2026-08-13 00:51:39 +08:00
add theme switch
This commit is contained in:
+82
-80
@@ -2,96 +2,98 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="ie=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="webkit" name="renderer">
|
||||
<title>Dev mode</title>
|
||||
<script>"use strict";
|
||||
(() => {
|
||||
window.GLOBAL_CONFIG = {
|
||||
IS_DEV: true,
|
||||
};
|
||||
try {
|
||||
// 1. 尝试读取用户存储的偏好
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
let theme = storedTheme; // 如果有存储,直接用
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="ie=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="webkit" name="renderer">
|
||||
<title>Dev mode</title>
|
||||
<script>"use strict";
|
||||
(() => {
|
||||
window.GLOBAL_CONFIG = {
|
||||
IS_DEV: true,
|
||||
THEME: "",
|
||||
};
|
||||
try {
|
||||
// 1. 尝试读取用户存储的偏好
|
||||
const storedTheme = localStorage.getItem("x-theme");
|
||||
let theme = storedTheme; // 如果有存储,直接用
|
||||
|
||||
// 2. 如果没有存储,则检测系统偏好
|
||||
if (!storedTheme) {
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
theme = prefersDark ? "dark" : "light";
|
||||
}
|
||||
|
||||
// 3. 将主题应用到根元素
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
|
||||
// 4. (可选)如果想让后续 JS 也能读到,可以同步存入 localStorage,但建议让用户主动选择时才存,避免覆盖“未选择”状态
|
||||
// 如果不想存储系统偏好,则不执行 localStorage.setItem
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="/main.tsx" type="module"></script>
|
||||
<style>
|
||||
:root {
|
||||
--x-init-fg: hsl(0 0% 10%);
|
||||
--x-init-body-fg: hsl(0 0% 10%);
|
||||
--x-init-body-bg: hsl(0 0% 90%);
|
||||
--x-init-loading-bg: hsl(0 0% 90%);
|
||||
--x-init-loading-fg: hsl(0 0% 10%);
|
||||
// 2. 如果没有存储,则检测系统偏好
|
||||
if (!storedTheme) {
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
theme = prefersDark ? "dark" : "light";
|
||||
window.THEME = theme;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--x-init-fg: hsl(0 0% 90%);
|
||||
--x-init-body-fg: hsl(0 0% 90%);
|
||||
--x-init-body-bg: hsl(0 0% 0%);
|
||||
--x-init-loading-bg: hsl(0 0% 0%);
|
||||
--x-init-loading-fg: hsl(0 0% 90%);
|
||||
}
|
||||
// 3. 将主题应用到根元素
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
// 4. (可选)如果想让后续 JS 也能读到,可以同步存入 localStorage,但建议让用户主动选择时才存,避免覆盖“未选择”状态
|
||||
// 如果不想存储系统偏好,则不执行 localStorage.setItem
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="/main.tsx" type="module"></script>
|
||||
<style>
|
||||
:root {
|
||||
--x-init-fg: hsl(0 0% 10%);
|
||||
--x-init-body-fg: hsl(0 0% 10%);
|
||||
--x-init-body-bg: hsl(0 0% 90%);
|
||||
--x-init-loading-bg: hsl(0 0% 90%);
|
||||
--x-init-loading-fg: hsl(0 0% 10%);
|
||||
}
|
||||
|
||||
body {
|
||||
gap: var(--x-init-gutter);
|
||||
margin: 0;
|
||||
background: var(--x-init-body-bg);
|
||||
padding: 0;
|
||||
color: var(--x-init-body-fg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--x-init-fg: hsl(0 0% 90%);
|
||||
--x-init-body-fg: hsl(0 0% 90%);
|
||||
--x-init-body-bg: hsl(0 0% 0%);
|
||||
--x-init-loading-bg: hsl(0 0% 0%);
|
||||
--x-init-loading-fg: hsl(0 0% 90%);
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
height: 100svh;
|
||||
font-family: monospace;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#loading::before {
|
||||
animation: spin 1s linear infinite;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--x-init-loading-bg);
|
||||
border-top-color: var(--x-init-loading-fg);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
}
|
||||
</style>
|
||||
body {
|
||||
gap: var(--x-init-gutter);
|
||||
margin: 0;
|
||||
background: var(--x-init-body-bg);
|
||||
padding: 0;
|
||||
color: var(--x-init-body-fg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
height: 100svh;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#loading::before {
|
||||
animation: spin 1s linear infinite;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--x-init-loading-bg);
|
||||
border-top-color: var(--x-init-loading-fg);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loading">Loading...</div>
|
||||
<div id="loading">Loading...</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
import "@/Components/ColorScheme/components/config.scss";
|
||||
import "@/Components/Theme/components/config.scss";
|
||||
import { type FC, useEffect, useRef, useState } from "react";
|
||||
import { serverFetch } from "@/Components/Fetch/server-fetch.ts";
|
||||
import { Footer } from "@/Components/Footer/components/index.tsx";
|
||||
@@ -11,6 +11,7 @@ import { Modules } from "@/Components/Module/components/index.tsx";
|
||||
import { Nav } from "@/Components/Nav/components/index.tsx";
|
||||
import { usePollStore } from "@/Components/Poll/components/store.ts";
|
||||
import { OK } from "@/Components/Rest/http-status.ts";
|
||||
import { Theme } from "@/Components/Theme/components/index.tsx";
|
||||
import { useToastStore } from "@/Components/Toast/components/store.ts";
|
||||
import { useUpdaterStore } from "@/Components/Updater/components/store.ts";
|
||||
import { useInterval } from "@/Components/Utils/components/use-interval.ts";
|
||||
@@ -23,11 +24,7 @@ export const Bootstrap: FC = () => {
|
||||
const isFetchingRef = useRef(false);
|
||||
const openToast = useToastStore((s) => s.open);
|
||||
const isUpdating = useUpdaterStore((s) => s.isUpdating);
|
||||
|
||||
// 轮询间隔控制
|
||||
const pollDelay = isUpdating ? null : TIMER;
|
||||
|
||||
// 核心数据分发逻辑
|
||||
const fetchPollData = async () => {
|
||||
if (isUpdating || isFetchingRef.current) {
|
||||
return;
|
||||
@@ -52,12 +49,10 @@ export const Bootstrap: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 优化点:进入页面立刻执行一次,避免 useInterval 产生 2 秒的白屏等待时间
|
||||
useEffect(() => {
|
||||
fetchPollData();
|
||||
}, []);
|
||||
|
||||
// 启动后续定时轮询
|
||||
useInterval(async () => {
|
||||
await fetchPollData();
|
||||
}, pollDelay);
|
||||
@@ -69,6 +64,7 @@ export const Bootstrap: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Theme />
|
||||
<Modules />
|
||||
<Footer />
|
||||
<Nav />
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 0 calc(var(--x-gutter) * 2) 0;
|
||||
}
|
||||
.link {
|
||||
position: relative;
|
||||
flex: 0 0 calc(var(--x-gutter) * 2);
|
||||
height: var(--x-gutter);
|
||||
&:first-child {
|
||||
border-top-left-radius: var(--x-radius);
|
||||
border-bottom-left-radius: var(--x-radius);
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: var(--x-radius);
|
||||
border-bottom-right-radius: var(--x-radius);
|
||||
}
|
||||
& + & {
|
||||
margin-left: 1px;
|
||||
}
|
||||
&:hover {
|
||||
transform: scale3d(1.5, 1.5, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// import { observer } from 'mobx-react-lite';
|
||||
// import type { FC } from 'react';
|
||||
// import { ColorSchemeStore } from '../stores';
|
||||
// import { colorSchemes } from '../stores/color-schemes';
|
||||
// import styles from './index.module.scss';
|
||||
// export const ColorScheme: FC = observer(() => (
|
||||
// <div className={styles.main}>
|
||||
// {Object.entries(colorSchemes).map(([schemeId, { name, color }]) => (
|
||||
// <a
|
||||
// className={styles.link}
|
||||
// data-active={schemeId === ColorSchemeStore.schemeId || undefined}
|
||||
// key={schemeId}
|
||||
// onClick={() => ColorSchemeStore.setSchemeId(schemeId)}
|
||||
// style={{ background: color }}
|
||||
// title={name}
|
||||
// />
|
||||
// ))}
|
||||
// </div>
|
||||
// ));
|
||||
@@ -1,5 +0,0 @@
|
||||
class Main {
|
||||
readonly id = 'colorScheme';
|
||||
readonly storageId = 'schemeId';
|
||||
}
|
||||
export const ColorSchemeConstants = new Main();
|
||||
@@ -1,103 +0,0 @@
|
||||
import { darken, lighten, rgba } from 'polished';
|
||||
import { gettext } from '@/Components/Language/index.ts';
|
||||
import type { ColorSchemeProps } from '../typings';const light = '#ccc';
|
||||
const dark = '#000';
|
||||
const topDarkBottomLight = 'linear-gradient(#000, #111)';
|
||||
export const colorSchemeDark: ColorSchemeProps = {
|
||||
name: gettext('Dark'),
|
||||
isDark: true,
|
||||
color: `linear-gradient(${rgba(light, 0.3)}, ${rgba(dark, 0.9)})`,
|
||||
fg: dark,
|
||||
bg: light,
|
||||
'selection.fg': light,
|
||||
'selection.bg': rgba(dark, 0.95),
|
||||
'html.bg': dark,
|
||||
'body.fg': light,
|
||||
'body.bg': dark,
|
||||
'a.fg': light,
|
||||
'app.border': dark,
|
||||
'app.fg': light,
|
||||
'app.bg': '#222',
|
||||
'title.fg': light,
|
||||
'title.bg': dark,
|
||||
'title.boxShadow': `0 1px 0 ${rgba(light, 0.1)}`,
|
||||
'sysLoad.fg': light,
|
||||
'sysLoad.bg': dark,
|
||||
'card.border': rgba('#000', 0.5),
|
||||
'card.fg': light,
|
||||
'card.bg': '#333',
|
||||
'card.hover.bg': `linear-gradient(to right, transparent, ${rgba(
|
||||
'#000',
|
||||
0.5
|
||||
)}, transparent)`,
|
||||
'card.boxShadow': 'inset 0 0 0 1px #000',
|
||||
'card.legend.fg': light,
|
||||
'card.legend.bg': topDarkBottomLight,
|
||||
'card.des.fg': light,
|
||||
'card.des.bg': rgba('#000', 0.3),
|
||||
'card.title.fg': light,
|
||||
'progress.fg': light,
|
||||
'progress.bg': topDarkBottomLight,
|
||||
'progress.value.fg': light,
|
||||
'progress.value.bg': '#0c0',
|
||||
'progress.value.after.bg': `linear-gradient(${[
|
||||
rgba('#fff', 0.2),
|
||||
'transparent',
|
||||
].join(',')})`,
|
||||
'progress.value.before.bg': `linear-gradient(to right, ${[
|
||||
rgba('#fff', 0.1),
|
||||
rgba('#fff', 0.2),
|
||||
rgba('#fff', 0.1),
|
||||
].join(',')})`,
|
||||
'network.stats.upload': lighten(0.2, '#c24b00'),
|
||||
'network.stats.download': lighten(0.2, '#007400'),
|
||||
'network.node.fg': light,
|
||||
'network.node.bg': '#252525',
|
||||
'network.node.border': dark,
|
||||
'network.node.row.bg': `linear-gradient(to right, ${[
|
||||
'transparent',
|
||||
rgba('#000', 0.5),
|
||||
'transparent',
|
||||
].join(',')})`,
|
||||
'ping.button.fg': light,
|
||||
'ping.button.bg': dark,
|
||||
'ping.result.fg': light,
|
||||
'ping.result.bg': dark,
|
||||
'status.success.fg': light,
|
||||
'status.success.bg': `linear-gradient(${darken(0.25, '#00e800')}, ${darken(
|
||||
0.2,
|
||||
'#00e800'
|
||||
)})`,
|
||||
'status.error.fg': light,
|
||||
'status.error.bg': `linear-gradient(${darken(0.45, '#b9b9b9')}, ${darken(
|
||||
0.4,
|
||||
'#b9b9b9'
|
||||
)})`,
|
||||
'search.fg': light,
|
||||
'search.bg': rgba(dark, 0.1),
|
||||
'search.hover.fg': light,
|
||||
'search.hover.bg': rgba(dark, 0.3),
|
||||
'benchmark.ruby.fg': dark,
|
||||
'benchmark.ruby.bg': rgba(dark, 0.1),
|
||||
'footer.fg': light,
|
||||
'footer.bg': dark,
|
||||
'nav.fg': light,
|
||||
'nav.bg': dark,
|
||||
'nav.hover.fg': light,
|
||||
'nav.hover.bg': `linear-gradient(${[
|
||||
lighten(0.15, dark),
|
||||
lighten(0.05, dark),
|
||||
].join(',')})`,
|
||||
'nav.active.fg': light,
|
||||
'nav.active.bg': `linear-gradient(${[
|
||||
lighten(0.3, dark),
|
||||
lighten(0.1, dark),
|
||||
].join(',')})`,
|
||||
'nav.border': rgba(light, 0.1),
|
||||
'starMe.fg': darken(0.1, light),
|
||||
'starMe.bg': dark,
|
||||
'starMe.hover.fg': light,
|
||||
'starMe.hover.bg': dark,
|
||||
'toast.fg': light,
|
||||
'toast.bg': dark,
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import { darken, lighten, rgba } from 'polished';
|
||||
import { gettext } from '@/Components/Language/index.ts';
|
||||
import type { ColorSchemeProps } from '../typings';const light = '#f8f8f8';
|
||||
const dark = '#333';
|
||||
const topDarkBottomLight = `linear-gradient(#282828, ${lighten(0.05, dark)})`;
|
||||
export const colorSchemeDefault: ColorSchemeProps = {
|
||||
name: gettext('Default'),
|
||||
isDark: false,
|
||||
color: `linear-gradient(${rgba(dark, 0.9)}, ${rgba(light, 0.5)})`,
|
||||
fg: dark,
|
||||
bg: light,
|
||||
'selection.fg': light,
|
||||
'selection.bg': rgba(dark, 0.95),
|
||||
'html.bg': dark,
|
||||
'body.fg': dark,
|
||||
'body.bg': dark,
|
||||
'a.fg': dark,
|
||||
'app.border': dark,
|
||||
'app.fg': dark,
|
||||
'app.bg': light,
|
||||
'title.fg': light,
|
||||
'title.bg': dark,
|
||||
'title.boxShadow': '0 1px 0 #000',
|
||||
'sysLoad.fg': light,
|
||||
'sysLoad.bg': dark,
|
||||
'card.border': rgba(dark, 0.1),
|
||||
'card.fg': dark,
|
||||
'card.bg': rgba(dark, 0.03),
|
||||
'card.hover.bg': `linear-gradient(to right, transparent, ${rgba(
|
||||
'#000',
|
||||
0.1
|
||||
)}, transparent)`,
|
||||
'card.boxShadow': [
|
||||
`0 -1px 0 ${rgba(dark, 0.3)}`,
|
||||
'inset 0 1px 0 #fff',
|
||||
`inset 0 -1px 0 ${rgba(dark, 0.3)}`,
|
||||
'0 1px 0 #fff',
|
||||
].join(','),
|
||||
'card.legend.fg': light,
|
||||
'card.legend.bg': topDarkBottomLight,
|
||||
'card.des.fg': dark,
|
||||
'card.des.bg': rgba(dark, 0.1),
|
||||
'card.title.fg': dark,
|
||||
'progress.fg': light,
|
||||
'progress.bg': topDarkBottomLight,
|
||||
'progress.value.fg': light,
|
||||
'progress.value.bg': '#0c0',
|
||||
'progress.value.after.bg': `linear-gradient(${[
|
||||
rgba('#fff', 0.45),
|
||||
'transparent',
|
||||
].join(',')})`,
|
||||
'progress.value.before.bg': `linear-gradient(to right, ${[
|
||||
rgba('#fff', 0.1),
|
||||
rgba('#fff', 0.95),
|
||||
rgba('#fff', 0.1),
|
||||
].join(',')})`,
|
||||
'network.stats.upload': '#c24b00',
|
||||
'network.stats.download': '#007400',
|
||||
'network.node.fg': dark,
|
||||
'network.node.bg': '#373c381a',
|
||||
'network.node.border': '#373c381a',
|
||||
'network.node.row.bg':
|
||||
'linear-gradient(to right, transparent, #ffffffe6, transparent)',
|
||||
'ping.button.fg': light,
|
||||
'ping.button.bg': dark,
|
||||
'ping.result.fg': light,
|
||||
'ping.result.bg': dark,
|
||||
'status.success.fg': light,
|
||||
'status.success.bg': `linear-gradient(${darken(0.15, '#00e800')}, #00e800)`,
|
||||
'status.error.fg': light,
|
||||
'status.error.bg': `linear-gradient(${darken(0.25, '#b9b9b9')}, #b9b9b9)`,
|
||||
'search.fg': dark,
|
||||
'search.bg': rgba(dark, 0.1),
|
||||
'search.hover.fg': light,
|
||||
'search.hover.bg': rgba(dark, 0.3),
|
||||
'benchmark.ruby.fg': dark,
|
||||
'benchmark.ruby.bg': rgba(dark, 0.1),
|
||||
'footer.fg': light,
|
||||
'footer.bg': dark,
|
||||
'nav.fg': light,
|
||||
'nav.bg': dark,
|
||||
'nav.hover.fg': light,
|
||||
'nav.hover.bg': `linear-gradient(${rgba(light, 0.25)}, ${rgba(light, 0.1)})`,
|
||||
'nav.active.fg': dark,
|
||||
'nav.active.bg': `linear-gradient(${light}, ${darken(0.15, light)})`,
|
||||
'nav.border': rgba(light, 0.1),
|
||||
'starMe.fg': darken(0.1, light),
|
||||
'starMe.bg': dark,
|
||||
'starMe.hover.fg': light,
|
||||
'starMe.hover.bg': dark,
|
||||
'toast.fg': light,
|
||||
'toast.bg': dark,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ColorSchemesProps } from '../typings';
|
||||
import { colorSchemeDark } from './color-scheme-dark';
|
||||
import { colorSchemeDefault } from './color-scheme-default';
|
||||
export const colorSchemes: ColorSchemesProps = {
|
||||
default: colorSchemeDefault,
|
||||
dark: colorSchemeDark,
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { configure, makeAutoObservable } from 'mobx';
|
||||
import { ColorSchemeConstants } from '../constants';
|
||||
import { colorSchemes } from './color-schemes';configure({
|
||||
enforceActions: 'observed',
|
||||
});
|
||||
const { storageId } = ColorSchemeConstants;
|
||||
class Main {
|
||||
schemeId: string = this.getStorageSchemeId();
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
setSchemeId = (schemeId: string) => {
|
||||
this.schemeId = schemeId;
|
||||
this.setStorageSchemeId(schemeId);
|
||||
};
|
||||
get scheme() {
|
||||
return colorSchemes?.[this.schemeId] ?? colorSchemes.default;
|
||||
}
|
||||
private getStorageSchemeId(): string {
|
||||
return localStorage.getItem(storageId) || 'default';
|
||||
}
|
||||
private setStorageSchemeId = (schemeId: string) => {
|
||||
localStorage.setItem(storageId, schemeId);
|
||||
};
|
||||
}
|
||||
export const ColorSchemeStore = new Main();
|
||||
@@ -1,75 +0,0 @@
|
||||
export interface ColorSchemeProps {
|
||||
name: string;
|
||||
isDark: boolean;
|
||||
color: string;
|
||||
fg: string;
|
||||
bg: string;
|
||||
'selection.fg': string;
|
||||
'selection.bg': string;
|
||||
'html.bg': string;
|
||||
'body.fg': string;
|
||||
'body.bg': string;
|
||||
'a.fg': string;
|
||||
'app.border': string;
|
||||
'app.fg': string;
|
||||
'app.bg': string;
|
||||
'title.fg': string;
|
||||
'title.bg': string;
|
||||
'title.boxShadow': string;
|
||||
'sysLoad.fg': string;
|
||||
'sysLoad.bg': string;
|
||||
'card.border': string;
|
||||
'card.fg': string;
|
||||
'card.bg': string;
|
||||
'card.hover.bg': string;
|
||||
'card.boxShadow': string;
|
||||
'card.legend.fg': string;
|
||||
'card.legend.bg': string;
|
||||
'card.des.fg': string;
|
||||
'card.des.bg': string;
|
||||
'card.title.fg': string;
|
||||
'progress.fg': string;
|
||||
'progress.bg': string;
|
||||
'progress.value.fg': string;
|
||||
'progress.value.bg': string;
|
||||
'progress.value.after.bg': string;
|
||||
'progress.value.before.bg': string;
|
||||
'network.stats.upload': string;
|
||||
'network.stats.download': string;
|
||||
'network.node.fg': string;
|
||||
'network.node.bg': string;
|
||||
'network.node.border': string;
|
||||
'network.node.row.bg': string;
|
||||
'ping.button.fg': string;
|
||||
'ping.button.bg': string;
|
||||
'ping.result.fg': string;
|
||||
'ping.result.bg': string;
|
||||
'status.success.fg': string;
|
||||
'status.success.bg': string;
|
||||
'status.error.fg': string;
|
||||
'status.error.bg': string;
|
||||
'search.fg': string;
|
||||
'search.bg': string;
|
||||
'search.hover.fg': string;
|
||||
'search.hover.bg': string;
|
||||
'benchmark.ruby.fg': string;
|
||||
'benchmark.ruby.bg': string;
|
||||
'footer.fg': string;
|
||||
'footer.bg': string;
|
||||
'nav.fg': string;
|
||||
'nav.bg': string;
|
||||
'nav.hover.fg': string;
|
||||
'nav.hover.bg': string;
|
||||
'nav.active.fg': string;
|
||||
'nav.active.bg': string;
|
||||
'nav.border': string;
|
||||
'starMe.fg': string;
|
||||
'starMe.bg': string;
|
||||
'starMe.hover.fg': string;
|
||||
'starMe.hover.bg': string;
|
||||
'toast.fg': string;
|
||||
'toast.bg': string;
|
||||
}
|
||||
export interface ColorSchemesProps {
|
||||
[name: string]: ColorSchemeProps;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { gettext } from "@/Components/Language/index.ts";
|
||||
|
||||
export const themes = [
|
||||
{
|
||||
color: { a: 0.9, c: 0, h: 0, l: 30 },
|
||||
id: "dark",
|
||||
name: gettext("LiangYe"),
|
||||
},
|
||||
{
|
||||
color: { a: 0.9, c: 0, h: 0, l: 80 },
|
||||
id: "light",
|
||||
name: gettext("XiGuang"),
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,45 @@
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
background: var(--x-theme-button-bg);
|
||||
padding: 0 var(--x-gutter);
|
||||
color: oklch(0% 0 0 / 0.8);
|
||||
// light
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
&:active {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
transform: scale(1.25);
|
||||
z-index: 1;
|
||||
}
|
||||
&:first-child {
|
||||
border-top-left-radius: 10rem;
|
||||
border-bottom-left-radius: 10rem;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: 10rem;
|
||||
border-bottom-right-radius: 10rem;
|
||||
}
|
||||
// dark
|
||||
&[data-is-dark] {
|
||||
color: oklch(100% 0 0 / 0.8);
|
||||
&:hover {
|
||||
filter: brightness(1.5);
|
||||
}
|
||||
&:active {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
&[data-active] {
|
||||
// box-shadow: 0 0 0 2px oklch(100% 0 0 / 0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { type FC, useCallback, useState } from "react";
|
||||
import {
|
||||
isBrightOklch,
|
||||
toOklchString,
|
||||
} from "@/Components/Utils/components/oklch/index.ts";
|
||||
import { WindowConfig } from "@/Components/WindowConfig/components/index.ts";
|
||||
import { themes } from "./constants.ts";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
export const Theme: FC = () => {
|
||||
const [theme, setTheme] = useState<string>(WindowConfig.THEME);
|
||||
const handleActiveTheme = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { id } = e.currentTarget.dataset;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
setTheme(id);
|
||||
document.documentElement.setAttribute("data-theme", id);
|
||||
localStorage.setItem("x-theme", id);
|
||||
},
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
{themes.map(({ id, name, color }) => (
|
||||
<button
|
||||
className={styles.button}
|
||||
data-active={id === theme || undefined}
|
||||
data-id={id}
|
||||
data-is-dark={!isBrightOklch(color) || undefined}
|
||||
key={id}
|
||||
onClick={handleActiveTheme}
|
||||
style={{
|
||||
"--x-theme-button-bg": toOklchString(color),
|
||||
} as React.CSSProperties}
|
||||
title={name}
|
||||
type="button"
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { OklchObject } from "./types.ts";
|
||||
|
||||
export function toNormalizedOklch(oklch: OklchObject): OklchObject {
|
||||
const { l, c, h, a } = oklch;
|
||||
const normalizedL = l > 1 ? l : l * 100;
|
||||
return {
|
||||
a,
|
||||
c,
|
||||
h,
|
||||
l: normalizedL,
|
||||
};
|
||||
}
|
||||
export function toOklchString(oklch: OklchObject): string {
|
||||
const { l, c, h, a } = toNormalizedOklch(oklch);
|
||||
return `oklch(${l}% ${c} ${h} / ${a})`;
|
||||
}
|
||||
export function isBrightOklch(oklch: OklchObject, threshold = 65): boolean {
|
||||
const { l, a } = toNormalizedOklch(oklch);
|
||||
if (a < 0.3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return l >= threshold;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import z from "zod";
|
||||
|
||||
type Percentage = `${number}%`;
|
||||
type Decimals = `${number}`; // 用于 Chroma (0-0.4) 或 Alpha (0-1)
|
||||
type Hue = `${number}`; // Hue 角度 (0-360)
|
||||
|
||||
export type OklchColor =
|
||||
| `oklch(${Percentage} ${Decimals} ${Hue})`
|
||||
| `oklch(${Percentage} ${Decimals} ${Hue} / ${Percentage})`
|
||||
| `oklch(${Percentage} ${Decimals} ${Hue} / ${Decimals})`;
|
||||
|
||||
export const OklchObjectSchema = z.strictObject({
|
||||
a: z.number().min(0).max(1),
|
||||
c: z.number().min(0).max(0.4),
|
||||
h: z.number().min(0).max(360),
|
||||
l: z.number().min(0).max(100),
|
||||
});
|
||||
|
||||
export type OklchObject = z.infer<typeof OklchObjectSchema>;
|
||||
@@ -6,4 +6,7 @@ export const WindowConfig = {
|
||||
IS_DEV: Boolean(
|
||||
(window as unknown as WindowProps)?.GLOBAL_CONFIG?.IS_DEV ?? false
|
||||
),
|
||||
THEME: String(
|
||||
(window as unknown as WindowProps)?.GLOBAL_CONFIG?.THEME ?? "light"
|
||||
),
|
||||
} as const satisfies WindowConfigProps;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type WindowConfigProps = {
|
||||
IS_DEV: boolean;
|
||||
AUTHORIZATION: string;
|
||||
THEME: string;
|
||||
};
|
||||
export type WindowProps = {
|
||||
GLOBAL_CONFIG: WindowConfigProps;
|
||||
|
||||
Reference in New Issue
Block a user