@zpress/ui 0.3.1 → 0.4.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/index.d.ts +2 -0
- package/dist/index.mjs +38 -3
- package/dist/theme/components/nav/layout.tsx +61 -4
- package/dist/theme/components/nav/theme-switcher.css +92 -0
- package/dist/theme/components/nav/theme-switcher.tsx +135 -0
- package/dist/theme/components/nav/vscode-tag.css +32 -0
- package/dist/theme/components/nav/vscode-tag.tsx +18 -0
- package/dist/theme/components/shared/browser-window.css +61 -0
- package/dist/theme/components/shared/browser-window.tsx +35 -0
- package/dist/theme/components/theme-provider.tsx +159 -0
- package/dist/theme/index.tsx +7 -0
- package/dist/theme/styles/overrides/rspress.css +3 -72
- package/dist/theme/styles/overrides/tokens.css +3 -34
- package/dist/theme/styles/themes/arcade-fx.css +360 -0
- package/dist/theme/styles/themes/arcade.css +96 -0
- package/dist/theme/styles/themes/base.css +133 -0
- package/dist/theme/styles/themes/midnight.css +96 -0
- package/package.json +3 -3
- package/src/theme/components/nav/layout.tsx +61 -4
- package/src/theme/components/nav/theme-switcher.css +92 -0
- package/src/theme/components/nav/theme-switcher.tsx +135 -0
- package/src/theme/components/nav/vscode-tag.css +32 -0
- package/src/theme/components/nav/vscode-tag.tsx +18 -0
- package/src/theme/components/shared/browser-window.css +61 -0
- package/src/theme/components/shared/browser-window.tsx +35 -0
- package/src/theme/components/theme-provider.tsx +159 -0
- package/src/theme/index.tsx +7 -0
- package/src/theme/styles/overrides/rspress.css +3 -72
- package/src/theme/styles/overrides/tokens.css +3 -34
- package/src/theme/styles/themes/arcade-fx.css +360 -0
- package/src/theme/styles/themes/arcade.css +96 -0
- package/src/theme/styles/themes/base.css +133 -0
- package/src/theme/styles/themes/midnight.css +96 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base theme — purple/violet palette.
|
|
3
|
+
*
|
|
4
|
+
* Default zpress theme. `:root` fallback block prevents FOUC
|
|
5
|
+
* by providing colors before ThemeProvider sets [data-zp-theme].
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* ── FOUC fallback — loads before JS hydration ───────────── */
|
|
9
|
+
:root {
|
|
10
|
+
--zp-c-brand-1: #a78bfa;
|
|
11
|
+
--zp-c-brand-2: #8b5cf6;
|
|
12
|
+
--zp-c-brand-3: #7c3aed;
|
|
13
|
+
--zp-c-brand-soft: rgba(167, 139, 250, 0.14);
|
|
14
|
+
|
|
15
|
+
--zp-c-bg: #ffffff;
|
|
16
|
+
--zp-c-bg-alt: #f9f9f9;
|
|
17
|
+
--zp-c-bg-elv: #f5f5f5;
|
|
18
|
+
--zp-c-bg-soft: #f0f0f0;
|
|
19
|
+
--zp-c-bg-icon: #cccccc;
|
|
20
|
+
|
|
21
|
+
--zp-c-text-1: #1a1a1a;
|
|
22
|
+
--zp-c-text-2: rgba(26, 26, 26, 0.72);
|
|
23
|
+
--zp-c-text-3: rgba(26, 26, 26, 0.48);
|
|
24
|
+
|
|
25
|
+
--zp-c-divider: #e2e2e2;
|
|
26
|
+
--zp-c-border: #d0d0d0;
|
|
27
|
+
--zp-c-gutter: #f5f5f5;
|
|
28
|
+
|
|
29
|
+
--zp-code-block-bg: #f5f5f5;
|
|
30
|
+
|
|
31
|
+
--zp-button-brand-bg: #7c3aed;
|
|
32
|
+
--zp-button-brand-hover-bg: #8b5cf6;
|
|
33
|
+
--zp-button-brand-active-bg: #6d28d9;
|
|
34
|
+
--zp-button-brand-text: #ffffff;
|
|
35
|
+
|
|
36
|
+
--rp-c-brand: #a78bfa;
|
|
37
|
+
--rp-c-brand-light: #c4b5fd;
|
|
38
|
+
--rp-c-brand-lighter: #ddd6fe;
|
|
39
|
+
--rp-c-brand-dark: #8b5cf6;
|
|
40
|
+
--rp-c-brand-darker: #7c3aed;
|
|
41
|
+
--rp-c-brand-tint: rgba(167, 139, 250, 0.14);
|
|
42
|
+
|
|
43
|
+
--rp-home-background-bg: #fff;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Light mode ──────────────────────────────────────────── */
|
|
47
|
+
html[data-zp-theme='base'] {
|
|
48
|
+
--zp-c-brand-1: #a78bfa;
|
|
49
|
+
--zp-c-brand-2: #8b5cf6;
|
|
50
|
+
--zp-c-brand-3: #7c3aed;
|
|
51
|
+
--zp-c-brand-soft: rgba(167, 139, 250, 0.14);
|
|
52
|
+
|
|
53
|
+
--zp-c-bg: #ffffff;
|
|
54
|
+
--zp-c-bg-alt: #f9f9f9;
|
|
55
|
+
--zp-c-bg-elv: #f5f5f5;
|
|
56
|
+
--zp-c-bg-soft: #f0f0f0;
|
|
57
|
+
--zp-c-bg-icon: #cccccc;
|
|
58
|
+
|
|
59
|
+
--zp-c-text-1: #1a1a1a;
|
|
60
|
+
--zp-c-text-2: rgba(26, 26, 26, 0.72);
|
|
61
|
+
--zp-c-text-3: rgba(26, 26, 26, 0.48);
|
|
62
|
+
|
|
63
|
+
--zp-c-divider: #e2e2e2;
|
|
64
|
+
--zp-c-border: #d0d0d0;
|
|
65
|
+
--zp-c-gutter: #f5f5f5;
|
|
66
|
+
|
|
67
|
+
--zp-code-block-bg: #f5f5f5;
|
|
68
|
+
|
|
69
|
+
--zp-button-brand-bg: #7c3aed;
|
|
70
|
+
--zp-button-brand-hover-bg: #8b5cf6;
|
|
71
|
+
--zp-button-brand-active-bg: #6d28d9;
|
|
72
|
+
--zp-button-brand-text: #ffffff;
|
|
73
|
+
|
|
74
|
+
--rp-c-brand: #a78bfa;
|
|
75
|
+
--rp-c-brand-light: #c4b5fd;
|
|
76
|
+
--rp-c-brand-lighter: #ddd6fe;
|
|
77
|
+
--rp-c-brand-dark: #8b5cf6;
|
|
78
|
+
--rp-c-brand-darker: #7c3aed;
|
|
79
|
+
--rp-c-brand-tint: rgba(167, 139, 250, 0.14);
|
|
80
|
+
|
|
81
|
+
--rp-home-background-bg: #fff;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── Dark mode ───────────────────────────────────────────── */
|
|
85
|
+
html[data-zp-theme='base'].rp-dark {
|
|
86
|
+
--zp-c-bg: #141414;
|
|
87
|
+
--zp-c-bg-alt: #1a1a1a;
|
|
88
|
+
--zp-c-bg-elv: #1f1f1f;
|
|
89
|
+
--zp-c-bg-soft: #222222;
|
|
90
|
+
--zp-c-bg-icon: #3d3d3d;
|
|
91
|
+
|
|
92
|
+
--zp-c-text-1: #fbfbfb;
|
|
93
|
+
--zp-c-text-2: rgba(251, 251, 251, 0.72);
|
|
94
|
+
--zp-c-text-3: rgba(251, 251, 251, 0.48);
|
|
95
|
+
|
|
96
|
+
--zp-c-divider: #2e2e2e;
|
|
97
|
+
--zp-c-border: #3a3a3a;
|
|
98
|
+
--zp-c-gutter: #1a1a1a;
|
|
99
|
+
|
|
100
|
+
--zp-c-brand-1: #a78bfa;
|
|
101
|
+
--zp-c-brand-2: #8b5cf6;
|
|
102
|
+
--zp-c-brand-3: #7c3aed;
|
|
103
|
+
--zp-c-brand-soft: rgba(167, 139, 250, 0.14);
|
|
104
|
+
|
|
105
|
+
--zp-code-block-bg: #1a1a1a;
|
|
106
|
+
|
|
107
|
+
--zp-button-brand-bg: #7c3aed;
|
|
108
|
+
--zp-button-brand-hover-bg: #8b5cf6;
|
|
109
|
+
--zp-button-brand-active-bg: #6d28d9;
|
|
110
|
+
--zp-button-brand-text: #fbfbfb;
|
|
111
|
+
|
|
112
|
+
--rp-c-bg: #141414;
|
|
113
|
+
--rp-c-bg-soft: #222222;
|
|
114
|
+
--rp-c-bg-mute: #1f1f1f;
|
|
115
|
+
|
|
116
|
+
--rp-c-text-1: #fbfbfb;
|
|
117
|
+
--rp-c-text-2: rgba(251, 251, 251, 0.72);
|
|
118
|
+
--rp-c-text-3: rgba(251, 251, 251, 0.48);
|
|
119
|
+
|
|
120
|
+
--rp-c-divider: #2e2e2e;
|
|
121
|
+
|
|
122
|
+
--rp-c-brand: #a78bfa;
|
|
123
|
+
--rp-c-brand-light: #c4b5fd;
|
|
124
|
+
--rp-c-brand-lighter: #ddd6fe;
|
|
125
|
+
--rp-c-brand-dark: #8b5cf6;
|
|
126
|
+
--rp-c-brand-darker: #7c3aed;
|
|
127
|
+
--rp-c-brand-tint: rgba(167, 139, 250, 0.14);
|
|
128
|
+
--rp-c-link: var(--rp-c-brand-light);
|
|
129
|
+
|
|
130
|
+
--rp-code-block-bg: #1a1a1a;
|
|
131
|
+
|
|
132
|
+
--rp-home-background-bg: #141414;
|
|
133
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Midnight theme — super dark, blue-tinted brand.
|
|
3
|
+
*
|
|
4
|
+
* Deep black backgrounds with high contrast white text.
|
|
5
|
+
* Both modes are very dark — light is "less dark", dark is "true black."
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* ── Light mode (less dark) ──────────────────────────────── */
|
|
9
|
+
html[data-zp-theme='midnight'] {
|
|
10
|
+
--zp-c-brand-1: #60a5fa;
|
|
11
|
+
--zp-c-brand-2: #3b82f6;
|
|
12
|
+
--zp-c-brand-3: #2563eb;
|
|
13
|
+
--zp-c-brand-soft: rgba(96, 165, 250, 0.14);
|
|
14
|
+
|
|
15
|
+
--zp-c-bg: #0f0f0f;
|
|
16
|
+
--zp-c-bg-alt: #121212;
|
|
17
|
+
--zp-c-bg-elv: #161616;
|
|
18
|
+
--zp-c-bg-soft: #1a1a1a;
|
|
19
|
+
--zp-c-bg-icon: #2a2a2a;
|
|
20
|
+
|
|
21
|
+
--zp-c-text-1: #f0f0f0;
|
|
22
|
+
--zp-c-text-2: rgba(240, 240, 240, 0.72);
|
|
23
|
+
--zp-c-text-3: rgba(240, 240, 240, 0.48);
|
|
24
|
+
|
|
25
|
+
--zp-c-divider: #1e1e1e;
|
|
26
|
+
--zp-c-border: #282828;
|
|
27
|
+
--zp-c-gutter: #121212;
|
|
28
|
+
|
|
29
|
+
--zp-code-block-bg: #121212;
|
|
30
|
+
|
|
31
|
+
--zp-button-brand-bg: #2563eb;
|
|
32
|
+
--zp-button-brand-hover-bg: #3b82f6;
|
|
33
|
+
--zp-button-brand-active-bg: #1d4ed8;
|
|
34
|
+
--zp-button-brand-text: #f0f0f0;
|
|
35
|
+
|
|
36
|
+
--rp-c-brand: #60a5fa;
|
|
37
|
+
--rp-c-brand-light: #93c5fd;
|
|
38
|
+
--rp-c-brand-lighter: #bfdbfe;
|
|
39
|
+
--rp-c-brand-dark: #3b82f6;
|
|
40
|
+
--rp-c-brand-darker: #2563eb;
|
|
41
|
+
--rp-c-brand-tint: rgba(96, 165, 250, 0.14);
|
|
42
|
+
|
|
43
|
+
--rp-c-bg: #0f0f0f;
|
|
44
|
+
--rp-c-bg-soft: #1a1a1a;
|
|
45
|
+
--rp-c-bg-mute: #161616;
|
|
46
|
+
|
|
47
|
+
--rp-c-text-1: #f0f0f0;
|
|
48
|
+
--rp-c-text-2: rgba(240, 240, 240, 0.72);
|
|
49
|
+
--rp-c-text-3: rgba(240, 240, 240, 0.48);
|
|
50
|
+
|
|
51
|
+
--rp-c-divider: #1e1e1e;
|
|
52
|
+
--rp-c-link: var(--rp-c-brand-light);
|
|
53
|
+
|
|
54
|
+
--rp-code-block-bg: #121212;
|
|
55
|
+
|
|
56
|
+
--rp-home-background-bg: #0f0f0f;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── Dark mode (true black) ──────────────────────────────── */
|
|
60
|
+
html[data-zp-theme='midnight'].rp-dark {
|
|
61
|
+
--zp-c-bg: #050505;
|
|
62
|
+
--zp-c-bg-alt: #0a0a0a;
|
|
63
|
+
--zp-c-bg-elv: #0e0e0e;
|
|
64
|
+
--zp-c-bg-soft: #121212;
|
|
65
|
+
--zp-c-bg-icon: #222222;
|
|
66
|
+
|
|
67
|
+
--zp-c-text-1: #fafafa;
|
|
68
|
+
--zp-c-text-2: rgba(250, 250, 250, 0.72);
|
|
69
|
+
--zp-c-text-3: rgba(250, 250, 250, 0.48);
|
|
70
|
+
|
|
71
|
+
--zp-c-divider: #181818;
|
|
72
|
+
--zp-c-border: #202020;
|
|
73
|
+
--zp-c-gutter: #0a0a0a;
|
|
74
|
+
|
|
75
|
+
--zp-code-block-bg: #0a0a0a;
|
|
76
|
+
|
|
77
|
+
--zp-button-brand-bg: #2563eb;
|
|
78
|
+
--zp-button-brand-hover-bg: #3b82f6;
|
|
79
|
+
--zp-button-brand-active-bg: #1d4ed8;
|
|
80
|
+
--zp-button-brand-text: #fafafa;
|
|
81
|
+
|
|
82
|
+
--rp-c-bg: #050505;
|
|
83
|
+
--rp-c-bg-soft: #121212;
|
|
84
|
+
--rp-c-bg-mute: #0e0e0e;
|
|
85
|
+
|
|
86
|
+
--rp-c-text-1: #fafafa;
|
|
87
|
+
--rp-c-text-2: rgba(250, 250, 250, 0.72);
|
|
88
|
+
--rp-c-text-3: rgba(250, 250, 250, 0.48);
|
|
89
|
+
|
|
90
|
+
--rp-c-divider: #181818;
|
|
91
|
+
--rp-c-link: var(--rp-c-brand-light);
|
|
92
|
+
|
|
93
|
+
--rp-code-block-bg: #0a0a0a;
|
|
94
|
+
|
|
95
|
+
--rp-home-background-bg: #050505;
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zpress/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Rspress plugin, theme components, and styles for zpress",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@iconify-json/catppuccin": "^1.2.17",
|
|
45
|
-
"@iconify-json/devicon": "^1.2.
|
|
45
|
+
"@iconify-json/devicon": "^1.2.60",
|
|
46
46
|
"@iconify-json/logos": "^1.2.10",
|
|
47
47
|
"@iconify-json/material-icon-theme": "^1.2.55",
|
|
48
48
|
"@iconify-json/mdi": "^1.2.3",
|
|
@@ -52,7 +52,7 @@
|
|
|
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/core": "0.
|
|
55
|
+
"@zpress/core": "0.5.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@rslib/core": "^0.20.0",
|
|
@@ -1,14 +1,71 @@
|
|
|
1
1
|
import { Layout as OriginalLayout } from '@rspress/core/theme-original'
|
|
2
2
|
import type React from 'react'
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
3
4
|
|
|
4
5
|
import { BranchTag } from './branch-tag'
|
|
6
|
+
import { ThemeSwitcher } from './theme-switcher'
|
|
7
|
+
import { VscodeTag } from './vscode-tag'
|
|
8
|
+
|
|
9
|
+
const VSCODE_OVERRIDES = [
|
|
10
|
+
/* Hide left sidebar and its placeholder */
|
|
11
|
+
'.rp-doc-layout__sidebar { display: none !important; }',
|
|
12
|
+
'.rp-doc-layout__sidebar-placeholder { display: none !important; }',
|
|
13
|
+
/* Hide right TOC and its placeholder */
|
|
14
|
+
'.rp-doc-layout__outline { display: none !important; }',
|
|
15
|
+
'.rp-doc-layout__outline-placeholder { display: none !important; }',
|
|
16
|
+
/* Center content at 1200px max */
|
|
17
|
+
'.rp-doc-layout__doc { max-width: 1200px !important; margin: 0 auto !important; }',
|
|
18
|
+
/* Hide nav items, social links, hamburger */
|
|
19
|
+
'.rp-nav-menu__item { display: none !important; }',
|
|
20
|
+
'.rp-social-links { display: none !important; }',
|
|
21
|
+
'.rp-nav-hamburger { display: none !important; }',
|
|
22
|
+
].join('\n')
|
|
23
|
+
|
|
24
|
+
function useVscodeMode(): boolean {
|
|
25
|
+
const [active, setActive] = useState(false)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const params = new URLSearchParams(globalThis.location.search)
|
|
29
|
+
const isVscode =
|
|
30
|
+
params.get('env') === 'vscode' || globalThis.sessionStorage.getItem('zpress-env') === 'vscode'
|
|
31
|
+
|
|
32
|
+
if (!isVscode) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
globalThis.sessionStorage.setItem('zpress-env', 'vscode')
|
|
37
|
+
setActive(true)
|
|
38
|
+
document.documentElement.dataset.zpressEnv = 'vscode'
|
|
39
|
+
|
|
40
|
+
const style = document.createElement('style')
|
|
41
|
+
style.textContent = VSCODE_OVERRIDES
|
|
42
|
+
document.head.append(style)
|
|
43
|
+
return () => {
|
|
44
|
+
style.remove()
|
|
45
|
+
delete document.documentElement.dataset.zpressEnv
|
|
46
|
+
}
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
return active
|
|
50
|
+
}
|
|
5
51
|
|
|
6
52
|
/**
|
|
7
53
|
* Custom Layout override for zpress.
|
|
8
|
-
* Wraps the original Rspress Layout and injects
|
|
9
|
-
* into
|
|
10
|
-
* bar in the navbar on all page types.
|
|
54
|
+
* Wraps the original Rspress Layout and injects BranchTag
|
|
55
|
+
* into `beforeNavMenu` and ThemeSwitcher into `afterNavMenu`.
|
|
11
56
|
*/
|
|
12
57
|
export function Layout(): React.ReactElement {
|
|
13
|
-
|
|
58
|
+
const vscode = useVscodeMode()
|
|
59
|
+
const navSlot = (() => {
|
|
60
|
+
if (vscode) {
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<BranchTag />
|
|
64
|
+
<VscodeTag />
|
|
65
|
+
</>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
return <BranchTag />
|
|
69
|
+
})()
|
|
70
|
+
return <OriginalLayout beforeNavMenu={navSlot} afterNavMenu={<ThemeSwitcher />} />
|
|
14
71
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme switcher dropdown styles.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.theme-switcher {
|
|
6
|
+
position: relative;
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
margin-left: 8px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.theme-switcher-btn {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
width: 32px;
|
|
17
|
+
height: 32px;
|
|
18
|
+
padding: 0;
|
|
19
|
+
border: 1px solid var(--zp-c-border);
|
|
20
|
+
border-radius: var(--rp-radius);
|
|
21
|
+
background: var(--zp-c-bg);
|
|
22
|
+
color: var(--zp-c-text-2);
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
transition:
|
|
25
|
+
color 0.2s,
|
|
26
|
+
border-color 0.2s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.theme-switcher-btn:hover {
|
|
30
|
+
color: var(--zp-c-text-1);
|
|
31
|
+
border-color: var(--zp-c-brand-1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.theme-switcher-dropdown {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: calc(100% + 8px);
|
|
37
|
+
right: 0;
|
|
38
|
+
z-index: 100;
|
|
39
|
+
min-width: 160px;
|
|
40
|
+
padding: 4px;
|
|
41
|
+
border: 1px solid var(--zp-c-border);
|
|
42
|
+
border-radius: var(--rp-radius);
|
|
43
|
+
background: var(--zp-c-bg-elv);
|
|
44
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.theme-switcher-option {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 8px;
|
|
51
|
+
width: 100%;
|
|
52
|
+
padding: 8px 12px;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: var(--rp-radius-small);
|
|
55
|
+
background: none;
|
|
56
|
+
color: var(--zp-c-text-2);
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
font-family: var(--zp-font-family-base);
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition:
|
|
61
|
+
background 0.15s,
|
|
62
|
+
color 0.15s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.theme-switcher-option:hover {
|
|
66
|
+
background: var(--zp-c-bg-soft);
|
|
67
|
+
color: var(--zp-c-text-1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.theme-switcher-option--active {
|
|
71
|
+
color: var(--zp-c-brand-1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.theme-switcher-swatch {
|
|
75
|
+
display: inline-block;
|
|
76
|
+
width: 12px;
|
|
77
|
+
height: 12px;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
border: 1px solid var(--zp-c-border);
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.theme-switcher-check {
|
|
84
|
+
margin-left: auto;
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
opacity: 0.7;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.theme-switcher-name {
|
|
90
|
+
flex: 1;
|
|
91
|
+
text-align: left;
|
|
92
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '../shared/icon.tsx'
|
|
4
|
+
|
|
5
|
+
import './theme-switcher.css'
|
|
6
|
+
|
|
7
|
+
declare const __ZPRESS_THEME_SWITCHER__: boolean
|
|
8
|
+
|
|
9
|
+
interface ThemeOption {
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly label: string
|
|
12
|
+
readonly swatch: string
|
|
13
|
+
readonly defaultColorMode: 'dark' | 'light' | 'toggle'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const THEME_OPTIONS: readonly ThemeOption[] = [
|
|
17
|
+
{ name: 'base', label: 'Base', swatch: '#a78bfa', defaultColorMode: 'toggle' },
|
|
18
|
+
{ name: 'midnight', label: 'Midnight', swatch: '#60a5fa', defaultColorMode: 'dark' },
|
|
19
|
+
{ name: 'arcade', label: 'Arcade', swatch: '#00ff88', defaultColorMode: 'dark' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the className string for a theme option button.
|
|
24
|
+
*/
|
|
25
|
+
function optionClassName(isActive: boolean): string {
|
|
26
|
+
if (isActive) {
|
|
27
|
+
return 'theme-switcher-option theme-switcher-option--active'
|
|
28
|
+
}
|
|
29
|
+
return 'theme-switcher-option'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Apply a theme by updating the DOM and persisting to localStorage.
|
|
34
|
+
*/
|
|
35
|
+
function applyTheme(theme: ThemeOption): void {
|
|
36
|
+
const html = document.documentElement
|
|
37
|
+
html.dataset.zpTheme = theme.name
|
|
38
|
+
localStorage.setItem('zpress-theme', theme.name)
|
|
39
|
+
|
|
40
|
+
if (theme.defaultColorMode === 'dark') {
|
|
41
|
+
html.classList.add('rp-dark')
|
|
42
|
+
html.dataset.dark = 'true'
|
|
43
|
+
localStorage.setItem('rspress-theme', 'dark')
|
|
44
|
+
} else if (theme.defaultColorMode === 'light') {
|
|
45
|
+
html.classList.remove('rp-dark')
|
|
46
|
+
html.dataset.dark = 'false'
|
|
47
|
+
localStorage.setItem('rspress-theme', 'light')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ThemeSwitcher — dropdown button for switching between built-in themes.
|
|
53
|
+
* Only renders when `__ZPRESS_THEME_SWITCHER__` build-time define is true.
|
|
54
|
+
*/
|
|
55
|
+
export function ThemeSwitcher(): React.ReactElement | null {
|
|
56
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
57
|
+
const [activeTheme, setActiveTheme] = useState(() => {
|
|
58
|
+
if (typeof globalThis === 'undefined') {
|
|
59
|
+
return 'base'
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return localStorage.getItem('zpress-theme') || 'base'
|
|
63
|
+
} catch {
|
|
64
|
+
return 'base'
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
68
|
+
|
|
69
|
+
const handleToggle = useCallback(() => {
|
|
70
|
+
setIsOpen((prev) => !prev)
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const handleSelect = useCallback((theme: ThemeOption) => {
|
|
74
|
+
setActiveTheme(theme.name)
|
|
75
|
+
applyTheme(theme)
|
|
76
|
+
setIsOpen(false)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
function handleClickOutside(event: MouseEvent): void {
|
|
81
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
82
|
+
setIsOpen(false)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleEscape(event: KeyboardEvent): void {
|
|
87
|
+
if (event.key === 'Escape') {
|
|
88
|
+
setIsOpen(false)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
93
|
+
document.addEventListener('keydown', handleEscape)
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
97
|
+
document.removeEventListener('keydown', handleEscape)
|
|
98
|
+
}
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
if (!__ZPRESS_THEME_SWITCHER__) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="theme-switcher" ref={containerRef}>
|
|
107
|
+
<button
|
|
108
|
+
className="theme-switcher-btn"
|
|
109
|
+
onClick={handleToggle}
|
|
110
|
+
aria-label="Switch theme"
|
|
111
|
+
type="button"
|
|
112
|
+
>
|
|
113
|
+
<Icon icon="pixelarticons:paint-bucket" width={16} height={16} />
|
|
114
|
+
</button>
|
|
115
|
+
{isOpen && (
|
|
116
|
+
<div className="theme-switcher-dropdown">
|
|
117
|
+
{THEME_OPTIONS.map((theme) => (
|
|
118
|
+
<button
|
|
119
|
+
key={theme.name}
|
|
120
|
+
className={optionClassName(activeTheme === theme.name)}
|
|
121
|
+
onClick={() => handleSelect(theme)}
|
|
122
|
+
type="button"
|
|
123
|
+
>
|
|
124
|
+
<span className="theme-switcher-swatch" style={{ backgroundColor: theme.swatch }} />
|
|
125
|
+
<span className="theme-switcher-name">{theme.label}</span>
|
|
126
|
+
{activeTheme === theme.name && (
|
|
127
|
+
<span className="theme-switcher-check">{'\u2713'}</span>
|
|
128
|
+
)}
|
|
129
|
+
</button>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code tag pill — mode indicator badge positioned in the nav bar.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.vscode-tag {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 5px;
|
|
9
|
+
margin-left: 6px;
|
|
10
|
+
padding: 3px 10px;
|
|
11
|
+
border-radius: 20px;
|
|
12
|
+
background: var(--zp-c-bg-soft);
|
|
13
|
+
border: 1px solid var(--zp-c-brand-1, #a78bfa);
|
|
14
|
+
color: var(--zp-c-brand-1, #a78bfa);
|
|
15
|
+
font-family: var(--zp-font-family-mono);
|
|
16
|
+
font-size: 11px;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
line-height: 1;
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.vscode-tag svg {
|
|
23
|
+
width: 14px;
|
|
24
|
+
height: 14px;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (max-width: 768px) {
|
|
29
|
+
.vscode-tag-text {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
import './vscode-tag.css'
|
|
4
|
+
import { Icon } from '../shared/icon.tsx'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* VS Code mode indicator — pill-shaped badge rendered next to the
|
|
8
|
+
* BranchTag when the page is loaded inside the VS Code webview
|
|
9
|
+
* (detected via `?env=vscode` query parameter).
|
|
10
|
+
*/
|
|
11
|
+
export function VscodeTag(): React.ReactElement {
|
|
12
|
+
return (
|
|
13
|
+
<span className="vscode-tag" title="VS Code preview mode" aria-label="VS Code preview mode">
|
|
14
|
+
<Icon icon="vscode-icons:file-type-vscode" width={14} height={14} />
|
|
15
|
+
<span className="vscode-tag-text">vscode</span>
|
|
16
|
+
</span>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.browser-window {
|
|
2
|
+
border: 1px solid var(--zp-c-border);
|
|
3
|
+
border-radius: 8px;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
background: var(--zp-c-bg-soft);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.browser-window__titlebar {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
padding: 8px 12px;
|
|
12
|
+
gap: 8px;
|
|
13
|
+
background: var(--zp-c-bg-elv);
|
|
14
|
+
border-bottom: 1px solid var(--zp-c-border);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.browser-window__dots {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 6px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.browser-window__dot {
|
|
23
|
+
width: 10px;
|
|
24
|
+
height: 10px;
|
|
25
|
+
border-radius: 50%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.browser-window__dot--close {
|
|
29
|
+
background: #ff5f57;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.browser-window__dot--minimize {
|
|
33
|
+
background: #febc2e;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.browser-window__dot--maximize {
|
|
37
|
+
background: #28c840;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.browser-window__url {
|
|
41
|
+
flex: 1;
|
|
42
|
+
text-align: center;
|
|
43
|
+
font-size: 11px;
|
|
44
|
+
color: var(--zp-c-text-3);
|
|
45
|
+
background: var(--zp-c-bg-soft);
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
padding: 2px 8px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.browser-window__content p {
|
|
51
|
+
margin: 0;
|
|
52
|
+
padding: 0;
|
|
53
|
+
line-height: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.browser-window__content img {
|
|
57
|
+
width: 100%;
|
|
58
|
+
display: block;
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: 0;
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { match, P } from 'ts-pattern'
|
|
3
|
+
|
|
4
|
+
import './browser-window.css'
|
|
5
|
+
|
|
6
|
+
// ── Types ────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface BrowserWindowProps {
|
|
9
|
+
readonly url?: string
|
|
10
|
+
readonly children: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ── Component ────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fake browser window chrome that wraps content in a title bar
|
|
17
|
+
* with traffic-light dots and an optional URL pill.
|
|
18
|
+
*/
|
|
19
|
+
export function BrowserWindow({ url, children }: BrowserWindowProps): React.ReactElement {
|
|
20
|
+
return (
|
|
21
|
+
<div className="browser-window">
|
|
22
|
+
<div className="browser-window__titlebar">
|
|
23
|
+
<div className="browser-window__dots">
|
|
24
|
+
<span className="browser-window__dot browser-window__dot--close" />
|
|
25
|
+
<span className="browser-window__dot browser-window__dot--minimize" />
|
|
26
|
+
<span className="browser-window__dot browser-window__dot--maximize" />
|
|
27
|
+
</div>
|
|
28
|
+
{match(url)
|
|
29
|
+
.with(P.nonNullable, (u) => <span className="browser-window__url">{u}</span>)
|
|
30
|
+
.otherwise(() => null)}
|
|
31
|
+
</div>
|
|
32
|
+
<div className="browser-window__content">{children}</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|