@a9s/cli 1.0.7 → 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/scripts/seed.js +202 -4
- 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/services.js +2 -2
- package/dist/src/state/atoms.js +3 -0
- package/dist/src/utils/config.js +36 -0
- package/dist/src/views/dynamodb/adapter.js +313 -9
- package/dist/src/views/dynamodb/capabilities/detailCapability.js +94 -0
- package/dist/src/views/dynamodb/capabilities/yankCapability.js +6 -0
- package/dist/src/views/dynamodb/capabilities/yankOptions.js +69 -0
- package/dist/src/views/dynamodb/schema.js +18 -0
- package/dist/src/views/dynamodb/types.js +1 -0
- package/dist/src/views/dynamodb/utils.js +175 -0
- package/dist/src/views/iam/adapter.js +2 -1
- package/dist/src/views/route53/adapter.js +166 -9
- package/dist/src/views/route53/capabilities/detailCapability.js +63 -0
- package/dist/src/views/route53/capabilities/yankCapability.js +6 -0
- package/dist/src/views/route53/capabilities/yankOptions.js +58 -0
- package/dist/src/views/route53/schema.js +18 -0
- package/dist/src/views/route53/types.js +1 -0
- 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/scripts/seed.js
CHANGED
|
@@ -93,7 +93,7 @@ async function checkLocalStack() {
|
|
|
93
93
|
process.exit(1);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
async function runAws(args) {
|
|
96
|
+
async function runAws(args, timeoutMs = 10000) {
|
|
97
97
|
const env = {
|
|
98
98
|
...process.env,
|
|
99
99
|
AWS_ACCESS_KEY_ID: "test",
|
|
@@ -102,7 +102,7 @@ async function runAws(args) {
|
|
|
102
102
|
};
|
|
103
103
|
const { stdout } = await execFileAsync("aws", ["--endpoint-url", "http://localhost:4566", ...args], {
|
|
104
104
|
env,
|
|
105
|
-
timeout:
|
|
105
|
+
timeout: timeoutMs,
|
|
106
106
|
});
|
|
107
107
|
return stdout;
|
|
108
108
|
}
|
|
@@ -195,6 +195,182 @@ async function ensureAttachedRolePolicy(roleName, policyArn) {
|
|
|
195
195
|
await runAws(["iam", "attach-role-policy", "--role-name", roleName, "--policy-arn", policyArn]);
|
|
196
196
|
console.log(` Attached managed policy to ${roleName}`);
|
|
197
197
|
}
|
|
198
|
+
async function seedDynamoDB() {
|
|
199
|
+
console.log("\nSeeding DynamoDB:");
|
|
200
|
+
const tables = [
|
|
201
|
+
{
|
|
202
|
+
name: "Users",
|
|
203
|
+
pkName: "userId",
|
|
204
|
+
skName: "timestamp",
|
|
205
|
+
items: [
|
|
206
|
+
{ userId: "user-001", timestamp: "2024-01-15T10:00:00Z", email: "alice@example.com", role: "admin" },
|
|
207
|
+
{ userId: "user-002", timestamp: "2024-01-16T14:30:00Z", email: "bob@example.com", role: "user" },
|
|
208
|
+
{ userId: "user-003", timestamp: "2024-01-17T09:45:00Z", email: "charlie@example.com", role: "user" },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "Orders",
|
|
213
|
+
pkName: "orderId",
|
|
214
|
+
items: [
|
|
215
|
+
{ orderId: "order-001", status: "completed", total: "99.99", items: "3" },
|
|
216
|
+
{ orderId: "order-002", status: "pending", total: "149.50", items: "5" },
|
|
217
|
+
{ orderId: "order-003", status: "shipped", total: "299.00", items: "1" },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
for (const table of tables) {
|
|
222
|
+
// Try to create table
|
|
223
|
+
try {
|
|
224
|
+
const keySchema = [{ AttributeName: table.pkName, KeyType: "HASH" }];
|
|
225
|
+
const attrDefs = [{ AttributeName: table.pkName, AttributeType: "S" }];
|
|
226
|
+
if (table.skName) {
|
|
227
|
+
keySchema.push({ AttributeName: table.skName, KeyType: "RANGE" });
|
|
228
|
+
attrDefs.push({ AttributeName: table.skName, AttributeType: "S" });
|
|
229
|
+
}
|
|
230
|
+
await runAws([
|
|
231
|
+
"dynamodb",
|
|
232
|
+
"create-table",
|
|
233
|
+
"--table-name",
|
|
234
|
+
table.name,
|
|
235
|
+
"--key-schema",
|
|
236
|
+
JSON.stringify(keySchema),
|
|
237
|
+
"--attribute-definitions",
|
|
238
|
+
JSON.stringify(attrDefs),
|
|
239
|
+
"--billing-mode",
|
|
240
|
+
"PAY_PER_REQUEST",
|
|
241
|
+
"--output",
|
|
242
|
+
"json",
|
|
243
|
+
], 15000);
|
|
244
|
+
console.log(` Created table: ${table.name}`);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
const err = e;
|
|
248
|
+
if (err.message?.includes("already exists")) {
|
|
249
|
+
console.log(` Table already exists: ${table.name}`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Add items to the table (whether newly created or existing)
|
|
256
|
+
let addedCount = 0;
|
|
257
|
+
for (const item of table.items) {
|
|
258
|
+
const itemObj = {};
|
|
259
|
+
for (const [k, v] of Object.entries(item)) {
|
|
260
|
+
itemObj[k] = { S: String(v) };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
await runAws([
|
|
264
|
+
"dynamodb",
|
|
265
|
+
"put-item",
|
|
266
|
+
"--table-name",
|
|
267
|
+
table.name,
|
|
268
|
+
"--item",
|
|
269
|
+
JSON.stringify(itemObj),
|
|
270
|
+
], 10000);
|
|
271
|
+
addedCount++;
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
console.log(` Warning: could not add item: ${e.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
console.log(` Added ${addedCount}/${table.items.length} items`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function seedRoute53() {
|
|
281
|
+
console.log("\nSeeding Route53:");
|
|
282
|
+
const zones = [
|
|
283
|
+
{ name: "example.com.", isPrivate: false },
|
|
284
|
+
{ name: "internal.local.", isPrivate: true },
|
|
285
|
+
{ name: "staging.test.", isPrivate: false },
|
|
286
|
+
];
|
|
287
|
+
for (const zone of zones) {
|
|
288
|
+
try {
|
|
289
|
+
// First check if zone already exists
|
|
290
|
+
const listOut = await runAws([
|
|
291
|
+
"route53",
|
|
292
|
+
"list-hosted-zones",
|
|
293
|
+
"--output",
|
|
294
|
+
"json",
|
|
295
|
+
], 10000).catch(() => "");
|
|
296
|
+
let zoneId;
|
|
297
|
+
if (listOut) {
|
|
298
|
+
try {
|
|
299
|
+
const listParsed = JSON.parse(listOut);
|
|
300
|
+
const existingZone = listParsed.HostedZones?.find((z) => z.Name === zone.name);
|
|
301
|
+
if (existingZone) {
|
|
302
|
+
zoneId = existingZone.Id;
|
|
303
|
+
console.log(` Hosted zone already exists: ${zone.name}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
// Ignore parse errors
|
|
308
|
+
console.log(` List zones error: ${e.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// If not found, create it
|
|
312
|
+
if (!zoneId) {
|
|
313
|
+
const createOut = await runAws([
|
|
314
|
+
"route53",
|
|
315
|
+
"create-hosted-zone",
|
|
316
|
+
"--name",
|
|
317
|
+
zone.name,
|
|
318
|
+
"--caller-reference",
|
|
319
|
+
`zone-${Date.now()}-${Math.random()}`,
|
|
320
|
+
"--hosted-zone-config",
|
|
321
|
+
JSON.stringify({ PrivateZone: zone.isPrivate, Comment: `Test zone for ${zone.name}` }),
|
|
322
|
+
"--output",
|
|
323
|
+
"json",
|
|
324
|
+
], 10000);
|
|
325
|
+
const parsed = JSON.parse(createOut);
|
|
326
|
+
zoneId = parsed.HostedZone?.Id;
|
|
327
|
+
if (!zoneId)
|
|
328
|
+
throw new Error(`Failed creating hosted zone ${zone.name}`);
|
|
329
|
+
console.log(` Created hosted zone: ${zone.name} (${zoneId})`);
|
|
330
|
+
}
|
|
331
|
+
// Add some DNS records to the zone
|
|
332
|
+
const records = [
|
|
333
|
+
{ name: `www.${zone.name}`, type: "A", value: "192.0.2.1" },
|
|
334
|
+
{ name: `api.${zone.name}`, type: "A", value: "192.0.2.2" },
|
|
335
|
+
{ name: `mail.${zone.name}`, type: "A", value: "192.0.2.3" },
|
|
336
|
+
{ name: zone.name, type: "MX", value: "10 mail.example.com." },
|
|
337
|
+
];
|
|
338
|
+
for (const record of records) {
|
|
339
|
+
try {
|
|
340
|
+
await runAws([
|
|
341
|
+
"route53",
|
|
342
|
+
"change-resource-record-sets",
|
|
343
|
+
"--hosted-zone-id",
|
|
344
|
+
zoneId,
|
|
345
|
+
"--change-batch",
|
|
346
|
+
JSON.stringify({
|
|
347
|
+
Changes: [
|
|
348
|
+
{
|
|
349
|
+
Action: "UPSERT",
|
|
350
|
+
ResourceRecordSet: {
|
|
351
|
+
Name: record.name,
|
|
352
|
+
Type: record.type,
|
|
353
|
+
TTL: 300,
|
|
354
|
+
ResourceRecords: [{ Value: record.value }],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
}),
|
|
359
|
+
], 10000);
|
|
360
|
+
console.log(` Ensured record: ${record.name} (${record.type})`);
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
const err = e;
|
|
364
|
+
console.log(` Warning adding record ${record.name}: ${err.message ?? String(e)}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
const err = e;
|
|
370
|
+
console.error(` Error seeding zone ${zone.name}: ${err.message ?? String(e)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
198
374
|
async function seedSecretsManager() {
|
|
199
375
|
console.log("\nSeeding Secrets Manager:");
|
|
200
376
|
const secrets = [
|
|
@@ -299,8 +475,30 @@ async function main() {
|
|
|
299
475
|
process.stdout.write(" Objects: ");
|
|
300
476
|
await seedBucket(bucket);
|
|
301
477
|
}
|
|
302
|
-
|
|
303
|
-
|
|
478
|
+
try {
|
|
479
|
+
await seedDynamoDB();
|
|
480
|
+
}
|
|
481
|
+
catch (e) {
|
|
482
|
+
console.error(`\nDynamoDB seeding failed: ${e.message}`);
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
await seedRoute53();
|
|
486
|
+
}
|
|
487
|
+
catch (e) {
|
|
488
|
+
console.error(`\nRoute53 seeding failed: ${e.message}`);
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
await seedIam();
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
console.error(`\nIAM seeding failed: ${e.message}`);
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
await seedSecretsManager();
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
console.error(`\nSecrets Manager seeding failed: ${e.message}`);
|
|
501
|
+
}
|
|
304
502
|
console.log("\nDone! LocalStack seeded with test data.");
|
|
305
503
|
console.log("Run: pnpm dev:local");
|
|
306
504
|
}
|
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
|
});
|