@tooee/themes 0.2.4 → 0.3.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.
Files changed (41) hide show
  1. package/dist/{CloseButton.d.ts → close-button.d.ts} +2 -2
  2. package/dist/close-button.d.ts.map +1 -0
  3. package/dist/{CloseButton.js → close-button.js} +5 -4
  4. package/dist/close-button.js.map +1 -0
  5. package/dist/context.d.ts +7 -7
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +28 -26
  8. package/dist/context.js.map +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/loader.d.ts +5 -5
  14. package/dist/loader.d.ts.map +1 -1
  15. package/dist/loader.js +80 -74
  16. package/dist/loader.js.map +1 -1
  17. package/dist/syntax-rules.d.ts +1 -1
  18. package/dist/syntax-rules.d.ts.map +1 -1
  19. package/dist/syntax-rules.js +17 -17
  20. package/dist/syntax-rules.js.map +1 -1
  21. package/dist/{ThemePicker.d.ts → theme-picker.d.ts} +2 -2
  22. package/dist/theme-picker.d.ts.map +1 -0
  23. package/dist/{ThemePicker.js → theme-picker.js} +37 -33
  24. package/dist/theme-picker.js.map +1 -0
  25. package/dist/types.d.ts +7 -9
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +52 -43
  28. package/dist/types.js.map +1 -1
  29. package/package.json +4 -4
  30. package/src/{CloseButton.tsx → close-button.tsx} +21 -15
  31. package/src/context.tsx +83 -74
  32. package/src/index.ts +10 -10
  33. package/src/loader.ts +109 -95
  34. package/src/syntax-rules.ts +21 -21
  35. package/src/theme-picker.tsx +189 -0
  36. package/src/types.ts +129 -110
  37. package/dist/CloseButton.d.ts.map +0 -1
  38. package/dist/CloseButton.js.map +0 -1
  39. package/dist/ThemePicker.d.ts.map +0 -1
  40. package/dist/ThemePicker.js.map +0 -1
  41. package/src/ThemePicker.tsx +0 -177
package/src/context.tsx CHANGED
@@ -1,148 +1,157 @@
1
- import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react"
2
- import { type SyntaxStyle } from "@opentui/core"
3
- import { writeGlobalConfig } from "@tooee/config"
4
- import type { ResolvedTheme } from "./types.js"
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 function ThemeProvider({ name, mode, theme: themeProp, children }: ThemeProviderProps) {
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 { theme: t.colors, syntax: t.syntax, name: t.name, mode: t.mode }
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, _setMode] = useState<"dark" | "light">(initialMode ?? DEFAULT_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, mode } })
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, mode } })
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: { name, mode } })
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
- theme: theme.colors,
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
- allThemes,
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
- () => ({ theme: theme.colors, syntax: theme.syntax, name: theme.name, mode }),
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) throw new Error("useThemeSwitcher must be used within ThemeSwitcherProvider")
147
- return ctx
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 "./ThemePicker.js"
13
- export type { ThemePickerEntry } from "./ThemePicker.js"
12
+ export { ThemePicker } from "./theme-picker.js";
13
+ export type { ThemePickerEntry } from "./theme-picker.js";
14
14
 
15
- export { CloseButton } from "./CloseButton.js"
15
+ export { CloseButton } from "./close-button.js";
package/src/loader.ts CHANGED
@@ -1,32 +1,42 @@
1
- import { type SyntaxStyle } from "@opentui/core"
2
- import { readFileSync, readdirSync, existsSync } from "fs"
3
- import { join, basename, dirname } from "path"
4
- import { type ThemeJSON, type ResolvedTheme, resolveTheme } from "./types.js"
5
- import { buildSyntaxStyle } from "./syntax-rules.js"
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
- function loadJsonThemesFromDir(dir: string, target: Map<string, ThemeJSON>) {
22
+ const loadJsonThemesFromDir = function loadJsonThemesFromDir(
23
+ dir: string,
24
+ target: Map<string, ThemeJSON>,
25
+ ) {
22
26
  try {
23
- if (!existsSync(dir)) return
27
+ if (!existsSync(dir)) {
28
+ return;
29
+ }
24
30
  for (const file of readdirSync(dir)) {
25
- if (!file.endsWith(".json")) continue
26
- const name = basename(file, ".json")
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
- target.set(name, JSON.parse(content) as ThemeJSON)
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) return themeJsonCache
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) break
59
- dir = parent
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 Array.from(loadThemes().keys()).sort()
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
- cursorLine: "#222436",
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
- diffRemoved: "#c53b53",
105
+ diffAddedBg: "#20303b",
106
+ diffAddedLineNumberBg: "#1b2b34",
122
107
  diffContext: "#828bb8",
123
- diffHunkHeader: "#828bb8",
108
+ diffContextBg: "#1e2030",
124
109
  diffHighlightAdded: "#b8db87",
125
110
  diffHighlightRemoved: "#e26a75",
126
- diffAddedBg: "#20303b",
127
- diffRemovedBg: "#37222c",
128
- diffContextBg: "#1e2030",
111
+ diffHunkHeader: "#828bb8",
129
112
  diffLineNumber: "#222436",
130
- diffAddedLineNumberBg: "#1b2b34",
113
+ diffRemoved: "#c53b53",
114
+ diffRemovedBg: "#37222c",
131
115
  diffRemovedLineNumberBg: "#2d1f26",
132
- markdownText: "#c0caf5",
133
- markdownHeading: "#bb9af7",
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
- markdownStrong: "#ff966c",
122
+ markdownHeading: "#bb9af7",
140
123
  markdownHorizontalRule: "#565f89",
141
- markdownListItem: "#7aa2f7",
142
- markdownListEnumeration: "#7dcfff",
143
124
  markdownImage: "#7aa2f7",
144
125
  markdownImageText: "#7dcfff",
145
- markdownCodeBlock: "#c0caf5",
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
- syntaxVariable: "#c0caf5",
150
- syntaxString: "#9ece6a",
138
+ syntaxKeyword: "#bb9af7",
151
139
  syntaxNumber: "#ff9e64",
152
- syntaxType: "#2ac3de",
153
140
  syntaxOperator: "#89ddff",
154
141
  syntaxPunctuation: "#a9b1d6",
155
- }
156
- return { name: DEFAULT_THEME_NAME, mode: DEFAULT_MODE, colors, syntax: buildSyntaxStyle(colors) }
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 defaultTheme: Theme = hardcodedDefaultTheme
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
+ };
@@ -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), bold: true, italic: true },
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), bold: true },
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), bold: true },
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, bold: 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, bold: 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, bold: 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), bold: true },
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), bold: true } },
202
- { scope: ["warning"], style: { foreground: RGBA.fromHex(resolved.warning), bold: true } },
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
+ };