@tooee/themes 0.2.4 → 0.3.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.
- package/dist/{CloseButton.d.ts → close-button.d.ts} +2 -2
- package/dist/close-button.d.ts.map +1 -0
- package/dist/{CloseButton.js → close-button.js} +5 -4
- package/dist/close-button.js.map +1 -0
- package/dist/context.d.ts +7 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +28 -26
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +5 -5
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +80 -74
- package/dist/loader.js.map +1 -1
- package/dist/syntax-rules.d.ts +1 -1
- package/dist/syntax-rules.d.ts.map +1 -1
- package/dist/syntax-rules.js +17 -17
- package/dist/syntax-rules.js.map +1 -1
- package/dist/{ThemePicker.d.ts → theme-picker.d.ts} +2 -2
- package/dist/theme-picker.d.ts.map +1 -0
- package/dist/{ThemePicker.js → theme-picker.js} +37 -33
- package/dist/theme-picker.js.map +1 -0
- package/dist/types.d.ts +7 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +52 -43
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
- package/src/{CloseButton.tsx → close-button.tsx} +21 -15
- package/src/context.tsx +83 -74
- package/src/index.ts +10 -10
- package/src/loader.ts +109 -95
- package/src/syntax-rules.ts +21 -21
- package/src/theme-picker.tsx +189 -0
- package/src/types.ts +129 -110
- package/dist/CloseButton.d.ts.map +0 -1
- package/dist/CloseButton.js.map +0 -1
- package/dist/ThemePicker.d.ts.map +0 -1
- package/dist/ThemePicker.js.map +0 -1
- package/src/ThemePicker.tsx +0 -177
package/src/context.tsx
CHANGED
|
@@ -1,148 +1,157 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useCallback, useMemo
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useMemo } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { SyntaxStyle } from "@opentui/core";
|
|
4
|
+
import { writeGlobalConfig } from "@tooee/config";
|
|
5
|
+
import type { ResolvedTheme } from "./types.js";
|
|
5
6
|
import {
|
|
6
|
-
type Theme,
|
|
7
7
|
buildTheme,
|
|
8
8
|
getThemeNames,
|
|
9
9
|
defaultTheme,
|
|
10
10
|
DEFAULT_THEME_NAME,
|
|
11
11
|
DEFAULT_MODE,
|
|
12
|
-
} from "./loader.js"
|
|
12
|
+
} from "./loader.js";
|
|
13
|
+
import type { Theme } from "./loader.js";
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Context
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
|
|
18
19
|
interface ThemeContextValue {
|
|
19
|
-
theme: ResolvedTheme
|
|
20
|
-
syntax: SyntaxStyle
|
|
21
|
-
name: string
|
|
22
|
-
mode: "dark" | "light"
|
|
20
|
+
theme: ResolvedTheme;
|
|
21
|
+
syntax: SyntaxStyle;
|
|
22
|
+
name: string;
|
|
23
|
+
mode: "dark" | "light";
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const ThemeContext = createContext<ThemeContextValue>({
|
|
26
|
-
theme: defaultTheme.colors,
|
|
27
|
-
syntax: defaultTheme.syntax,
|
|
28
|
-
name: defaultTheme.name,
|
|
29
27
|
mode: defaultTheme.mode,
|
|
30
|
-
|
|
28
|
+
name: defaultTheme.name,
|
|
29
|
+
syntax: defaultTheme.syntax,
|
|
30
|
+
theme: defaultTheme.colors,
|
|
31
|
+
});
|
|
31
32
|
|
|
32
33
|
export interface ThemeProviderProps {
|
|
33
34
|
/** Theme name (e.g. "tokyonight", "catppuccin", "dracula") */
|
|
34
|
-
name?: string
|
|
35
|
+
name?: string;
|
|
35
36
|
/** Color mode */
|
|
36
|
-
mode?: "dark" | "light"
|
|
37
|
+
mode?: "dark" | "light";
|
|
37
38
|
/** Full Theme object (overrides name/mode if provided) */
|
|
38
|
-
theme?: Theme
|
|
39
|
-
children: ReactNode
|
|
39
|
+
theme?: Theme;
|
|
40
|
+
children: ReactNode;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export
|
|
43
|
+
export const ThemeProvider = function ThemeProvider({
|
|
44
|
+
name,
|
|
45
|
+
mode,
|
|
46
|
+
theme: themeProp,
|
|
47
|
+
children,
|
|
48
|
+
}: ThemeProviderProps): ReactNode {
|
|
43
49
|
const resolved = useMemo<ThemeContextValue>(() => {
|
|
44
50
|
if (themeProp) {
|
|
45
51
|
return {
|
|
46
|
-
theme: themeProp.colors,
|
|
47
|
-
syntax: themeProp.syntax,
|
|
48
|
-
name: themeProp.name,
|
|
49
52
|
mode: themeProp.mode,
|
|
50
|
-
|
|
53
|
+
name: themeProp.name,
|
|
54
|
+
syntax: themeProp.syntax,
|
|
55
|
+
theme: themeProp.colors,
|
|
56
|
+
};
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
const t = buildTheme(name ?? DEFAULT_THEME_NAME, mode ?? DEFAULT_MODE)
|
|
54
|
-
return {
|
|
55
|
-
}, [themeProp, name, mode])
|
|
59
|
+
const t = buildTheme(name ?? DEFAULT_THEME_NAME, mode ?? DEFAULT_MODE);
|
|
60
|
+
return { mode: t.mode, name: t.name, syntax: t.syntax, theme: t.colors };
|
|
61
|
+
}, [themeProp, name, mode]);
|
|
56
62
|
|
|
57
|
-
return <ThemeContext.Provider value={resolved}>{children}</ThemeContext.Provider
|
|
58
|
-
}
|
|
63
|
+
return <ThemeContext.Provider value={resolved}>{children}</ThemeContext.Provider>;
|
|
64
|
+
};
|
|
59
65
|
|
|
60
|
-
export function useTheme(): ThemeContextValue {
|
|
61
|
-
return useContext(ThemeContext)
|
|
62
|
-
}
|
|
66
|
+
export const useTheme = function useTheme(): ThemeContextValue {
|
|
67
|
+
return useContext(ThemeContext);
|
|
68
|
+
};
|
|
63
69
|
|
|
64
70
|
// ---------------------------------------------------------------------------
|
|
65
71
|
// ThemeSwitcherProvider + useThemeSwitcher
|
|
66
72
|
// ---------------------------------------------------------------------------
|
|
67
73
|
|
|
68
74
|
interface ThemeSwitcherContextValue extends ThemeContextValue {
|
|
69
|
-
nextTheme: () => void
|
|
70
|
-
prevTheme: () => void
|
|
71
|
-
setTheme: (name: string, opts?: { persist?: boolean }) => void
|
|
72
|
-
allThemes: string[]
|
|
75
|
+
nextTheme: () => void;
|
|
76
|
+
prevTheme: () => void;
|
|
77
|
+
setTheme: (name: string, opts?: { persist?: boolean }) => void;
|
|
78
|
+
allThemes: string[];
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
const ThemeSwitcherContext = createContext<ThemeSwitcherContextValue | null>(null)
|
|
81
|
+
const ThemeSwitcherContext = createContext<ThemeSwitcherContextValue | null>(null);
|
|
76
82
|
|
|
77
83
|
export interface ThemeSwitcherProviderProps {
|
|
78
|
-
initialTheme?: string
|
|
79
|
-
initialMode?: "dark" | "light"
|
|
80
|
-
children: ReactNode
|
|
84
|
+
initialTheme?: string;
|
|
85
|
+
initialMode?: "dark" | "light";
|
|
86
|
+
children: ReactNode;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
export function ThemeSwitcherProvider({
|
|
89
|
+
export const ThemeSwitcherProvider = function ThemeSwitcherProvider({
|
|
84
90
|
initialTheme,
|
|
85
91
|
initialMode,
|
|
86
92
|
children,
|
|
87
|
-
}: ThemeSwitcherProviderProps) {
|
|
88
|
-
const allThemes = useMemo(() => getThemeNames(), [])
|
|
89
|
-
const [themeName, setThemeName] = useState(initialTheme ?? DEFAULT_THEME_NAME)
|
|
90
|
-
const [mode,
|
|
93
|
+
}: ThemeSwitcherProviderProps): ReactNode {
|
|
94
|
+
const allThemes = useMemo(() => getThemeNames(), []);
|
|
95
|
+
const [themeName, setThemeName] = useState(initialTheme ?? DEFAULT_THEME_NAME);
|
|
96
|
+
const [mode, setMode] = useState<"dark" | "light">(initialMode ?? DEFAULT_MODE);
|
|
97
|
+
void setMode;
|
|
91
98
|
|
|
92
|
-
const theme = useMemo(() => buildTheme(themeName, mode), [themeName, mode])
|
|
99
|
+
const theme = useMemo(() => buildTheme(themeName, mode), [themeName, mode]);
|
|
93
100
|
|
|
94
101
|
const nextTheme = useCallback(() => {
|
|
95
|
-
const idx = allThemes.indexOf(themeName)
|
|
96
|
-
const next = allThemes[(idx + 1) % allThemes.length]
|
|
97
|
-
setThemeName(next)
|
|
98
|
-
writeGlobalConfig({ theme: { name: next
|
|
99
|
-
}, [allThemes, mode, themeName])
|
|
102
|
+
const idx = allThemes.indexOf(themeName);
|
|
103
|
+
const next = allThemes[(idx + 1) % allThemes.length];
|
|
104
|
+
setThemeName(next);
|
|
105
|
+
writeGlobalConfig({ theme: { mode, name: next } });
|
|
106
|
+
}, [allThemes, mode, themeName]);
|
|
100
107
|
|
|
101
108
|
const prevTheme = useCallback(() => {
|
|
102
|
-
const idx = allThemes.indexOf(themeName)
|
|
103
|
-
const prev = allThemes[(idx - 1 + allThemes.length) % allThemes.length]
|
|
104
|
-
setThemeName(prev)
|
|
105
|
-
writeGlobalConfig({ theme: { name: prev
|
|
106
|
-
}, [allThemes, mode, themeName])
|
|
109
|
+
const idx = allThemes.indexOf(themeName);
|
|
110
|
+
const prev = allThemes[(idx - 1 + allThemes.length) % allThemes.length];
|
|
111
|
+
setThemeName(prev);
|
|
112
|
+
writeGlobalConfig({ theme: { mode, name: prev } });
|
|
113
|
+
}, [allThemes, mode, themeName]);
|
|
107
114
|
|
|
108
115
|
const setThemeByName = useCallback(
|
|
109
116
|
(name: string, opts?: { persist?: boolean }) => {
|
|
110
|
-
setThemeName(name)
|
|
111
|
-
if (opts?.persist) {
|
|
112
|
-
writeGlobalConfig({ theme: {
|
|
117
|
+
setThemeName(name);
|
|
118
|
+
if (opts?.persist === true) {
|
|
119
|
+
writeGlobalConfig({ theme: { mode, name } });
|
|
113
120
|
}
|
|
114
121
|
},
|
|
115
122
|
[mode],
|
|
116
|
-
)
|
|
123
|
+
);
|
|
117
124
|
|
|
118
125
|
const value = useMemo<ThemeSwitcherContextValue>(
|
|
119
126
|
() => ({
|
|
120
|
-
|
|
121
|
-
syntax: theme.syntax,
|
|
122
|
-
name: theme.name,
|
|
127
|
+
allThemes,
|
|
123
128
|
mode,
|
|
129
|
+
name: theme.name,
|
|
124
130
|
nextTheme,
|
|
125
131
|
prevTheme,
|
|
126
132
|
setTheme: setThemeByName,
|
|
127
|
-
|
|
133
|
+
syntax: theme.syntax,
|
|
134
|
+
theme: theme.colors,
|
|
128
135
|
}),
|
|
129
136
|
[theme, mode, nextTheme, prevTheme, setThemeByName, allThemes],
|
|
130
|
-
)
|
|
137
|
+
);
|
|
131
138
|
|
|
132
139
|
const themeValue = useMemo<ThemeContextValue>(
|
|
133
|
-
() => ({
|
|
140
|
+
() => ({ mode, name: theme.name, syntax: theme.syntax, theme: theme.colors }),
|
|
134
141
|
[theme, mode],
|
|
135
|
-
)
|
|
142
|
+
);
|
|
136
143
|
|
|
137
144
|
return (
|
|
138
145
|
<ThemeSwitcherContext.Provider value={value}>
|
|
139
146
|
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
|
|
140
147
|
</ThemeSwitcherContext.Provider>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function useThemeSwitcher(): ThemeSwitcherContextValue {
|
|
145
|
-
const ctx = useContext(ThemeSwitcherContext)
|
|
146
|
-
if (!ctx)
|
|
147
|
-
|
|
148
|
-
}
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const useThemeSwitcher = function useThemeSwitcher(): ThemeSwitcherContextValue {
|
|
152
|
+
const ctx = useContext(ThemeSwitcherContext);
|
|
153
|
+
if (!ctx) {
|
|
154
|
+
throw new Error("useThemeSwitcher must be used within ThemeSwitcherProvider");
|
|
155
|
+
}
|
|
156
|
+
return ctx;
|
|
157
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
export { resolveTheme } from "./types.js"
|
|
2
|
-
export type { ThemeJSON, ResolvedTheme } from "./types.js"
|
|
1
|
+
export { resolveTheme } from "./types.js";
|
|
2
|
+
export type { ThemeJSON, ResolvedTheme } from "./types.js";
|
|
3
3
|
|
|
4
|
-
export { buildSyntaxStyle } from "./syntax-rules.js"
|
|
4
|
+
export { buildSyntaxStyle } from "./syntax-rules.js";
|
|
5
5
|
|
|
6
|
-
export { loadThemes, getThemeNames, defaultTheme } from "./loader.js"
|
|
7
|
-
export type { Theme } from "./loader.js"
|
|
6
|
+
export { loadThemes, getThemeNames, defaultTheme } from "./loader.js";
|
|
7
|
+
export type { Theme } from "./loader.js";
|
|
8
8
|
|
|
9
|
-
export { ThemeProvider, ThemeSwitcherProvider, useTheme, useThemeSwitcher } from "./context.js"
|
|
10
|
-
export type { ThemeProviderProps, ThemeSwitcherProviderProps } from "./context.js"
|
|
9
|
+
export { ThemeProvider, ThemeSwitcherProvider, useTheme, useThemeSwitcher } from "./context.js";
|
|
10
|
+
export type { ThemeProviderProps, ThemeSwitcherProviderProps } from "./context.js";
|
|
11
11
|
|
|
12
|
-
export { ThemePicker } from "./
|
|
13
|
-
export type { ThemePickerEntry } from "./
|
|
12
|
+
export { ThemePicker } from "./theme-picker.js";
|
|
13
|
+
export type { ThemePickerEntry } from "./theme-picker.js";
|
|
14
14
|
|
|
15
|
-
export { CloseButton } from "./
|
|
15
|
+
export { CloseButton } from "./close-button.js";
|
package/src/loader.ts
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFileSync, readdirSync, existsSync } from "fs"
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import type { SyntaxStyle } from "@opentui/core";
|
|
2
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveTheme } from "./types.js";
|
|
5
|
+
import type { ThemeJSON, ResolvedTheme } from "./types.js";
|
|
6
|
+
import { buildSyntaxStyle } from "./syntax-rules.js";
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Theme loading
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
|
|
11
12
|
export interface Theme {
|
|
12
|
-
name: string
|
|
13
|
-
mode: "dark" | "light"
|
|
14
|
-
colors: ResolvedTheme
|
|
15
|
-
syntax: SyntaxStyle
|
|
13
|
+
name: string;
|
|
14
|
+
mode: "dark" | "light";
|
|
15
|
+
colors: ResolvedTheme;
|
|
16
|
+
syntax: SyntaxStyle;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/** Cache of loaded theme JSONs by name */
|
|
19
|
-
const themeJsonCache = new Map<string, ThemeJSON>()
|
|
20
|
+
const themeJsonCache = new Map<string, ThemeJSON>();
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
const loadJsonThemesFromDir = function loadJsonThemesFromDir(
|
|
23
|
+
dir: string,
|
|
24
|
+
target: Map<string, ThemeJSON>,
|
|
25
|
+
) {
|
|
22
26
|
try {
|
|
23
|
-
if (!existsSync(dir))
|
|
27
|
+
if (!existsSync(dir)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
24
30
|
for (const file of readdirSync(dir)) {
|
|
25
|
-
if (!file.endsWith(".json"))
|
|
26
|
-
|
|
31
|
+
if (!file.endsWith(".json")) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const name = path.basename(file, ".json");
|
|
27
35
|
try {
|
|
28
|
-
const content = readFileSync(join(dir, file), "utf-8")
|
|
29
|
-
|
|
36
|
+
const content = readFileSync(path.join(dir, file), "utf-8");
|
|
37
|
+
// Deferred(lint-sweep): replace with schema-based validation of untrusted JSON
|
|
38
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- untrusted external JSON, validated in a later sweep
|
|
39
|
+
target.set(name, JSON.parse(content) as ThemeJSON);
|
|
30
40
|
} catch {
|
|
31
41
|
// skip invalid files
|
|
32
42
|
}
|
|
@@ -34,126 +44,130 @@ function loadJsonThemesFromDir(dir: string, target: Map<string, ThemeJSON>) {
|
|
|
34
44
|
} catch {
|
|
35
45
|
// dir not readable
|
|
36
46
|
}
|
|
37
|
-
}
|
|
47
|
+
};
|
|
38
48
|
|
|
39
49
|
/** Load all bundled themes from packages/themes/themes/ */
|
|
40
|
-
function loadBundledThemes(): Map<string, ThemeJSON> {
|
|
41
|
-
if (themeJsonCache.size > 0)
|
|
50
|
+
const loadBundledThemes = function loadBundledThemes(): Map<string, ThemeJSON> {
|
|
51
|
+
if (themeJsonCache.size > 0) {
|
|
52
|
+
return themeJsonCache;
|
|
53
|
+
}
|
|
42
54
|
|
|
43
55
|
// Bundled themes
|
|
44
|
-
const bundledDir = join(dirname(new URL(import.meta.url).pathname), "..", "themes")
|
|
45
|
-
loadJsonThemesFromDir(bundledDir, themeJsonCache)
|
|
56
|
+
const bundledDir = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "themes");
|
|
57
|
+
loadJsonThemesFromDir(bundledDir, themeJsonCache);
|
|
46
58
|
|
|
47
59
|
// XDG config: ~/.config/tooee/themes/
|
|
48
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? "", ".config")
|
|
49
|
-
loadJsonThemesFromDir(join(xdgConfig, "tooee", "themes"), themeJsonCache)
|
|
60
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? path.join(process.env.HOME ?? "", ".config");
|
|
61
|
+
loadJsonThemesFromDir(path.join(xdgConfig, "tooee", "themes"), themeJsonCache);
|
|
50
62
|
|
|
51
63
|
// Project-local: search upward for .tooee/themes/
|
|
52
|
-
let dir = process.cwd()
|
|
53
|
-
const seen = new Set<string>()
|
|
64
|
+
let dir = process.cwd();
|
|
65
|
+
const seen = new Set<string>();
|
|
54
66
|
while (dir && !seen.has(dir)) {
|
|
55
|
-
seen.add(dir)
|
|
56
|
-
loadJsonThemesFromDir(join(dir, ".tooee", "themes"), themeJsonCache)
|
|
57
|
-
const parent = dirname(dir)
|
|
58
|
-
if (parent === dir)
|
|
59
|
-
|
|
67
|
+
seen.add(dir);
|
|
68
|
+
loadJsonThemesFromDir(path.join(dir, ".tooee", "themes"), themeJsonCache);
|
|
69
|
+
const parent = path.dirname(dir);
|
|
70
|
+
if (parent === dir) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
dir = parent;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
|
-
return themeJsonCache
|
|
63
|
-
}
|
|
76
|
+
return themeJsonCache;
|
|
77
|
+
};
|
|
64
78
|
|
|
65
|
-
export function loadThemes(): Map<string, ThemeJSON> {
|
|
66
|
-
return loadBundledThemes()
|
|
67
|
-
}
|
|
79
|
+
export const loadThemes = function loadThemes(): Map<string, ThemeJSON> {
|
|
80
|
+
return loadBundledThemes();
|
|
81
|
+
};
|
|
68
82
|
|
|
69
|
-
export function getThemeNames(): string[] {
|
|
70
|
-
return
|
|
71
|
-
}
|
|
83
|
+
export const getThemeNames = function getThemeNames(): string[] {
|
|
84
|
+
return [...loadThemes().keys()].toSorted();
|
|
85
|
+
};
|
|
72
86
|
|
|
73
87
|
// ---------------------------------------------------------------------------
|
|
74
88
|
// Default theme
|
|
75
89
|
// ---------------------------------------------------------------------------
|
|
76
90
|
|
|
77
|
-
export const DEFAULT_THEME_NAME = "tokyonight"
|
|
78
|
-
export const DEFAULT_MODE: "dark" | "light" = "dark"
|
|
79
|
-
|
|
80
|
-
export function buildTheme(name: string, mode: "dark" | "light"): Theme {
|
|
81
|
-
const themes = loadThemes()
|
|
82
|
-
const json = themes.get(name)
|
|
83
|
-
if (!json) {
|
|
84
|
-
// Fall back to tokyonight, then first available, then hardcoded
|
|
85
|
-
const fallbackJson = themes.get(DEFAULT_THEME_NAME) ?? themes.values().next().value
|
|
86
|
-
if (fallbackJson) {
|
|
87
|
-
const resolved = resolveTheme(fallbackJson, mode)
|
|
88
|
-
return { name, mode, colors: resolved, syntax: buildSyntaxStyle(resolved) }
|
|
89
|
-
}
|
|
90
|
-
// Absolute fallback — hardcoded Tokyo Night colors
|
|
91
|
-
return hardcodedDefaultTheme
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
const resolved = resolveTheme(json, mode)
|
|
95
|
-
return { name, mode, colors: resolved, syntax: buildSyntaxStyle(resolved) }
|
|
96
|
-
} catch {
|
|
97
|
-
return hardcodedDefaultTheme
|
|
98
|
-
}
|
|
99
|
-
}
|
|
91
|
+
export const DEFAULT_THEME_NAME = "tokyonight";
|
|
92
|
+
export const DEFAULT_MODE: "dark" | "light" = "dark";
|
|
100
93
|
|
|
101
94
|
const hardcodedDefaultTheme: Theme = (() => {
|
|
102
95
|
const colors: ResolvedTheme = {
|
|
103
|
-
primary: "#7aa2f7",
|
|
104
|
-
secondary: "#bb9af7",
|
|
105
96
|
accent: "#7dcfff",
|
|
106
|
-
error: "#f7768e",
|
|
107
|
-
warning: "#e0af68",
|
|
108
|
-
success: "#9ece6a",
|
|
109
|
-
info: "#7aa2f7",
|
|
110
|
-
text: "#c0caf5",
|
|
111
|
-
textMuted: "#565f89",
|
|
112
97
|
background: "#1a1b26",
|
|
113
|
-
backgroundPanel: "#1e2030",
|
|
114
98
|
backgroundElement: "#222436",
|
|
115
|
-
|
|
116
|
-
selection: "#1e2030",
|
|
99
|
+
backgroundPanel: "#1e2030",
|
|
117
100
|
border: "#565f89",
|
|
118
101
|
borderActive: "#737aa2",
|
|
119
102
|
borderSubtle: "#414868",
|
|
103
|
+
cursorLine: "#222436",
|
|
120
104
|
diffAdded: "#4fd6be",
|
|
121
|
-
|
|
105
|
+
diffAddedBg: "#20303b",
|
|
106
|
+
diffAddedLineNumberBg: "#1b2b34",
|
|
122
107
|
diffContext: "#828bb8",
|
|
123
|
-
|
|
108
|
+
diffContextBg: "#1e2030",
|
|
124
109
|
diffHighlightAdded: "#b8db87",
|
|
125
110
|
diffHighlightRemoved: "#e26a75",
|
|
126
|
-
|
|
127
|
-
diffRemovedBg: "#37222c",
|
|
128
|
-
diffContextBg: "#1e2030",
|
|
111
|
+
diffHunkHeader: "#828bb8",
|
|
129
112
|
diffLineNumber: "#222436",
|
|
130
|
-
|
|
113
|
+
diffRemoved: "#c53b53",
|
|
114
|
+
diffRemovedBg: "#37222c",
|
|
131
115
|
diffRemovedLineNumberBg: "#2d1f26",
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
markdownLink: "#7aa2f7",
|
|
135
|
-
markdownLinkText: "#7dcfff",
|
|
136
|
-
markdownCode: "#9ece6a",
|
|
116
|
+
error: "#f7768e",
|
|
117
|
+
info: "#7aa2f7",
|
|
137
118
|
markdownBlockQuote: "#e0af68",
|
|
119
|
+
markdownCode: "#9ece6a",
|
|
120
|
+
markdownCodeBlock: "#c0caf5",
|
|
138
121
|
markdownEmph: "#e0af68",
|
|
139
|
-
|
|
122
|
+
markdownHeading: "#bb9af7",
|
|
140
123
|
markdownHorizontalRule: "#565f89",
|
|
141
|
-
markdownListItem: "#7aa2f7",
|
|
142
|
-
markdownListEnumeration: "#7dcfff",
|
|
143
124
|
markdownImage: "#7aa2f7",
|
|
144
125
|
markdownImageText: "#7dcfff",
|
|
145
|
-
|
|
126
|
+
markdownLink: "#7aa2f7",
|
|
127
|
+
markdownLinkText: "#7dcfff",
|
|
128
|
+
markdownListEnumeration: "#7dcfff",
|
|
129
|
+
markdownListItem: "#7aa2f7",
|
|
130
|
+
markdownStrong: "#ff966c",
|
|
131
|
+
markdownText: "#c0caf5",
|
|
132
|
+
primary: "#7aa2f7",
|
|
133
|
+
secondary: "#bb9af7",
|
|
134
|
+
selection: "#1e2030",
|
|
135
|
+
success: "#9ece6a",
|
|
146
136
|
syntaxComment: "#565f89",
|
|
147
|
-
syntaxKeyword: "#bb9af7",
|
|
148
137
|
syntaxFunction: "#7aa2f7",
|
|
149
|
-
|
|
150
|
-
syntaxString: "#9ece6a",
|
|
138
|
+
syntaxKeyword: "#bb9af7",
|
|
151
139
|
syntaxNumber: "#ff9e64",
|
|
152
|
-
syntaxType: "#2ac3de",
|
|
153
140
|
syntaxOperator: "#89ddff",
|
|
154
141
|
syntaxPunctuation: "#a9b1d6",
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
syntaxString: "#9ece6a",
|
|
143
|
+
syntaxType: "#2ac3de",
|
|
144
|
+
syntaxVariable: "#c0caf5",
|
|
145
|
+
text: "#c0caf5",
|
|
146
|
+
textMuted: "#565f89",
|
|
147
|
+
warning: "#e0af68",
|
|
148
|
+
};
|
|
149
|
+
return { colors, mode: DEFAULT_MODE, name: DEFAULT_THEME_NAME, syntax: buildSyntaxStyle(colors) };
|
|
150
|
+
})();
|
|
151
|
+
|
|
152
|
+
export const defaultTheme: Theme = hardcodedDefaultTheme;
|
|
158
153
|
|
|
159
|
-
export const
|
|
154
|
+
export const buildTheme = function buildTheme(name: string, mode: "dark" | "light"): Theme {
|
|
155
|
+
const themes = loadThemes();
|
|
156
|
+
const json = themes.get(name);
|
|
157
|
+
if (!json) {
|
|
158
|
+
// Fall back to tokyonight, then first available, then hardcoded
|
|
159
|
+
const fallbackJson = themes.get(DEFAULT_THEME_NAME) ?? themes.values().next().value;
|
|
160
|
+
if (fallbackJson) {
|
|
161
|
+
const resolved = resolveTheme(fallbackJson, mode);
|
|
162
|
+
return { colors: resolved, mode, name, syntax: buildSyntaxStyle(resolved) };
|
|
163
|
+
}
|
|
164
|
+
// Absolute fallback — hardcoded Tokyo Night colors
|
|
165
|
+
return hardcodedDefaultTheme;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const resolved = resolveTheme(json, mode);
|
|
169
|
+
return { colors: resolved, mode, name, syntax: buildSyntaxStyle(resolved) };
|
|
170
|
+
} catch {
|
|
171
|
+
return hardcodedDefaultTheme;
|
|
172
|
+
}
|
|
173
|
+
};
|
package/src/syntax-rules.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { RGBA, SyntaxStyle } from "@opentui/core"
|
|
2
|
-
import type { ResolvedTheme } from "./types.js"
|
|
1
|
+
import { RGBA, SyntaxStyle } from "@opentui/core";
|
|
2
|
+
import type { ResolvedTheme } from "./types.js";
|
|
3
3
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// SyntaxStyle builder
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
function getSyntaxRules(resolved: ResolvedTheme) {
|
|
8
|
+
const getSyntaxRules = function getSyntaxRules(resolved: ResolvedTheme) {
|
|
9
9
|
return [
|
|
10
10
|
{ scope: ["default"], style: { foreground: RGBA.fromHex(resolved.text) } },
|
|
11
11
|
{ scope: ["prompt"], style: { foreground: RGBA.fromHex(resolved.accent) } },
|
|
@@ -22,7 +22,7 @@ function getSyntaxRules(resolved: ResolvedTheme) {
|
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
scope: ["keyword.type"],
|
|
25
|
-
style: { foreground: RGBA.fromHex(resolved.syntaxType),
|
|
25
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.syntaxType), italic: true },
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
scope: ["keyword.function", "function.method"],
|
|
@@ -100,11 +100,11 @@ function getSyntaxRules(resolved: ResolvedTheme) {
|
|
|
100
100
|
"markup.heading.5",
|
|
101
101
|
"markup.heading.6",
|
|
102
102
|
],
|
|
103
|
-
style: { foreground: RGBA.fromHex(resolved.markdownHeading)
|
|
103
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.markdownHeading) },
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
scope: ["markup.bold", "markup.strong"],
|
|
107
|
-
style: { foreground: RGBA.fromHex(resolved.markdownStrong)
|
|
107
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.markdownStrong) },
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
scope: ["markup.italic"],
|
|
@@ -122,8 +122,8 @@ function getSyntaxRules(resolved: ResolvedTheme) {
|
|
|
122
122
|
{
|
|
123
123
|
scope: ["markup.raw.inline"],
|
|
124
124
|
style: {
|
|
125
|
-
foreground: RGBA.fromHex(resolved.markdownCode),
|
|
126
125
|
background: RGBA.fromHex(resolved.background),
|
|
126
|
+
foreground: RGBA.fromHex(resolved.markdownCode),
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
129
|
{
|
|
@@ -149,21 +149,21 @@ function getSyntaxRules(resolved: ResolvedTheme) {
|
|
|
149
149
|
{ scope: ["float"], style: { foreground: RGBA.fromHex(resolved.syntaxNumber) } },
|
|
150
150
|
{
|
|
151
151
|
scope: ["comment.error"],
|
|
152
|
-
style: { foreground: RGBA.fromHex(resolved.error), italic: true
|
|
152
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.error), italic: true },
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
scope: ["comment.warning"],
|
|
156
|
-
style: { foreground: RGBA.fromHex(resolved.warning), italic: true
|
|
156
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.warning), italic: true },
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
scope: ["comment.todo", "comment.note"],
|
|
160
|
-
style: { foreground: RGBA.fromHex(resolved.info), italic: true
|
|
160
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.info), italic: true },
|
|
161
161
|
},
|
|
162
162
|
{ scope: ["namespace"], style: { foreground: RGBA.fromHex(resolved.syntaxType) } },
|
|
163
163
|
{ scope: ["field"], style: { foreground: RGBA.fromHex(resolved.syntaxVariable) } },
|
|
164
164
|
{
|
|
165
165
|
scope: ["type.definition"],
|
|
166
|
-
style: { foreground: RGBA.fromHex(resolved.syntaxType)
|
|
166
|
+
style: { bold: true, foreground: RGBA.fromHex(resolved.syntaxType) },
|
|
167
167
|
},
|
|
168
168
|
{ scope: ["keyword.export"], style: { foreground: RGBA.fromHex(resolved.syntaxKeyword) } },
|
|
169
169
|
{ scope: ["attribute", "annotation"], style: { foreground: RGBA.fromHex(resolved.warning) } },
|
|
@@ -180,31 +180,31 @@ function getSyntaxRules(resolved: ResolvedTheme) {
|
|
|
180
180
|
{
|
|
181
181
|
scope: ["diff.plus"],
|
|
182
182
|
style: {
|
|
183
|
-
foreground: RGBA.fromHex(resolved.diffAdded),
|
|
184
183
|
background: RGBA.fromHex(resolved.diffAddedBg),
|
|
184
|
+
foreground: RGBA.fromHex(resolved.diffAdded),
|
|
185
185
|
},
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
scope: ["diff.minus"],
|
|
189
189
|
style: {
|
|
190
|
-
foreground: RGBA.fromHex(resolved.diffRemoved),
|
|
191
190
|
background: RGBA.fromHex(resolved.diffRemovedBg),
|
|
191
|
+
foreground: RGBA.fromHex(resolved.diffRemoved),
|
|
192
192
|
},
|
|
193
193
|
},
|
|
194
194
|
{
|
|
195
195
|
scope: ["diff.delta"],
|
|
196
196
|
style: {
|
|
197
|
-
foreground: RGBA.fromHex(resolved.diffContext),
|
|
198
197
|
background: RGBA.fromHex(resolved.diffContextBg),
|
|
198
|
+
foreground: RGBA.fromHex(resolved.diffContext),
|
|
199
199
|
},
|
|
200
200
|
},
|
|
201
|
-
{ scope: ["error"], style: { foreground: RGBA.fromHex(resolved.error)
|
|
202
|
-
{ scope: ["warning"], style: { foreground: RGBA.fromHex(resolved.warning)
|
|
201
|
+
{ scope: ["error"], style: { bold: true, foreground: RGBA.fromHex(resolved.error) } },
|
|
202
|
+
{ scope: ["warning"], style: { bold: true, foreground: RGBA.fromHex(resolved.warning) } },
|
|
203
203
|
{ scope: ["info"], style: { foreground: RGBA.fromHex(resolved.info) } },
|
|
204
204
|
{ scope: ["debug"], style: { foreground: RGBA.fromHex(resolved.textMuted) } },
|
|
205
|
-
]
|
|
206
|
-
}
|
|
205
|
+
];
|
|
206
|
+
};
|
|
207
207
|
|
|
208
|
-
export function buildSyntaxStyle(resolved: ResolvedTheme): SyntaxStyle {
|
|
209
|
-
return SyntaxStyle.fromTheme(getSyntaxRules(resolved))
|
|
210
|
-
}
|
|
208
|
+
export const buildSyntaxStyle = function buildSyntaxStyle(resolved: ResolvedTheme): SyntaxStyle {
|
|
209
|
+
return SyntaxStyle.fromTheme(getSyntaxRules(resolved));
|
|
210
|
+
};
|