@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.
Files changed (41) hide show
  1. package/dist/scripts/seed.js +202 -4
  2. package/dist/src/App.js +35 -3
  3. package/dist/src/components/AdvancedTextInput.js +3 -1
  4. package/dist/src/components/AutocompleteInput.js +3 -1
  5. package/dist/src/components/DetailPanel.js +3 -1
  6. package/dist/src/components/DiffViewer.js +3 -1
  7. package/dist/src/components/ErrorStatePanel.js +3 -1
  8. package/dist/src/components/HUD.js +3 -1
  9. package/dist/src/components/HelpPanel.js +6 -4
  10. package/dist/src/components/ModeBar.js +5 -8
  11. package/dist/src/components/Table/index.js +19 -26
  12. package/dist/src/components/TableSkeleton.js +3 -1
  13. package/dist/src/components/YankHelpPanel.js +3 -1
  14. package/dist/src/constants/commands.js +2 -1
  15. package/dist/src/constants/theme.js +608 -0
  16. package/dist/src/contexts/ThemeContext.js +13 -0
  17. package/dist/src/features/AppMainView.integration.test.js +1 -0
  18. package/dist/src/features/AppMainView.js +6 -4
  19. package/dist/src/hooks/useCommandRouter.js +5 -0
  20. package/dist/src/hooks/usePickerManager.js +35 -1
  21. package/dist/src/index.js +2 -1
  22. package/dist/src/services.js +2 -2
  23. package/dist/src/state/atoms.js +3 -0
  24. package/dist/src/utils/config.js +36 -0
  25. package/dist/src/views/dynamodb/adapter.js +313 -9
  26. package/dist/src/views/dynamodb/capabilities/detailCapability.js +94 -0
  27. package/dist/src/views/dynamodb/capabilities/yankCapability.js +6 -0
  28. package/dist/src/views/dynamodb/capabilities/yankOptions.js +69 -0
  29. package/dist/src/views/dynamodb/schema.js +18 -0
  30. package/dist/src/views/dynamodb/types.js +1 -0
  31. package/dist/src/views/dynamodb/utils.js +175 -0
  32. package/dist/src/views/iam/adapter.js +2 -1
  33. package/dist/src/views/route53/adapter.js +166 -9
  34. package/dist/src/views/route53/capabilities/detailCapability.js +63 -0
  35. package/dist/src/views/route53/capabilities/yankCapability.js +6 -0
  36. package/dist/src/views/route53/capabilities/yankOptions.js +58 -0
  37. package/dist/src/views/route53/schema.js +18 -0
  38. package/dist/src/views/route53/types.js +1 -0
  39. package/dist/src/views/s3/adapter.js +2 -1
  40. package/dist/src/views/secretsmanager/adapter.js +2 -1
  41. package/package.json +2 -1
