claudeup 4.35.1 → 4.36.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 (58) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/scope-squares.test.tsx +165 -0
  3. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  4. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  5. package/src/main.tsx +21 -5
  6. package/src/opentui.d.ts +21 -12
  7. package/src/ui/App.tsx +19 -12
  8. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  9. package/src/ui/adapters/settingsAdapter.ts +119 -116
  10. package/src/ui/adapters/skillsAdapter.ts +203 -196
  11. package/src/ui/components/CategoryHeader.tsx +9 -8
  12. package/src/ui/components/EmptyFilterState.tsx +10 -5
  13. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  14. package/src/ui/components/ScopeIndicator.tsx +10 -6
  15. package/src/ui/components/ScrollableList.tsx +3 -2
  16. package/src/ui/components/SearchInput.tsx +2 -1
  17. package/src/ui/components/StyledText.tsx +5 -4
  18. package/src/ui/components/TabBar.tsx +4 -3
  19. package/src/ui/components/layout/FooterHints.tsx +37 -30
  20. package/src/ui/components/layout/Panel.tsx +6 -5
  21. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  22. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  23. package/src/ui/components/layout/ScreenLayout.tsx +27 -24
  24. package/src/ui/components/layout/index.ts +3 -3
  25. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  26. package/src/ui/components/modals/InputModal.tsx +14 -6
  27. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  28. package/src/ui/components/modals/MessageModal.tsx +9 -8
  29. package/src/ui/components/modals/SelectModal.tsx +11 -7
  30. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  31. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  32. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  33. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  34. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  35. package/src/ui/components/primitives/MetaText.tsx +3 -3
  36. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  37. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  38. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  39. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  40. package/src/ui/registry.ts +11 -11
  41. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  42. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  43. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  44. package/src/ui/renderers/pluginRenderers.tsx +528 -471
  45. package/src/ui/renderers/profileRenderers.tsx +346 -300
  46. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  47. package/src/ui/renderers/skillRenderers.tsx +410 -326
  48. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  49. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  50. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  51. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  52. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  53. package/src/ui/screens/McpScreen.tsx +12 -3
  54. package/src/ui/screens/PluginsScreen.tsx +17 -7
  55. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  56. package/src/ui/screens/SkillsScreen.tsx +832 -688
  57. package/src/ui/theme-mode.ts +73 -0
  58. package/src/ui/theme.ts +147 -53
@@ -3,47 +3,50 @@ import type { SkillInfo, SkillSetInfo } from "../../types/index.js";
3
3
  // ─── Item types ───────────────────────────────────────────────────────────────
4
4
 
5
5
  export interface SkillCategoryItem {
6
- id: string;
7
- kind: "category";
8
- label: string;
9
- title: string;
10
- categoryKey: string;
11
- count?: number;
12
- tone: "purple" | "green" | "teal" | "yellow" | "gray" | "red";
13
- badge?: string;
14
- star?: string;
6
+ id: string;
7
+ kind: "category";
8
+ label: string;
9
+ title: string;
10
+ categoryKey: string;
11
+ count?: number;
12
+ tone: "purple" | "green" | "teal" | "yellow" | "gray" | "red";
13
+ badge?: string;
14
+ star?: string;
15
15
  }
16
16
 
17
17
  export interface SkillSkillItem {
18
- id: string;
19
- kind: "skill";
20
- label: string;
21
- skill: SkillInfo;
22
- /** Extra indent level for child skills inside an expanded skill set */
23
- indent?: number;
18
+ id: string;
19
+ kind: "skill";
20
+ label: string;
21
+ skill: SkillInfo;
22
+ /** Extra indent level for child skills inside an expanded skill set */
23
+ indent?: number;
24
24
  }
25
25
 
26
26
  export interface SkillSetItem {
27
- id: string;
28
- kind: "skillset";
29
- label: string;
30
- skillSet: SkillSetInfo;
31
- expanded: boolean;
27
+ id: string;
28
+ kind: "skillset";
29
+ label: string;
30
+ skillSet: SkillSetInfo;
31
+ expanded: boolean;
32
32
  }
33
33
 
34
- export type SkillBrowserItem = SkillCategoryItem | SkillSkillItem | SkillSetItem;
34
+ export type SkillBrowserItem =
35
+ | SkillCategoryItem
36
+ | SkillSkillItem
37
+ | SkillSetItem;
35
38
 
