@@ -438,13 +438,8 @@ var (
438438 Foreground (lipgloss .Color ("#00FF87" ))
439439)
440440
441- // asciiTitle is a styled "CLI PLAY" header.
442- var asciiTitle = strings .Join ([]string {
443- " ___ _ ___ ___ _ _ __ __" ,
444- " / __|| | |_ _| | _ \\ | | /_\\ \\ \\ / /" ,
445- "| (__ | |__ | | | _/| |__ / _ \\ \\ V / " ,
446- " \\ ___||____||___| |_| |____|/_/ \\ _\\ |_| " ,
447- }, "\n " )
441+ // compactTitle is a single-line styled title for the menu header.
442+ const compactTitle = "CLI PLAY"
448443
449444// colWidth is the fixed visual width of a single game entry in multi-column mode.
450445// cursor(3) + shortcut(4) + icon(3) + name(16) + gap(2) = 28.
@@ -469,20 +464,17 @@ func (m Model) contentHeight(showTitle, showStats, showPreview, showTip bool) in
469464 cols := m .columnCount ()
470465 lines := 0
471466 if showTitle {
472- lines += 5 // 4 ASCII art lines + blank
467+ lines ++ // compact single-line title
473468 }
474469 if showStats {
475- lines += 2 // stats + blank
470+ lines ++ // stats line
476471 }
477- // Game list: category headers + game rows in grid + inter-category blanks.
478- for i , cat := range categories {
479- if i > 0 {
480- lines ++ // blank between categories
481- }
472+ // Game list: category headers + game rows in grid (no inter-category blanks).
473+ for _ , cat := range categories {
482474 lines ++ // header
483475 lines += (len (cat .Indices ) + cols - 1 ) / cols // game rows
484476 }
485- lines += 2 // blank + settings row
477+ lines ++ // settings row
486478 if showPreview {
487479 lines += 8 // border+title+rules+controls ~ 7-8 lines
488480 }
@@ -497,8 +489,8 @@ func (m Model) contentHeight(showTitle, showStats, showPreview, showTip bool) in
497489func (m Model ) View () string {
498490 var b strings.Builder
499491
500- // Available inner height: total minus border (2) and padding (2) .
501- innerH := m .height - 4
492+ // Available inner height: total minus border (2).
493+ innerH := m .height - 2
502494
503495 // Progressively hide elements to fit: preview first, then title, then stats, then tips.
504496 showTitle := true
@@ -519,10 +511,10 @@ func (m Model) View() string {
519511 showTip = false
520512 }
521513
522- // Title (#22).
514+ // Compact title on a single line (#22).
523515 if showTitle {
524- b .WriteString (titleStyle .Render (asciiTitle ))
525- b .WriteString ("\n \n " )
516+ b .WriteString (titleStyle .Render (compactTitle ))
517+ b .WriteString ("\n " )
526518 }
527519
528520 // Session stats bar (#26).
@@ -533,13 +525,13 @@ func (m Model) View() string {
533525 }
534526 statsLine := fmt .Sprintf ("Games played: %d | Session: %dm" , m .gamesPlayed , elapsed )
535527 b .WriteString (statsStyle .Render (statsLine ))
536- b .WriteString ("\n \n " )
528+ b .WriteString ("\n " )
537529 }
538530
539531 // Welcome back flash (#27).
540532 if m .showWelcome {
541533 b .WriteString (welcomeStyle .Render (" Welcome back!" ))
542- b .WriteString ("\n \n " )
534+ b .WriteString ("\n " )
543535 }
544536
545537 // Game list with categories (#30), icons (#24), shortcuts (#23).
@@ -553,13 +545,7 @@ func (m Model) View() string {
553545 break
554546 }
555547
556- // Inter-category spacing.
557- if catI > 0 {
558- b .WriteString ("\n " )
559- visualRow ++
560- }
561-
562- // Category header.
548+ // Category header (no inter-category blank lines to save vertical space).
563549 cat := categories [catI ]
564550 b .WriteString (categoryStyle .Render (fmt .Sprintf (" %s %s" , cat .Icon , cat .Name )))
565551 b .WriteString ("\n " )
@@ -594,7 +580,6 @@ func (m Model) View() string {
594580 }
595581
596582 // Settings entry.
597- b .WriteString ("\n " )
598583 for ri , row := range menuRows {
599584 if row .gameIndex == SettingsIndex {
600585 b .WriteString (m .renderEntry (row , ri , compact ))
@@ -627,11 +612,7 @@ func (m Model) View() string {
627612 panel := lipgloss .NewStyle ().
628613 Border (panelBorder ).
629614 BorderForeground (lipgloss .Color ("240" )).
630- Padding (1 , 2 ).
631- BorderTop (true ).
632- BorderBottom (true ).
633- BorderLeft (true ).
634- BorderRight (true ).
615+ Padding (0 , 2 ).
635616 Render (b .String ())
636617
637618 return lipgloss .Place (m .width , m .height , lipgloss .Center , lipgloss .Center , panel )
@@ -665,39 +646,139 @@ func (m Model) renderPreview() string {
665646 return previewBorder .Render (pb .String ())
666647}
667648
668- // MenuText returns the menu layout as plain text (no ANSI styling).
669- // The transition model uses this to pre-compute character positions
670- // for the reveal animation.
671- func MenuText (width , height int ) string {
649+
650+ // TransitionText returns the menu layout as plain text (no ANSI, no border)
651+ // for the transition reveal animation. It mirrors the View() content using
652+ // the same responsive logic (column count, show/hide flags) so the revealed
653+ // text matches the real menu layout. The caller (transition) centers it.
654+ func (m Model ) TransitionText () string {
672655 var b strings.Builder
673656
674- b .WriteString (asciiTitle )
675- b .WriteString ("\n \n " )
657+ innerH := m .height - 2
658+ showTitle := true
659+ showStats := true
660+ showPreview := true
661+ showTip := true
676662
677- displayIdx := 0
678- for catIdx := range categories {
679- cat := categories [catIdx ]
680- if catIdx > 0 {
681- b .WriteString ("\n " )
682- }
683- b .WriteString (fmt .Sprintf (" %s %s\n " , cat .Icon , cat .Name ))
663+ if m .contentHeight (showTitle , showStats , showPreview , showTip ) > innerH {
664+ showPreview = false
665+ }
666+ if m .contentHeight (showTitle , showStats , showPreview , showTip ) > innerH {
667+ showTitle = false
668+ }
669+ if m .contentHeight (showTitle , showStats , showPreview , showTip ) > innerH {
670+ showStats = false
671+ }
672+ if m .contentHeight (showTitle , showStats , showPreview , showTip ) > innerH {
673+ showTip = false
674+ }
675+
676+ if showTitle {
677+ b .WriteString (compactTitle )
678+ b .WriteString ("\n " )
679+ }
680+ if showStats {
681+ b .WriteString (fmt .Sprintf ("Games played: %d | Session: 0m" , m .gamesPlayed ))
682+ b .WriteString ("\n " )
683+ }
684+
685+ cols := m .columnCount ()
686+ compact := cols > 1
687+
688+ for catI := range categories {
689+ cat := categories [catI ]
690+ b .WriteString (fmt .Sprintf (" %s\n " , cat .Name ))
691+
692+ // Collect game rows for this category.
693+ var catGames []int
684694 for _ , gi := range cat .Indices {
685- label := shortcutLabel (displayIdx )
686- icon := gameIcon [gi ]
687- b .WriteString (fmt .Sprintf (" [%s] %-3s%-16s%s\n " , label , icon , Games [gi ].Name , Games [gi ].Description ))
688- displayIdx ++
695+ for ri , row := range menuRows {
696+ if row .gameIndex == gi {
697+ catGames = append (catGames , ri )
698+ break
699+ }
700+ }
701+ }
702+
703+ // Render in grid.
704+ for i := 0 ; i < len (catGames ); i += cols {
705+ for j := 0 ; j < cols && i + j < len (catGames ); j ++ {
706+ ri := catGames [i + j ]
707+ row := menuRows [ri ]
708+ b .WriteString (plainEntry (row , ri == m .cursor , compact ))
709+ if compact && j < cols - 1 && i + j + 1 < len (catGames ) {
710+ b .WriteString (" " )
711+ }
712+ }
713+ b .WriteString ("\n " )
689714 }
690715 }
691716
692- b .WriteString ("\n " )
693- b .WriteString (fmt .Sprintf (" \u2699 %-16s%s\n " , "Settings" , "Preferences and configuration" ))
717+ // Settings.
718+ for ri , row := range menuRows {
719+ if row .gameIndex == SettingsIndex {
720+ b .WriteString (plainEntry (row , ri == m .cursor , compact ))
721+ b .WriteString ("\n " )
722+ break
723+ }
724+ }
694725
695726 b .WriteString ("\n " )
696- b .WriteString (" \u2191 \u2193 Navigate | Enter Select | 1-9/0/a-f Quick Select | Q Quit" )
727+ if showTip && m .tipIndex < len (tips ) {
728+ b .WriteString (tips [m .tipIndex ])
729+ b .WriteString ("\n " )
730+ }
731+ b .WriteString (" \u2190 \u2191 \u2193 \u2192 Navigate | Enter Select | 1-9/0/a-f Quick Select | Q Quit" )
697732
698733 return b .String ()
699734}
700735
736+ // plainEntry renders a single menu entry as plain text (no ANSI).
737+ func plainEntry (row menuRow , selected , compact bool ) string {
738+ var e strings.Builder
739+
740+ // Cursor.
741+ if selected {
742+ e .WriteString (" \u25b6 " )
743+ } else {
744+ e .WriteString (" " )
745+ }
746+
747+ // Shortcut.
748+ if row .gameIndex < SettingsIndex && row .displayIndex >= 0 {
749+ e .WriteString (fmt .Sprintf ("[%s] " , shortcutLabel (row .displayIndex )))
750+ } else {
751+ e .WriteString (" " )
752+ }
753+
754+ // Icon.
755+ if icon , ok := gameIcon [row .gameIndex ]; ok {
756+ e .WriteString (fmt .Sprintf ("%-3s" , icon ))
757+ } else if row .gameIndex == SettingsIndex {
758+ e .WriteString ("\u2699 " )
759+ }
760+
761+ // Name.
762+ name := ""
763+ if row .gameIndex == SettingsIndex {
764+ name = "Settings"
765+ } else if row .gameIndex >= 0 && row .gameIndex < len (Games ) {
766+ name = Games [row .gameIndex ].Name
767+ }
768+ e .WriteString (fmt .Sprintf ("%-16s" , name ))
769+
770+ // Description only in single-column.
771+ if ! compact {
772+ if row .gameIndex == SettingsIndex {
773+ e .WriteString ("Preferences and configuration" )
774+ } else if row .gameIndex >= 0 && row .gameIndex < len (Games ) {
775+ e .WriteString (Games [row .gameIndex ].Description )
776+ }
777+ }
778+
779+ return e .String ()
780+ }
781+
701782// Selected returns the index of the selected game, or -1 if none.
702783func (m Model ) Selected () int {
703784 return m .selected
0 commit comments