skill-search 0.0.1
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 +381 -0
- package/bin/skill.js +348 -0
- package/bin/tui.js +33 -0
- package/package.json +53 -0
- package/scripts/package-lock.json +6 -0
- package/scripts/setup-env.bat +58 -0
- package/scripts/test-scan.js +42 -0
- package/src/actions.js +216 -0
- package/src/api.js +306 -0
- package/src/cache.js +107 -0
- package/src/config.js +220 -0
- package/src/fallback-index.json +6 -0
- package/src/interactive.js +23 -0
- package/src/localCrawler.js +204 -0
- package/src/matcher.js +170 -0
- package/src/store.js +156 -0
- package/src/syncer.js +226 -0
- package/src/theme.js +191 -0
- package/src/tui/ActionModal.js +209 -0
- package/src/tui/AddDelView.js +212 -0
- package/src/tui/App.js +739 -0
- package/src/tui/AsciiHeader.js +35 -0
- package/src/tui/CommandPalette.js +64 -0
- package/src/tui/ConfigView.js +168 -0
- package/src/tui/DetailView.js +139 -0
- package/src/tui/DualPane.js +114 -0
- package/src/tui/PrimaryView.js +163 -0
- package/src/tui/SearchBox.js +26 -0
- package/src/tui/SearchView.js +121 -0
- package/src/tui/SkillList.js +102 -0
- package/src/tui/SyncView.js +143 -0
- package/src/tui/ThemeView.js +116 -0
- package/src/utils.js +83 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/tui/SearchBox.js - Central Search Input Component
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { Box, Text } = require('ink');
|
|
5
|
+
const TextInput = require('ink-text-input').default;
|
|
6
|
+
|
|
7
|
+
function SearchBox({ value, onChange, onSubmit, placeholder, isFocused }) {
|
|
8
|
+
return React.createElement(Box, {
|
|
9
|
+
borderStyle: 'single',
|
|
10
|
+
borderColor: isFocused ? 'cyan' : 'gray',
|
|
11
|
+
paddingX: 1
|
|
12
|
+
},
|
|
13
|
+
React.createElement(Box, { flexGrow: 1 },
|
|
14
|
+
React.createElement(TextInput, {
|
|
15
|
+
value: value,
|
|
16
|
+
onChange: onChange,
|
|
17
|
+
onSubmit: onSubmit,
|
|
18
|
+
placeholder: placeholder || 'Type to search...',
|
|
19
|
+
focus: isFocused
|
|
20
|
+
})
|
|
21
|
+
),
|
|
22
|
+
React.createElement(Text, { color: 'gray' }, ' [/] Commands [Esc] Quit')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = SearchBox;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/tui/SearchView.js - Search View Component
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { useState, useEffect } = React;
|
|
5
|
+
const { Box, Text, useInput } = require('ink');
|
|
6
|
+
const TextInput = require('ink-text-input').default;
|
|
7
|
+
const { searchSkills } = require('../matcher');
|
|
8
|
+
|
|
9
|
+
function SearchView({ onSelect, onCancel }) {
|
|
10
|
+
const [query, setQuery] = useState('');
|
|
11
|
+
const [results, setResults] = useState([]);
|
|
12
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
+
const [isInputMode, setIsInputMode] = useState(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (query.length >= 2) {
|
|
17
|
+
performSearch(query);
|
|
18
|
+
} else {
|
|
19
|
+
setResults([]);
|
|
20
|
+
}
|
|
21
|
+
}, [query]);
|
|
22
|
+
|
|
23
|
+
const performSearch = async (q) => {
|
|
24
|
+
const res = await searchSkills(q);
|
|
25
|
+
setResults(res);
|
|
26
|
+
setSelectedIndex(0);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useInput((input, key) => {
|
|
30
|
+
if (key.escape) {
|
|
31
|
+
if (!isInputMode) {
|
|
32
|
+
setIsInputMode(true);
|
|
33
|
+
} else {
|
|
34
|
+
onCancel();
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isInputMode) {
|
|
40
|
+
if (key.upArrow) {
|
|
41
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
42
|
+
} else if (key.downArrow) {
|
|
43
|
+
setSelectedIndex(Math.min(results.length - 1, selectedIndex + 1));
|
|
44
|
+
} else if (key.return && results.length > 0) {
|
|
45
|
+
onSelect(results[selectedIndex]);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
if (key.downArrow && results.length > 0) {
|
|
49
|
+
setIsInputMode(false);
|
|
50
|
+
setSelectedIndex(0);
|
|
51
|
+
} else if (key.return && results.length > 0) {
|
|
52
|
+
setIsInputMode(false);
|
|
53
|
+
setSelectedIndex(0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
59
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
60
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, '🔍 Search Skills')
|
|
61
|
+
),
|
|
62
|
+
|
|
63
|
+
// Search input
|
|
64
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
65
|
+
React.createElement(Text, { color: 'gray' }, '> '),
|
|
66
|
+
React.createElement(TextInput, {
|
|
67
|
+
value: query,
|
|
68
|
+
onChange: setQuery,
|
|
69
|
+
placeholder: 'Type to search...',
|
|
70
|
+
focus: isInputMode
|
|
71
|
+
})
|
|
72
|
+
),
|
|
73
|
+
|
|
74
|
+
// Results
|
|
75
|
+
results.length > 0 && React.createElement(Box, { flexDirection: 'column' },
|
|
76
|
+
React.createElement(Text, { color: 'gray', dimColor: true },
|
|
77
|
+
`Found ${results.length} results:`
|
|
78
|
+
),
|
|
79
|
+
React.createElement(Box, { marginTop: 1, flexDirection: 'column' },
|
|
80
|
+
results.slice(0, 10).map((skill, idx) => {
|
|
81
|
+
const isSelected = !isInputMode && idx === selectedIndex;
|
|
82
|
+
const score = Math.round((1 - (skill.score || 0)) * 100);
|
|
83
|
+
|
|
84
|
+
return React.createElement(Box, { key: skill.id },
|
|
85
|
+
React.createElement(Text, {
|
|
86
|
+
color: isSelected ? 'cyan' : 'white',
|
|
87
|
+
inverse: isSelected
|
|
88
|
+
},
|
|
89
|
+
isSelected ? '▶ ' : ' '
|
|
90
|
+
),
|
|
91
|
+
React.createElement(Text, {
|
|
92
|
+
color: isSelected ? 'cyan' : 'green'
|
|
93
|
+
},
|
|
94
|
+
skill.id.padEnd(25)
|
|
95
|
+
),
|
|
96
|
+
React.createElement(Text, { color: 'gray' },
|
|
97
|
+
(skill.name || '').slice(0, 30)
|
|
98
|
+
),
|
|
99
|
+
React.createElement(Text, { color: 'gray', dimColor: true },
|
|
100
|
+
` (${score}%)`
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
),
|
|
106
|
+
|
|
107
|
+
results.length === 0 && query.length >= 2 && React.createElement(
|
|
108
|
+
Text, { color: 'yellow' }, `No results for "${query}"`
|
|
109
|
+
),
|
|
110
|
+
|
|
111
|
+
React.createElement(Box, { marginTop: 1 },
|
|
112
|
+
React.createElement(Text, { color: 'gray', dimColor: true },
|
|
113
|
+
isInputMode
|
|
114
|
+
? 'Down Navigate to results │ Esc Cancel'
|
|
115
|
+
: 'Up/Down Navigate │ Enter Select │ Esc Back to input'
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = SearchView;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/tui/SkillList.js - Skills List View Component
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { useState, useEffect } = React;
|
|
5
|
+
const { Box, Text, useInput } = require('ink');
|
|
6
|
+
const { listSkills } = require('../matcher');
|
|
7
|
+
const store = require('../store');
|
|
8
|
+
|
|
9
|
+
function SkillList({ onSelect }) {
|
|
10
|
+
const [skills, setSkills] = useState([]);
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
14
|
+
const visibleCount = 15; // Number of visible items
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
loadSkills();
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const loadSkills = async () => {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
try {
|
|
23
|
+
const allSkills = await listSkills();
|
|
24
|
+
setSkills(allSkills);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
setSkills([]);
|
|
27
|
+
}
|
|
28
|
+
setLoading(false);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
useInput((input, key) => {
|
|
32
|
+
if (skills.length === 0) return;
|
|
33
|
+
|
|
34
|
+
if (key.upArrow) {
|
|
35
|
+
const newIndex = Math.max(0, selectedIndex - 1);
|
|
36
|
+
setSelectedIndex(newIndex);
|
|
37
|
+
if (newIndex < scrollOffset) {
|
|
38
|
+
setScrollOffset(newIndex);
|
|
39
|
+
}
|
|
40
|
+
} else if (key.downArrow) {
|
|
41
|
+
const newIndex = Math.min(skills.length - 1, selectedIndex + 1);
|
|
42
|
+
setSelectedIndex(newIndex);
|
|
43
|
+
if (newIndex >= scrollOffset + visibleCount) {
|
|
44
|
+
setScrollOffset(newIndex - visibleCount + 1);
|
|
45
|
+
}
|
|
46
|
+
} else if (key.return) {
|
|
47
|
+
onSelect(skills[selectedIndex]);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (loading) {
|
|
52
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
53
|
+
React.createElement(Text, { color: 'yellow' }, '⏳ Loading skills...')
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (skills.length === 0) {
|
|
58
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
59
|
+
React.createElement(Text, { color: 'yellow' }, '⚠️ No local data found.'),
|
|
60
|
+
React.createElement(Text, { color: 'gray' }, 'Press "y" to open Sync view and sync data first.')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const visibleSkills = skills.slice(scrollOffset, scrollOffset + visibleCount);
|
|
65
|
+
|
|
66
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
67
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
68
|
+
React.createElement(Text, { bold: true, color: 'cyan' },
|
|
69
|
+
`📚 Skills (${skills.length} total)`
|
|
70
|
+
),
|
|
71
|
+
scrollOffset > 0 && React.createElement(Text, { color: 'gray' }, ' ▲'),
|
|
72
|
+
scrollOffset + visibleCount < skills.length && React.createElement(Text, { color: 'gray' }, ' ▼')
|
|
73
|
+
),
|
|
74
|
+
|
|
75
|
+
visibleSkills.map((skill, idx) => {
|
|
76
|
+
const actualIndex = scrollOffset + idx;
|
|
77
|
+
const isSelected = actualIndex === selectedIndex;
|
|
78
|
+
const hasDoc = store.getDoc(skill.id) !== null;
|
|
79
|
+
|
|
80
|
+
return React.createElement(Box, { key: skill.id },
|
|
81
|
+
React.createElement(Text, {
|
|
82
|
+
color: isSelected ? 'cyan' : 'white',
|
|
83
|
+
bold: isSelected,
|
|
84
|
+
inverse: isSelected
|
|
85
|
+
},
|
|
86
|
+
isSelected ? '▶ ' : ' '
|
|
87
|
+
),
|
|
88
|
+
React.createElement(Text, {
|
|
89
|
+
color: isSelected ? 'cyan' : 'green'
|
|
90
|
+
},
|
|
91
|
+
skill.id.padEnd(25)
|
|
92
|
+
),
|
|
93
|
+
React.createElement(Text, { color: 'gray' },
|
|
94
|
+
(skill.name || skill.description || '').slice(0, 35)
|
|
95
|
+
),
|
|
96
|
+
hasDoc && React.createElement(Text, { color: 'green' }, ' ✓')
|
|
97
|
+
);
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = SkillList;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// src/tui/SyncView.js - Sync View Component
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { useState } = React;
|
|
5
|
+
const { Box, Text, useInput } = require('ink');
|
|
6
|
+
const Spinner = require('ink-spinner').default;
|
|
7
|
+
const syncer = require('../syncer');
|
|
8
|
+
|
|
9
|
+
function SyncView({ onBack }) {
|
|
10
|
+
const [status, setStatus] = useState(null);
|
|
11
|
+
const [syncing, setSyncing] = useState(false);
|
|
12
|
+
const [progress, setProgress] = useState('');
|
|
13
|
+
const [result, setResult] = useState(null);
|
|
14
|
+
const [selectedOption, setSelectedOption] = useState(0);
|
|
15
|
+
|
|
16
|
+
const options = [
|
|
17
|
+
{ key: 'local', label: 'Sync Local Skills', desc: 'Sync locally found skills to primary directory' },
|
|
18
|
+
{ key: 'remote', label: 'Sync Remote Skills', desc: 'Download all remote skills to primary directory' },
|
|
19
|
+
{ key: 'all', label: 'Sync All', desc: 'Sync both local and remote skills to primary directory' },
|
|
20
|
+
{ key: 'status', label: 'View Status', desc: 'Check sync status' }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
const currentStatus = syncer.getStatus();
|
|
25
|
+
setStatus(currentStatus);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
useInput((input, key) => {
|
|
29
|
+
if (syncing) return;
|
|
30
|
+
|
|
31
|
+
if (key.escape) {
|
|
32
|
+
onBack();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (key.upArrow) {
|
|
37
|
+
setSelectedOption(Math.max(0, selectedOption - 1));
|
|
38
|
+
} else if (key.downArrow) {
|
|
39
|
+
setSelectedOption(Math.min(options.length - 1, selectedOption + 1));
|
|
40
|
+
} else if (key.return) {
|
|
41
|
+
executeOption(options[selectedOption].key);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const executeOption = async (optionKey) => {
|
|
46
|
+
setResult(null);
|
|
47
|
+
|
|
48
|
+
if (optionKey === 'status') {
|
|
49
|
+
const s = syncer.getStatus();
|
|
50
|
+
setStatus(s);
|
|
51
|
+
setResult(s ? 'Status refreshed' : 'No sync history');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setSyncing(true);
|
|
56
|
+
setProgress('Starting sync...');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await syncer.sync({
|
|
60
|
+
mode: optionKey // 'local', 'remote', 'all'
|
|
61
|
+
});
|
|
62
|
+
setResult('✅ Sync completed successfully!');
|
|
63
|
+
setStatus(syncer.getStatus());
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setResult(`❌ Sync failed: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setSyncing(false);
|
|
69
|
+
setProgress('');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
73
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
74
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, '🔄 Sync Manager')
|
|
75
|
+
),
|
|
76
|
+
|
|
77
|
+
// Current status
|
|
78
|
+
status && React.createElement(Box, {
|
|
79
|
+
flexDirection: 'column',
|
|
80
|
+
marginBottom: 1,
|
|
81
|
+
marginBottom: 1,
|
|
82
|
+
// Removed borderStyle
|
|
83
|
+
// paddingX: 1
|
|
84
|
+
},
|
|
85
|
+
// Use dim color for label for consistency
|
|
86
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, ' 📊 Current Status:'),
|
|
87
|
+
React.createElement(Text, null,
|
|
88
|
+
` Last Sync: ${status.lastSync ? new Date(status.lastSync).toLocaleString() : 'Never'}`
|
|
89
|
+
),
|
|
90
|
+
React.createElement(Text, null, ` Total Skills: ${status.totalSkills || 0}`),
|
|
91
|
+
React.createElement(Text, null, ` Cached Docs: ${status.syncedDocs || 0}`)
|
|
92
|
+
),
|
|
93
|
+
|
|
94
|
+
!status && React.createElement(Text, { color: 'yellow', marginBottom: 1 },
|
|
95
|
+
'⚠️ No sync history. Please sync first.'
|
|
96
|
+
),
|
|
97
|
+
|
|
98
|
+
// Options
|
|
99
|
+
React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
100
|
+
React.createElement(Text, { color: 'gray', marginBottom: 1 }, 'Select an action:'),
|
|
101
|
+
options.map((opt, idx) => {
|
|
102
|
+
const isSelected = idx === selectedOption;
|
|
103
|
+
return React.createElement(Box, { key: opt.key },
|
|
104
|
+
React.createElement(Text, {
|
|
105
|
+
color: isSelected ? 'cyan' : 'white',
|
|
106
|
+
inverse: isSelected
|
|
107
|
+
},
|
|
108
|
+
isSelected ? '▶ ' : ' '
|
|
109
|
+
),
|
|
110
|
+
React.createElement(Text, {
|
|
111
|
+
color: isSelected ? 'cyan' : 'white',
|
|
112
|
+
bold: isSelected
|
|
113
|
+
},
|
|
114
|
+
opt.label
|
|
115
|
+
),
|
|
116
|
+
React.createElement(Text, { color: 'gray' }, ` - ${opt.desc}`)
|
|
117
|
+
);
|
|
118
|
+
})
|
|
119
|
+
),
|
|
120
|
+
|
|
121
|
+
// Progress
|
|
122
|
+
syncing && React.createElement(Box, { marginTop: 1 },
|
|
123
|
+
React.createElement(Spinner, { type: 'dots' }),
|
|
124
|
+
React.createElement(Text, { color: 'yellow' }, ` ${progress}`)
|
|
125
|
+
),
|
|
126
|
+
|
|
127
|
+
// Result
|
|
128
|
+
result && React.createElement(Box, { marginTop: 1 },
|
|
129
|
+
React.createElement(Text, {
|
|
130
|
+
color: result.startsWith('✅') ? 'green' :
|
|
131
|
+
result.startsWith('❌') ? 'red' : 'gray'
|
|
132
|
+
}, result)
|
|
133
|
+
),
|
|
134
|
+
|
|
135
|
+
React.createElement(Box, { marginTop: 1 },
|
|
136
|
+
React.createElement(Text, { color: 'gray', dimColor: true },
|
|
137
|
+
'Up/Down Navigate │ Enter Execute'
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = SyncView;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/tui/ThemeView.js - Theme Selection View Component
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { useState, useEffect } = React;
|
|
5
|
+
const { Box, Text, useInput } = require('ink');
|
|
6
|
+
const theme = require('../theme');
|
|
7
|
+
|
|
8
|
+
function ThemeView({ onBack, onThemeChange }) {
|
|
9
|
+
const [currentTheme, setCurrentTheme] = useState(theme.getThemeName());
|
|
10
|
+
const [selectedOption, setSelectedOption] = useState(0);
|
|
11
|
+
const [message, setMessage] = useState(null);
|
|
12
|
+
|
|
13
|
+
const availableThemes = theme.getAvailableThemes();
|
|
14
|
+
const t = theme.getTheme(); // Get current theme colors
|
|
15
|
+
|
|
16
|
+
useInput((input, key) => {
|
|
17
|
+
if (key.escape) {
|
|
18
|
+
onBack();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (key.upArrow) {
|
|
23
|
+
setSelectedOption(Math.max(0, selectedOption - 1));
|
|
24
|
+
} else if (key.downArrow) {
|
|
25
|
+
setSelectedOption(Math.min(availableThemes.length - 1, selectedOption + 1));
|
|
26
|
+
} else if (key.return) {
|
|
27
|
+
const selected = availableThemes[selectedOption];
|
|
28
|
+
if (selected.key !== currentTheme) {
|
|
29
|
+
theme.setThemeName(selected.key);
|
|
30
|
+
setCurrentTheme(selected.key);
|
|
31
|
+
setMessage(`✅ Theme changed to ${selected.name}. Restart for full effect.`);
|
|
32
|
+
if (onThemeChange) {
|
|
33
|
+
onThemeChange(selected.key);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
setMessage(`Already using ${selected.name} theme.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
42
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
43
|
+
React.createElement(Text, { bold: true, color: t.primary }, '🎨 Theme Settings')
|
|
44
|
+
),
|
|
45
|
+
|
|
46
|
+
// Current theme display
|
|
47
|
+
React.createElement(Box, {
|
|
48
|
+
flexDirection: 'column',
|
|
49
|
+
marginBottom: 1
|
|
50
|
+
},
|
|
51
|
+
React.createElement(Text, { color: t.textDim }, ` Current Theme: ${theme.THEMES[currentTheme]?.name || currentTheme}`)
|
|
52
|
+
),
|
|
53
|
+
|
|
54
|
+
// Theme options
|
|
55
|
+
React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
56
|
+
React.createElement(Text, { color: t.textDim, marginBottom: 1 }, 'Select a theme:'),
|
|
57
|
+
availableThemes.map((themeOption, idx) => {
|
|
58
|
+
const isSelected = idx === selectedOption;
|
|
59
|
+
const isCurrent = themeOption.key === currentTheme;
|
|
60
|
+
|
|
61
|
+
return React.createElement(Box, { key: themeOption.key },
|
|
62
|
+
React.createElement(Text, {
|
|
63
|
+
color: isSelected ? t.selected : t.text,
|
|
64
|
+
inverse: isSelected
|
|
65
|
+
},
|
|
66
|
+
isSelected ? '▶ ' : ' '
|
|
67
|
+
),
|
|
68
|
+
React.createElement(Text, {
|
|
69
|
+
color: isSelected ? t.selected : t.text,
|
|
70
|
+
bold: isSelected
|
|
71
|
+
},
|
|
72
|
+
themeOption.name
|
|
73
|
+
),
|
|
74
|
+
isCurrent && React.createElement(Text, { color: t.success }, ' (current)')
|
|
75
|
+
);
|
|
76
|
+
})
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
// Preview box
|
|
80
|
+
React.createElement(Box, {
|
|
81
|
+
flexDirection: 'column',
|
|
82
|
+
marginTop: 1,
|
|
83
|
+
borderStyle: 'single',
|
|
84
|
+
borderColor: availableThemes[selectedOption]?.key === 'dark' ? 'cyan' : 'blue',
|
|
85
|
+
paddingX: 1,
|
|
86
|
+
paddingY: 0
|
|
87
|
+
},
|
|
88
|
+
React.createElement(Text, { bold: true }, `Preview: ${availableThemes[selectedOption]?.name} Theme`),
|
|
89
|
+
React.createElement(Text, {
|
|
90
|
+
color: availableThemes[selectedOption]?.key === 'dark' ? 'cyan' : 'blue'
|
|
91
|
+
}, 'Primary Text'),
|
|
92
|
+
React.createElement(Text, {
|
|
93
|
+
color: availableThemes[selectedOption]?.key === 'dark' ? 'green' : 'magenta'
|
|
94
|
+
}, 'Local Highlight'),
|
|
95
|
+
React.createElement(Text, {
|
|
96
|
+
color: availableThemes[selectedOption]?.key === 'dark' ? 'yellow' : 'red'
|
|
97
|
+
}, 'Remote Highlight'),
|
|
98
|
+
React.createElement(Text, { color: 'gray' }, 'Dimmed Text')
|
|
99
|
+
),
|
|
100
|
+
|
|
101
|
+
// Message
|
|
102
|
+
message && React.createElement(Box, { marginTop: 1 },
|
|
103
|
+
React.createElement(Text, {
|
|
104
|
+
color: message.startsWith('✅') ? t.success : t.textDim
|
|
105
|
+
}, message)
|
|
106
|
+
),
|
|
107
|
+
|
|
108
|
+
React.createElement(Box, { marginTop: 1 },
|
|
109
|
+
React.createElement(Text, { color: t.textDim, dimColor: true },
|
|
110
|
+
'Up/Down Navigate │ Enter Select │ Esc Back'
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = ThemeView;
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { marked } = require('marked');
|
|
2
|
+
const { markedTerminal } = require('marked-terminal');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const clipboardy = require('clipboardy');
|
|
5
|
+
|
|
6
|
+
// Configure marked with terminal renderer (marked-terminal v6+ API)
|
|
7
|
+
marked.use(markedTerminal({
|
|
8
|
+
codespan: chalk.yellow,
|
|
9
|
+
code: chalk.yellow,
|
|
10
|
+
link: chalk.blue.underline,
|
|
11
|
+
strong: chalk.bold.cyan,
|
|
12
|
+
firstHeading: chalk.bold.magenta.underline,
|
|
13
|
+
heading: chalk.bold.green
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
function formatMarkdown(content) {
|
|
17
|
+
if (!content) return '';
|
|
18
|
+
try {
|
|
19
|
+
return marked(content);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(chalk.red('Markdown parsing failed'));
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function copyToClipboard(content) {
|
|
27
|
+
try {
|
|
28
|
+
await clipboardy.write(content);
|
|
29
|
+
return true;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(chalk.red('Clipboard copy failed: ' + err.message));
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format byte size
|
|
38
|
+
*/
|
|
39
|
+
function formatBytes(bytes, decimals = 2) {
|
|
40
|
+
if (bytes === 0) return '0 Bytes';
|
|
41
|
+
|
|
42
|
+
const k = 1024;
|
|
43
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
44
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
45
|
+
|
|
46
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
47
|
+
|
|
48
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
formatMarkdown,
|
|
53
|
+
copyToClipboard,
|
|
54
|
+
formatBytes,
|
|
55
|
+
parseFrontMatter
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse Front Matter from Markdown
|
|
60
|
+
*/
|
|
61
|
+
function parseFrontMatter(content) {
|
|
62
|
+
if (!content) return {};
|
|
63
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
64
|
+
if (!match) return {};
|
|
65
|
+
|
|
66
|
+
const frontMatter = {};
|
|
67
|
+
const lines = match[1].split('\n');
|
|
68
|
+
|
|
69
|
+
lines.forEach(line => {
|
|
70
|
+
const parts = line.split(':');
|
|
71
|
+
if (parts.length >= 2) {
|
|
72
|
+
const key = parts[0].trim();
|
|
73
|
+
let value = parts.slice(1).join(':').trim();
|
|
74
|
+
// Remove quotes if present
|
|
75
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
76
|
+
value = value.slice(1, -1);
|
|
77
|
+
}
|
|
78
|
+
frontMatter[key] = value;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return frontMatter;
|
|
83
|
+
}
|