Skip to content

Commit 42031ea

Browse files
committed
side navigation bar and responsive design
1 parent 8296a10 commit 42031ea

7 files changed

Lines changed: 477 additions & 93 deletions

File tree

frontend/src/App.jsx

Lines changed: 258 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88
*/
99

1010
import {
11+
Button,
1112
makeStyles,
13+
Subtitle2,
1214
Subtitle1,
13-
Tab,
14-
TabList,
1515
Text,
1616
tokens,
1717
} from '@fluentui/react-components'
1818
import {
1919
Bot24Regular,
20+
ChevronLeft24Regular,
21+
ChevronRight24Regular,
2022
DataHistogram24Regular,
2123
DocumentEdit24Regular,
2224
Flow24Regular,
@@ -43,31 +45,174 @@ import SettingsPage from './features/settings/SettingsPage'
4345
import useTabPreferences from './features/settings/useTabPreferences'
4446
import { listWorkbenchAgents } from './services/api'
4547

48+
const NAV_COLLAPSED_STORAGE_KEY = 'app-nav-collapsed'
49+
4650
const useStyles = makeStyles({
4751
app: {
4852
minHeight: '100vh',
4953
backgroundColor: tokens.colorNeutralBackground3,
54+
overflowX: 'hidden',
55+
display: 'flex',
56+
flexDirection: 'column',
5057
},
5158
header: {
5259
backgroundColor: tokens.colorBrandBackground,
5360
color: tokens.colorNeutralForegroundOnBrand,
5461
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
5562
boxShadow: tokens.shadow4,
63+
'@media (max-width: 768px)': {
64+
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`,
65+
},
66+
'@media (max-width: 480px)': {
67+
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
68+
},
69+
},
70+
headerInner: {
71+
maxWidth: '1400px',
72+
margin: '0 auto',
5673
},
5774
title: {
5875
color: tokens.colorNeutralForegroundOnBrand,
76+
overflowWrap: 'anywhere',
5977
},
6078
subtitle: {
6179
color: tokens.colorNeutralForegroundOnBrand,
6280
opacity: 0.9,
6381
marginTop: tokens.spacingVerticalXS,
82+
overflowWrap: 'anywhere',
6483
},
65-
nav: {
84+
shell: {
85+
display: 'flex',
86+
flex: 1,
87+
minHeight: 0,
88+
},
89+
sidebar: {
90+
width: '280px',
91+
flexShrink: 0,
92+
display: 'flex',
93+
flexDirection: 'column',
6694
backgroundColor: tokens.colorNeutralBackground1,
95+
borderRight: `1px solid ${tokens.colorNeutralStroke1}`,
96+
transitionDuration: tokens.durationNormal,
97+
transitionProperty: 'width',
98+
transitionTimingFunction: tokens.curveEasyEase,
99+
minHeight: 0,
100+
'@media (max-width: 768px)': {
101+
width: '232px',
102+
},
103+
},
104+
sidebarCollapsed: {
105+
width: '88px',
106+
'@media (max-width: 768px)': {
107+
width: '72px',
108+
},
109+
},
110+
sidebarHeader: {
111+
display: 'flex',
112+
alignItems: 'center',
113+
justifyContent: 'space-between',
114+
gap: tokens.spacingHorizontalS,
115+
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalM}`,
67116
borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
68-
padding: `0 ${tokens.spacingHorizontalXL}`,
117+
},
118+
sidebarTitle: {
119+
minWidth: 0,
120+
overflow: 'hidden',
121+
},
122+
collapseButton: {
123+
minWidth: '36px',
124+
width: '36px',
125+
height: '36px',
126+
padding: 0,
127+
flexShrink: 0,
128+
},
129+
navList: {
130+
display: 'flex',
131+
flexDirection: 'column',
132+
gap: tokens.spacingVerticalXS,
133+
padding: tokens.spacingHorizontalS,
134+
overflowY: 'auto',
135+
flex: 1,
136+
minHeight: 0,
137+
},
138+
navButton: {
139+
width: '100%',
140+
display: 'flex',
141+
alignItems: 'center',
142+
gap: tokens.spacingHorizontalM,
143+
border: 'none',
144+
borderRadius: tokens.borderRadiusLarge,
145+
backgroundColor: 'transparent',
146+
color: tokens.colorNeutralForeground2,
147+
cursor: 'pointer',
148+
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
149+
textAlign: 'left',
150+
transitionDuration: tokens.durationNormal,
151+
transitionProperty: 'background-color, color',
152+
transitionTimingFunction: tokens.curveEasyEase,
153+
':hover': {
154+
backgroundColor: tokens.colorNeutralBackground1Hover,
155+
color: tokens.colorNeutralForeground1,
156+
},
157+
},
158+
navButtonCollapsed: {
159+
justifyContent: 'center',
160+
padding: `${tokens.spacingVerticalS} 0`,
161+
},
162+
navButtonActive: {
163+
backgroundColor: tokens.colorBrandBackground2,
164+
color: tokens.colorBrandForeground1,
165+
boxShadow: `inset 3px 0 0 ${tokens.colorBrandStroke1}`,
166+
':hover': {
167+
backgroundColor: tokens.colorBrandBackground2Hover,
168+
color: tokens.colorBrandForeground1,
169+
},
170+
},
171+
navIcon: {
172+
display: 'flex',
173+
alignItems: 'center',
174+
justifyContent: 'center',
175+
flexShrink: 0,
176+
},
177+
navLabelWrap: {
178+
minWidth: 0,
179+
display: 'flex',
180+
flexDirection: 'column',
181+
gap: '2px',
182+
},
183+
navLabel: {
184+
overflow: 'hidden',
185+
textOverflow: 'ellipsis',
186+
whiteSpace: 'nowrap',
187+
},
188+
navPath: {
189+
color: tokens.colorNeutralForeground4,
190+
overflow: 'hidden',
191+
textOverflow: 'ellipsis',
192+
whiteSpace: 'nowrap',
193+
},
194+
contentArea: {
195+
flex: 1,
196+
minWidth: 0,
197+
minHeight: 0,
198+
display: 'flex',
199+
flexDirection: 'column',
69200
},
70201
content: {
202+
width: '100%',
203+
flex: 1,
204+
minWidth: 0,
205+
minHeight: 0,
206+
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
207+
'@media (max-width: 768px)': {
208+
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`,
209+
},
210+
'@media (max-width: 480px)': {
211+
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
212+
},
213+
},
214+
contentInner: {
215+
width: '100%',
71216
maxWidth: '1400px',
72217
margin: '0 auto',
73218
},
@@ -77,8 +222,24 @@ export default function App() {
77222
const styles = useStyles()
78223
const location = useLocation()
79224
const navigate = useNavigate()
225+
const [isNavCollapsed, setIsNavCollapsed] = useState(false)
80226

81227
const [menuAgents, setMenuAgents] = useState([])
228+
useEffect(() => {
229+
const storedValue = window.localStorage.getItem(NAV_COLLAPSED_STORAGE_KEY)
230+
if (storedValue !== null) {
231+
setIsNavCollapsed(storedValue === 'true')
232+
return
233+
}
234+
if (window.innerWidth <= 1024) {
235+
setIsNavCollapsed(true)
236+
}
237+
}, [])
238+
239+
useEffect(() => {
240+
window.localStorage.setItem(NAV_COLLAPSED_STORAGE_KEY, String(isNavCollapsed))
241+
}, [isNavCollapsed])
242+
82243
useEffect(() => {
83244
listWorkbenchAgents()
84245
.then((data) => {
@@ -136,63 +297,105 @@ export default function App() {
136297
?? allTabs.find((tab) => location.pathname.startsWith(tab.path))?.value
137298
?? 'csvtickets'
138299

300+
const mainNavTabs = navTabs.filter((tab) => tab.value !== 'settings')
301+
302+
const renderNavButton = (tab) => {
303+
const isActive = activeTab === tab.value
304+
return (
305+
<button
306+
key={tab.value}
307+
type="button"
308+
className={`${styles.navButton} ${isNavCollapsed ? styles.navButtonCollapsed : ''} ${isActive ? styles.navButtonActive : ''}`}
309+
onClick={() => navigate(tab.path)}
310+
data-testid={tab.testId}
311+
aria-label={tab.label}
312+
title={isNavCollapsed ? tab.label : undefined}
313+
>
314+
<span className={styles.navIcon}>{tab.icon}</span>
315+
{!isNavCollapsed && (
316+
<span className={styles.navLabelWrap}>
317+
<Text className={styles.navLabel} weight={isActive ? 'semibold' : 'regular'}>
318+
{tab.label}
319+
</Text>
320+
<Text className={styles.navPath} size={200}>
321+
{tab.path}
322+
</Text>
323+
</span>
324+
)}
325+
</button>
326+
)
327+
}
328+
139329
return (
140330
<div className={styles.app}>
141331
<header className={styles.header}>
142-
<Subtitle1 className={styles.title}>CSV Ticket Viewer</Subtitle1>
143-
<Text className={styles.subtitle} size={300}>
144-
View and filter ticket data from CSV exports
145-
</Text>
332+
<div className={styles.headerInner}>
333+
<Subtitle1 className={styles.title}>CSV Ticket Viewer</Subtitle1>
334+
<Text className={styles.subtitle} size={300}>
335+
View and filter ticket data from CSV exports
336+
</Text>
337+
</div>
146338
</header>
147339

148-
<nav className={styles.nav}>
149-
<TabList
150-
selectedValue={activeTab}
151-
onTabSelect={(_, data) => {
152-
const selected = navTabs.find((tab) => tab.value === data.value)
153-
if (selected) {
154-
navigate(selected.path)
155-
}
156-
}}
157-
size="large"
158-
>
159-
{navTabs.map((tab) => (
160-
<Tab key={tab.value} value={tab.value} icon={tab.icon} data-testid={tab.testId}>
161-
{tab.label}
162-
</Tab>
163-
))}
164-
</TabList>
165-
</nav>
166-
167-
<main className={styles.content}>
168-
<Routes>
169-
<Route path="/" element={<Navigate to="/csvtickets" replace />} />
170-
<Route path="/kba-drafter" element={<KBADrafterPage />} />
171-
<Route path="/csvtickets" element={<CSVTicketTable />} />
172-
{USECASE_DEMO_DEFINITIONS.map((definition) => (
173-
<Route
174-
key={definition.id}
175-
path={definition.route}
176-
element={<UsecaseDemoPage definition={definition} />}
177-
/>
178-
))}
179-
<Route path="/kitchensink" element={<KitchenSink />} />
180-
<Route path="/fields" element={<FieldsDocs />} />
181-
<Route path="/workbench" element={<WorkbenchPage />} />
182-
{menuAgents.map((agent) => (
183-
<Route
184-
key={agent.id}
185-
path={`/agent-run/${agent.id}`}
186-
element={<AgentRunPage agent={agent} />}
340+
<div className={styles.shell}>
341+
<aside className={`${styles.sidebar} ${isNavCollapsed ? styles.sidebarCollapsed : ''}`}>
342+
<div className={styles.sidebarHeader}>
343+
{!isNavCollapsed && (
344+
<div className={styles.sidebarTitle}>
345+
<Subtitle2>Navigation</Subtitle2>
346+
<Text size={200}>Main sections</Text>
347+
</div>
348+
)}
349+
<Button
350+
className={styles.collapseButton}
351+
appearance="subtle"
352+
icon={isNavCollapsed ? <ChevronRight24Regular /> : <ChevronLeft24Regular />}
353+
onClick={() => setIsNavCollapsed((current) => !current)}
354+
aria-label={isNavCollapsed ? 'Expand navigation' : 'Collapse navigation'}
355+
title={isNavCollapsed ? 'Expand navigation' : 'Collapse navigation'}
187356
/>
188-
))}
189-
<Route path="/agent" element={<AgentChat />} />
190-
<Route path="/activity" element={<ActivityPage />} />
191-
<Route path="/workflow" element={<WorkflowPage />} />
192-
<Route path="/settings" element={<SettingsPage tabPrefs={tabPrefs} />} />
193-
<Route path="*" element={<Navigate to="/csvtickets" replace />} />
194-
</Routes>
195-
</main>
357+
</div>
358+
359+
<nav className={styles.navList} aria-label="Main navigation">
360+
{mainNavTabs.map(renderNavButton)}
361+
{renderNavButton(settingsTab)}
362+
</nav>
363+
</aside>
364+
365+
<div className={styles.contentArea}>
366+
<main className={styles.content}>
367+
<div className={styles.contentInner}>
368+
<Routes>
369+
<Route path="/" element={<Navigate to="/csvtickets" replace />} />
370+
<Route path="/kba-drafter" element={<KBADrafterPage />} />
371+
<Route path="/csvtickets" element={<CSVTicketTable />} />
372+
{USECASE_DEMO_DEFINITIONS.map((definition) => (
373+
<Route
374+
key={definition.id}
375+
path={definition.route}
376+
element={<UsecaseDemoPage definition={definition} />}
377+
/>
378+
))}
379+
<Route path="/kitchensink" element={<KitchenSink />} />
380+
<Route path="/fields" element={<FieldsDocs />} />
381+
<Route path="/workbench" element={<WorkbenchPage />} />
382+
{menuAgents.map((agent) => (
383+
<Route
384+
key={agent.id}
385+
path={`/agent-run/${agent.id}`}
386+
element={<AgentRunPage agent={agent} />}
387+
/>
388+
))}
389+
<Route path="/agent" element={<AgentChat />} />
390+
<Route path="/activity" element={<ActivityPage />} />
391+
<Route path="/workflow" element={<WorkflowPage />} />
392+
<Route path="/settings" element={<SettingsPage tabPrefs={tabPrefs} />} />
393+
<Route path="*" element={<Navigate to="/csvtickets" replace />} />
394+
</Routes>
395+
</div>
396+
</main>
397+
</div>
398+
</div>
196399
</div>
197400
)
198401
}

0 commit comments

Comments
 (0)