claudeup 4.35.1 → 4.37.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 (72) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/catalog-cache-store.test.ts +271 -0
  3. package/src/__tests__/catalog-notice.test.ts +155 -0
  4. package/src/__tests__/github-budget.test.ts +200 -0
  5. package/src/__tests__/plugin-manager-fallback.test.ts +200 -8
  6. package/src/__tests__/scope-squares.test.tsx +165 -0
  7. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  8. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  9. package/src/main.tsx +21 -5
  10. package/src/opentui.d.ts +21 -12
  11. package/src/services/catalog-cache-store.ts +218 -0
  12. package/src/services/github-budget.ts +274 -0
  13. package/src/services/marketplace-catalog-git.ts +170 -0
  14. package/src/services/marketplace-catalog.ts +95 -0
  15. package/src/services/marketplace-fetcher.ts +310 -87
  16. package/src/services/plugin-manager.ts +103 -92
  17. package/src/ui/App.tsx +19 -12
  18. package/src/ui/adapters/catalogNotice.ts +122 -0
  19. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  20. package/src/ui/adapters/settingsAdapter.ts +119 -116
  21. package/src/ui/adapters/skillsAdapter.ts +203 -196
  22. package/src/ui/components/CategoryHeader.tsx +9 -8
  23. package/src/ui/components/EmptyFilterState.tsx +10 -5
  24. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  25. package/src/ui/components/ScopeIndicator.tsx +10 -6
  26. package/src/ui/components/ScrollableList.tsx +3 -2
  27. package/src/ui/components/SearchInput.tsx +2 -1
  28. package/src/ui/components/StyledText.tsx +5 -4
  29. package/src/ui/components/TabBar.tsx +4 -3
  30. package/src/ui/components/layout/FooterHints.tsx +37 -30
  31. package/src/ui/components/layout/Panel.tsx +6 -5
  32. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  33. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  34. package/src/ui/components/layout/ScreenLayout.tsx +46 -26
  35. package/src/ui/components/layout/index.ts +3 -3
  36. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  37. package/src/ui/components/modals/InputModal.tsx +14 -6
  38. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  39. package/src/ui/components/modals/MessageModal.tsx +9 -8
  40. package/src/ui/components/modals/SelectModal.tsx +11 -7
  41. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  42. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  43. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  44. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  45. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  46. package/src/ui/components/primitives/MetaText.tsx +3 -3
  47. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  48. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  49. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  50. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  51. package/src/ui/registry.ts +11 -11
  52. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  53. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  54. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  55. package/src/ui/renderers/pluginRenderers.tsx +566 -471
  56. package/src/ui/renderers/profileRenderers.tsx +346 -300
  57. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  58. package/src/ui/renderers/skillRenderers.tsx +410 -326
  59. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  60. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  61. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  62. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  63. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  64. package/src/ui/screens/McpScreen.tsx +12 -3
  65. package/src/ui/screens/PluginsScreen.tsx +152 -33
  66. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  67. package/src/ui/screens/SkillsScreen.tsx +832 -688
  68. package/src/ui/state/reducer.ts +11 -2
  69. package/src/ui/state/types.ts +16 -1
  70. package/src/ui/theme-mode.ts +73 -0
  71. package/src/ui/theme.ts +147 -53
  72. package/src/utils/config-dir.ts +47 -0
@@ -1,15 +1,20 @@
1
- import React from "react";
1
+ import type React from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  /** One footer hint: a key (or key group) and the action it performs. */
4
5
  export interface FooterHint {
5
- /** Key(s) for this action, e.g. ["↑", "↓"] or ["U"]. Joined for display. */
6
- keys: string[];
7
- label: string;
6
+ /** Key(s) for this action, e.g. ["↑", "↓"] or ["U"]. Joined for display. */
7
+ keys: string[];
8
+ label: string;
8
9
  }
9
10
 
