@tooee/themes 0.1.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 (51) hide show
  1. package/README.md +5 -0
  2. package/dist/ThemePicker.d.ts +14 -0
  3. package/dist/ThemePicker.d.ts.map +1 -0
  4. package/dist/ThemePicker.js +96 -0
  5. package/dist/ThemePicker.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/theme.d.ts +111 -0
  11. package/dist/theme.d.ts.map +1 -0
  12. package/dist/theme.js +560 -0
  13. package/dist/theme.js.map +1 -0
  14. package/package.json +43 -0
  15. package/src/ThemePicker.tsx +163 -0
  16. package/src/index.ts +21 -0
  17. package/src/theme.tsx +727 -0
  18. package/src/themes/aura.json +69 -0
  19. package/src/themes/ayu.json +80 -0
  20. package/src/themes/catppuccin-frappe.json +233 -0
  21. package/src/themes/catppuccin-macchiato.json +233 -0
  22. package/src/themes/catppuccin.json +112 -0
  23. package/src/themes/cobalt2.json +228 -0
  24. package/src/themes/cursor.json +249 -0
  25. package/src/themes/dracula.json +219 -0
  26. package/src/themes/everforest.json +241 -0
  27. package/src/themes/flexoki.json +237 -0
  28. package/src/themes/github-light.json +56 -0
  29. package/src/themes/github.json +233 -0
  30. package/src/themes/gruvbox.json +95 -0
  31. package/src/themes/kanagawa.json +77 -0
  32. package/src/themes/lucent-orng.json +227 -0
  33. package/src/themes/material.json +235 -0
  34. package/src/themes/matrix.json +77 -0
  35. package/src/themes/mercury.json +252 -0
  36. package/src/themes/monokai.json +221 -0
  37. package/src/themes/nightowl.json +221 -0
  38. package/src/themes/nord.json +223 -0
  39. package/src/themes/one-dark.json +84 -0
  40. package/src/themes/opencode-light.json +62 -0
  41. package/src/themes/opencode.json +245 -0
  42. package/src/themes/orng.json +245 -0
  43. package/src/themes/osaka-jade.json +93 -0
  44. package/src/themes/palenight.json +222 -0
  45. package/src/themes/rosepine.json +234 -0
  46. package/src/themes/solarized.json +223 -0
  47. package/src/themes/synthwave84.json +226 -0
  48. package/src/themes/tokyonight.json +243 -0
  49. package/src/themes/vercel.json +245 -0
  50. package/src/themes/vesper.json +218 -0
  51. package/src/themes/zenburn.json +223 -0
