@@ -521,6 +521,22 @@ export function ThemeSelector({ activeTheme, onSelect, disabled }: ThemeSelector
521521 return ( ) => window . removeEventListener ( 'keydown' , handler ) ;
522522 } , [ expanded ] ) ;
523523
524+ // Close on click outside
525+ useEffect ( ( ) => {
526+ if ( ! expanded ) return ;
527+ const handler = ( e : MouseEvent ) => {
528+ if ( panelRef . current && ! panelRef . current . contains ( e . target as Node ) ) {
529+ setExpanded ( false ) ;
530+ }
531+ } ;
532+ // Use setTimeout so the opening click doesn't immediately close it
533+ const t = setTimeout ( ( ) => window . addEventListener ( 'mousedown' , handler ) , 0 ) ;
534+ return ( ) => {
535+ clearTimeout ( t ) ;
536+ window . removeEventListener ( 'mousedown' , handler ) ;
537+ } ;
538+ } , [ expanded ] ) ;
539+
524540 // Filter themes
525541 const filteredThemes = useMemo ( ( ) => {
526542 let result = THEMES ;
@@ -563,7 +579,7 @@ export function ThemeSelector({ activeTheme, onSelect, disabled }: ThemeSelector
563579 ) ;
564580
565581 return (
566- < div className = "mb-6" >
582+ < div className = "relative mb-6" >
567583 { /* ── Trigger Row ─────────────────────────────────────── */ }
568584 < div className = "flex items-center justify-center gap-3" >
569585 { /* Current theme pill */ }
@@ -613,124 +629,105 @@ export function ThemeSelector({ activeTheme, onSelect, disabled }: ThemeSelector
613629 </ button >
614630 </ div >
615631
616- { /* ── Expanded Panel ──────────────────────────────────── */ }
617- < div
618- className = "grid transition-[grid-template-rows] duration-300 ease-out"
619- style = { { gridTemplateRows : expanded ? '1fr' : '0fr' } }
620- >
621- < div className = "overflow-hidden" >
622- < div ref = { panelRef } className = "pt-5" >
623- < div className = "rounded-2xl bg-sc-bg-dark/80 p-4 ring-1 ring-white/[0.06] backdrop-blur-sm" >
624- { /* Search */ }
625- < div className = "relative mb-3" >
626- < svg
627- aria-hidden = "true"
628- className = "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-sc-fg-dim"
629- fill = "none"
630- viewBox = "0 0 24 24"
631- stroke = "currentColor"
632- strokeWidth = { 2 }
633- >
634- < path
635- strokeLinecap = "round"
636- strokeLinejoin = "round"
637- d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
638- />
639- </ svg >
640- < input
641- ref = { searchRef }
642- type = "text"
643- placeholder = "Search themes..."
644- value = { search }
645- onChange = { e => setSearch ( e . target . value ) }
646- className = "w-full rounded-xl bg-sc-bg/80 py-2.5 pl-10 pr-4 text-sm text-sc-fg placeholder:text-sc-fg-dim ring-1 ring-white/[0.06] transition-all focus:outline-none focus:ring-sc-purple/40 focus:shadow-[0_0_20px_rgba(225,53,255,0.08)]"
632+ { /* ── Floating Overlay Panel ────────────────────────── */ }
633+ { expanded && (
634+ < div ref = { panelRef } className = "absolute left-0 right-0 top-full z-40 pt-3 animate-drop-in" >
635+ < div className = "rounded-2xl bg-sc-bg-dark/95 p-4 shadow-[0_16px_48px_rgba(0,0,0,0.4)] ring-1 ring-white/[0.08] backdrop-blur-xl" >
636+ { /* Search */ }
637+ < div className = "relative mb-3" >
638+ < svg
639+ aria-hidden = "true"
640+ className = "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-sc-fg-dim"
641+ fill = "none"
642+ viewBox = "0 0 24 24"
643+ stroke = "currentColor"
644+ strokeWidth = { 2 }
645+ >
646+ < path
647+ strokeLinecap = "round"
648+ strokeLinejoin = "round"
649+ d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
647650 />
648- { search && (
649- < button
650- type = "button"
651- onClick = { ( ) => setSearch ( '' ) }
652- className = "absolute right-3 top-1/2 -translate-y-1/2 text-sc-fg-dim hover:text-sc-fg"
653- >
654- < svg
655- aria-hidden = "true"
656- className = "h-4 w-4"
657- fill = "none"
658- viewBox = "0 0 24 24"
659- stroke = "currentColor"
660- strokeWidth = { 2 }
661- >
662- < path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 18L18 6M6 6l12 12" />
663- </ svg >
664- </ button >
665- ) }
666- </ div >
667-
668- { /* Family filter pills */ }
669- < div className = "mb-3 flex flex-wrap gap-1.5" >
651+ </ svg >
652+ < input
653+ ref = { searchRef }
654+ type = "text"
655+ placeholder = "Search themes..."
656+ value = { search }
657+ onChange = { e => setSearch ( e . target . value ) }
658+ className = "w-full rounded-xl bg-sc-bg/80 py-2.5 pl-10 pr-4 text-sm text-sc-fg placeholder:text-sc-fg-dim ring-1 ring-white/[0.06] transition-all focus:outline-none focus:ring-sc-purple/40 focus:shadow-[0_0_20px_rgba(225,53,255,0.08)]"
659+ />
660+ { search && (
670661 < button
671662 type = "button"
672- onClick = { ( ) => setActiveFamily ( null ) }
673- className = { `rounded-lg px-2.5 py-1 text-xs font-medium transition-all ${
674- activeFamily === null
675- ? 'bg-sc-purple/15 text-sc-purple ring-1 ring-sc-purple/30'
676- : 'text-sc-fg-dim hover:bg-sc-bg-highlight hover:text-sc-fg'
677- } `}
663+ onClick = { ( ) => setSearch ( '' ) }
664+ className = "absolute right-3 top-1/2 -translate-y-1/2 text-sc-fg-dim hover:text-sc-fg"
678665 >
679- All ({ THEMES . length } )
666+ < svg
667+ aria-hidden = "true"
668+ className = "h-4 w-4"
669+ fill = "none"
670+ viewBox = "0 0 24 24"
671+ stroke = "currentColor"
672+ strokeWidth = { 2 }
673+ >
674+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 18L18 6M6 6l12 12" />
675+ </ svg >
680676 </ button >
681- { FAMILIES . map ( fam => {
682- const count = THEMES . filter ( t => t . family === fam . id ) . length ;
683- return (
684- < button
685- type = "button"
686- key = { fam . id }
687- onClick = { ( ) => setActiveFamily ( f => ( f === fam . id ? null : fam . id ) ) }
688- className = { `rounded-lg px-2.5 py-1 text-xs font-medium transition-all ${
689- activeFamily === fam . id
690- ? 'bg-sc-purple/15 text-sc-purple ring-1 ring-sc-purple/30'
691- : 'text-sc-fg-dim hover:bg-sc-bg-highlight hover:text-sc-fg'
692- } `}
693- >
694- { fam . label } ({ count } )
695- </ button >
696- ) ;
697- } ) }
698- </ div >
677+ ) }
678+ </ div >
699679
700- { /* Theme grid */ }
701- < div className = "editor-scrollbar max-h-[360px] overflow-y-auto pr-1" >
702- { filteredThemes . length === 0 && (
703- < div className = "py-8 text-center text-sm text-sc-fg-dim" >
704- No themes match “{ search } ”
705- </ div >
706- ) }
680+ { /* Family filter pills */ }
681+ < div className = "mb-3 flex flex-wrap gap-1.5" >
682+ < button
683+ type = "button"
684+ onClick = { ( ) => setActiveFamily ( null ) }
685+ className = { `rounded-lg px-2.5 py-1 text-xs font-medium transition-all ${
686+ activeFamily === null
687+ ? 'bg-sc-purple/15 text-sc-purple ring-1 ring-sc-purple/30'
688+ : 'text-sc-fg-dim hover:bg-sc-bg-highlight hover:text-sc-fg'
689+ } `}
690+ >
691+ All ({ THEMES . length } )
692+ </ button >
693+ { FAMILIES . map ( fam => {
694+ const count = THEMES . filter ( t => t . family === fam . id ) . length ;
695+ return (
696+ < button
697+ type = "button"
698+ key = { fam . id }
699+ onClick = { ( ) => setActiveFamily ( f => ( f === fam . id ? null : fam . id ) ) }
700+ className = { `rounded-lg px-2.5 py-1 text-xs font-medium transition-all ${
701+ activeFamily === fam . id
702+ ? 'bg-sc-purple/15 text-sc-purple ring-1 ring-sc-purple/30'
703+ : 'text-sc-fg-dim hover:bg-sc-bg-highlight hover:text-sc-fg'
704+ } `}
705+ >
706+ { fam . label } ({ count } )
707+ </ button >
708+ ) ;
709+ } ) }
710+ </ div >
707711
708- { /* Grouped view (when no filter active) */ }
709- { groupedThemes ?. map ( ( { family, themes } ) => (
710- < div key = { family . id } className = "mb-4 last:mb-0" >
711- < div className = "mb-2 flex items-center gap-2 px-1" >
712- < h4 className = "text-xs font-semibold uppercase tracking-widest text-sc-fg-dim" >
713- { family . label }
714- </ h4 >
715- < div className = "h-px flex-1 bg-white/[0.04]" />
716- </ div >
717- < div className = "grid grid-cols-1 gap-1.5 sm:grid-cols-2" >
718- { themes . map ( theme => (
719- < ThemeCard
720- key = { theme . id }
721- theme = { theme }
722- isActive = { activeTheme === theme . id }
723- onSelect = { ( ) => handleSelect ( theme . id ) }
724- />
725- ) ) }
726- </ div >
727- </ div >
728- ) ) }
712+ { /* Theme grid */ }
713+ < div className = "editor-scrollbar max-h-[360px] overflow-y-auto pr-1" >
714+ { filteredThemes . length === 0 && (
715+ < div className = "py-8 text-center text-sm text-sc-fg-dim" >
716+ No themes match “{ search } ”
717+ </ div >
718+ ) }
729719
730- { /* Flat view (when searching or family selected) */ }
731- { ! groupedThemes && (
720+ { /* Grouped view (when no filter active) */ }
721+ { groupedThemes ?. map ( ( { family, themes } ) => (
722+ < div key = { family . id } className = "mb-4 last:mb-0" >
723+ < div className = "mb-2 flex items-center gap-2 px-1" >
724+ < h4 className = "text-xs font-semibold uppercase tracking-widest text-sc-fg-dim" >
725+ { family . label }
726+ </ h4 >
727+ < div className = "h-px flex-1 bg-white/[0.04]" />
728+ </ div >
732729 < div className = "grid grid-cols-1 gap-1.5 sm:grid-cols-2" >
733- { filteredThemes . map ( theme => (
730+ { themes . map ( theme => (
734731 < ThemeCard
735732 key = { theme . id }
736733 theme = { theme }
@@ -739,19 +736,33 @@ export function ThemeSelector({ activeTheme, onSelect, disabled }: ThemeSelector
739736 />
740737 ) ) }
741738 </ div >
742- ) }
743- </ div >
744-
745- { /* Footer count */ }
746- { ( search || activeFamily ) && filteredThemes . length > 0 && (
747- < div className = "mt-2 text-center text-xs text-sc-fg-dim" >
748- { filteredThemes . length } of { THEMES . length } themes
739+ </ div >
740+ ) ) }
741+
742+ { /* Flat view (when searching or family selected) */ }
743+ { ! groupedThemes && (
744+ < div className = "grid grid-cols-1 gap-1.5 sm:grid-cols-2" >
745+ { filteredThemes . map ( theme => (
746+ < ThemeCard
747+ key = { theme . id }
748+ theme = { theme }
749+ isActive = { activeTheme === theme . id }
750+ onSelect = { ( ) => handleSelect ( theme . id ) }
751+ />
752+ ) ) }
749753 </ div >
750754 ) }
751755 </ div >
756+
757+ { /* Footer count */ }
758+ { ( search || activeFamily ) && filteredThemes . length > 0 && (
759+ < div className = "mt-2 text-center text-xs text-sc-fg-dim" >
760+ { filteredThemes . length } of { THEMES . length } themes
761+ </ div >
762+ ) }
752763 </ div >
753764 </ div >
754- </ div >
765+ ) }
755766 </ div >
756767 ) ;
757768}
0 commit comments