mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-12 03:48:38 +00:00
* fix: consolidate icon imports, fix stale refs and package name collision three components were importing directly from lucide-react instead of going through the centralized @/lib/lucide-icons module like the rest of the codebase. added the missing Keyboard and BarChart2 exports to the icons module and updated the imports. also: - removed duplicate mermaid init comment in ProcessFlowModal - replaced placeholder issue #XXX with a descriptive note in git.ts - updated stale KuzuDB reference to LadybugDB in ARCHITECTURE.md - renamed gitnexus-web package.json name from "gitnexus" to "gitnexus-web" to avoid collision with the CLI package * fix: complete package rename in lockfile and cite #2054 in git.ts comment Address tri-review findings: package-lock.json name fields (top-level and packages[""]) now match the renamed gitnexus-web package, and the getCanonicalRemote doc comment cites #2054 instead of dropping the issue reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(autofix): apply prettier + eslint fixes via /autofix command --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
723 lines
22 KiB
TypeScript
723 lines
22 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
X,
|
|
GitBranch,
|
|
Search,
|
|
Filter,
|
|
Zap,
|
|
Keyboard,
|
|
BarChart2,
|
|
HelpCircle,
|
|
} from '@/lib/lucide-icons';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface HelpPanelProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
nodeCount: number;
|
|
edgeCount: number;
|
|
}
|
|
|
|
type TabId = 'overview' | 'graph' | 'search' | 'ai' | 'shortcuts' | 'status';
|
|
|
|
interface Tab {
|
|
id: TabId;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
const tabs: Tab[] = [
|
|
{ id: 'overview', icon: <HelpCircle className="h-4 w-4" /> },
|
|
{ id: 'graph', icon: <GitBranch className="h-4 w-4" /> },
|
|
{ id: 'search', icon: <Search className="h-4 w-4" /> },
|
|
{ id: 'ai', icon: <Zap className="h-4 w-4" /> },
|
|
{ id: 'shortcuts', icon: <Keyboard className="h-4 w-4" /> },
|
|
{ id: 'status', icon: <BarChart2 className="h-4 w-4" /> },
|
|
];
|
|
|
|
const shortcuts = [
|
|
{ labelKey: 'shortcuts.searchNodes', mac: '⌘ K', win: 'Ctrl K' },
|
|
{ labelKey: 'shortcuts.deselectClose', mac: 'Esc', win: 'Esc' },
|
|
];
|
|
|
|
const nodeColors = [
|
|
{ color: '#10b981', labelKey: 'nodeTypes.function', descKey: 'nodeTypes.functionDesc' },
|
|
{ color: '#3b82f6', labelKey: 'nodeTypes.file', descKey: 'nodeTypes.fileDesc' },
|
|
{ color: '#f59e0b', labelKey: 'nodeTypes.class', descKey: 'nodeTypes.classDesc' },
|
|
{ color: '#14b8a6', labelKey: 'nodeTypes.method', descKey: 'nodeTypes.methodDesc' },
|
|
{ color: '#ec4899', labelKey: 'nodeTypes.interface', descKey: 'nodeTypes.interfaceDesc' },
|
|
{ color: '#6366f1', labelKey: 'nodeTypes.folder', descKey: 'nodeTypes.folderDesc' },
|
|
];
|
|
|
|
const getStatusItems = (t: (key: string) => string, nodeCount: number, edgeCount: number) => [
|
|
{
|
|
badge: (
|
|
<span
|
|
style={{
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: '50%',
|
|
background: '#34d399',
|
|
display: 'inline-block',
|
|
flexShrink: 0,
|
|
}}
|
|
/>
|
|
),
|
|
title: t('status.ready'),
|
|
desc: t('status.readyDesc'),
|
|
},
|
|
{
|
|
badge: (
|
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#a78bfa', flexShrink: 0 }}>
|
|
{nodeCount}
|
|
</span>
|
|
),
|
|
title: t('status.nodesCount'),
|
|
desc: t('status.nodesCountDesc'),
|
|
},
|
|
{
|
|
badge: (
|
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#60a5fa', flexShrink: 0 }}>
|
|
{edgeCount}
|
|
</span>
|
|
),
|
|
title: t('status.edgesCount'),
|
|
desc: t('status.edgesCountDesc'),
|
|
},
|
|
{
|
|
badge: (
|
|
<span
|
|
style={{
|
|
fontSize: 11,
|
|
fontWeight: 500,
|
|
color: '#34d399',
|
|
flexShrink: 0,
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{t('status.semanticReadyBadge')}
|
|
</span>
|
|
),
|
|
title: t('status.aiIndexStatus'),
|
|
desc: t('status.aiIndexStatusDesc'),
|
|
},
|
|
// { badge: <span style={{ fontSize: 11, fontWeight: 500, color: '#9ca3af', flexShrink: 0 }}>typescript</span>, title: 'Language', desc: 'Primary language detected in the repo' },
|
|
];
|
|
|
|
const kbdStyle: React.CSSProperties = {
|
|
fontSize: 11,
|
|
background: 'rgba(255,255,255,0.08)',
|
|
borderRadius: 4,
|
|
padding: '2px 8px',
|
|
color: '#e2e2e8',
|
|
fontFamily: 'monospace',
|
|
border: '0.5px solid rgba(255,255,255,0.12)',
|
|
whiteSpace: 'nowrap',
|
|
};
|
|
|
|
const kbdWinStyle: React.CSSProperties = {
|
|
...kbdStyle,
|
|
color: '#93c5fd',
|
|
};
|
|
|
|
function TabContent({
|
|
active,
|
|
nodeCount,
|
|
edgeCount,
|
|
}: {
|
|
active: TabId;
|
|
nodeCount: number;
|
|
edgeCount: number;
|
|
}) {
|
|
const { t } = useTranslation('help');
|
|
|
|
if (active === 'overview')
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<p
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
margin: '0 0 4px',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{t('overview.gettingStarted')}
|
|
</p>
|
|
|
|
<div
|
|
style={{
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 10,
|
|
padding: '12px 14px',
|
|
borderLeft: '2px solid #a78bfa',
|
|
}}
|
|
>
|
|
<p style={{ fontSize: 13, fontWeight: 500, color: '#e2e2e8', margin: '0 0 4px' }}>
|
|
{t('overview.whatIsTitle')}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('overview.whatIsDescription')}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 10,
|
|
padding: '12px 14px',
|
|
borderLeft: '2px solid #34d399',
|
|
}}
|
|
>
|
|
<p style={{ fontSize: 13, fontWeight: 500, color: '#e2e2e8', margin: '0 0 4px' }}>
|
|
{t('overview.currentRepoTitle')}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('overview.loadedCounts', { nodeCount, edgeCount })}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 10,
|
|
padding: '12px 14px',
|
|
borderLeft: '2px solid #60a5fa',
|
|
}}
|
|
>
|
|
<p style={{ fontSize: 13, fontWeight: 500, color: '#e2e2e8', margin: '0 0 4px' }}>
|
|
{t('overview.threeWaysTitle')}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
<strong style={{ color: '#e2e2e8', fontWeight: 500 }}>1.</strong>{' '}
|
|
{t('overview.wayInspect')}
|
|
<br />
|
|
<strong style={{ color: '#e2e2e8', fontWeight: 500 }}>2.</strong>{' '}
|
|
{t('overview.waySearch')}
|
|
<br />
|
|
<strong style={{ color: '#e2e2e8', fontWeight: 500 }}>3.</strong> {t('overview.wayAsk')}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 10,
|
|
padding: '12px 14px',
|
|
borderLeft: '2px solid #fbbf24',
|
|
}}
|
|
>
|
|
<p style={{ fontSize: 13, fontWeight: 500, color: '#e2e2e8', margin: '0 0 4px' }}>
|
|
{t('overview.navigationTitle')}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
· {t('overview.navZoom')} <br />· {t('overview.navPan')} <br />·{' '}
|
|
{t('overview.navFocus')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (active === 'graph')
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<p
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
margin: '0 0 4px',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{t('graph.nodeColorLegend')}
|
|
</p>
|
|
|
|
{nodeColors.map(({ color, labelKey, descKey }) => {
|
|
const label = t(labelKey);
|
|
return (
|
|
<div key={labelKey} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
|
<span
|
|
style={{
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: '50%',
|
|
background: color,
|
|
flexShrink: 0,
|
|
marginTop: 2,
|
|
}}
|
|
/>
|
|
<div>
|
|
<p style={{ fontSize: 12, fontWeight: 500, color: '#e2e2e8', margin: '0 0 2px' }}>
|
|
{t('graph.nodeLabel', { label })}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0 }}>{t(descKey)}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
<div style={{ borderTop: '0.5px solid rgba(255,255,255,0.08)', margin: '4px 0' }} />
|
|
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('graph.sizeDescription')}
|
|
</p>
|
|
|
|
<div
|
|
style={{ background: 'rgba(255,255,255,0.04)', borderRadius: 10, padding: '10px 14px' }}
|
|
>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('graph.detailDescription')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (active === 'search')
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<p
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
margin: '0 0 4px',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{t('search.title')}
|
|
</p>
|
|
|
|
<div
|
|
style={{ background: 'rgba(255,255,255,0.04)', borderRadius: 10, padding: '12px 14px' }}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
<kbd style={kbdStyle}>⌘K</kbd>/<kbd style={kbdStyle}>Ctrl K</kbd>
|
|
<p style={{ fontSize: 12, fontWeight: 500, color: '#e2e2e8', margin: 0 }}>
|
|
{t('search.searchNodes')}
|
|
</p>
|
|
</div>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('search.searchDescription')}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
style={{ background: 'rgba(255,255,255,0.04)', borderRadius: 10, padding: '12px 14px' }}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
<Filter style={{ width: 14, height: 14, color: '#a78bfa', flexShrink: 0 }} />
|
|
<p style={{ fontSize: 12, fontWeight: 500, color: '#e2e2e8', margin: 0 }}>
|
|
{t('search.filterPanel')}
|
|
</p>
|
|
</div>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('search.filterDescription')}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
style={{ background: 'rgba(255,255,255,0.04)', borderRadius: 10, padding: '12px 14px' }}
|
|
>
|
|
<p style={{ fontSize: 12, fontWeight: 500, color: '#e2e2e8', margin: '0 0 6px' }}>
|
|
{t('search.syntax')}
|
|
</p>
|
|
{[
|
|
{ query: 'auth', hintKey: 'search.hints.nameFragment' },
|
|
{ query: './utils/', hintKey: 'search.hints.pathPrefix' },
|
|
{ query: 'type:config', hintKey: 'search.hints.nodeType' },
|
|
].map(({ query, hintKey }) => (
|
|
<div
|
|
key={query}
|
|
style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4 }}
|
|
>
|
|
<code
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#a78bfa',
|
|
background: 'rgba(167,139,250,0.1)',
|
|
borderRadius: 4,
|
|
padding: '1px 6px',
|
|
fontFamily: 'monospace',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{query}
|
|
</code>
|
|
<span style={{ fontSize: 12, color: '#6b7280' }}>{t(hintKey)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (active === 'ai')
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<p
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
margin: '0 0 4px',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{t('ai.title')}
|
|
</p>
|
|
|
|
<div
|
|
style={{
|
|
background: 'rgba(167,139,250,0.08)',
|
|
border: '0.5px solid rgba(167,139,250,0.25)',
|
|
borderRadius: 10,
|
|
padding: '12px 14px',
|
|
}}
|
|
>
|
|
<p style={{ fontSize: 12, fontWeight: 500, color: '#a78bfa', margin: '0 0 4px' }}>
|
|
{t('ai.semanticReady')}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0, lineHeight: 1.6 }}>
|
|
{t('ai.description')}
|
|
</p>
|
|
</div>
|
|
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: '4px 0 2px' }}>{t('tryAsking')}</p>
|
|
{[
|
|
t('ai.questions.dependencies'),
|
|
t('ai.questions.circular'),
|
|
t('ai.questions.connected'),
|
|
t('ai.questions.imports'),
|
|
].map((q) => (
|
|
<div
|
|
key={q}
|
|
style={{
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 8,
|
|
padding: '8px 12px',
|
|
fontSize: 12,
|
|
color: '#e2e2e8',
|
|
fontStyle: 'italic',
|
|
}}
|
|
>
|
|
{q}
|
|
</div>
|
|
))}
|
|
|
|
<div style={{ borderTop: '0.5px solid rgba(255,255,255,0.08)', margin: '4px 0' }} />
|
|
|
|
<p style={{ fontSize: 12, color: '#6b7280', margin: 0, lineHeight: 1.6 }}>
|
|
{t('ai.openPrompt')}
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
if (active === 'shortcuts')
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
|
{/* Column headers */}
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '1fr 80px 88px',
|
|
gap: 8,
|
|
padding: '0 0 8px',
|
|
borderBottom: '0.5px solid rgba(255,255,255,0.08)',
|
|
marginBottom: 4,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{t('shortcuts.columns.action')}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
Mac
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#93c5fd',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
Windows
|
|
</span>
|
|
</div>
|
|
|
|
{shortcuts.map(({ labelKey, mac, win }, i) => (
|
|
<div
|
|
key={labelKey}
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '1fr 80px 88px',
|
|
gap: 8,
|
|
alignItems: 'center',
|
|
padding: '8px 0',
|
|
borderBottom:
|
|
i < shortcuts.length - 1 ? '0.5px solid rgba(255,255,255,0.05)' : 'none',
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 12, color: '#9ca3af' }}>{t(labelKey)}</span>
|
|
<span style={{ display: 'flex', justifyContent: 'center' }}>
|
|
<kbd style={kbdStyle}>{mac}</kbd>
|
|
</span>
|
|
<span style={{ display: 'flex', justifyContent: 'center' }}>
|
|
<kbd style={kbdWinStyle}>{win}</kbd>
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
if (active === 'status')
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
<p
|
|
style={{
|
|
fontSize: 11,
|
|
color: '#6b7280',
|
|
margin: '0 0 4px',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
{t('status.explained')}
|
|
</p>
|
|
{getStatusItems(t, nodeCount, edgeCount).map(({ badge, title, desc }) => (
|
|
<div
|
|
key={title}
|
|
style={{
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 10,
|
|
padding: '10px 14px',
|
|
display: 'flex',
|
|
gap: 12,
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
{badge}
|
|
<div>
|
|
<p style={{ fontSize: 12, fontWeight: 500, color: '#e2e2e8', margin: '0 0 2px' }}>
|
|
{title}
|
|
</p>
|
|
<p style={{ fontSize: 12, color: '#9ca3af', margin: 0 }}>{desc}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
export const HelpPanel = ({ isOpen, onClose, nodeCount, edgeCount }: HelpPanelProps) => {
|
|
const { t } = useTranslation('help');
|
|
const [active, setActive] = useState<TabId>('overview');
|
|
const localizedTabs = tabs.map((tab) => ({ ...tab, label: t(`tabs.${tab.id}`) }));
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 50,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
{/* Backdrop */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: 0,
|
|
background: 'rgba(0,0,0,0.6)',
|
|
backdropFilter: 'blur(4px)',
|
|
}}
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Panel */}
|
|
<div
|
|
style={{
|
|
position: 'relative',
|
|
background: '#12121a',
|
|
border: '0.5px solid rgba(255,255,255,0.12)',
|
|
borderRadius: 16,
|
|
boxShadow: '0 25px 60px rgba(0,0,0,0.7)',
|
|
width: '100%',
|
|
maxWidth: 680,
|
|
margin: '0 16px',
|
|
height: '60vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
fontFamily: 'var(--font-mono, monospace)',
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: '16px 20px',
|
|
borderBottom: '0.5px solid rgba(255,255,255,0.08)',
|
|
background: 'rgba(255,255,255,0.02)',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'rgba(167,139,250,0.15)',
|
|
borderRadius: 12,
|
|
}}
|
|
>
|
|
<HelpCircle style={{ width: 20, height: 20, color: '#a78bfa' }} />
|
|
</div>
|
|
<div>
|
|
<h2 style={{ fontSize: 16, fontWeight: 600, color: '#e2e2e8', margin: 0 }}>
|
|
{t('title')}
|
|
</h2>
|
|
<p style={{ fontSize: 12, color: '#6b7280', margin: 0 }}>{t('footer')}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
style={{
|
|
padding: 8,
|
|
color: '#6b7280',
|
|
background: 'transparent',
|
|
border: 'none',
|
|
borderRadius: 8,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
transition: 'color 0.15s',
|
|
}}
|
|
onMouseEnter={(e) => (e.currentTarget.style.color = '#e2e2e8')}
|
|
onMouseLeave={(e) => (e.currentTarget.style.color = '#6b7280')}
|
|
>
|
|
<X style={{ width: 20, height: 20 }} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body: sidebar + content */}
|
|
<div
|
|
style={{ display: 'grid', gridTemplateColumns: '168px 1fr', flex: 1, overflow: 'hidden' }}
|
|
>
|
|
{/* Sidebar nav */}
|
|
<div
|
|
style={{
|
|
borderRight: '0.5px solid rgba(255,255,255,0.08)',
|
|
padding: '12px 8px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 2,
|
|
}}
|
|
>
|
|
{localizedTabs.map(({ id, label, icon }) => {
|
|
const isActive = active === id;
|
|
return (
|
|
<button
|
|
key={id}
|
|
onClick={() => setActive(id)}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
textAlign: 'left',
|
|
background: isActive ? 'rgba(167,139,250,0.12)' : 'transparent',
|
|
border: 'none',
|
|
borderRadius: 8,
|
|
padding: '8px 10px',
|
|
fontSize: 12,
|
|
fontFamily: 'inherit',
|
|
color: isActive ? '#a78bfa' : '#9ca3af',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.15s',
|
|
width: '100%',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (!isActive) {
|
|
e.currentTarget.style.color = '#e2e2e8';
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.04)';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (!isActive) {
|
|
e.currentTarget.style.color = '#9ca3af';
|
|
e.currentTarget.style.background = 'transparent';
|
|
}
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
color: isActive ? '#a78bfa' : '#6b7280',
|
|
display: 'flex',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{icon}
|
|
</span>
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Content pane */}
|
|
<div style={{ padding: '20px', overflowY: 'auto' }}>
|
|
<TabContent active={active} nodeCount={nodeCount} edgeCount={edgeCount} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: '10px 20px',
|
|
borderTop: '0.5px solid rgba(255,255,255,0.08)',
|
|
background: 'rgba(255,255,255,0.01)',
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 11, color: '#4b5563' }}>{t('footerLong')}</span>
|
|
<a
|
|
href="https://github.com/abhigyanpatwari/GitNexus"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ fontSize: 11, color: '#a78bfa', textDecoration: 'none' }}
|
|
>
|
|
{t('docsGithub')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|