@@ -0,0 +1,163 @@
1
+ import { useState, useMemo, useCallback } from "react"
2
+ import { useKeyboard } from "@opentui/react"
3
+ import { useTheme } from "./theme.js"
4
+
5
+ export interface ThemePickerEntry {
6
+ id: string
7
+ title: string
8
+ }
9
+
10
+ interface ThemePickerProps {
11
+ entries: ThemePickerEntry[]
12
+ currentTheme: string
13
+ onSelect: (name: string) => void
14
+ onClose: () => void
15
+ onNavigate: (name: string) => void
16
+ }
17
+
18
+ function fuzzyMatch(query: string, text: string): number | null {
19
+ const lowerQuery = query.toLowerCase()
20
+ const lowerText = text.toLowerCase()
21
+
22
+ let qi = 0
23
+ let score = 0
24
+ let lastMatchIndex = -2
25
+
26
+ for (let ti = 0; ti < lowerText.length && qi < lowerQuery.length; ti++) {
27
+ if (lowerText[ti] === lowerQuery[qi]) {
28
+ if (ti === 0) score += 3
29
+ else if (" -./".includes(lowerText[ti - 1]!)) score += 2
30
+ else if (ti === lastMatchIndex + 1) score += 1
31
+
32
+ lastMatchIndex = ti
33
+ qi++
34
+ }
35
+ }
36
+
37
+ return qi === lowerQuery.length ? score : null
38
+ }
39
+
40
+ export function ThemePicker({
41
+ entries,
42
+ currentTheme,
43
+ onSelect,
44
+ onClose,
45
+ onNavigate,
46
+ }: ThemePickerProps) {
47
+ const { theme } = useTheme()
48
+ const [filter, setFilter] = useState("")
49
+ const [activeIndex, setActiveIndex] = useState(() => {
50
+ const idx = entries.findIndex((e) => e.id === currentTheme)
51
+ return idx >= 0 ? idx : 0
52
+ })
53
+
54
+ const filtered = useMemo(() => {
55
+ if (!filter) return entries
56
+ const results: { entry: ThemePickerEntry; score: number }[] = []
57
+ for (const entry of entries) {
58
+ const score = fuzzyMatch(filter, entry.title)
59
+ if (score !== null) results.push({ entry, score })
60
+ }
61
+ results.sort((a, b) => b.score - a.score)
62
+ return results.map((r) => r.entry)
63
+ }, [entries, filter])
64
+
65
+ const handleSelect = useCallback(() => {
66
+ const item = filtered[activeIndex]
67
+ if (item) {
68
+ onSelect(item.id)
69
+ }
70
+ }, [filtered, activeIndex, onSelect])
71
+
72
+ const navigateTo = useCallback(
73
+ (index: number) => {
74
+ setActiveIndex(index)
75
+ const item = filtered[index]
76
+ if (item) {
77
+ onNavigate(item.id)
78
+ }
79
+ },
80
+ [filtered, onNavigate],
81
+ )
82
+
83
+ useKeyboard((key) => {
84
+ if (key.name === "escape") {
85
+ key.preventDefault()
86
+ onClose()
87
+ } else if (key.name === "return") {
88
+ key.preventDefault()
89
+ handleSelect()
90
+ } else if (key.name === "up") {
91
+ key.preventDefault()
92
+ navigateTo(Math.max(0, activeIndex - 1))
93
+ } else if (key.name === "down") {
94
+ key.preventDefault()
95
+ navigateTo(Math.min(filtered.length - 1, activeIndex + 1))
96
+ }
97
+ })
98
+
99
+ return (
100
+ <box
101
+ position="absolute"
102
+ left="20%"
103
+ right="20%"
104
+ top={2}
105
+ maxHeight="60%"
106
+ flexDirection="column"
107
+ backgroundColor={theme.backgroundPanel}
108
+ border
109
+ borderColor={theme.border}
110
+ >
111
+ {/* Filter row */}
112
+ <box flexDirection="row" paddingLeft={1} paddingRight={1} height={1}>
113
+ <text content="🎨 " fg={theme.accent} />
114
+ <input
115
+ focused
116
+ placeholder="Filter themes..."
117
+ onInput={(value: string) => {
118
+ setFilter(value)
119
+ setActiveIndex(0)
120
+ // Preview first match
121
+ if (!value) {
122
+ if (entries.length > 0) onNavigate(entries[0]!.id)
123
+ } else {
124
+ const results: { entry: ThemePickerEntry; score: number }[] = []
125
+ for (const entry of entries) {
126
+ const score = fuzzyMatch(value, entry.title)
127
+ if (score !== null) results.push({ entry, score })
128
+ }
129
+ results.sort((a, b) => b.score - a.score)
130
+ if (results.length > 0) onNavigate(results[0]!.entry.id)
131
+ }
132
+ }}
133
+ backgroundColor="transparent"
134
+ focusedBackgroundColor="transparent"
135
+ textColor={theme.text}
136
+ placeholderColor={theme.textMuted}
137
+ cursorColor={theme.accent}
138
+ style={{ flexGrow: 1 }}
139
+ />
140
+ <text content={` ${filtered.length}`} fg={theme.textMuted} />
141
+ </box>
142
+
143
+ {/* Separator */}
144
+ <box height={1} width="100%" backgroundColor={theme.border} />
145
+
146
+ {/* Theme list */}
147
+ <scrollbox focused={false} style={{ flexGrow: 1 }}>
148
+ {filtered.map((entry, i) => (
149
+ <box
150
+ key={entry.id}
151
+ flexDirection="row"
152
+ paddingLeft={1}
153
+ paddingRight={1}
154
+ height={1}
155
+ backgroundColor={i === activeIndex ? theme.backgroundElement : undefined}
156
+ >
157
+ <text content={entry.title} fg={theme.text} style={{ flexGrow: 1 }} />
158
+ </box>
159
+ ))}
160
+ </scrollbox>
161
+ </box>
162
+ )
163
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export {
2
+ ThemeProvider,
3
+ ThemeSwitcherProvider,
4
+ useTheme,
5
+ useThemeSwitcher,
6
+ defaultTheme,
7
+ resolveTheme,
8
+ buildSyntaxStyle,
9
+ loadThemes,
10
+ getThemeNames,
11
+ } from "./theme.js"
12
+ export type {
13
+ Theme,
14
+ ThemeJSON,
15
+ ResolvedTheme,
16
+ ThemeProviderProps,
17
+ ThemeSwitcherProviderProps,
18
+ } from "./theme.js"
19
+
20
+ export { ThemePicker } from "./ThemePicker.jsx"
21
+ export type { ThemePickerEntry } from "./ThemePicker.jsx"