@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.
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
@@ -0,0 +1,189 @@
1
+ import { useState, useMemo, useCallback } from "react";
2
+ import { useCommand } from "@tooee/commands";
3
+ import { fuzzyMatch } from "@tooee/fuzzy";
4
+ import { useTheme } from "./context.js";
5
+ import { CloseButton } from "./close-button.js";
6
+
7
+ export interface ThemePickerEntry {
8
+ id: string;
9
+ title: string;
10
+ }
11
+
12
+ interface ThemePickerProps {
13
+ entries: ThemePickerEntry[];
14
+ currentTheme: string;
15
+ onSelect: (name: string) => void;
16
+ onClose: () => void;
17
+ onNavigate: (name: string) => void;
18
+ }
19
+
20
+ export const ThemePicker = function ThemePicker({
21
+ entries,
22
+ currentTheme,
23
+ onSelect,
24
+ onClose,
25
+ onNavigate,
26
+ }: ThemePickerProps): React.ReactNode {
27
+ const { theme } = useTheme();
28
+ const [filter, setFilter] = useState("");
29
+ const [activeIndex, setActiveIndex] = useState(() => {
30
+ const idx = entries.findIndex((e) => e.id === currentTheme);
31
+ return Math.max(idx, 0);
32
+ });
33
+
34
+ const filtered = useMemo(() => {
35
+ if (!filter) {
36
+ return entries;
37
+ }
38
+ const results: { entry: ThemePickerEntry; score: number }[] = [];
39
+ for (const entry of entries) {
40
+ const score = fuzzyMatch(filter, entry.title);
41
+ if (score !== null) {
42
+ results.push({ entry, score });
43
+ }
44
+ }
45
+ results.sort((a, b) => b.score - a.score);
46
+ return results.map((r) => r.entry);
47
+ }, [entries, filter]);
48
+
49
+ const handleSelect = useCallback(() => {
50
+ const item = filtered[activeIndex];
51
+ if (item !== undefined) {
52
+ onSelect(item.id);
53
+ }
54
+ }, [filtered, activeIndex, onSelect]);
55
+
56
+ const navigateTo = useCallback(
57
+ (index: number) => {
58
+ setActiveIndex(index);
59
+ const item = filtered[index];
60
+ if (item !== undefined) {
61
+ onNavigate(item.id);
62
+ }
63
+ },
64
+ [filtered, onNavigate],
65
+ );
66
+
67
+ const moveUp = useCallback(() => {
68
+ navigateTo(Math.max(0, activeIndex - 1));
69
+ }, [activeIndex, navigateTo]);
70
+
71
+ const moveDown = useCallback(() => {
72
+ navigateTo(Math.min(filtered.length - 1, activeIndex + 1));
73
+ }, [activeIndex, filtered.length, navigateTo]);
74
+
75
+ useCommand({
76
+ handler: onClose,
77
+ hidden: true,
78
+ hotkey: "Escape",
79
+ id: "theme-picker:close",
80
+ modes: ["insert", "cursor"],
81
+ title: "Close theme picker",
82
+ });
83
+ useCommand({
84
+ handler: handleSelect,
85
+ hidden: true,
86
+ hotkey: "Enter",
87
+ id: "theme-picker:select",
88
+ modes: ["insert", "cursor"],
89
+ title: "Select theme",
90
+ });
91
+ useCommand({
92
+ handler: moveUp,
93
+ hidden: true,
94
+ hotkey: "up",
95
+ id: "theme-picker:move-up",
96
+ modes: ["insert", "cursor"],
97
+ title: "Move up",
98
+ });
99
+ useCommand({
100
+ handler: moveDown,
101
+ hidden: true,
102
+ hotkey: "down",
103
+ id: "theme-picker:move-down",
104
+ modes: ["insert", "cursor"],
105
+ title: "Move down",
106
+ });
107
+
108
+ return (
109
+ <box
110
+ position="absolute"
111
+ left="20%"
112
+ right="20%"
113
+ top={2}
114
+ maxHeight="60%"
115
+ flexDirection="column"
116
+ backgroundColor={theme.backgroundPanel}
117
+ border
118
+ borderColor={theme.border}
119
+ >
120
+ {/* Filter row */}
121
+ <box flexDirection="row" paddingLeft={1} paddingRight={1} height={1}>
122
+ <text content="🎨 " fg={theme.accent} />
123
+ <input
124
+ focused
125
+ placeholder="Filter themes..."
126
+ onSubmit={handleSelect}
127
+ onInput={(value: string) => {
128
+ setFilter(value);
129
+ setActiveIndex(0);
130
+ // Preview first match
131
+ if (value) {
132
+ const results: { entry: ThemePickerEntry; score: number }[] = [];
133
+ for (const entry of entries) {
134
+ const score = fuzzyMatch(value, entry.title);
135
+ if (score !== null) {
136
+ results.push({ entry, score });
137
+ }
138
+ }
139
+ results.sort((a, b) => b.score - a.score);
140
+ if (results.length > 0) {
141
+ onNavigate(results[0].entry.id);
142
+ }
143
+ } else if (entries.length > 0) {
144
+ onNavigate(entries[0].id);
145
+ }
146
+ }}
147
+ backgroundColor="transparent"
148
+ focusedBackgroundColor="transparent"
149
+ textColor={theme.text}
150
+ placeholderColor={theme.textMuted}
151
+ cursorColor={theme.accent}
152
+ style={{ flexGrow: 1 }}
153
+ />
154
+ <text content={` ${filtered.length}`} fg={theme.textMuted} />
155
+ <CloseButton onClose={onClose} />
156
+ </box>
157
+
158
+ {/* Separator */}
159
+ <box height={1} width="100%" backgroundColor={theme.border} />
160
+
161
+ {/* Theme list */}
162
+ <scrollbox focused={false} style={{ flexGrow: 1 }}>
163
+ {filtered.map(
164
+ (entry, i): React.ReactNode => (
165
+ <box
166
+ key={entry.id}
167
+ flexDirection="row"
168
+ paddingLeft={1}
169
+ paddingRight={1}
170
+ height={1}
171
+ backgroundColor={i === activeIndex ? theme.backgroundElement : undefined}
172
+ onMouseDown={(event) => {
173
+ // Left-click applies the theme — same code path as Enter.
174
+ if (event.button !== 0) {
175
+ return;
176
+ }
177
+ event.preventDefault();
178
+ event.stopPropagation();
179
+ onSelect(entry.id);
180
+ }}
181
+ >
182
+ <text content={entry.title} fg={theme.text} style={{ flexGrow: 1 }} />
183
+ </box>
184
+ ),
185
+ )}
186
+ </scrollbox>
187
+ </box>
188
+ );
189
+ };
package/src/types.ts CHANGED
@@ -2,15 +2,16 @@
2
2
  // Theme JSON format (OpenCode-compatible)
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
- type HexColor = `#${string}`
6
- type RefName = string
7
- type Variant = { dark: HexColor | RefName; light: HexColor | RefName }
8
- type ColorValue = HexColor | RefName | Variant
5
+ interface Variant {
6
+ dark: string;
7
+ light: string;
8
+ }
9
+ type ColorValue = string | Variant;
9
10
 
