Skip to content

Commit 9c353fa

Browse files
committed
feat: add breadcrumb navigation and keyboard shortcuts overlay
- Breadcrumb path display in CodeView file cards - KeyboardShortcutsDialog component (Ctrl+/ to toggle) - Ctrl+/ global keyboard shortcut handler - Marked.js configuration restored in index.tsx - Escape priority: shortcuts > AI chat > search > settings
1 parent 1d1c9f7 commit 9c353fa

5 files changed

Lines changed: 114 additions & 4 deletions

File tree

structure-insight/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SearchDialog from './components/SearchDialog';
1111
import ConfirmationDialog from './components/ConfirmationDialog';
1212
import AIChat from './components/AIChat';
1313
import FileRankDialog from './components/FileRankDialog';
14+
import KeyboardShortcutsDialog from './components/KeyboardShortcutsDialog';
1415

1516
const App: React.FC = () => {
1617
const codeViewRef = React.useRef<HTMLDivElement>(null);
@@ -112,6 +113,14 @@ const App: React.FC = () => {
112113
/>
113114
)}
114115
</AnimatePresence>
116+
<AnimatePresence>
117+
{state.isShortcutsOpen && (
118+
<KeyboardShortcutsDialog
119+
isOpen={state.isShortcutsOpen}
120+
onClose={() => handlers.setIsShortcutsOpen(false)}
121+
/>
122+
)}
123+
</AnimatePresence>
115124
<AnimatePresence>
116125
{state.toastMessage && <Toast message={state.toastMessage} onDone={() => handlers.setToastMessage(null)} />}
117126
</AnimatePresence>

