claudeup 4.6.1 → 4.8.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/package.json +1 -1
- package/src/__tests__/gap-fill-versions.test.ts +382 -0
- package/src/data/settings-catalog.js +2 -7
- package/src/data/settings-catalog.ts +2 -7
- package/src/opentui.d.ts +7 -2
- package/src/prerunner/index.js +31 -17
- package/src/prerunner/index.ts +35 -18
- package/src/services/claude-settings.js +74 -0
- package/src/services/claude-settings.ts +92 -0
- package/src/services/plugin-manager.js +13 -16
- package/src/services/plugin-manager.ts +17 -16
- package/src/services/settings-manager.js +84 -5
- package/src/services/settings-manager.ts +86 -5
- package/src/ui/adapters/settingsAdapter.js +8 -8
- package/src/ui/adapters/settingsAdapter.ts +8 -8
- package/src/ui/components/TabBar.js +1 -23
- package/src/ui/components/TabBar.tsx +1 -26
- package/src/ui/components/modals/ConfirmModal.js +1 -1
- package/src/ui/components/modals/ConfirmModal.tsx +17 -16
- package/src/ui/components/modals/InputModal.js +2 -13
- package/src/ui/components/modals/InputModal.tsx +21 -24
- package/src/ui/components/modals/LoadingModal.js +1 -1
- package/src/ui/components/modals/LoadingModal.tsx +6 -6
- package/src/ui/components/modals/MessageModal.js +4 -4
- package/src/ui/components/modals/MessageModal.tsx +13 -13
- package/src/ui/components/modals/ModalContainer.js +25 -2
- package/src/ui/components/modals/ModalContainer.tsx +25 -2
- package/src/ui/components/modals/SelectModal.js +3 -4
- package/src/ui/components/modals/SelectModal.tsx +18 -15
- package/src/ui/renderers/settingsRenderers.js +1 -1
- package/src/ui/renderers/settingsRenderers.tsx +5 -3
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +3 -1
- package/src/ui/screens/EnvVarsScreen.js +27 -10
- package/src/ui/screens/EnvVarsScreen.tsx +33 -16
- package/src/ui/screens/McpRegistryScreen.js +2 -2
- package/src/ui/screens/McpRegistryScreen.tsx +3 -1
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +2 -1
- package/src/ui/screens/ModelSelectorScreen.js +2 -2
- package/src/ui/screens/ModelSelectorScreen.tsx +3 -2
- package/src/ui/screens/ProfilesScreen.js +1 -1
- package/src/ui/screens/ProfilesScreen.tsx +2 -1
- package/src/ui/screens/StatusLineScreen.js +1 -1
- package/src/ui/screens/StatusLineScreen.tsx +2 -1
- package/src/ui/state/DimensionsContext.js +2 -2
- package/src/ui/state/DimensionsContext.tsx +3 -3
- package/src/ui/components/ScrollableDetail.js +0 -23
- package/src/ui/components/ScrollableDetail.tsx +0 -55
|
@@ -3,19 +3,20 @@ import { useApp, useModal } from "../state/AppContext.js";
|
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
6
|
import {
|
|
8
7
|
SETTINGS_CATALOG,
|
|
9
8
|
} from "../../data/settings-catalog.js";
|
|
10
9
|
import {
|
|
11
10
|
readAllSettingsBothScopes,
|
|
12
11
|
writeSettingValue,
|
|
12
|
+
discoverOutputStyles,
|
|
13
13
|
} from "../../services/settings-manager.js";
|
|
14
14
|
import {
|
|
15
15
|
buildSettingsBrowserItems,
|
|
16
16
|
type SettingsBrowserItem,
|
|
17
17
|
} from "../adapters/settingsAdapter.js";
|
|
18
18
|
import { renderSettingRow, renderSettingDetail } from "../renderers/settingsRenderers.js";
|
|
19
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
19
20
|
|
|
20
21
|
export function SettingsScreen() {
|
|
21
22
|
const { state, dispatch } = useApp();
|
|
@@ -26,6 +27,12 @@ export function SettingsScreen() {
|
|
|
26
27
|
const fetchData = useCallback(async () => {
|
|
27
28
|
dispatch({ type: "SETTINGS_DATA_LOADING" });
|
|
28
29
|
try {
|
|
30
|
+
// Populate dynamic output style options from installed plugins
|
|
31
|
+
const outputStyleSetting = SETTINGS_CATALOG.find((s) => s.id === "output-style");
|
|
32
|
+
if (outputStyleSetting && outputStyleSetting.type === "select") {
|
|
33
|
+
outputStyleSetting.options = await discoverOutputStyles(state.projectPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
const values = await readAllSettingsBothScopes(
|
|
30
37
|
SETTINGS_CATALOG,
|
|
31
38
|
state.projectPath,
|
|
@@ -95,22 +102,32 @@ export function SettingsScreen() {
|
|
|
95
102
|
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
96
103
|
}
|
|
97
104
|
} else {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
scope
|
|
109
|
-
|
|
105
|
+
// String type: if already set, clear it; if unset, show input modal
|
|
106
|
+
if (currentValue !== undefined && currentValue !== "") {
|
|
107
|
+
try {
|
|
108
|
+
await writeSettingValue(setting, undefined, scope, state.projectPath);
|
|
109
|
+
await fetchData();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const newValue = await modal.input(
|
|
115
|
+
`${setting.name} — ${scope}`,
|
|
116
|
+
setting.description,
|
|
117
|
+
currentValue || setting.defaultValue || "",
|
|
110
118
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
if (newValue === null) return;
|
|
120
|
+
try {
|
|
121
|
+
await writeSettingValue(
|
|
122
|
+
setting,
|
|
123
|
+
newValue || undefined,
|
|
124
|
+
scope,
|
|
125
|
+
state.projectPath,
|
|
126
|
+
);
|
|
127
|
+
await fetchData();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
130
|
+
}
|
|
114
131
|
}
|
|
115
132
|
}
|
|
116
133
|
};
|
|
@@ -4,9 +4,9 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
|
|
|
4
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
5
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
6
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
7
|
import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
|
|
9
8
|
import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
|
|
9
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
10
10
|
/**
|
|
11
11
|
* Deduplicate servers by name, keeping only the latest version.
|
|
12
12
|
* Uses version string comparison, falling back to published_at date.
|
|
@@ -222,6 +222,6 @@ export function McpRegistryScreen() {
|
|
|
222
222
|
isActive: isSearchActive,
|
|
223
223
|
query: searchQuery,
|
|
224
224
|
placeholder: searchPlaceholder,
|
|
225
|
-
}, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
|
|
225
|
+
}, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight, getKey: (server) => server.name })), detailPanel: renderDetail() }));
|
|
226
226
|
}
|
|
227
227
|
export default McpRegistryScreen;
|
|
@@ -3,10 +3,11 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
|
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
|
|
8
8
|
import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
|
|
9
9
|
import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
|
|
10
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Deduplicate servers by name, keeping only the latest version.
|
|
@@ -332,6 +333,7 @@ export function McpRegistryScreen() {
|
|
|
332
333
|
selectedIndex={mcpRegistry.selectedIndex}
|
|
333
334
|
renderItem={renderListItem}
|
|
334
335
|
maxHeight={dimensions.listPanelHeight}
|
|
336
|
+
getKey={(server) => server.name}
|
|
335
337
|
/>
|
|
336
338
|
)
|
|
337
339
|
}
|
|
@@ -4,10 +4,10 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
|
|
|
4
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
5
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
6
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
7
|
import { getMcpServersByCategory, getCategoryDisplayName, categoryOrder, } from "../../data/mcp-servers.js";
|
|
9
8
|
import { addMcpServer, removeMcpServer, getInstalledMcpServers, getEnabledMcpServers, } from "../../services/claude-settings.js";
|
|
10
9
|
import { renderMcpRow, renderMcpDetail, } from "../renderers/mcpRenderers.js";
|
|
10
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
11
11
|
export function McpScreen() {
|
|
12
12
|
const { state, dispatch } = useApp();
|
|
13
13
|
const { mcp } = state;
|
|
@@ -3,7 +3,7 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
|
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import {
|
|
8
8
|
getMcpServersByCategory,
|
|
9
9
|
getCategoryDisplayName,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
renderMcpDetail,
|
|
22
22
|
type McpListItem,
|
|
23
23
|
} from "../renderers/mcpRenderers.js";
|
|
24
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
24
25
|
|
|
25
26
|
export function McpScreen() {
|
|
26
27
|
const { state, dispatch } = useApp();
|
|
@@ -2,9 +2,9 @@ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useEffect } from "react";
|
|
3
3
|
import { useApp } from "../state/AppContext.js";
|
|
4
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
6
5
|
import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
|
|
7
6
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
7
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
8
|
const RECENT_MODELS = [
|
|
9
9
|
{
|
|
10
10
|
id: "recent-1",
|
|
@@ -285,6 +285,6 @@ export function ModelSelectorScreen() {
|
|
|
285
285
|
// Cursor line: > c
|
|
286
286
|
// Separator: handled by list? or explicit?
|
|
287
287
|
const listHeight = Math.max(5, dimensions.contentHeight - 5);
|
|
288
|
-
return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight,
|
|
288
|
+
return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, getKey: (item) => item.id }) }), _jsx("box", { height: 1, children: _jsx("text", { fg: "#888888", children: footerHints }) })] }));
|
|
289
289
|
}
|
|
290
290
|
export default ModelSelectorScreen;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useMemo, useEffect } from "react";
|
|
2
2
|
import { useApp } from "../state/AppContext.js";
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
|
|
6
6
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
7
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
8
|
|
|
8
9
|
interface ModelItem {
|
|
9
10
|
id: string;
|
|
@@ -423,7 +424,7 @@ export function ModelSelectorScreen() {
|
|
|
423
424
|
selectedIndex={modelSelector.selectedIndex}
|
|
424
425
|
renderItem={renderItem}
|
|
425
426
|
maxHeight={listHeight}
|
|
426
|
-
|
|
427
|
+
getKey={(item) => item.id}
|
|
427
428
|
/>
|
|
428
429
|
</box>
|
|
429
430
|
|
|
@@ -4,12 +4,12 @@ import { useApp, useModal } from "../state/AppContext.js";
|
|
|
4
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
5
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
6
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
7
|
import { listProfiles, applyProfile, renameProfile, deleteProfile, exportProfileToJson, importProfileFromJson, } from "../../services/profiles.js";
|
|
9
8
|
import { readSettings, writeSettings, } from "../../services/claude-settings.js";
|
|
10
9
|
import { writeClipboard, readClipboard, ClipboardUnavailableError, } from "../../utils/clipboard.js";
|
|
11
10
|
import { PREDEFINED_PROFILES, } from "../../data/predefined-profiles.js";
|
|
12
11
|
import { buildProfileListItems, renderProfileRow, renderProfileDetail, } from "../renderers/profileRenderers.js";
|
|
12
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
13
13
|
export function ProfilesScreen() {
|
|
14
14
|
const { state, dispatch } = useApp();
|
|
15
15
|
const { profiles: profilesState } = state;
|
|
@@ -3,7 +3,7 @@ import { useApp, useModal } from "../state/AppContext.js";
|
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import {
|
|
8
8
|
listProfiles,
|
|
9
9
|
applyProfile,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
renderProfileDetail,
|
|
32
32
|
type ProfileListItem,
|
|
33
33
|
} from "../renderers/profileRenderers.js";
|
|
34
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
34
35
|
|
|
35
36
|
export function ProfilesScreen() {
|
|
36
37
|
const { state, dispatch } = useApp();
|
|
@@ -4,8 +4,8 @@ import { useApp, useModal } from "../state/AppContext.js";
|
|
|
4
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
5
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
6
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
7
|
import { statusLineCategories } from "../../data/statuslines.js";
|
|
8
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
9
9
|
import { setStatusLine, getStatusLine, setGlobalStatusLine, getGlobalStatusLine, } from "../../services/claude-settings.js";
|
|
10
10
|
export function StatusLineScreen() {
|
|
11
11
|
const { state, dispatch } = useApp();
|
|
@@ -3,9 +3,10 @@ import { useApp, useModal } from "../state/AppContext.js";
|
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import { statusLineCategories } from "../../data/statuslines.js";
|
|
8
8
|
import type { StatusLineConfig } from "../../types/index.js";
|
|
9
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
9
10
|
import {
|
|
10
11
|
setStatusLine,
|
|
11
12
|
getStatusLine,
|
|
@@ -21,9 +21,9 @@ function calculateDimensions(columns, rows, showProgress, showDebug, showUpdateB
|
|
|
21
21
|
contentHeight = Math.max(10, contentHeight); // Minimum 10 lines for full layout
|
|
22
22
|
// Calculate available content width (accounting for padding)
|
|
23
23
|
const contentWidth = Math.max(40, terminalWidth - 4);
|
|
24
|
-
// Calculate list panel height for
|
|
24
|
+
// Calculate list panel height for scrollbox
|
|
25
25
|
// ScreenLayout uses: panelHeight = contentHeight - 4 (header) - 1 (footer)
|
|
26
|
-
// The
|
|
26
|
+
// The scrollbox sits inside the panel
|
|
27
27
|
const listPanelHeight = Math.max(3, contentHeight - SCREEN_HEADER_HEIGHT - SCREEN_FOOTER_HEIGHT);
|
|
28
28
|
return {
|
|
29
29
|
terminalWidth,
|
|
@@ -10,7 +10,7 @@ interface Dimensions {
|
|
|
10
10
|
contentHeight: number;
|
|
11
11
|
/** Available width for content (excluding borders, padding) */
|
|
12
12
|
contentWidth: number;
|
|
13
|
-
/** Available lines for
|
|
13
|
+
/** Available lines for scrollbox in list panels */
|
|
14
14
|
listPanelHeight: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -52,9 +52,9 @@ function calculateDimensions(
|
|
|
52
52
|
// Calculate available content width (accounting for padding)
|
|
53
53
|
const contentWidth = Math.max(40, terminalWidth - 4);
|
|
54
54
|
|
|
55
|
-
// Calculate list panel height for
|
|
55
|
+
// Calculate list panel height for scrollbox
|
|
56
56
|
// ScreenLayout uses: panelHeight = contentHeight - 4 (header) - 1 (footer)
|
|
57
|
-
// The
|
|
57
|
+
// The scrollbox sits inside the panel
|
|
58
58
|
const listPanelHeight = Math.max(
|
|
59
59
|
3,
|
|
60
60
|
contentHeight - SCREEN_HEADER_HEIGHT - SCREEN_FOOTER_HEIGHT,
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
3
|
-
/**
|
|
4
|
-
* A scrollable detail panel that renders an array of lines
|
|
5
|
-
* with automatic scroll tracking. When content exceeds maxHeight,
|
|
6
|
-
* it shows a scroll indicator and clips to fit.
|
|
7
|
-
*/
|
|
8
|
-
export function ScrollableDetail({ lines, maxHeight, scrollTrigger = 0, }) {
|
|
9
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
10
|
-
// Reset scroll when content changes (new item selected)
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
setScrollOffset(0);
|
|
13
|
-
}, [scrollTrigger]);
|
|
14
|
-
const totalLines = lines.length;
|
|
15
|
-
const visibleLines = Math.max(1, maxHeight - 1); // -1 for scroll indicator
|
|
16
|
-
const canScroll = totalLines > visibleLines;
|
|
17
|
-
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
18
|
-
const clampedOffset = Math.min(scrollOffset, maxOffset);
|
|
19
|
-
const visibleContent = lines.slice(clampedOffset, clampedOffset + visibleLines);
|
|
20
|
-
const scrollUp = clampedOffset > 0;
|
|
21
|
-
const scrollDown = clampedOffset < maxOffset;
|
|
22
|
-
return (_jsxs("box", { flexDirection: "column", children: [scrollUp && (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: ["\u2191 ", clampedOffset, " more"] }) })), visibleContent, scrollDown && (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: ["\u2193 ", totalLines - clampedOffset - visibleLines, " more"] }) }))] }));
|
|
23
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
interface ScrollableDetailProps {
|
|
4
|
-
/** Array of content lines to display */
|
|
5
|
-
lines: React.ReactNode[];
|
|
6
|
-
/** Maximum visible height */
|
|
7
|
-
maxHeight: number;
|
|
8
|
-
/** External scroll trigger — changes when list selection changes */
|
|
9
|
-
scrollTrigger?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A scrollable detail panel that renders an array of lines
|
|
14
|
-
* with automatic scroll tracking. When content exceeds maxHeight,
|
|
15
|
-
* it shows a scroll indicator and clips to fit.
|
|
16
|
-
*/
|
|
17
|
-
export function ScrollableDetail({
|
|
18
|
-
lines,
|
|
19
|
-
maxHeight,
|
|
20
|
-
scrollTrigger = 0,
|
|
21
|
-
}: ScrollableDetailProps) {
|
|
22
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
23
|
-
|
|
24
|
-
// Reset scroll when content changes (new item selected)
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
setScrollOffset(0);
|
|
27
|
-
}, [scrollTrigger]);
|
|
28
|
-
|
|
29
|
-
const totalLines = lines.length;
|
|
30
|
-
const visibleLines = Math.max(1, maxHeight - 1); // -1 for scroll indicator
|
|
31
|
-
const canScroll = totalLines > visibleLines;
|
|
32
|
-
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
33
|
-
const clampedOffset = Math.min(scrollOffset, maxOffset);
|
|
34
|
-
|
|
35
|
-
const visibleContent = lines.slice(clampedOffset, clampedOffset + visibleLines);
|
|
36
|
-
|
|
37
|
-
const scrollUp = clampedOffset > 0;
|
|
38
|
-
const scrollDown = clampedOffset < maxOffset;
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<box flexDirection="column">
|
|
42
|
-
{scrollUp && (
|
|
43
|
-
<box>
|
|
44
|
-
<text fg="cyan">↑ {clampedOffset} more</text>
|
|
45
|
-
</box>
|
|
46
|
-
)}
|
|
47
|
-
{visibleContent}
|
|
48
|
-
{scrollDown && (
|
|
49
|
-
<box>
|
|
50
|
-
<text fg="cyan">↓ {totalLines - clampedOffset - visibleLines} more</text>
|
|
51
|
-
</box>
|
|
52
|
-
)}
|
|
53
|
-
</box>
|
|
54
|
-
);
|
|
55
|
-
}
|