tlc-claude-code 1.2.27 → 1.2.29
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/README.md +9 -4
- package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
- package/templates/docs-sync.yml +91 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { EnvironmentBadge } from './EnvironmentBadge.js';
|
|
5
|
+
describe('EnvironmentBadge', () => {
|
|
6
|
+
describe('Environment Detection', () => {
|
|
7
|
+
it('shows local environment', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local" }));
|
|
9
|
+
expect(lastFrame()).toMatch(/local|dev/i);
|
|
10
|
+
});
|
|
11
|
+
it('shows staging environment', () => {
|
|
12
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging" }));
|
|
13
|
+
expect(lastFrame()).toMatch(/staging|stage/i);
|
|
14
|
+
});
|
|
15
|
+
it('shows production environment', () => {
|
|
16
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production" }));
|
|
17
|
+
expect(lastFrame()).toMatch(/prod|production/i);
|
|
18
|
+
});
|
|
19
|
+
it('shows VPS environment', () => {
|
|
20
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps" }));
|
|
21
|
+
expect(lastFrame()).toMatch(/vps|server/i);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('Colors', () => {
|
|
25
|
+
it('uses green for local', () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local" }));
|
|
27
|
+
// Just verify it renders
|
|
28
|
+
expect(lastFrame()).toContain('local');
|
|
29
|
+
});
|
|
30
|
+
it('uses yellow for staging', () => {
|
|
31
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging" }));
|
|
32
|
+
expect(lastFrame()).toContain('staging');
|
|
33
|
+
});
|
|
34
|
+
it('uses red for production', () => {
|
|
35
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production" }));
|
|
36
|
+
expect(lastFrame()).toMatch(/prod/i);
|
|
37
|
+
});
|
|
38
|
+
it('uses cyan for VPS', () => {
|
|
39
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps" }));
|
|
40
|
+
expect(lastFrame()).toMatch(/vps/i);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('Branch Info', () => {
|
|
44
|
+
it('shows branch name', () => {
|
|
45
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local", branch: "main" }));
|
|
46
|
+
expect(lastFrame()).toContain('main');
|
|
47
|
+
});
|
|
48
|
+
it('shows feature branch', () => {
|
|
49
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", branch: "feature/auth" }));
|
|
50
|
+
expect(lastFrame()).toContain('feature/auth');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('Version Info', () => {
|
|
54
|
+
it('shows version', () => {
|
|
55
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production", version: "1.2.3" }));
|
|
56
|
+
expect(lastFrame()).toContain('1.2.3');
|
|
57
|
+
});
|
|
58
|
+
it('shows commit hash', () => {
|
|
59
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging", commit: "abc1234" }));
|
|
60
|
+
expect(lastFrame()).toContain('abc1234');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('Production Warning', () => {
|
|
64
|
+
it('shows warning for production', () => {
|
|
65
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production" }));
|
|
66
|
+
expect(lastFrame()).toMatch(/⚠|warning|prod|!|caution/i);
|
|
67
|
+
});
|
|
68
|
+
it('no warning for local', () => {
|
|
69
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local" }));
|
|
70
|
+
const output = lastFrame() || '';
|
|
71
|
+
// Should not have production warning
|
|
72
|
+
expect(output).toContain('local');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('Compact Mode', () => {
|
|
76
|
+
it('shows compact badge', () => {
|
|
77
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local", compact: true }));
|
|
78
|
+
expect(lastFrame()).toMatch(/local|L|DEV/i);
|
|
79
|
+
});
|
|
80
|
+
it('shows full badge by default', () => {
|
|
81
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging", branch: "develop" }));
|
|
82
|
+
expect(lastFrame()).toContain('staging');
|
|
83
|
+
expect(lastFrame()).toContain('develop');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('URL Display', () => {
|
|
87
|
+
it('shows environment URL', () => {
|
|
88
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", url: "https://dev.example.com" }));
|
|
89
|
+
expect(lastFrame()).toContain('dev.example.com');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('Connection Status', () => {
|
|
93
|
+
it('shows connected status', () => {
|
|
94
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", connected: true }));
|
|
95
|
+
expect(lastFrame()).toMatch(/●|connected|online/i);
|
|
96
|
+
});
|
|
97
|
+
it('shows disconnected status', () => {
|
|
98
|
+
const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", connected: false }));
|
|
99
|
+
expect(lastFrame()).toMatch(/○|disconnected|offline/i);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface FocusArea {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
shortcut?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FocusIndicatorProps {
|
|
7
|
+
areas: FocusArea[];
|
|
8
|
+
currentArea: string;
|
|
9
|
+
isTrapped?: boolean;
|
|
10
|
+
trappedLabel?: string;
|
|
11
|
+
showSkipLinks?: boolean;
|
|
12
|
+
highContrast?: boolean;
|
|
13
|
+
compact?: boolean;
|
|
14
|
+
breadcrumb?: string[];
|
|
15
|
+
isActive?: boolean;
|
|
16
|
+
onFocusChange?: (areaId: string) => void;
|
|
17
|
+
onEscape?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function FocusIndicator({ areas, currentArea, isTrapped, trappedLabel, showSkipLinks, highContrast, compact, breadcrumb, isActive, onFocusChange, onEscape, }: FocusIndicatorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
export function FocusIndicator({ areas, currentArea, isTrapped = false, trappedLabel, showSkipLinks = false, highContrast = false, compact = false, breadcrumb, isActive = true, onFocusChange, onEscape, }) {
|
|
5
|
+
useInput((input, key) => {
|
|
6
|
+
if (!isActive)
|
|
7
|
+
return;
|
|
8
|
+
// Escape from trap
|
|
9
|
+
if (key.escape && isTrapped && onEscape) {
|
|
10
|
+
onEscape();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Tab navigation
|
|
14
|
+
if (key.tab && !isTrapped && onFocusChange) {
|
|
15
|
+
const currentIndex = areas.findIndex((a) => a.id === currentArea);
|
|
16
|
+
const nextIndex = key.shift
|
|
17
|
+
? (currentIndex - 1 + areas.length) % areas.length
|
|
18
|
+
: (currentIndex + 1) % areas.length;
|
|
19
|
+
onFocusChange(areas[nextIndex].id);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Number shortcut navigation
|
|
23
|
+
if (!isTrapped && onFocusChange) {
|
|
24
|
+
const num = parseInt(input, 10);
|
|
25
|
+
if (num >= 1 && num <= areas.length) {
|
|
26
|
+
onFocusChange(areas[num - 1].id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}, { isActive });
|
|
30
|
+
// Empty state
|
|
31
|
+
if (areas.length === 0) {
|
|
32
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "No focus areas" }) }));
|
|
33
|
+
}
|
|
34
|
+
// Trapped mode (modal)
|
|
35
|
+
if (isTrapped) {
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u25B6 ", trappedLabel || 'Modal'] }), _jsx(Text, { dimColor: true, children: " (Esc to close)" })] }), !compact && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: areas.map((a) => a.label).join(' │ ') }) }))] }));
|
|
37
|
+
}
|
|
38
|
+
// Compact mode
|
|
39
|
+
if (compact) {
|
|
40
|
+
const current = areas.find((a) => a.id === currentArea);
|
|
41
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u25B6 ", current?.label || currentArea] }), _jsx(Text, { dimColor: true, children: " Tab to navigate" })] }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", children: [showSkipLinks && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "[Skip to main content]" }) })), breadcrumb && breadcrumb.length > 0 && (_jsx(Box, { marginBottom: 1, children: breadcrumb.map((item, idx) => (_jsxs(React.Fragment, { children: [idx > 0 && _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { color: idx === breadcrumb.length - 1 ? 'cyan' : undefined, dimColor: idx !== breadcrumb.length - 1, children: item })] }, idx))) })), _jsx(Box, { children: areas.map((area, idx) => {
|
|
44
|
+
const isCurrent = area.id === currentArea;
|
|
45
|
+
return (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { color: isCurrent ? 'cyan' : undefined, children: isCurrent ? '▶ ' : ' ' }), area.shortcut && (_jsxs(Text, { color: highContrast ? 'white' : 'yellow', children: ["[", area.shortcut, "]"] })), _jsx(Text, { bold: isCurrent || highContrast, color: isCurrent ? 'cyan' : highContrast ? 'white' : undefined, dimColor: !isCurrent && !highContrast, children: area.label })] }, area.id));
|
|
46
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Tab next \u2022 Shift+Tab prev \u2022 1-", areas.length, " jump"] }) })] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { FocusIndicator } from './FocusIndicator.js';
|
|
5
|
+
const sampleAreas = [
|
|
6
|
+
{ id: 'projects', label: 'Projects', shortcut: '1' },
|
|
7
|
+
{ id: 'tasks', label: 'Tasks', shortcut: '2' },
|
|
8
|
+
{ id: 'logs', label: 'Logs', shortcut: '3' },
|
|
9
|
+
{ id: 'team', label: 'Team', shortcut: '4' },
|
|
10
|
+
];
|
|
11
|
+
describe('FocusIndicator', () => {
|
|
12
|
+
describe('Current Focus', () => {
|
|
13
|
+
it('shows current focus area', () => {
|
|
14
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
|
|
15
|
+
expect(lastFrame()).toContain('Projects');
|
|
16
|
+
});
|
|
17
|
+
it('highlights current area', () => {
|
|
18
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "tasks" }));
|
|
19
|
+
expect(lastFrame()).toContain('Tasks');
|
|
20
|
+
});
|
|
21
|
+
it('shows focus indicator icon', () => {
|
|
22
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "logs" }));
|
|
23
|
+
expect(lastFrame()).toMatch(/▶|→|●|focus/i);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('Tab Navigation', () => {
|
|
27
|
+
it('shows all focus areas', () => {
|
|
28
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
|
|
29
|
+
expect(lastFrame()).toContain('Projects');
|
|
30
|
+
expect(lastFrame()).toContain('Tasks');
|
|
31
|
+
expect(lastFrame()).toContain('Logs');
|
|
32
|
+
expect(lastFrame()).toContain('Team');
|
|
33
|
+
});
|
|
34
|
+
it('shows area shortcuts', () => {
|
|
35
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
|
|
36
|
+
expect(lastFrame()).toContain('1');
|
|
37
|
+
expect(lastFrame()).toContain('2');
|
|
38
|
+
});
|
|
39
|
+
it('shows Tab hint for navigation', () => {
|
|
40
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
|
|
41
|
+
expect(lastFrame()).toMatch(/tab/i);
|
|
42
|
+
});
|
|
43
|
+
it('calls onFocusChange when area changed', () => {
|
|
44
|
+
const onFocusChange = vi.fn();
|
|
45
|
+
render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", onFocusChange: onFocusChange }));
|
|
46
|
+
// Focus change happens on Tab or number key
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('Focus Trap', () => {
|
|
50
|
+
it('shows modal indicator when trapped', () => {
|
|
51
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true, trappedLabel: "Settings" }));
|
|
52
|
+
expect(lastFrame()).toContain('Settings');
|
|
53
|
+
});
|
|
54
|
+
it('shows escape hint when trapped', () => {
|
|
55
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true }));
|
|
56
|
+
expect(lastFrame()).toMatch(/esc|close|exit/i);
|
|
57
|
+
});
|
|
58
|
+
it('dims other areas when trapped', () => {
|
|
59
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true }));
|
|
60
|
+
// When trapped, main areas should be dimmed
|
|
61
|
+
expect(lastFrame()).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('Skip Links', () => {
|
|
65
|
+
it('shows skip to content hint', () => {
|
|
66
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", showSkipLinks: true }));
|
|
67
|
+
expect(lastFrame()).toMatch(/skip|content|main/i);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('High Contrast Mode', () => {
|
|
71
|
+
it('supports high contrast mode', () => {
|
|
72
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", highContrast: true }));
|
|
73
|
+
expect(lastFrame()).toContain('Projects');
|
|
74
|
+
});
|
|
75
|
+
it('uses bold text in high contrast', () => {
|
|
76
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", highContrast: true }));
|
|
77
|
+
// Visual verification - renders without error
|
|
78
|
+
expect(lastFrame()).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('Compact Mode', () => {
|
|
82
|
+
it('shows compact indicator', () => {
|
|
83
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", compact: true }));
|
|
84
|
+
expect(lastFrame()).toContain('Projects');
|
|
85
|
+
});
|
|
86
|
+
it('hides shortcuts in compact mode', () => {
|
|
87
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", compact: true }));
|
|
88
|
+
const output = lastFrame() || '';
|
|
89
|
+
// Should be shorter
|
|
90
|
+
expect(output.length).toBeLessThan(200);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('Breadcrumb Path', () => {
|
|
94
|
+
it('shows breadcrumb when path provided', () => {
|
|
95
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", breadcrumb: ['Dashboard', 'Projects', 'Alpha'] }));
|
|
96
|
+
expect(lastFrame()).toContain('Dashboard');
|
|
97
|
+
expect(lastFrame()).toContain('Alpha');
|
|
98
|
+
});
|
|
99
|
+
it('uses separator for breadcrumb', () => {
|
|
100
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", breadcrumb: ['A', 'B'] }));
|
|
101
|
+
expect(lastFrame()).toMatch(/›|>|→|\//);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('Empty State', () => {
|
|
105
|
+
it('handles no areas gracefully', () => {
|
|
106
|
+
const { lastFrame } = render(_jsx(FocusIndicator, { areas: [], currentArea: "" }));
|
|
107
|
+
expect(lastFrame()).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('Callbacks', () => {
|
|
111
|
+
it('calls onEscape when trapped and Esc pressed', () => {
|
|
112
|
+
const onEscape = vi.fn();
|
|
113
|
+
render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true, onEscape: onEscape }));
|
|
114
|
+
// Escape happens on Esc key
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Shortcut {
|
|
2
|
+
key: string;
|
|
3
|
+
description: string;
|
|
4
|
+
context: string;
|
|
5
|
+
}
|
|
6
|
+
export interface KeyboardHelpProps {
|
|
7
|
+
shortcuts: Shortcut[];
|
|
8
|
+
searchQuery?: string;
|
|
9
|
+
currentContext?: string;
|
|
10
|
+
compact?: boolean;
|
|
11
|
+
isActive?: boolean;
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
onSearch?: (query: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function KeyboardHelp({ shortcuts, searchQuery, currentContext, compact, isActive, onClose, onSearch, }: KeyboardHelpProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
function groupByContext(shortcuts) {
|
|
5
|
+
const groups = {};
|
|
6
|
+
const order = ['global', 'navigation', 'actions'];
|
|
7
|
+
for (const shortcut of shortcuts) {
|
|
8
|
+
if (!groups[shortcut.context]) {
|
|
9
|
+
groups[shortcut.context] = [];
|
|
10
|
+
}
|
|
11
|
+
groups[shortcut.context].push(shortcut);
|
|
12
|
+
}
|
|
13
|
+
// Sort by predefined order, then alphabetically
|
|
14
|
+
const sortedContexts = Object.keys(groups).sort((a, b) => {
|
|
15
|
+
const aIndex = order.indexOf(a);
|
|
16
|
+
const bIndex = order.indexOf(b);
|
|
17
|
+
if (aIndex !== -1 && bIndex !== -1)
|
|
18
|
+
return aIndex - bIndex;
|
|
19
|
+
if (aIndex !== -1)
|
|
20
|
+
return -1;
|
|
21
|
+
if (bIndex !== -1)
|
|
22
|
+
return 1;
|
|
23
|
+
return a.localeCompare(b);
|
|
24
|
+
});
|
|
25
|
+
return sortedContexts.map((context) => ({
|
|
26
|
+
context,
|
|
27
|
+
shortcuts: groups[context],
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
export function KeyboardHelp({ shortcuts, searchQuery = '', currentContext, compact = false, isActive = true, onClose, onSearch, }) {
|
|
31
|
+
// Filter shortcuts
|
|
32
|
+
const filteredShortcuts = useMemo(() => {
|
|
33
|
+
if (!searchQuery)
|
|
34
|
+
return shortcuts;
|
|
35
|
+
const query = searchQuery.toLowerCase();
|
|
36
|
+
return shortcuts.filter((s) => s.key.toLowerCase().includes(query) ||
|
|
37
|
+
s.description.toLowerCase().includes(query));
|
|
38
|
+
}, [shortcuts, searchQuery]);
|
|
39
|
+
// Group shortcuts
|
|
40
|
+
const groupedShortcuts = useMemo(() => groupByContext(filteredShortcuts), [filteredShortcuts]);
|
|
41
|
+
useInput((input, key) => {
|
|
42
|
+
if (!isActive)
|
|
43
|
+
return;
|
|
44
|
+
// Close on Escape or ?
|
|
45
|
+
if (key.escape || input === '?') {
|
|
46
|
+
onClose?.();
|
|
47
|
+
}
|
|
48
|
+
}, { isActive });
|
|
49
|
+
// Empty state
|
|
50
|
+
if (shortcuts.length === 0) {
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No shortcuts configured" }) })] }));
|
|
52
|
+
}
|
|
53
|
+
// No matches
|
|
54
|
+
if (filteredShortcuts.length === 0) {
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Keyboard Shortcuts" }) }), searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Search: " }), _jsxs(Text, { color: "yellow", children: ["\"", searchQuery, "\""] })] })), _jsxs(Text, { color: "yellow", children: ["No shortcuts matching \"", searchQuery, "\""] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc close \u2022 / search" }) })] }));
|
|
56
|
+
}
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsxs(Text, { dimColor: true, children: [" (", filteredShortcuts.length, ")"] })] }), searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Search: " }), _jsxs(Text, { color: "yellow", children: ["\"", searchQuery, "\""] })] })), groupedShortcuts.map((group) => {
|
|
58
|
+
const isCurrentContext = group.context === currentContext;
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: compact ? 0 : 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: isCurrentContext ? 'cyan' : 'gray', children: [group.context.toUpperCase(), isCurrentContext && ' (current)'] }) }), group.shortcuts.map((shortcut, idx) => (_jsxs(Box, { marginBottom: compact ? 0 : 1, children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: "yellow", bold: true, children: shortcut.key }) }), _jsx(Text, { children: shortcut.description })] }, `${group.context}-${idx}`)))] }, group.context));
|
|
60
|
+
}), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc or ? close \u2022 / search" }) })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { KeyboardHelp } from './KeyboardHelp.js';
|
|
5
|
+
const sampleShortcuts = [
|
|
6
|
+
// Global
|
|
7
|
+
{ key: '?', description: 'Show keyboard help', context: 'global' },
|
|
8
|
+
{ key: 'Ctrl+K', description: 'Open command palette', context: 'global' },
|
|
9
|
+
{ key: 'q', description: 'Quit', context: 'global' },
|
|
10
|
+
// Navigation
|
|
11
|
+
{ key: 'j/↓', description: 'Move down', context: 'navigation' },
|
|
12
|
+
{ key: 'k/↑', description: 'Move up', context: 'navigation' },
|
|
13
|
+
{ key: 'h/←', description: 'Move left', context: 'navigation' },
|
|
14
|
+
{ key: 'l/→', description: 'Move right', context: 'navigation' },
|
|
15
|
+
{ key: 'Tab', description: 'Next section', context: 'navigation' },
|
|
16
|
+
{ key: 'Shift+Tab', description: 'Previous section', context: 'navigation' },
|
|
17
|
+
// Actions
|
|
18
|
+
{ key: 'Enter', description: 'Select/confirm', context: 'actions' },
|
|
19
|
+
{ key: 'Esc', description: 'Cancel/back', context: 'actions' },
|
|
20
|
+
{ key: 'e', description: 'Edit', context: 'actions' },
|
|
21
|
+
{ key: 'r', description: 'Refresh', context: 'actions' },
|
|
22
|
+
];
|
|
23
|
+
describe('KeyboardHelp', () => {
|
|
24
|
+
describe('Shortcut Display', () => {
|
|
25
|
+
it('shows shortcut keys', () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
27
|
+
expect(lastFrame()).toContain('Ctrl+K');
|
|
28
|
+
expect(lastFrame()).toContain('j/↓');
|
|
29
|
+
});
|
|
30
|
+
it('shows shortcut descriptions', () => {
|
|
31
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
32
|
+
expect(lastFrame()).toContain('Open command palette');
|
|
33
|
+
expect(lastFrame()).toContain('Move down');
|
|
34
|
+
});
|
|
35
|
+
it('formats key combinations clearly', () => {
|
|
36
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
37
|
+
expect(lastFrame()).toContain('Ctrl+K');
|
|
38
|
+
expect(lastFrame()).toContain('Shift+Tab');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('Context Grouping', () => {
|
|
42
|
+
it('shows Global section', () => {
|
|
43
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
44
|
+
expect(lastFrame()).toMatch(/global/i);
|
|
45
|
+
});
|
|
46
|
+
it('shows Navigation section', () => {
|
|
47
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
48
|
+
expect(lastFrame()).toMatch(/navigation/i);
|
|
49
|
+
});
|
|
50
|
+
it('shows Actions section', () => {
|
|
51
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
52
|
+
expect(lastFrame()).toMatch(/actions/i);
|
|
53
|
+
});
|
|
54
|
+
it('groups shortcuts by context', () => {
|
|
55
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
56
|
+
// All navigation shortcuts should be together
|
|
57
|
+
expect(lastFrame()).toContain('Move down');
|
|
58
|
+
expect(lastFrame()).toContain('Move up');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('Search', () => {
|
|
62
|
+
it('filters shortcuts by key', () => {
|
|
63
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "ctrl" }));
|
|
64
|
+
expect(lastFrame()).toContain('Ctrl+K');
|
|
65
|
+
});
|
|
66
|
+
it('filters shortcuts by description', () => {
|
|
67
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "palette" }));
|
|
68
|
+
expect(lastFrame()).toContain('command palette');
|
|
69
|
+
});
|
|
70
|
+
it('shows no results message', () => {
|
|
71
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "xyznonexistent" }));
|
|
72
|
+
expect(lastFrame()).toMatch(/no.*match|no.*shortcut|not.*found/i);
|
|
73
|
+
});
|
|
74
|
+
it('is case insensitive', () => {
|
|
75
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "CTRL" }));
|
|
76
|
+
expect(lastFrame()).toContain('Ctrl+K');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('Dismissible Overlay', () => {
|
|
80
|
+
it('shows close hint', () => {
|
|
81
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
82
|
+
expect(lastFrame()).toMatch(/esc|close|dismiss|\?/i);
|
|
83
|
+
});
|
|
84
|
+
it('calls onClose when dismissed', () => {
|
|
85
|
+
const onClose = vi.fn();
|
|
86
|
+
render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, onClose: onClose }));
|
|
87
|
+
// Close happens on Esc or ? key
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('Current Context', () => {
|
|
91
|
+
it('highlights current context shortcuts', () => {
|
|
92
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, currentContext: "navigation" }));
|
|
93
|
+
expect(lastFrame()).toMatch(/navigation/i);
|
|
94
|
+
});
|
|
95
|
+
it('shows all shortcuts when no context', () => {
|
|
96
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
97
|
+
expect(lastFrame()).toContain('Quit');
|
|
98
|
+
expect(lastFrame()).toContain('Move down');
|
|
99
|
+
expect(lastFrame()).toContain('Select/confirm');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('Header', () => {
|
|
103
|
+
it('shows Keyboard Shortcuts title', () => {
|
|
104
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
105
|
+
expect(lastFrame()).toMatch(/keyboard|shortcuts|help/i);
|
|
106
|
+
});
|
|
107
|
+
it('shows shortcut count', () => {
|
|
108
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
109
|
+
// Should show total count
|
|
110
|
+
expect(lastFrame()).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('Empty State', () => {
|
|
114
|
+
it('shows message when no shortcuts', () => {
|
|
115
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: [] }));
|
|
116
|
+
expect(lastFrame()).toMatch(/no.*shortcut|empty/i);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('Compact Mode', () => {
|
|
120
|
+
it('supports compact display', () => {
|
|
121
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, compact: true }));
|
|
122
|
+
expect(lastFrame()).toContain('Ctrl+K');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('Navigation', () => {
|
|
126
|
+
it('shows search hint', () => {
|
|
127
|
+
const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
|
|
128
|
+
expect(lastFrame()).toMatch(/\/|search|type/i);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface LogSearchProps {
|
|
2
|
+
query: string;
|
|
3
|
+
matchCount?: number;
|
|
4
|
+
currentMatch?: number;
|
|
5
|
+
caseSensitive?: boolean;
|
|
6
|
+
isActive?: boolean;
|
|
7
|
+
onChange: (query: string) => void;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onNext?: () => void;
|
|
10
|
+
onPrev?: () => void;
|
|
11
|
+
onToggleCase?: () => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function LogSearch({ query, matchCount, currentMatch, caseSensitive, isActive, onChange, onClose, onNext, onPrev, onToggleCase, }: LogSearchProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export function LogSearch({ query, matchCount = 0, currentMatch = 0, caseSensitive = false, isActive = true, onChange, onClose, onNext, onPrev, onToggleCase, }) {
|
|
4
|
+
useInput((input, key) => {
|
|
5
|
+
if (!isActive)
|
|
6
|
+
return;
|
|
7
|
+
// Close on Escape
|
|
8
|
+
if (key.escape) {
|
|
9
|
+
onClose();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Submit on Enter (just keeps current search)
|
|
13
|
+
if (key.return) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// Next/Prev match
|
|
17
|
+
if (input === 'n' && !key.ctrl && matchCount > 0) {
|
|
18
|
+
onNext?.();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (input === 'N' && matchCount > 0) {
|
|
22
|
+
onPrev?.();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Toggle case sensitivity
|
|
26
|
+
if (key.ctrl && input === 'c') {
|
|
27
|
+
onToggleCase?.();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Backspace
|
|
31
|
+
if (key.backspace || key.delete) {
|
|
32
|
+
onChange(query.slice(0, -1));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Regular character input
|
|
36
|
+
if (input && !key.ctrl && !key.meta) {
|
|
37
|
+
onChange(query + input);
|
|
38
|
+
}
|
|
39
|
+
}, { isActive });
|
|
40
|
+
const hasMatches = matchCount > 0;
|
|
41
|
+
const showMatchCount = query.length > 0;
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "/" }), _jsx(Text, { children: query }), _jsx(Text, { color: "cyan", children: "\u258F" }), showMatchCount && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: hasMatches ? 'green' : 'red', children: currentMatch > 0 ? `${currentMatch} of ${matchCount}` : `${matchCount} match${matchCount !== 1 ? 'es' : ''}` }) })), caseSensitive && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: "magenta", children: "[Aa]" }) }))] }), query.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Type to search logs..." }) })), query.length > 0 && matchCount === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "No matches found" }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Enter search \u2022 Esc close", matchCount > 1 && ' • n/N next/prev'] }) })] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|