gitforest 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bunignore +7 -0
- package/.github/workflows/ci.yml +73 -0
- package/CLAUDE.md +111 -0
- package/CONTRIBUTING.md +145 -0
- package/README.md +168 -0
- package/bun.lock +267 -0
- package/bunfig.toml +15 -0
- package/cli +0 -0
- package/config/gitforest.example.yaml +94 -0
- package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
- package/docs/ai/VERIFICATION_REPORT.md +87 -0
- package/docs/ai/architecture.md +169 -0
- package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
- package/docs/ai/checks/check-2025-12-02.md +55 -0
- package/docs/ai/checks/test-verification-report.md +85 -0
- package/docs/ai/implementation-guide.md +776 -0
- package/docs/ai/research/gitty-codebase-analysis.md +221 -0
- package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
- package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
- package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
- package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
- package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
- package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
- package/docs/ai/tickets/TASK-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
- package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
- package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
- package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
- package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
- package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
- package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
- package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
- package/docs/ai/tkt-001-fix-database-error.md +217 -0
- package/docs/ai/ui-enhancement-plan.md +562 -0
- package/package.json +50 -0
- package/src/app.tsx +43 -0
- package/src/cli/config.ts +94 -0
- package/src/cli/formatters.ts +632 -0
- package/src/cli/index.ts +583 -0
- package/src/components/CloneDialog.tsx +137 -0
- package/src/components/ColumnHeader.tsx +128 -0
- package/src/components/CommandPalette.tsx +120 -0
- package/src/components/ConfirmDialog.tsx +105 -0
- package/src/components/ErrorBoundary.tsx +128 -0
- package/src/components/FilterBar.tsx +71 -0
- package/src/components/FilterOptionsOverlay.tsx +131 -0
- package/src/components/HelpOverlay.tsx +120 -0
- package/src/components/Layout.tsx +379 -0
- package/src/components/MarkdownRenderer.tsx +127 -0
- package/src/components/ProgressBar.tsx +53 -0
- package/src/components/ProjectItem.tsx +143 -0
- package/src/components/ProjectList.tsx +90 -0
- package/src/components/RepoDetailModal.tsx +367 -0
- package/src/components/StatusBar.tsx +188 -0
- package/src/components/UnifiedProjectItem.tsx +436 -0
- package/src/components/ViewModeIndicator.tsx +37 -0
- package/src/components/onboarding/CompleteStep.tsx +82 -0
- package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
- package/src/components/onboarding/DirectoriesStep.tsx +847 -0
- package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
- package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
- package/src/components/onboarding/OnboardingWizard.tsx +130 -0
- package/src/components/onboarding/WelcomeStep.tsx +69 -0
- package/src/config/loader.ts +263 -0
- package/src/config/onboarding.ts +67 -0
- package/src/constants.ts +96 -0
- package/src/db/index.ts +147 -0
- package/src/db/schema.ts +70 -0
- package/src/git/commands.ts +283 -0
- package/src/git/index.ts +2 -0
- package/src/git/operations.ts +93 -0
- package/src/git/service.ts +539 -0
- package/src/git/status.ts +84 -0
- package/src/git/types.ts +5 -0
- package/src/github/auth.ts +311 -0
- package/src/github/cache.ts +231 -0
- package/src/github/cli.ts +22 -0
- package/src/github/unified.ts +415 -0
- package/src/hooks/useBackgroundFetch.ts +76 -0
- package/src/hooks/useConfirmDialogActions.ts +120 -0
- package/src/hooks/useKeyBindings.ts +656 -0
- package/src/hooks/useProjects.ts +47 -0
- package/src/hooks/useUnifiedRepos.ts +317 -0
- package/src/index.tsx +494 -0
- package/src/operations/batch.ts +280 -0
- package/src/operations/commands.ts +140 -0
- package/src/operations/index.ts +37 -0
- package/src/scanner/index.ts +424 -0
- package/src/scanner/markers.ts +43 -0
- package/src/scanner/submodules.ts +61 -0
- package/src/services/git.ts +484 -0
- package/src/services/github.ts +676 -0
- package/src/services/index.ts +28 -0
- package/src/services/types.ts +99 -0
- package/src/state/actions.ts +175 -0
- package/src/state/reducer.ts +294 -0
- package/src/state/store.tsx +216 -0
- package/src/state/types.ts +8 -0
- package/src/types/index.ts +383 -0
- package/src/ui/theme.ts +44 -0
- package/src/utils/array.ts +14 -0
- package/src/utils/debug.ts +38 -0
- package/src/utils/errors.ts +17 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/markdown.ts +230 -0
- package/src/utils/project-utils.ts +129 -0
- package/src/utils/rate-limiter.ts +134 -0
- package/src/utils/retry.ts +147 -0
- package/src/utils/timeout.ts +56 -0
- package/test/integration/app.isolated.tsx +240 -0
- package/test/integration/cli-commands.test.ts +287 -0
- package/test/integration/cli-validation.test.ts +264 -0
- package/test/integration/git-operations.test.ts +218 -0
- package/test/integration/scanner.test.ts +228 -0
- package/test/preload.ts +18 -0
- package/test/unit/cli/commands.test.ts +13 -0
- package/test/unit/cli/formatters.test.ts +1116 -0
- package/test/unit/cli/github-commands.test.ts +12 -0
- package/test/unit/components/CloneDialog.test.tsx +240 -0
- package/test/unit/components/ColumnHeader.test.tsx +128 -0
- package/test/unit/components/CommandPalette.test.tsx +355 -0
- package/test/unit/components/ConfirmDialog.test.tsx +111 -0
- package/test/unit/components/ErrorBoundary.test.tsx +139 -0
- package/test/unit/components/FilterBar.test.tsx +43 -0
- package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
- package/test/unit/components/HelpOverlay.test.tsx +90 -0
- package/test/unit/components/Layout.test.tsx +328 -0
- package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
- package/test/unit/components/ProgressBar.test.tsx +138 -0
- package/test/unit/components/ProjectItem.test.tsx +182 -0
- package/test/unit/components/ProjectList.test.tsx +311 -0
- package/test/unit/components/RepoDetailModal.test.tsx +445 -0
- package/test/unit/components/StatusBar.test.tsx +112 -0
- package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
- package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
- package/test/unit/components/test-utils.tsx +63 -0
- package/test/unit/config/loader.test.ts +692 -0
- package/test/unit/db/database.test.ts +978 -0
- package/test/unit/db/index.test.ts +314 -0
- package/test/unit/fixtures/setup.ts +186 -0
- package/test/unit/git/commands-untested.test.ts +205 -0
- package/test/unit/git/commands.test.ts +269 -0
- package/test/unit/git/operations.test.ts +322 -0
- package/test/unit/git/status.test.ts +219 -0
- package/test/unit/github/auth.test.ts +317 -0
- package/test/unit/github/cache.test.ts +1028 -0
- package/test/unit/github/cli.test.ts +135 -0
- package/test/unit/github/unified.test.ts +1201 -0
- package/test/unit/graceful-shutdown.test.ts +83 -0
- package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
- package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
- package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
- package/test/unit/hooks/useProjects.test.tsx +186 -0
- package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
- package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
- package/test/unit/mocks/config.ts +109 -0
- package/test/unit/mocks/git-service.ts +274 -0
- package/test/unit/mocks/github-service.ts +250 -0
- package/test/unit/mocks/index.ts +72 -0
- package/test/unit/mocks/project.ts +148 -0
- package/test/unit/mocks/state-mocks.ts +187 -0
- package/test/unit/mocks/unified.ts +169 -0
- package/test/unit/operations/batch.test.ts +216 -0
- package/test/unit/operations/commands.test.ts +550 -0
- package/test/unit/scanner/errors.test.ts +297 -0
- package/test/unit/scanner/index.test.ts +1011 -0
- package/test/unit/scanner/markers.test.ts +150 -0
- package/test/unit/scanner/submodules.test.ts +99 -0
- package/test/unit/services/git-errors.test.ts +190 -0
- package/test/unit/services/git.test.ts +442 -0
- package/test/unit/services/github-errors.test.ts +293 -0
- package/test/unit/services/github.test.ts +200 -0
- package/test/unit/state/actions.test.ts +217 -0
- package/test/unit/state/reducer.test.ts +745 -0
- package/test/unit/state/store.test.tsx +711 -0
- package/test/unit/types/commands.test.ts +220 -0
- package/test/unit/types/schema.test.ts +179 -0
- package/test/unit/utils/array.test.ts +73 -0
- package/test/unit/utils/debug.test.ts +23 -0
- package/test/unit/utils/errors.test.ts +295 -0
- package/test/unit/utils/markdown.test.ts +163 -0
- package/test/unit/utils/project-utils.test.ts +756 -0
- package/test/unit/utils/rate-limiter.test.ts +256 -0
- package/test/unit/utils/retry.test.ts +165 -0
- package/test/unit/utils/strip-ansi.ts +13 -0
- package/test/unit/utils/timeout.test.ts +93 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { NF, COL } from "./UnifiedProjectItem.tsx";
|
|
3
|
+
import { palette } from "../ui/theme.ts";
|
|
4
|
+
import type { SortField, SortDirection } from "../types/index.ts";
|
|
5
|
+
|
|
6
|
+
interface ColumnHeaderProps {
|
|
7
|
+
sortBy?: SortField;
|
|
8
|
+
sortDirection?: SortDirection;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Column headers for the project list
|
|
13
|
+
* Matches column widths from UnifiedProjectItem exactly
|
|
14
|
+
* Highlights the currently sorted column
|
|
15
|
+
*/
|
|
16
|
+
export function ColumnHeader({ sortBy = 'status', sortDirection = 'desc' }: ColumnHeaderProps) {
|
|
17
|
+
// Helper to get header styling based on sort state
|
|
18
|
+
const getStyle = (field: SortField) => {
|
|
19
|
+
const isSorted = sortBy === field;
|
|
20
|
+
return {
|
|
21
|
+
color: isSorted ? palette.text.warning : palette.text.muted,
|
|
22
|
+
bold: isSorted,
|
|
23
|
+
underline: isSorted,
|
|
24
|
+
arrow: isSorted ? (sortDirection === 'desc' ? '↓' : '↑') : '',
|
|
25
|
+
bgColor: undefined,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const formatHeader = (
|
|
30
|
+
label: string,
|
|
31
|
+
style: ReturnType<typeof getStyle>,
|
|
32
|
+
width: number,
|
|
33
|
+
padStart = false
|
|
34
|
+
) => {
|
|
35
|
+
const text = `${label}${style.arrow ? ` ${style.arrow}` : ''}`;
|
|
36
|
+
const padded = padStart ? text.padStart(width) : text.padEnd(width);
|
|
37
|
+
return (
|
|
38
|
+
<Text
|
|
39
|
+
color={style.color}
|
|
40
|
+
bold={style.bold}
|
|
41
|
+
underline={style.underline}
|
|
42
|
+
backgroundColor={style.bgColor}
|
|
43
|
+
>
|
|
44
|
+
{padded}
|
|
45
|
+
</Text>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const nameStyle = getStyle('name');
|
|
50
|
+
const statusStyle = getStyle('status');
|
|
51
|
+
const branchStyle = getStyle('branch');
|
|
52
|
+
const syncStyle = getStyle('sync');
|
|
53
|
+
const langStyle = getStyle('language');
|
|
54
|
+
const starsStyle = getStyle('stars');
|
|
55
|
+
const forksStyle = getStyle('forks');
|
|
56
|
+
const activityStyle = getStyle('lastActivity');
|
|
57
|
+
const sizeStyle = getStyle('size');
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Box>
|
|
61
|
+
{/* Cursor space */}
|
|
62
|
+
<Text color="gray">{' '}</Text>
|
|
63
|
+
|
|
64
|
+
{/* Selection "Sel " */}
|
|
65
|
+
<Text color="gray">{'Sel '.padEnd(COL.sel)}</Text>
|
|
66
|
+
|
|
67
|
+
{/* Status header - just icon */}
|
|
68
|
+
<Text color={statusStyle.color} bold={statusStyle.bold} backgroundColor={statusStyle.bgColor}>
|
|
69
|
+
{`${statusStyle.arrow || '●'}`.padEnd(COL.status)}
|
|
70
|
+
</Text>
|
|
71
|
+
|
|
72
|
+
{/* Source icon + Name header */}
|
|
73
|
+
<Text color="gray">{NF.repo}</Text>
|
|
74
|
+
<Text color="gray">{' '}</Text>
|
|
75
|
+
{formatHeader('Name', nameStyle, COL.name - 2)}
|
|
76
|
+
|
|
77
|
+
{/* Gap */}
|
|
78
|
+
<Text>{' '}</Text>
|
|
79
|
+
|
|
80
|
+
{/* Branch header */}
|
|
81
|
+
{formatHeader(`${NF.git_branch_oct} Branch`, branchStyle, COL.branch)}
|
|
82
|
+
|
|
83
|
+
{/* Gap */}
|
|
84
|
+
<Text>{' '}</Text>
|
|
85
|
+
|
|
86
|
+
{/* Sync header */}
|
|
87
|
+
{formatHeader('Sync', syncStyle, COL.sync)}
|
|
88
|
+
|
|
89
|
+
{/* Gap */}
|
|
90
|
+
<Text>{' '}</Text>
|
|
91
|
+
|
|
92
|
+
{/* Language icon */}
|
|
93
|
+
{formatHeader(NF.code, langStyle, COL.lang, true)}
|
|
94
|
+
|
|
95
|
+
{/* Gap */}
|
|
96
|
+
<Text>{' '}</Text>
|
|
97
|
+
|
|
98
|
+
{/* Stars header */}
|
|
99
|
+
{formatHeader(NF.star, starsStyle, COL.stars, true)}
|
|
100
|
+
|
|
101
|
+
{/* Gap */}
|
|
102
|
+
<Text>{' '}</Text>
|
|
103
|
+
|
|
104
|
+
{/* Forks header */}
|
|
105
|
+
{formatHeader(NF.repo_forked, forksStyle, COL.forks, true)}
|
|
106
|
+
|
|
107
|
+
{/* Gap */}
|
|
108
|
+
<Text>{' '}</Text>
|
|
109
|
+
|
|
110
|
+
{/* Updated header */}
|
|
111
|
+
{formatHeader(NF.clock, activityStyle, COL.updated, true)}
|
|
112
|
+
|
|
113
|
+
{/* Gap */}
|
|
114
|
+
<Text>{' '}</Text>
|
|
115
|
+
|
|
116
|
+
{/* Size header */}
|
|
117
|
+
{formatHeader(NF.database, sizeStyle, COL.size, true)}
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Separator line under headers
|
|
124
|
+
* No longer needed - cleaner without it
|
|
125
|
+
*/
|
|
126
|
+
export function ColumnSeparator() {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { CommandConfig, UnifiedRepo } from "../types/index.ts";
|
|
3
|
+
|
|
4
|
+
export interface CommandPaletteProps {
|
|
5
|
+
commands: CommandConfig[];
|
|
6
|
+
selectedRepos: UnifiedRepo[];
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CommandPalette({
|
|
11
|
+
commands,
|
|
12
|
+
selectedRepos,
|
|
13
|
+
onClose: _onClose,
|
|
14
|
+
}: CommandPaletteProps) {
|
|
15
|
+
// onClose is handled by parent through keybindings
|
|
16
|
+
void _onClose;
|
|
17
|
+
const repoCount = selectedRepos.length;
|
|
18
|
+
const repoNames = selectedRepos.slice(0, 3).map((r) => r.name);
|
|
19
|
+
const moreCount = repoCount > 3 ? repoCount - 3 : 0;
|
|
20
|
+
|
|
21
|
+
// Get the first local path for context
|
|
22
|
+
const targetPath = selectedRepos[0]?.localPath || selectedRepos[0]?.local?.path;
|
|
23
|
+
|
|
24
|
+
if (commands.length === 0) {
|
|
25
|
+
return (
|
|
26
|
+
<Box
|
|
27
|
+
flexDirection="column"
|
|
28
|
+
borderStyle="round"
|
|
29
|
+
borderColor="cyan"
|
|
30
|
+
paddingX={2}
|
|
31
|
+
paddingY={1}
|
|
32
|
+
>
|
|
33
|
+
<Box marginBottom={1}>
|
|
34
|
+
<Text bold color="cyan">
|
|
35
|
+
Command Palette
|
|
36
|
+
</Text>
|
|
37
|
+
</Box>
|
|
38
|
+
|
|
39
|
+
<Text dimColor>
|
|
40
|
+
No custom commands configured.
|
|
41
|
+
</Text>
|
|
42
|
+
<Text dimColor>
|
|
43
|
+
Add commands to your config file:
|
|
44
|
+
</Text>
|
|
45
|
+
<Box marginTop={1} flexDirection="column">
|
|
46
|
+
<Text color="gray">commands:</Text>
|
|
47
|
+
<Text color="gray"> - name: "Open in Editor"</Text>
|
|
48
|
+
<Text color="gray"> key: "e"</Text>
|
|
49
|
+
<Text color="gray"> command: "code ."</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
|
|
52
|
+
<Box marginTop={1}>
|
|
53
|
+
<Text dimColor>[Esc] Close</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Box
|
|
61
|
+
flexDirection="column"
|
|
62
|
+
borderStyle="round"
|
|
63
|
+
borderColor="cyan"
|
|
64
|
+
paddingX={2}
|
|
65
|
+
paddingY={1}
|
|
66
|
+
>
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<Box marginBottom={1}>
|
|
69
|
+
<Text bold color="cyan">
|
|
70
|
+
Command Palette
|
|
71
|
+
</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
|
|
74
|
+
{/* Target info */}
|
|
75
|
+
<Box marginBottom={1} flexDirection="column">
|
|
76
|
+
<Box gap={1}>
|
|
77
|
+
<Text>Target:</Text>
|
|
78
|
+
<Text color="green">
|
|
79
|
+
{repoNames.join(", ")}
|
|
80
|
+
{moreCount > 0 && <Text dimColor> +{moreCount} more</Text>}
|
|
81
|
+
</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
{targetPath && (
|
|
84
|
+
<Box gap={1}>
|
|
85
|
+
<Text dimColor>Path:</Text>
|
|
86
|
+
<Text dimColor>{targetPath}</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
)}
|
|
89
|
+
</Box>
|
|
90
|
+
|
|
91
|
+
{/* Commands list */}
|
|
92
|
+
<Box flexDirection="column" gap={0}>
|
|
93
|
+
<Box marginBottom={1}>
|
|
94
|
+
<Text bold underline>
|
|
95
|
+
Available Commands
|
|
96
|
+
</Text>
|
|
97
|
+
</Box>
|
|
98
|
+
{commands.map((cmd) => (
|
|
99
|
+
<Box key={cmd.key} gap={1}>
|
|
100
|
+
<Box width={6}>
|
|
101
|
+
<Text color="yellow">[{cmd.key}]</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
<Box width={24}>
|
|
104
|
+
<Text>{cmd.name}</Text>
|
|
105
|
+
</Box>
|
|
106
|
+
<Text dimColor>{cmd.command}</Text>
|
|
107
|
+
{cmd.confirm && <Text color="red"> (confirm)</Text>}
|
|
108
|
+
{cmd.background && <Text color="blue"> (bg)</Text>}
|
|
109
|
+
</Box>
|
|
110
|
+
))}
|
|
111
|
+
</Box>
|
|
112
|
+
|
|
113
|
+
{/* Footer */}
|
|
114
|
+
<Box marginTop={1} gap={2}>
|
|
115
|
+
<Text dimColor>[key] Execute</Text>
|
|
116
|
+
<Text dimColor>[Esc] Close</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
|
|
4
|
+
export interface ConfirmDialogProps {
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
items: string[];
|
|
8
|
+
showVisibilityToggle?: boolean;
|
|
9
|
+
defaultVisibility?: "private" | "public";
|
|
10
|
+
onConfirm: (options: { visibility?: "private" | "public" }) => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ConfirmDialog({
|
|
15
|
+
title,
|
|
16
|
+
message,
|
|
17
|
+
items,
|
|
18
|
+
showVisibilityToggle = false,
|
|
19
|
+
defaultVisibility = "private",
|
|
20
|
+
onConfirm,
|
|
21
|
+
onCancel,
|
|
22
|
+
}: ConfirmDialogProps) {
|
|
23
|
+
const [visibility, setVisibility] = useState<"private" | "public">(
|
|
24
|
+
defaultVisibility
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
useInput((input, key) => {
|
|
28
|
+
if (input === "y" || input === "Y") {
|
|
29
|
+
onConfirm({ visibility: showVisibilityToggle ? visibility : undefined });
|
|
30
|
+
} else if (input === "n" || input === "N" || key.escape) {
|
|
31
|
+
onCancel();
|
|
32
|
+
} else if (showVisibilityToggle && (input === "v" || input === "V")) {
|
|
33
|
+
setVisibility((prev) => (prev === "private" ? "public" : "private"));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const maxItems = 5;
|
|
38
|
+
const displayItems = items.slice(0, maxItems);
|
|
39
|
+
const remainingCount = items.length - maxItems;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Box
|
|
43
|
+
flexDirection="column"
|
|
44
|
+
borderStyle="round"
|
|
45
|
+
borderColor="yellow"
|
|
46
|
+
paddingX={2}
|
|
47
|
+
paddingY={1}
|
|
48
|
+
width={50}
|
|
49
|
+
>
|
|
50
|
+
<Box marginBottom={1}>
|
|
51
|
+
<Text bold color="yellow">
|
|
52
|
+
{title}
|
|
53
|
+
</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
|
|
56
|
+
<Box marginBottom={1}>
|
|
57
|
+
<Text>{message}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
|
|
60
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
61
|
+
{displayItems.map((item) => (
|
|
62
|
+
<Text key={item} color="cyan">
|
|
63
|
+
{" • "}
|
|
64
|
+
{item}
|
|
65
|
+
</Text>
|
|
66
|
+
))}
|
|
67
|
+
{remainingCount > 0 && (
|
|
68
|
+
<Text dimColor>{" "}...and {remainingCount} more</Text>
|
|
69
|
+
)}
|
|
70
|
+
</Box>
|
|
71
|
+
|
|
72
|
+
{showVisibilityToggle && (
|
|
73
|
+
<Box marginBottom={1}>
|
|
74
|
+
<Text>Visibility: </Text>
|
|
75
|
+
<Text
|
|
76
|
+
bold
|
|
77
|
+
color={visibility === "private" ? "green" : "yellow"}
|
|
78
|
+
>
|
|
79
|
+
{visibility === "private" ? "[●] Private" : "[ ] Private"}
|
|
80
|
+
</Text>
|
|
81
|
+
<Text> </Text>
|
|
82
|
+
<Text
|
|
83
|
+
bold
|
|
84
|
+
color={visibility === "public" ? "green" : "yellow"}
|
|
85
|
+
>
|
|
86
|
+
{visibility === "public" ? "[●] Public" : "[ ] Public"}
|
|
87
|
+
</Text>
|
|
88
|
+
<Text dimColor> (v to toggle)</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
<Box>
|
|
93
|
+
<Text dimColor>Press </Text>
|
|
94
|
+
<Text color="green" bold>
|
|
95
|
+
y
|
|
96
|
+
</Text>
|
|
97
|
+
<Text dimColor> to confirm, </Text>
|
|
98
|
+
<Text color="red" bold>
|
|
99
|
+
n
|
|
100
|
+
</Text>
|
|
101
|
+
<Text dimColor> to cancel</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { Component, type ReactNode } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
onRetry?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface State {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
errorInfo: React.ErrorInfo | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
17
|
+
constructor(props: Props) {
|
|
18
|
+
super(props);
|
|
19
|
+
this.state = {
|
|
20
|
+
hasError: false,
|
|
21
|
+
error: null,
|
|
22
|
+
errorInfo: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static getDerivedStateFromError(error: Error): Partial<State> {
|
|
27
|
+
return {
|
|
28
|
+
hasError: true,
|
|
29
|
+
error,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
34
|
+
console.error("Error caught by ErrorBoundary:", error);
|
|
35
|
+
console.error("Error info:", errorInfo);
|
|
36
|
+
|
|
37
|
+
this.setState({
|
|
38
|
+
error,
|
|
39
|
+
errorInfo,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
handleRetry = () => {
|
|
44
|
+
this.setState({
|
|
45
|
+
hasError: false,
|
|
46
|
+
error: null,
|
|
47
|
+
errorInfo: null,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (this.props.onRetry) {
|
|
51
|
+
this.props.onRetry();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
override render() {
|
|
56
|
+
if (this.state.hasError && this.state.error) {
|
|
57
|
+
if (this.props.fallback) {
|
|
58
|
+
return this.props.fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ErrorFallback
|
|
63
|
+
error={this.state.error}
|
|
64
|
+
onRetry={this.handleRetry}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this.props.children;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ErrorFallbackProps {
|
|
74
|
+
error: Error;
|
|
75
|
+
onRetry: () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ErrorFallback({ error, onRetry }: ErrorFallbackProps) {
|
|
79
|
+
useInput(() => {
|
|
80
|
+
onRetry();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Box
|
|
85
|
+
flexDirection="column"
|
|
86
|
+
borderStyle="round"
|
|
87
|
+
borderColor="red"
|
|
88
|
+
paddingX={2}
|
|
89
|
+
paddingY={1}
|
|
90
|
+
>
|
|
91
|
+
<Box marginBottom={1}>
|
|
92
|
+
<Text bold color="red">
|
|
93
|
+
⚠️ An Error Occurred
|
|
94
|
+
</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
|
|
97
|
+
<Box marginBottom={1}>
|
|
98
|
+
<Text color="yellow" bold>
|
|
99
|
+
Error:
|
|
100
|
+
</Text>
|
|
101
|
+
<Text>{error.message}</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
|
|
104
|
+
{error.stack && (
|
|
105
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
106
|
+
<Box marginBottom={1}>
|
|
107
|
+
<Text dimColor>
|
|
108
|
+
Stack trace (first 5 lines):
|
|
109
|
+
</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
<Box flexDirection="column">
|
|
112
|
+
{error.stack.split('\n').slice(0, 5).map((line, index) => (
|
|
113
|
+
<Text key={index} dimColor>
|
|
114
|
+
{line.trim()}
|
|
115
|
+
</Text>
|
|
116
|
+
))}
|
|
117
|
+
</Box>
|
|
118
|
+
</Box>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<Box marginTop={1}>
|
|
122
|
+
<Text dimColor>
|
|
123
|
+
Press any key to retry
|
|
124
|
+
</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TextInput } from "@inkjs/ui";
|
|
4
|
+
import { useStore } from "../state/store.tsx";
|
|
5
|
+
import { setFilter, setMode } from "../state/actions.ts";
|
|
6
|
+
import { ViewModeIndicator } from "./ViewModeIndicator.tsx";
|
|
7
|
+
import { palette } from "../ui/theme.ts";
|
|
8
|
+
|
|
9
|
+
export function FilterBar() {
|
|
10
|
+
const { state, dispatch } = useStore();
|
|
11
|
+
const {
|
|
12
|
+
filterText,
|
|
13
|
+
mode,
|
|
14
|
+
sortBy,
|
|
15
|
+
sortDirection,
|
|
16
|
+
viewMode,
|
|
17
|
+
isLoadingGitHub,
|
|
18
|
+
githubError,
|
|
19
|
+
quickFilter,
|
|
20
|
+
languageFilter,
|
|
21
|
+
} = state;
|
|
22
|
+
|
|
23
|
+
const isFilterMode = mode === "filter";
|
|
24
|
+
const sortArrow = sortDirection === "desc" ? "↓" : "↑";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Box gap={2} alignItems="center" flexWrap="wrap">
|
|
28
|
+
{/* View mode indicator */}
|
|
29
|
+
<ViewModeIndicator
|
|
30
|
+
mode={viewMode}
|
|
31
|
+
isLoadingGitHub={isLoadingGitHub}
|
|
32
|
+
githubError={githubError}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
{/* Filter input */}
|
|
36
|
+
<Box>
|
|
37
|
+
<Text color={isFilterMode ? "cyan" : "gray"}>Filter: </Text>
|
|
38
|
+
{isFilterMode ? (
|
|
39
|
+
<TextInput
|
|
40
|
+
key={filterText} // Force remount when filter text changes externally
|
|
41
|
+
defaultValue={filterText}
|
|
42
|
+
onChange={(value) => dispatch(setFilter(value))}
|
|
43
|
+
placeholder="type to filter..."
|
|
44
|
+
/>
|
|
45
|
+
) : (
|
|
46
|
+
<Text color={filterText ? "white" : "gray"}>
|
|
47
|
+
{filterText || "(press / to filter)"}
|
|
48
|
+
</Text>
|
|
49
|
+
)}
|
|
50
|
+
</Box>
|
|
51
|
+
|
|
52
|
+
{/* Active filters summary */}
|
|
53
|
+
<Box gap={1} alignItems="center" flexWrap="wrap">
|
|
54
|
+
{quickFilter !== "all" && (
|
|
55
|
+
<Text color={palette.text.warning}>[{quickFilter}]</Text>
|
|
56
|
+
)}
|
|
57
|
+
{languageFilter && (
|
|
58
|
+
<Text color={palette.text.info}>Lang:{languageFilter}</Text>
|
|
59
|
+
)}
|
|
60
|
+
</Box>
|
|
61
|
+
|
|
62
|
+
{/* Sort indicator */}
|
|
63
|
+
<Box flexGrow={1} justifyContent="flex-end" gap={1} alignItems="center">
|
|
64
|
+
<Text color={palette.text.muted} bold>
|
|
65
|
+
Sort: {sortBy} {sortArrow}
|
|
66
|
+
</Text>
|
|
67
|
+
<Text color="gray">(press s to change)</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
</Box>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { useStore } from "../state/store.tsx";
|
|
3
|
+
|
|
4
|
+
export function FilterOptionsOverlay() {
|
|
5
|
+
const { state } = useStore();
|
|
6
|
+
const { quickFilter, viewMode, sortBy, languageFilter } = state;
|
|
7
|
+
|
|
8
|
+
// Highlight the currently active filter
|
|
9
|
+
const isActive = (filter: string) => quickFilter === filter;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Box
|
|
13
|
+
flexDirection="column"
|
|
14
|
+
borderStyle="round"
|
|
15
|
+
borderColor="cyan"
|
|
16
|
+
paddingX={2}
|
|
17
|
+
paddingY={1}
|
|
18
|
+
>
|
|
19
|
+
{/* Title */}
|
|
20
|
+
<Box marginBottom={1}>
|
|
21
|
+
<Text bold color="cyan">Filter Options</Text>
|
|
22
|
+
</Box>
|
|
23
|
+
|
|
24
|
+
{/* Quick Filters section */}
|
|
25
|
+
<Box marginBottom={1}>
|
|
26
|
+
<Text bold underline>Quick Filters:</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
29
|
+
<Box gap={2}>
|
|
30
|
+
<FilterKey num="0" label="All repos" active={isActive("all")} />
|
|
31
|
+
<FilterKey num="1" label="Dirty (uncommitted)" active={isActive("dirty")} />
|
|
32
|
+
</Box>
|
|
33
|
+
<Box gap={2}>
|
|
34
|
+
<FilterKey num="2" label="Unpushed commits" active={isActive("unpushed")} />
|
|
35
|
+
<FilterKey num="3" label="No remote configured" active={isActive("no-remote")} />
|
|
36
|
+
</Box>
|
|
37
|
+
<Box gap={2}>
|
|
38
|
+
<FilterKey num="4" label="GitHub-only" active={isActive("github-only")} />
|
|
39
|
+
<FilterKey num="5" label="Local-only" active={isActive("local-only")} />
|
|
40
|
+
</Box>
|
|
41
|
+
<Box gap={2}>
|
|
42
|
+
<FilterKey num="6" label="Private repos" active={isActive("private")} />
|
|
43
|
+
<FilterKey num="7" label="Public repos" active={isActive("public")} />
|
|
44
|
+
</Box>
|
|
45
|
+
<Box gap={2}>
|
|
46
|
+
<FilterKey num="8" label="Archived" active={isActive("archived")} />
|
|
47
|
+
<FilterKey num="9" label="Forks" active={isActive("forks")} />
|
|
48
|
+
</Box>
|
|
49
|
+
</Box>
|
|
50
|
+
|
|
51
|
+
{/* View Modes */}
|
|
52
|
+
<Box marginBottom={1}>
|
|
53
|
+
<Text bold underline>View Modes:</Text>
|
|
54
|
+
<Text dimColor> (Tab to cycle)</Text>
|
|
55
|
+
</Box>
|
|
56
|
+
<Box marginBottom={1}>
|
|
57
|
+
<ViewModeOption mode="local" current={viewMode} />
|
|
58
|
+
<Text dimColor> → </Text>
|
|
59
|
+
<ViewModeOption mode="github" current={viewMode} />
|
|
60
|
+
<Text dimColor> → </Text>
|
|
61
|
+
<ViewModeOption mode="combined" current={viewMode} />
|
|
62
|
+
</Box>
|
|
63
|
+
|
|
64
|
+
{/* Sort */}
|
|
65
|
+
<Box marginBottom={1}>
|
|
66
|
+
<Text bold underline>Sort:</Text>
|
|
67
|
+
<Text dimColor> (s to cycle)</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
<Box marginBottom={1}>
|
|
70
|
+
<SortOption field="status" current={sortBy} />
|
|
71
|
+
<Text dimColor> → </Text>
|
|
72
|
+
<SortOption field="name" current={sortBy} />
|
|
73
|
+
<Text dimColor> → </Text>
|
|
74
|
+
<SortOption field="lastActivity" current={sortBy} />
|
|
75
|
+
<Text dimColor> → </Text>
|
|
76
|
+
<SortOption field="stars" current={sortBy} />
|
|
77
|
+
<Text dimColor> → </Text>
|
|
78
|
+
<SortOption field="size" current={sortBy} />
|
|
79
|
+
</Box>
|
|
80
|
+
|
|
81
|
+
{/* Search */}
|
|
82
|
+
<Box marginBottom={1}>
|
|
83
|
+
<Text bold underline>Search:</Text>
|
|
84
|
+
</Box>
|
|
85
|
+
<Box marginBottom={1}>
|
|
86
|
+
<Text color="yellow">/</Text>
|
|
87
|
+
<Text> Start text search (by name, description, path)</Text>
|
|
88
|
+
</Box>
|
|
89
|
+
|
|
90
|
+
{/* Language Filter */}
|
|
91
|
+
<Box marginBottom={1}>
|
|
92
|
+
<Text bold underline>Language Filter:</Text>
|
|
93
|
+
</Box>
|
|
94
|
+
<Box marginBottom={1}>
|
|
95
|
+
<Text>Type language name to filter (e.g., typescript, rust)</Text>
|
|
96
|
+
</Box>
|
|
97
|
+
<Box marginBottom={1}>
|
|
98
|
+
<Text>Current: </Text>
|
|
99
|
+
<Text color={languageFilter ? "cyan" : "gray"}>
|
|
100
|
+
{languageFilter || "[none]"}
|
|
101
|
+
</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
|
|
104
|
+
{/* Footer */}
|
|
105
|
+
<Box marginTop={1}>
|
|
106
|
+
<Text dimColor>Press any key to close</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Helper components
|
|
113
|
+
function FilterKey({ num, label, active }: { num: string; label: string; active: boolean }) {
|
|
114
|
+
return (
|
|
115
|
+
<Box width={24}>
|
|
116
|
+
<Text color="yellow">{num}</Text>
|
|
117
|
+
<Text color={active ? "cyan" : undefined}> {label}</Text>
|
|
118
|
+
{active && <Text color="green"> ✓</Text>}
|
|
119
|
+
</Box>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ViewModeOption({ mode, current }: { mode: string; current: string }) {
|
|
124
|
+
const isActive = mode === current;
|
|
125
|
+
return <Text color={isActive ? "cyan" : "gray"}>{mode}</Text>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function SortOption({ field, current }: { field: string; current: string }) {
|
|
129
|
+
const isActive = field === current;
|
|
130
|
+
return <Text color={isActive ? "cyan" : "gray"}>{field}</Text>;
|
|
131
|
+
}
|