@@ -55,9 +55,21 @@ const StackBuilder = () => {
5555 const [ , setLastChanges ] = useState < Array < { category : string ; message : string } > > ( [ ] ) ;
5656
5757 const sectionRefs = useRef < Record < string , HTMLElement | null > > ( { } ) ;
58- const contentRef = useRef < HTMLDivElement > ( null ) ;
58+ const contentRef = useRef < HTMLDivElement | null > ( null ) ;
59+ const scrollAreaRef = useRef < HTMLDivElement | null > ( null ) ;
5960 const lastAppliedStackString = useRef < string > ( "" ) ;
6061
62+ useEffect ( ( ) => {
63+ if ( scrollAreaRef . current ) {
64+ const viewport = scrollAreaRef . current . querySelector < HTMLDivElement > (
65+ '[data-slot="scroll-area-viewport"]' ,
66+ ) ;
67+ if ( viewport ) {
68+ contentRef . current = viewport ;
69+ }
70+ }
71+ } , [ viewMode ] ) ;
72+
6173 const compatibilityAnalysis = analyzeStackCompatibility ( stack ) ;
6274
6375 const projectNameError = validateProjectName ( stack . projectName || "" ) ;
@@ -459,15 +471,17 @@ const StackBuilder = () => {
459471 < PresetDropdown onApplyPreset = { applyPreset } />
460472
461473 < DropdownMenu >
462- < DropdownMenuTrigger asChild >
463- < button
464- type = "button"
465- className = "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
466- >
467- < Settings className = "h-3 w-3" />
468- Settings
469- < ChevronDown className = "ml-auto h-3 w-3" />
470- </ button >
474+ < DropdownMenuTrigger
475+ render = {
476+ < button
477+ type = "button"
478+ className = "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-fd-background px-2 py-1.5 font-medium text-muted-foreground text-xs transition-all hover:border-muted-foreground/30 hover:bg-muted hover:text-foreground"
479+ />
480+ }
481+ >
482+ < Settings className = "h-3 w-3" />
483+ Settings
484+ < ChevronDown className = "ml-auto h-3 w-3" />
471485 </ DropdownMenuTrigger >
472486 < DropdownMenuContent align = "end" className = "w-64 bg-fd-background" >
473487 < YoloToggle stack = { stack } onToggle = { ( yolo ) => setStack ( { yolo } ) } />
@@ -526,97 +540,103 @@ const StackBuilder = () => {
526540 onSelectFile = { setSelectedFile }
527541 />
528542 ) : (
529- < ScrollArea ref = { contentRef } className = "flex-1 overflow-hidden scroll-smooth" >
530- < main className = "p-3 sm:p-4" >
531- { CATEGORY_ORDER . map ( ( categoryKey ) => {
532- const categoryOptions =
533- TECH_OPTIONS [ categoryKey as keyof typeof TECH_OPTIONS ] || [ ] ;
534- const categoryDisplayName = getCategoryDisplayName ( categoryKey ) ;
535-
536- const filteredOptions = categoryOptions ;
537-
538- if ( filteredOptions . length === 0 ) return null ;
539-
540- return (
541- < section
542- ref = { ( el ) => {
543- sectionRefs . current [ categoryKey ] = el ;
544- } }
545- key = { categoryKey }
546- id = { `section-${ categoryKey } ` }
547- className = "mb-6 scroll-mt-4 sm:mb-8"
548- >
549- < div className = "mb-3 flex items-center border-border border-b pb-2 text-muted-foreground" >
550- < Terminal className = "mr-2 h-4 w-4 shrink-0 sm:h-5 sm:w-5" />
551- < h2 className = "font-semibold text-foreground text-sm sm:text-base" >
552- { categoryDisplayName }
553- </ h2 >
554- { compatibilityAnalysis . notes [ categoryKey ] ?. hasIssue && (
555- < Tooltip delayDuration = { 100 } >
556- < TooltipTrigger asChild >
557- < InfoIcon className = "ml-2 h-4 w-4 shrink-0 cursor-help text-muted-foreground" />
558- </ TooltipTrigger >
559- < TooltipContent side = "top" align = "start" >
560- < ul className = "list-disc space-y-1 pl-4 text-xs" >
561- { compatibilityAnalysis . notes [ categoryKey ] . notes . map ( ( note ) => (
562- < li key = { note } > { note } </ li >
563- ) ) }
564- </ ul >
565- </ TooltipContent >
566- </ Tooltip >
567- ) }
568- </ div >
569-
570- < div className = "grid grid-cols-1 xs:grid-cols-2 gap-2 sm:gap-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4" >
571- { filteredOptions . map ( ( tech ) => {
572- let isSelected = false ;
573- const category = categoryKey as keyof StackState ;
574- const currentValue = stack [ category ] ;
575-
576- if (
577- category === "addons" ||
578- category === "examples" ||
579- category === "webFrontend" ||
580- category === "nativeFrontend"
581- ) {
582- isSelected = ( ( currentValue as string [ ] ) || [ ] ) . includes ( tech . id ) ;
583- } else {
584- isSelected = currentValue === tech . id ;
585- }
586-
587- const isDisabled = ! isOptionCompatible (
588- stack ,
589- categoryKey as keyof typeof TECH_OPTIONS ,
590- tech . id ,
591- ) ;
592-
593- const disabledReason = isDisabled
594- ? getDisabledReason (
595- stack ,
596- categoryKey as keyof typeof TECH_OPTIONS ,
597- tech . id ,
598- )
599- : null ;
600-
601- return (
602- < Tooltip key = { tech . id } delayDuration = { 100 } >
603- < TooltipTrigger asChild >
604- < motion . div
605- className = { cn (
606- "relative cursor-pointer rounded border p-2 transition-all sm:p-3" ,
607- isSelected
608- ? "border-primary bg-primary/10"
609- : isDisabled
610- ? "border-destructive/30 bg-destructive/5 opacity-50 hover:opacity-75"
611- : "border-border hover:border-muted hover:bg-muted" ,
612- ) }
613- whileHover = { { scale : 1.02 } }
614- whileTap = { { scale : 0.98 } }
615- onClick = { ( ) =>
616- handleTechSelect (
617- categoryKey as keyof typeof TECH_OPTIONS ,
618- tech . id ,
619- )
543+ < div ref = { scrollAreaRef } className = "flex-1" >
544+ < ScrollArea className = "h-full overflow-hidden scroll-smooth" >
545+ < main className = "p-3 sm:p-4" >
546+ { CATEGORY_ORDER . map ( ( categoryKey ) => {
547+ const categoryOptions =
548+ TECH_OPTIONS [ categoryKey as keyof typeof TECH_OPTIONS ] || [ ] ;
549+ const categoryDisplayName = getCategoryDisplayName ( categoryKey ) ;
550+
551+ const filteredOptions = categoryOptions ;
552+
553+ if ( filteredOptions . length === 0 ) return null ;
554+
555+ return (
556+ < section
557+ ref = { ( el ) => {
558+ sectionRefs . current [ categoryKey ] = el ;
559+ } }
560+ key = { categoryKey }
561+ id = { `section-${ categoryKey } ` }
562+ className = "mb-6 scroll-mt-4 sm:mb-8"
563+ >
564+ < div className = "mb-3 flex items-center border-border border-b pb-2 text-muted-foreground" >
565+ < Terminal className = "mr-2 h-4 w-4 shrink-0 sm:h-5 sm:w-5" />
566+ < h2 className = "font-semibold text-foreground text-sm sm:text-base" >
567+ { categoryDisplayName }
568+ </ h2 >
569+ { compatibilityAnalysis . notes [ categoryKey ] ?. hasIssue && (
570+ < Tooltip delay = { 100 } >
571+ < TooltipTrigger
572+ render = {
573+ < InfoIcon className = "ml-2 h-4 w-4 shrink-0 cursor-help text-muted-foreground" />
574+ }
575+ />
576+ < TooltipContent side = "top" align = "start" >
577+ < ul className = "list-disc space-y-1 pl-4 text-xs" >
578+ { compatibilityAnalysis . notes [ categoryKey ] . notes . map ( ( note ) => (
579+ < li key = { note } > { note } </ li >
580+ ) ) }
581+ </ ul >
582+ </ TooltipContent >
583+ </ Tooltip >
584+ ) }
585+ </ div >
586+
587+ < div className = "grid grid-cols-1 xs:grid-cols-2 gap-2 sm:gap-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4" >
588+ { filteredOptions . map ( ( tech ) => {
589+ let isSelected = false ;
590+ const category = categoryKey as keyof StackState ;
591+ const currentValue = stack [ category ] ;
592+
593+ if (
594+ category === "addons" ||
595+ category === "examples" ||
596+ category === "webFrontend" ||
597+ category === "nativeFrontend"
598+ ) {
599+ isSelected = ( ( currentValue as string [ ] ) || [ ] ) . includes ( tech . id ) ;
600+ } else {
601+ isSelected = currentValue === tech . id ;
602+ }
603+
604+ const isDisabled = ! isOptionCompatible (
605+ stack ,
606+ categoryKey as keyof typeof TECH_OPTIONS ,
607+ tech . id ,
608+ ) ;
609+
610+ const disabledReason = isDisabled
611+ ? getDisabledReason (
612+ stack ,
613+ categoryKey as keyof typeof TECH_OPTIONS ,
614+ tech . id ,
615+ )
616+ : null ;
617+
618+ return (
619+ < Tooltip key = { tech . id } delay = { 100 } >
620+ < TooltipTrigger
621+ render = {
622+ < motion . div
623+ className = { cn (
624+ "relative cursor-pointer rounded border p-2 transition-all sm:p-3" ,
625+ isSelected
626+ ? "border-primary bg-primary/10"
627+ : isDisabled
628+ ? "border-destructive/30 bg-destructive/5 opacity-50 hover:opacity-75"
629+ : "border-border hover:border-muted hover:bg-muted" ,
630+ ) }
631+ whileHover = { { scale : 1.02 } }
632+ whileTap = { { scale : 0.98 } }
633+ onClick = { ( ) =>
634+ handleTechSelect (
635+ categoryKey as keyof typeof TECH_OPTIONS ,
636+ tech . id ,
637+ )
638+ }
639+ />
620640 }
621641 >
622642 < div className = "flex items-start" >
@@ -653,23 +673,23 @@ const StackBuilder = () => {
653673 Default
654674 </ span >
655675 ) }
656- </ motion . div >
657- </ TooltipTrigger >
658- { disabledReason && (
659- < TooltipContent side = "top" align = "center" className = "max-w- xs" >
660- < p className = "text-xs" > { disabledReason } </ p >
661- </ TooltipContent >
662- ) }
663- </ Tooltip >
664- ) ;
665- } ) }
666- </ div >
667- </ section >
668- ) ;
669- } ) }
670- < div className = "h-10" / >
671- </ main >
672- </ ScrollArea >
676+ </ TooltipTrigger >
677+ { disabledReason && (
678+ < TooltipContent side = "top" align = "center" className = "max-w-xs" >
679+ < p className = "text- xs" > { disabledReason } </ p >
680+ </ TooltipContent >
681+ ) }
682+ </ Tooltip >
683+ ) ;
684+ } ) }
685+ </ div >
686+ </ section >
687+ ) ;
688+ } ) }
689+ < div className = "h-10" />
690+ </ main >
691+ </ ScrollArea >
692+ </ div >
673693 ) }
674694 </ div >
675695 </ div >
0 commit comments