feat: more UI design

This commit is contained in:
2024-11-06 00:02:02 +02:00
parent bdf7d2c2d2
commit 68635d4963
3 changed files with 113 additions and 27 deletions

View File

@@ -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 <thing> 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

View File

@@ -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) {

View File

@@ -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)
}