@tooee/themes 0.7.3 → 0.9.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.
@@ -1,189 +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 "./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
- };