10
- const KEY_BG = "#3f3f46";
11
- const KEY_FG = "#fafafa";
12
- const LABEL_FG = "gray";
11
+ // The shared chip palette, not a local one. This file used to define its own
12
+ // (`theme.colors.dim` block + terminal-foreground ink), which is why the footer
13
+ // stayed a low-contrast grey-on-grey strip after the palette was made vivid —
14
+ // the new colours simply never reached it. Anything chip-shaped uses theme.hints.
15
+ const KEY_BG = theme.hints.defaultBg;
16
+ const KEY_FG = theme.hints.fg;
17
+ const LABEL_FG = theme.colors.muted;
13
18
  const GAP = " ";
14
19
 
15
20
  /**
@@ -20,33 +25,35 @@ const GAP = " ";
20
25
  * Each key is shown in a chip so it reads as a pressable key, not prose.
21
26
  * Shared across screens so every footer looks the same.
22
27
  */
23
- export function FooterHints({ hints }: { hints: FooterHint[] }): React.ReactNode {
24
- const nodes: React.ReactNode[] = [];
25
- hints.forEach((hint, i) => {
26
- if (i > 0) {
27
- nodes.push(
28
- <span key={`gap-${i}`} fg={LABEL_FG}>
29
- {GAP}
30
- </span>,
31
- );
32
- }
33
- nodes.push(
34
- <span key={`k-${i}`} bg={KEY_BG} fg={KEY_FG}>
35
- {` ${hint.keys.join("")} `}
36
- </span>,
37
- );
38
- nodes.push(
39
- <span key={`l-${i}`} fg={LABEL_FG}>
40
- {` ${hint.label}`}
41
- </span>,
42
- );
43
- });
44
- return <text>{nodes}</text>;
28
+ export function FooterHints({
29
+ hints,
30
+ }: { hints: FooterHint[] }): React.ReactNode {
31
+ const nodes: React.ReactNode[] = [];
32
+ hints.forEach((hint, i) => {
33
+ if (i > 0) {
34
+ nodes.push(
35
+ <span key={`gap-${i}`} fg={LABEL_FG}>
36
+ {GAP}
37
+ </span>,
38
+ );
39
+ }
40
+ nodes.push(
41
+ <span key={`k-${i}`} bg={KEY_BG} fg={KEY_FG}>
42
+ {` ${hint.keys.join("")} `}
43
+ </span>,
44
+ );
45
+ nodes.push(
46
+ <span key={`l-${i}`} fg={LABEL_FG}>
47
+ {` ${hint.label}`}
48
+ </span>,
49
+ );
50
+ });
51
+ return <text fg={theme.colors.text}>{nodes}</text>;
45
52
  }
46
53
 
47
54
  /** Convenience for callers that already have a node: same as <FooterHints>. */
48
55
  export function renderFooterHints(hints: FooterHint[]): React.ReactNode {
49
- return <FooterHints hints={hints} />;
56
+ return <FooterHints hints={hints} />;
50
57
  }
51
58
 
52
59
  export default FooterHints;
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme, type UiColor } from "../../theme.js";
2
3
 