@@ -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: 5000,
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
- await seedIam();
303
- await seedSecretsManager();
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 { currentlySelectedServiceAtom, selectedRegionAtom, selectedProfileAtom, revealSecretsAtom, } from "./state/atoms.js";
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: "green", children: state.yankFeedbackMessage }) })), state.pendingAction && state.pendingAction.effect.type === "prompt" && (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: "cyan", 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: "yellow", 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 })] }) }));
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: "gray", children: rendered.text });
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: "gray", dimColor: true, children: suggestion }))] }));
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: "blue", children: title }), _jsx(Text, { color: "gray", children: "─".repeat(40) }), isLoading ? (_jsx(Text, { color: "gray", children: "Loading..." })) : (_jsxs(_Fragment, { children: [hasMoreAbove && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\u2191 ", clampedOffset, " more above"] })), visibleFields.map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: f.label.padEnd(labelWidth + 2) }), _jsx(Text, { children: f.value })] }, f.label))), hasMoreBelow && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\u2193 ", fields.length - clampedOffset - visibleLines, " more below"] }))] })), _jsx(Text, { color: "gray", children: "─".repeat(40) }), _jsx(Text, { color: "gray", children: "j/k scroll \u2022 Esc close" })] }));
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: "red", bold: true, children: "Original" }) }), _jsx(Box, { children: _jsx(Text, { color: "green", bold: true, children: "Updated" }) })] }), _jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { color: "gray", children: "-".repeat(30) }) }), _jsx(Text, { color: "gray", children: "-".repeat(30) })] }), _jsxs(Box, { gap: 2, flexDirection: "column", children: [hasMoreAbove && (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2191 ", clampedOffset, " lines above"] }) }), _jsxs(Text, { color: "gray", 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: "gray", dimColor: true, children: ["\u2193 ", maxLines - clampedOffset - visibleLines, " more lines"] }) }), _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2193 ", maxLines - clampedOffset - visibleLines, " more lines"] })] }))] })] }));
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
- return (_jsx(Box, { width: "100%", borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: title }), _jsx(Text, { children: message }), hint ? _jsx(Text, { color: "gray", children: hint }) : null] }) }));
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: "blue", bold: true, children: compactName }), _jsx(Text, { color: "yellow", bold: true, children: idPart }), _jsx(Text, { color: "gray", bold: true, children: "\u00B7" }), _jsx(Text, { color: "green", bold: true, children: region }), _jsx(Text, { color: "gray", bold: true, children: "\u00B7" }), _jsx(Text, { color: "magenta", bold: true, children: profilePart }), _jsx(Text, { children: " ".repeat(topPadLen) }), loading ? (_jsx(Text, { color: "cyan", bold: true, children: SPINNER_FRAMES[spinnerIndex] })) : null] }), _jsxs(Text, { color: "cyan", 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: "gray", color: "white", children: [pathDisplay, " ".repeat(padLen)] })] })] }));
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: "blue", children: title }), _jsx(Text, { color: "gray", children: scopeLabel }), _jsx(Box, { children: tabRow.map((chip) => {
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: "blue", color: "white" }
31
- : { color: "cyan" }), bold: isActive, children: chip.label }, `chip-${chip.idx}`));
32
- }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visibleItems.map((item, idx) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", bold: true, children: truncate(item.key, keyColWidth).padEnd(keyColWidth) }), _jsx(Text, { children: truncate(item.description, descColWidth) })] }, `${item.key}-${scrollOffset + idx}`))) })] }));
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: "gray", wrap: "truncate-end", children: entries.map((entry, idx) => {
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: "yellow", children: keyPart }), descPart ? _jsxs(Text, { color: "gray", children: [" ", descPart] }) : null, idx < entries.length - 1 ? _jsx(Text, { color: "gray", children: " \u2022 " }) : null] }, `hint-${idx}`));
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: MODE_COLORS[mode], 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
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
- // Color constants for table styling
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 = false) {
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 ? COLORS.highlightSelectedText : COLORS.highlightText;
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: COLORS.separator, children: [" ", "\u2502", " "] }, `sep-${i}`));
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: COLORS.selectedText, bold: true, children: highlighted }, `cell-${i}`));
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: COLORS.selectedBg, children: parts }) : _jsx(Box, { children: parts });
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: COLORS.separator, children: [" ", "\u2502", " "] }, `sep-${i}`));
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: COLORS.headerText, children: truncate(col.label, width) }, col.key));
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: "cyan", children: markerDisplay }, `${col.key}-markers-only`));
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: COLORS.headerText, children: labelDisplay }, `${col.key}-label`));
96
- parts.push(_jsx(Text, { color: "cyan", children: markerText }, `${col.key}-markers`));
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: COLORS.headerText, children: " ".repeat(trailingPadLen) }, `${col.key}-pad`));
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: COLORS.separator, children: columns.map((col, i) => "─".repeat(colWidths[i])).join("─┼─") }));
104
- const renderEmpty = () => (_jsx(Text, { color: COLORS.emptyText, children: filterText ? `No results for "${filterText}"` : "No items" }));
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: COLORS.headerText, children: contextLabel })), contextLabel && _jsx(Box, { height: 1 }), renderHeader(), renderDivider(), renderEmpty()] }));
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: COLORS.headerText, 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: "gray", children: [scrollOffset + visibleRows.length, " / ", rows.length, " items"] }) }))] }));
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
  });