Skip to content

Commit 84e09ee

Browse files
committed
feat: responsive tab bar with SVG icons on mobile
1 parent ce6a1ca commit 84e09ee

1 file changed

Lines changed: 41 additions & 4 deletions

File tree

src/cli/dashboard/src/components/layout/TabBar.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState, useEffect } from 'react';
12
import type { ScenarioClientPayload } from '../../hooks/useScenario';
23

34
type Tab = 'sim' | 'viz' | 'settings' | 'reports' | 'chat' | 'log' | 'about';
@@ -8,6 +9,27 @@ interface TabBarProps {
89
scenario: ScenarioClientPayload;
910
}
1011

12+
function TabIcon({ id, size = 16 }: { id: Tab; size?: number }) {
13+
const s = size;
14+
const props = { width: s, height: s, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };
15+
switch (id) {
16+
case 'sim':
17+
return <svg {...props}><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" /></svg>;
18+
case 'viz':
19+
return <svg {...props}><circle cx="8" cy="8" r="3" /><circle cx="16" cy="16" r="3" /><circle cx="18" cy="8" r="2" /><circle cx="6" cy="16" r="2" /><line x1="10.5" y1="9.5" x2="14" y2="14" /></svg>;
20+
case 'settings':
21+
return <svg {...props}><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg>;
22+
case 'reports':
23+
return <svg {...props}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /></svg>;
24+
case 'chat':
25+
return <svg {...props}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg>;
26+
case 'log':
27+
return <svg {...props}><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></svg>;
28+
case 'about':
29+
return <svg {...props}><circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" /></svg>;
30+
}
31+
}
32+
1133
const TABS: Array<{ id: Tab; label: string }> = [
1234
{ id: 'sim', label: 'SIM' },
1335
{ id: 'viz', label: 'VIZ' },
@@ -18,8 +40,17 @@ const TABS: Array<{ id: Tab; label: string }> = [
1840
{ id: 'about', label: 'ABOUT' },
1941
];
2042

43+
const MOBILE_BREAKPOINT = 640;
44+
2145
export function TabBar({ active, onTabChange, scenario }: TabBarProps) {
2246
const tabs = TABS.filter(t => t.id !== 'chat' || scenario.policies.characterChat);
47+
const [compact, setCompact] = useState(window.innerWidth < MOBILE_BREAKPOINT);
48+
49+
useEffect(() => {
50+
const onResize = () => setCompact(window.innerWidth < MOBILE_BREAKPOINT);
51+
window.addEventListener('resize', onResize);
52+
return () => window.removeEventListener('resize', onResize);
53+
}, []);
2354

2455
return (
2556
<nav
@@ -35,24 +66,30 @@ export function TabBar({ active, onTabChange, scenario }: TabBarProps) {
3566
role="tab"
3667
aria-selected={active === tab.id}
3768
aria-controls={`panel-${tab.id}`}
69+
aria-label={tab.label}
3870
id={`tab-${tab.id}`}
3971
className="cursor-pointer transition-colors"
4072
style={{
41-
padding: '8px 0',
73+
padding: compact ? '8px 0' : '8px 0',
4274
flex: 1,
4375
fontFamily: 'var(--sans)',
44-
fontSize: '12px',
76+
fontSize: compact ? '10px' : '12px',
4577
fontWeight: 700,
46-
letterSpacing: '0.5px',
78+
letterSpacing: compact ? '0' : '0.5px',
4779
textTransform: 'uppercase' as const,
4880
color: active === tab.id ? 'var(--amber)' : 'var(--text-3)',
4981
background: active === tab.id ? 'var(--bg-card)' : 'transparent',
5082
border: 'none',
5183
borderBottom: active === tab.id ? '2px solid var(--amber)' : '2px solid transparent',
5284
marginBottom: '-1px',
85+
display: 'flex',
86+
flexDirection: 'column',
87+
alignItems: 'center',
88+
gap: compact ? '2px' : '0',
5389
}}
5490
>
55-
{tab.label}
91+
{compact && <TabIcon id={tab.id} size={16} />}
92+
{!compact && tab.label}
5693
</button>
5794
))}
5895
</nav>

0 commit comments

Comments
 (0)