10
11
  export interface ThemeJSON {
11
- $schema?: string
12
- defs?: Record<string, HexColor | RefName>
13
- theme: Record<string, ColorValue>
12
+ $schema?: string;
13
+ defs?: Record<string, string>;
14
+ theme: Record<string, ColorValue>;
14
15
  }
15
16
 
16
17
  // ---------------------------------------------------------------------------
@@ -19,62 +20,62 @@ export interface ThemeJSON {
19
20
 
20
21
  export interface ResolvedTheme {
21
22
  // UI
22
- primary: string
23
- secondary: string
24
- accent: string
25
- error: string
26
- warning: string
27
- success: string
28
- info: string
29
- text: string
30
- textMuted: string
31
- background: string
32
- backgroundPanel: string
33
- backgroundElement: string
34
- border: string
35
- borderActive: string
36
- borderSubtle: string
23
+ primary: string;
24
+ secondary: string;
25
+ accent: string;
26
+ error: string;
27
+ warning: string;
28
+ success: string;
29
+ info: string;
30
+ text: string;
31
+ textMuted: string;
32
+ background: string;
33
+ backgroundPanel: string;
34
+ backgroundElement: string;
35
+ border: string;
36
+ borderActive: string;
37
+ borderSubtle: string;
37
38
  // Diff
38
- diffAdded: string
39
- diffRemoved: string
40
- diffContext: string
41
- diffHunkHeader: string
42
- diffHighlightAdded: string
43
- diffHighlightRemoved: string
44
- diffAddedBg: string
45
- diffRemovedBg: string
46
- diffContextBg: string
47
- diffLineNumber: string
48
- diffAddedLineNumberBg: string
49
- diffRemovedLineNumberBg: string
39
+ diffAdded: string;
40
+ diffRemoved: string;
41
+ diffContext: string;
42
+ diffHunkHeader: string;
43
+ diffHighlightAdded: string;
44
+ diffHighlightRemoved: string;
45
+ diffAddedBg: string;
46
+ diffRemovedBg: string;
47
+ diffContextBg: string;
48
+ diffLineNumber: string;
49
+ diffAddedLineNumberBg: string;
50
+ diffRemovedLineNumberBg: string;
50
51
  // Markdown
51
- markdownText: string
52
- markdownHeading: string
53
- markdownLink: string
54
- markdownLinkText: string
55
- markdownCode: string
56
- markdownBlockQuote: string
57
- markdownEmph: string
58
- markdownStrong: string
59
- markdownHorizontalRule: string
60
- markdownListItem: string
61
- markdownListEnumeration: string
62
- markdownImage: string
63
- markdownImageText: string
64
- markdownCodeBlock: string
52
+ markdownText: string;
53
+ markdownHeading: string;
54
+ markdownLink: string;
55
+ markdownLinkText: string;
56
+ markdownCode: string;
57
+ markdownBlockQuote: string;
58
+ markdownEmph: string;
59
+ markdownStrong: string;
60
+ markdownHorizontalRule: string;
61
+ markdownListItem: string;
62
+ markdownListEnumeration: string;
63
+ markdownImage: string;
64
+ markdownImageText: string;
65
+ markdownCodeBlock: string;
65
66
  // Cursor/Selection
66
- cursorLine: string
67
- selection: string
67
+ cursorLine: string;
68
+ selection: string;
68
69
  // Syntax
69
- syntaxComment: string
70
- syntaxKeyword: string
71
- syntaxFunction: string
72
- syntaxVariable: string
73
- syntaxString: string
74
- syntaxNumber: string
75
- syntaxType: string
76
- syntaxOperator: string
77
- syntaxPunctuation: string
70
+ syntaxComment: string;
71
+ syntaxKeyword: string;
72
+ syntaxFunction: string;
73
+ syntaxVariable: string;
74
+ syntaxString: string;
75
+ syntaxNumber: string;
76
+ syntaxType: string;
77
+ syntaxOperator: string;
78
+ syntaxPunctuation: string;
78
79
  }
79
80
 
80
81
  // All keys of ResolvedTheme for iteration
@@ -131,91 +132,109 @@ export const RESOLVED_KEYS: (keyof ResolvedTheme)[] = [
131
132
  "syntaxType",
132
133
  "syntaxOperator",
133
134
  "syntaxPunctuation",
134
- ]
135
+ ];
135
136
 
