@zpress/ui 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/head/css/loader-backdrop.css +1 -0
- package/dist/head/css/loader-dots.css +1 -0
- package/dist/head/css/themes/arcade.css +1 -0
- package/dist/head/css/themes/base.css +1 -0
- package/dist/head/css/themes/midnight.css +1 -0
- package/dist/head/js/color-mode-dark.js +1 -0
- package/dist/head/js/color-mode-light.js +1 -0
- package/dist/head/js/loader-dots.js +1 -0
- package/dist/head/js/vscode-detect.js +1 -0
- package/dist/index.mjs +95 -21
- package/dist/theme/components/nav/layout.tsx +34 -68
- package/dist/theme/components/nav/theme-switcher.tsx +23 -7
- package/dist/theme/components/theme-provider.tsx +81 -7
- package/dist/theme/index.tsx +4 -0
- package/dist/theme/styles/overrides/vscode.css +36 -0
- package/package.json +6 -4
- package/src/theme/components/nav/layout.tsx +34 -68
- package/src/theme/components/nav/theme-switcher.tsx +23 -7
- package/src/theme/components/theme-provider.tsx +81 -7
- package/src/theme/index.tsx +4 -0
- package/src/theme/styles/overrides/vscode.css +36 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
html:before{content:"";position:fixed;inset:0;background-color:var(--zp-c-bg, #fff);z-index:9998;pointer-events:none;opacity:1;transition:opacity .2s ease}html.zp-loader-fade:before{opacity:0}html[data-zp-ready]:before{display:none}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
html:after{content:attr(data-zp-loader-text);position:fixed;top:50%;left:50%;width:8em;margin-left:-4em;margin-top:-.5em;text-align:left;color:var(--zp-c-text-2, #888);font-family:Geist Pixel Square,ui-sans-serif,system-ui,sans-serif;font-size:24px;font-weight:400;letter-spacing:.04em;z-index:9999;pointer-events:none;opacity:1;transition:opacity .2s ease}html.zp-loader-fade:after{opacity:0}html[data-zp-ready]:after{display:none}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--zp-c-brand-1: #00ff88;--zp-c-brand-2: #00cc6a;--zp-c-brand-3: #00aa55;--zp-c-brand-soft: rgba(0, 255, 136, .14);--zp-c-bg: #0d0d1a;--zp-c-bg-alt: #10102a;--zp-c-bg-elv: #141433;--zp-c-bg-soft: #18183d;--zp-c-bg-icon: #2a2a55;--zp-c-text-1: #e0ffe0;--zp-c-text-2: rgba(224, 255, 224, .72);--zp-c-text-3: rgba(224, 255, 224, .48);--zp-c-divider: #1a1a40;--zp-c-border: #252560;--zp-c-gutter: #10102a;--zp-code-block-bg: #10102a;--zp-button-brand-bg: #00aa55;--zp-button-brand-hover-bg: #00cc6a;--zp-button-brand-active-bg: #008844;--zp-button-brand-text: #0d0d1a;--rp-c-brand: #00ff88;--rp-c-brand-light: #66ffbb;--rp-c-brand-lighter: #99ffcc;--rp-c-brand-dark: #00cc6a;--rp-c-brand-darker: #00aa55;--rp-c-brand-tint: rgba(0, 255, 136, .14);--rp-home-background-bg: #0d0d1a;--rp-c-bg: #0d0d1a;--rp-c-bg-soft: #18183d;--rp-c-text-1: #e0ffe0;--rp-c-text-2: rgba(224, 255, 224, .72);--rp-c-text-3: rgba(224, 255, 224, .48);--rp-c-divider: #1a1a40}html,body{background-color:var(--zp-c-bg);color:var(--zp-c-text-1)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--zp-c-brand-1: #a78bfa;--zp-c-brand-2: #8b5cf6;--zp-c-brand-3: #7c3aed;--zp-c-brand-soft: rgba(167, 139, 250, .14);--zp-c-bg: #ffffff;--zp-c-bg-alt: #f9f9f9;--zp-c-bg-elv: #f5f5f5;--zp-c-bg-soft: #f0f0f0;--zp-c-bg-icon: #cccccc;--zp-c-text-1: #1a1a1a;--zp-c-text-2: rgba(26, 26, 26, .72);--zp-c-text-3: rgba(26, 26, 26, .48);--zp-c-divider: #e2e2e2;--zp-c-border: #d0d0d0;--zp-c-gutter: #f5f5f5;--zp-code-block-bg: #f5f5f5;--zp-button-brand-bg: #7c3aed;--zp-button-brand-hover-bg: #8b5cf6;--zp-button-brand-active-bg: #6d28d9;--zp-button-brand-text: #ffffff;--rp-c-brand: #a78bfa;--rp-c-brand-light: #c4b5fd;--rp-c-brand-lighter: #ddd6fe;--rp-c-brand-dark: #8b5cf6;--rp-c-brand-darker: #7c3aed;--rp-c-brand-tint: rgba(167, 139, 250, .14);--rp-home-background-bg: #fff;--rp-c-bg: #ffffff;--rp-c-bg-soft: #f0f0f0;--rp-c-text-1: #1a1a1a;--rp-c-text-2: rgba(26, 26, 26, .72);--rp-c-text-3: rgba(26, 26, 26, .48);--rp-c-divider: #e2e2e2}html,body{background-color:var(--zp-c-bg);color:var(--zp-c-text-1)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--zp-c-brand-1: #60a5fa;--zp-c-brand-2: #3b82f6;--zp-c-brand-3: #2563eb;--zp-c-brand-soft: rgba(96, 165, 250, .14);--zp-c-bg: #0f0f0f;--zp-c-bg-alt: #121212;--zp-c-bg-elv: #161616;--zp-c-bg-soft: #1a1a1a;--zp-c-bg-icon: #2a2a2a;--zp-c-text-1: #f0f0f0;--zp-c-text-2: rgba(240, 240, 240, .72);--zp-c-text-3: rgba(240, 240, 240, .48);--zp-c-divider: #1e1e1e;--zp-c-border: #282828;--zp-c-gutter: #121212;--zp-code-block-bg: #121212;--zp-button-brand-bg: #2563eb;--zp-button-brand-hover-bg: #3b82f6;--zp-button-brand-active-bg: #1d4ed8;--zp-button-brand-text: #f0f0f0;--rp-c-brand: #60a5fa;--rp-c-brand-light: #93c5fd;--rp-c-brand-lighter: #bfdbfe;--rp-c-brand-dark: #3b82f6;--rp-c-brand-darker: #2563eb;--rp-c-brand-tint: rgba(96, 165, 250, .14);--rp-home-background-bg: #0f0f0f;--rp-c-bg: #0f0f0f;--rp-c-bg-soft: #1a1a1a;--rp-c-text-1: #f0f0f0;--rp-c-text-2: rgba(240, 240, 240, .72);--rp-c-text-3: rgba(240, 240, 240, .48);--rp-c-divider: #1e1e1e}html,body{background-color:var(--zp-c-bg);color:var(--zp-c-text-1)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
try{localStorage.setItem("rspress-theme-appearance","dark")}catch{}document.documentElement.classList.add("rp-dark","dark"),document.documentElement.style.colorScheme="dark";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
try{localStorage.setItem("rspress-theme-appearance","light")}catch{}document.documentElement.classList.remove("rp-dark","dark"),document.documentElement.style.colorScheme="light";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){var t=["loading","loading.","loading..","loading..."],e=0;document.documentElement.dataset.zpLoaderText=t[0],window.__zpDotsInterval=setInterval(function(){e=(e+1)%4,document.documentElement.dataset.zpLoaderText=t[e]},300)})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
try{var s=sessionStorage.getItem("zpress-env"),p=new URLSearchParams(location.search).get("env");(s==="vscode"||p==="vscode")&&(document.documentElement.dataset.zpressEnv="vscode",p==="vscode"&&sessionStorage.setItem("zpress-env","vscode"))}catch{}
|
package/dist/index.mjs
CHANGED
|
@@ -2,20 +2,59 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import node_path from "node:path";
|
|
4
4
|
import { isBuiltInTheme, resolveDefaultColorMode } from "@zpress/theme";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
function resolveAsset(relativePath) {
|
|
6
|
+
return node_path.resolve(import.meta.dirname, 'head', relativePath);
|
|
7
|
+
}
|
|
8
|
+
function readAsset(relativePath) {
|
|
9
|
+
const fullPath = resolveAsset(relativePath);
|
|
10
|
+
try {
|
|
11
|
+
const content = readFileSync(fullPath, 'utf8').trim();
|
|
12
|
+
return [
|
|
13
|
+
null,
|
|
14
|
+
content
|
|
15
|
+
];
|
|
16
|
+
} catch {
|
|
17
|
+
const error = {
|
|
18
|
+
_tag: 'AssetError',
|
|
19
|
+
type: 'missing',
|
|
20
|
+
message: `Missing head asset: ${relativePath} — run "pnpm build" in packages/ui first`,
|
|
21
|
+
path: fullPath
|
|
22
|
+
};
|
|
23
|
+
return [
|
|
24
|
+
error,
|
|
25
|
+
null
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function readCss(relativePath) {
|
|
30
|
+
const [error, content] = readAsset(relativePath);
|
|
31
|
+
if (error) {
|
|
32
|
+
process.stderr.write(`[zpress] ${error.message}\n`);
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
function readJs(relativePath) {
|
|
38
|
+
const [error, content] = readAsset(relativePath);
|
|
39
|
+
if (error) {
|
|
40
|
+
process.stderr.write(`[zpress] ${error.message}\n`);
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
const THEME_CSS_MAP = {
|
|
46
|
+
base: readCss('css/themes/base.css'),
|
|
47
|
+
midnight: readCss('css/themes/midnight.css'),
|
|
48
|
+
arcade: readCss('css/themes/arcade.css')
|
|
13
49
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
50
|
+
const BACKDROP_CSS = readCss('css/loader-backdrop.css');
|
|
51
|
+
const DOTS_LOADER_CSS = readCss('css/loader-dots.css');
|
|
52
|
+
const LOADER_CSS = BACKDROP_CSS + DOTS_LOADER_CSS;
|
|
53
|
+
function getThemeCss(themeName) {
|
|
54
|
+
if (!Object.hasOwn(THEME_CSS_MAP, themeName)) return LOADER_CSS;
|
|
55
|
+
const themeColors = THEME_CSS_MAP[themeName];
|
|
56
|
+
if (themeColors) return themeColors + LOADER_CSS;
|
|
57
|
+
return LOADER_CSS;
|
|
19
58
|
}
|
|
20
59
|
function zpressPlugin() {
|
|
21
60
|
const componentsDir = node_path.resolve(import.meta.dirname, 'theme', 'components');
|
|
@@ -65,6 +104,25 @@ function resolveThemeDarkColors(config) {
|
|
|
65
104
|
if (config.theme && config.theme.darkColors) return config.theme.darkColors;
|
|
66
105
|
return {};
|
|
67
106
|
}
|
|
107
|
+
const COLOR_MODE_DARK_JS = readJs('js/color-mode-dark.js');
|
|
108
|
+
const COLOR_MODE_LIGHT_JS = readJs('js/color-mode-light.js');
|
|
109
|
+
const VSCODE_DETECT_JS = readJs('js/vscode-detect.js');
|
|
110
|
+
const LOADER_DOTS_JS = readJs('js/loader-dots.js');
|
|
111
|
+
function buildColorModeJs(colorMode) {
|
|
112
|
+
if ('dark' === colorMode) return COLOR_MODE_DARK_JS;
|
|
113
|
+
if ('light' === colorMode) return COLOR_MODE_LIGHT_JS;
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
function buildHeadScriptBody(options) {
|
|
117
|
+
const colorModeJs = buildColorModeJs(options.colorMode);
|
|
118
|
+
const themeAttrJs = `document.documentElement.dataset.zpTheme=function(){try{var t=localStorage.getItem('zpress-theme');if(t)return t}catch(_){}return ${JSON.stringify(options.themeName)}}();`;
|
|
119
|
+
return [
|
|
120
|
+
colorModeJs,
|
|
121
|
+
themeAttrJs,
|
|
122
|
+
VSCODE_DETECT_JS,
|
|
123
|
+
LOADER_DOTS_JS
|
|
124
|
+
].filter(Boolean).join('');
|
|
125
|
+
}
|
|
68
126
|
function createRspressConfig(options) {
|
|
69
127
|
const { config, paths, logLevel } = options;
|
|
70
128
|
const sidebar = loadGenerated(paths.contentDir, 'sidebar.json', {});
|
|
@@ -76,13 +134,11 @@ function createRspressConfig(options) {
|
|
|
76
134
|
const themeSwitcher = resolveThemeSwitcher(config);
|
|
77
135
|
const themeColors = resolveThemeColors(config);
|
|
78
136
|
const themeDarkColors = resolveThemeDarkColors(config);
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return [];
|
|
85
|
-
})();
|
|
137
|
+
const themeCss = getThemeCss(themeName);
|
|
138
|
+
const headScriptBody = buildHeadScriptBody({
|
|
139
|
+
colorMode,
|
|
140
|
+
themeName
|
|
141
|
+
});
|
|
86
142
|
return {
|
|
87
143
|
root: paths.contentDir,
|
|
88
144
|
outDir: paths.distDir,
|
|
@@ -99,7 +155,6 @@ function createRspressConfig(options) {
|
|
|
99
155
|
plugins: [
|
|
100
156
|
zpressPlugin()
|
|
101
157
|
],
|
|
102
|
-
head: headElements,
|
|
103
158
|
builderConfig: {
|
|
104
159
|
...(()=>{
|
|
105
160
|
if (logLevel) return {
|
|
@@ -107,6 +162,25 @@ function createRspressConfig(options) {
|
|
|
107
162
|
};
|
|
108
163
|
return {};
|
|
109
164
|
})(),
|
|
165
|
+
html: {
|
|
166
|
+
tags: [
|
|
167
|
+
{
|
|
168
|
+
tag: 'style',
|
|
169
|
+
children: themeCss,
|
|
170
|
+
attrs: {
|
|
171
|
+
'data-zpress-theme-css': true
|
|
172
|
+
},
|
|
173
|
+
append: false,
|
|
174
|
+
head: true
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
tag: "script",
|
|
178
|
+
children: `(function(){${headScriptBody}})()`,
|
|
179
|
+
append: false,
|
|
180
|
+
head: true
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
},
|
|
110
184
|
resolve: {
|
|
111
185
|
alias: {
|
|
112
186
|
'@zpress/ui/theme': node_path.resolve(import.meta.dirname, 'theme', 'index.tsx')
|
|
@@ -6,82 +6,48 @@ import { BranchTag } from './branch-tag'
|
|
|
6
6
|
import { ThemeSwitcher } from './theme-switcher'
|
|
7
7
|
import { VscodeTag } from './vscode-tag'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* Center content at 1200px max */
|
|
27
|
-
html[data-zpress-env="vscode"] .rp-doc-layout__doc {
|
|
28
|
-
max-width: 1200px;
|
|
29
|
-
margin: 0 auto;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/* Hide nav items, social links, hamburger */
|
|
33
|
-
html[data-zpress-env="vscode"] .rp-nav-menu__item {
|
|
34
|
-
display: none;
|
|
35
|
-
}
|
|
36
|
-
html[data-zpress-env="vscode"] .rp-social-links {
|
|
37
|
-
display: none;
|
|
38
|
-
}
|
|
39
|
-
html[data-zpress-env="vscode"] .rp-nav-hamburger {
|
|
40
|
-
display: none;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Hide mobile navigation elements */
|
|
44
|
-
html[data-zpress-env="vscode"] .rp-nav-screen {
|
|
45
|
-
display: none;
|
|
46
|
-
}
|
|
47
|
-
html[data-zpress-env="vscode"] .rp-nav-screen-menu {
|
|
48
|
-
display: none;
|
|
49
|
-
}
|
|
50
|
-
html[data-zpress-env="vscode"] .rp-local-nav {
|
|
51
|
-
display: none;
|
|
52
|
-
}
|
|
53
|
-
html[data-zpress-env="vscode"] .rp-appearance {
|
|
54
|
-
display: none;
|
|
55
|
-
}
|
|
56
|
-
html[data-zpress-env="vscode"] .rp-doc-layout__menu {
|
|
57
|
-
display: none;
|
|
9
|
+
/**
|
|
10
|
+
* Detect vscode mode synchronously from sessionStorage and URL params.
|
|
11
|
+
* Returns false during SSR — client initializes correctly via lazy useState.
|
|
12
|
+
*/
|
|
13
|
+
function readVscodeMode(): boolean {
|
|
14
|
+
if (globalThis.window === undefined) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const params = new URLSearchParams(globalThis.location.search)
|
|
19
|
+
return (
|
|
20
|
+
params.get('env') === 'vscode' || globalThis.sessionStorage.getItem('zpress-env') === 'vscode'
|
|
21
|
+
)
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
58
25
|
}
|
|
59
|
-
`
|
|
60
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when the page is loaded in VS Code preview mode.
|
|
29
|
+
*
|
|
30
|
+
* Initializes synchronously from sessionStorage/URL so the VscodeTag
|
|
31
|
+
* renders in the same paint as the rest of the nav. The data-zpress-env
|
|
32
|
+
* attribute and static vscode.css are applied by the inline head script
|
|
33
|
+
* before React mounts, so no dynamic style injection is needed here.
|
|
34
|
+
*/
|
|
61
35
|
function useVscodeMode(): boolean {
|
|
62
|
-
const [active
|
|
36
|
+
const [active] = useState<boolean>(readVscodeMode)
|
|
63
37
|
|
|
38
|
+
// Persist vscode mode across SPA route changes — the inline head script
|
|
39
|
+
// sets sessionStorage on first load via URL param, but client-side
|
|
40
|
+
// navigation may lose the param. This ensures the flag survives.
|
|
64
41
|
useEffect(() => {
|
|
65
|
-
|
|
66
|
-
const isVscode =
|
|
67
|
-
params.get('env') === 'vscode' || globalThis.sessionStorage.getItem('zpress-env') === 'vscode'
|
|
68
|
-
|
|
69
|
-
if (!isVscode) {
|
|
42
|
+
if (!active) {
|
|
70
43
|
return
|
|
71
44
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const style = document.createElement('style')
|
|
78
|
-
style.textContent = VSCODE_OVERRIDES
|
|
79
|
-
document.head.append(style)
|
|
80
|
-
return () => {
|
|
81
|
-
style.remove()
|
|
82
|
-
delete document.documentElement.dataset.zpressEnv
|
|
45
|
+
try {
|
|
46
|
+
sessionStorage.setItem('zpress-env', 'vscode')
|
|
47
|
+
} catch {
|
|
48
|
+
// sessionStorage may be blocked in some environments
|
|
83
49
|
}
|
|
84
|
-
}, [])
|
|
50
|
+
}, [active])
|
|
85
51
|
|
|
86
52
|
return active
|
|
87
53
|
}
|
|
@@ -17,9 +17,23 @@ const THEME_OPTIONS: readonly ThemeOption[] = [
|
|
|
17
17
|
{ name: 'base', label: 'Base', swatch: '#a78bfa', defaultColorMode: 'toggle' },
|
|
18
18
|
{ name: 'midnight', label: 'Midnight', swatch: '#60a5fa', defaultColorMode: 'dark' },
|
|
19
19
|
{ name: 'arcade', label: 'Arcade', swatch: '#00ff88', defaultColorMode: 'dark' },
|
|
20
|
-
{ name: 'arcade-fx', label: 'Arcade FX', swatch: '#ff00ff', defaultColorMode: 'dark' },
|
|
21
20
|
]
|
|
22
21
|
|
|
22
|
+
const VALID_THEME_NAMES = new Set(THEME_OPTIONS.map((t) => t.name))
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a stored theme name, rejecting unknown values.
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeThemeName(raw: string | null): string {
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return 'base'
|
|
30
|
+
}
|
|
31
|
+
if (VALID_THEME_NAMES.has(raw)) {
|
|
32
|
+
return raw
|
|
33
|
+
}
|
|
34
|
+
return 'base'
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* Build the className string for a theme option button.
|
|
25
39
|
*/
|
|
@@ -39,13 +53,15 @@ function applyTheme(theme: ThemeOption): void {
|
|
|
39
53
|
localStorage.setItem('zpress-theme', theme.name)
|
|
40
54
|
|
|
41
55
|
if (theme.defaultColorMode === 'dark') {
|
|
42
|
-
|
|
56
|
+
// 'rp-dark' is Rspress's dark mode class; 'dark' is added for Tailwind compatibility
|
|
57
|
+
html.classList.add('rp-dark', 'dark')
|
|
43
58
|
html.dataset.dark = 'true'
|
|
44
|
-
localStorage.setItem('rspress-theme', 'dark')
|
|
59
|
+
localStorage.setItem('rspress-theme-appearance', 'dark')
|
|
45
60
|
} else if (theme.defaultColorMode === 'light') {
|
|
46
|
-
|
|
61
|
+
// Remove both Rspress and Tailwind dark mode classes
|
|
62
|
+
html.classList.remove('rp-dark', 'dark')
|
|
47
63
|
html.dataset.dark = 'false'
|
|
48
|
-
localStorage.setItem('rspress-theme', 'light')
|
|
64
|
+
localStorage.setItem('rspress-theme-appearance', 'light')
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
|
|
@@ -56,11 +72,11 @@ function applyTheme(theme: ThemeOption): void {
|
|
|
56
72
|
export function ThemeSwitcher(): React.ReactElement | null {
|
|
57
73
|
const [isOpen, setIsOpen] = useState(false)
|
|
58
74
|
const [activeTheme, setActiveTheme] = useState(() => {
|
|
59
|
-
if (
|
|
75
|
+
if (globalThis.window === undefined) {
|
|
60
76
|
return 'base'
|
|
61
77
|
}
|
|
62
78
|
try {
|
|
63
|
-
return localStorage.getItem('zpress-theme')
|
|
79
|
+
return sanitizeThemeName(globalThis.localStorage.getItem('zpress-theme'))
|
|
64
80
|
} catch {
|
|
65
81
|
return 'base'
|
|
66
82
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react'
|
|
2
2
|
import type React from 'react'
|
|
3
3
|
|
|
4
4
|
declare const __ZPRESS_THEME_NAME__: string
|
|
@@ -87,14 +87,75 @@ function parseColors(raw: string): Record<string, string> {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* useLayoutEffect on the client, useEffect on the server (avoids SSR warning).
|
|
92
|
+
* Ensures DOM mutations happen synchronously before browser paint.
|
|
93
|
+
*/
|
|
94
|
+
function getIsomorphicEffect(): typeof useLayoutEffect {
|
|
95
|
+
if (globalThis.window !== undefined) {
|
|
96
|
+
return useLayoutEffect
|
|
97
|
+
}
|
|
98
|
+
return useEffect
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const useIsomorphicLayoutEffect = getIsomorphicEffect()
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Minimum time (ms) the loading overlay stays visible before fading out.
|
|
105
|
+
*/
|
|
106
|
+
const LOADER_MIN_DISPLAY_MS = 150
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Duration (ms) of the CSS fade-out transition. Must match the
|
|
110
|
+
* `transition: opacity` value in loader-backdrop.css / loader-dots.css.
|
|
111
|
+
*/
|
|
112
|
+
const LOADER_FADE_MS = 200
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Dismiss the loading overlay with a two-phase approach:
|
|
116
|
+
* 1. Add `zp-loader-fade` class → CSS transitions opacity to 0
|
|
117
|
+
* 2. After transition completes → set `data-zp-ready` attribute → CSS hard removes (display: none)
|
|
118
|
+
*
|
|
119
|
+
* Also clears the JS-driven dots animation interval set by loader-dots.js.
|
|
120
|
+
*
|
|
121
|
+
* Returns a cleanup function that cancels pending timers.
|
|
122
|
+
*/
|
|
123
|
+
function clearDotsInterval(): void {
|
|
124
|
+
const dotsInterval = (globalThis as Record<string, unknown>).__zpDotsInterval
|
|
125
|
+
if (typeof dotsInterval === 'number') {
|
|
126
|
+
clearInterval(dotsInterval)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dismissLoader(html: HTMLElement): () => void {
|
|
131
|
+
const fadeTimer = setTimeout(() => {
|
|
132
|
+
html.classList.add('zp-loader-fade')
|
|
133
|
+
}, LOADER_MIN_DISPLAY_MS)
|
|
134
|
+
|
|
135
|
+
const removeTimer = setTimeout(() => {
|
|
136
|
+
html.dataset.zpReady = 'true'
|
|
137
|
+
html.classList.remove('zp-loader-fade')
|
|
138
|
+
clearDotsInterval()
|
|
139
|
+
}, LOADER_MIN_DISPLAY_MS + LOADER_FADE_MS)
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
clearTimeout(fadeTimer)
|
|
143
|
+
clearTimeout(removeTimer)
|
|
144
|
+
html.classList.remove('zp-loader-fade')
|
|
145
|
+
clearDotsInterval()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
90
149
|
/**
|
|
91
150
|
* ThemeProvider — global UI component that configures the active theme.
|
|
92
151
|
*
|
|
93
152
|
* Sets `data-zp-theme` attribute, forces color mode, and applies
|
|
94
153
|
* inline CSS custom property overrides from build-time defines.
|
|
154
|
+
* Sets `data-zp-ready` on <html> to dismiss the loading overlay
|
|
155
|
+
* injected by critical CSS.
|
|
95
156
|
*/
|
|
96
157
|
export function ThemeProvider(): React.ReactElement | null {
|
|
97
|
-
|
|
158
|
+
useIsomorphicLayoutEffect(() => {
|
|
98
159
|
const html = document.documentElement
|
|
99
160
|
const themeName = safeGetItem('zpress-theme') || __ZPRESS_THEME_NAME__
|
|
100
161
|
const colorMode = __ZPRESS_COLOR_MODE__
|
|
@@ -108,13 +169,21 @@ export function ThemeProvider(): React.ReactElement | null {
|
|
|
108
169
|
|
|
109
170
|
// 2. Force color mode if not toggle
|
|
110
171
|
if (colorMode === 'dark') {
|
|
111
|
-
html.classList.add('rp-dark')
|
|
172
|
+
html.classList.add('rp-dark', 'dark')
|
|
112
173
|
html.dataset.dark = 'true'
|
|
113
|
-
|
|
174
|
+
try {
|
|
175
|
+
localStorage.setItem('rspress-theme-appearance', 'dark')
|
|
176
|
+
} catch {
|
|
177
|
+
// storage unavailable
|
|
178
|
+
}
|
|
114
179
|
} else if (colorMode === 'light') {
|
|
115
|
-
html.classList.remove('rp-dark')
|
|
180
|
+
html.classList.remove('rp-dark', 'dark')
|
|
116
181
|
html.dataset.dark = 'false'
|
|
117
|
-
|
|
182
|
+
try {
|
|
183
|
+
localStorage.setItem('rspress-theme-appearance', 'light')
|
|
184
|
+
} catch {
|
|
185
|
+
// storage unavailable
|
|
186
|
+
}
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
// 3. Apply base color overrides
|
|
@@ -145,12 +214,17 @@ export function ThemeProvider(): React.ReactElement | null {
|
|
|
145
214
|
|
|
146
215
|
observer.observe(html, { attributes: true, attributeFilter: ['class'] })
|
|
147
216
|
|
|
217
|
+
// 5. Dismiss loading overlay
|
|
218
|
+
const cancelLoader = dismissLoader(html)
|
|
219
|
+
|
|
148
220
|
return () => {
|
|
149
221
|
observer.disconnect()
|
|
222
|
+
cancelLoader()
|
|
150
223
|
}
|
|
151
224
|
}
|
|
152
225
|
|
|
153
|
-
//
|
|
226
|
+
// 5. Dismiss loading overlay (no dark-color observer needed)
|
|
227
|
+
return dismissLoader(html)
|
|
154
228
|
}, [])
|
|
155
229
|
|
|
156
230
|
return null
|
package/dist/theme/index.tsx
CHANGED
|
@@ -16,6 +16,9 @@ import './styles/overrides/rspress.css'
|
|
|
16
16
|
import './styles/themes/base.css'
|
|
17
17
|
import './styles/themes/midnight.css'
|
|
18
18
|
import './styles/themes/arcade.css'
|
|
19
|
+
// arcade-fx.css is intentionally separate from arcade.css:
|
|
20
|
+
// arcade.css = color palette tokens, arcade-fx.css = visual effects
|
|
21
|
+
// (border trace, neon pulse, CRT scanlines, etc.) scoped to [data-zp-theme='arcade']
|
|
19
22
|
import './styles/themes/arcade-fx.css'
|
|
20
23
|
import './styles/overrides/details.css'
|
|
21
24
|
import './styles/overrides/scrollbar.css'
|
|
@@ -23,6 +26,7 @@ import './styles/overrides/sidebar.css'
|
|
|
23
26
|
import './styles/overrides/home.css'
|
|
24
27
|
import './styles/overrides/home-card.css'
|
|
25
28
|
import './styles/overrides/section-card.css'
|
|
29
|
+
import './styles/overrides/vscode.css'
|
|
26
30
|
|
|
27
31
|
// Re-export everything from the original Rspress theme
|
|
28
32
|
// (theme-original avoids circular resolution when used inside a themeDir)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code preview mode overrides.
|
|
3
|
+
*
|
|
4
|
+
* Applied when html[data-zpress-env="vscode"] is set — injected synchronously
|
|
5
|
+
* by the inline head script before first paint.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* Hide left sidebar and its placeholder */
|
|
9
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__sidebar,
|
|
10
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__sidebar-placeholder {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Hide right TOC and its placeholder */
|
|
15
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__outline,
|
|
16
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__outline-placeholder {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Center content at 1200px max */
|
|
21
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__doc {
|
|
22
|
+
max-width: 1200px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Hide nav items, social links, hamburger, mobile nav, appearance toggle */
|
|
27
|
+
html[data-zpress-env='vscode'] .rp-nav-menu__item,
|
|
28
|
+
html[data-zpress-env='vscode'] .rp-social-links,
|
|
29
|
+
html[data-zpress-env='vscode'] .rp-nav-hamburger,
|
|
30
|
+
html[data-zpress-env='vscode'] .rp-nav-screen,
|
|
31
|
+
html[data-zpress-env='vscode'] .rp-nav-screen-menu,
|
|
32
|
+
html[data-zpress-env='vscode'] .rp-local-nav,
|
|
33
|
+
html[data-zpress-env='vscode'] .rp-appearance,
|
|
34
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__menu {
|
|
35
|
+
display: none;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zpress/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Rspress plugin, theme components, and styles for zpress",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -52,15 +52,16 @@
|
|
|
52
52
|
"@iconify-json/vscode-icons": "^1.2.45",
|
|
53
53
|
"@iconify/react": "^6.0.2",
|
|
54
54
|
"ts-pattern": "^5.9.0",
|
|
55
|
-
"@zpress/config": "0.2.
|
|
56
|
-
"@zpress/core": "0.6.
|
|
57
|
-
"@zpress/theme": "0.2.
|
|
55
|
+
"@zpress/config": "0.2.1",
|
|
56
|
+
"@zpress/core": "0.6.1",
|
|
57
|
+
"@zpress/theme": "0.2.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@rslib/core": "^0.20.0",
|
|
61
61
|
"@rspress/core": "^2.0.5",
|
|
62
62
|
"@types/react": "^19.2.14",
|
|
63
63
|
"@types/react-dom": "^19.2.3",
|
|
64
|
+
"esbuild": "^0.27.4",
|
|
64
65
|
"geist": "^1.7.0",
|
|
65
66
|
"react": "^19.2.4",
|
|
66
67
|
"react-dom": "^19.2.4",
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
},
|
|
77
78
|
"scripts": {
|
|
78
79
|
"build": "rslib build",
|
|
80
|
+
"postbuild": "node scripts/minify-head.mjs",
|
|
79
81
|
"dev": "rslib build --watch --no-clean",
|
|
80
82
|
"typecheck": "tsc --noEmit"
|
|
81
83
|
}
|
|
@@ -6,82 +6,48 @@ import { BranchTag } from './branch-tag'
|
|
|
6
6
|
import { ThemeSwitcher } from './theme-switcher'
|
|
7
7
|
import { VscodeTag } from './vscode-tag'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* Center content at 1200px max */
|
|
27
|
-
html[data-zpress-env="vscode"] .rp-doc-layout__doc {
|
|
28
|
-
max-width: 1200px;
|
|
29
|
-
margin: 0 auto;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/* Hide nav items, social links, hamburger */
|
|
33
|
-
html[data-zpress-env="vscode"] .rp-nav-menu__item {
|
|
34
|
-
display: none;
|
|
35
|
-
}
|
|
36
|
-
html[data-zpress-env="vscode"] .rp-social-links {
|
|
37
|
-
display: none;
|
|
38
|
-
}
|
|
39
|
-
html[data-zpress-env="vscode"] .rp-nav-hamburger {
|
|
40
|
-
display: none;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Hide mobile navigation elements */
|
|
44
|
-
html[data-zpress-env="vscode"] .rp-nav-screen {
|
|
45
|
-
display: none;
|
|
46
|
-
}
|
|
47
|
-
html[data-zpress-env="vscode"] .rp-nav-screen-menu {
|
|
48
|
-
display: none;
|
|
49
|
-
}
|
|
50
|
-
html[data-zpress-env="vscode"] .rp-local-nav {
|
|
51
|
-
display: none;
|
|
52
|
-
}
|
|
53
|
-
html[data-zpress-env="vscode"] .rp-appearance {
|
|
54
|
-
display: none;
|
|
55
|
-
}
|
|
56
|
-
html[data-zpress-env="vscode"] .rp-doc-layout__menu {
|
|
57
|
-
display: none;
|
|
9
|
+
/**
|
|
10
|
+
* Detect vscode mode synchronously from sessionStorage and URL params.
|
|
11
|
+
* Returns false during SSR — client initializes correctly via lazy useState.
|
|
12
|
+
*/
|
|
13
|
+
function readVscodeMode(): boolean {
|
|
14
|
+
if (globalThis.window === undefined) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const params = new URLSearchParams(globalThis.location.search)
|
|
19
|
+
return (
|
|
20
|
+
params.get('env') === 'vscode' || globalThis.sessionStorage.getItem('zpress-env') === 'vscode'
|
|
21
|
+
)
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
58
25
|
}
|
|
59
|
-
`
|
|
60
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when the page is loaded in VS Code preview mode.
|
|
29
|
+
*
|
|
30
|
+
* Initializes synchronously from sessionStorage/URL so the VscodeTag
|
|
31
|
+
* renders in the same paint as the rest of the nav. The data-zpress-env
|
|
32
|
+
* attribute and static vscode.css are applied by the inline head script
|
|
33
|
+
* before React mounts, so no dynamic style injection is needed here.
|
|
34
|
+
*/
|
|
61
35
|
function useVscodeMode(): boolean {
|
|
62
|
-
const [active
|
|
36
|
+
const [active] = useState<boolean>(readVscodeMode)
|
|
63
37
|
|
|
38
|
+
// Persist vscode mode across SPA route changes — the inline head script
|
|
39
|
+
// sets sessionStorage on first load via URL param, but client-side
|
|
40
|
+
// navigation may lose the param. This ensures the flag survives.
|
|
64
41
|
useEffect(() => {
|
|
65
|
-
|
|
66
|
-
const isVscode =
|
|
67
|
-
params.get('env') === 'vscode' || globalThis.sessionStorage.getItem('zpress-env') === 'vscode'
|
|
68
|
-
|
|
69
|
-
if (!isVscode) {
|
|
42
|
+
if (!active) {
|
|
70
43
|
return
|
|
71
44
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const style = document.createElement('style')
|
|
78
|
-
style.textContent = VSCODE_OVERRIDES
|
|
79
|
-
document.head.append(style)
|
|
80
|
-
return () => {
|
|
81
|
-
style.remove()
|
|
82
|
-
delete document.documentElement.dataset.zpressEnv
|
|
45
|
+
try {
|
|
46
|
+
sessionStorage.setItem('zpress-env', 'vscode')
|
|
47
|
+
} catch {
|
|
48
|
+
// sessionStorage may be blocked in some environments
|
|
83
49
|
}
|
|
84
|
-
}, [])
|
|
50
|
+
}, [active])
|
|
85
51
|
|
|
86
52
|
return active
|
|
87
53
|
}
|
|
@@ -17,9 +17,23 @@ const THEME_OPTIONS: readonly ThemeOption[] = [
|
|
|
17
17
|
{ name: 'base', label: 'Base', swatch: '#a78bfa', defaultColorMode: 'toggle' },
|
|
18
18
|
{ name: 'midnight', label: 'Midnight', swatch: '#60a5fa', defaultColorMode: 'dark' },
|
|
19
19
|
{ name: 'arcade', label: 'Arcade', swatch: '#00ff88', defaultColorMode: 'dark' },
|
|
20
|
-
{ name: 'arcade-fx', label: 'Arcade FX', swatch: '#ff00ff', defaultColorMode: 'dark' },
|
|
21
20
|
]
|
|
22
21
|
|
|
22
|
+
const VALID_THEME_NAMES = new Set(THEME_OPTIONS.map((t) => t.name))
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a stored theme name, rejecting unknown values.
|
|
26
|
+
*/
|
|
27
|
+
function sanitizeThemeName(raw: string | null): string {
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return 'base'
|
|
30
|
+
}
|
|
31
|
+
if (VALID_THEME_NAMES.has(raw)) {
|
|
32
|
+
return raw
|
|
33
|
+
}
|
|
34
|
+
return 'base'
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* Build the className string for a theme option button.
|
|
25
39
|
*/
|
|
@@ -39,13 +53,15 @@ function applyTheme(theme: ThemeOption): void {
|
|
|
39
53
|
localStorage.setItem('zpress-theme', theme.name)
|
|
40
54
|
|
|
41
55
|
if (theme.defaultColorMode === 'dark') {
|
|
42
|
-
|
|
56
|
+
// 'rp-dark' is Rspress's dark mode class; 'dark' is added for Tailwind compatibility
|
|
57
|
+
html.classList.add('rp-dark', 'dark')
|
|
43
58
|
html.dataset.dark = 'true'
|
|
44
|
-
localStorage.setItem('rspress-theme', 'dark')
|
|
59
|
+
localStorage.setItem('rspress-theme-appearance', 'dark')
|
|
45
60
|
} else if (theme.defaultColorMode === 'light') {
|
|
46
|
-
|
|
61
|
+
// Remove both Rspress and Tailwind dark mode classes
|
|
62
|
+
html.classList.remove('rp-dark', 'dark')
|
|
47
63
|
html.dataset.dark = 'false'
|
|
48
|
-
localStorage.setItem('rspress-theme', 'light')
|
|
64
|
+
localStorage.setItem('rspress-theme-appearance', 'light')
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
|
|
@@ -56,11 +72,11 @@ function applyTheme(theme: ThemeOption): void {
|
|
|
56
72
|
export function ThemeSwitcher(): React.ReactElement | null {
|
|
57
73
|
const [isOpen, setIsOpen] = useState(false)
|
|
58
74
|
const [activeTheme, setActiveTheme] = useState(() => {
|
|
59
|
-
if (
|
|
75
|
+
if (globalThis.window === undefined) {
|
|
60
76
|
return 'base'
|
|
61
77
|
}
|
|
62
78
|
try {
|
|
63
|
-
return localStorage.getItem('zpress-theme')
|
|
79
|
+
return sanitizeThemeName(globalThis.localStorage.getItem('zpress-theme'))
|
|
64
80
|
} catch {
|
|
65
81
|
return 'base'
|
|
66
82
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react'
|
|
2
2
|
import type React from 'react'
|
|
3
3
|
|
|
4
4
|
declare const __ZPRESS_THEME_NAME__: string
|
|
@@ -87,14 +87,75 @@ function parseColors(raw: string): Record<string, string> {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* useLayoutEffect on the client, useEffect on the server (avoids SSR warning).
|
|
92
|
+
* Ensures DOM mutations happen synchronously before browser paint.
|
|
93
|
+
*/
|
|
94
|
+
function getIsomorphicEffect(): typeof useLayoutEffect {
|
|
95
|
+
if (globalThis.window !== undefined) {
|
|
96
|
+
return useLayoutEffect
|
|
97
|
+
}
|
|
98
|
+
return useEffect
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const useIsomorphicLayoutEffect = getIsomorphicEffect()
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Minimum time (ms) the loading overlay stays visible before fading out.
|
|
105
|
+
*/
|
|
106
|
+
const LOADER_MIN_DISPLAY_MS = 150
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Duration (ms) of the CSS fade-out transition. Must match the
|
|
110
|
+
* `transition: opacity` value in loader-backdrop.css / loader-dots.css.
|
|
111
|
+
*/
|
|
112
|
+
const LOADER_FADE_MS = 200
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Dismiss the loading overlay with a two-phase approach:
|
|
116
|
+
* 1. Add `zp-loader-fade` class → CSS transitions opacity to 0
|
|
117
|
+
* 2. After transition completes → set `data-zp-ready` attribute → CSS hard removes (display: none)
|
|
118
|
+
*
|
|
119
|
+
* Also clears the JS-driven dots animation interval set by loader-dots.js.
|
|
120
|
+
*
|
|
121
|
+
* Returns a cleanup function that cancels pending timers.
|
|
122
|
+
*/
|
|
123
|
+
function clearDotsInterval(): void {
|
|
124
|
+
const dotsInterval = (globalThis as Record<string, unknown>).__zpDotsInterval
|
|
125
|
+
if (typeof dotsInterval === 'number') {
|
|
126
|
+
clearInterval(dotsInterval)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dismissLoader(html: HTMLElement): () => void {
|
|
131
|
+
const fadeTimer = setTimeout(() => {
|
|
132
|
+
html.classList.add('zp-loader-fade')
|
|
133
|
+
}, LOADER_MIN_DISPLAY_MS)
|
|
134
|
+
|
|
135
|
+
const removeTimer = setTimeout(() => {
|
|
136
|
+
html.dataset.zpReady = 'true'
|
|
137
|
+
html.classList.remove('zp-loader-fade')
|
|
138
|
+
clearDotsInterval()
|
|
139
|
+
}, LOADER_MIN_DISPLAY_MS + LOADER_FADE_MS)
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
clearTimeout(fadeTimer)
|
|
143
|
+
clearTimeout(removeTimer)
|
|
144
|
+
html.classList.remove('zp-loader-fade')
|
|
145
|
+
clearDotsInterval()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
90
149
|
/**
|
|
91
150
|
* ThemeProvider — global UI component that configures the active theme.
|
|
92
151
|
*
|
|
93
152
|
* Sets `data-zp-theme` attribute, forces color mode, and applies
|
|
94
153
|
* inline CSS custom property overrides from build-time defines.
|
|
154
|
+
* Sets `data-zp-ready` on <html> to dismiss the loading overlay
|
|
155
|
+
* injected by critical CSS.
|
|
95
156
|
*/
|
|
96
157
|
export function ThemeProvider(): React.ReactElement | null {
|
|
97
|
-
|
|
158
|
+
useIsomorphicLayoutEffect(() => {
|
|
98
159
|
const html = document.documentElement
|
|
99
160
|
const themeName = safeGetItem('zpress-theme') || __ZPRESS_THEME_NAME__
|
|
100
161
|
const colorMode = __ZPRESS_COLOR_MODE__
|
|
@@ -108,13 +169,21 @@ export function ThemeProvider(): React.ReactElement | null {
|
|
|
108
169
|
|
|
109
170
|
// 2. Force color mode if not toggle
|
|
110
171
|
if (colorMode === 'dark') {
|
|
111
|
-
html.classList.add('rp-dark')
|
|
172
|
+
html.classList.add('rp-dark', 'dark')
|
|
112
173
|
html.dataset.dark = 'true'
|
|
113
|
-
|
|
174
|
+
try {
|
|
175
|
+
localStorage.setItem('rspress-theme-appearance', 'dark')
|
|
176
|
+
} catch {
|
|
177
|
+
// storage unavailable
|
|
178
|
+
}
|
|
114
179
|
} else if (colorMode === 'light') {
|
|
115
|
-
html.classList.remove('rp-dark')
|
|
180
|
+
html.classList.remove('rp-dark', 'dark')
|
|
116
181
|
html.dataset.dark = 'false'
|
|
117
|
-
|
|
182
|
+
try {
|
|
183
|
+
localStorage.setItem('rspress-theme-appearance', 'light')
|
|
184
|
+
} catch {
|
|
185
|
+
// storage unavailable
|
|
186
|
+
}
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
// 3. Apply base color overrides
|
|
@@ -145,12 +214,17 @@ export function ThemeProvider(): React.ReactElement | null {
|
|
|
145
214
|
|
|
146
215
|
observer.observe(html, { attributes: true, attributeFilter: ['class'] })
|
|
147
216
|
|
|
217
|
+
// 5. Dismiss loading overlay
|
|
218
|
+
const cancelLoader = dismissLoader(html)
|
|
219
|
+
|
|
148
220
|
return () => {
|
|
149
221
|
observer.disconnect()
|
|
222
|
+
cancelLoader()
|
|
150
223
|
}
|
|
151
224
|
}
|
|
152
225
|
|
|
153
|
-
//
|
|
226
|
+
// 5. Dismiss loading overlay (no dark-color observer needed)
|
|
227
|
+
return dismissLoader(html)
|
|
154
228
|
}, [])
|
|
155
229
|
|
|
156
230
|
return null
|
package/src/theme/index.tsx
CHANGED
|
@@ -16,6 +16,9 @@ import './styles/overrides/rspress.css'
|
|
|
16
16
|
import './styles/themes/base.css'
|
|
17
17
|
import './styles/themes/midnight.css'
|
|
18
18
|
import './styles/themes/arcade.css'
|
|
19
|
+
// arcade-fx.css is intentionally separate from arcade.css:
|
|
20
|
+
// arcade.css = color palette tokens, arcade-fx.css = visual effects
|
|
21
|
+
// (border trace, neon pulse, CRT scanlines, etc.) scoped to [data-zp-theme='arcade']
|
|
19
22
|
import './styles/themes/arcade-fx.css'
|
|
20
23
|
import './styles/overrides/details.css'
|
|
21
24
|
import './styles/overrides/scrollbar.css'
|
|
@@ -23,6 +26,7 @@ import './styles/overrides/sidebar.css'
|
|
|
23
26
|
import './styles/overrides/home.css'
|
|
24
27
|
import './styles/overrides/home-card.css'
|
|
25
28
|
import './styles/overrides/section-card.css'
|
|
29
|
+
import './styles/overrides/vscode.css'
|
|
26
30
|
|
|
27
31
|
// Re-export everything from the original Rspress theme
|
|
28
32
|
// (theme-original avoids circular resolution when used inside a themeDir)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code preview mode overrides.
|
|
3
|
+
*
|
|
4
|
+
* Applied when html[data-zpress-env="vscode"] is set — injected synchronously
|
|
5
|
+
* by the inline head script before first paint.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* Hide left sidebar and its placeholder */
|
|
9
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__sidebar,
|
|
10
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__sidebar-placeholder {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Hide right TOC and its placeholder */
|
|
15
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__outline,
|
|
16
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__outline-placeholder {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Center content at 1200px max */
|
|
21
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__doc {
|
|
22
|
+
max-width: 1200px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Hide nav items, social links, hamburger, mobile nav, appearance toggle */
|
|
27
|
+
html[data-zpress-env='vscode'] .rp-nav-menu__item,
|
|
28
|
+
html[data-zpress-env='vscode'] .rp-social-links,
|
|
29
|
+
html[data-zpress-env='vscode'] .rp-nav-hamburger,
|
|
30
|
+
html[data-zpress-env='vscode'] .rp-nav-screen,
|
|
31
|
+
html[data-zpress-env='vscode'] .rp-nav-screen-menu,
|
|
32
|
+
html[data-zpress-env='vscode'] .rp-local-nav,
|
|
33
|
+
html[data-zpress-env='vscode'] .rp-appearance,
|
|
34
|
+
html[data-zpress-env='vscode'] .rp-doc-layout__menu {
|
|
35
|
+
display: none;
|
|
36
|
+
}
|