structure-insight/components/CodeView.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ const FileCard: React.FC<FileCardProps> = ({
152152

153153
return (
154154
<div className={`bg-light-panel dark:bg-dark-panel rounded-lg overflow-hidden border border-light-border dark:border-dark-border transition-colors duration-300 focus-within:ring-2 focus-within:ring-primary ${file.excluded ? 'opacity-75' : ''}`}>
155+
<div className="px-3 py-1.5 border-b border-light-border dark:border-dark-border text-xs text-light-subtle-text dark:text-dark-subtle-text">
156+
{file.path.split('/').map((segment, i, segments) => (
157+
<React.Fragment key={i}>
158+
{i > 0 && <span className="mx-1 opacity-50">&gt;</span>}
159+
<span className={i === segments.length - 1 ? 'font-semibold' : ''}>{segment}</span>
160+
</React.Fragment>
161+
))}
162+
</div>
155163
<div className="flex justify-between items-center p-3 bg-light-header/80 dark:bg-dark-header/80 border-b border-light-border dark:border-dark-border sticky top-0 z-[1] backdrop-blur-sm">
156164
<div className="font-mono text-sm text-light-text dark:text-dark-text truncate flex items-center" title={file.path}>
157165
<i className="fa-regular fa-file-lines mr-2 text-light-subtle-text dark:text-dark-subtle-text"></i>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import { motion } from 'framer-motion';
3+
4+
interface KeyboardShortcutsDialogProps {
5+
isOpen: boolean;
6+
onClose: () => void;
7+
}
8+
9+
const shortcuts = [
10+
{ keys: ['Ctrl', 'O'], description: 'Open folder' },
11+
{ keys: ['Ctrl', 'F'], description: 'Find in files' },
12+
{ keys: ['Ctrl', 'S'], description: 'Save as text' },
13+
{ keys: ['Escape'], description: 'Close dialog' },
14+
{ keys: ['Ctrl', '/'], description: 'Show this help' },
15+
];
16+
17+
const KeyboardShortcutsDialog: React.FC<KeyboardShortcutsDialogProps> = ({ isOpen, onClose }) => {
18+
React.useEffect(() => {
19+
if (isOpen) {
20+
const handleKeyDown = (e: KeyboardEvent) => {
21+
if (e.key === 'Escape') onClose();
22+
};
23+
window.addEventListener('keydown', handleKeyDown);
24+
return () => window.removeEventListener('keydown', handleKeyDown);
25+
}
26+
}, [isOpen, onClose]);
27+
28+
if (!isOpen) return null;
29+
30+
return (
31+
<motion.div
32+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
33+
initial={{ opacity: 0 }}
34+
animate={{ opacity: 1 }}
35+
exit={{ opacity: 0 }}
36+
onClick={onClose}
37+
>
38+
<motion.div
39+
className="bg-light-panel dark:bg-dark-panel rounded-xl shadow-2xl border border-light-border dark:border-dark-border w-full max-w-sm overflow-hidden"
40+
onClick={(e) => e.stopPropagation()}
41+
initial={{ scale: 0.95, opacity: 0, y: 10 }}
42+
animate={{ scale: 1, opacity: 1, y: 0 }}
43+
exit={{ scale: 0.95, opacity: 0, y: 10 }}
44+
transition={{ duration: 0.2, ease: 'easeOut' }}
45+
>
46+
<div className="flex items-center justify-between px-6 py-4 border-b border-light-border dark:border-dark-border bg-light-bg/50 dark:bg-dark-bg/50 backdrop-blur-md">
47+
<h3 className="font-bold text-lg text-light-text dark:text-dark-text">Keyboard Shortcuts</h3>
48+
<button
49+
onClick={onClose}
50+
className="w-8 h-8 rounded-full hover:bg-light-border dark:hover:bg-dark-border flex items-center justify-center text-light-subtle-text dark:text-dark-subtle-text transition-colors"
51+
>
52+
<i className="fa-solid fa-times"></i>
53+
</button>
54+
</div>
55+
56+
<div className="p-6">
57+
<div className="grid gap-3">
58+
{shortcuts.map(({ keys, description }) => (
59+
<div key={description} className="flex items-center justify-between py-1.5">
60+
<span className="text-sm text-light-text dark:text-dark-text">{description}</span>
61+
<div className="flex items-center gap-1">
62+
{keys.map((key, i) => (
63+
<React.Fragment key={key}>
64+
<kbd className="px-2 py-1 text-xs font-mono font-medium rounded-md bg-light-bg dark:bg-dark-bg border border-light-border dark:border-dark-border text-light-subtle-text dark:text-dark-subtle-text shadow-sm">
65+
{key}
66+
</kbd>
67+
{i < keys.length - 1 && (
68+
<span className="text-xs text-light-subtle-text dark:text-dark-subtle-text">+</span>
69+
)}
70+
</React.Fragment>
71+
))}
72+
</div>
73+
</div>
74+
))}
75+
</div>
76+
</div>
77+
</motion.div>
78+
</motion.div>
79+
);
80+
};
81+
82+
export default React.memo(KeyboardShortcutsDialog);

structure-insight/hooks/useAppLogic.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const useAppLogic = (
2020
const [isSettingsOpen, setIsSettingsOpen] = React.useState(false);
2121
const [isAiChatOpen, setIsAiChatOpen] = React.useState(false);
2222
const [isFileRankOpen, setIsFileRankOpen] = React.useState(false);
23+
const [isShortcutsOpen, setIsShortcutsOpen] = React.useState(false);
2324
const [toastMessage, setToastMessage] = React.useState<string | null>(null);
2425
const [confirmation, setConfirmation] = React.useState<ConfirmationState>({isOpen: false, title: '', message: '', onConfirm: () => {}});
2526

@@ -188,9 +189,11 @@ export const useAppLogic = (
188189
if (e.key === 'f') { e.preventDefault(); if (processedData) setIsSearchOpen(p => !p); }
189190
if (e.key === 's') { e.preventDefault(); if (processedData) handleSave(); }
190191
if (e.key === 'o') { e.preventDefault(); handleFileSelect(); }
192+
if (e.key === '/') { e.preventDefault(); setIsShortcutsOpen(p => !p); }
191193
}
192194
if (e.key === 'Escape') {
193-
if (isAiChatOpen) { e.preventDefault(); setIsAiChatOpen(false); }
195+
if (isShortcutsOpen) { e.preventDefault(); setIsShortcutsOpen(false); }
196+
else if (isAiChatOpen) { e.preventDefault(); setIsAiChatOpen(false); }
194197
else if (isSearchOpen) { e.preventDefault(); setIsSearchOpen(false); }
195198
else if (isFileRankOpen) { e.preventDefault(); setIsFileRankOpen(false); }
196199
else if (isSettingsOpen) { e.preventDefault(); setIsSettingsOpen(false); }
@@ -199,7 +202,7 @@ export const useAppLogic = (
199202
};
200203
window.addEventListener('keydown', handleGlobalKeys);
201204
return () => window.removeEventListener('keydown', handleGlobalKeys);
202-
}, [isSearchOpen, isSettingsOpen, isAiChatOpen, isFileRankOpen, isLoading, processedData, handleSave, handleFileSelect, handleCancel]);
205+
}, [isSearchOpen, isSettingsOpen, isAiChatOpen, isFileRankOpen, isShortcutsOpen, isLoading, processedData, handleSave, handleFileSelect, handleCancel]);
203206

204207
// --- Memoized Stats ---
205208
const stats = React.useMemo(() => {
@@ -219,7 +222,7 @@ export const useAppLogic = (
219222
editingPath, markdownPreviewPaths, confirmation,
220223
isDark, panelWidth, extractContent, fontSize, showCharCount, maxCharsThreshold,
221224
lastProcessedFiles, mobileView, stats,
222-
isSearchOpen, isFileRankOpen, searchResults, activeResultIndex, isMobile, isAiChatOpen,
225+
isSearchOpen, isFileRankOpen, isShortcutsOpen, searchResults, activeResultIndex, isMobile, isAiChatOpen,
223226
selectedFilePath, selectedFile, activeView,
224227
searchQuery, searchOptions, activeMatchIndexInFile,
225228
},
@@ -230,7 +233,7 @@ export const useAppLogic = (
230233
handleDeleteFile, handleFileTreeSelect, setEditingPath, handleSaveEdit, handleToggleMarkdownPreview,
231234
handleMouseDownResize,
232235
handleMobileViewToggle,
233-
setIsSearchOpen, setIsFileRankOpen, handleSearch, handleNavigate, setIsAiChatOpen,
236+
setIsSearchOpen, setIsFileRankOpen, setIsShortcutsOpen, handleSearch, handleNavigate, setIsAiChatOpen,
234237
setActiveView,
235238
handleCopyPath,
236239
handleToggleExclude,

structure-insight/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom/client';
3+
import { marked } from 'marked';
34
import App from './App';
45

6+
// Configure marked for markdown rendering with syntax highlighting
7+
marked.setOptions({
8+
langPrefix: 'hljs language-',
9+
gfm: true,
10+
breaks: true,
11+
});
12+
513
const rootElement = document.getElementById('root');
614
if (!rootElement) {
715
throw new Error("Could not find root element to mount to");

0 commit comments

Comments
 (0)