Spaces:
Running
Running
Commit ·
e85ce30
1
Parent(s): 0366c3e
added dark mode
Browse files- frontend/src/App.tsx +1 -1
- frontend/src/components/Sidebar.tsx +25 -0
- frontend/src/index.css +95 -1
- frontend/src/pages/BacktestResults.tsx +5 -5
- frontend/src/pages/Dashboard.tsx +6 -6
- frontend/src/pages/FactorAnalysis.tsx +9 -9
- frontend/src/pages/HoldingsTracker.tsx +6 -6
- frontend/src/pages/Login.tsx +1 -1
- frontend/src/pages/MarketExplorer.tsx +6 -6
- frontend/src/pages/PortfolioAnalysis.tsx +1 -1
- frontend/src/pages/Register.tsx +1 -1
- frontend/src/pages/Sentiment.tsx +5 -5
- frontend/src/pages/StrategyBuilder.tsx +7 -4
frontend/src/App.tsx
CHANGED
|
@@ -29,7 +29,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
|
| 29 |
return (
|
| 30 |
<div className="app-layout">
|
| 31 |
<Sidebar onExpandChange={setSidebarExpanded} />
|
| 32 |
-
<main className="app-main" style={{ marginLeft: sidebarExpanded ? 260 : 68 }}>
|
| 33 |
{children}
|
| 34 |
</main>
|
| 35 |
</div>
|
|
|
|
| 29 |
return (
|
| 30 |
<div className="app-layout">
|
| 31 |
<Sidebar onExpandChange={setSidebarExpanded} />
|
| 32 |
+
<main className="app-main" style={{ marginLeft: sidebarExpanded ? 260 : 68, transition: 'margin-left 0.25s cubic-bezier(0.4, 0, 0.2, 1)' }}>
|
| 33 |
{children}
|
| 34 |
</main>
|
| 35 |
</div>
|
frontend/src/components/Sidebar.tsx
CHANGED
|
@@ -12,6 +12,9 @@ export default function Sidebar({ onExpandChange }: SidebarProps) {
|
|
| 12 |
const [drawerOpen, setDrawerOpen] = useState(false);
|
| 13 |
const [isMobile, setIsMobile] = useState(false);
|
| 14 |
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
useEffect(() => {
|
| 17 |
const check = () => setIsMobile(window.innerWidth <= 768);
|
|
@@ -20,6 +23,12 @@ export default function Sidebar({ onExpandChange }: SidebarProps) {
|
|
| 20 |
return () => window.removeEventListener('resize', check);
|
| 21 |
}, []);
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
// Close drawer on route change
|
| 24 |
useEffect(() => { setDrawerOpen(false); }, [location.pathname]);
|
| 25 |
|
|
@@ -29,6 +38,14 @@ export default function Sidebar({ onExpandChange }: SidebarProps) {
|
|
| 29 |
navigate('/login');
|
| 30 |
};
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
const allLinks = [
|
| 33 |
{
|
| 34 |
section: 'Overview',
|
|
@@ -119,6 +136,10 @@ export default function Sidebar({ onExpandChange }: SidebarProps) {
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
<button className="sidebar-link sidebar-logout" onClick={handleLogout}>
|
| 123 |
<span className="sidebar-icon">
|
| 124 |
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd"/></svg>
|
|
@@ -192,6 +213,10 @@ export default function Sidebar({ onExpandChange }: SidebarProps) {
|
|
| 192 |
</div>
|
| 193 |
</div>
|
| 194 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
<button className="sidebar-link sidebar-logout" onClick={handleLogout} style={{ width: '100%' }}>
|
| 196 |
<span className="sidebar-icon">
|
| 197 |
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd"/></svg>
|
|
|
|
| 12 |
const [drawerOpen, setDrawerOpen] = useState(false);
|
| 13 |
const [isMobile, setIsMobile] = useState(false);
|
| 14 |
const [isExpanded, setIsExpanded] = useState(false);
|
| 15 |
+
const [isDark, setIsDark] = useState(() => {
|
| 16 |
+
return localStorage.getItem('qh_theme') === 'dark' || document.documentElement.getAttribute('data-theme') === 'dark';
|
| 17 |
+
});
|
| 18 |
|
| 19 |
useEffect(() => {
|
| 20 |
const check = () => setIsMobile(window.innerWidth <= 768);
|
|
|
|
| 23 |
return () => window.removeEventListener('resize', check);
|
| 24 |
}, []);
|
| 25 |
|
| 26 |
+
// Apply theme on mount and when toggled
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
| 29 |
+
localStorage.setItem('qh_theme', isDark ? 'dark' : 'light');
|
| 30 |
+
}, [isDark]);
|
| 31 |
+
|
| 32 |
// Close drawer on route change
|
| 33 |
useEffect(() => { setDrawerOpen(false); }, [location.pathname]);
|
| 34 |
|
|
|
|
| 38 |
navigate('/login');
|
| 39 |
};
|
| 40 |
|
| 41 |
+
const toggleTheme = () => setIsDark(prev => !prev);
|
| 42 |
+
|
| 43 |
+
const themeIcon = isDark ? (
|
| 44 |
+
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd"/></svg>
|
| 45 |
+
) : (
|
| 46 |
+
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
| 47 |
+
);
|
| 48 |
+
|
| 49 |
const allLinks = [
|
| 50 |
{
|
| 51 |
section: 'Overview',
|
|
|
|
| 136 |
</div>
|
| 137 |
</div>
|
| 138 |
)}
|
| 139 |
+
<button className="sidebar-link" onClick={toggleTheme}>
|
| 140 |
+
<span className="sidebar-icon">{themeIcon}</span>
|
| 141 |
+
<span className="sidebar-label">{isDark ? 'Light Mode' : 'Dark Mode'}</span>
|
| 142 |
+
</button>
|
| 143 |
<button className="sidebar-link sidebar-logout" onClick={handleLogout}>
|
| 144 |
<span className="sidebar-icon">
|
| 145 |
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd"/></svg>
|
|
|
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
)}
|
| 216 |
+
<button className="sidebar-link" onClick={toggleTheme} style={{ width: '100%' }}>
|
| 217 |
+
<span className="sidebar-icon">{themeIcon}</span>
|
| 218 |
+
<span className="sidebar-label">{isDark ? 'Light Mode' : 'Dark Mode'}</span>
|
| 219 |
+
</button>
|
| 220 |
<button className="sidebar-link sidebar-logout" onClick={handleLogout} style={{ width: '100%' }}>
|
| 221 |
<span className="sidebar-icon">
|
| 222 |
<svg viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd"/></svg>
|
frontend/src/index.css
CHANGED
|
@@ -60,8 +60,102 @@
|
|
| 60 |
--transition-fast: 150ms ease;
|
| 61 |
--transition-base: 250ms ease;
|
| 62 |
--transition-slow: 400ms ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
/* ── Reset ───────────────────────────────────────────────────────────── */
|
| 66 |
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 67 |
|
|
@@ -464,7 +558,7 @@ tr:hover td { background: var(--bg-hover); }
|
|
| 464 |
}
|
| 465 |
.badge-primary { background: var(--accent-lighter); color: var(--accent); }
|
| 466 |
.badge-emerald { background: #e6f9f0; color: var(--green-positive); }
|
| 467 |
-
.badge-rose { background:
|
| 468 |
.badge-amber { background: #fffbeb; color: var(--amber-neutral); }
|
| 469 |
|
| 470 |
/* ── Tabs ────────────────────────────────────────────────────────────── */
|
|
|
|
| 60 |
--transition-fast: 150ms ease;
|
| 61 |
--transition-base: 250ms ease;
|
| 62 |
--transition-slow: 400ms ease;
|
| 63 |
+
|
| 64 |
+
/* Chart-specific variables */
|
| 65 |
+
--chart-axis: #8892a4;
|
| 66 |
+
--chart-grid: rgba(148, 163, 184, 0.08);
|
| 67 |
+
--chart-tooltip-bg: #ffffff;
|
| 68 |
+
--chart-tooltip-border: #e2e5ea;
|
| 69 |
+
--chart-bar-bg: #f1f5f9;
|
| 70 |
+
--chart-green: #00b386;
|
| 71 |
+
--chart-green-fill: rgba(0, 179, 134, 0.15);
|
| 72 |
+
--error-bg: #fef2f2;
|
| 73 |
+
--error-border: #fecaca;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* ═══════════════════════════════════════════════════════════════════════
|
| 77 |
+
Dark Mode Theme — True Black
|
| 78 |
+
═══════════════════════════════════════════════════════════════════════ */
|
| 79 |
+
[data-theme="dark"] {
|
| 80 |
+
--bg-primary: #0a0a0a;
|
| 81 |
+
--bg-secondary: #141414;
|
| 82 |
+
--bg-tertiary: #1c1c1c;
|
| 83 |
+
--bg-card: #111111;
|
| 84 |
+
--bg-glass: rgba(17, 17, 17, 0.92);
|
| 85 |
+
--bg-hover: rgba(0, 125, 99, 0.1);
|
| 86 |
+
|
| 87 |
+
--text-primary: #e8e8e8;
|
| 88 |
+
--text-secondary: #a0a8b8;
|
| 89 |
+
--text-muted: #6b7588;
|
| 90 |
+
--text-accent: #00c896;
|
| 91 |
+
--text-on-dark: #f0f4f2;
|
| 92 |
+
--text-on-dark-muted: #a3b8b0;
|
| 93 |
+
|
| 94 |
+
--accent: #00a87a;
|
| 95 |
+
--accent-light: #00c896;
|
| 96 |
+
--accent-lighter: rgba(0, 168, 122, 0.12);
|
| 97 |
+
--accent-dark: #008f68;
|
| 98 |
+
--accent-gold: #d4a843;
|
| 99 |
+
--accent-gold-light: #e6c470;
|
| 100 |
+
--green-positive: #10b981;
|
| 101 |
+
--red-negative: #ef4444;
|
| 102 |
+
--amber-neutral: #d4a843;
|
| 103 |
+
--blue-info: #3b82f6;
|
| 104 |
+
|
| 105 |
+
--gradient-accent: linear-gradient(135deg, #00a87a, #00c896);
|
| 106 |
+
--gradient-card-hover: linear-gradient(145deg, rgba(0,168,122,0.04), rgba(0,168,122,0.08));
|
| 107 |
+
|
| 108 |
+
--border-color: #1f2228;
|
| 109 |
+
--border-subtle: #181b20;
|
| 110 |
+
--border-accent: rgba(0, 168, 122, 0.25);
|
| 111 |
+
|
| 112 |
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
| 113 |
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
| 114 |
+
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.5);
|
| 115 |
+
--shadow-card: 0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.15);
|
| 116 |
+
--shadow-card-hover: 0 10px 40px rgba(0, 0, 0, 0.35);
|
| 117 |
+
|
| 118 |
+
--chart-axis: #6b7588;
|
| 119 |
+
--chart-grid: rgba(100, 116, 139, 0.1);
|
| 120 |
+
--chart-tooltip-bg: #1c1c1c;
|
| 121 |
+
--chart-tooltip-border: #2a2d32;
|
| 122 |
+
--chart-bar-bg: #1c1c1c;
|
| 123 |
+
--chart-green: #00d09c;
|
| 124 |
+
--chart-green-fill: rgba(0, 208, 156, 0.2);
|
| 125 |
+
--error-bg: rgba(239, 68, 68, 0.1);
|
| 126 |
+
--error-border: rgba(239, 68, 68, 0.25);
|
| 127 |
}
|
| 128 |
|
| 129 |
+
/* Dark mode body transition */
|
| 130 |
+
body {
|
| 131 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/* Dark mode: badge overrides */
|
| 135 |
+
[data-theme="dark"] .badge-primary { background: rgba(0,168,122,0.15); color: var(--accent); }
|
| 136 |
+
[data-theme="dark"] .badge-emerald { background: rgba(16,185,129,0.15); color: var(--green-positive); }
|
| 137 |
+
[data-theme="dark"] .badge-rose { background: rgba(239,68,68,0.15); color: var(--red-negative); }
|
| 138 |
+
[data-theme="dark"] .badge-amber { background: rgba(212,168,67,0.15); color: var(--amber-neutral); }
|
| 139 |
+
|
| 140 |
+
/* Dark mode: table header */
|
| 141 |
+
[data-theme="dark"] th { background: var(--bg-tertiary); }
|
| 142 |
+
|
| 143 |
+
/* Dark mode: scrollbar */
|
| 144 |
+
[data-theme="dark"] ::-webkit-scrollbar-track { background: var(--bg-secondary); }
|
| 145 |
+
[data-theme="dark"] ::-webkit-scrollbar-thumb { background: #2a2d32; }
|
| 146 |
+
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
| 147 |
+
|
| 148 |
+
/* Dark mode: selection */
|
| 149 |
+
[data-theme="dark"] ::selection { background: rgba(0, 168, 122, 0.25); color: var(--text-primary); }
|
| 150 |
+
|
| 151 |
+
/* Dark mode: React Flow (Strategy Builder) */
|
| 152 |
+
[data-theme="dark"] .react-flow__controls { background: var(--bg-tertiary); border-color: var(--border-color); border-radius: var(--radius-md); }
|
| 153 |
+
[data-theme="dark"] .react-flow__controls-button { background: var(--bg-card); border-color: var(--border-color); fill: var(--text-secondary); }
|
| 154 |
+
[data-theme="dark"] .react-flow__controls-button:hover { background: var(--bg-hover); }
|
| 155 |
+
[data-theme="dark"] .react-flow__minimap { background: var(--bg-tertiary) !important; }
|
| 156 |
+
[data-theme="dark"] .react-flow__attribution { background: transparent !important; }
|
| 157 |
+
[data-theme="dark"] .react-flow__attribution a { color: var(--text-muted) !important; }
|
| 158 |
+
|
| 159 |
/* ── Reset ───────────────────────────────────────────────────────────── */
|
| 160 |
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
| 161 |
|
|
|
|
| 558 |
}
|
| 559 |
.badge-primary { background: var(--accent-lighter); color: var(--accent); }
|
| 560 |
.badge-emerald { background: #e6f9f0; color: var(--green-positive); }
|
| 561 |
+
.badge-rose { background: var(--error-bg); color: var(--red-negative); }
|
| 562 |
.badge-amber { background: #fffbeb; color: var(--amber-neutral); }
|
| 563 |
|
| 564 |
/* ── Tabs ────────────────────────────────────────────────────────────── */
|
frontend/src/pages/BacktestResults.tsx
CHANGED
|
@@ -66,11 +66,11 @@ export default function BacktestResults() {
|
|
| 66 |
<div className="card-header"><h3>Equity Curve</h3></div>
|
| 67 |
<div className="chart-container" style={{height:260,padding:'0.5rem'}}>
|
| 68 |
<ResponsiveContainer><AreaChart data={selectedBt.equity_curve||[]}>
|
| 69 |
-
<defs><linearGradient id="eqG" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="
|
| 70 |
-
<XAxis dataKey="date" tick={{fontSize:9,fill:'
|
| 71 |
-
<YAxis tick={{fontSize:9,fill:'
|
| 72 |
-
<Tooltip contentStyle={{background:'
|
| 73 |
-
<Area type="monotone" dataKey="portfolio_value" stroke="
|
| 74 |
</AreaChart></ResponsiveContainer>
|
| 75 |
</div>
|
| 76 |
</div>
|
|
|
|
| 66 |
<div className="card-header"><h3>Equity Curve</h3></div>
|
| 67 |
<div className="chart-container" style={{height:260,padding:'0.5rem'}}>
|
| 68 |
<ResponsiveContainer><AreaChart data={selectedBt.equity_curve||[]}>
|
| 69 |
+
<defs><linearGradient id="eqG" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="var(--chart-green)" stopOpacity={0.18}/><stop offset="95%" stopColor="var(--chart-green)" stopOpacity={0}/></linearGradient></defs>
|
| 70 |
+
<XAxis dataKey="date" tick={{fontSize:9,fill:'var(--chart-axis)'}} tickFormatter={v=>v?.slice(5)}/>
|
| 71 |
+
<YAxis tick={{fontSize:9,fill:'var(--chart-axis)'}} tickFormatter={v=>`$${(v/1e6).toFixed(1)}M`}/>
|
| 72 |
+
<Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}}/>
|
| 73 |
+
<Area type="monotone" dataKey="portfolio_value" stroke="var(--chart-green)" fill="url(#eqG)" strokeWidth={2} dot={false}/>
|
| 74 |
</AreaChart></ResponsiveContainer>
|
| 75 |
</div>
|
| 76 |
</div>
|
frontend/src/pages/Dashboard.tsx
CHANGED
|
@@ -211,18 +211,18 @@ export default function Dashboard() {
|
|
| 211 |
<AreaChart data={marketData}>
|
| 212 |
<defs>
|
| 213 |
<linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1">
|
| 214 |
-
<stop offset="5%" stopColor="
|
| 215 |
-
<stop offset="95%" stopColor="
|
| 216 |
</linearGradient>
|
| 217 |
</defs>
|
| 218 |
-
<CartesianGrid strokeDasharray="3 3" stroke="
|
| 219 |
-
<XAxis dataKey="date" tick={{fontSize:10,fill:'
|
| 220 |
-
<YAxis tick={{fontSize:10,fill:'
|
| 221 |
<Tooltip
|
| 222 |
contentStyle={{background:'var(--bg-card)',border:'1px solid var(--border-color)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}}
|
| 223 |
formatter={(v: any) => [Number(v).toLocaleString('en-US', {minimumFractionDigits:2}), 'Price']}
|
| 224 |
/>
|
| 225 |
-
<Area type="monotone" dataKey="price" stroke="
|
| 226 |
</AreaChart>
|
| 227 |
</ResponsiveContainer>
|
| 228 |
</div>
|
|
|
|
| 211 |
<AreaChart data={marketData}>
|
| 212 |
<defs>
|
| 213 |
<linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1">
|
| 214 |
+
<stop offset="5%" stopColor="var(--chart-green)" stopOpacity={0.15} />
|
| 215 |
+
<stop offset="95%" stopColor="var(--chart-green)" stopOpacity={0} />
|
| 216 |
</linearGradient>
|
| 217 |
</defs>
|
| 218 |
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
| 219 |
+
<XAxis dataKey="date" tick={{fontSize:10,fill:'var(--chart-axis)'}} tickFormatter={v => v?.slice(5)} />
|
| 220 |
+
<YAxis tick={{fontSize:10,fill:'var(--chart-axis)'}} domain={['auto','auto']} tickFormatter={v => v?.toLocaleString()} />
|
| 221 |
<Tooltip
|
| 222 |
contentStyle={{background:'var(--bg-card)',border:'1px solid var(--border-color)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}}
|
| 223 |
formatter={(v: any) => [Number(v).toLocaleString('en-US', {minimumFractionDigits:2}), 'Price']}
|
| 224 |
/>
|
| 225 |
+
<Area type="monotone" dataKey="price" stroke="var(--chart-green)" fill="url(#colorPrice)" strokeWidth={2} dot={false} />
|
| 226 |
</AreaChart>
|
| 227 |
</ResponsiveContainer>
|
| 228 |
</div>
|
frontend/src/pages/FactorAnalysis.tsx
CHANGED
|
@@ -137,13 +137,13 @@ export default function FactorAnalysis() {
|
|
| 137 |
<div style={{height:300}}>
|
| 138 |
<ResponsiveContainer>
|
| 139 |
<RadarChart data={getRadarData()}>
|
| 140 |
-
<PolarGrid stroke="
|
| 141 |
-
<PolarAngleAxis dataKey="factor" tick={{fontSize:11,fill:'
|
| 142 |
-
<PolarRadiusAxis tick={{fontSize:9,fill:'
|
| 143 |
{results.tickers?.slice(0, 4).map((t: string, i: number) => (
|
| 144 |
<Radar key={t} name={t} dataKey={t} stroke={['#6366f1','#10b981','#f59e0b','#f43f5e'][i]} fill={['#6366f1','#10b981','#f59e0b','#f43f5e'][i]} fillOpacity={0.1} strokeWidth={2} />
|
| 145 |
))}
|
| 146 |
-
<Tooltip contentStyle={{background:'
|
| 147 |
</RadarChart>
|
| 148 |
</ResponsiveContainer>
|
| 149 |
</div>
|
|
@@ -256,10 +256,10 @@ export default function FactorAnalysis() {
|
|
| 256 |
<div style={{height:220}}>
|
| 257 |
<ResponsiveContainer>
|
| 258 |
<BarChart data={prediction.top_features} layout="vertical" margin={{left:100,right:20,top:5,bottom:5}}>
|
| 259 |
-
<CartesianGrid strokeDasharray="3 3" stroke="
|
| 260 |
-
<XAxis type="number" tick={{fontSize:10,fill:'
|
| 261 |
-
<YAxis type="category" dataKey="name" tick={{fontSize:10,fill:'
|
| 262 |
-
<Tooltip contentStyle={{background:'
|
| 263 |
<Bar dataKey="importance" radius={[0,4,4,0]}>
|
| 264 |
{prediction.top_features?.map((_: any, i: number) => (
|
| 265 |
<Cell key={i} fill={['#6366f1','#818cf8','#a5b4fc','#c7d2fe','#e0e7ff','#eef2ff','#f0f0ff','#f5f5ff','#fafafe','#fdfdfd'][i] || '#c7d2fe'} />
|
|
@@ -304,7 +304,7 @@ export default function FactorAnalysis() {
|
|
| 304 |
{regime.regime_probabilities && Object.entries(regime.regime_probabilities).map(([label, prob]: [string, any]) => (
|
| 305 |
<div key={label} style={{display:'flex',alignItems:'center',gap:'0.75rem'}}>
|
| 306 |
<span style={{width:120,fontSize:'0.8rem',fontWeight:500,display:'flex',alignItems:'center',gap:'0.35rem'}}><RegimeIcon regime={label} size={12}/> {label.replace('_',' ')}</span>
|
| 307 |
-
<div style={{flex:1,height:20,borderRadius:10,background:'
|
| 308 |
<div style={{width:`${prob*100}%`,height:'100%',borderRadius:10,background:regimeColor(label),transition:'width 0.5s ease'}} />
|
| 309 |
</div>
|
| 310 |
<span style={{fontSize:'0.8rem',fontWeight:600,width:45,textAlign:'right'}}>{(prob*100).toFixed(1)}%</span>
|
|
|
|
| 137 |
<div style={{height:300}}>
|
| 138 |
<ResponsiveContainer>
|
| 139 |
<RadarChart data={getRadarData()}>
|
| 140 |
+
<PolarGrid stroke="var(--chart-grid)" />
|
| 141 |
+
<PolarAngleAxis dataKey="factor" tick={{fontSize:11,fill:'var(--chart-axis)'}} />
|
| 142 |
+
<PolarRadiusAxis tick={{fontSize:9,fill:'var(--chart-axis)'}} />
|
| 143 |
{results.tickers?.slice(0, 4).map((t: string, i: number) => (
|
| 144 |
<Radar key={t} name={t} dataKey={t} stroke={['#6366f1','#10b981','#f59e0b','#f43f5e'][i]} fill={['#6366f1','#10b981','#f59e0b','#f43f5e'][i]} fillOpacity={0.1} strokeWidth={2} />
|
| 145 |
))}
|
| 146 |
+
<Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}} />
|
| 147 |
</RadarChart>
|
| 148 |
</ResponsiveContainer>
|
| 149 |
</div>
|
|
|
|
| 256 |
<div style={{height:220}}>
|
| 257 |
<ResponsiveContainer>
|
| 258 |
<BarChart data={prediction.top_features} layout="vertical" margin={{left:100,right:20,top:5,bottom:5}}>
|
| 259 |
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
| 260 |
+
<XAxis type="number" tick={{fontSize:10,fill:'var(--chart-axis)'}} />
|
| 261 |
+
<YAxis type="category" dataKey="name" tick={{fontSize:10,fill:'var(--chart-axis)'}} width={95} />
|
| 262 |
+
<Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}} />
|
| 263 |
<Bar dataKey="importance" radius={[0,4,4,0]}>
|
| 264 |
{prediction.top_features?.map((_: any, i: number) => (
|
| 265 |
<Cell key={i} fill={['#6366f1','#818cf8','#a5b4fc','#c7d2fe','#e0e7ff','#eef2ff','#f0f0ff','#f5f5ff','#fafafe','#fdfdfd'][i] || '#c7d2fe'} />
|
|
|
|
| 304 |
{regime.regime_probabilities && Object.entries(regime.regime_probabilities).map(([label, prob]: [string, any]) => (
|
| 305 |
<div key={label} style={{display:'flex',alignItems:'center',gap:'0.75rem'}}>
|
| 306 |
<span style={{width:120,fontSize:'0.8rem',fontWeight:500,display:'flex',alignItems:'center',gap:'0.35rem'}}><RegimeIcon regime={label} size={12}/> {label.replace('_',' ')}</span>
|
| 307 |
+
<div style={{flex:1,height:20,borderRadius:10,background:'var(--chart-bar-bg)',overflow:'hidden'}}>
|
| 308 |
<div style={{width:`${prob*100}%`,height:'100%',borderRadius:10,background:regimeColor(label),transition:'width 0.5s ease'}} />
|
| 309 |
</div>
|
| 310 |
<span style={{fontSize:'0.8rem',fontWeight:600,width:45,textAlign:'right'}}>{(prob*100).toFixed(1)}%</span>
|
frontend/src/pages/HoldingsTracker.tsx
CHANGED
|
@@ -305,9 +305,9 @@ export default function HoldingsTracker() {
|
|
| 305 |
<div style={{ width: '100%', height: 280 }}>
|
| 306 |
<ResponsiveContainer>
|
| 307 |
<BarChart data={holdings.map(h => ({ ticker: h.ticker, pnl: h.pnl, pnl_pct: h.pnl_pct }))} layout="vertical">
|
| 308 |
-
|
| 309 |
-
<XAxis type="number" tick={{ fontSize: 10, fill: '
|
| 310 |
-
<YAxis dataKey="ticker" type="category" tick={{ fontSize: 11, fill: '
|
| 311 |
<Tooltip formatter={(v: any) => `$${fmt(Number(v))}`} contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
|
| 312 |
<Bar dataKey="pnl" radius={[0, 4, 4, 0]}>
|
| 313 |
{holdings.map((h, i) => <Cell key={i} fill={h.pnl >= 0 ? '#0a8f5c' : '#c23030'} />)}
|
|
@@ -338,9 +338,9 @@ export default function HoldingsTracker() {
|
|
| 338 |
<div style={{ width: '100%', height: 350 }}>
|
| 339 |
<ResponsiveContainer>
|
| 340 |
<BarChart data={allScenarios} layout="vertical">
|
| 341 |
-
<CartesianGrid strokeDasharray="3 3" stroke="
|
| 342 |
-
<XAxis type="number" tick={{ fontSize: 10, fill:'
|
| 343 |
-
<YAxis dataKey="name" type="category" tick={{ fontSize: 10, fill:'
|
| 344 |
<Tooltip formatter={(v: any) => `${Number(v).toFixed(2)}%`} contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
|
| 345 |
<Bar dataKey="total_impact_pct" radius={[0, 4, 4, 0]}>
|
| 346 |
{allScenarios.map((s, i) => <Cell key={i} fill={s.total_impact_pct >= 0 ? '#0a8f5c' : '#c23030'} />)}
|
|
|
|
| 305 |
<div style={{ width: '100%', height: 280 }}>
|
| 306 |
<ResponsiveContainer>
|
| 307 |
<BarChart data={holdings.map(h => ({ ticker: h.ticker, pnl: h.pnl, pnl_pct: h.pnl_pct }))} layout="vertical">
|
| 308 |
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
| 309 |
+
<XAxis type="number" tick={{ fontSize: 10, fill: 'var(--chart-axis)' }} tickFormatter={v => `$${Number(v).toLocaleString()}`} />
|
| 310 |
+
<YAxis dataKey="ticker" type="category" tick={{ fontSize: 11, fill: 'var(--chart-axis)', fontWeight: 500 }} width={60} />
|
| 311 |
<Tooltip formatter={(v: any) => `$${fmt(Number(v))}`} contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
|
| 312 |
<Bar dataKey="pnl" radius={[0, 4, 4, 0]}>
|
| 313 |
{holdings.map((h, i) => <Cell key={i} fill={h.pnl >= 0 ? '#0a8f5c' : '#c23030'} />)}
|
|
|
|
| 338 |
<div style={{ width: '100%', height: 350 }}>
|
| 339 |
<ResponsiveContainer>
|
| 340 |
<BarChart data={allScenarios} layout="vertical">
|
| 341 |
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
| 342 |
+
<XAxis type="number" tick={{ fontSize: 10, fill:'var(--chart-axis)' }} tickFormatter={v => `${v.toFixed(0)}%`} />
|
| 343 |
+
<YAxis dataKey="name" type="category" tick={{ fontSize: 10, fill:'var(--chart-axis)' }} width={140} />
|
| 344 |
<Tooltip formatter={(v: any) => `${Number(v).toFixed(2)}%`} contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
|
| 345 |
<Bar dataKey="total_impact_pct" radius={[0, 4, 4, 0]}>
|
| 346 |
{allScenarios.map((s, i) => <Cell key={i} fill={s.total_impact_pct >= 0 ? '#0a8f5c' : '#c23030'} />)}
|
frontend/src/pages/Login.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export default function Login() {
|
|
| 42 |
</div>
|
| 43 |
|
| 44 |
{error && (
|
| 45 |
-
<div style={{padding:'0.75rem 1rem',background:'
|
| 46 |
{error}
|
| 47 |
</div>
|
| 48 |
)}
|
|
|
|
| 42 |
</div>
|
| 43 |
|
| 44 |
{error && (
|
| 45 |
+
<div style={{padding:'0.75rem 1rem',background:'var(--error-bg)',border:'1px solid var(--error-border)',borderRadius:'var(--radius-md)',color:'var(--red-negative)',fontSize:'0.85rem',marginBottom:'1.25rem'}}>
|
| 46 |
{error}
|
| 47 |
</div>
|
| 48 |
)}
|
frontend/src/pages/MarketExplorer.tsx
CHANGED
|
@@ -96,14 +96,14 @@ export default function MarketExplorer() {
|
|
| 96 |
<AreaChart data={priceData}>
|
| 97 |
<defs>
|
| 98 |
<linearGradient id="colorClose" x1="0" y1="0" x2="0" y2="1">
|
| 99 |
-
<stop offset="5%" stopColor="
|
| 100 |
-
<stop offset="95%" stopColor="
|
| 101 |
</linearGradient>
|
| 102 |
</defs>
|
| 103 |
-
<XAxis dataKey="date" tick={{fontSize:10,fill:'
|
| 104 |
-
<YAxis tick={{fontSize:10,fill:'
|
| 105 |
-
<Tooltip contentStyle={{background:'
|
| 106 |
-
<Area type="monotone" dataKey="close" stroke="
|
| 107 |
</AreaChart>
|
| 108 |
</ResponsiveContainer>
|
| 109 |
</div>
|
|
|
|
| 96 |
<AreaChart data={priceData}>
|
| 97 |
<defs>
|
| 98 |
<linearGradient id="colorClose" x1="0" y1="0" x2="0" y2="1">
|
| 99 |
+
<stop offset="5%" stopColor="var(--chart-green)" stopOpacity={0.18} />
|
| 100 |
+
<stop offset="95%" stopColor="var(--chart-green)" stopOpacity={0} />
|
| 101 |
</linearGradient>
|
| 102 |
</defs>
|
| 103 |
+
<XAxis dataKey="date" tick={{fontSize:10,fill:'var(--chart-axis)'}} tickFormatter={v => v?.slice(5)} />
|
| 104 |
+
<YAxis tick={{fontSize:10,fill:'var(--chart-axis)'}} domain={['auto','auto']} />
|
| 105 |
+
<Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}} />
|
| 106 |
+
<Area type="monotone" dataKey="close" stroke="var(--chart-green)" fill="url(#colorClose)" strokeWidth={2} dot={false} />
|
| 107 |
</AreaChart>
|
| 108 |
</ResponsiveContainer>
|
| 109 |
</div>
|
frontend/src/pages/PortfolioAnalysis.tsx
CHANGED
|
@@ -154,7 +154,7 @@ export default function PortfolioAnalysis() {
|
|
| 154 |
strokeWidth={2} stroke="#fff" label={({name, value}) => `${name}: ${value}%`}>
|
| 155 |
{pieData.map((_: any, i: number) => <Cell key={i} fill={COLORS[i % COLORS.length]} />)}
|
| 156 |
</Pie>
|
| 157 |
-
<Tooltip contentStyle={{background:'
|
| 158 |
</PieChart>
|
| 159 |
</ResponsiveContainer>
|
| 160 |
</div>
|
|
|
|
| 154 |
strokeWidth={2} stroke="#fff" label={({name, value}) => `${name}: ${value}%`}>
|
| 155 |
{pieData.map((_: any, i: number) => <Cell key={i} fill={COLORS[i % COLORS.length]} />)}
|
| 156 |
</Pie>
|
| 157 |
+
<Tooltip contentStyle={{background:'var(--chart-tooltip-bg)',border:'1px solid var(--chart-tooltip-border)',borderRadius:8,fontSize:'0.8rem',boxShadow:'var(--shadow-md)'}} />
|
| 158 |
</PieChart>
|
| 159 |
</ResponsiveContainer>
|
| 160 |
</div>
|
frontend/src/pages/Register.tsx
CHANGED
|
@@ -62,7 +62,7 @@ export default function Register() {
|
|
| 62 |
</div>
|
| 63 |
|
| 64 |
{error && (
|
| 65 |
-
<div style={{padding:'0.75rem 1rem',background:'
|
| 66 |
{error}
|
| 67 |
</div>
|
| 68 |
)}
|
|
|
|
| 62 |
</div>
|
| 63 |
|
| 64 |
{error && (
|
| 65 |
+
<div style={{padding:'0.75rem 1rem',background:'var(--error-bg)',border:'1px solid var(--error-border)',borderRadius:'var(--radius-md)',color:'var(--red-negative)',fontSize:'0.85rem',marginBottom:'1.25rem'}}>
|
| 66 |
{error}
|
| 67 |
</div>
|
| 68 |
)}
|
frontend/src/pages/Sentiment.tsx
CHANGED
|
@@ -87,7 +87,7 @@ export default function Sentiment() {
|
|
| 87 |
<div style={{height:130,display:'flex',alignItems:'center',justifyContent:'center'}}>
|
| 88 |
<ResponsiveContainer width="100%" height="100%">
|
| 89 |
<RadialBarChart cx="50%" cy="85%" innerRadius="65%" outerRadius="100%" startAngle={180} endAngle={0} data={gaugeData} barSize={12}>
|
| 90 |
-
<RadialBar background={{ fill: '
|
| 91 |
</RadialBarChart>
|
| 92 |
</ResponsiveContainer>
|
| 93 |
</div>
|
|
@@ -108,9 +108,9 @@ export default function Sentiment() {
|
|
| 108 |
<div style={{ width: '100%', height: 320 }}>
|
| 109 |
<ResponsiveContainer>
|
| 110 |
<BarChart data={marketMood.breakdown}>
|
| 111 |
-
<CartesianGrid strokeDasharray="3 3" stroke="
|
| 112 |
-
<XAxis dataKey="ticker" tick={{ fontSize: 11, fill: '
|
| 113 |
-
<YAxis tick={{ fontSize: 10, fill: '
|
| 114 |
<Tooltip
|
| 115 |
formatter={(v: any) => (Number(v)).toFixed(3)}
|
| 116 |
contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem', boxShadow:'var(--shadow-md)' }}
|
|
@@ -231,7 +231,7 @@ export default function Sentiment() {
|
|
| 231 |
]}
|
| 232 |
cx="50%" cy="50%" innerRadius={50} outerRadius={85} dataKey="value"
|
| 233 |
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
|
| 234 |
-
labelLine={{ stroke: '
|
| 235 |
/>
|
| 236 |
<Tooltip contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
|
| 237 |
</PieChart>
|
|
|
|
| 87 |
<div style={{height:130,display:'flex',alignItems:'center',justifyContent:'center'}}>
|
| 88 |
<ResponsiveContainer width="100%" height="100%">
|
| 89 |
<RadialBarChart cx="50%" cy="85%" innerRadius="65%" outerRadius="100%" startAngle={180} endAngle={0} data={gaugeData} barSize={12}>
|
| 90 |
+
<RadialBar background={{ fill: 'var(--bg-tertiary)' }} dataKey="value" cornerRadius={6} />
|
| 91 |
</RadialBarChart>
|
| 92 |
</ResponsiveContainer>
|
| 93 |
</div>
|
|
|
|
| 108 |
<div style={{ width: '100%', height: 320 }}>
|
| 109 |
<ResponsiveContainer>
|
| 110 |
<BarChart data={marketMood.breakdown}>
|
| 111 |
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
|
| 112 |
+
<XAxis dataKey="ticker" tick={{ fontSize: 11, fill: 'var(--chart-axis)', fontWeight: 500 }} />
|
| 113 |
+
<YAxis tick={{ fontSize: 10, fill: 'var(--chart-axis)' }} domain={[-1, 1]} />
|
| 114 |
<Tooltip
|
| 115 |
formatter={(v: any) => (Number(v)).toFixed(3)}
|
| 116 |
contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem', boxShadow:'var(--shadow-md)' }}
|
|
|
|
| 231 |
]}
|
| 232 |
cx="50%" cy="50%" innerRadius={50} outerRadius={85} dataKey="value"
|
| 233 |
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
|
| 234 |
+
labelLine={{ stroke: 'var(--chart-axis)', strokeWidth: 1 }}
|
| 235 |
/>
|
| 236 |
<Tooltip contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
|
| 237 |
</PieChart>
|
frontend/src/pages/StrategyBuilder.tsx
CHANGED
|
@@ -35,7 +35,7 @@ export default function StrategyBuilder() {
|
|
| 35 |
border: `2px solid ${nodeType.color}`,
|
| 36 |
borderRadius: '12px',
|
| 37 |
padding: '12px 20px',
|
| 38 |
-
color: '
|
| 39 |
fontSize: '13px',
|
| 40 |
fontWeight: '600',
|
| 41 |
minWidth: '160px',
|
|
@@ -104,11 +104,11 @@ export default function StrategyBuilder() {
|
|
| 104 |
nodes={nodes} edges={edges}
|
| 105 |
onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect}
|
| 106 |
fitView
|
| 107 |
-
style={{background:'
|
| 108 |
>
|
| 109 |
-
<Background color="
|
| 110 |
<Controls />
|
| 111 |
-
<MiniMap nodeColor="
|
| 112 |
<Panel position="top-left">
|
| 113 |
<div style={{display:'flex',gap:'0.375rem',flexWrap:'wrap',maxWidth:'400px'}}>
|
| 114 |
{NODE_TYPES.map(nt => (
|
|
@@ -130,6 +130,9 @@ export default function StrategyBuilder() {
|
|
| 130 |
<p style={{fontSize:'0.8rem',color:'var(--text-secondary)',marginBottom:'1rem'}}>
|
| 131 |
Add nodes from the palette, connect them, then generate the config.
|
| 132 |
</p>
|
|
|
|
|
|
|
|
|
|
| 133 |
<button className="btn btn-primary" style={{width:'100%',marginBottom:'0.75rem'}} onClick={convertToConfig} disabled={nodes.length === 0}>
|
| 134 |
Generate Config
|
| 135 |
</button>
|
|
|
|
| 35 |
border: `2px solid ${nodeType.color}`,
|
| 36 |
borderRadius: '12px',
|
| 37 |
padding: '12px 20px',
|
| 38 |
+
color: 'var(--text-primary)',
|
| 39 |
fontSize: '13px',
|
| 40 |
fontWeight: '600',
|
| 41 |
minWidth: '160px',
|
|
|
|
| 104 |
nodes={nodes} edges={edges}
|
| 105 |
onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect}
|
| 106 |
fitView
|
| 107 |
+
style={{background:'var(--bg-secondary)'}}
|
| 108 |
>
|
| 109 |
+
<Background color="var(--border-color)" gap={20} />
|
| 110 |
<Controls />
|
| 111 |
+
<MiniMap nodeColor="var(--accent)" style={{background:'var(--bg-tertiary)'}} />
|
| 112 |
<Panel position="top-left">
|
| 113 |
<div style={{display:'flex',gap:'0.375rem',flexWrap:'wrap',maxWidth:'400px'}}>
|
| 114 |
{NODE_TYPES.map(nt => (
|
|
|
|
| 130 |
<p style={{fontSize:'0.8rem',color:'var(--text-secondary)',marginBottom:'1rem'}}>
|
| 131 |
Add nodes from the palette, connect them, then generate the config.
|
| 132 |
</p>
|
| 133 |
+
<p style={{fontSize:'0.72rem',color:'var(--text-muted)',marginBottom:'1rem',display:'flex',alignItems:'center',gap:'0.35rem'}}>
|
| 134 |
+
<kbd style={{padding:'0.1rem 0.35rem',borderRadius:4,border:'1px solid var(--border-color)',background:'var(--bg-tertiary)',fontSize:'0.65rem',fontFamily:'var(--font-mono)'}}>⌫</kbd> Press Backspace to delete a selected node
|
| 135 |
+
</p>
|
| 136 |
<button className="btn btn-primary" style={{width:'100%',marginBottom:'0.75rem'}} onClick={convertToConfig} disabled={nodes.length === 0}>
|
| 137 |
Generate Config
|
| 138 |
</button>
|