ralph-cli-sandboxed 0.3.0 → 0.4.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/dist/commands/action.d.ts +7 -0
- package/dist/commands/action.js +276 -0
- package/dist/commands/chat.js +95 -7
- package/dist/commands/config.js +6 -18
- package/dist/commands/fix-config.d.ts +4 -0
- package/dist/commands/fix-config.js +388 -0
- package/dist/commands/help.js +17 -0
- package/dist/commands/init.js +89 -2
- package/dist/commands/listen.js +50 -9
- package/dist/commands/prd.js +2 -2
- package/dist/config/languages.json +4 -0
- package/dist/index.js +4 -0
- package/dist/providers/telegram.d.ts +6 -2
- package/dist/providers/telegram.js +68 -2
- package/dist/templates/macos-scripts.d.ts +42 -0
- package/dist/templates/macos-scripts.js +448 -0
- package/dist/tui/ConfigEditor.d.ts +7 -0
- package/dist/tui/ConfigEditor.js +313 -0
- package/dist/tui/components/ArrayEditor.d.ts +22 -0
- package/dist/tui/components/ArrayEditor.js +193 -0
- package/dist/tui/components/BooleanToggle.d.ts +19 -0
- package/dist/tui/components/BooleanToggle.js +43 -0
- package/dist/tui/components/EditorPanel.d.ts +50 -0
- package/dist/tui/components/EditorPanel.js +232 -0
- package/dist/tui/components/HelpPanel.d.ts +13 -0
- package/dist/tui/components/HelpPanel.js +69 -0
- package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
- package/dist/tui/components/JsonSnippetEditor.js +380 -0
- package/dist/tui/components/KeyValueEditor.d.ts +34 -0
- package/dist/tui/components/KeyValueEditor.js +261 -0
- package/dist/tui/components/ObjectEditor.d.ts +23 -0
- package/dist/tui/components/ObjectEditor.js +227 -0
- package/dist/tui/components/PresetSelector.d.ts +23 -0
- package/dist/tui/components/PresetSelector.js +58 -0
- package/dist/tui/components/Preview.d.ts +18 -0
- package/dist/tui/components/Preview.js +190 -0
- package/dist/tui/components/ScrollableContainer.d.ts +38 -0
- package/dist/tui/components/ScrollableContainer.js +77 -0
- package/dist/tui/components/SectionNav.d.ts +31 -0
- package/dist/tui/components/SectionNav.js +130 -0
- package/dist/tui/components/StringEditor.d.ts +21 -0
- package/dist/tui/components/StringEditor.js +29 -0
- package/dist/tui/hooks/useConfig.d.ts +16 -0
- package/dist/tui/hooks/useConfig.js +89 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
- package/dist/tui/hooks/useTerminalSize.js +48 -0
- package/dist/tui/utils/presets.d.ts +52 -0
- package/dist/tui/utils/presets.js +191 -0
- package/dist/tui/utils/validation.d.ts +49 -0
- package/dist/tui/utils/validation.js +198 -0
- package/dist/utils/chat-client.d.ts +31 -1
- package/dist/utils/chat-client.js +27 -1
- package/dist/utils/config.d.ts +7 -2
- package/docs/MACOS-DEVELOPMENT.md +435 -0
- package/package.json +1 -1
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
/**
|
|
6
|
+
* Built-in hints for known notification providers.
|
|
7
|
+
*/
|
|
8
|
+
export const PROVIDER_HINTS = {
|
|
9
|
+
ntfy: [
|
|
10
|
+
{ key: "topic", description: "ntfy topic name", required: true },
|
|
11
|
+
{ key: "server", description: "ntfy server URL (default: https://ntfy.sh)" },
|
|
12
|
+
{ key: "priority", description: "Message priority (1-5)" },
|
|
13
|
+
{ key: "tags", description: "Comma-separated tags/emojis" },
|
|
14
|
+
],
|
|
15
|
+
pushover: [
|
|
16
|
+
{ key: "user", description: "Pushover user key", required: true },
|
|
17
|
+
{ key: "token", description: "Pushover app token", required: true },
|
|
18
|
+
{ key: "device", description: "Target device name" },
|
|
19
|
+
{ key: "priority", description: "Message priority (-2 to 2)" },
|
|
20
|
+
{ key: "sound", description: "Notification sound name" },
|
|
21
|
+
],
|
|
22
|
+
gotify: [
|
|
23
|
+
{ key: "server", description: "Gotify server URL", required: true },
|
|
24
|
+
{ key: "token", description: "Gotify app token", required: true },
|
|
25
|
+
{ key: "priority", description: "Message priority (0-10)" },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* KeyValueEditor component for editing key-value pairs with provider-specific hints.
|
|
30
|
+
* Enhanced version of ObjectEditor with support for common key suggestions.
|
|
31
|
+
*/
|
|
32
|
+
export function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused = true, providerName, }) {
|
|
33
|
+
const [editEntries, setEditEntries] = useState({ ...entries });
|
|
34
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
35
|
+
const [mode, setMode] = useState("list");
|
|
36
|
+
const [editText, setEditText] = useState("");
|
|
37
|
+
const [newKey, setNewKey] = useState("");
|
|
38
|
+
const [expandedKeys, setExpandedKeys] = useState(new Set());
|
|
39
|
+
const [hintIndex, setHintIndex] = useState(0);
|
|
40
|
+
// Get hints for the current provider
|
|
41
|
+
const providerHints = useMemo(() => {
|
|
42
|
+
if (!providerName)
|
|
43
|
+
return [];
|
|
44
|
+
return PROVIDER_HINTS[providerName] || [];
|
|
45
|
+
}, [providerName]);
|
|
46
|
+
// Get hints that are not already in entries
|
|
47
|
+
const availableHints = useMemo(() => {
|
|
48
|
+
return providerHints.filter(hint => !(hint.key in editEntries));
|
|
49
|
+
}, [providerHints, editEntries]);
|
|
50
|
+
// Get sorted keys for consistent ordering
|
|
51
|
+
const keys = Object.keys(editEntries).sort();
|
|
52
|
+
// Total options includes all keys plus "+ Add entry" option (and "+ Add from hints" if available)
|
|
53
|
+
const hasHints = availableHints.length > 0;
|
|
54
|
+
const totalOptions = keys.length + (hasHints ? 2 : 1);
|
|
55
|
+
// Navigation handlers
|
|
56
|
+
const handleNavigateUp = useCallback(() => {
|
|
57
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
|
|
58
|
+
}, [totalOptions]);
|
|
59
|
+
const handleNavigateDown = useCallback(() => {
|
|
60
|
+
setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
|
|
61
|
+
}, [totalOptions]);
|
|
62
|
+
// Toggle expansion of a key to show its value
|
|
63
|
+
const handleToggleExpand = useCallback(() => {
|
|
64
|
+
if (highlightedIndex < keys.length) {
|
|
65
|
+
const key = keys[highlightedIndex];
|
|
66
|
+
setExpandedKeys((prev) => {
|
|
67
|
+
const newSet = new Set(prev);
|
|
68
|
+
if (newSet.has(key)) {
|
|
69
|
+
newSet.delete(key);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
newSet.add(key);
|
|
73
|
+
}
|
|
74
|
+
return newSet;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}, [highlightedIndex, keys]);
|
|
78
|
+
// Delete the highlighted entry
|
|
79
|
+
const handleDelete = useCallback(() => {
|
|
80
|
+
if (highlightedIndex < keys.length) {
|
|
81
|
+
const keyToDelete = keys[highlightedIndex];
|
|
82
|
+
const newEntries = { ...editEntries };
|
|
83
|
+
delete newEntries[keyToDelete];
|
|
84
|
+
setEditEntries(newEntries);
|
|
85
|
+
// Remove from expanded set if present
|
|
86
|
+
setExpandedKeys((prev) => {
|
|
87
|
+
const newSet = new Set(prev);
|
|
88
|
+
newSet.delete(keyToDelete);
|
|
89
|
+
return newSet;
|
|
90
|
+
});
|
|
91
|
+
// Adjust highlighted index if needed
|
|
92
|
+
const newKeys = Object.keys(newEntries);
|
|
93
|
+
if (highlightedIndex >= newKeys.length && newKeys.length > 0) {
|
|
94
|
+
setHighlightedIndex(newKeys.length - 1);
|
|
95
|
+
}
|
|
96
|
+
else if (newKeys.length === 0) {
|
|
97
|
+
setHighlightedIndex(0);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [highlightedIndex, keys, editEntries]);
|
|
101
|
+
// Start editing a value
|
|
102
|
+
const handleStartEdit = useCallback(() => {
|
|
103
|
+
if (highlightedIndex < keys.length) {
|
|
104
|
+
const key = keys[highlightedIndex];
|
|
105
|
+
setEditText(editEntries[key] || "");
|
|
106
|
+
setNewKey(key);
|
|
107
|
+
setMode("edit-value");
|
|
108
|
+
}
|
|
109
|
+
else if (hasHints && highlightedIndex === keys.length) {
|
|
110
|
+
// "+ Add from hints" option
|
|
111
|
+
setHintIndex(0);
|
|
112
|
+
setMode("select-hint");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// "+ Add entry" option - start with key input
|
|
116
|
+
setEditText("");
|
|
117
|
+
setNewKey("");
|
|
118
|
+
setMode("add-key");
|
|
119
|
+
}
|
|
120
|
+
}, [highlightedIndex, keys, editEntries, hasHints]);
|
|
121
|
+
// Confirm key input when adding
|
|
122
|
+
const handleKeySubmit = useCallback(() => {
|
|
123
|
+
const trimmedKey = editText.trim();
|
|
124
|
+
if (trimmedKey) {
|
|
125
|
+
// Check if key already exists
|
|
126
|
+
if (editEntries[trimmedKey] !== undefined) {
|
|
127
|
+
// Edit existing key instead
|
|
128
|
+
setNewKey(trimmedKey);
|
|
129
|
+
setEditText(editEntries[trimmedKey]);
|
|
130
|
+
setMode("edit-value");
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
setNewKey(trimmedKey);
|
|
134
|
+
setEditText("");
|
|
135
|
+
setMode("add-value");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Empty key - cancel
|
|
140
|
+
setMode("list");
|
|
141
|
+
setEditText("");
|
|
142
|
+
}
|
|
143
|
+
}, [editText, editEntries]);
|
|
144
|
+
// Confirm value input
|
|
145
|
+
const handleValueSubmit = useCallback(() => {
|
|
146
|
+
const trimmedValue = editText.trim();
|
|
147
|
+
if (newKey) {
|
|
148
|
+
const newEntries = { ...editEntries };
|
|
149
|
+
newEntries[newKey] = trimmedValue;
|
|
150
|
+
setEditEntries(newEntries);
|
|
151
|
+
// Update highlighted index to the new/edited key
|
|
152
|
+
const sortedKeys = Object.keys(newEntries).sort();
|
|
153
|
+
const newIndex = sortedKeys.indexOf(newKey);
|
|
154
|
+
if (newIndex >= 0) {
|
|
155
|
+
setHighlightedIndex(newIndex);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
setMode("list");
|
|
159
|
+
setEditText("");
|
|
160
|
+
setNewKey("");
|
|
161
|
+
}, [editText, newKey, editEntries]);
|
|
162
|
+
// Cancel text input
|
|
163
|
+
const handleTextCancel = useCallback(() => {
|
|
164
|
+
setMode("list");
|
|
165
|
+
setEditText("");
|
|
166
|
+
setNewKey("");
|
|
167
|
+
}, []);
|
|
168
|
+
// Handle hint selection
|
|
169
|
+
const handleSelectHint = useCallback(() => {
|
|
170
|
+
if (availableHints.length > 0 && hintIndex < availableHints.length) {
|
|
171
|
+
const hint = availableHints[hintIndex];
|
|
172
|
+
setNewKey(hint.key);
|
|
173
|
+
setEditText("");
|
|
174
|
+
setMode("add-value");
|
|
175
|
+
}
|
|
176
|
+
}, [availableHints, hintIndex]);
|
|
177
|
+
// Handle keyboard input for list mode
|
|
178
|
+
useInput((input, key) => {
|
|
179
|
+
if (!isFocused || mode !== "list")
|
|
180
|
+
return;
|
|
181
|
+
// j/k or arrow keys for navigation
|
|
182
|
+
if (input === "j" || key.downArrow) {
|
|
183
|
+
handleNavigateDown();
|
|
184
|
+
}
|
|
185
|
+
else if (input === "k" || key.upArrow) {
|
|
186
|
+
handleNavigateUp();
|
|
187
|
+
}
|
|
188
|
+
else if (key.return || input === "e") {
|
|
189
|
+
// Enter or 'e' to edit/add
|
|
190
|
+
handleStartEdit();
|
|
191
|
+
}
|
|
192
|
+
else if (input === "d" || key.delete) {
|
|
193
|
+
// 'd' or Delete to remove
|
|
194
|
+
handleDelete();
|
|
195
|
+
}
|
|
196
|
+
else if (key.tab || input === " ") {
|
|
197
|
+
// Tab or Space to expand/collapse value
|
|
198
|
+
handleToggleExpand();
|
|
199
|
+
}
|
|
200
|
+
else if (key.escape) {
|
|
201
|
+
onCancel();
|
|
202
|
+
}
|
|
203
|
+
else if (input === "s" || input === "S") {
|
|
204
|
+
// 's' to save and confirm
|
|
205
|
+
onConfirm(editEntries);
|
|
206
|
+
}
|
|
207
|
+
}, { isActive: isFocused && mode === "list" });
|
|
208
|
+
// Handle keyboard input for hint selection mode
|
|
209
|
+
useInput((input, key) => {
|
|
210
|
+
if (!isFocused || mode !== "select-hint")
|
|
211
|
+
return;
|
|
212
|
+
if (input === "j" || key.downArrow) {
|
|
213
|
+
setHintIndex((prev) => (prev < availableHints.length - 1 ? prev + 1 : 0));
|
|
214
|
+
}
|
|
215
|
+
else if (input === "k" || key.upArrow) {
|
|
216
|
+
setHintIndex((prev) => (prev > 0 ? prev - 1 : availableHints.length - 1));
|
|
217
|
+
}
|
|
218
|
+
else if (key.return) {
|
|
219
|
+
handleSelectHint();
|
|
220
|
+
}
|
|
221
|
+
else if (key.escape) {
|
|
222
|
+
setMode("list");
|
|
223
|
+
}
|
|
224
|
+
}, { isActive: isFocused && mode === "select-hint" });
|
|
225
|
+
// Handle keyboard input for text editing modes
|
|
226
|
+
useInput((_input, key) => {
|
|
227
|
+
if (!isFocused || mode === "list" || mode === "select-hint")
|
|
228
|
+
return;
|
|
229
|
+
if (key.escape) {
|
|
230
|
+
handleTextCancel();
|
|
231
|
+
}
|
|
232
|
+
}, { isActive: isFocused && mode !== "list" && mode !== "select-hint" });
|
|
233
|
+
// Render hint selection mode
|
|
234
|
+
if (mode === "select-hint") {
|
|
235
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Select a Common Key" }), providerName && _jsxs(Text, { dimColor: true, children: [" (", providerName, ")"] })] }), availableHints.map((hint, index) => {
|
|
236
|
+
const isHighlighted = index === hintIndex;
|
|
237
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: hint.required ? "yellow" : isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: hint.key }), hint.required && _jsx(Text, { color: "red", children: " *" }), _jsxs(Text, { dimColor: true, children: [" - ", hint.description] })] }, hint.key));
|
|
238
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "j/k: navigate | Enter: select | Esc: cancel" }) })] }));
|
|
239
|
+
}
|
|
240
|
+
// Render key input mode
|
|
241
|
+
if (mode === "add-key") {
|
|
242
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Add New Entry - Enter Key" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleKeySubmit, focus: isFocused })] }), availableHints.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Common keys for ", providerName, ":"] }), availableHints.slice(0, 4).map((hint) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: hint.required ? "yellow" : "gray", children: [hint.key, hint.required && " *"] }) }, hint.key)))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
|
|
243
|
+
}
|
|
244
|
+
// Render value input mode (for add or edit)
|
|
245
|
+
if (mode === "add-value" || mode === "edit-value") {
|
|
246
|
+
// Find hint description for the current key
|
|
247
|
+
const currentHint = providerHints.find(h => h.key === newKey);
|
|
248
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add-value" ? "Add New Entry - Enter Value" : `Edit Value for "${newKey}"` }) }), mode === "add-value" && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(Text, { color: "yellow", children: newKey }), currentHint && currentHint.required && _jsx(Text, { color: "red", children: " (required)" })] })), currentHint && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: currentHint.description }) })), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Value: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleValueSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
|
|
249
|
+
}
|
|
250
|
+
// Check for missing required keys
|
|
251
|
+
const missingRequired = providerHints.filter(h => h.required && !(h.key in editEntries));
|
|
252
|
+
// Render list mode
|
|
253
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), providerName && _jsxs(Text, { dimColor: true, children: [" - ", providerName] })] }), missingRequired.length > 0 && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0 Missing required: " }), _jsx(Text, { color: "yellow", children: missingRequired.map(h => h.key).join(", ") })] })), keys.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (keys.map((key, index) => {
|
|
254
|
+
const isHighlighted = index === highlightedIndex;
|
|
255
|
+
const isExpanded = expandedKeys.has(key);
|
|
256
|
+
const value = editEntries[key];
|
|
257
|
+
const hint = providerHints.find(h => h.key === key);
|
|
258
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: isExpanded ? "▼ " : "▶ " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : hint?.required ? "yellow" : "yellow", inverse: isHighlighted, children: key }), hint?.required && _jsx(Text, { color: "red", children: " *" }), !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { dimColor: true, children: value.length > 30 ? value.substring(0, 30) + "..." : value })] }))] }), isExpanded && (_jsx(Box, { marginLeft: 6, children: _jsx(Text, { color: "green", children: value || "(empty)" }) }))] }, `entry-${key}`));
|
|
259
|
+
})), hasHints && (_jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === keys.length ? "green" : undefined, children: highlightedIndex === keys.length ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: highlightedIndex === keys.length, color: highlightedIndex === keys.length ? "green" : "gray", inverse: highlightedIndex === keys.length, children: "+ Add from hints" })] })), _jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "green" : undefined, children: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: highlightedIndex === (hasHints ? keys.length + 1 : keys.length), color: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "green" : "gray", inverse: highlightedIndex === (hasHints ? keys.length + 1 : keys.length), children: "+ Add custom entry" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Tab/Space: expand | Enter/e: edit" }), _jsx(Text, { dimColor: true, children: "d: delete | s: save | Esc: cancel" })] })] }));
|
|
260
|
+
}
|
|
261
|
+
export default KeyValueEditor;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ObjectEditorProps {
|
|
3
|
+
/** The label to display for this object field */
|
|
4
|
+
label: string;
|
|
5
|
+
/** The current object entries as key-value pairs */
|
|
6
|
+
entries: Record<string, string>;
|
|
7
|
+
/** Called when the user confirms the edit */
|
|
8
|
+
onConfirm: (newEntries: Record<string, string>) => void;
|
|
9
|
+
/** Called when the user cancels the edit (Esc) */
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
/** Whether this editor has focus */
|
|
12
|
+
isFocused?: boolean;
|
|
13
|
+
/** Maximum height for the entries list (for scrolling) */
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* ObjectEditor component for editing key-value pairs.
|
|
18
|
+
* Used for environment variables, mcpServers, actions, and similar objects.
|
|
19
|
+
* Shows keys with expandable values, supports adding and deleting entries.
|
|
20
|
+
* Supports scrolling for long lists with Page Up/Down.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ObjectEditor({ label, entries, onConfirm, onCancel, isFocused, maxHeight, }: ObjectEditorProps): React.ReactElement;
|
|
23
|
+
export default ObjectEditor;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
/**
|
|
6
|
+
* ObjectEditor component for editing key-value pairs.
|
|
7
|
+
* Used for environment variables, mcpServers, actions, and similar objects.
|
|
8
|
+
* Shows keys with expandable values, supports adding and deleting entries.
|
|
9
|
+
* Supports scrolling for long lists with Page Up/Down.
|
|
10
|
+
*/
|
|
11
|
+
export function ObjectEditor({ label, entries, onConfirm, onCancel, isFocused = true, maxHeight = 10, }) {
|
|
12
|
+
const [editEntries, setEditEntries] = useState({ ...entries });
|
|
13
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
14
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
15
|
+
const [mode, setMode] = useState("list");
|
|
16
|
+
const [editText, setEditText] = useState("");
|
|
17
|
+
const [newKey, setNewKey] = useState("");
|
|
18
|
+
const [expandedKeys, setExpandedKeys] = useState(new Set());
|
|
19
|
+
// Get sorted keys for consistent ordering
|
|
20
|
+
const keys = Object.keys(editEntries).sort();
|
|
21
|
+
// Total options includes all keys plus "+ Add entry" option
|
|
22
|
+
const totalOptions = keys.length + 1;
|
|
23
|
+
// Auto-scroll to keep highlighted item visible
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (highlightedIndex < scrollOffset) {
|
|
26
|
+
setScrollOffset(highlightedIndex);
|
|
27
|
+
}
|
|
28
|
+
else if (highlightedIndex >= scrollOffset + maxHeight) {
|
|
29
|
+
setScrollOffset(highlightedIndex - maxHeight + 1);
|
|
30
|
+
}
|
|
31
|
+
}, [highlightedIndex, scrollOffset, maxHeight]);
|
|
32
|
+
// Navigation handlers
|
|
33
|
+
const handleNavigateUp = useCallback(() => {
|
|
34
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
|
|
35
|
+
}, [totalOptions]);
|
|
36
|
+
const handleNavigateDown = useCallback(() => {
|
|
37
|
+
setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
|
|
38
|
+
}, [totalOptions]);
|
|
39
|
+
const handlePageUp = useCallback(() => {
|
|
40
|
+
const newIndex = Math.max(0, highlightedIndex - maxHeight);
|
|
41
|
+
setHighlightedIndex(newIndex);
|
|
42
|
+
}, [highlightedIndex, maxHeight]);
|
|
43
|
+
const handlePageDown = useCallback(() => {
|
|
44
|
+
const newIndex = Math.min(totalOptions - 1, highlightedIndex + maxHeight);
|
|
45
|
+
setHighlightedIndex(newIndex);
|
|
46
|
+
}, [highlightedIndex, maxHeight, totalOptions]);
|
|
47
|
+
// Toggle expansion of a key to show its value
|
|
48
|
+
const handleToggleExpand = useCallback(() => {
|
|
49
|
+
if (highlightedIndex < keys.length) {
|
|
50
|
+
const key = keys[highlightedIndex];
|
|
51
|
+
setExpandedKeys((prev) => {
|
|
52
|
+
const newSet = new Set(prev);
|
|
53
|
+
if (newSet.has(key)) {
|
|
54
|
+
newSet.delete(key);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
newSet.add(key);
|
|
58
|
+
}
|
|
59
|
+
return newSet;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}, [highlightedIndex, keys]);
|
|
63
|
+
// Delete the highlighted entry
|
|
64
|
+
const handleDelete = useCallback(() => {
|
|
65
|
+
if (highlightedIndex < keys.length) {
|
|
66
|
+
const keyToDelete = keys[highlightedIndex];
|
|
67
|
+
const newEntries = { ...editEntries };
|
|
68
|
+
delete newEntries[keyToDelete];
|
|
69
|
+
setEditEntries(newEntries);
|
|
70
|
+
// Remove from expanded set if present
|
|
71
|
+
setExpandedKeys((prev) => {
|
|
72
|
+
const newSet = new Set(prev);
|
|
73
|
+
newSet.delete(keyToDelete);
|
|
74
|
+
return newSet;
|
|
75
|
+
});
|
|
76
|
+
// Adjust highlighted index if needed
|
|
77
|
+
const newKeys = Object.keys(newEntries);
|
|
78
|
+
if (highlightedIndex >= newKeys.length && newKeys.length > 0) {
|
|
79
|
+
setHighlightedIndex(newKeys.length - 1);
|
|
80
|
+
}
|
|
81
|
+
else if (newKeys.length === 0) {
|
|
82
|
+
setHighlightedIndex(0);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, [highlightedIndex, keys, editEntries]);
|
|
86
|
+
// Start editing a value
|
|
87
|
+
const handleStartEdit = useCallback(() => {
|
|
88
|
+
if (highlightedIndex < keys.length) {
|
|
89
|
+
const key = keys[highlightedIndex];
|
|
90
|
+
setEditText(editEntries[key] || "");
|
|
91
|
+
setNewKey(key);
|
|
92
|
+
setMode("edit-value");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Add new entry - start with key input
|
|
96
|
+
setEditText("");
|
|
97
|
+
setNewKey("");
|
|
98
|
+
setMode("add-key");
|
|
99
|
+
}
|
|
100
|
+
}, [highlightedIndex, keys, editEntries]);
|
|
101
|
+
// Confirm key input when adding
|
|
102
|
+
const handleKeySubmit = useCallback(() => {
|
|
103
|
+
const trimmedKey = editText.trim();
|
|
104
|
+
if (trimmedKey) {
|
|
105
|
+
// Check if key already exists
|
|
106
|
+
if (editEntries[trimmedKey] !== undefined) {
|
|
107
|
+
// Edit existing key instead
|
|
108
|
+
setNewKey(trimmedKey);
|
|
109
|
+
setEditText(editEntries[trimmedKey]);
|
|
110
|
+
setMode("edit-value");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
setNewKey(trimmedKey);
|
|
114
|
+
setEditText("");
|
|
115
|
+
setMode("add-value");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Empty key - cancel
|
|
120
|
+
setMode("list");
|
|
121
|
+
setEditText("");
|
|
122
|
+
}
|
|
123
|
+
}, [editText, editEntries]);
|
|
124
|
+
// Confirm value input
|
|
125
|
+
const handleValueSubmit = useCallback(() => {
|
|
126
|
+
const trimmedValue = editText.trim();
|
|
127
|
+
if (newKey) {
|
|
128
|
+
const newEntries = { ...editEntries };
|
|
129
|
+
newEntries[newKey] = trimmedValue;
|
|
130
|
+
setEditEntries(newEntries);
|
|
131
|
+
// Update highlighted index to the new/edited key
|
|
132
|
+
const sortedKeys = Object.keys(newEntries).sort();
|
|
133
|
+
const newIndex = sortedKeys.indexOf(newKey);
|
|
134
|
+
if (newIndex >= 0) {
|
|
135
|
+
setHighlightedIndex(newIndex);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
setMode("list");
|
|
139
|
+
setEditText("");
|
|
140
|
+
setNewKey("");
|
|
141
|
+
}, [editText, newKey, editEntries]);
|
|
142
|
+
// Cancel text input
|
|
143
|
+
const handleTextCancel = useCallback(() => {
|
|
144
|
+
setMode("list");
|
|
145
|
+
setEditText("");
|
|
146
|
+
setNewKey("");
|
|
147
|
+
}, []);
|
|
148
|
+
// Handle keyboard input for list mode
|
|
149
|
+
useInput((input, key) => {
|
|
150
|
+
if (!isFocused || mode !== "list")
|
|
151
|
+
return;
|
|
152
|
+
// j/k or arrow keys for navigation
|
|
153
|
+
if (input === "j" || key.downArrow) {
|
|
154
|
+
handleNavigateDown();
|
|
155
|
+
}
|
|
156
|
+
else if (input === "k" || key.upArrow) {
|
|
157
|
+
handleNavigateUp();
|
|
158
|
+
}
|
|
159
|
+
else if (key.pageUp) {
|
|
160
|
+
handlePageUp();
|
|
161
|
+
}
|
|
162
|
+
else if (key.pageDown) {
|
|
163
|
+
handlePageDown();
|
|
164
|
+
}
|
|
165
|
+
else if (key.return || input === "e") {
|
|
166
|
+
// Enter or 'e' to edit/add
|
|
167
|
+
handleStartEdit();
|
|
168
|
+
}
|
|
169
|
+
else if (input === "d" || key.delete) {
|
|
170
|
+
// 'd' or Delete to remove
|
|
171
|
+
handleDelete();
|
|
172
|
+
}
|
|
173
|
+
else if (key.tab || input === " ") {
|
|
174
|
+
// Tab or Space to expand/collapse value
|
|
175
|
+
handleToggleExpand();
|
|
176
|
+
}
|
|
177
|
+
else if (key.escape) {
|
|
178
|
+
onCancel();
|
|
179
|
+
}
|
|
180
|
+
else if (input === "s" || input === "S") {
|
|
181
|
+
// 's' to save and confirm
|
|
182
|
+
onConfirm(editEntries);
|
|
183
|
+
}
|
|
184
|
+
}, { isActive: isFocused && mode === "list" });
|
|
185
|
+
// Handle keyboard input for text editing modes
|
|
186
|
+
useInput((_input, key) => {
|
|
187
|
+
if (!isFocused || mode === "list")
|
|
188
|
+
return;
|
|
189
|
+
if (key.escape) {
|
|
190
|
+
handleTextCancel();
|
|
191
|
+
}
|
|
192
|
+
}, { isActive: isFocused && mode !== "list" });
|
|
193
|
+
// Calculate visible items based on scroll offset
|
|
194
|
+
const visibleItems = useMemo(() => {
|
|
195
|
+
const allKeys = [...keys, "__add_entry__"];
|
|
196
|
+
const endIndex = Math.min(scrollOffset + maxHeight, allKeys.length);
|
|
197
|
+
return allKeys.slice(scrollOffset, endIndex);
|
|
198
|
+
}, [keys, scrollOffset, maxHeight]);
|
|
199
|
+
// Check if we have overflow
|
|
200
|
+
const canScrollUp = scrollOffset > 0;
|
|
201
|
+
const canScrollDown = scrollOffset + maxHeight < totalOptions;
|
|
202
|
+
const hasOverflow = totalOptions > maxHeight;
|
|
203
|
+
// Render key input mode
|
|
204
|
+
if (mode === "add-key") {
|
|
205
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Add New Entry - Enter Key" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleKeySubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
|
|
206
|
+
}
|
|
207
|
+
// Render value input mode (for add or edit)
|
|
208
|
+
if (mode === "add-value" || mode === "edit-value") {
|
|
209
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add-value" ? "Add New Entry - Enter Value" : `Edit Value for "${newKey}"` }) }), mode === "add-value" && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(Text, { color: "yellow", children: newKey })] })), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Value: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleValueSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
|
|
210
|
+
}
|
|
211
|
+
// Render list mode
|
|
212
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" [", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), keys.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (visibleItems.map((key, visibleIndex) => {
|
|
213
|
+
// Check if this is the "Add entry" option
|
|
214
|
+
if (key === "__add_entry__") {
|
|
215
|
+
const actualIndex = keys.length;
|
|
216
|
+
const isHighlighted = actualIndex === highlightedIndex;
|
|
217
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "green" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "green" : "gray", inverse: isHighlighted, children: "+ Add entry" })] }, "add-entry"));
|
|
218
|
+
}
|
|
219
|
+
// Regular entry
|
|
220
|
+
const actualIndex = scrollOffset + visibleIndex;
|
|
221
|
+
const isHighlighted = actualIndex === highlightedIndex;
|
|
222
|
+
const isExpanded = expandedKeys.has(key);
|
|
223
|
+
const value = editEntries[key];
|
|
224
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: isExpanded ? "▼ " : "▶ " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : "yellow", inverse: isHighlighted, children: key }), !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { dimColor: true, children: value.length > 30 ? value.substring(0, 30) + "..." : value })] }))] }), isExpanded && (_jsx(Box, { marginLeft: 6, children: _jsx(Text, { color: "green", children: value || "(empty)" }) }))] }, `entry-${key}`));
|
|
225
|
+
})), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollDown ? "cyan" : "gray", dimColor: !canScrollDown, children: canScrollDown ? " ▼ more" : "" }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Tab/Space: expand | Enter/e: edit" }), _jsxs(Text, { dimColor: true, children: ["d: delete | s: save | Esc: cancel", hasOverflow && " | PgUp/Dn: scroll"] })] })] }));
|
|
226
|
+
}
|
|
227
|
+
export default ObjectEditor;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ConfigPreset } from "../utils/presets.js";
|
|
3
|
+
import type { RalphConfig } from "../../utils/config.js";
|
|
4
|
+
export interface PresetSelectorProps {
|
|
5
|
+
/** The section ID to show presets for */
|
|
6
|
+
sectionId: string;
|
|
7
|
+
/** Current configuration (to detect active preset) */
|
|
8
|
+
config: RalphConfig;
|
|
9
|
+
/** Called when a preset is selected */
|
|
10
|
+
onSelectPreset: (preset: ConfigPreset) => void;
|
|
11
|
+
/** Called when user skips preset selection */
|
|
12
|
+
onSkip: () => void;
|
|
13
|
+
/** Called when user cancels */
|
|
14
|
+
onCancel: () => void;
|
|
15
|
+
/** Whether this component has focus */
|
|
16
|
+
isFocused?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* PresetSelector component displays available presets for a config section.
|
|
20
|
+
* Allows the user to quickly apply a preset template or skip to manual editing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function PresetSelector({ sectionId, config, onSelectPreset, onSkip, onCancel, isFocused, }: PresetSelectorProps): React.ReactElement;
|
|
23
|
+
export default PresetSelector;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { getPresetsForSection, detectActivePreset } from "../utils/presets.js";
|
|
5
|
+
/**
|
|
6
|
+
* PresetSelector component displays available presets for a config section.
|
|
7
|
+
* Allows the user to quickly apply a preset template or skip to manual editing.
|
|
8
|
+
*/
|
|
9
|
+
export function PresetSelector({ sectionId, config, onSelectPreset, onSkip, onCancel, isFocused = true, }) {
|
|
10
|
+
const presets = useMemo(() => getPresetsForSection(sectionId), [sectionId]);
|
|
11
|
+
const activePresetId = useMemo(() => detectActivePreset(config, sectionId), [config, sectionId]);
|
|
12
|
+
// Include "Skip" option at the end
|
|
13
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
14
|
+
const totalOptions = presets.length + 1; // +1 for "Skip" option
|
|
15
|
+
// Navigation handlers
|
|
16
|
+
const handleNavigateUp = useCallback(() => {
|
|
17
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
|
|
18
|
+
}, [totalOptions]);
|
|
19
|
+
const handleNavigateDown = useCallback(() => {
|
|
20
|
+
setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
|
|
21
|
+
}, [totalOptions]);
|
|
22
|
+
const handleSelect = useCallback(() => {
|
|
23
|
+
if (highlightedIndex < presets.length) {
|
|
24
|
+
const preset = presets[highlightedIndex];
|
|
25
|
+
onSelectPreset(preset);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Skip option selected
|
|
29
|
+
onSkip();
|
|
30
|
+
}
|
|
31
|
+
}, [highlightedIndex, presets, onSelectPreset, onSkip]);
|
|
32
|
+
// Handle keyboard input
|
|
33
|
+
useInput((input, key) => {
|
|
34
|
+
if (!isFocused)
|
|
35
|
+
return;
|
|
36
|
+
// j/k or arrow keys for navigation
|
|
37
|
+
if (input === "j" || key.downArrow) {
|
|
38
|
+
handleNavigateDown();
|
|
39
|
+
}
|
|
40
|
+
else if (input === "k" || key.upArrow) {
|
|
41
|
+
handleNavigateUp();
|
|
42
|
+
}
|
|
43
|
+
else if (key.return) {
|
|
44
|
+
handleSelect();
|
|
45
|
+
}
|
|
46
|
+
else if (key.escape) {
|
|
47
|
+
onCancel();
|
|
48
|
+
}
|
|
49
|
+
}, { isActive: isFocused });
|
|
50
|
+
// Get section title for display
|
|
51
|
+
const sectionTitle = sectionId.charAt(0).toUpperCase() + sectionId.slice(1);
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: ["Use Preset: ", sectionTitle] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select a preset to auto-fill common settings, or skip to configure manually." }) }), presets.map((preset, index) => {
|
|
53
|
+
const isHighlighted = index === highlightedIndex;
|
|
54
|
+
const isActive = preset.id === activePresetId;
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : isActive ? "green" : undefined, inverse: isHighlighted, children: preset.name }), isActive && _jsx(Text, { color: "green", children: " (active)" })] }), _jsx(Box, { marginLeft: 4, children: _jsx(Text, { dimColor: true, children: preset.description }) })] }, preset.id));
|
|
56
|
+
}), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: highlightedIndex === presets.length ? "cyan" : undefined, children: highlightedIndex === presets.length ? "▸ " : " " }), _jsx(Text, { bold: highlightedIndex === presets.length, color: highlightedIndex === presets.length ? "yellow" : "gray", inverse: highlightedIndex === presets.length, children: "Skip - Configure manually" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "j/k: navigate | Enter: select | Esc: back" }) })] }));
|
|
57
|
+
}
|
|
58
|
+
export default PresetSelector;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { RalphConfig } from "../../utils/config.js";
|
|
3
|
+
export interface PreviewProps {
|
|
4
|
+
/** The current configuration */
|
|
5
|
+
config: RalphConfig | null;
|
|
6
|
+
/** Currently selected section ID */
|
|
7
|
+
selectedSection: string;
|
|
8
|
+
/** Whether the preview panel is visible */
|
|
9
|
+
visible?: boolean;
|
|
10
|
+
/** Maximum height for the preview (lines) */
|
|
11
|
+
maxHeight?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Preview component displays the current section's config as syntax-highlighted JSON.
|
|
15
|
+
* Updates live as edits are made.
|
|
16
|
+
*/
|
|
17
|
+
export declare function Preview({ config, selectedSection, visible, maxHeight, }: PreviewProps): React.ReactElement | null;
|
|
18
|
+
export default Preview;
|