88 */
99
1010import {
11+ Button ,
1112 makeStyles ,
13+ Subtitle2 ,
1214 Subtitle1 ,
13- Tab ,
14- TabList ,
1515 Text ,
1616 tokens ,
1717} from '@fluentui/react-components'
1818import {
1919 Bot24Regular ,
20+ ChevronLeft24Regular ,
21+ ChevronRight24Regular ,
2022 DataHistogram24Regular ,
2123 DocumentEdit24Regular ,
2224 Flow24Regular ,
@@ -43,31 +45,174 @@ import SettingsPage from './features/settings/SettingsPage'
4345import useTabPreferences from './features/settings/useTabPreferences'
4446import { listWorkbenchAgents } from './services/api'
4547
48+ const NAV_COLLAPSED_STORAGE_KEY = 'app-nav-collapsed'
49+
4650const 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