@a9s/cli 1.0.8 → 1.0.9
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/src/App.js +35 -3
- package/dist/src/components/AdvancedTextInput.js +3 -1
- package/dist/src/components/AutocompleteInput.js +3 -1
- package/dist/src/components/DetailPanel.js +3 -1
- package/dist/src/components/DiffViewer.js +3 -1
- package/dist/src/components/ErrorStatePanel.js +3 -1
- package/dist/src/components/HUD.js +3 -1
- package/dist/src/components/HelpPanel.js +6 -4
- package/dist/src/components/ModeBar.js +5 -8
- package/dist/src/components/Table/index.js +19 -26
- package/dist/src/components/TableSkeleton.js +3 -1
- package/dist/src/components/YankHelpPanel.js +3 -1
- package/dist/src/constants/commands.js +2 -1
- package/dist/src/constants/theme.js +608 -0
- package/dist/src/contexts/ThemeContext.js +13 -0
- package/dist/src/features/AppMainView.integration.test.js +1 -0
- package/dist/src/features/AppMainView.js +6 -4
- package/dist/src/hooks/useCommandRouter.js +5 -0
- package/dist/src/hooks/usePickerManager.js +35 -1
- package/dist/src/index.js +2 -1
- package/dist/src/state/atoms.js +3 -0
- package/dist/src/utils/config.js +36 -0
- package/dist/src/views/dynamodb/adapter.js +2 -1
- package/dist/src/views/iam/adapter.js +2 -1
- package/dist/src/views/route53/adapter.js +2 -1
- package/dist/src/views/s3/adapter.js +2 -1
- package/dist/src/views/secretsmanager/adapter.js +2 -1
- package/package.json +2 -1
package/dist/src/App.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useApp } from "ink";
|
|
4
4
|
import { useAtom } from "jotai";
|
|
5
5
|
import clipboardy from "clipboardy";
|
|
@@ -24,7 +24,9 @@ import { deriveYankHeaderMarkers } from "./hooks/yankHeaderMarkers.js";
|
|
|
24
24
|
import { AppMainView } from "./features/AppMainView.js";
|
|
25
25
|
import { AVAILABLE_COMMANDS } from "./constants/commands.js";
|
|
26
26
|
import { buildHelpTabs, triggerToString } from "./constants/keybindings.js";
|
|
27
|
-
import {
|
|
27
|
+
import { useTheme } from "./contexts/ThemeContext.js";
|
|
28
|
+
import { saveConfig } from "./utils/config.js";
|
|
29
|
+
import { currentlySelectedServiceAtom, selectedRegionAtom, selectedProfileAtom, revealSecretsAtom, themeNameAtom, } from "./state/atoms.js";
|
|
28
30
|
const INITIAL_AWS_PROFILE = process.env.AWS_PROFILE;
|
|
29
31
|
export function App({ initialService, endpointUrl }) {
|
|
30
32
|
const { exit } = useApp();
|
|
@@ -33,6 +35,13 @@ export function App({ initialService, endpointUrl }) {
|
|
|
33
35
|
const [selectedProfile, setSelectedProfile] = useAtom(selectedProfileAtom);
|
|
34
36
|
const [currentService, setCurrentService] = useAtom(currentlySelectedServiceAtom);
|
|
35
37
|
const [revealSecrets, setRevealSecrets] = useAtom(revealSecretsAtom);
|
|
38
|
+
const [themeName, setThemeName] = useAtom(themeNameAtom);
|
|
39
|
+
const THEME = useTheme();
|
|
40
|
+
// Live theme preview: refs to restore original when picker is cancelled
|
|
41
|
+
const themeNameRef = useRef(themeName);
|
|
42
|
+
themeNameRef.current = themeName; // always in sync, not a dep
|
|
43
|
+
const originalThemeRef = useRef(themeName);
|
|
44
|
+
const themePickerConfirmedRef = useRef(false);
|
|
36
45
|
const { accountName, accountId, awsProfile, currentIdentity, region } = useAwsContext(endpointUrl, selectedRegion, selectedProfile);
|
|
37
46
|
const availableRegions = useAwsRegions(selectedRegion, selectedProfile);
|
|
38
47
|
const availableProfiles = useAwsProfiles();
|
|
@@ -73,6 +82,23 @@ export function App({ initialService, endpointUrl }) {
|
|
|
73
82
|
pickers.openPicker("resource");
|
|
74
83
|
setDidOpenInitialResources(true);
|
|
75
84
|
}, [didOpenInitialResources, pickers]);
|
|
85
|
+
// Save original theme when theme picker opens; restore it if picker is cancelled
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (pickers.theme.open) {
|
|
89
|
+
originalThemeRef.current = themeNameRef.current;
|
|
90
|
+
themePickerConfirmedRef.current = false;
|
|
91
|
+
}
|
|
92
|
+
else if (!themePickerConfirmedRef.current) {
|
|
93
|
+
setThemeName(originalThemeRef.current);
|
|
94
|
+
}
|
|
95
|
+
}, [pickers.theme.open]);
|
|
96
|
+
// Live preview: apply hovered theme immediately as selection changes
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!pickers.theme.open || !pickers.theme.selectedRow)
|
|
99
|
+
return;
|
|
100
|
+
setThemeName(pickers.theme.selectedRow.id);
|
|
101
|
+
}, [pickers.theme.open, pickers.theme.selectedRow, setThemeName]);
|
|
76
102
|
const switchAdapter = useCallback((serviceId) => {
|
|
77
103
|
setCurrentService(serviceId);
|
|
78
104
|
actions.setFilterText("");
|
|
@@ -207,6 +233,7 @@ export function App({ initialService, endpointUrl }) {
|
|
|
207
233
|
openProfilePicker: () => pickers.openPicker("profile"),
|
|
208
234
|
openRegionPicker: () => pickers.openPicker("region"),
|
|
209
235
|
openResourcePicker: () => pickers.openPicker("resource"),
|
|
236
|
+
openThemePicker: () => pickers.openPicker("theme"),
|
|
210
237
|
exit,
|
|
211
238
|
});
|
|
212
239
|
const handleFilterChange = useCallback((value) => {
|
|
@@ -373,6 +400,11 @@ export function App({ initialService, endpointUrl }) {
|
|
|
373
400
|
onSelectResource: switchAdapter,
|
|
374
401
|
onSelectRegion: setSelectedRegion,
|
|
375
402
|
onSelectProfile: setSelectedProfile,
|
|
403
|
+
onSelectTheme: (name) => {
|
|
404
|
+
themePickerConfirmedRef.current = true;
|
|
405
|
+
setThemeName(name);
|
|
406
|
+
saveConfig({ theme: name });
|
|
407
|
+
},
|
|
376
408
|
}),
|
|
377
409
|
},
|
|
378
410
|
mode: {
|
|
@@ -472,5 +504,5 @@ export function App({ initialService, endpointUrl }) {
|
|
|
472
504
|
});
|
|
473
505
|
useMainInput(inputDispatch);
|
|
474
506
|
const activePickerFilter = pickers.activePicker?.filter ?? state.filterText;
|
|
475
|
-
return (_jsx(FullscreenBox, { children: _jsxs(Box, { flexDirection: "column", width: termCols, height: termRows, children: [_jsx(HUD, { serviceLabel: adapter.label, hudColor: adapter.hudColor, path: path, accountName: accountName, accountId: accountId, awsProfile: awsProfile, currentIdentity: currentIdentity, region: region, terminalWidth: termCols, loading: isLoading || Boolean(state.describeState?.loading) }), _jsx(Box, { flexDirection: "row", width: "100%", flexGrow: 1, children: _jsx(AppMainView, { helpPanel: helpPanel, helpTabs: helpTabs, pickers: pickers, error: error, describeState: state.describeState, isLoading: isLoading, filteredRows: filteredRows, columns: columns, selectedIndex: navigation.selectedIndex, scrollOffset: navigation.scrollOffset, filterText: state.filterText, adapter: adapter, termCols: termCols, tableHeight: tableHeight, yankHelpOpen: state.yankHelpOpen, yankOptions: yankOptions, yankHelpRow: selectedRow, uploadPending: state.uploadPending, uploadPreview: uploadPreview, panelScrollOffset: panelScrollOffset, ...(yankHeaderMarkers ? { headerMarkers: yankHeaderMarkers } : {}) }) }), !helpPanel.helpOpen && state.yankFeedbackMessage && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color:
|
|
507
|
+
return (_jsx(FullscreenBox, { children: _jsxs(Box, { flexDirection: "column", width: termCols, height: termRows, backgroundColor: THEME.global.mainBg, children: [_jsx(HUD, { serviceLabel: adapter.label, hudColor: THEME.serviceColors[adapter.id] ?? adapter.hudColor, path: path, accountName: accountName, accountId: accountId, awsProfile: awsProfile, currentIdentity: currentIdentity, region: region, terminalWidth: termCols, loading: isLoading || Boolean(state.describeState?.loading) }), _jsx(Box, { flexDirection: "row", width: "100%", flexGrow: 1, children: _jsx(AppMainView, { helpPanel: helpPanel, helpTabs: helpTabs, pickers: pickers, error: error, describeState: state.describeState, isLoading: isLoading, filteredRows: filteredRows, columns: columns, selectedIndex: navigation.selectedIndex, scrollOffset: navigation.scrollOffset, filterText: state.filterText, adapter: adapter, termCols: termCols, tableHeight: tableHeight, yankHelpOpen: state.yankHelpOpen, yankOptions: yankOptions, yankHelpRow: selectedRow, uploadPending: state.uploadPending, uploadPreview: uploadPreview, panelScrollOffset: panelScrollOffset, ...(yankHeaderMarkers ? { headerMarkers: yankHeaderMarkers } : {}) }) }), !helpPanel.helpOpen && state.yankFeedbackMessage && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: THEME.feedback.successText, children: state.yankFeedbackMessage }) })), state.pendingAction && state.pendingAction.effect.type === "prompt" && (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: THEME.feedback.promptText, children: [state.pendingAction.effect.label, " "] }), _jsx(AdvancedTextInput, { value: state.pendingAction.inputValue, onChange: (value) => actions.setPendingInputValue(value), onSubmit: () => submitPendingAction(state.pendingAction, true), focus: true })] })), state.pendingAction && state.pendingAction.effect.type === "confirm" && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: THEME.feedback.confirmText, children: [state.pendingAction.effect.message, " (y/n)"] }) })), _jsx(ModeBar, { mode: state.mode, filterText: activePickerFilter, commandText: state.commandText, commandCursorToEndToken: state.commandCursorToEndToken, hintOverride: bottomHint, pickerSearchActive: pickers.activePicker?.pickerMode === "search", onFilterChange: handleFilterChange, onCommandChange: actions.setCommandText, onFilterSubmit: handleFilterSubmit, onCommandSubmit: handleCommandSubmit })] }) }));
|
|
476
508
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { Text, useInput } from "ink";
|
|
4
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
4
5
|
function clamp(n, min, max) {
|
|
5
6
|
return Math.max(min, Math.min(max, n));
|
|
6
7
|
}
|
|
@@ -153,6 +154,7 @@ export function applyAdvancedInputEdit(currentValue, currentCursor, input, key)
|
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
export function AdvancedTextInput({ value, onChange, onSubmit, placeholder, focus = true, cursorToEndToken, }) {
|
|
157
|
+
const THEME = useTheme();
|
|
156
158
|
const [cursor, setCursor] = useState(value.length);
|
|
157
159
|
useEffect(() => {
|
|
158
160
|
setCursor((prev) => clamp(prev, 0, value.length));
|
|
@@ -194,7 +196,7 @@ export function AdvancedTextInput({ value, onChange, onSubmit, placeholder, focu
|
|
|
194
196
|
};
|
|
195
197
|
}, [cursor, placeholder, value]);
|
|
196
198
|
if (rendered.isPlaceholder) {
|
|
197
|
-
return _jsx(Text, { color:
|
|
199
|
+
return _jsx(Text, { color: THEME.input.placeholderText, children: rendered.text });
|
|
198
200
|
}
|
|
199
201
|
return (_jsxs(Text, { children: [rendered.before, _jsx(Text, { inverse: true, children: rendered.at }), rendered.after] }));
|
|
200
202
|
}
|
|
@@ -2,7 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useEffect, useImperativeHandle, useMemo, useState } from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { AdvancedTextInput } from "./AdvancedTextInput.js";
|
|
5
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
5
6
|
export const AutocompleteInput = React.forwardRef(({ value, onChange, onSubmit, placeholder, suggestions = [], focus = true, cursorToEndToken }, ref) => {
|
|
7
|
+
const THEME = useTheme();
|
|
6
8
|
const [inputKey, setInputKey] = useState(0);
|
|
7
9
|
const matchingSuggestions = useMemo(() => {
|
|
8
10
|
if (!value || suggestions.length === 0)
|
|
@@ -25,5 +27,5 @@ export const AutocompleteInput = React.forwardRef(({ value, onChange, onSubmit,
|
|
|
25
27
|
return;
|
|
26
28
|
setInputKey((k) => k + 1);
|
|
27
29
|
}, [cursorToEndToken]);
|
|
28
|
-
return (_jsxs(Box, { children: [_jsx(AdvancedTextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder, focus: focus, ...(cursorToEndToken !== undefined ? { cursorToEndToken } : {}) }, `autocomplete-input-${inputKey}`), suggestion && (_jsx(Text, { color:
|
|
30
|
+
return (_jsxs(Box, { children: [_jsx(AdvancedTextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder, focus: focus, ...(cursorToEndToken !== undefined ? { cursorToEndToken } : {}) }, `autocomplete-input-${inputKey}`), suggestion && (_jsx(Text, { color: THEME.input.suggestionText, dimColor: true, children: suggestion }))] }));
|
|
29
31
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
3
4
|
export function DetailPanel({ title, fields, isLoading, scrollOffset, visibleLines, }) {
|
|
5
|
+
const THEME = useTheme();
|
|
4
6
|
const labelWidth = Math.max(...fields.map((f) => f.label.length), 12);
|
|
5
7
|
// Clamp scrollOffset to valid range
|
|
6
8
|
const clampedOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, fields.length - visibleLines)));
|
|
@@ -8,5 +10,5 @@ export function DetailPanel({ title, fields, isLoading, scrollOffset, visibleLin
|
|
|
8
10
|
const visibleFields = fields.slice(clampedOffset, clampedOffset + visibleLines);
|
|
9
11
|
const hasMoreAbove = clampedOffset > 0;
|
|
10
12
|
const hasMoreBelow = clampedOffset + visibleLines < fields.length;
|
|
11
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { bold: true, color:
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { bold: true, color: THEME.panel.panelTitleText, children: title }), _jsx(Text, { color: THEME.panel.panelDividerText, children: "─".repeat(40) }), isLoading ? (_jsx(Text, { color: THEME.panel.panelHintText, children: "Loading..." })) : (_jsxs(_Fragment, { children: [hasMoreAbove && (_jsxs(Text, { color: THEME.panel.panelHintText, dimColor: true, children: ["\u2191 ", clampedOffset, " more above"] })), visibleFields.map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: THEME.panel.detailFieldLabelText, children: f.label.padEnd(labelWidth + 2) }), _jsx(Text, { children: f.value })] }, f.label))), hasMoreBelow && (_jsxs(Text, { color: THEME.panel.panelHintText, dimColor: true, children: ["\u2193 ", fields.length - clampedOffset - visibleLines, " more below"] }))] })), _jsx(Text, { color: THEME.panel.panelDividerText, children: "─".repeat(40) }), _jsx(Text, { color: THEME.panel.panelHintText, children: "j/k scroll \u2022 Esc close" })] }));
|
|
12
14
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
3
4
|
export function DiffViewer({ oldValue, newValue, scrollOffset, visibleLines }) {
|
|
5
|
+
const THEME = useTheme();
|
|
4
6
|
const oldLines = oldValue.split("\n");
|
|
5
7
|
const newLines = newValue.split("\n");
|
|
6
8
|
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
@@ -13,5 +15,5 @@ export function DiffViewer({ oldValue, newValue, scrollOffset, visibleLines }) {
|
|
|
13
15
|
const hasMoreBelow = clampedOffset + visibleLines < maxLines;
|
|
14
16
|
// Calculate column width
|
|
15
17
|
const colWidth = 35;
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { color:
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { color: THEME.diff.originalHeaderText, bold: true, children: "Original" }) }), _jsx(Box, { children: _jsx(Text, { color: THEME.diff.updatedHeaderText, bold: true, children: "Updated" }) })] }), _jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { color: THEME.diff.diffDividerText, children: "-".repeat(30) }) }), _jsx(Text, { color: THEME.diff.diffDividerText, children: "-".repeat(30) })] }), _jsxs(Box, { gap: 2, flexDirection: "column", children: [hasMoreAbove && (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsxs(Text, { color: THEME.diff.diffDividerText, dimColor: true, children: ["\u2191 ", clampedOffset, " lines above"] }) }), _jsxs(Text, { color: THEME.diff.diffDividerText, dimColor: true, children: ["\u2191 ", clampedOffset, " lines above"] })] })), _jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { children: oldDisplay }) }), _jsx(Box, { children: _jsx(Text, { children: newDisplay }) })] }), hasMoreBelow && (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsxs(Text, { color: THEME.diff.diffDividerText, dimColor: true, children: ["\u2193 ", maxLines - clampedOffset - visibleLines, " more lines"] }) }), _jsxs(Text, { color: THEME.diff.diffDividerText, dimColor: true, children: ["\u2193 ", maxLines - clampedOffset - visibleLines, " more lines"] })] }))] })] }));
|
|
17
19
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
3
4
|
export function ErrorStatePanel({ title, message, hint }) {
|
|
4
|
-
|
|
5
|
+
const THEME = useTheme();
|
|
6
|
+
return (_jsx(Box, { width: "100%", borderStyle: "round", borderColor: THEME.error.errorBorderText, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: THEME.error.errorTitleText, children: title }), _jsx(Text, { children: message }), hint ? _jsx(Text, { color: THEME.error.errorHintText, children: hint }) : null] }) }));
|
|
5
7
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
4
5
|
export function HUD({ serviceLabel, hudColor, path, accountName, accountId, awsProfile, currentIdentity, region, terminalWidth, loading = false, }) {
|
|
6
|
+
const THEME = useTheme();
|
|
5
7
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
6
8
|
const [spinnerIndex, setSpinnerIndex] = React.useState(0);
|
|
7
9
|
React.useEffect(() => {
|
|
@@ -27,5 +29,5 @@ export function HUD({ serviceLabel, hudColor, path, accountName, accountId, awsP
|
|
|
27
29
|
const label = ` ${serviceLabel.toUpperCase()} `;
|
|
28
30
|
const pathDisplay = ` ${path} `;
|
|
29
31
|
const padLen = Math.max(0, terminalWidth - label.length - pathDisplay.length);
|
|
30
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color:
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: THEME.hud.accountNameText, bold: true, children: compactName }), _jsx(Text, { color: THEME.hud.accountIdText, bold: true, children: idPart }), _jsx(Text, { color: THEME.hud.separatorText, bold: true, children: "\u00B7" }), _jsx(Text, { color: THEME.hud.regionText, bold: true, children: region }), _jsx(Text, { color: THEME.hud.separatorText, bold: true, children: "\u00B7" }), _jsx(Text, { color: THEME.hud.profileText, bold: true, children: profilePart }), _jsx(Text, { children: " ".repeat(topPadLen) }), loading ? (_jsx(Text, { color: THEME.hud.loadingSpinnerText, bold: true, children: SPINNER_FRAMES[spinnerIndex] })) : null] }), _jsxs(Text, { color: THEME.hud.currentIdentityText, wrap: "truncate-end", children: [identityLine, " ".repeat(identityPadLen)] }), _jsxs(Box, { children: [_jsx(Text, { backgroundColor: hudColor.bg, color: hudColor.fg, bold: true, children: label }), _jsxs(Text, { backgroundColor: THEME.hud.pathBarBg, color: THEME.hud.pathBarText, children: [pathDisplay, " ".repeat(padLen)] })] })] }));
|
|
31
33
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
3
4
|
function truncate(text, maxLen) {
|
|
4
5
|
if (text.length <= maxLen)
|
|
5
6
|
return text;
|
|
@@ -8,6 +9,7 @@ function truncate(text, maxLen) {
|
|
|
8
9
|
return `${text.slice(0, maxLen - 1)}…`;
|
|
9
10
|
}
|
|
10
11
|
export function HelpPanel({ title, scopeLabel, tabs, activeTab, terminalWidth, maxRows, scrollOffset, }) {
|
|
12
|
+
const THEME = useTheme();
|
|
11
13
|
const currentTab = tabs[activeTab] ?? tabs[0];
|
|
12
14
|
const keyColWidth = 12;
|
|
13
15
|
const descColWidth = Math.max(16, terminalWidth - keyColWidth - 8);
|
|
@@ -24,10 +26,10 @@ export function HelpPanel({ title, scopeLabel, tabs, activeTab, terminalWidth, m
|
|
|
24
26
|
}
|
|
25
27
|
const listRowsBudget = Math.max(1, maxRows);
|
|
26
28
|
const visibleItems = (currentTab?.items ?? []).slice(scrollOffset, scrollOffset + listRowsBudget);
|
|
27
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, color:
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, color: THEME.panel.panelTitleText, children: title }), _jsx(Text, { color: THEME.panel.panelHintText, children: scopeLabel }), _jsx(Box, { children: tabRow.map((chip) => {
|
|
28
30
|
const isActive = chip.idx === activeTab;
|
|
29
31
|
return (_jsx(Text, { ...(isActive
|
|
30
|
-
? { backgroundColor:
|
|
31
|
-
: { color:
|
|
32
|
-
}) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visibleItems.map((item, idx) => (_jsxs(Box, { children: [_jsx(Text, { color:
|
|
32
|
+
? { backgroundColor: THEME.panel.activeTabBg, color: THEME.panel.activeTabText }
|
|
33
|
+
: { color: THEME.panel.inactiveTabText }), bold: isActive, children: chip.label }, `chip-${chip.idx}`));
|
|
34
|
+
}) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visibleItems.map((item, idx) => (_jsxs(Box, { children: [_jsx(Text, { color: THEME.panel.keyText, bold: true, children: truncate(item.key, keyColWidth).padEnd(keyColWidth) }), _jsx(Text, { children: truncate(item.description, descColWidth) })] }, `${item.key}-${scrollOffset + idx}`))) })] }));
|
|
33
35
|
}
|
|
@@ -3,17 +3,14 @@ import React, { useRef } from "react";
|
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { AutocompleteInput } from "./AutocompleteInput.js";
|
|
5
5
|
import { AVAILABLE_COMMANDS } from "../constants/commands.js";
|
|
6
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
6
7
|
const MODE_ICONS = {
|
|
7
8
|
navigate: "◉",
|
|
8
9
|
search: "/",
|
|
9
10
|
command: ":",
|
|
10
11
|
};
|
|
11
|
-
const MODE_COLORS = {
|
|
12
|
-
navigate: "blue",
|
|
13
|
-
search: "blue",
|
|
14
|
-
command: "blue",
|
|
15
|
-
};
|
|
16
12
|
export const ModeBar = React.forwardRef(({ mode, filterText, commandText, commandCursorToEndToken, hintOverride, pickerSearchActive, onFilterChange, onCommandChange, onFilterSubmit, onCommandSubmit, }, ref) => {
|
|
13
|
+
const THEME = useTheme();
|
|
17
14
|
const commandInputRef = useRef(null);
|
|
18
15
|
const filterInputRef = useRef(null);
|
|
19
16
|
const renderHint = (hint) => {
|
|
@@ -22,11 +19,11 @@ export const ModeBar = React.forwardRef(({ mode, filterText, commandText, comman
|
|
|
22
19
|
.split("•")
|
|
23
20
|
.map((x) => x.trim())
|
|
24
21
|
.filter(Boolean);
|
|
25
|
-
return (_jsx(Text, { color:
|
|
22
|
+
return (_jsx(Text, { color: THEME.modebar.keybindingDescText, wrap: "truncate-end", children: entries.map((entry, idx) => {
|
|
26
23
|
const [rawKey, rawDesc] = entry.split("·").map((x) => x.trim());
|
|
27
24
|
const keyPart = rawKey ?? entry;
|
|
28
25
|
const descPart = rawDesc ?? "";
|
|
29
|
-
return (_jsxs(React.Fragment, { children: [_jsx(Text, { color:
|
|
26
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: THEME.modebar.keybindingKeyText, children: keyPart }), descPart ? _jsxs(Text, { color: THEME.modebar.keybindingDescText, children: [" ", descPart] }) : null, idx < entries.length - 1 ? _jsx(Text, { color: THEME.modebar.keybindingSeparatorText, children: " \u2022 " }) : null] }, `hint-${idx}`));
|
|
30
27
|
}) }));
|
|
31
28
|
};
|
|
32
29
|
React.useImperativeHandle(ref, () => ({
|
|
@@ -37,7 +34,7 @@ export const ModeBar = React.forwardRef(({ mode, filterText, commandText, comman
|
|
|
37
34
|
const icon = isPickerSearch ? "/" : MODE_ICONS[mode];
|
|
38
35
|
const showNavigateHint = mode === "navigate" && !isPickerSearch;
|
|
39
36
|
const showFilterInput = mode === "search" || isPickerSearch;
|
|
40
|
-
return (_jsx(Box, { flexDirection: "column", width: "100%", children: _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color:
|
|
37
|
+
return (_jsx(Box, { flexDirection: "column", width: "100%", children: _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: THEME.modebar.modeIconText, bold: true, children: icon }), _jsx(Text, { children: " " }), showNavigateHint && renderHint(hintOverride ?? ""), showFilterInput && (_jsx(AutocompleteInput, { ref: filterInputRef, value: filterText, onChange: onFilterChange, onSubmit: onFilterSubmit, placeholder: "Type to filter", focus: showFilterInput })), mode === "command" && (_jsx(AutocompleteInput, { ref: commandInputRef, value: commandText, onChange: onCommandChange, onSubmit: onCommandSubmit, placeholder: "Type a command", suggestions: [...AVAILABLE_COMMANDS], focus: mode === "command", ...(commandCursorToEndToken !== undefined
|
|
41
38
|
? { cursorToEndToken: commandCursorToEndToken }
|
|
42
39
|
: {}) }))] }) }));
|
|
43
40
|
});
|
|
@@ -2,16 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React, { useMemo } from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { computeColumnWidths } from "./widths.js";
|
|
5
|
-
|
|
6
|
-
const COLORS = {
|
|
7
|
-
separator: "gray", // │ and ─ dividers
|
|
8
|
-
headerText: "blue", // Column header text
|
|
9
|
-
selectedBg: "cyan", // Selected row background
|
|
10
|
-
selectedText: "black", // Selected row text
|
|
11
|
-
highlightText: "yellow", // Filtered match highlight
|
|
12
|
-
emptyText: "gray", // Empty state text
|
|
13
|
-
highlightSelectedText: "black", // Highlight text on selected row
|
|
14
|
-
};
|
|
5
|
+
import { useTheme } from "../../contexts/ThemeContext.js";
|
|
15
6
|
function truncate(str, maxLen) {
|
|
16
7
|
if (str.length <= maxLen)
|
|
17
8
|
return str.padEnd(maxLen);
|
|
@@ -26,14 +17,14 @@ function truncateNoPad(str, maxLen) {
|
|
|
26
17
|
return "…";
|
|
27
18
|
return str.slice(0, maxLen - 1) + "…";
|
|
28
19
|
}
|
|
29
|
-
function highlightMatch(text, filter, isSelected
|
|
20
|
+
function highlightMatch(text, filter, isSelected, theme) {
|
|
30
21
|
if (!filter || !text)
|
|
31
22
|
return [text];
|
|
32
23
|
const parts = [];
|
|
33
24
|
const lowerText = text.toLowerCase();
|
|
34
25
|
const lowerFilter = filter.toLowerCase();
|
|
35
26
|
let lastIdx = 0;
|
|
36
|
-
const highlightColor = isSelected ?
|
|
27
|
+
const highlightColor = isSelected ? theme.table.filterMatchSelectedText : theme.table.filterMatchText;
|
|
37
28
|
let idx = lowerText.indexOf(lowerFilter);
|
|
38
29
|
while (idx !== -1) {
|
|
39
30
|
if (idx > lastIdx) {
|
|
@@ -49,24 +40,26 @@ function highlightMatch(text, filter, isSelected = false) {
|
|
|
49
40
|
return parts.length > 0 ? parts : [text];
|
|
50
41
|
}
|
|
51
42
|
const Row = React.memo(function Row({ row, isSelected, columns, colWidths, filterText }) {
|
|
43
|
+
const THEME = useTheme();
|
|
52
44
|
const parts = [];
|
|
53
45
|
columns.forEach((col, i) => {
|
|
54
46
|
if (i > 0)
|
|
55
|
-
parts.push(_jsxs(Text, { color:
|
|
47
|
+
parts.push(_jsxs(Text, { color: THEME.table.rowSeparatorText, children: [" ", "\u2502", " "] }, `sep-${i}`));
|
|
56
48
|
const cellData = row.cells[col.key] ?? "";
|
|
57
49
|
const cellValue = typeof cellData === "string" ? cellData : cellData.displayName;
|
|
58
50
|
const truncated = truncate(cellValue, colWidths[i]);
|
|
59
|
-
const highlighted = filterText && truncated ? highlightMatch(truncated, filterText, isSelected) : [truncated];
|
|
51
|
+
const highlighted = filterText && truncated ? highlightMatch(truncated, filterText, isSelected, THEME) : [truncated];
|
|
60
52
|
if (isSelected) {
|
|
61
|
-
parts.push(_jsx(Text, { color:
|
|
53
|
+
parts.push(_jsx(Text, { color: THEME.table.selectedRowText, bold: true, children: highlighted }, `cell-${i}`));
|
|
62
54
|
}
|
|
63
55
|
else {
|
|
64
56
|
parts.push(_jsx(Text, { children: highlighted }, `cell-${i}`));
|
|
65
57
|
}
|
|
66
58
|
});
|
|
67
|
-
return isSelected ? _jsx(Box, { backgroundColor:
|
|
59
|
+
return isSelected ? _jsx(Box, { backgroundColor: THEME.table.selectedRowBg, children: parts }) : _jsx(Box, { children: parts });
|
|
68
60
|
});
|
|
69
61
|
export const Table = React.memo(function Table({ columns, rows, selectedIndex, filterText, terminalWidth, maxHeight, scrollOffset, contextLabel, headerMarkers, }) {
|
|
62
|
+
const THEME = useTheme();
|
|
70
63
|
// Memoize column widths computation
|
|
71
64
|
const colWidths = useMemo(() => computeColumnWidths(columns, terminalWidth), [columns, terminalWidth]);
|
|
72
65
|
// Rows are pre-filtered by parent, no need to filter again
|
|
@@ -76,34 +69,34 @@ export const Table = React.memo(function Table({ columns, rows, selectedIndex, f
|
|
|
76
69
|
const parts = [];
|
|
77
70
|
columns.forEach((col, i) => {
|
|
78
71
|
if (i > 0)
|
|
79
|
-
parts.push(_jsxs(Text, { color:
|
|
72
|
+
parts.push(_jsxs(Text, { color: THEME.table.rowSeparatorText, children: [" ", "\u2502", " "] }, `sep-${i}`));
|
|
80
73
|
const width = colWidths[i];
|
|
81
74
|
const markers = headerMarkers?.[col.key] ?? [];
|
|
82
75
|
const markerText = markers.length > 0 ? ` [${markers.join(",")}]` : "";
|
|
83
76
|
if (!markerText) {
|
|
84
|
-
parts.push(_jsx(Text, { bold: true, color:
|
|
77
|
+
parts.push(_jsx(Text, { bold: true, color: THEME.table.columnHeaderText, children: truncate(col.label, width) }, col.key));
|
|
85
78
|
return;
|
|
86
79
|
}
|
|
87
80
|
if (markerText.length >= width) {
|
|
88
81
|
const markerDisplay = truncate(markerText, width);
|
|
89
|
-
parts.push(_jsx(Text, { color:
|
|
82
|
+
parts.push(_jsx(Text, { color: THEME.table.columnHeaderMarker, children: markerDisplay }, `${col.key}-markers-only`));
|
|
90
83
|
return;
|
|
91
84
|
}
|
|
92
85
|
const labelMax = width - markerText.length;
|
|
93
86
|
const labelDisplay = truncateNoPad(col.label, labelMax);
|
|
94
87
|
const trailingPadLen = Math.max(0, width - (labelDisplay.length + markerText.length));
|
|
95
|
-
parts.push(_jsx(Text, { bold: true, color:
|
|
96
|
-
parts.push(_jsx(Text, { color:
|
|
88
|
+
parts.push(_jsx(Text, { bold: true, color: THEME.table.columnHeaderText, children: labelDisplay }, `${col.key}-label`));
|
|
89
|
+
parts.push(_jsx(Text, { color: THEME.table.columnHeaderMarker, children: markerText }, `${col.key}-markers`));
|
|
97
90
|
if (trailingPadLen > 0) {
|
|
98
|
-
parts.push(_jsx(Text, { color:
|
|
91
|
+
parts.push(_jsx(Text, { color: THEME.table.columnHeaderText, children: " ".repeat(trailingPadLen) }, `${col.key}-pad`));
|
|
99
92
|
}
|
|
100
93
|
});
|
|
101
94
|
return _jsx(Box, { children: parts });
|
|
102
95
|
};
|
|
103
|
-
const renderDivider = () => (_jsx(Text, { color:
|
|
104
|
-
const renderEmpty = () => (_jsx(Text, { color:
|
|
96
|
+
const renderDivider = () => (_jsx(Text, { color: THEME.table.rowSeparatorText, children: columns.map((col, i) => "─".repeat(colWidths[i])).join("─┼─") }));
|
|
97
|
+
const renderEmpty = () => (_jsx(Text, { color: THEME.table.emptyStateText, children: filterText ? `No results for "${filterText}"` : "No items" }));
|
|
105
98
|
if (rows.length === 0) {
|
|
106
|
-
return (_jsxs(Box, { flexDirection: "column", children: [contextLabel && (_jsx(Text, { bold: true, color:
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", children: [contextLabel && (_jsx(Text, { bold: true, color: THEME.table.columnHeaderText, children: contextLabel })), contextLabel && _jsx(Box, { height: 1 }), renderHeader(), renderDivider(), renderEmpty()] }));
|
|
107
100
|
}
|
|
108
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [contextLabel && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color:
|
|
101
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [contextLabel && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: THEME.table.columnHeaderText, children: contextLabel }), _jsx(Box, { height: 1 })] })), renderHeader(), renderDivider(), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visibleRows.map((row, i) => (_jsx(Row, { row: row, isSelected: i === adjustedSelected, columns: columns, colWidths: colWidths, filterText: filterText }, row.id))) }), rows.length > maxHeight && (_jsx(Box, { paddingTop: 1, children: _jsxs(Text, { color: THEME.table.scrollPositionText, children: [scrollOffset + visibleRows.length, " / ", rows.length, " items"] }) }))] }));
|
|
109
102
|
});
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { computeColumnWidths } from "./Table/widths.js";
|
|
5
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
5
6
|
function fill(len, ch = "░") {
|
|
6
7
|
return ch.repeat(Math.max(1, len));
|
|
7
8
|
}
|
|
@@ -11,6 +12,7 @@ function truncate(str, maxLen) {
|
|
|
11
12
|
return str.slice(0, Math.max(1, maxLen - 1)) + "…";
|
|
12
13
|
}
|
|
13
14
|
export function TableSkeleton({ columns, terminalWidth, rows = 8, contextLabel, }) {
|
|
15
|
+
const THEME = useTheme();
|
|
14
16
|
const FRAMES = ["░", "▒", "▓"];
|
|
15
17
|
const [frame, setFrame] = React.useState(0);
|
|
16
18
|
const colWidths = computeColumnWidths(columns, terminalWidth);
|
|
@@ -21,5 +23,5 @@ export function TableSkeleton({ columns, terminalWidth, rows = 8, contextLabel,
|
|
|
21
23
|
return () => clearInterval(timer);
|
|
22
24
|
}, []);
|
|
23
25
|
const shade = FRAMES[frame] ?? "░";
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [contextLabel ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color:
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [contextLabel ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: THEME.skeleton.skeletonContextLabelText, children: contextLabel }), _jsx(Box, { height: 1 })] })) : null, _jsx(Box, { children: columns.map((col, i) => (_jsxs(React.Fragment, { children: [i > 0 ? _jsx(Text, { color: THEME.skeleton.skeletonSeparatorText, children: " \u2502 " }) : null, _jsx(Text, { bold: true, color: THEME.skeleton.skeletonHeaderText, children: truncate(col.label, colWidths[i] ?? 1) })] }, col.key))) }), _jsx(Text, { color: THEME.skeleton.skeletonDividerText, children: columns.map((_, i) => fill(colWidths[i] ?? 1, "─")).join("─┼─") }), Array.from({ length: rows }).map((_, rowIdx) => (_jsx(Box, { children: columns.map((col, i) => (_jsxs(React.Fragment, { children: [i > 0 ? _jsx(Text, { color: THEME.skeleton.skeletonSeparatorText, children: " \u2502 " }) : null, _jsx(Text, { color: THEME.skeleton.skeletonCellText, children: fill(Math.max(1, colWidths[i] ?? 1), shade) })] }, `${col.key}-${rowIdx}`))) }, `skeleton-row-${rowIdx}`)))] }));
|
|
25
27
|
}
|
|
@@ -3,7 +3,9 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { Alert, Badge, StatusMessage, UnorderedList } from "@inkjs/ui";
|
|
5
5
|
import { triggerToString } from "../constants/keybindings.js";
|
|
6
|
+
import { useTheme } from "../contexts/ThemeContext.js";
|
|
6
7
|
export function YankHelpPanel({ options, row }) {
|
|
8
|
+
const THEME = useTheme();
|
|
7
9
|
const [resolvedValues, setResolvedValues] = useState({});
|
|
8
10
|
useEffect(() => {
|
|
9
11
|
let isActive = true;
|
|
@@ -37,7 +39,7 @@ export function YankHelpPanel({ options, row }) {
|
|
|
37
39
|
isActive = false;
|
|
38
40
|
};
|
|
39
41
|
}, [options, row]);
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Alert, { variant: "info", title: "Yank Options", children: "Press key to copy, Esc or ? to close" }), _jsx(Box, { height: 1 }), !row && _jsx(StatusMessage, { variant: "warning", children: "No row selected" }), _jsx(UnorderedList, { children: options.map((option) => (_jsx(UnorderedList.Item, { children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Badge, { color:
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Alert, { variant: "info", title: "Yank Options", children: "Press key to copy, Esc or ? to close" }), _jsx(Box, { height: 1 }), !row && _jsx(StatusMessage, { variant: "warning", children: "No row selected" }), _jsx(UnorderedList, { children: options.map((option) => (_jsx(UnorderedList.Item, { children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Badge, { color: THEME.panel.keyText, children: triggerToString(option.trigger) }), _jsxs(Text, { children: [" ", option.label] })] }), _jsx(Text, { color: THEME.panel.panelHintText, children: row
|
|
41
43
|
? ` -> ${resolvedValues[`${option.label}-${triggerToString(option.trigger)}`] ?? "(loading...)"}`
|
|
42
44
|
: " -> (no value)" })] }) }, `${option.label}-${triggerToString(option.trigger)}`))) })] }));
|
|
43
45
|
}
|
|
@@ -5,6 +5,7 @@ export const AVAILABLE_COMMANDS = [
|
|
|
5
5
|
"regions",
|
|
6
6
|
"profiles",
|
|
7
7
|
"resources",
|
|
8
|
+
"theme",
|
|
8
9
|
"region",
|
|
9
10
|
"profile",
|
|
10
11
|
"use-region",
|
|
@@ -12,4 +13,4 @@ export const AVAILABLE_COMMANDS = [
|
|
|
12
13
|
"$default",
|
|
13
14
|
"quit",
|
|
14
15
|
];
|
|
15
|
-
export const COMMAND_MODE_HINT = " Commands: s3 route53 dynamodb iam quit regions profiles resources • Enter run • Esc cancel";
|
|
16
|
+
export const COMMAND_MODE_HINT = " Commands: s3 route53 dynamodb iam quit regions profiles resources theme • Enter run • Esc cancel";
|