mirror of
https://github.com/Kyren223/eko.git
synced 2026-07-14 16:00:32 +00:00
feat: more UI design
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user