Skip to content

Commit 14b31ed

Browse files
authored
Merge pull request #85 from 0xjuanma/new-design
refactor[design]: design overhaul and styling updates across the app
2 parents 95cd8f2 + f1d0883 commit 14b31ed

File tree

21 files changed

+1271
-99
lines changed

21 files changed

+1271
-99
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- **Full Statistics Dialog** - Press `x` to view full match statistics
1515

1616
### Changed
17+
- **Unified Header Design** - All panel titles now use consistent compact header style with gradient text and diagonal fill pattern
18+
- **Visual Overhaul** - Refreshed main menu logo and updated styling across views
1719

1820
### Fixed
1921

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ var rootCmd = &cobra.Command{
6565
}
6666
}()
6767

68-
p := tea.NewProgram(app.New(mockFlag, debugFlag, isDevBuild, newVersionAvailable), tea.WithAltScreen())
68+
p := tea.NewProgram(app.New(mockFlag, debugFlag, isDevBuild, newVersionAvailable, Version), tea.WithAltScreen())
6969
if _, err := p.Run(); err != nil {
7070
fmt.Fprintf(os.Stderr, "Error running application: %v\n", err)
7171
os.Exit(1)

internal/app/model.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/0xjuanma/golazo/internal/notify"
1515
"github.com/0xjuanma/golazo/internal/reddit"
1616
"github.com/0xjuanma/golazo/internal/ui"
17+
"github.com/0xjuanma/golazo/internal/ui/logo"
1718
"github.com/charmbracelet/bubbles/key"
1819
"github.com/charmbracelet/bubbles/list"
1920
"github.com/charmbracelet/bubbles/spinner"
@@ -89,10 +90,11 @@ type model struct {
8990

9091
// Configuration
9192
useMockData bool
92-
debugMode bool // Enable debug logging to file
93-
isDevBuild bool // Whether this is a development build
94-
newVersionAvailable bool // Whether a new version of Golazo is available
95-
statsDateRange int // 1, 3, or 5 days (default: 1)
93+
debugMode bool // Enable debug logging to file
94+
isDevBuild bool // Whether this is a development build
95+
newVersionAvailable bool // Whether a new version of Golazo is available
96+
appVersion string // Current application version string
97+
statsDateRange int // 1, 3, or 5 days (default: 1)
9698

9799
// Settings view state
98100
settingsState *ui.SettingsState
@@ -110,14 +112,18 @@ type model struct {
110112

111113
// Notifications
112114
notifier *notify.DesktopNotifier
115+
116+
// Logo animation (main view only)
117+
animatedLogo *logo.AnimatedLogo
113118
}
114119

115120
// New creates a new application model with default values.
116121
// useMockData determines whether to use mock data instead of real API data.
117122
// debugMode enables debug logging to a file.
118123
// isDevBuild indicates if this is a development build.
119124
// newVersionAvailable indicates if a newer version is available.
120-
func New(useMockData bool, debugMode bool, isDevBuild bool, newVersionAvailable bool) model {
125+
// appVersion is the current application version string.
126+
func New(useMockData bool, debugMode bool, isDevBuild bool, newVersionAvailable bool, appVersion string) model {
121127
s := spinner.New()
122128
s.Spinner = spinner.Line
123129
s.Style = ui.SpinnerStyle()
@@ -198,13 +204,17 @@ func New(useMockData bool, debugMode bool, isDevBuild bool, newVersionAvailable
198204
redditClient, _ = reddit.NewClient()
199205
}
200206

207+
// Initialize animated logo for main view
208+
animatedLogo := logo.NewAnimatedLogoWithType(appVersion, false, logo.DefaultOpts(), 1200, 1, logo.AnimationWave)
209+
201210
return model{
202211
currentView: viewMain,
203212
matchDetailsCache: make(map[int]*api.MatchDetails),
204213
useMockData: useMockData,
205214
debugMode: debugMode,
206215
isDevBuild: isDevBuild,
207216
newVersionAvailable: newVersionAvailable,
217+
appVersion: appVersion,
208218
fotmobClient: fotmob.NewClient(),
209219
parser: fotmob.NewLiveUpdateParser(),
210220
redditClient: redditClient,
@@ -223,6 +233,7 @@ func New(useMockData bool, debugMode bool, isDevBuild bool, newVersionAvailable
223233
statsDateRange: 1,
224234
pendingSelection: -1, // No pending selection
225235
dialogOverlay: ui.NewDialogOverlay(), // Initialize dialog overlay
236+
animatedLogo: animatedLogo, // Initialize animated logo
226237
}
227238
}
228239

internal/app/update.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5353
return m.handleStatsDayData(msg)
5454

5555
case ui.TickMsg:
56-
return m.handleRandomSpinnerTick(msg)
56+
return m.handleAnimationTick(msg)
5757

5858
case mainViewCheckMsg:
5959
return m.handleMainViewCheck(msg)
@@ -77,7 +77,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7777
default:
7878
// Fallback handler for ui.TickMsg type assertion
7979
if _, ok := msg.(ui.TickMsg); ok {
80-
return m.handleRandomSpinnerTick(msg.(ui.TickMsg))
80+
return m.handleAnimationTick(msg.(ui.TickMsg))
8181
}
8282
}
8383

@@ -932,14 +932,21 @@ func filterMatchesByDays(matches []api.Match, days int) []api.Match {
932932
return filtered
933933
}
934934

935-
// handleRandomSpinnerTick updates all active spinner animations.
936-
// Uses a SINGLE tick chain - all spinners share the same tick rate.
937-
func (m model) handleRandomSpinnerTick(msg ui.TickMsg) (tea.Model, tea.Cmd) {
935+
// handleAnimationTick updates all UI animations: logo reveal and loading spinners.
936+
// Uses a SINGLE tick chain - all animations share the same 70ms tick rate.
937+
func (m model) handleAnimationTick(msg ui.TickMsg) (tea.Model, tea.Cmd) {
938+
// Logo animation (main view, one-time)
939+
logoAnimating := false
940+
if m.currentView == viewMain && m.animatedLogo != nil && !m.animatedLogo.IsComplete() {
941+
m.animatedLogo.Tick()
942+
logoAnimating = true
943+
}
944+
938945
// Check if any spinner needs to be animated
939-
needsTick := m.mainViewLoading || m.liveViewLoading || m.statsViewLoading || m.polling
946+
spinnersActive := m.mainViewLoading || m.liveViewLoading || m.statsViewLoading || m.polling
940947

941-
if !needsTick {
942-
// No spinners active - don't continue the tick chain
948+
if !logoAnimating && !spinnersActive {
949+
// No animations active - don't continue the tick chain
943950
return m, nil
944951
}
945952

internal/app/view.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (m model) View() string {
2222

2323
switch m.currentView {
2424
case viewMain:
25-
return ui.RenderMainMenu(m.width, m.height, m.selected, m.spinner, m.randomSpinner, m.mainViewLoading, m.getStatusBannerType())
25+
return ui.RenderMainMenu(m.width, m.height, m.selected, m.spinner, m.randomSpinner, m.mainViewLoading, m.getStatusBannerType(), m.animatedLogo)
2626

2727
case viewLiveMatches:
2828
m.ensureLiveListSize()
@@ -67,7 +67,7 @@ func (m model) View() string {
6767
return ui.RenderSettingsView(m.width, m.height, m.settingsState, m.getStatusBannerType())
6868

6969
default:
70-
return ui.RenderMainMenu(m.width, m.height, m.selected, m.spinner, m.randomSpinner, m.mainViewLoading, m.getStatusBannerType())
70+
return ui.RenderMainMenu(m.width, m.height, m.selected, m.spinner, m.randomSpinner, m.mainViewLoading, m.getStatusBannerType(), m.animatedLogo)
7171
}
7272
}
7373

internal/constants/strings.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ const (
99

1010
// Panel titles
1111
const (
12-
PanelLiveMatches = "Live Matches"
13-
PanelFinishedMatches = "Finished Matches"
14-
PanelMinuteByMinute = "Minute-by-minute"
15-
PanelMatchStatistics = "Match Statistics"
16-
PanelUpdates = "Updates"
12+
PanelLiveMatches = "Live Matches"
13+
PanelFinishedMatches = "Finished Matches"
14+
PanelMatchDetails = "Match Details"
15+
PanelMatchList = "Match List"
16+
PanelUpcomingMatches = "Upcoming Matches"
17+
PanelMinuteByMinute = "Minute-by-minute"
18+
PanelMatchStatistics = "Match Statistics"
19+
PanelUpdates = "Updates"
20+
PanelLeaguePreferences = "League Preferences"
1721
)
1822

1923
// Empty state messages

internal/ui/design/header.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Package design provides reusable UI design components.
2+
package design
3+
4+
import (
5+
"fmt"
6+
"strings"
7+
8+
"github.com/charmbracelet/lipgloss"
9+
"github.com/lucasb-eyer/go-colorful"
10+
)
11+
12+
const diag = `╱`
13+
14+
// RenderHeader renders a header with gradient text followed by diagonal fill.
15+
// text is the header text to display.
16+
// width is the total width to fill.
17+
// Returns a styled header string with gradient text and diagonal lines.
18+
func RenderHeader(text string, width int) string {
19+
return renderHeaderWithFocus(text, width, true)
20+
}
21+
22+
// RenderHeaderDim renders a dimmed header for unfocused state.
23+
// text is the header text to display.
24+
// width is the total width to fill.
25+
func RenderHeaderDim(text string, width int) string {
26+
return renderHeaderWithFocus(text, width, false)
27+
}
28+
29+
// renderHeaderWithFocus renders header with gradient or dim styling based on focus.
30+
func renderHeaderWithFocus(text string, width int, focused bool) string {
31+
startHex, endHex := AdaptiveGradientColors()
32+
33+
var title string
34+
var diagColor string
35+
36+
if focused {
37+
title = applyHeaderGradient(text, startHex, endHex)
38+
diagColor = startHex
39+
} else {
40+
// Dim style for unfocused
41+
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#666666", Dark: "#555555"}).Bold(true)
42+
title = dimStyle.Render(text)
43+
diagColor = "#555555"
44+
}
45+
46+
remainingWidth := width - lipgloss.Width(text) - 2
47+
if remainingWidth > 0 {
48+
lines := strings.Repeat(diag, remainingWidth)
49+
styledLines := lipgloss.NewStyle().Foreground(lipgloss.Color(diagColor)).Render(lines)
50+
title = fmt.Sprintf("%s %s", title, styledLines)
51+
}
52+
return title
53+
}
54+
55+
// RenderHeaderCentered renders a header with diagonal fills on both sides.
56+
// text is the header text to display centered.
57+
// width is the total width to fill.
58+
func RenderHeaderCentered(text string, width int) string {
59+
startHex, endHex := AdaptiveGradientColors()
60+
title := applyHeaderGradient(text, startHex, endHex)
61+
62+
textWidth := lipgloss.Width(text)
63+
remainingWidth := width - textWidth - 2 // 2 for spaces around text
64+
if remainingWidth <= 0 {
65+
return title
66+
}
67+
68+
leftWidth := remainingWidth / 2
69+
rightWidth := remainingWidth - leftWidth
70+
71+
leftLines := strings.Repeat(diag, leftWidth)
72+
rightLines := strings.Repeat(diag, rightWidth)
73+
74+
styledLeft := lipgloss.NewStyle().Foreground(lipgloss.Color(startHex)).Render(leftLines)
75+
styledRight := lipgloss.NewStyle().Foreground(lipgloss.Color(endHex)).Render(rightLines)
76+
77+
return fmt.Sprintf("%s %s %s", styledLeft, title, styledRight)
78+
}
79+
80+
// applyHeaderGradient applies a gradient to a single line of text.
81+
func applyHeaderGradient(text string, startHex, endHex string) string {
82+
startColor, err1 := colorful.Hex(startHex)
83+
endColor, err2 := colorful.Hex(endHex)
84+
if err1 != nil || err2 != nil {
85+
return text
86+
}
87+
88+
runes := []rune(text)
89+
if len(runes) == 0 {
90+
return text
91+
}
92+
93+
var result strings.Builder
94+
for i, char := range runes {
95+
if char == ' ' {
96+
result.WriteRune(' ')
97+
continue
98+
}
99+
ratio := float64(i) / float64(max(len(runes)-1, 1))
100+
color := startColor.BlendLab(endColor, ratio)
101+
hexColor := color.Hex()
102+
charStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(hexColor)).Bold(true)
103+
result.WriteString(charStyle.Render(string(char)))
104+
}
105+
106+
return result.String()
107+
}

internal/ui/dialog_statistics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (d *StatisticsDialog) View(width, height int) string {
7070
// Build the content
7171
content := d.renderContent(dialogWidth - 6) // Account for padding and border
7272

73-
return RenderDialogFrameWithHelp("Match Statistics", content, constants.HelpStatisticsDialog, dialogWidth, dialogHeight)
73+
return RenderDialogFrameWithHelp(constants.PanelMatchStatistics, content, constants.HelpStatisticsDialog, dialogWidth, dialogHeight)
7474
}
7575

7676
// renderContent renders the statistics content.

internal/ui/dialog_styles.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ui
33
import (
44
"strings"
55

6+
"github.com/0xjuanma/golazo/internal/ui/design"
67
"github.com/charmbracelet/lipgloss"
78
)
89

@@ -118,7 +119,7 @@ func DialogBadgeHighlight(value string) string {
118119

119120
// RenderDialogFrame wraps content in a dialog frame with title bar.
120121
func RenderDialogFrame(title, content string, width, height int) string {
121-
titleBar := RenderDialogTitleBar(title, width-6) // Account for border and padding
122+
titleBar := design.RenderHeader(title, width-6) // Use compact header with gradient
122123

123124
innerContent := lipgloss.JoinVertical(lipgloss.Left, titleBar, "", content)
124125

@@ -132,7 +133,7 @@ func RenderDialogFrame(title, content string, width, height int) string {
132133

133134
// RenderDialogFrameWithHelp wraps content in a dialog frame with title bar and help text.
134135
func RenderDialogFrameWithHelp(title, content, help string, width, height int) string {
135-
titleBar := RenderDialogTitleBar(title, width-6) // Account for border and padding
136+
titleBar := design.RenderHeader(title, width-6) // Use compact header with gradient
136137
helpRendered := dialogHelpStyle.Width(width - 6).Align(lipgloss.Center).Render(help)
137138

138139
innerContent := lipgloss.JoinVertical(lipgloss.Left, titleBar, "", content, helpRendered)

internal/ui/list_panels.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/0xjuanma/golazo/internal/api"
88
"github.com/0xjuanma/golazo/internal/constants"
9+
"github.com/0xjuanma/golazo/internal/ui/design"
910
"github.com/charmbracelet/bubbles/list"
1011
"github.com/charmbracelet/bubbles/spinner"
1112
"github.com/charmbracelet/bubbles/viewport"
@@ -38,7 +39,7 @@ func (g GoalLinksMap) GetReplayURL(matchID, minute int) string {
3839
func RenderLiveMatchesListPanel(width, height int, listModel list.Model, upcomingMatches []MatchDisplay) string {
3940
contentWidth := width - 6
4041

41-
title := neonPanelTitleStyle.Width(contentWidth).Render(constants.PanelLiveMatches)
42+
title := design.RenderHeader(constants.PanelLiveMatches, contentWidth)
4243

4344
var listView string
4445
if len(listModel.Items()) == 0 {
@@ -56,7 +57,7 @@ func RenderLiveMatchesListPanel(width, height int, listModel list.Model, upcomin
5657
if len(upcomingMatches) > 0 {
5758
maxUpcomingHeight := innerHeight / 2
5859

59-
upcomingTitle := neonHeaderStyle.Render("Upcoming")
60+
upcomingTitle := design.RenderHeader(constants.PanelUpcomingMatches, contentWidth)
6061

6162
var upcomingLines []string
6263
upcomingLines = append(upcomingLines, upcomingTitle)
@@ -126,17 +127,9 @@ func renderUpcomingMatchLine(match MatchDisplay, maxWidth int) string {
126127
func RenderStatsListPanel(width, height int, finishedList list.Model, dateRange int, rightPanelFocused bool) string {
127128
var header string
128129
if rightPanelFocused {
129-
header = lipgloss.NewStyle().
130-
Foreground(neonDim).
131-
Bold(true).
132-
PaddingBottom(0).
133-
BorderBottom(true).
134-
BorderStyle(lipgloss.NormalBorder()).
135-
BorderForeground(neonDim).
136-
MarginBottom(0).
137-
Render("Match List")
130+
header = design.RenderHeaderDim(constants.PanelMatchList, width-6)
138131
} else {
139-
header = neonHeaderStyle.Render("Match List")
132+
header = design.RenderHeader(constants.PanelMatchList, width-6)
140133
}
141134

142135
dateSelector := renderDateRangeSelector(width-6, dateRange)

0 commit comments

Comments
 (0)