11"use client" ;
22
3- import { Box , Grid3X3 , Rocket , Search , Zap } from "lucide-react" ;
3+ import { Box , ChevronRight , Rocket , Search , Zap } from "lucide-react" ;
44import { useState } from "react" ;
55
66import { PATTERNS } from "@/app/const" ;
@@ -14,58 +14,67 @@ type PatternsSidebarProps = {
1414 onSelectPattern : ( pattern : Pattern ) => void ;
1515} ;
1616
17- type CategoryFilter = "all" | "spaceship" | "oscillator" | "still" ;
17+ type PatternCategory = "spaceship" | "oscillator" | "still" ;
1818
1919const categoryIcons = {
2020 still : Box ,
2121 oscillator : Zap ,
2222 spaceship : Rocket ,
23- custom : Grid3X3 ,
2423} ;
2524
2625const categoryLabels = {
2726 still : "Still Lives" ,
2827 oscillator : "Oscillators" ,
2928 spaceship : "Spaceships" ,
30- custom : "Custom" ,
3129} ;
3230
33- const categoryFilters : Array < { value : CategoryFilter ; label : string } > = [
34- { value : "all" , label : "All" } ,
35- { value : "spaceship" , label : "Spaceships" } ,
36- { value : "oscillator" , label : "Oscillators" } ,
37- { value : "still" , label : "Still Lives" } ,
38- ] ;
31+ const categoryOrder : PatternCategory [ ] = [ "spaceship" , "oscillator" , "still" ] ;
3932
4033export const PatternsSidebar = ( { onSelectPattern } : PatternsSidebarProps ) => {
4134 const [ search , setSearch ] = useState ( "" ) ;
42- const [ selectedCategory , setSelectedCategory ] = useState < CategoryFilter > ( "all" ) ;
4335 const [ selectedPattern , setSelectedPattern ] = useState < { id : string ; name : string } | null > ( null ) ;
36+ const [ expandedCategories , setExpandedCategories ] = useState < Record < PatternCategory , boolean > > ( {
37+ spaceship : true ,
38+ oscillator : true ,
39+ still : true ,
40+ } ) ;
4441
4542 const handleSelectPattern = ( pattern : Pattern ) => {
4643 setSelectedPattern ( { id : pattern . id , name : pattern . name } ) ;
4744 onSelectPattern ( pattern ) ;
4845 } ;
4946
5047 const filteredPatterns = PATTERNS . filter ( ( p ) => {
51- const matchesSearch =
48+ return (
5249 p . name . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) ||
53- p . description . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) ;
54- const matchesCategory = selectedCategory === "all" || p . category === selectedCategory ;
55- return matchesSearch && matchesCategory ;
50+ p . description . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
51+ ) ;
5652 } ) ;
5753
5854 const groupedPatterns = filteredPatterns . reduce (
5955 ( acc , pattern ) => {
60- if ( ! acc [ pattern . category ] ) acc [ pattern . category ] = [ ] ;
61- acc [ pattern . category ] . push ( pattern ) ;
56+ if ( pattern . category in acc ) {
57+ const category = pattern . category as PatternCategory ;
58+ acc [ category ] . push ( pattern ) ;
59+ }
6260 return acc ;
6361 } ,
64- { } as Record < string , Pattern [ ] > ,
62+ {
63+ spaceship : [ ] ,
64+ oscillator : [ ] ,
65+ still : [ ] ,
66+ } as Record < PatternCategory , Pattern [ ] > ,
6567 ) ;
6668
69+ const toggleCategory = ( category : PatternCategory ) => {
70+ setExpandedCategories ( ( prev ) => ( {
71+ ...prev ,
72+ [ category ] : ! prev [ category ] ,
73+ } ) ) ;
74+ } ;
75+
6776 return (
68- < div className = "flex flex-col h-full rounded-xl border border-border/50 bg-card/60 backdrop-blur-md overflow-hidden " >
77+ < div className = "flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-border/50 bg-card/60 backdrop-blur-md" >
6978 { /* Header */ }
7079 < div className = "p-4 border-b border-border/50" >
7180 < h2 className = "text-sm font-semibold text-foreground mb-3" > Patterns</ h2 >
@@ -78,52 +87,49 @@ export const PatternsSidebar = ({ onSelectPattern }: PatternsSidebarProps) => {
7887 className = "pl-9 h-9 bg-background/50 border-border/50 text-sm"
7988 />
8089 </ div >
81- < div className = "mt-3 flex flex-wrap gap-2" >
82- { categoryFilters . map ( ( category ) => {
83- const isActive = selectedCategory === category . value ;
84-
85- return (
86- < button
87- key = { category . value }
88- type = "button"
89- onClick = { ( ) => setSelectedCategory ( category . value ) }
90- className = { [
91- "rounded-md border px-2.5 py-1 text-xs font-medium transition-colors" ,
92- isActive
93- ? "border-primary/40 bg-primary/15 text-primary"
94- : "border-border/60 bg-background/40 text-muted-foreground hover:bg-accent/60" ,
95- ] . join ( " " ) }
96- >
97- { category . label }
98- </ button >
99- ) ;
100- } ) }
101- </ div >
10290 </ div >
10391
10492 { /* Patterns List */ }
105- < ScrollArea className = "flex-1" >
93+ < ScrollArea className = "min-h-0 flex-1" >
10694 < div className = "p-3 space-y-4" >
107- { selectedCategory === "all" ? (
108- Object . entries ( groupedPatterns ) . map ( ( [ category , patterns ] ) => {
109- const Icon = categoryIcons [ category as keyof typeof categoryIcons ] ;
110- return (
111- < div key = { category } >
112- < div className = "flex items-center gap-2 px-2 mb-2" >
113- < Icon className = "h-3.5 w-3.5 text-muted-foreground" />
114- < span className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
115- { categoryLabels [ category as keyof typeof categoryLabels ] }
116- </ span >
117- </ div >
95+ { categoryOrder . map ( ( category ) => {
96+ const patterns = groupedPatterns [ category ] ;
97+
98+ if ( patterns . length === 0 ) return null ;
99+
100+ const Icon = categoryIcons [ category ] ;
101+ const isExpanded = expandedCategories [ category ] ;
102+
103+ return (
104+ < div key = { category } >
105+ < button
106+ type = "button"
107+ onClick = { ( ) => toggleCategory ( category ) }
108+ className = "mb-2 flex w-full items-center gap-2 rounded-md px-2 py-1 text-left hover:bg-accent/40"
109+ >
110+ < ChevronRight
111+ className = { [
112+ "h-3.5 w-3.5 text-muted-foreground transition-transform" ,
113+ isExpanded ? "rotate-90" : "rotate-0" ,
114+ ] . join ( " " ) }
115+ />
116+ < Icon className = "h-3.5 w-3.5 text-muted-foreground" />
117+ < span className = "text-xs font-medium text-muted-foreground uppercase tracking-wide" >
118+ { categoryLabels [ category ] }
119+ </ span >
120+ </ button >
121+
122+ { isExpanded && (
118123 < div className = "space-y-1" >
119124 { patterns . map ( ( pattern ) => (
120125 < div
121126 key = { pattern . id }
122- className = {
127+ className = { [
128+ "rounded-lg border transition-colors" ,
123129 selectedPattern ?. id === pattern . id
124- ? "rounded-lg bg-primary/10 ring-1 ring-primary/40 "
125- : "rounded-lg"
126- }
130+ ? "border-primary/50 bg-primary/10 ring-1 ring-primary/30 "
131+ : "border-transparent" ,
132+ ] . join ( " " ) }
127133 >
128134 < PatternItem
129135 pattern = { pattern }
@@ -132,25 +138,10 @@ export const PatternsSidebar = ({ onSelectPattern }: PatternsSidebarProps) => {
132138 </ div >
133139 ) ) }
134140 </ div >
135- </ div >
136- ) ;
137- } )
138- ) : (
139- < div className = "space-y-1" >
140- { filteredPatterns . map ( ( pattern ) => (
141- < div
142- key = { pattern . id }
143- className = {
144- selectedPattern ?. id === pattern . id
145- ? "rounded-lg bg-primary/10 ring-1 ring-primary/40"
146- : "rounded-lg"
147- }
148- >
149- < PatternItem pattern = { pattern } onSelect = { ( ) => handleSelectPattern ( pattern ) } />
150- </ div >
151- ) ) }
152- </ div >
153- ) }
141+ ) }
142+ </ div >
143+ ) ;
144+ } ) }
154145
155146 { filteredPatterns . length === 0 && (
156147 < div className = "text-center py-8 text-muted-foreground text-sm" > No patterns found</ div >
0 commit comments