36
39
  // ─── Adapter ─────────────────────────────────────────────────────────────────
37
40
 
38
41
  export interface BuildSkillBrowserItemsArgs {
39
- recommended: SkillInfo[];
40
- popular: SkillInfo[];
41
- installed: SkillInfo[];
42
- searchResults: SkillInfo[];
43
- query: string;
44
- isSearchLoading: boolean;
45
- skillSets?: SkillSetInfo[];
46
- expandedSets?: Set<string>;
42
+ recommended: SkillInfo[];
43
+ popular: SkillInfo[];
44
+ installed: SkillInfo[];
45
+ searchResults: SkillInfo[];
46
+ query: string;
47
+ isSearchLoading: boolean;
48
+ skillSets?: SkillSetInfo[];
49
+ expandedSets?: Set<string>;
47
50
  }
48
51
 
49
52
  /**
@@ -51,172 +54,176 @@ export interface BuildSkillBrowserItemsArgs {
51
54
  * Extracted from SkillsScreen so it can be tested independently.
52
55
  */
53
56
  export function buildSkillBrowserItems({
54
- recommended,
55
- popular,
56
- installed,
57
- searchResults,
58
- query,
59
- isSearchLoading,
60
- skillSets = [],
61
- expandedSets = new Set(),
57
+ recommended,
58
+ popular,
59
+ installed,
60
+ searchResults,
61
+ query,
62
+ isSearchLoading,
63
+ skillSets = [],
64
+ expandedSets = new Set(),
62
65
  }: BuildSkillBrowserItemsArgs): SkillBrowserItem[] {
63
- const lowerQuery = query.toLowerCase();
64
- const items: SkillBrowserItem[] = [];
65
-
66
- // ── INSTALLED: always shown at top (if any) ──
67
- const installedFiltered = lowerQuery
68
- ? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
69
- : installed;
70
-
71
- if (installedFiltered.length > 0) {
72
- items.push({
73
- id: "cat:installed",
74
- kind: "category",
75
- label: `Installed (${installedFiltered.length})`,
76
- title: "Installed",
77
- categoryKey: "installed",
78
- count: installedFiltered.length,
79
- tone: "purple",
80
- star: "\u25CF ",
81
- });
82
- for (const skill of installedFiltered) {
83
- items.push({
84
- id: `skill:${skill.id}`,
85
- kind: "skill",
86
- label: skill.name,
87
- skill,
88
- });
89
- }
90
- }
91
-
92
- // ── RECOMMENDED: skill sets + individual skills, all as first-class items ──
93
- const filteredSets = lowerQuery
94
- ? skillSets.filter((s) => {
95
- if (s.name.toLowerCase().includes(lowerQuery)) return true;
96
- if (s.description.toLowerCase().includes(lowerQuery)) return true;
97
- if (s.loaded && s.skills.some((sk) => sk.name.toLowerCase().includes(lowerQuery))) return true;
98
- return false;
99
- })
100
- : skillSets;
101
-
102
- const filteredRec = lowerQuery
103
- ? recommended.filter(
104
- (s) =>
105
- s.name.toLowerCase().includes(lowerQuery) ||
106
- (s.description || "").toLowerCase().includes(lowerQuery),
107
- )
108
- : recommended;
109
-
110
- const recommendedCount = filteredSets.length + filteredRec.length;
111
-
112
- items.push({
113
- id: "cat:recommended",
114
- kind: "category",
115
- label: "Recommended",
116
- title: "Recommended",
117
- categoryKey: "recommended",
118
- count: recommendedCount,
119
- tone: "green",
120
- star: "\u2605 ",
121
- });
122
-
123
- // Skill sets first within recommended
124
- for (const set of filteredSets) {
125
- const isExpanded = expandedSets.has(set.id);
126
- items.push({
127
- id: `skillset:${set.id}`,
128
- kind: "skillset",
129
- label: set.name,
130
- skillSet: set,
131
- expanded: isExpanded,
132
- });
133
- // When expanded, show child skills as indented skill items
134
- if (isExpanded && set.loaded) {
135
- const childSkills = lowerQuery
136
- ? set.skills.filter((sk) => sk.name.toLowerCase().includes(lowerQuery))
137
- : set.skills;
138
- for (const skill of childSkills) {
139
- items.push({
140
- id: `skill:${skill.id}`,
141
- kind: "skill",
142
- label: skill.name,
143
- skill,
144
- indent: 2,
145
- });
146
- }
147
- }
148
- }
149
-
150
- // Then individual recommended skills
151
- for (const skill of filteredRec) {
152
- items.push({
153
- id: `skill:${skill.id}`,
154
- kind: "skill",
155
- label: skill.name,
156
- skill,
157
- });
158
- }
159
-
160
- // ── SEARCH MODE ──
161
- if (query.length >= 2) {
162
- if (!isSearchLoading && searchResults.length > 0) {
163
- const recNames = new Set(recommended.map((s) => s.name));
164
- const deduped = searchResults
165
- .filter((s) => !recNames.has(s.name))
166
- .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
167
-
168
- if (deduped.length > 0) {
169
- items.push({
170
- id: "cat:search",
171
- kind: "category",
172
- label: `Search (${deduped.length})`,
173
- title: "Search",
174
- categoryKey: "popular",
175
- count: deduped.length,
176
- tone: "teal",
177
- });
178
- for (const skill of deduped) {
179
- items.push({
180
- id: `skill:${skill.id}`,
181
- kind: "skill",
182
- label: skill.name,
183
- skill,
184
- });
185
- }
186
- }
187
- }
188
- return items;
189
- }
190
-
191
- // ── POPULAR (default, no search query) — only skills with meaningful stars ──
192
- // Dedup by name — API can return same skill name from different repos
193
- const seenPopular = new Set<string>();
194
- const popularDeduped = popular
195
- .filter((s) => (s.stars ?? 0) >= 5)
196
- .filter((s) => {
197
- if (seenPopular.has(s.name)) return false;
198
- seenPopular.add(s.name);
199
- return true;
200
- });
201
- if (popularDeduped.length > 0) {
202
- items.push({
203
- id: "cat:popular",
204
- kind: "category",
205
- label: "Popular",
206
- title: "Popular",
207
- categoryKey: "popular",
208
- count: popularDeduped.length,
209
- tone: "teal",
210
- });
211
- for (const skill of popularDeduped) {
212
- items.push({
213
- id: `skill:${skill.id}`,
214
- kind: "skill",
215
- label: skill.name,
216
- skill,
217
- });
218
- }
219
- }
220
-
221
- return items;
66
+ const lowerQuery = query.toLowerCase();
67
+ const items: SkillBrowserItem[] = [];
68
+
69
+ // ── INSTALLED: always shown at top (if any) ──
70
+ const installedFiltered = lowerQuery
71
+ ? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
72
+ : installed;
73
+
74
+ if (installedFiltered.length > 0) {
75
+ items.push({
76
+ id: "cat:installed",
77
+ kind: "category",
78
+ label: `Installed (${installedFiltered.length})`,
79
+ title: "Installed",
80
+ categoryKey: "installed",
81
+ count: installedFiltered.length,
82
+ tone: "purple",
83
+ star: "\u25CF ",
84
+ });
85
+ for (const skill of installedFiltered) {
86
+ items.push({
87
+ id: `skill:${skill.id}`,
88
+ kind: "skill",
89
+ label: skill.name,
90
+ skill,
91
+ });
92
+ }
93
+ }
94
+
95
+ // ── RECOMMENDED: skill sets + individual skills, all as first-class items ──
96
+ const filteredSets = lowerQuery
97
+ ? skillSets.filter((s) => {
98
+ if (s.name.toLowerCase().includes(lowerQuery)) return true;
99
+ if (s.description.toLowerCase().includes(lowerQuery)) return true;
100
+ if (
101
+ s.loaded &&
102
+ s.skills.some((sk) => sk.name.toLowerCase().includes(lowerQuery))
103
+ )
104
+ return true;
105
+ return false;
106
+ })
107
+ : skillSets;
108
+
109
+ const filteredRec = lowerQuery
110
+ ? recommended.filter(
111
+ (s) =>
112
+ s.name.toLowerCase().includes(lowerQuery) ||
113
+ (s.description || "").toLowerCase().includes(lowerQuery),
114
+ )
115
+ : recommended;
116
+
117
+ const recommendedCount = filteredSets.length + filteredRec.length;
118
+
119
+ items.push({
120
+ id: "cat:recommended",
121
+ kind: "category",
122
+ label: "Recommended",
123
+ title: "Recommended",
124
+ categoryKey: "recommended",
125
+ count: recommendedCount,
126
+ tone: "green",
127
+ star: "\u2605 ",
128
+ });
129
+
130
+ // Skill sets first within recommended
131
+ for (const set of filteredSets) {
132
+ const isExpanded = expandedSets.has(set.id);
133
+ items.push({
134
+ id: `skillset:${set.id}`,
135
+ kind: "skillset",
136
+ label: set.name,
137
+ skillSet: set,
138
+ expanded: isExpanded,
139
+ });
140
+ // When expanded, show child skills as indented skill items
141
+ if (isExpanded && set.loaded) {
142
+ const childSkills = lowerQuery
143
+ ? set.skills.filter((sk) => sk.name.toLowerCase().includes(lowerQuery))
144
+ : set.skills;
145
+ for (const skill of childSkills) {
146
+ items.push({
147
+ id: `skill:${skill.id}`,
148
+ kind: "skill",
149
+ label: skill.name,
150
+ skill,
151
+ indent: 2,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ // Then individual recommended skills
158
+ for (const skill of filteredRec) {
159
+ items.push({
160
+ id: `skill:${skill.id}`,
161
+ kind: "skill",
162
+ label: skill.name,
163
+ skill,
164
+ });
165
+ }
166
+
167
+ // ── SEARCH MODE ──
168
+ if (query.length >= 2) {
169
+ if (!isSearchLoading && searchResults.length > 0) {
170
+ const recNames = new Set(recommended.map((s) => s.name));
171
+ const deduped = searchResults
172
+ .filter((s) => !recNames.has(s.name))
173
+ .sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
174
+
175
+ if (deduped.length > 0) {
176
+ items.push({
177
+ id: "cat:search",
178
+ kind: "category",
179
+ label: `Search (${deduped.length})`,
180
+ title: "Search",
181
+ categoryKey: "popular",
182
+ count: deduped.length,
183
+ tone: "teal",
184
+ });
185
+ for (const skill of deduped) {
186
+ items.push({
187
+ id: `skill:${skill.id}`,
188
+ kind: "skill",
189
+ label: skill.name,
190
+ skill,
191
+ });
192
+ }
193
+ }
194
+ }
195
+ return items;
196
+ }
197
+
198
+ // ── POPULAR (default, no search query) only skills with meaningful stars ──
199
+ // Dedup by name — API can return same skill name from different repos
200
+ const seenPopular = new Set<string>();
201
+ const popularDeduped = popular
202
+ .filter((s) => (s.stars ?? 0) >= 5)
203
+ .filter((s) => {
204
+ if (seenPopular.has(s.name)) return false;
205
+ seenPopular.add(s.name);
206
+ return true;
207
+ });
208
+ if (popularDeduped.length > 0) {
209
+ items.push({
210
+ id: "cat:popular",
211
+ kind: "category",
212
+ label: "Popular",
213
+ title: "Popular",
214
+ categoryKey: "popular",
215
+ count: popularDeduped.length,
216
+ tone: "teal",
217
+ });
218
+ for (const skill of popularDeduped) {
219
+ items.push({
220
+ id: `skill:${skill.id}`,
221
+ kind: "skill",
222
+ label: skill.name,
223
+ skill,
224
+ });
225
+ }
226
+ }
227
+
228
+ return items;
222
229
  }
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme, type UiColor } from "../theme.js";
2
3
 
3
4
  interface CategoryHeaderProps {
4
5
  /** Category title */
@@ -8,7 +9,7 @@ interface CategoryHeaderProps {
8
9
  /** Status badge (e.g., "✓ Configured", "3 plugins") */
9
10
  status?: string;
10
11
  /** Status badge color */
11
- statusColor?: string;
12
+ statusColor?: UiColor;
12
13
  /** Whether category is expanded */
13
14
  expanded?: boolean;
14
15
  /** Number of items in category */
@@ -19,7 +20,7 @@ export function CategoryHeader({
19
20
  title,
20
21
  version,
21
22
  status,
22
- statusColor = "green",
23
+ statusColor = theme.colors.success,
23
24
  expanded = true,
24
25
  count,
25
26
  }: CategoryHeaderProps) {
@@ -30,14 +31,14 @@ export function CategoryHeader({
30
31
 
31
32
  // Simple format without dynamic line calculation
32
33
  return (
33
- <text>
34
- <span fg="#666666">{expandIcon}</span>
35
- <span fg="white">
34
+ <text fg={theme.colors.text}>
35
+ <span fg={theme.colors.muted}>{expandIcon}</span>
36
+ <span fg={theme.colors.text}>
36
37
  <strong> {title}</strong>
37
38
  </span>
38
- <span fg="#666666">{versionBadge}</span>
39
- <span fg="#666666">{countBadge}</span>
40
- <span fg="#666666"> ────</span>
39
+ <span fg={theme.colors.muted}>{versionBadge}</span>
40
+ <span fg={theme.colors.muted}>{countBadge}</span>
41
+ <span fg={theme.colors.border}> ────</span>
41
42
  <span fg={statusColor}>{statusText}</span>
42
43
  </text>
43
44
  );
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme } from "../theme.js";
2
3
 
3
4
  interface EmptyFilterStateProps {
4
5
  query: string;
@@ -11,16 +12,20 @@ export function EmptyFilterState({
11
12
  }: EmptyFilterStateProps) {
12
13
  return (
13
14
  <box flexDirection="column" marginTop={2} paddingLeft={2} paddingRight={2}>
14
- <text fg="yellow">No {entityName} found for "{query}"</text>
15
+ <text fg={theme.colors.warning}>
16
+ No {entityName} found for "{query}"
17
+ </text>
15
18
  <box marginTop={1}>
16
- <text fg="gray">Try a different search term, or if you think</text>
19
+ <text fg={theme.colors.muted}>
20
+ Try a different search term, or if you think
21
+ </text>
17
22
  </box>
18
- <text fg="gray">this is a bug, report it at:</text>
23
+ <text fg={theme.colors.muted}>this is a bug, report it at:</text>
19
24
  <box marginTop={1}>
20
- <text fg="#5c9aff">github.com/MadAppGang/magus/issues</text>
25
+ <text fg={theme.colors.link}>github.com/MadAppGang/magus/issues</text>
21
26
  </box>
22
27
  <box marginTop={1}>
23
- <text fg="gray">Press Esc to clear the filter.</text>
28
+ <text fg={theme.colors.muted}>Press Esc to clear the filter.</text>
24
29
  </box>
25
30
  </box>
26
31
  );
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme, type UiColor } from "../theme.js";
2
3
 
3
4
  /**
4
5
  * Scope indicator for list items.
@@ -15,16 +16,19 @@ export function scopeIndicatorText(
15
16
  user?: boolean,
16
17
  project?: boolean,
17
18
  local?: boolean,
18
- ): { text: string; segments: Array<{ char: string; bg: string; fg: string }> } {
19
- const segments: Array<{ char: string; bg: string; fg: string }> = [
19
+ ): {
20
+ text: string;
21
+ segments: Array<{ char: string; bg: UiColor; fg: UiColor }>;
22
+ } {
23
+ const segments: Array<{ char: string; bg: UiColor; fg: UiColor }> = [
20
24
  user
21
- ? { char: "u", bg: "cyan", fg: "black" }
25
+ ? { char: "u", bg: theme.colors.info, fg: theme.hints.fg }
22
26
  : { char: " ", bg: "", fg: "" },
23
27
  project
24
- ? { char: "p", bg: "green", fg: "black" }
28
+ ? { char: "p", bg: theme.colors.success, fg: theme.hints.fg }
25
29
  : { char: " ", bg: "", fg: "" },
26
30
  local
27
- ? { char: "l", bg: "yellow", fg: "black" }
31
+ ? { char: "l", bg: theme.colors.warning, fg: theme.hints.fg }
28
32
  : { char: " ", bg: "", fg: "" },
29
33
  ];
30
34
 
@@ -42,7 +46,7 @@ export function ScopeIndicator({ user, project, local }: ScopeIndicatorProps) {
42
46
  const { segments } = scopeIndicatorText(user, project, local);
43
47
 
44
48
  return (
45
- <text>
49
+ <text fg={theme.colors.text}>
46
50
  {segments.map((s, i) =>
47
51
  s.bg ? (
48
52
  <span key={i} bg={s.bg} fg={s.fg}>
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useMemo } from "react";
2
+ import { theme } from "../theme.js";
2
3
 
3
4
  interface ScrollableListProps<T> {
4
5
  /** Array of items to display */
@@ -62,7 +63,7 @@ export function ScrollableList<T>({
62
63
  return (
63
64
  <box flexDirection="column">
64
65
  {showScrollIndicators && hasItemsAbove && (
65
- <text fg="cyan">↑ {scrollOffset} more</text>
66
+ <text fg={theme.colors.info}>↑ {scrollOffset} more</text>
66
67
  )}
67
68
 
68
69
  {visibleItems.map(({ item, originalIndex }) => (
@@ -76,7 +77,7 @@ export function ScrollableList<T>({
76
77
  ))}
77
78
 
78
79
  {showScrollIndicators && hasItemsBelow && (
79
- <text fg="cyan">↓ {itemsBelow} more</text>
80
+ <text fg={theme.colors.info}>↓ {itemsBelow} more</text>
80
81
  )}
81
82
  </box>
82
83
  );
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
3
+ import { theme } from "../theme.js";
3
4
 
4
5
  interface SearchInputProps {
5
6
  /** Current search value */
@@ -41,7 +42,7 @@ export function SearchInput({
41
42
 
42
43
  return (
43
44
  <box flexDirection="row">
44
- <text fg="cyan">❯ </text>
45
+ <text fg={theme.colors.info}>❯ </text>
45
46
  <input
46
47
  value={value}
47
48
  onChange={onChange}
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme } from "../theme.js";
2
3
 
3
4
  interface StyledTextProps {
4
5
  /** Text content */
@@ -25,8 +26,8 @@ interface StyledTextProps {
25
26
  * Converts props to nested tag pattern required by OpenTUI.
26
27
  *
27
28
  * Example:
28
- * <StyledText bold color="green">Success</StyledText>
29
- * becomes: <text fg="green"><strong>Success</strong></text>
29
+ * <StyledText bold color={theme.colors.success}>Success</StyledText>
30
+ * becomes: <text fg={theme.colors.success}><strong>Success</strong></text>
30
31
  */
31
32
  export function StyledText({
32
33
  children,
@@ -52,12 +53,12 @@ export function StyledText({
52
53
  }
53
54
  // Note: dim and inverse use span with colors since OpenTUI doesn't have these as React elements
54
55
  if (dim) {
55
- content = <span fg="#666666">{content}</span>;
56
+ content = <span fg={theme.colors.muted}>{content}</span>;
56
57
  }
57
58
  if (inverse) {
58
59
  // Inverse effect approximated with contrasting colors
59
60
  content = (
60
- <span fg="black" bg="white">
61
+ <span fg={theme.hints.fg} bg={theme.colors.text}>
61
62
  {content}
62
63
  </span>
63
64
  );
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import type { Screen } from "../state/types.js";
3
+ import { theme } from "../theme.js";
3
4
 
4
5
  interface Tab {
5
6
  key: string;
@@ -34,7 +35,7 @@ export function TabBar({ currentScreen }: TabBarProps) {
34
35
  {/* Tab content */}
35
36
  {isSelected ? (
36
37
  <box>
37
- <text bg="#7e57c2" fg="white">
38
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
38
39
  <strong>
39
40
  {" "}
40
41
  {tab.key}:{tab.label}{" "}
@@ -43,14 +44,14 @@ export function TabBar({ currentScreen }: TabBarProps) {
43
44
  </box>
44
45
  ) : (
45
46
  <box>
46
- <text fg="gray">
47
+ <text fg={theme.colors.muted}>
47
48
  {" "}
48
49
  {tab.key}:{tab.label}{" "}
49
50
  </text>
50
51
  </box>
51
52
  )}
52
53
  {/* Separator */}
53
- {!isLast && <text fg="#666666">│</text>}
54
+ {!isLast && <text fg={theme.colors.muted}>│</text>}
54
55
  </box>
55
56
  );
56
57
  })}