claudeup 4.19.0 → 4.23.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.
- package/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-badge.test.ts +91 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +7 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +0 -4
- package/src/ui/adapters/pluginsAdapter.ts +13 -0
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +0 -101
- package/src/ui/state/types.ts +0 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -205
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -1018
- package/src/services/alias-store.js +0 -129
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -499
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -355
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/FlagDetailEditor.js +0 -0
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1111
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -769
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -547
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the flat list of items for the SkillsScreen list panel.
|
|
3
|
-
* Extracted from SkillsScreen so it can be tested independently.
|
|
4
|
-
*/
|
|
5
|
-
export function buildSkillBrowserItems({ recommended, popular, installed, searchResults, query, isSearchLoading, skillSets = [], expandedSets = new Set(), }) {
|
|
6
|
-
const lowerQuery = query.toLowerCase();
|
|
7
|
-
const items = [];
|
|
8
|
-
// ── INSTALLED: always shown at top (if any) ──
|
|
9
|
-
const installedFiltered = lowerQuery
|
|
10
|
-
? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
|
|
11
|
-
: installed;
|
|
12
|
-
if (installedFiltered.length > 0) {
|
|
13
|
-
items.push({
|
|
14
|
-
id: "cat:installed",
|
|
15
|
-
kind: "category",
|
|
16
|
-
label: `Installed (${installedFiltered.length})`,
|
|
17
|
-
title: "Installed",
|
|
18
|
-
categoryKey: "installed",
|
|
19
|
-
count: installedFiltered.length,
|
|
20
|
-
tone: "purple",
|
|
21
|
-
star: "\u25CF ",
|
|
22
|
-
});
|
|
23
|
-
for (const skill of installedFiltered) {
|
|
24
|
-
items.push({
|
|
25
|
-
id: `skill:${skill.id}`,
|
|
26
|
-
kind: "skill",
|
|
27
|
-
label: skill.name,
|
|
28
|
-
skill,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// ── RECOMMENDED: skill sets + individual skills, all as first-class items ──
|
|
33
|
-
const filteredSets = lowerQuery
|
|
34
|
-
? skillSets.filter((s) => {
|
|
35
|
-
if (s.name.toLowerCase().includes(lowerQuery))
|
|
36
|
-
return true;
|
|
37
|
-
if (s.description.toLowerCase().includes(lowerQuery))
|
|
38
|
-
return true;
|
|
39
|
-
if (s.loaded && s.skills.some((sk) => sk.name.toLowerCase().includes(lowerQuery)))
|
|
40
|
-
return true;
|
|
41
|
-
return false;
|
|
42
|
-
})
|
|
43
|
-
: skillSets;
|
|
44
|
-
const filteredRec = lowerQuery
|
|
45
|
-
? recommended.filter((s) => s.name.toLowerCase().includes(lowerQuery) ||
|
|
46
|
-
(s.description || "").toLowerCase().includes(lowerQuery))
|
|
47
|
-
: recommended;
|
|
48
|
-
const recommendedCount = filteredSets.length + filteredRec.length;
|
|
49
|
-
items.push({
|
|
50
|
-
id: "cat:recommended",
|
|
51
|
-
kind: "category",
|
|
52
|
-
label: "Recommended",
|
|
53
|
-
title: "Recommended",
|
|
54
|
-
categoryKey: "recommended",
|
|
55
|
-
count: recommendedCount,
|
|
56
|
-
tone: "green",
|
|
57
|
-
star: "\u2605 ",
|
|
58
|
-
});
|
|
59
|
-
// Skill sets first within recommended
|
|
60
|
-
for (const set of filteredSets) {
|
|
61
|
-
const isExpanded = expandedSets.has(set.id);
|
|
62
|
-
items.push({
|
|
63
|
-
id: `skillset:${set.id}`,
|
|
64
|
-
kind: "skillset",
|
|
65
|
-
label: set.name,
|
|
66
|
-
skillSet: set,
|
|
67
|
-
expanded: isExpanded,
|
|
68
|
-
});
|
|
69
|
-
// When expanded, show child skills as indented skill items
|
|
70
|
-
if (isExpanded && set.loaded) {
|
|
71
|
-
const childSkills = lowerQuery
|
|
72
|
-
? set.skills.filter((sk) => sk.name.toLowerCase().includes(lowerQuery))
|
|
73
|
-
: set.skills;
|
|
74
|
-
for (const skill of childSkills) {
|
|
75
|
-
items.push({
|
|
76
|
-
id: `skill:${skill.id}`,
|
|
77
|
-
kind: "skill",
|
|
78
|
-
label: skill.name,
|
|
79
|
-
skill,
|
|
80
|
-
indent: 2,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Then individual recommended skills
|
|
86
|
-
for (const skill of filteredRec) {
|
|
87
|
-
items.push({
|
|
88
|
-
id: `skill:${skill.id}`,
|
|
89
|
-
kind: "skill",
|
|
90
|
-
label: skill.name,
|
|
91
|
-
skill,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
// ── SEARCH MODE ──
|
|
95
|
-
if (query.length >= 2) {
|
|
96
|
-
if (!isSearchLoading && searchResults.length > 0) {
|
|
97
|
-
const recNames = new Set(recommended.map((s) => s.name));
|
|
98
|
-
const deduped = searchResults
|
|
99
|
-
.filter((s) => !recNames.has(s.name))
|
|
100
|
-
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
101
|
-
if (deduped.length > 0) {
|
|
102
|
-
items.push({
|
|
103
|
-
id: "cat:search",
|
|
104
|
-
kind: "category",
|
|
105
|
-
label: `Search (${deduped.length})`,
|
|
106
|
-
title: "Search",
|
|
107
|
-
categoryKey: "popular",
|
|
108
|
-
count: deduped.length,
|
|
109
|
-
tone: "teal",
|
|
110
|
-
});
|
|
111
|
-
for (const skill of deduped) {
|
|
112
|
-
items.push({
|
|
113
|
-
id: `skill:${skill.id}`,
|
|
114
|
-
kind: "skill",
|
|
115
|
-
label: skill.name,
|
|
116
|
-
skill,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return items;
|
|
122
|
-
}
|
|
123
|
-
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
124
|
-
// Dedup by name — API can return same skill name from different repos
|
|
125
|
-
const seenPopular = new Set();
|
|
126
|
-
const popularDeduped = popular
|
|
127
|
-
.filter((s) => (s.stars ?? 0) >= 5)
|
|
128
|
-
.filter((s) => {
|
|
129
|
-
if (seenPopular.has(s.name))
|
|
130
|
-
return false;
|
|
131
|
-
seenPopular.add(s.name);
|
|
132
|
-
return true;
|
|
133
|
-
});
|
|
134
|
-
if (popularDeduped.length > 0) {
|
|
135
|
-
items.push({
|
|
136
|
-
id: "cat:popular",
|
|
137
|
-
kind: "category",
|
|
138
|
-
label: "Popular",
|
|
139
|
-
title: "Popular",
|
|
140
|
-
categoryKey: "popular",
|
|
141
|
-
count: popularDeduped.length,
|
|
142
|
-
tone: "teal",
|
|
143
|
-
});
|
|
144
|
-
for (const skill of popularDeduped) {
|
|
145
|
-
items.push({
|
|
146
|
-
id: `skill:${skill.id}`,
|
|
147
|
-
kind: "skill",
|
|
148
|
-
label: skill.name,
|
|
149
|
-
skill,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return items;
|
|
154
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
export function CategoryHeader({ title, status, statusColor = "green", expanded = true, count, }) {
|
|
3
|
-
const expandIcon = expanded ? "▼" : "▶";
|
|
4
|
-
const countBadge = count !== undefined ? ` (${count})` : "";
|
|
5
|
-
const statusText = status ? ` ${status}` : "";
|
|
6
|
-
// Simple format without dynamic line calculation
|
|
7
|
-
return (_jsxs("text", { children: [_jsx("span", { fg: "#666666", children: expandIcon }), _jsx("span", { fg: "white", children: _jsxs("strong", { children: [" ", title] }) }), _jsx("span", { fg: "#666666", children: countBadge }), _jsx("span", { fg: "#666666", children: " \u2500\u2500\u2500\u2500" }), _jsx("span", { fg: statusColor, children: statusText })] }));
|
|
8
|
-
}
|
|
9
|
-
export default CategoryHeader;
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
export function EmptyFilterState({ query, entityName = "items", }) {
|
|
3
|
-
return (_jsxs("box", { flexDirection: "column", marginTop: 2, paddingLeft: 2, paddingRight: 2, children: [_jsxs("text", { fg: "yellow", children: ["No ", entityName, " found for \"", query, "\""] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Try a different search term, or if you think" }) }), _jsx("text", { fg: "gray", children: "this is a bug, report it at:" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#5c9aff", children: "github.com/MadAppGang/magus/issues" }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the filter." }) })] }));
|
|
4
|
-
}
|
|
Binary file
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* Scope indicator for list items.
|
|
4
|
-
* Shows colored letter badges when installed, empty space when not.
|
|
5
|
-
*
|
|
6
|
-
* Installed in user+project: u p
|
|
7
|
-
* Installed in project only: p
|
|
8
|
-
* Not installed: (3 spaces)
|
|
9
|
-
*
|
|
10
|
-
* Colors match the detail panel: cyan=user, green=project, yellow=local
|
|
11
|
-
*/
|
|
12
|
-
export function scopeIndicatorText(user, project, local) {
|
|
13
|
-
const segments = [
|
|
14
|
-
user
|
|
15
|
-
? { char: "u", bg: "cyan", fg: "black" }
|
|
16
|
-
: { char: " ", bg: "", fg: "" },
|
|
17
|
-
project
|
|
18
|
-
? { char: "p", bg: "green", fg: "black" }
|
|
19
|
-
: { char: " ", bg: "", fg: "" },
|
|
20
|
-
local
|
|
21
|
-
? { char: "l", bg: "yellow", fg: "black" }
|
|
22
|
-
: { char: " ", bg: "", fg: "" },
|
|
23
|
-
];
|
|
24
|
-
const text = segments.map((s) => s.char).join("");
|
|
25
|
-
return { text, segments };
|
|
26
|
-
}
|
|
27
|
-
export function ScopeIndicator({ user, project, local }) {
|
|
28
|
-
const { segments } = scopeIndicatorText(user, project, local);
|
|
29
|
-
return (_jsx("text", { children: segments.map((s, i) => s.bg ? (_jsx("span", { bg: s.bg, fg: s.fg, children: s.char }, i)) : (_jsx("span", { children: " " }, i))) }));
|
|
30
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useMemo } from "react";
|
|
3
|
-
export function ScrollableList({ items, selectedIndex, renderItem, maxHeight, showScrollIndicators = true, getKey, }) {
|
|
4
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
5
|
-
const hasItemsAbove = scrollOffset > 0;
|
|
6
|
-
const hasItemsBelow = scrollOffset + maxHeight < items.length;
|
|
7
|
-
const indicatorLines = (showScrollIndicators && hasItemsAbove ? 1 : 0) +
|
|
8
|
-
(showScrollIndicators && hasItemsBelow ? 1 : 0);
|
|
9
|
-
const effectiveMaxHeight = Math.max(1, maxHeight - indicatorLines);
|
|
10
|
-
// Adjust scroll offset to keep selected item visible
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (selectedIndex < scrollOffset) {
|
|
13
|
-
setScrollOffset(selectedIndex);
|
|
14
|
-
}
|
|
15
|
-
else if (selectedIndex >= scrollOffset + effectiveMaxHeight) {
|
|
16
|
-
setScrollOffset(selectedIndex - effectiveMaxHeight + 1);
|
|
17
|
-
}
|
|
18
|
-
}, [selectedIndex, effectiveMaxHeight, scrollOffset]);
|
|
19
|
-
// Reset scroll when items change drastically (e.g. search)
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (scrollOffset >= items.length) {
|
|
22
|
-
setScrollOffset(Math.max(0, items.length - effectiveMaxHeight));
|
|
23
|
-
}
|
|
24
|
-
}, [items.length, scrollOffset, effectiveMaxHeight]);
|
|
25
|
-
const visibleItems = useMemo(() => {
|
|
26
|
-
const start = scrollOffset;
|
|
27
|
-
const end = Math.min(scrollOffset + effectiveMaxHeight, items.length);
|
|
28
|
-
return items.slice(start, end).map((item, idx) => ({
|
|
29
|
-
item,
|
|
30
|
-
originalIndex: start + idx,
|
|
31
|
-
}));
|
|
32
|
-
}, [items, scrollOffset, effectiveMaxHeight]);
|
|
33
|
-
const itemsBelow = items.length - scrollOffset - effectiveMaxHeight;
|
|
34
|
-
return (_jsxs("box", { flexDirection: "column", children: [showScrollIndicators && hasItemsAbove && (_jsxs("text", { fg: "cyan", children: ["\u2191 ", scrollOffset, " more"] })), visibleItems.map(({ item, originalIndex }) => (_jsx("box", { width: "100%", overflow: "hidden", children: renderItem(item, originalIndex, originalIndex === selectedIndex) }, getKey ? getKey(item, originalIndex) : `${originalIndex}`))), showScrollIndicators && hasItemsBelow && (_jsxs("text", { fg: "cyan", children: ["\u2193 ", itemsBelow, " more"] }))] }));
|
|
35
|
-
}
|
|
36
|
-
export default ScrollableList;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
|
|
3
|
-
export function SearchInput({ value, onChange, placeholder = "Search...", isActive, onExit, onSubmit, }) {
|
|
4
|
-
// Handle keyboard shortcuts when active
|
|
5
|
-
useKeyboardHandler((_input, key) => {
|
|
6
|
-
if (!isActive)
|
|
7
|
-
return;
|
|
8
|
-
if (key.escape) {
|
|
9
|
-
onExit?.();
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
if (key.return) {
|
|
13
|
-
onSubmit?.();
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
return (_jsxs("box", { flexDirection: "row", children: [_jsx("text", { fg: "cyan", children: "\u276F " }), _jsx("input", { value: value, onChange: onChange, placeholder: placeholder, focused: isActive, width: 40 })] }));
|
|
18
|
-
}
|
|
19
|
-
export default SearchInput;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* StyledText - Utility component to simplify text styling with OpenTUI
|
|
4
|
-
*
|
|
5
|
-
* Converts props to nested tag pattern required by OpenTUI.
|
|
6
|
-
*
|
|
7
|
-
* Example:
|
|
8
|
-
* <StyledText bold color="green">Success</StyledText>
|
|
9
|
-
* becomes: <text fg="green"><strong>Success</strong></text>
|
|
10
|
-
*/
|
|
11
|
-
export function StyledText({ children, bold = false, color, bgColor, dim = false, italic = false, underline = false, inverse = false, }) {
|
|
12
|
-
let content = children;
|
|
13
|
-
// Apply text modifiers (innermost to outermost)
|
|
14
|
-
if (bold) {
|
|
15
|
-
content = _jsx("strong", { children: content });
|
|
16
|
-
}
|
|
17
|
-
if (italic) {
|
|
18
|
-
content = _jsx("em", { children: content });
|
|
19
|
-
}
|
|
20
|
-
if (underline) {
|
|
21
|
-
content = _jsx("u", { children: content });
|
|
22
|
-
}
|
|
23
|
-
// Note: dim and inverse use span with colors since OpenTUI doesn't have these as React elements
|
|
24
|
-
if (dim) {
|
|
25
|
-
content = _jsx("span", { fg: "#666666", children: content });
|
|
26
|
-
}
|
|
27
|
-
if (inverse) {
|
|
28
|
-
// Inverse effect approximated with contrasting colors
|
|
29
|
-
content = (_jsx("span", { fg: "black", bg: "white", children: content }));
|
|
30
|
-
}
|
|
31
|
-
// Apply colors (outer layer)
|
|
32
|
-
const textProps = {};
|
|
33
|
-
if (color)
|
|
34
|
-
textProps.fg = color;
|
|
35
|
-
if (bgColor)
|
|
36
|
-
textProps.bg = bgColor;
|
|
37
|
-
return _jsx("text", { ...textProps, children: content });
|
|
38
|
-
}
|
|
39
|
-
export default StyledText;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
const TABS = [
|
|
3
|
-
{ key: "1", label: "Plugins", screen: "plugins" },
|
|
4
|
-
{ key: "2", label: "Skills", screen: "skills" },
|
|
5
|
-
{ key: "3", label: "MCP", screen: "mcp" },
|
|
6
|
-
{ key: "4", label: "Settings", screen: "settings" },
|
|
7
|
-
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
8
|
-
{ key: "6", label: "CLI", screen: "cli-tools" },
|
|
9
|
-
{ key: "7", label: "Git State", screen: "gitignore" },
|
|
10
|
-
{ key: "8", label: "Alias", screen: "alias" },
|
|
11
|
-
];
|
|
12
|
-
export function TabBar({ currentScreen }) {
|
|
13
|
-
return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
|
|
14
|
-
const isSelected = tab.screen === currentScreen;
|
|
15
|
-
const isLast = index === TABS.length - 1;
|
|
16
|
-
return (_jsxs("box", { flexDirection: "row", children: [isSelected ? (_jsx("box", { children: _jsx("text", { bg: "#7e57c2", fg: "white", children: _jsxs("strong", { children: [" ", tab.key, ":", tab.label, " "] }) }) })) : (_jsx("box", { children: _jsxs("text", { fg: "gray", children: [" ", tab.key, ":", tab.label, " "] }) })), !isLast && _jsx("text", { fg: "#666666", children: "\u2502" })] }, tab.key));
|
|
17
|
-
}) }));
|
|
18
|
-
}
|
|
19
|
-
export default TabBar;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
const KEY_BG = "#3f3f46";
|
|
3
|
-
const KEY_FG = "#fafafa";
|
|
4
|
-
const LABEL_FG = "gray";
|
|
5
|
-
const GAP = " ";
|
|
6
|
-
/**
|
|
7
|
-
* Render a row of footer hints as key badges + labels:
|
|
8
|
-
*
|
|
9
|
-
* [↑↓] move [U] update [/] search
|
|
10
|
-
*
|
|
11
|
-
* Each key is shown in a chip so it reads as a pressable key, not prose.
|
|
12
|
-
* Shared across screens so every footer looks the same.
|
|
13
|
-
*/
|
|
14
|
-
export function FooterHints({ hints }) {
|
|
15
|
-
const nodes = [];
|
|
16
|
-
hints.forEach((hint, i) => {
|
|
17
|
-
if (i > 0) {
|
|
18
|
-
nodes.push(_jsx("span", { fg: LABEL_FG, children: GAP }, `gap-${i}`));
|
|
19
|
-
}
|
|
20
|
-
nodes.push(_jsx("span", { bg: KEY_BG, fg: KEY_FG, children: ` ${hint.keys.join("")} ` }, `k-${i}`));
|
|
21
|
-
nodes.push(_jsx("span", { fg: LABEL_FG, children: ` ${hint.label}` }, `l-${i}`));
|
|
22
|
-
});
|
|
23
|
-
return _jsx("text", { children: nodes });
|
|
24
|
-
}
|
|
25
|
-
/** Convenience for callers that already have a node: same as <FooterHints>. */
|
|
26
|
-
export function renderFooterHints(hints) {
|
|
27
|
-
return _jsx(FooterHints, { hints: hints });
|
|
28
|
-
}
|
|
29
|
-
export default FooterHints;
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
export function Panel({ title, children, borderColor = "#7e57c2", titleColor = "#7e57c2", width, height, flexGrow = 1, focused = false, }) {
|
|
3
|
-
const activeColor = focused ? "#7e57c2" : borderColor;
|
|
4
|
-
return (_jsxs("box", { flexDirection: "column", width: width, height: height, flexGrow: flexGrow, border: true, borderStyle: "single", borderColor: activeColor, padding: 1, children: [title && (_jsx("box", { marginBottom: 0, children: _jsx("text", { fg: titleColor, children: _jsx("strong", { children: title }) }) })), _jsx("box", { flexDirection: "column", flexGrow: 1, children: children })] }));
|
|
5
|
-
}
|
|
6
|
-
export default Panel;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
export function ProgressBar({ message, current, total }) {
|
|
3
|
-
const isDeterminate = current !== undefined && total !== undefined && total > 0;
|
|
4
|
-
if (isDeterminate) {
|
|
5
|
-
const barWidth = 20;
|
|
6
|
-
const filled = Math.round((current / total) * barWidth);
|
|
7
|
-
const empty = barWidth - filled;
|
|
8
|
-
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
9
|
-
return (_jsxs("box", { children: [_jsx("text", { fg: "cyan", children: "\u27F3" }), _jsxs("text", { children: [" ", message, " "] }), _jsxs("text", { fg: "cyan", children: ["[", bar, "] ", current, "/", total] })] }));
|
|
10
|
-
}
|
|
11
|
-
// Indeterminate progress
|
|
12
|
-
return (_jsxs("box", { children: [_jsx("text", { fg: "cyan", children: "\u27F3" }), _jsxs("text", { children: [" ", message] }), _jsx("text", { fg: "gray", children: " ..." })] }));
|
|
13
|
-
}
|
|
14
|
-
export default ProgressBar;
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
export function ScopeTabs({ scope, onToggle: _onToggle, toggleHint, }) {
|
|
3
|
-
const isProject = scope === "project";
|
|
4
|
-
return (_jsxs("box", { marginBottom: 1, flexDirection: "row", gap: 1, children: [_jsx("box", { children: isProject ? (_jsx("text", { bg: "cyan", fg: "black", children: _jsx("strong", { children: " \u25C6 Project " }) })) : (_jsx("text", { fg: "gray", children: " \u25CB Project " })) }), _jsx("box", { children: !isProject ? (_jsx("text", { bg: "magenta", fg: "white", children: _jsx("strong", { children: " \u25C6 Global " }) })) : (_jsx("text", { fg: "gray", children: " \u25CB Global " })) }), toggleHint && (_jsx("box", { marginLeft: 2, children: _jsxs("text", { fg: "#666666", children: ["(", toggleHint, ")"] }) }))] }));
|
|
5
|
-
}
|
|
6
|
-
export default ScopeTabs;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
3
|
-
import { TabBar } from "../TabBar.js";
|
|
4
|
-
import { FooterHints } from "./FooterHints.js";
|
|
5
|
-
const HEADER_COLOR = "#7e57c2";
|
|
6
|
-
export function ScreenLayout({ title, subtitle, currentScreen, search, statusLine, footerHints, listPanel, detailPanel, }) {
|
|
7
|
-
const dimensions = useDimensions();
|
|
8
|
-
const hasSearchBar = search && (search.isActive || search.query);
|
|
9
|
-
// Fixed chrome: top line + tabs + line + header + separator + footer = 6
|
|
10
|
-
// Search bar adds 1 when active
|
|
11
|
-
const fixedHeight = 6 + (hasSearchBar ? 1 : 0);
|
|
12
|
-
const panelHeight = Math.max(5, dimensions.contentHeight - fixedHeight);
|
|
13
|
-
const lineWidth = Math.max(10, dimensions.terminalWidth - 4);
|
|
14
|
-
return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(TabBar, { currentScreen: currentScreen }) }), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#333333", children: "─".repeat(lineWidth) }) }), _jsxs("box", { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: HEADER_COLOR, children: _jsx("strong", { children: title }) }), subtitle && _jsx("text", { fg: "gray", children: subtitle }), !subtitle && statusLine ? statusLine : null] }), hasSearchBar && (_jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: search.isActive ? (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "white", children: search.query }), _jsx("span", { bg: "white", fg: "black", children: " " })] })) : (_jsxs("text", { children: [_jsx("span", { fg: "green", children: "Filter: " }), _jsx("span", { fg: "yellow", children: search.query }), _jsx("span", { fg: "gray", children: " (Esc to clear)" })] })) })), _jsx("box", { height: 1, paddingLeft: 1, paddingRight: 1, children: _jsx("text", { fg: "#444444", children: "─".repeat(lineWidth) }) }), _jsxs("box", { flexDirection: "row", height: panelHeight, children: [_jsx("box", { flexDirection: "column", width: "49%", height: panelHeight, paddingRight: 1, children: listPanel }), _jsx("box", { flexDirection: "column", width: 1, height: panelHeight, children: _jsx("text", { fg: "#444444", children: "│".repeat(panelHeight) }) }), _jsx("box", { width: "50%", height: panelHeight, paddingLeft: 1, children: _jsx("scrollbox", { height: panelHeight, scrollY: true, scrollX: false, children: _jsx("box", { flexDirection: "column", children: detailPanel }) }) })] }), _jsx("box", { height: 1, paddingLeft: 1, children: Array.isArray(footerHints) ? (_jsx(FooterHints, { hints: footerHints })) : typeof footerHints === "string" ? (_jsx("text", { fg: "gray", children: footerHints })) : (footerHints) })] }));
|
|
15
|
-
}
|
|
16
|
-
export default ScreenLayout;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
3
|
-
export function ConfirmModal({ title, message, onConfirm, onCancel, }) {
|
|
4
|
-
useKeyboard((key) => {
|
|
5
|
-
if (key.name === "y" || key.name === "Y") {
|
|
6
|
-
onConfirm();
|
|
7
|
-
}
|
|
8
|
-
else if (key.name === "n" || key.name === "N" || key.name === "escape") {
|
|
9
|
-
onCancel();
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsxs("box", { marginTop: 1, flexDirection: "row", children: [_jsx("text", { fg: "#71717A", children: "Press " }), _jsx("text", { fg: "#EDEDED", children: "Y" }), _jsx("text", { fg: "#71717A", children: " to confirm \u2022 " }), _jsx("text", { fg: "#EDEDED", children: "N" }), _jsx("text", { fg: "#71717A", children: " to cancel" })] })] }));
|
|
13
|
-
}
|
|
14
|
-
export default ConfirmModal;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
export function InputModal({ title, label, defaultValue = "", onSubmit, }) {
|
|
3
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: label }) }), _jsx("box", { border: true, borderStyle: "rounded", borderColor: "#3F3F46", paddingLeft: 1, paddingRight: 1, width: 54, children: _jsx("input", { value: defaultValue, onSubmit: onSubmit, focused: true, width: 50 }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "\u21B5 to confirm \u2022 Esc to cancel" }) })] }));
|
|
4
|
-
}
|
|
5
|
-
export default InputModal;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
3
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
|
-
export function LoadingModal({ message }) {
|
|
5
|
-
const [frame, setFrame] = useState(0);
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
const interval = setInterval(() => {
|
|
8
|
-
setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
|
|
9
|
-
}, 80);
|
|
10
|
-
return () => clearInterval(interval);
|
|
11
|
-
}, []);
|
|
12
|
-
const lines = message.split("\n");
|
|
13
|
-
const mainMessage = lines[0];
|
|
14
|
-
const detail = lines.slice(1).join("\n");
|
|
15
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, children: [_jsxs("box", { flexDirection: "row", children: [_jsx("text", { fg: "#A1A1AA", children: SPINNER_FRAMES[frame] }), _jsxs("text", { fg: "#EDEDED", children: [" ", mainMessage] })] }), detail ? (_jsx("box", { marginTop: 0, paddingLeft: 2, children: _jsx("text", { fg: "#525252", children: detail }) })) : null] }));
|
|
16
|
-
}
|
|
17
|
-
export default LoadingModal;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
3
|
-
const variantConfig = {
|
|
4
|
-
info: { icon: "ℹ", color: "#60A5FA" },
|
|
5
|
-
success: { icon: "✓", color: "#4ADE80" },
|
|
6
|
-
error: { icon: "✗", color: "#F87171" },
|
|
7
|
-
};
|
|
8
|
-
export function MessageModal({ title, message, variant, onDismiss, }) {
|
|
9
|
-
const config = variantConfig[variant];
|
|
10
|
-
useKeyboard(() => {
|
|
11
|
-
// Any key dismisses
|
|
12
|
-
onDismiss();
|
|
13
|
-
});
|
|
14
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsxs("box", { marginBottom: 1, children: [_jsxs("text", { fg: config.color, children: [config.icon, " "] }), _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) })] }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "Press any key to continue" }) })] }));
|
|
15
|
-
}
|
|
16
|
-
export default MessageModal;
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import React, { useState } from "react";
|
|
3
|
-
import { useApp } from "../../state/AppContext.js";
|
|
4
|
-
import { useKeyboard } from "../../hooks/useKeyboard.js";
|
|
5
|
-
import { ConfirmModal } from "./ConfirmModal.js";
|
|
6
|
-
import { InputModal } from "./InputModal.js";
|
|
7
|
-
import { SelectModal } from "./SelectModal.js";
|
|
8
|
-
import { MessageModal } from "./MessageModal.js";
|
|
9
|
-
import { LoadingModal } from "./LoadingModal.js";
|
|
10
|
-
import { VersionMismatchModal } from "./VersionMismatchModal.js";
|
|
11
|
-
/**
|
|
12
|
-
* Container that renders the active modal as an overlay
|
|
13
|
-
* Handles ALL keyboard events when a modal is open to avoid
|
|
14
|
-
* conflicts with multiple useKeyboard hooks in child components
|
|
15
|
-
*
|
|
16
|
-
* Input modal: Enter/Escape handled by OpenTUI <input> onSubmit + ModalContainer escape
|
|
17
|
-
* Select modal: keyboard fully handled here (index tracking + Enter/arrows)
|
|
18
|
-
*/
|
|
19
|
-
export function ModalContainer() {
|
|
20
|
-
const { state } = useApp();
|
|
21
|
-
const { modal } = state;
|
|
22
|
-
// Track select modal index here (lifted from SelectModal)
|
|
23
|
-
const [selectIndex, setSelectIndex] = useState(0);
|
|
24
|
-
// Reset select index when modal changes
|
|
25
|
-
const modalRef = React.useRef(modal);
|
|
26
|
-
if (modal !== modalRef.current) {
|
|
27
|
-
modalRef.current = modal;
|
|
28
|
-
if (modal?.type === "select") {
|
|
29
|
-
setSelectIndex(modal.defaultIndex ?? 0);
|
|
30
|
-
}
|
|
31
|
-
else if (modal?.type === "version-mismatch") {
|
|
32
|
-
setSelectIndex(0);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
// Handle keyboard events for modals
|
|
36
|
-
useKeyboard((key) => {
|
|
37
|
-
if (!modal)
|
|
38
|
-
return;
|
|
39
|
-
if (modal.type === "loading")
|
|
40
|
-
return;
|
|
41
|
-
// Escape — close any modal
|
|
42
|
-
if (key.name === "escape") {
|
|
43
|
-
if (modal.type === "confirm")
|
|
44
|
-
modal.onCancel();
|
|
45
|
-
else if (modal.type === "input")
|
|
46
|
-
modal.onCancel();
|
|
47
|
-
else if (modal.type === "select")
|
|
48
|
-
modal.onCancel();
|
|
49
|
-
else if (modal.type === "message")
|
|
50
|
-
modal.onDismiss();
|
|
51
|
-
else if (modal.type === "version-mismatch")
|
|
52
|
-
modal.onDismiss();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
// 'q' to close — but NOT for input modals (need to type 'q')
|
|
56
|
-
if (key.name === "q" && modal.type !== "input") {
|
|
57
|
-
if (modal.type === "confirm")
|
|
58
|
-
modal.onCancel();
|
|
59
|
-
else if (modal.type === "select")
|
|
60
|
-
modal.onCancel();
|
|
61
|
-
else if (modal.type === "message")
|
|
62
|
-
modal.onDismiss();
|
|
63
|
-
else if (modal.type === "version-mismatch")
|
|
64
|
-
modal.onDismiss();
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
// Input modal — let OpenTUI <input> handle Enter via onSubmit
|
|
68
|
-
if (modal.type === "input") {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
// Select modal — handle navigation and selection
|
|
72
|
-
if (modal.type === "select") {
|
|
73
|
-
if (key.name === "return" || key.name === "enter") {
|
|
74
|
-
modal.onSelect(modal.options[selectIndex].value);
|
|
75
|
-
}
|
|
76
|
-
else if (key.name === "up" || key.name === "k") {
|
|
77
|
-
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
78
|
-
}
|
|
79
|
-
else if (key.name === "down" || key.name === "j") {
|
|
80
|
-
setSelectIndex((prev) => Math.min(modal.options.length - 1, prev + 1));
|
|
81
|
-
}
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Version-mismatch modal — handle navigation and selection (2 options)
|
|
85
|
-
if (modal.type === "version-mismatch") {
|
|
86
|
-
if (key.name === "return" || key.name === "enter") {
|
|
87
|
-
if (selectIndex === 0)
|
|
88
|
-
modal.onFix();
|
|
89
|
-
else
|
|
90
|
-
modal.onDismiss();
|
|
91
|
-
}
|
|
92
|
-
else if (key.name === "up" || key.name === "k") {
|
|
93
|
-
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
94
|
-
}
|
|
95
|
-
else if (key.name === "down" || key.name === "j") {
|
|
96
|
-
setSelectIndex((prev) => Math.min(1, prev + 1));
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
// Message modal — Enter to dismiss
|
|
101
|
-
if (modal.type === "message") {
|
|
102
|
-
if (key.name === "return" || key.name === "enter") {
|
|
103
|
-
modal.onDismiss();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Confirm modal — Enter to confirm
|
|
107
|
-
if (modal.type === "confirm") {
|
|
108
|
-
if (key.name === "return" || key.name === "enter") {
|
|
109
|
-
modal.onConfirm();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (!modal) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
const renderModal = () => {
|
|
117
|
-
switch (modal.type) {
|
|
118
|
-
case "confirm":
|
|
119
|
-
return (_jsx(ConfirmModal, { title: modal.title, message: modal.message, onConfirm: modal.onConfirm, onCancel: modal.onCancel }));
|
|
120
|
-
case "input":
|
|
121
|
-
return (_jsx(InputModal, { title: modal.title, label: modal.label, defaultValue: modal.defaultValue, onSubmit: modal.onSubmit, onCancel: modal.onCancel }));
|
|
122
|
-
case "select":
|
|
123
|
-
return (_jsx(SelectModal, { title: modal.title, message: modal.message, options: modal.options, defaultIndex: selectIndex, onSelect: modal.onSelect, onCancel: modal.onCancel }));
|
|
124
|
-
case "message":
|
|
125
|
-
return (_jsx(MessageModal, { title: modal.title, message: modal.message, variant: modal.variant, onDismiss: modal.onDismiss }));
|
|
126
|
-
case "loading":
|
|
127
|
-
return _jsx(LoadingModal, { message: modal.message });
|
|
128
|
-
case "version-mismatch":
|
|
129
|
-
return (_jsx(VersionMismatchModal, { mismatches: modal.mismatches, defaultIndex: selectIndex }));
|
|
130
|
-
default:
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
// Center the modal on screen
|
|
135
|
-
return (_jsx("box", { position: "absolute", width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: renderModal() }));
|
|
136
|
-
}
|
|
137
|
-
export default ModalContainer;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
3
|
-
export function SelectModal({ title, message, options, defaultIndex, onSelect: _onSelect, onCancel: _onCancel, }) {
|
|
4
|
-
// Keyboard handling is done by ModalContainer
|
|
5
|
-
// defaultIndex is the live selectedIndex from ModalContainer state
|
|
6
|
-
const selectedIndex = defaultIndex ?? 0;
|
|
7
|
-
// Size to terminal width: 60% of available, clamped to [50, 90].
|
|
8
|
-
// Previously hardcoded at 50, which forced descriptions and long item
|
|
9
|
-
// labels (e.g. plugin specs) to wrap mid-sentence and produce orphan
|
|
10
|
-
// punctuation lines.
|
|
11
|
-
const dimensions = useDimensions();
|
|
12
|
-
const width = Math.max(50, Math.min(90, Math.floor(dimensions.terminalWidth * 0.6)));
|
|
13
|
-
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: width, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { flexDirection: "column", paddingLeft: 1, children: options.map((option, idx) => {
|
|
14
|
-
const isSelected = idx === selectedIndex;
|
|
15
|
-
return (_jsxs("text", { fg: isSelected ? "#F4F4F5" : "#A1A1AA", children: [_jsx("span", { fg: isSelected ? "#F4F4F5" : "#71717A", children: isSelected ? "❯ " : " " }), isSelected ? _jsx("strong", { children: option.label }) : option.label] }, option.value));
|
|
16
|
-
}) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
|
|
17
|
-
}
|
|
18
|
-
export default SelectModal;
|