3
4
  interface PanelProps {
4
5
  /** Panel title */
@@ -6,9 +7,9 @@ interface PanelProps {
6
7
  /** Panel content */
7
8
  children: React.ReactNode;
8
9
  /** Border color */
9
- borderColor?: string;
10
+ borderColor?: UiColor;
10
11
  /** Title color */
11
- titleColor?: string;
12
+ titleColor?: UiColor;
12
13
  /** Panel width - number or percentage like "50%" */
13
14
  width?: number | `${number}%` | "auto";
14
15
  /** Panel height - number or percentage like "50%" */
@@ -22,14 +23,14 @@ interface PanelProps {
22
23
  export function Panel({
23
24
  title,
24
25
  children,
25
- borderColor = "#7e57c2",
26
- titleColor = "#7e57c2",
26
+ borderColor = theme.colors.accent,
27
+ titleColor = theme.colors.accent,
27
28
  width,
28
29
  height,
29
30
  flexGrow = 1,
30
31
  focused = false,
31
32
  }: PanelProps) {
32
- const activeColor = focused ? "#7e57c2" : borderColor;
33
+ const activeColor = focused ? theme.colors.accent : borderColor;
33
34
 
34
35
  return (
35
36
  <box
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  interface ProgressBarProps {
4
5
  /** Progress message */
@@ -21,9 +22,9 @@ export function ProgressBar({ message, current, total }: ProgressBarProps) {
21
22
 
22
23
  return (
23
24
  <box>
24
- <text fg="cyan">⟳</text>
25
- <text> {message} </text>
26
- <text fg="cyan">
25
+ <text fg={theme.colors.info}>⟳</text>
26
+ <text fg={theme.colors.text}> {message} </text>
27
+ <text fg={theme.colors.info}>
27
28
  [{bar}] {current}/{total}
28
29
  </text>
29
30
  </box>
@@ -33,9 +34,9 @@ export function ProgressBar({ message, current, total }: ProgressBarProps) {
33
34
  // Indeterminate progress
34
35
  return (
35
36
  <box>
36
- <text fg="cyan">⟳</text>
37
- <text> {message}</text>
38
- <text fg="gray"> ...</text>
37
+ <text fg={theme.colors.info}>⟳</text>
38
+ <text fg={theme.colors.text}> {message}</text>
39
+ <text fg={theme.colors.muted}> ...</text>
39
40
  </box>
40
41
  );
41
42
  }
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  interface ScopeTabsProps {
4
5
  /** Current scope */
@@ -21,29 +22,29 @@ export function ScopeTabs({
21
22
  {/* Project tab */}
22
23
  <box>
23
24
  {isProject ? (
24
- <text bg="cyan" fg="black">
25
+ <text bg={theme.colors.info} fg={theme.hints.fg}>
25
26
  <strong> ◆ Project </strong>
26
27
  </text>
27
28
  ) : (
28
- <text fg="gray"> ○ Project </text>
29
+ <text fg={theme.colors.muted}> ○ Project </text>
29
30
  )}
30
31
  </box>
31
32
 
32
33
  {/* Global tab */}
33
34
  <box>
34
35
  {!isProject ? (
35
- <text bg="magenta" fg="white">
36
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
36
37
  <strong> ◆ Global </strong>
37
38
  </text>
38
39
  ) : (
39
- <text fg="gray"> ○ Global </text>
40
+ <text fg={theme.colors.muted}> ○ Global </text>
40
41
  )}
41
42
  </box>
42
43
 
43
44
  {/* Toggle hint */}
44
45
  {toggleHint && (
45
46
  <box marginLeft={2}>
46
- <text fg="#666666">({toggleHint})</text>
47
+ <text fg={theme.colors.muted}>({toggleHint})</text>
47
48
  </box>
48
49
  )}
49
50
  </box>
@@ -3,6 +3,7 @@ import { useDimensions } from "../../state/DimensionsContext.js";
3
3
  import { TabBar } from "../TabBar.js";
4
4
  import type { Screen } from "../../state/types.js";
5
5
  import { FooterHints, type FooterHint } from "./FooterHints.js";
6
+ import { theme } from "../../theme.js";
6
7
 
7
8
  interface ScreenLayoutProps {
8
9
  /** Screen title (e.g., "claudeup Plugins") */
@@ -22,6 +23,15 @@ interface ScreenLayoutProps {
22
23
  };
23
24
  /** Status line content (for screens without search) - shown in second row */
24
25
  statusLine?: React.ReactNode;
26
+ /**
27
+ * One-line warning shown under the header, above the content.
28
+ *
29
+ * For conditions that make the data on screen untrustworthy — not for
30
+ * transient progress. It sits in the fixed chrome rather than in a panel so it
31
+ * cannot be scrolled out of view, and it takes a row from the panels only
32
+ * while it is present.
33
+ */
34
+ notice?: React.ReactNode;
25
35
  /** Footer hints (left side). Pass a FooterHint[] to render key badges
26
36
  * (the standard look). A string or ReactNode is still accepted for
27
37
  * free-form footers. */
@@ -32,7 +42,7 @@ interface ScreenLayoutProps {
32
42
  detailPanel: React.ReactNode;
33
43
  }
34
44
 
35
- const HEADER_COLOR = "#7e57c2";
45
+ const HEADER_COLOR = theme.colors.accent;
36
46
 
37
47
  export function ScreenLayout({
38
48
  title,
@@ -40,6 +50,7 @@ export function ScreenLayout({
40
50
  currentScreen,
41
51
  search,
42
52
  statusLine,
53
+ notice,
43
54
  footerHints,
44
55
  listPanel,
45
56
  detailPanel,
@@ -49,8 +60,8 @@ export function ScreenLayout({
49
60
  const hasSearchBar = search && (search.isActive || search.query);
50
61
 
51
62
  // Fixed chrome: top line + tabs + line + header + separator + footer = 6
52
- // Search bar adds 1 when active
53
- const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
63
+ // Search bar adds 1 when active; the notice adds 1 while present.
64
+ const fixedHeight = 6 + (hasSearchBar ? 1 : 0) + (notice ? 1 : 0);
54
65
  const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
55
66
  const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
56
67
 
@@ -58,7 +69,7 @@ export function ScreenLayout({
58
69
  <box flexDirection="column" height={dimensions.contentHeight}>
59
70
  {/* Line above tabs */}
60
71
  <box height={1} paddingLeft={1} paddingRight={1}>
61
- <text fg="#333333">{"─".repeat(lineWidth)}</text>
72
+ <text fg={theme.colors.dim}>{"─".repeat(lineWidth)}</text>
62
73
  </box>
63
74
 
64
75
  {/* Tab bar */}
@@ -68,15 +79,21 @@ export function ScreenLayout({
68
79
 
69
80
  {/* Line below tabs */}
70
81
  <box height={1} paddingLeft={1} paddingRight={1}>
71
- <text fg="#333333">{"─".repeat(lineWidth)}</text>
82
+ <text fg={theme.colors.dim}>{"─".repeat(lineWidth)}</text>
72
83
  </box>
73
84
 
74
85
  {/* Header — title on left, subtitle/status on right */}
75
- <box height={1} paddingLeft={1} paddingRight={1} flexDirection="row" justifyContent="space-between">
86
+ <box
87
+ height={1}
88
+ paddingLeft={1}
89
+ paddingRight={1}
90
+ flexDirection="row"
91
+ justifyContent="space-between"
92
+ >
76
93
  <text fg={HEADER_COLOR}>
77
94
  <strong>{title}</strong>
78
95
  </text>
79
- {subtitle && <text fg="gray">{subtitle}</text>}
96
+ {subtitle && <text fg={theme.colors.muted}>{subtitle}</text>}
80
97
  {!subtitle && statusLine ? statusLine : null}
81
98
  </box>
82
99
 
@@ -84,24 +101,33 @@ export function ScreenLayout({
84
101
  {hasSearchBar && (
85
102
  <box height={1} paddingLeft={1} paddingRight={1}>
86
103
  {search.isActive ? (
87
- <text>
88
- <span fg="green">{"Filter: "}</span>
89
- <span fg="white">{search.query}</span>
90
- <span bg="white" fg="black"> </span>
104
+ <text fg={theme.colors.text}>
105
+ <span fg={theme.colors.success}>{"Filter: "}</span>
106
+ <span fg={theme.colors.text}>{search.query}</span>
107
+ <span bg={theme.colors.text} fg={theme.hints.fg}>
108
+ {" "}
109
+ </span>
91
110
  </text>
92
111
  ) : (
93
- <text>
94
- <span fg="green">{"Filter: "}</span>
95
- <span fg="yellow">{search.query}</span>
96
- <span fg="gray"> (Esc to clear)</span>
112
+ <text fg={theme.colors.text}>
113
+ <span fg={theme.colors.success}>{"Filter: "}</span>
114
+ <span fg={theme.colors.warning}>{search.query}</span>
115
+ <span fg={theme.colors.muted}> (Esc to clear)</span>
97
116
  </text>
98
117
  )}
99
118
  </box>
100
119
  )}
101
120
 
121
+ {/* Warning line — only when the data on screen cannot be trusted */}
122
+ {notice && (
123
+ <box height={1} paddingLeft={1} paddingRight={1}>
124
+ {notice}
125
+ </box>
126
+ )}
127
+
102
128
  {/* Separator below header */}
103
129
  <box height={1} paddingLeft={1} paddingRight={1}>
104
- <text fg="#444444">{"─".repeat(lineWidth)}</text>
130
+ <text fg={theme.colors.border}>{"─".repeat(lineWidth)}</text>
105
131
  </box>
106
132
 
107
133
  {/* Main content area */}
@@ -118,19 +144,13 @@ export function ScreenLayout({
118
144
 
119
145
  {/* Vertical separator */}
120
146
  <box flexDirection="column" width={1} height={panelHeight}>
121
- <text fg="#444444">{"│".repeat(panelHeight)}</text>
147
+ <text fg={theme.colors.border}>{"│".repeat(panelHeight)}</text>
122
148
  </box>
123
149
 
124
150
  {/* Detail panel — scrollable */}
125
151
  <box width="50%" height={panelHeight} paddingLeft={1}>
126
- <scrollbox
127
- height={panelHeight}
128
- scrollY={true}
129
- scrollX={false}
130
- >
131
- <box flexDirection="column">
132
- {detailPanel}
133
- </box>
152
+ <scrollbox height={panelHeight} scrollY={true} scrollX={false}>
153
+ <box flexDirection="column">{detailPanel}</box>
134
154
  </scrollbox>
135
155
  </box>
136
156
  </box>
@@ -140,7 +160,7 @@ export function ScreenLayout({
140
160
  {Array.isArray(footerHints) ? (
141
161
  <FooterHints hints={footerHints as FooterHint[]} />
142
162
  ) : typeof footerHints === "string" ? (
143
- <text fg="gray">{footerHints}</text>
163
+ <text fg={theme.colors.muted}>{footerHints}</text>
144
164
  ) : (
145
165
  footerHints
146
166
  )}
@@ -3,7 +3,7 @@ export { ScopeTabs } from "./ScopeTabs.js";
3
3
  export { ProgressBar } from "./ProgressBar.js";
4
4
  export { ScreenLayout } from "./ScreenLayout.js";
5
5
  export {
6
- FooterHints,
7
- renderFooterHints,
8
- type FooterHint,
6
+ FooterHints,
7
+ renderFooterHints,
8
+ type FooterHint,
9
9
  } from "./FooterHints.js";
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { useKeyboard } from "../../hooks/useKeyboard.js";
3
+ import { theme } from "../../theme.js";
3
4
 
4
5
  interface ConfirmModalProps {
5
6
  /** Modal title */
@@ -19,9 +20,9 @@ export function ConfirmModal({
19
20
  onCancel,
20
21
  }: ConfirmModalProps) {
21
22
  useKeyboard((key) => {
22
- if (key.name === "y" || key.name === "Y") {
23
+ if (key.name === "y") {
23
24
  onConfirm();
24
- } else if (key.name === "n" || key.name === "N" || key.name === "escape") {
25
+ } else if (key.name === "n" || key.name === "escape") {
25
26
  onCancel();
26
27
  }
27
28
  });
@@ -31,8 +32,8 @@ export function ConfirmModal({
31
32
  flexDirection="column"
32
33
  border
33
34
  borderStyle="rounded"
34
- borderColor="#525252"
35
- backgroundColor="#1C1C1E"
35
+ borderColor={theme.colors.dim}
36
+ backgroundColor={theme.surface.bg}
36
37
  paddingLeft={3}
37
38
  paddingRight={3}
38
39
  paddingTop={1}
@@ -40,19 +41,19 @@ export function ConfirmModal({
40
41
  width={60}
41
42
  >
42
43
  <box marginBottom={1}>
43
- <text fg="#EDEDED">
44
+ <text fg={theme.colors.text}>
44
45
  <strong>{title}</strong>
45
46
  </text>
46
47
  </box>
47
48
  <box marginBottom={1}>
48
- <text fg="#A1A1AA">{message}</text>
49
+ <text fg={theme.colors.muted}>{message}</text>
49
50
  </box>
50
51
  <box marginTop={1} flexDirection="row">
51
- <text fg="#71717A">Press </text>
52
- <text fg="#EDEDED">Y</text>
53
- <text fg="#71717A"> to confirm • </text>
54
- <text fg="#EDEDED">N</text>
55
- <text fg="#71717A"> to cancel</text>
52
+ <text fg={theme.colors.muted}>Press </text>
53
+ <text fg={theme.colors.text}>Y</text>
54
+ <text fg={theme.colors.muted}> to confirm • </text>
55
+ <text fg={theme.colors.text}>N</text>
56
+ <text fg={theme.colors.muted}> to cancel</text>
56
57
  </box>
57
58
  </box>
58
59
  );
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  interface InputModalProps {
4
5
  /** Modal title */
@@ -24,8 +25,8 @@ export function InputModal({
24
25
  flexDirection="column"
25
26
  border
26
27
  borderStyle="rounded"
27
- borderColor="#525252"
28
- backgroundColor="#1C1C1E"
28
+ borderColor={theme.colors.dim}
29
+ backgroundColor={theme.surface.bg}
29
30
  paddingLeft={3}
30
31
  paddingRight={3}
31
32
  paddingTop={1}
@@ -33,16 +34,23 @@ export function InputModal({
33
34
  width={60}
34
35
  >
35
36
  <box marginBottom={1}>
36
- <text fg="#EDEDED">
37
+ <text fg={theme.colors.text}>
37
38
  <strong>{title}</strong>
38
39
  </text>
39
40
  </box>
40
41
 
41
42
  <box marginBottom={1}>
42
- <text fg="#A1A1AA">{label}</text>
43
+ <text fg={theme.colors.muted}>{label}</text>
43
44
  </box>
44
45
 
45
- <box border borderStyle="rounded" borderColor="#3F3F46" paddingLeft={1} paddingRight={1} width={54}>
46
+ <box
47
+ border
48
+ borderStyle="rounded"
49
+ borderColor={theme.colors.dim}
50
+ paddingLeft={1}
51
+ paddingRight={1}
52
+ width={54}
53
+ >
46
54
  <input
47
55
  value={defaultValue}
48
56
  onSubmit={onSubmit as any}
@@ -52,7 +60,7 @@ export function InputModal({
52
60
  </box>
53
61
 
54
62
  <box marginTop={1}>
55
- <text fg="#71717A">↵ to confirm • Esc to cancel</text>
63
+ <text fg={theme.colors.muted}>↵ to confirm • Esc to cancel</text>
56
64
  </box>
57
65
  </box>
58
66
  );
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
+ import { theme } from "../../theme.js";
2
3
 
3
4
  interface LoadingModalProps {
4
5
  /** Loading message — supports multiline (first line = status, subsequent = detail) */
@@ -27,20 +28,20 @@ export function LoadingModal({ message }: LoadingModalProps) {
27
28
  flexDirection="column"
28
29
  border
29
30
  borderStyle="rounded"
30
- borderColor="#525252"
31
- backgroundColor="#1C1C1E"
31
+ borderColor={theme.colors.dim}
32
+ backgroundColor={theme.surface.bg}
32
33
  paddingLeft={3}
33
34
  paddingRight={3}
34
35
  paddingTop={1}
35
36
  paddingBottom={1}
36
37
  >
37
38
  <box flexDirection="row">
38
- <text fg="#A1A1AA">{SPINNER_FRAMES[frame]}</text>
39
- <text fg="#EDEDED"> {mainMessage}</text>
39
+ <text fg={theme.colors.muted}>{SPINNER_FRAMES[frame]}</text>
40
+ <text fg={theme.colors.text}> {mainMessage}</text>
40
41
  </box>
41
42
  {detail ? (
42
43
  <box marginTop={0} paddingLeft={2}>
43
- <text fg="#525252">{detail}</text>
44
+ <text fg={theme.colors.dim}>{detail}</text>
44
45
  </box>
45
46
  ) : null}
46
47
  </box>
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { useKeyboard } from "../../hooks/useKeyboard.js";
3
+ import { theme } from "../../theme.js";
3
4
 
4
5
  interface MessageModalProps {
5
6
  /** Modal title */
@@ -13,9 +14,9 @@ interface MessageModalProps {
13
14
  }
14
15
 
15
16
  const variantConfig = {
16
- info: { icon: "ℹ", color: "#60A5FA" },
17
- success: { icon: "✓", color: "#4ADE80" },
18
- error: { icon: "✗", color: "#F87171" },
17
+ info: { icon: "ℹ", color: theme.colors.link },
18
+ success: { icon: "✓", color: theme.colors.success },
19
+ error: { icon: "✗", color: theme.colors.danger },
19
20
  } as const;
20
21
 
21
22
  export function MessageModal({
@@ -36,8 +37,8 @@ export function MessageModal({
36
37
  flexDirection="column"
37
38
  border
38
39
  borderStyle="rounded"
39
- borderColor="#525252"
40
- backgroundColor="#1C1C1E"
40
+ borderColor={theme.colors.dim}
41
+ backgroundColor={theme.surface.bg}
41
42
  paddingLeft={3}
42
43
  paddingRight={3}
43
44
  paddingTop={1}
@@ -46,17 +47,17 @@ export function MessageModal({
46
47
  >
47
48
  <box marginBottom={1}>
48
49
  <text fg={config.color}>{config.icon} </text>
49
- <text fg="#EDEDED">
50
+ <text fg={theme.colors.text}>
50
51
  <strong>{title}</strong>
51
52
  </text>
52
53
  </box>
53
54
 
54
55
  <box marginBottom={1}>
55
- <text fg="#A1A1AA">{message}</text>
56
+ <text fg={theme.colors.muted}>{message}</text>
56
57
  </box>
57
58
 
58
59
  <box marginTop={1}>
59
- <text fg="#71717A">Press any key to continue</text>
60
+ <text fg={theme.colors.muted}>Press any key to continue</text>
60
61
  </box>
61
62
  </box>
62
63
  );
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import type { SelectOption } from "../../state/types.js";
3
3
  import { useDimensions } from "../../state/DimensionsContext.js";
4
+ import { theme } from "../../theme.js";
4
5
 
5
6
  interface SelectModalProps {
6
7
  /** Modal title */
@@ -44,8 +45,8 @@ export function SelectModal({
44
45
  flexDirection="column"
45
46
  border
46
47
  borderStyle="rounded"
47
- borderColor="#525252"
48
- backgroundColor="#1C1C1E"
48
+ borderColor={theme.colors.dim}
49
+ backgroundColor={theme.surface.bg}
49
50
  paddingLeft={3}
50
51
  paddingRight={3}
51
52
  paddingTop={1}
@@ -53,21 +54,24 @@ export function SelectModal({
53
54
  width={width}
54
55
  >
55
56
  <box marginBottom={1}>
56
- <text fg="#EDEDED">
57
+ <text fg={theme.colors.text}>
57
58
  <strong>{title}</strong>
58
59
  </text>
59
60
  </box>
60
61
 
61
62
  <box marginBottom={1}>
62
- <text fg="#A1A1AA">{message}</text>
63
+ <text fg={theme.colors.muted}>{message}</text>
63
64
  </box>
64
65
 
65
66
  <box flexDirection="column" paddingLeft={1}>
66
67
  {options.map((option, idx) => {
67
68
  const isSelected = idx === selectedIndex;
68
69
  return (
69
- <text key={option.value} fg={isSelected ? "#F4F4F5" : "#A1A1AA"}>
70
- <span fg={isSelected ? "#F4F4F5" : "#71717A"}>
70
+ <text
71
+ key={option.value}
72
+ fg={isSelected ? theme.colors.text : theme.colors.muted}
73
+ >
74
+ <span fg={isSelected ? theme.colors.text : theme.colors.muted}>
71
75
  {isSelected ? "❯ " : " "}
72
76
  </span>
73
77
  {isSelected ? <strong>{option.label}</strong> : option.label}
@@ -77,7 +81,7 @@ export function SelectModal({
77
81
  </box>
78
82
 
79
83
  <box marginTop={1}>
80
- <text fg="#71717A">↑↓ Select • ↵ Confirm • Esc Cancel</text>
84
+ <text fg={theme.colors.muted}>↑↓ Select • ↵ Confirm • Esc Cancel</text>
81
85
  </box>
82
86
  </box>
83
87
  );