@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
44import { Input } from '@/components/ui/input' ;
55import { Badge } from '@/components/ui/badge' ;
66import { ListChangedIndicator } from '@/components/ListChangedIndicator' ;
7+ import { ChevronDown , ChevronRight } from 'lucide-react' ;
78
89// Resource interface with annotations per MCP spec
910interface Resource {
@@ -59,13 +60,57 @@ function getPriorityLabel(priority: number): { label: string; variant: 'default'
5960 return { label : 'low' , variant : 'default' } ;
6061}
6162
63+ // Collapsible section component for accordion pattern
64+ function AccordionSection ( {
65+ title,
66+ count,
67+ isOpen,
68+ onToggle,
69+ children,
70+ } : {
71+ title : string ;
72+ count : number ;
73+ isOpen : boolean ;
74+ onToggle : ( ) => void ;
75+ children : React . ReactNode ;
76+ } ) {
77+ return (
78+ < div className = "border rounded-md" >
79+ < button
80+ className = "w-full flex items-center gap-2 p-2 text-sm font-medium hover:bg-muted/50 transition-colors"
81+ onClick = { onToggle }
82+ >
83+ { isOpen ? (
84+ < ChevronDown className = "h-4 w-4" />
85+ ) : (
86+ < ChevronRight className = "h-4 w-4" />
87+ ) }
88+ < span > { title } </ span >
89+ < span className = "text-muted-foreground" > ({ count } )</ span >
90+ </ button >
91+ { isOpen && < div className = "p-2 pt-0 border-t" > { children } </ div > }
92+ </ div >
93+ ) ;
94+ }
95+
6296export function Resources ( ) {
6397 const [ hasResourcesChanged , setHasResourcesChanged ] = useState ( true ) ;
6498 const [ selectedResource , setSelectedResource ] = useState < Resource > ( mockResources [ 0 ] ) ;
6599 const [ searchFilter , setSearchFilter ] = useState ( '' ) ;
66100 const [ templateInputs , setTemplateInputs ] = useState < Record < string , string > > ( { } ) ;
67101 const [ subscriptions , setSubscriptions ] = useState < Subscription [ ] > ( mockSubscriptions ) ;
68102
103+ // Accordion state - Resources expanded by default, others collapsed
104+ const [ expandedSections , setExpandedSections ] = useState ( {
105+ resources : true ,
106+ templates : false ,
107+ subscriptions : false ,
108+ } ) ;
109+
110+ const toggleSection = ( section : keyof typeof expandedSections ) => {
111+ setExpandedSections ( ( prev ) => ( { ...prev , [ section ] : ! prev [ section ] } ) ) ;
112+ } ;
113+
69114 const handleRefresh = ( ) => {
70115 setHasResourcesChanged ( false ) ;
71116 } ;
@@ -125,86 +170,99 @@ export function Resources() {
125170 onChange = { ( e ) => setSearchFilter ( e . target . value ) }
126171 />
127172
128- < div className = "space-y-3" >
173+ { /* Accordion Sections */ }
174+ < div className = "space-y-2" >
129175 { /* Resources Section */ }
130- < p className = "text-xs font-semibold text-muted-foreground uppercase" >
131- Resources
132- </ p >
133- < div className = "space-y-1" >
134- { filteredResources . map ( ( resource ) => (
135- < div key = { resource . uri } className = "space-y-1" >
136- < Button
137- variant = { selectedResource . uri === resource . uri ? 'default' : 'ghost' }
138- className = "w-full justify-start text-sm"
139- size = "sm"
140- onClick = { ( ) => setSelectedResource ( resource ) }
141- >
142- { resource . uri . split ( '/' ) . pop ( ) }
143- </ Button >
144- { /* Annotation badges */ }
145- { resource . annotations && Object . keys ( resource . annotations ) . length > 0 && (
146- < div className = "flex flex-wrap gap-1 pl-3 pb-1" >
147- { resource . annotations . audience && (
148- < Badge variant = "secondary" className = "text-xs" >
149- { resource . annotations . audience }
150- </ Badge >
151- ) }
152- { resource . annotations . priority !== undefined && (
153- < Badge
154- variant = { getPriorityLabel ( resource . annotations . priority ) . variant }
155- className = "text-xs"
156- >
157- priority: { getPriorityLabel ( resource . annotations . priority ) . label }
158- </ Badge >
159- ) }
160- </ div >
161- ) }
162- </ div >
163- ) ) }
164- </ div >
176+ < AccordionSection
177+ title = "Resources"
178+ count = { filteredResources . length }
179+ isOpen = { expandedSections . resources }
180+ onToggle = { ( ) => toggleSection ( 'resources' ) }
181+ >
182+ < div className = "space-y-1 pt-2" >
183+ { filteredResources . map ( ( resource ) => (
184+ < div key = { resource . uri } className = "space-y-1" >
185+ < Button
186+ variant = { selectedResource . uri === resource . uri ? 'default' : 'ghost' }
187+ className = "w-full justify-start text-sm"
188+ size = "sm"
189+ onClick = { ( ) => setSelectedResource ( resource ) }
190+ >
191+ { resource . uri . split ( '/' ) . pop ( ) }
192+ </ Button >
193+ { /* Annotation badges */ }
194+ { resource . annotations && Object . keys ( resource . annotations ) . length > 0 && (
195+ < div className = "flex flex-wrap gap-1 pl-3 pb-1" >
196+ { resource . annotations . audience && (
197+ < Badge variant = "secondary" className = "text-xs" >
198+ { resource . annotations . audience }
199+ </ Badge >
200+ ) }
201+ { resource . annotations . priority !== undefined && (
202+ < Badge
203+ variant = { getPriorityLabel ( resource . annotations . priority ) . variant }
204+ className = "text-xs"
205+ >
206+ priority: { getPriorityLabel ( resource . annotations . priority ) . label }
207+ </ Badge >
208+ ) }
209+ </ div >
210+ ) }
211+ </ div >
212+ ) ) }
213+ </ div >
214+ </ AccordionSection >
165215
166216 { /* Templates Section */ }
167- < p className = "text-xs font-semibold text-muted-foreground uppercase pt-2" >
168- Templates
169- </ p >
170- < div className = "space-y-2" >
171- { mockTemplates . map ( ( template ) => {
172- const varMatch = template . uriTemplate . match ( / \{ ( \w + ) \} / ) ;
173- const varName = varMatch ? varMatch [ 1 ] : '' ;
174- return (
175- < div key = { template . uriTemplate } className = "space-y-1" >
176- < p className = "text-sm text-muted-foreground" > { template . uriTemplate } </ p >
177- < div className = "flex gap-1" >
178- < Input
179- placeholder = { varName }
180- className = "h-7 text-xs"
181- value = { templateInputs [ template . uriTemplate ] || '' }
182- onChange = { ( e ) =>
183- handleTemplateInputChange ( template . uriTemplate , e . target . value )
184- }
185- />
186- < Button
187- size = "sm"
188- variant = "outline"
189- className = "h-7 px-2"
190- onClick = { ( ) => handleTemplateGo ( template ) }
191- >
192- Go
193- </ Button >
217+ < AccordionSection
218+ title = "Templates"
219+ count = { mockTemplates . length }
220+ isOpen = { expandedSections . templates }
221+ onToggle = { ( ) => toggleSection ( 'templates' ) }
222+ >
223+ < div className = "space-y-2 pt-2" >
224+ { mockTemplates . map ( ( template ) => {
225+ const varMatch = template . uriTemplate . match ( / \{ ( \w + ) \} / ) ;
226+ const varName = varMatch ? varMatch [ 1 ] : '' ;
227+ return (
228+ < div key = { template . uriTemplate } className = "space-y-1" >
229+ < p className = "text-sm text-muted-foreground" > { template . uriTemplate } </ p >
230+ < div className = "flex gap-1" >
231+ < Input
232+ placeholder = { varName }
233+ className = "h-7 text-xs"
234+ value = { templateInputs [ template . uriTemplate ] || '' }
235+ onChange = { ( e ) =>
236+ handleTemplateInputChange ( template . uriTemplate , e . target . value )
237+ }
238+ />
239+ < Button
240+ size = "sm"
241+ variant = "outline"
242+ className = "h-7 px-2"
243+ onClick = { ( ) => handleTemplateGo ( template ) }
244+ >
245+ Go
246+ </ Button >
247+ </ div >
194248 </ div >
195- </ div >
196- ) ;
197- } ) }
198- </ div >
249+ ) ;
250+ } ) }
251+ </ div >
252+ </ AccordionSection >
199253
200254 { /* Subscriptions Section */ }
201- { subscriptions . length > 0 && (
202- < >
203- < p className = "text-xs font-semibold text-muted-foreground uppercase pt-2" >
204- Subscriptions
205- </ p >
206- < div className = "space-y-1" >
207- { subscriptions . map ( ( sub ) => (
255+ < AccordionSection
256+ title = "Subscriptions"
257+ count = { subscriptions . length }
258+ isOpen = { expandedSections . subscriptions }
259+ onToggle = { ( ) => toggleSection ( 'subscriptions' ) }
260+ >
261+ < div className = "space-y-1 pt-2" >
262+ { subscriptions . length === 0 ? (
263+ < p className = "text-sm text-muted-foreground" > No active subscriptions</ p >
264+ ) : (
265+ subscriptions . map ( ( sub ) => (
208266 < div
209267 key = { sub . uri }
210268 className = "flex items-center justify-between text-sm py-1"
@@ -222,10 +280,10 @@ export function Resources() {
222280 Unsub
223281 </ Button >
224282 </ div >
225- ) ) }
226- </ div >
227- </ >
228- ) }
283+ ) )
284+ ) }
285+ </ div >
286+ </ AccordionSection >
229287 </ div >
230288 </ CardContent >
231289 </ Card >
0 commit comments