diff --git a/internal/client/ui/auth/auth.go b/internal/client/ui/auth/auth.go index 647d238..9444491 100644 --- a/internal/client/ui/auth/auth.go +++ b/internal/client/ui/auth/auth.go @@ -20,13 +20,16 @@ var ( errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F16265")) fieldStyle = lipgloss.NewStyle(). PaddingLeft(1). - BorderStyle(lipgloss.RoundedBorder()). + Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("#007E8A")) - focusedButton = focusedStyle.Render("[ Submit ]") + focusedButton = focusedStyle.Render("[ sign-up ]") blurredButton = fmt.Sprintf("[ %s ]", lipgloss.NewStyle(). - Foreground(lipgloss.Color("240")). - Render("Submit")) + Foreground(lipgloss.Color("240")). + Render("sign-up")) + + headerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#54D7A9")) + titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#5874FF")) ) type Model struct { @@ -53,33 +56,56 @@ func New() Model { switch i { case 0: + field.Header = "Username" field.Input.Placeholder = "Username" field.Focus() + field.Input.Validate = func(username string) error { + if len(username) == 0 { + return errors.New("* required field") + } + return nil + } case 1: + field.Header = "Email (Optional)" field.Input.Placeholder = "Email" field.Input.CharLimit = 64 field.Input.Validate = func(email string) error { + if len(email) == 0 { + return errors.New("* required field") + } + var errs []string if !strings.ContainsRune(email, '@') { - return errors.New("missing @") + errs = append(errs, "- missing @") } if !strings.ContainsRune(email, '.') { - return errors.New("missing .") + errs = append(errs, "- missing .") } proton := strings.Contains(email, "proton.me") || strings.Contains(email, "protonmail.com") gmail := strings.Contains(email, "gmail.com") outlook := strings.Contains(email, "hotmail.com") if !(proton || gmail || outlook) { - return errors.New("unknown email provider") + errs = append(errs, "- unknown email provider") + } + if errs != nil { + return errors.New(strings.Join(errs, "\n")) } return nil } case 2: + field.Header = "Password" field.Input.Placeholder = "Password" field.Input.EchoMode = textinput.EchoPassword field.Input.EchoCharacter = '*' // field.Input.EchoCharacter = '•' + field.Input.Validate = func(password string) error { + if len(password) == 0 { + return errors.New("* required field") + } + return nil + } } + field.Header = headerStyle.Render(field.Header) m.fields[i] = field } @@ -93,6 +119,7 @@ func (m Model) Init() tea.Cmd { func (m Model) View() string { var builder strings.Builder + builder.WriteRune('\n') for i := range m.fields { builder.WriteString(m.fields[i].View()) if i < len(m.fields)-1 { @@ -105,19 +132,26 @@ func (m Model) View() string { button = &focusedButton } submit := lipgloss.NewStyle(). - PaddingLeft((lipgloss.Width(builder.String()) - lipgloss.Width(*button)) / 2). Render(*button) fmt.Fprintf(&builder, "\n\n%s", submit) - width, height := lipgloss.Width(builder.String()), lipgloss.Height(builder.String()) + title := titleStyle.Render(` +____ _ ____ _ _ _ _ ___ +[__ | | __ |\ | __ | | |__] +___] | |__] | \| |__| | + `) + + // Tiny offset so odd numbers will have the extra char on the right, ie: 12 13 + content := lipgloss.JoinVertical(lipgloss.Center-0.01, title, builder.String()) + width := lipgloss.Width(content) vp := viewport.New(50, 25) - vp.SetContent(builder.String()) + vp.SetContent(content) vp.Style = vp.Style. Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#0000FF")). + BorderForeground(lipgloss.Color("#007E8A")). // NOTE: Without -1 it wraps/truncates, not sure why - Padding((vp.Height-height)/2-1, (vp.Width-width)/2-1) + Padding(0, (vp.Width-width)/2-1) return lipgloss.Place( m.width, m.height, @@ -141,7 +175,22 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyTab, tea.KeyShiftTab, tea.KeyEnter, tea.KeyUp, tea.KeyDown: // User pressed submit if key == tea.KeyEnter && m.focusIndex == len(m.fields) { - return m, tea.Quit + var cmds []tea.Cmd + for i, field := range m.fields { + if field.Input.Validate == nil { + continue + } + field.Input.Err = field.Input.Validate(field.Input.Value()) + if field.Input.Err != nil { + var cmd tea.Cmd + m.fields[i], cmd = field.Update(msg) + cmds = append(cmds, cmd) + } + } + if len(cmds) == 0 { + return m, tea.Quit + } + return m, tea.Batch(cmds...) } // Cycle indexes diff --git a/internal/client/ui/auth/field/field.go b/internal/client/ui/auth/field/field.go index afc55b1..623cdd6 100644 --- a/internal/client/ui/auth/field/field.go +++ b/internal/client/ui/auth/field/field.go @@ -1,20 +1,20 @@ package field import ( - "strings" - "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + + "github.com/kyren223/eko/internal/client/ui" ) - type Model struct { - Input textinput.Model - Style lipgloss.Style + Input textinput.Model + Style lipgloss.Style FocusedStyle lipgloss.Style BlurredStyle lipgloss.Style - ErrorStyle lipgloss.Style + ErrorStyle lipgloss.Style + Header string } func New(width int) Model { @@ -37,15 +37,18 @@ func (m Model) Init() tea.Cmd { } func (m Model) View() string { - var builder strings.Builder - - builder.WriteString(m.Style.Render(m.Input.View())) - builder.WriteRune('\n') + style := m.Style + error := "" if m.Input.Err != nil { - builder.WriteString(m.ErrorStyle.Render(m.Input.Err.Error())) + error = m.Input.Err.Error() + style = style.BorderForeground(m.ErrorStyle.GetForeground()) } + field := ui.AddBorderHeader(m.Header, 1, style, m.Input.View()) - return builder.String() + error = m.ErrorStyle.MaxWidth(lipgloss.Width(field)).Render(error) + + // return lipgloss.JoinVertical(lipgloss.Left, borderTop, field, error) + return lipgloss.JoinVertical(lipgloss.Left, field, error) } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { diff --git a/internal/client/ui/ui.go b/internal/client/ui/ui.go index 425e6b1..b03df59 100644 --- a/internal/client/ui/ui.go +++ b/internal/client/ui/ui.go @@ -1,7 +1,41 @@ package ui -import tea "github.com/charmbracelet/bubbletea" +import ( + "fmt" + "strings" -type ModelTransition struct{ + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type ModelTransition struct { Model tea.Model } + +func AddBorderHeader(header string, headerOffset int, style lipgloss.Style, render string) string { + b := style.GetBorderStyle() + body := style.UnsetBorderTop().Render(render) + + bodyWidth, headerWidth := lipgloss.Width(body), lipgloss.Width(header) + leftCornerWidth, rightCornerWidth := lipgloss.Width(b.TopLeft), lipgloss.Width(b.TopRight) + topWidth := bodyWidth - leftCornerWidth - rightCornerWidth + + leftWidth := headerOffset + rightWidth := topWidth - leftWidth - headerWidth + + topStyle := lipgloss.NewStyle(). + Background(style.GetBorderTopBackground()). + Foreground(style.GetBorderTopForeground()) + + left := b.TopLeft + strings.Repeat(b.Top, leftWidth) + right := topStyle.Render(strings.Repeat(b.Top, rightWidth) + b.TopRight) + + borderTop := lipgloss.NewStyle(). + Inline(true). + MaxWidth(bodyWidth). + Background(style.GetBorderTopBackground()). + Foreground(style.GetBorderTopForeground()). + Render(fmt.Sprintf("%s%s%s", left, header, right)) + + return lipgloss.JoinVertical(lipgloss.Left, borderTop, body) +}