@zpress/ui 0.3.0 → 0.4.0

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.
Files changed (29) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.mjs +45 -4
  3. package/dist/theme/components/nav/layout.tsx +4 -4
  4. package/dist/theme/components/nav/theme-switcher.css +92 -0
  5. package/dist/theme/components/nav/theme-switcher.tsx +135 -0
  6. package/dist/theme/components/shared/browser-window.css +61 -0
  7. package/dist/theme/components/shared/browser-window.tsx +35 -0
  8. package/dist/theme/components/theme-provider.tsx +159 -0
  9. package/dist/theme/index.tsx +7 -0
  10. package/dist/theme/styles/overrides/rspress.css +3 -72
  11. package/dist/theme/styles/overrides/tokens.css +3 -34
  12. package/dist/theme/styles/themes/arcade-fx.css +360 -0
  13. package/dist/theme/styles/themes/arcade.css +96 -0
  14. package/dist/theme/styles/themes/base.css +133 -0
  15. package/dist/theme/styles/themes/midnight.css +96 -0
  16. package/package.json +2 -2
  17. package/src/theme/components/nav/layout.tsx +4 -4
  18. package/src/theme/components/nav/theme-switcher.css +92 -0
  19. package/src/theme/components/nav/theme-switcher.tsx +135 -0
  20. package/src/theme/components/shared/browser-window.css +61 -0
  21. package/src/theme/components/shared/browser-window.tsx +35 -0
  22. package/src/theme/components/theme-provider.tsx +159 -0
  23. package/src/theme/index.tsx +7 -0
  24. package/src/theme/styles/overrides/rspress.css +3 -72
  25. package/src/theme/styles/overrides/tokens.css +3 -34
  26. package/src/theme/styles/themes/arcade-fx.css +360 -0
  27. package/src/theme/styles/themes/arcade.css +96 -0
  28. package/src/theme/styles/themes/base.css +133 -0
  29. package/src/theme/styles/themes/midnight.css +96 -0
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare function createRspressConfig(options: CreateRspressConfigOptions)
12
12
  declare interface CreateRspressConfigOptions {
13
13
  readonly config: ZpressConfig;
14
14
  readonly paths: Paths;
15
+ readonly logLevel?: 'info' | 'warn' | 'error' | 'silent';
15
16
  }
16
17
 
17
18
  /**
@@ -21,6 +22,8 @@ declare interface CreateRspressConfigOptions {
21
22
  * CSS import — not through the plugin globalStyles property.
22
23
  * Nav-level components (e.g. BranchTag) are injected via layout
23
24
  * slot props in the custom Layout component, not globalUIComponents.
25
+ * ThemeProvider is registered as a globalUIComponent to configure
26
+ * the active theme on every page.
24
27
  */
25
28
  export declare function zpressPlugin(): RspressPlugin;
26
29
 
package/dist/index.mjs CHANGED
@@ -1,9 +1,14 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import node_path from "node:path";
4
+ import { resolveDefaultColorMode } from "@zpress/core";
4
5
  function zpressPlugin() {
6
+ const componentsDir = node_path.resolve(import.meta.dirname, 'theme', 'components');
5
7
  return {
6
- name: 'zpress'
8
+ name: 'zpress',
9
+ globalUIComponents: [
10
+ node_path.resolve(componentsDir, 'theme-provider.tsx')
11
+ ]
7
12
  };
8
13
  }
9
14
  function loadGenerated(contentDir, name, fallback) {
@@ -24,12 +29,37 @@ function detectGitBranch() {
24
29
  return '';
25
30
  }
26
31
  }