136
137
  // Fallbacks used when a theme key is missing
137
138
  export const FALLBACKS: Record<string, string> = {
138
- primary: "#808080",
139
- secondary: "#808080",
140
139
  accent: "#808080",
141
- error: "#808080",
142
- warning: "#808080",
143
- success: "#808080",
144
- info: "#808080",
145
- text: "#d4d4d4",
146
- textMuted: "#808080",
147
140
  background: "#1e1e1e",
148
- backgroundPanel: "#1e1e1e",
149
141
  backgroundElement: "#1e1e1e",
150
- cursorLine: "#1e1e1e",
151
- selection: "#1e1e1e",
142
+ backgroundPanel: "#1e1e1e",
152
143
  border: "#808080",
153
144
  borderActive: "#808080",
154
145
  borderSubtle: "#808080",
146
+ cursorLine: "#1e1e1e",
155
147
  diffAdded: "#4fd6be",
156
- diffRemoved: "#c53b53",
148
+ diffAddedBg: "#1e3a1e",
149
+ diffAddedLineNumberBg: "#1e3a1e",
157
150
  diffContext: "#808080",
158
- diffHunkHeader: "#808080",
151
+ diffContextBg: "#1e1e1e",
159
152
  diffHighlightAdded: "#4fd6be",
160
153
  diffHighlightRemoved: "#c53b53",
161
- diffAddedBg: "#1e3a1e",
162
- diffRemovedBg: "#3a1e1e",
163
- diffContextBg: "#1e1e1e",
154
+ diffHunkHeader: "#808080",
164
155
  diffLineNumber: "#808080",
165
- diffAddedLineNumberBg: "#1e3a1e",
156
+ diffRemoved: "#c53b53",
157
+ diffRemovedBg: "#3a1e1e",
166
158
  diffRemovedLineNumberBg: "#3a1e1e",
167
- markdownText: "#d4d4d4",
168
- markdownHeading: "#808080",
169
- markdownLink: "#808080",
170
- markdownLinkText: "#808080",
171
- markdownCode: "#808080",
159
+ error: "#808080",
160
+ info: "#808080",
172
161
  markdownBlockQuote: "#808080",
162
+ markdownCode: "#808080",
163
+ markdownCodeBlock: "#d4d4d4",
173
164
  markdownEmph: "#808080",
174
- markdownStrong: "#808080",
165
+ markdownHeading: "#808080",
175
166
  markdownHorizontalRule: "#808080",
176
- markdownListItem: "#808080",
177
- markdownListEnumeration: "#808080",
178
167
  markdownImage: "#808080",
179
168
  markdownImageText: "#808080",
180
- markdownCodeBlock: "#d4d4d4",
169
+ markdownLink: "#808080",
170
+ markdownLinkText: "#808080",
171
+ markdownListEnumeration: "#808080",
172
+ markdownListItem: "#808080",
173
+ markdownStrong: "#808080",
174
+ markdownText: "#d4d4d4",
175
+ primary: "#808080",
176
+ secondary: "#808080",
177
+ selection: "#1e1e1e",
178
+ success: "#808080",
181
179
  syntaxComment: "#808080",
182
- syntaxKeyword: "#808080",
183
180
  syntaxFunction: "#808080",
184
- syntaxVariable: "#808080",
185
- syntaxString: "#808080",
181
+ syntaxKeyword: "#808080",
186
182
  syntaxNumber: "#808080",
187
- syntaxType: "#808080",
188
183
  syntaxOperator: "#808080",
189
184
  syntaxPunctuation: "#808080",
190
- }
185
+ syntaxString: "#808080",
186
+ syntaxType: "#808080",
187
+ syntaxVariable: "#808080",
188
+ text: "#d4d4d4",
189
+ textMuted: "#808080",
190
+ warning: "#808080",
191
+ };
191
192
 
192
193
  // ---------------------------------------------------------------------------
193
194
  // Resolution
194
195
  // ---------------------------------------------------------------------------
195
196
 
196
- export function resolveTheme(json: ThemeJSON, mode: "dark" | "light"): ResolvedTheme {
197
- const defs = json.defs ?? {}
197
+ export const resolveTheme = function resolveTheme(
198
+ json: ThemeJSON,
199
+ mode: "dark" | "light",
200
+ ): ResolvedTheme {
201
+ const defs = json.defs ?? {};
198
202
 
199
- function resolveColor(c: ColorValue, seen: Set<string> = new Set()): string {
203
+ const resolveColor = function resolveColor(c: ColorValue, seen = new Set<string>()): string {
200
204
  if (typeof c === "string") {
201
- if (c === "transparent" || c === "none") return "#00000000"
202
- if (c.startsWith("#")) return c
203
- if (seen.has(c)) return "#808080" // reference cycle — fall back like an unknown ref
204
- if (defs[c] != null) return resolveColor(defs[c] as ColorValue, new Set(seen).add(c))
205
- if (json.theme[c] !== undefined)
206
- return resolveColor(json.theme[c] as ColorValue, new Set(seen).add(c))
207
- return "#808080"
205
+ if (c === "transparent" || c === "none") {
206
+ return "#00000000";
207
+ }
208
+ if (c.startsWith("#")) {
209
+ return c;
210
+ }
211
+ if (seen.has(c)) {
212
+ return "#808080";
213
+ }
214
+ if (defs[c] !== undefined && defs[c] !== null) {
215
+ return resolveColor(defs[c], new Set(seen).add(c));
216
+ }
217
+ if (json.theme[c] !== undefined) {
218
+ return resolveColor(json.theme[c], new Set(seen).add(c));
219
+ }
220
+ return "#808080";
208
221
  }
209
- return resolveColor(c[mode], seen)
210
- }
222
+ return resolveColor(c[mode], seen);
223
+ };
211
224
 
212
- const result = {} as Record<string, string>
225
+ const result = {} as Record<string, string>;
213
226
  for (const key of RESOLVED_KEYS) {
214
- const val = json.theme[key]
215
- result[key] = val !== undefined ? resolveColor(val) : (FALLBACKS[key] ?? "#808080")
227
+ const val = json.theme[key];
228
+ result[key] = val === undefined ? (FALLBACKS[key] ?? "#808080") : resolveColor(val);
216
229
  }
217
230
  // Dynamic fallbacks that reference other resolved keys
218
- if (json.theme["cursorLine"] === undefined) result.cursorLine = result.backgroundElement
219
- if (json.theme["selection"] === undefined) result.selection = result.backgroundPanel
220
- return result as unknown as ResolvedTheme
221
- }
231
+ if (json.theme.cursorLine === undefined) {
232
+ result.cursorLine = result.backgroundElement;
233
+ }
234
+ if (json.theme.selection === undefined) {
235
+ result.selection = result.backgroundPanel;
236
+ }
237
+ // Deferred(lint-sweep): replace with a typed exhaustive key map for compile-time completeness
238
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- RESOLVED_KEYS drives every required key and defaults missing values
239
+ return result as unknown as ResolvedTheme;
240
+ };
@@ -1 +0,0 @@
1
- {"version":3,"file":"CloseButton.d.ts","sourceRoot":"","sources":["../src/CloseButton.tsx"],"names":[],"mappings":"AAIA,UAAU,gBAAgB;IACxB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,KAAW,EAAE,KAAK,EAAE,EAAE,gBAAgB,6BAmB5E"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"CloseButton.js","sourceRoot":"","sources":["../src/CloseButton.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAWvC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,OAAO,EAAE,KAAK,GAAG,GAAG,EAAE,KAAK,EAAoB;IAC3E,MAAM,EAAE,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAA;IAE5B,MAAM,eAAe,GAAG,WAAW,CACjC,CAAC,KAAiB,EAAE,EAAE;QACpB,8CAA8C;QAC9C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC9B,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,KAAK,CAAC,eAAe,EAAE,CAAA;QACvB,OAAO,EAAE,CAAA;IACX,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAA;IAED,OAAO,CACL,cAAK,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,EAAE,eAAe,YAC9D,eAAM,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,IAAI,KAAK,CAAC,SAAS,GAAI,GAClD,CACP,CAAA;AACH,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ThemePicker.d.ts","sourceRoot":"","sources":["../src/ThemePicker.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd;AAED,UAAU,gBAAgB;IACxB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC;AAED,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,UAAU,GACX,EAAE,gBAAgB,6BAuJlB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ThemePicker.js","sourceRoot":"","sources":["../src/ThemePicker.tsx"],"names":[],"mappings":"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"}
@@ -1,177 +0,0 @@
1
- import { useState, useMemo, useCallback } from "react"
2
- import { useCommand } from "@tooee/commands"
3
- import { fuzzyMatch } from "@tooee/fuzzy"
4
- import { useTheme } from "./context.js"
5
- import { CloseButton } from "./CloseButton.js"
6
-
7
- export interface ThemePickerEntry {
8
- id: string
9
- title: string
10
- }
11
-
12
- interface ThemePickerProps {
13
- entries: ThemePickerEntry[]
14
- currentTheme: string
15
- onSelect: (name: string) => void
16
- onClose: () => void
17
- onNavigate: (name: string) => void
18
- }
19
-
20
- export function ThemePicker({
21
- entries,
22
- currentTheme,
23
- onSelect,
24
- onClose,
25
- onNavigate,
26
- }: ThemePickerProps) {
27
- const { theme } = useTheme()
28
- const [filter, setFilter] = useState("")
29
- const [activeIndex, setActiveIndex] = useState(() => {
30
- const idx = entries.findIndex((e) => e.id === currentTheme)
31
- return idx >= 0 ? idx : 0
32
- })
33
-
34
- const filtered = useMemo(() => {
35
- if (!filter) return entries
36
- const results: { entry: ThemePickerEntry; score: number }[] = []
37
- for (const entry of entries) {
38
- const score = fuzzyMatch(filter, entry.title)
39
- if (score !== null) results.push({ entry, score })
40
- }
41
- results.sort((a, b) => b.score - a.score)
42
- return results.map((r) => r.entry)
43
- }, [entries, filter])
44
-
45
- const handleSelect = useCallback(() => {
46
- const item = filtered[activeIndex]
47
- if (item) {
48
- onSelect(item.id)
49
- }
50
- }, [filtered, activeIndex, onSelect])
51
-
52
- const navigateTo = useCallback(
53
- (index: number) => {
54
- setActiveIndex(index)
55
- const item = filtered[index]
56
- if (item) {
57
- onNavigate(item.id)
58
- }
59
- },
60
- [filtered, onNavigate],
61
- )
62
-
63
- const moveUp = useCallback(() => {
64
- navigateTo(Math.max(0, activeIndex - 1))
65
- }, [activeIndex, navigateTo])
66
-
67
- const moveDown = useCallback(() => {
68
- navigateTo(Math.min(filtered.length - 1, activeIndex + 1))
69
- }, [activeIndex, filtered.length, navigateTo])
70
-
71
- useCommand({
72
- id: "theme-picker:close",
73
- title: "Close theme picker",
74
- hotkey: "Escape",
75
- modes: ["insert", "cursor"],
76
- hidden: true,
77
- handler: onClose,
78
- })
79
- useCommand({
80
- id: "theme-picker:select",
81
- title: "Select theme",
82
- hotkey: "Enter",
83
- modes: ["insert", "cursor"],
84
- hidden: true,
85
- handler: handleSelect,
86
- })
87
- useCommand({
88
- id: "theme-picker:move-up",
89
- title: "Move up",
90
- hotkey: "up",
91
- modes: ["insert", "cursor"],
92
- hidden: true,
93
- handler: moveUp,
94
- })
95
- useCommand({
96
- id: "theme-picker:move-down",
97
- title: "Move down",
98
- hotkey: "down",
99
- modes: ["insert", "cursor"],
100
- hidden: true,
101
- handler: moveDown,
102
- })
103
-
104
- return (
105
- <box
106
- position="absolute"
107
- left="20%"
108
- right="20%"
109
- top={2}
110
- maxHeight="60%"
111
- flexDirection="column"
112
- backgroundColor={theme.backgroundPanel}
113
- border
114
- borderColor={theme.border}
115
- >
116
- {/* Filter row */}
117
- <box flexDirection="row" paddingLeft={1} paddingRight={1} height={1}>
118
- <text content="🎨 " fg={theme.accent} />
119
- <input
120
- focused
121
- placeholder="Filter themes..."
122
- onSubmit={handleSelect}
123
- onInput={(value: string) => {
124
- setFilter(value)
125
- setActiveIndex(0)
126
- // Preview first match
127
- if (!value) {
128
- if (entries.length > 0) onNavigate(entries[0]!.id)
129
- } else {
130
- const results: { entry: ThemePickerEntry; score: number }[] = []
131
- for (const entry of entries) {
132
- const score = fuzzyMatch(value, entry.title)
133
- if (score !== null) results.push({ entry, score })
134
- }
135
- results.sort((a, b) => b.score - a.score)
136
- if (results.length > 0) onNavigate(results[0]!.entry.id)
137
- }
138
- }}
139
- backgroundColor="transparent"
140
- focusedBackgroundColor="transparent"
141
- textColor={theme.text}
142
- placeholderColor={theme.textMuted}
143
- cursorColor={theme.accent}
144
- style={{ flexGrow: 1 }}
145
- />
146
- <text content={` ${filtered.length}`} fg={theme.textMuted} />
147
- <CloseButton onClose={onClose} />
148
- </box>
149
-
150
- {/* Separator */}
151
- <box height={1} width="100%" backgroundColor={theme.border} />
152
-
153
- {/* Theme list */}
154
- <scrollbox focused={false} style={{ flexGrow: 1 }}>
155
- {filtered.map((entry, i) => (
156
- <box
157
- key={entry.id}
158
- flexDirection="row"
159
- paddingLeft={1}
160
- paddingRight={1}
161
- height={1}
162
- backgroundColor={i === activeIndex ? theme.backgroundElement : undefined}
163
- onMouseDown={(event) => {
164
- // Left-click applies the theme — same code path as Enter.
165
- if (event.button !== 0) return
166
- event.preventDefault()
167
- event.stopPropagation()
168
- onSelect(entry.id)
169
- }}
170
- >
171
- <text content={entry.title} fg={theme.text} style={{ flexGrow: 1 }} />
172
- </box>
173
- ))}
174
- </scrollbox>
175
- </box>
176
- )
177
- }