32
+ function resolveThemeName(config) {
33
+ if (config.theme && config.theme.name) return config.theme.name;
34
+ return 'base';
35
+ }
36
+ function resolveColorMode(config, themeName) {
37
+ if (config.theme && config.theme.colorMode) return config.theme.colorMode;
38
+ return resolveDefaultColorMode(themeName);
39
+ }
40
+ function resolveThemeSwitcher(config) {
41
+ if (config.theme && config.theme.switcher) return config.theme.switcher;
42
+ return false;
43
+ }
44
+ function resolveThemeColors(config) {
45
+ if (config.theme && config.theme.colors) return config.theme.colors;
46
+ return {};
47
+ }
48
+ function resolveThemeDarkColors(config) {
49
+ if (config.theme && config.theme.darkColors) return config.theme.darkColors;
50
+ return {};
51
+ }
27
52
  function createRspressConfig(options) {
28
- const { config, paths } = options;
53
+ const { config, paths, logLevel } = options;
29
54
  const sidebar = loadGenerated(paths.contentDir, 'sidebar.json', {});
30
55
  const nav = loadGenerated(paths.contentDir, 'nav.json', []);
31
56
  const workspaces = loadGenerated(paths.contentDir, 'workspaces.json', []);
32
57
  const gitBranch = detectGitBranch();
58
+ const themeName = resolveThemeName(config);
59
+ const colorMode = resolveColorMode(config, themeName);
60
+ const themeSwitcher = resolveThemeSwitcher(config);
61
+ const themeColors = resolveThemeColors(config);
62
+ const themeDarkColors = resolveThemeDarkColors(config);
33
63
  return {
34
64
  root: paths.contentDir,
35
65
  outDir: paths.distDir,
@@ -47,6 +77,12 @@ function createRspressConfig(options) {
47
77
  zpressPlugin()
48
78
  ],
49
79
  builderConfig: {
80
+ ...(()=>{
81
+ if (logLevel) return {
82
+ logLevel
83
+ };
84
+ return {};
85
+ })(),
50
86
  resolve: {
51
87
  alias: {
52
88
  '@zpress/ui/theme': node_path.resolve(import.meta.dirname, 'theme', 'index.tsx')
@@ -54,7 +90,12 @@ function createRspressConfig(options) {
54
90
  },
55
91
  source: {
56
92
  define: {
57
- __ZPRESS_GIT_BRANCH__: JSON.stringify(gitBranch)
93
+ __ZPRESS_GIT_BRANCH__: JSON.stringify(gitBranch),
94
+ __ZPRESS_THEME_NAME__: JSON.stringify(themeName),
95
+ __ZPRESS_COLOR_MODE__: JSON.stringify(colorMode),
96
+ __ZPRESS_THEME_COLORS__: JSON.stringify(JSON.stringify(themeColors)),
97
+ __ZPRESS_THEME_DARK_COLORS__: JSON.stringify(JSON.stringify(themeDarkColors)),
98
+ __ZPRESS_THEME_SWITCHER__: JSON.stringify(themeSwitcher)
58
99
  }
59
100
  },
60
101
  output: {
@@ -66,7 +107,7 @@ function createRspressConfig(options) {
66
107
  themeConfig: {
67
108
  sidebar,
68
109
  nav,
69
- darkMode: true,
110
+ darkMode: 'toggle' === colorMode,
70
111
  search: true,
71
112
  workspaces
72
113
  }
@@ -2,13 +2,13 @@ import { Layout as OriginalLayout } from '@rspress/core/theme-original'
2
2
  import type React from 'react'
3
3
 
4
4
  import { BranchTag } from './branch-tag'
5
+ import { ThemeSwitcher } from './theme-switcher'
5
6
 
6
7
  /**
7
8
  * Custom Layout override for zpress.
8
- * Wraps the original Rspress Layout and injects the BranchTag
9
- * into the `beforeNavMenu` slot so it renders before the search
10
- * bar in the navbar on all page types.
9
+ * Wraps the original Rspress Layout and injects BranchTag
10
+ * into `beforeNavMenu` and ThemeSwitcher into `afterNavMenu`.
11
11
  */
12
12
  export function Layout(): React.ReactElement {
13
- return <OriginalLayout beforeNavMenu={<BranchTag />} />
13
+ return <OriginalLayout beforeNavMenu={<BranchTag />} afterNavMenu={<ThemeSwitcher />} />
14
14
  }
@@ -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,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
+ }
@@ -0,0 +1,159 @@
1
+ import { useEffect } from 'react'
2
+ import type React from 'react'
3
+
4
+ declare const __ZPRESS_THEME_NAME__: string
5
+ declare const __ZPRESS_COLOR_MODE__: string
6
+ declare const __ZPRESS_THEME_COLORS__: string
7
+ declare const __ZPRESS_THEME_DARK_COLORS__: string
8
+
9
+ const COLOR_VAR_MAP: Record<string, readonly string[]> = {
10
+ brand: ['--zp-c-brand-1', '--rp-c-brand'],
11
+ brandLight: ['--rp-c-brand-light'],
12
+ brandDark: ['--zp-c-brand-2', '--rp-c-brand-dark'],
13
+ brandSoft: ['--zp-c-brand-soft', '--rp-c-brand-tint'],
14
+ bg: ['--zp-c-bg', '--rp-c-bg'],
15
+ bgAlt: ['--zp-c-bg-alt'],
16
+ bgElv: ['--zp-c-bg-elv'],
17
+ bgSoft: ['--zp-c-bg-soft', '--rp-c-bg-soft'],
18
+ text1: ['--zp-c-text-1', '--rp-c-text-1'],
19
+ text2: ['--zp-c-text-2', '--rp-c-text-2'],
20
+ text3: ['--zp-c-text-3', '--rp-c-text-3'],
21
+ divider: ['--zp-c-divider', '--rp-c-divider'],
22
+ border: ['--zp-c-border'],
23
+ homeBg: ['--rp-home-background-bg'],
24
+ }
25
+
26
+ /**
27
+ * Build a flat list of [cssVar, value] pairs from a color overrides object.
28
+ */
29
+ function resolveColorPairs(colors: Record<string, string>): readonly (readonly [string, string])[] {
30
+ return Object.entries(colors).flatMap(([key, value]) => {
31
+ const vars = COLOR_VAR_MAP[key]
32
+ if (!vars) {
33
+ return []
34
+ }
35
+ return vars.map((cssVar) => [cssVar, value] as const)
36
+ })
37
+ }
38
+
39
+ /**
40
+ * Apply a ThemeColors object as inline CSS custom properties on <html>.
41
+ */
42
+ function applyColorOverrides(html: HTMLElement, colors: Record<string, string>): void {
43
+ const pairs = resolveColorPairs(colors)
44
+ // oxlint-disable-next-line unicorn/no-array-for-each -- DOM side effect; for-loops also banned
45
+ pairs.forEach(([cssVar, value]) => {
46
+ html.style.setProperty(cssVar, value)
47
+ })
48
+ }
49
+
50
+ /**
51
+ * Collect all CSS variable names from the color map.
52
+ */
53
+ const ALL_CSS_VARS: readonly string[] = Object.values(COLOR_VAR_MAP).flat()
54
+
55
+ /**
56
+ * Remove all color overrides previously set as inline styles.
57
+ */
58
+ function clearColorOverrides(html: HTMLElement): void {
59
+ // oxlint-disable-next-line unicorn/no-array-for-each -- DOM side effect; for-loops also banned
60
+ ALL_CSS_VARS.forEach((cssVar) => {
61
+ html.style.removeProperty(cssVar)
62
+ })
63
+ }
64
+
65
+ /**
66
+ * Safe localStorage read — returns null if storage is unavailable.
67
+ */
68
+ function safeGetItem(key: string): string | null {
69
+ try {
70
+ return localStorage.getItem(key)
71
+ } catch {
72
+ return null
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Parse a JSON build-time define, returning an empty object on failure.
78
+ */
79
+ function parseColors(raw: string): Record<string, string> {
80
+ if (!raw || raw === '""' || raw === 'undefined') {
81
+ return {}
82
+ }
83
+ try {
84
+ return JSON.parse(raw) as Record<string, string>
85
+ } catch {
86
+ return {}
87
+ }
88
+ }
89
+
90
+ /**
91
+ * ThemeProvider — global UI component that configures the active theme.
92
+ *
93
+ * Sets `data-zp-theme` attribute, forces color mode, and applies
94
+ * inline CSS custom property overrides from build-time defines.
95
+ */
96
+ export function ThemeProvider(): React.ReactElement | null {
97
+ useEffect(() => {
98
+ const html = document.documentElement
99
+ const themeName = safeGetItem('zpress-theme') || __ZPRESS_THEME_NAME__
100
+ const colorMode = __ZPRESS_COLOR_MODE__
101
+ const colors = parseColors(__ZPRESS_THEME_COLORS__)
102
+ const darkColors = parseColors(__ZPRESS_THEME_DARK_COLORS__)
103
+ const hasColors = Object.keys(colors).length > 0
104
+ const hasDarkColors = Object.keys(darkColors).length > 0
105
+
106
+ // 1. Set theme attribute
107
+ html.dataset.zpTheme = themeName
108
+
109
+ // 2. Force color mode if not toggle
110
+ if (colorMode === 'dark') {
111
+ html.classList.add('rp-dark')
112
+ html.dataset.dark = 'true'
113
+ localStorage.setItem('rspress-theme', 'dark')
114
+ } else if (colorMode === 'light') {
115
+ html.classList.remove('rp-dark')
116
+ html.dataset.dark = 'false'
117
+ localStorage.setItem('rspress-theme', 'light')
118
+ }
119
+
120
+ // 3. Apply base color overrides
121
+ if (hasColors) {
122
+ applyColorOverrides(html, colors)
123
+ }
124
+
125
+ // 4. Observe dark mode changes for dark-specific overrides
126
+ if (hasDarkColors) {
127
+ const isDark = html.classList.contains('rp-dark')
128
+ if (isDark) {
129
+ applyColorOverrides(html, darkColors)
130
+ }
131
+
132
+ const observer = new MutationObserver((mutations) => {
133
+ const classChanged = mutations.some((m) => m.attributeName === 'class')
134
+ if (classChanged) {
135
+ const nowDark = html.classList.contains('rp-dark')
136
+ clearColorOverrides(html)
137
+ if (hasColors) {
138
+ applyColorOverrides(html, colors)
139
+ }
140
+ if (nowDark && hasDarkColors) {
141
+ applyColorOverrides(html, darkColors)
142
+ }
143
+ }
144
+ })
145
+
146
+ observer.observe(html, { attributes: true, attributeFilter: ['class'] })
147
+
148
+ return () => {
149
+ observer.disconnect()
150
+ }
151
+ }
152
+
153
+ // no cleanup needed when dark color overrides are absent
154
+ }, [])
155
+
156
+ return null
157
+ }
158
+
159
+ export { ThemeProvider as default }
@@ -12,6 +12,11 @@
12
12
  import './styles/overrides/fonts.css'
13
13
  import './styles/overrides/tokens.css'
14
14
  import './styles/overrides/rspress.css'
15
+ // Theme color palettes — scoped via [data-zp-theme] selectors
16
+ import './styles/themes/base.css'
17
+ import './styles/themes/midnight.css'
18
+ import './styles/themes/arcade.css'
19
+ import './styles/themes/arcade-fx.css'
15
20
  import './styles/overrides/details.css'
16
21
  import './styles/overrides/scrollbar.css'
17
22
  import './styles/overrides/sidebar.css'
@@ -43,6 +48,8 @@ export type { TechTagProps } from './components/shared/tech-tag'
43
48
  export { TechIconTable } from './components/shared/tech-icon-table'
44
49
  export type { TechIconEntry, TechIconTableProps } from './components/shared/tech-icon-table'
45
50
  export { Icon } from './components/shared/icon'
51
+ export { BrowserWindow } from './components/shared/browser-window'
52
+ export type { BrowserWindowProps } from './components/shared/browser-window'
46
53
 
47
54
  // Home page overrides — shadow the wildcard re-exports from theme-original
48
55
  export { HomeFeature } from './components/home/feature'