refactor(ui): simplify list component and add markdown rendering

Ayman Bagabas created

Change summary

internal/ui/list/items.go     | 139 +++++++-------
internal/ui/list/list.go      | 348 ++++++++++++++----------------------
internal/ui/list/list_test.go | 162 +++++------------
internal/ui/list/styles.go    |  25 --
4 files changed, 253 insertions(+), 421 deletions(-)

Detailed changes

internal/ui/list/items.go 🔗

@@ -1,101 +1,102 @@
 package list
 
-import "strings"
+import (
+	"io"
 
-// RenderedItem represents a rendered item as a string.
-type RenderedItem interface {
-	Item
-	// Height returns the height of the rendered item in lines.
-	Height() int
-}
+	"charm.land/lipgloss/v2"
+	"github.com/charmbracelet/crush/internal/ui/common"
+	"github.com/charmbracelet/glamour/v2"
+	"github.com/charmbracelet/glamour/v2/ansi"
+)
 
-// Item represents a single item in the [List] component.
+// Item represents a rendered item in the [List] component.
 type Item interface {
-	// ID returns the unique identifier of the item.
-	ID() string
-	// Render returns the rendered string representation of the item.
-	Render() string
+	// Content is the rendered content of the item.
+	Content() string
+	// Height returns the height of the item based on its content.
+	Height() int
 }
 
-// StringItem is a simple implementation of the [Item] interface that holds a
-// string.
-type StringItem struct {
-	ItemID  string
-	Content string
-}
+// Gap is [GapItem] to be used as a vertical gap in the list.
+var Gap = GapItem{}
 
-// NewStringItem creates a new StringItem with the given ID and content.
-func NewStringItem(id, content string) StringItem {
-	return StringItem{
-		ItemID:  id,
-		Content: content,
-	}
-}
+// GapItem represents a vertical gap in the list.
+type GapItem struct{}
 
-// ID returns the unique identifier of the string item.
-func (s StringItem) ID() string {
-	return s.ItemID
+// Content returns the content of the gap item.
+func (g GapItem) Content() string {
+	return ""
 }
 
-// Render returns the rendered string representation of the string item.
-func (s StringItem) Render() string {
-	return s.Content
+// Height returns the height of the gap item.
+func (g GapItem) Height() int {
+	return 1
 }
 
-// Gap is [GapItem] to be used as a vertical gap in the list.
-var Gap = GapItem{}
-
-// GapItem is a one-line vertical gap in the list.
-type GapItem struct{}
+// StringItem represents a simple string item in the list.
+type StringItem struct {
+	content string
+}
 
-// ID returns the unique identifier of the gap.
-func (g GapItem) ID() string {
-	return "gap"
+// NewStringItem creates a new [StringItem] with the given id and content.
+func NewStringItem(content string) StringItem {
+	return StringItem{
+		content: content,
+	}
 }
 
-// Render returns the rendered string representation of the gap.
-func (g GapItem) Render() string {
-	return "\n"
+// Content returns the content of the string item.
+func (s StringItem) Content() string {
+	return s.content
 }
 
-// Height returns the height of the rendered gap in lines.
-func (g GapItem) Height() int {
-	return 1
+// Height returns the height of the string item based on its content.
+func (s StringItem) Height() int {
+	return lipgloss.Height(s.content)
 }
 
-// CachedItem wraps an Item and caches its rendered string representation and height.
-type CachedItem struct {
-	item     Item
-	rendered string
-	height   int
+// MarkdownItem represents a markdown item in the list.
+type MarkdownItem struct {
+	StringItem
 }
 
-// NewCachedItem creates a new CachedItem from the given Item.
-func NewCachedItem(item Item, rendered string) CachedItem {
-	height := 1 + strings.Count(rendered, "\n")
-	return CachedItem{
-		item:     item,
-		rendered: rendered,
-		height:   height,
+// NewMarkdownItem creates a new [MarkdownItem] with the given id and content.
+func NewMarkdownItem(id, content string) MarkdownItem {
+	return MarkdownItem{
+		StringItem: StringItem{
+			content: content,
+		},
 	}
 }
 
-// ID returns the unique identifier of the cached item.
-func (c CachedItem) ID() string {
-	return c.item.ID()
+// Content returns the content of the markdown item.
+func (m MarkdownItem) Content() string {
+	return m.StringItem.Content()
 }
 
-// Item returns the underlying Item.
-func (c CachedItem) Item() Item {
-	return c.item
+// Height returns the height of the markdown item based on its content.
+func (m MarkdownItem) Height() int {
+	return m.StringItem.Height()
 }
 
-// Render returns the cached rendered string representation of the item.
-func (c CachedItem) Render() string {
-	return c.rendered
+// MarkdownItemMaxWidth is the maximum width for rendering markdown items.
+const MarkdownItemMaxWidth = 120
+
+// MarkdownItemRenderer renders [MarkdownItem]s in a [List].
+type MarkdownItemRenderer struct {
+	Styles *ansi.StyleConfig
 }
 
-// Height returns the cached height of the rendered item in lines.
-func (c CachedItem) Height() int {
-	return c.height
+// Render implements [ItemRenderer].
+func (m *MarkdownItemRenderer) Render(w io.Writer, list *List, index int, item Item) {
+	width := min(list.Width(), MarkdownItemMaxWidth)
+	var r *glamour.TermRenderer
+	if m.Styles != nil {
+		r = common.MarkdownRenderer(*m.Styles, width)
+	} else {
+		r = common.PlainMarkdownRenderer(width)
+	}
+
+	rendered, _ := r.Render(item.Content())
+	_, _ = io.WriteString(w, rendered)
 }

internal/ui/list/list.go 🔗

@@ -2,97 +2,64 @@
 package list
 
 import (
-	"image"
+	"io"
 	"slices"
 	"strings"
 
-	"charm.land/lipgloss/v2"
-	"github.com/charmbracelet/x/exp/ordered"
-	lru "github.com/hashicorp/golang-lru/v2"
+	uv "github.com/charmbracelet/ultraviolet"
 )
 
-// List represents a component that display a list of [Item]s.
-type List struct {
-	// idx is the current focused index in the list. -1 means no item is focused.
-	idx int
-
-	items []Item
-
-	// yOffset is the current vertical offset for scrolling.
-	yOffset int
+// ItemRenderer is an interface for rendering items in the list.
+type ItemRenderer interface {
+	// Render renders the item as a string.
+	Render(w io.Writer, l *List, index int, item Item)
+}
 
-	// linesCount is the cached total number of rendered lines in the list.
-	linesCount int
+// DefaultItemRenderer is the default implementation of [ItemRenderer].
+type DefaultItemRenderer struct{}
 
-	// rect is the bounding rectangle of the list.
-	rect image.Rectangle
+// Render renders the item as a string using its content.
+func (r *DefaultItemRenderer) Render(w io.Writer, list *List, index int, item Item) {
+	_, _ = io.WriteString(w, item.Content())
+}
 
-	// reverse indicates if the list is in reverse order.
-	reverse bool
+// List represents a component that renders a list of [Item]s via
+// [ItemRenderer]s. It supports focus management and styling.
+type List struct {
+	// items is the master list of items.
+	items []Item
 
-	// hasFocus indicates if the list has focus.
-	hasFocus bool
+	// rend is the item renderer for the list.
+	rend ItemRenderer
 
-	styles Styles
+	// width is the width of the list.
+	width int
 
-	cache *lru.Cache[string, RenderedItem]
+	// yOffset is the vertical scroll offset. -1 means scrolled to bottom.
+	yOffset int
 }
 
 // New creates a new [List] component with the given items.
-func New(items ...Item) *List {
-	cache, _ := lru.New[string, RenderedItem](256)
+func New(rend ItemRenderer, items ...Item) *List {
+	if rend == nil {
+		rend = &DefaultItemRenderer{}
+	}
 	l := &List{
-		idx:    -1,
-		items:  items,
-		styles: DefaultStyles(),
-		cache:  cache,
+		rend:    rend,
+		yOffset: -1,
 	}
+	l.Append(items...)
 	return l
 }
 
-// SetStyles sets the styles for the list.
-func (l *List) SetStyles(s Styles) {
-	l.styles = s
-}
-
-// SetReverse sets the reverse order of the list.
-func (l *List) SetReverse(reverse bool) {
-	l.reverse = reverse
-}
-
-// IsReverse returns true if the list is in reverse order.
-func (l *List) IsReverse() bool {
-	return l.reverse
-}
-
-// SetBounds sets the bounding rectangle of the list.
-func (l *List) SetBounds(rect image.Rectangle) {
-	if l.rect.Dx() != rect.Dx() {
-		// Clear the cache if the width has changed. This is necessary because
-		// the rendered items are wrapped to the width of the list.
-		l.cache.Purge()
-	}
-	l.rect = rect
+// SetWidth sets the width of the list.
+func (l *List) SetWidth(width int) {
+	l.width = width
 }
 
 // Width returns the width of the list.
 func (l *List) Width() int {
-	return l.rect.Dx()
-}
-
-// Height returns the height of the list.
-func (l *List) Height() int {
-	return l.rect.Dy()
-}
-
-// X returns the X position of the list.
-func (l *List) X() int {
-	return l.rect.Min.X
-}
-
-// Y returns the Y position of the list.
-func (l *List) Y() int {
-	return l.rect.Min.Y
+	return l.width
 }
 
 // Len returns the number of items in the list.
@@ -100,7 +67,7 @@ func (l *List) Len() int {
 	return len(l.items)
 }
 
-// Items returns the items in the list.
+// Items returns a new slice of all items in the list.
 func (l *List) Items() []Item {
 	return l.items
 }
@@ -136,186 +103,139 @@ func (l *List) Append(items ...Item) {
 	l.items = append(l.items, items...)
 }
 
-// Focus focuses the list
-func (l *List) Focus() {
-	l.hasFocus = true
-	if l.idx < 0 && len(l.items) > 0 {
-		l.FocusFirst()
-	}
+// GotoBottom scrolls the list to the bottom.
+func (l *List) GotoBottom() {
+	l.yOffset = -1
 }
 
-// FocusFirst focuses the first item in the list.
-func (l *List) FocusFirst() {
-	if !l.hasFocus {
-		l.Focus()
-	}
-	if l.reverse {
-		l.idx = len(l.items) - 1
-		return
-	}
-	l.idx = 0
+// GotoTop scrolls the list to the top.
+func (l *List) GotoTop() {
+	l.yOffset = 0
 }
 
-// FocusLast focuses the last item in the list.
-func (l *List) FocusLast() {
-	if !l.hasFocus {
-		l.Focus()
-	}
-	if l.reverse {
-		l.idx = 0
-		return
+// TotalHeight returns the total height of all items in the list.
+func (l *List) TotalHeight() int {
+	total := 0
+	for _, item := range l.items {
+		total += item.Height()
 	}
-	l.idx = len(l.items) - 1
+	return total
 }
 
-// focus moves the focus by n offset. Positive n moves down, negative n moves up.
-func (l *List) focus(n int) {
-	if l.reverse {
-		n = -n
+// ScrollUp scrolls the list up by the given number of lines.
+func (l *List) ScrollUp(lines int) {
+	if l.yOffset == -1 {
+		// Calculate total height
+		totalHeight := l.TotalHeight()
+		l.yOffset = totalHeight
 	}
-
-	if n < 0 {
-		if l.idx+n < 0 {
-			l.idx = 0
-		} else {
-			l.idx += n
-		}
-	} else if n > 0 {
-		if l.idx+n >= len(l.items) {
-			l.idx = len(l.items) - 1
-		} else {
-			l.idx += n
-		}
+	l.yOffset -= lines
+	if l.yOffset < 0 {
+		l.yOffset = 0
 	}
 }
 
-// FocusNext focuses the next item in the list.
-func (l *List) FocusNext() {
-	if !l.hasFocus {
-		l.Focus()
+// ScrollDown scrolls the list down by the given number of lines.
+func (l *List) ScrollDown(lines int) {
+	if l.yOffset == -1 {
+		// Already at bottom
+		return
 	}
-	l.focus(1)
-}
-
-// FocusPrev focuses the previous item in the list.
-func (l *List) FocusPrev() {
-	if !l.hasFocus {
-		l.Focus()
+	l.yOffset += lines
+	totalHeight := l.TotalHeight()
+	if l.yOffset >= totalHeight {
+		l.yOffset = -1 // Scroll to bottom
 	}
-	l.focus(-1)
 }
 
-// FocusedItem returns the currently focused item.
-func (l *List) FocusedItem() (Item, bool) {
-	return l.At(l.idx)
-}
-
-// Blur removes focus from the list.
-func (l *List) Blur() {
-	l.hasFocus = false
+// YOffset returns the current vertical scroll offset.
+func (l *List) YOffset() int {
+	if l.yOffset == -1 {
+		return l.TotalHeight()
+	}
+	return l.yOffset
 }
 
-// ScrollUp scrolls the list up by n lines.
-func (l *List) ScrollUp(n int) {
-	l.scroll(-n)
+// Render renders the whole list as a string.
+func (l *List) Render() string {
+	return l.RenderRange(0, len(l.items))
 }
 
-// ScrollDown scrolls the list down by n lines.
-func (l *List) ScrollDown(n int) {
-	l.scroll(n)
+// Draw draws the list to the given [uv.Screen] in the specified area.
+func (l *List) Draw(scr uv.Screen, area uv.Rectangle) {
+	yOffset := l.YOffset()
+	rendered := l.RenderLines(yOffset, yOffset+area.Dy())
+	uv.NewStyledString(rendered).Draw(scr, area)
 }
 
-// scroll scrolls the list by n lines. Positive n scrolls down, negative n scrolls up.
-func (l *List) scroll(n int) {
-	if l.reverse {
-		n = -n
-	}
-
-	if n > 0 {
-		l.yOffset += n
-		if l.linesCount > l.Height() && l.yOffset > l.linesCount-l.Height() {
-			l.yOffset = l.linesCount - l.Height()
+// RenderRange renders a range of items from start to end indices.
+func (l *List) RenderRange(start, end int) string {
+	var b strings.Builder
+	for i := start; i < end && i < len(l.items); i++ {
+		item, ok := l.At(i)
+		if !ok {
+			continue
 		}
-	} else if n < 0 {
-		l.yOffset += n
-		if l.yOffset < 0 {
-			l.yOffset = 0
+
+		l.rend.Render(&b, l, i, item)
+		if i < end-1 && i < len(l.items)-1 {
+			b.WriteString("\n")
 		}
 	}
+
+	return b.String()
 }
 
-// Render renders the first n items that fit within the list's height and
-// returns the rendered string.
-func (l *List) Render() string {
-	var rendered []string
-	availableHeight := l.Height()
-	i := 0
-	if l.reverse {
-		i = len(l.items) - 1
-	}
+// RenderLines renders the list based on the start and end y offsets.
+func (l *List) RenderLines(startY, endY int) string {
+	var b strings.Builder
+	currentY := 0
+	for i := 0; i < len(l.items); i++ {
+		item, ok := l.At(i)
+		if !ok {
+			continue
+		}
 
-	// Render items until we run out of space
-	for i >= 0 && i < len(l.items) {
-		itemStyle := l.styles.NormalItem
-		if l.hasFocus && l.idx == i {
-			itemStyle = l.styles.FocusedItem
+		itemHeight := item.Height()
+		if currentY+itemHeight <= startY {
+			// Skip this item as it's above the startY
+			currentY += itemHeight
+			continue
+		}
+		if currentY >= endY {
+			// Stop rendering as we've reached endY
+			break
 		}
 
-		listWidth := l.Width() - itemStyle.GetHorizontalFrameSize()
+		// Render the item to a temporary buffer if needed
+		if currentY < startY || currentY+itemHeight > endY {
+			var tempBuf strings.Builder
+			l.rend.Render(&tempBuf, l, i, item)
+			lines := strings.Split(tempBuf.String(), "\n")
 
-		item, ok := l.At(i)
-		if ok {
-			cachedItem, ok := l.cache.Get(item.ID())
-			if !ok {
-				renderedItem := lipgloss.Wrap(item.Render(), listWidth, "")
-				cachedItem = NewCachedItem(item, renderedItem)
-				l.cache.Add(item.ID(), cachedItem)
+			// Calculate the visible lines
+			startLine := 0
+			if currentY < startY {
+				startLine = startY - currentY
+			}
+			endLine := itemHeight
+			if currentY+itemHeight > endY {
+				endLine = endY - currentY
 			}
 
-			renderedString := itemStyle.Render(cachedItem.Render())
-			rendered = append(rendered, renderedString)
-		}
-
-		if l.reverse {
-			i--
+			// Write only the visible lines
+			for j := startLine; j < endLine && j < len(lines); j++ {
+				b.WriteString(lines[j])
+				b.WriteString("\n")
+			}
 		} else {
-			i++
+			// Render the whole item directly
+			l.rend.Render(&b, l, i, item)
+			b.WriteString("\n")
 		}
-	}
 
-	if l.reverse {
-		slices.Reverse(rendered)
+		currentY += itemHeight
 	}
 
-	var sb strings.Builder
-	for i, item := range rendered {
-		sb.WriteString(item)
-		if i < len(rendered)-1 {
-			sb.WriteString("\n")
-		}
-	}
-
-	linesCount := strings.Count(sb.String(), "\n") + 1
-	l.linesCount = linesCount
-
-	if linesCount <= availableHeight {
-		return sb.String()
-	}
-
-	lines := strings.Split(sb.String(), "\n")
-	yOffset := ordered.Clamp(l.yOffset, 0, linesCount-availableHeight)
-	if l.reverse {
-		start := len(lines) - availableHeight - yOffset
-		end := max(availableHeight, len(lines)-l.yOffset)
-		return strings.Join(lines[start:end], "\n")
-	}
-
-	start := 0 + yOffset
-	end := min(len(lines), availableHeight+yOffset)
-	return strings.Join(lines[start:end], "\n")
-}
-
-// View returns the rendered view of the list.
-func (l *List) View() string {
-	return l.Render()
+	return strings.TrimRight(b.String(), "\n")
 }

internal/ui/list/list_test.go 🔗

@@ -1,154 +1,90 @@
 package list
 
 import (
-	"image"
 	"testing"
 )
 
 func TestNewList(t *testing.T) {
 	items := []Item{
-		NewStringItem("1", "Item 1"),
-		NewStringItem("2", "Item 2"),
-		NewStringItem("3", "Item 3"),
+		NewStringItem("Item 1"),
+		NewStringItem("Item 2"),
+		NewStringItem("Item 3"),
 	}
 
-	bounds := image.Rect(0, 0, 10, 5)
-	list := New(bounds, items...)
+	var defaultRend DefaultItemRenderer
+	list := New(&defaultRend, items...)
 
-	if list.Count() != len(items) {
-		t.Errorf("expected list count %d, got %d", len(items), list.Count())
+	if list.Len() != len(items) {
+		t.Errorf("expected list count %d, got %d", len(items), list.Len())
 	}
 
-	for i, item := range items {
-		gotItem, ok := list.At(i)
-		if !ok {
-			t.Errorf("expected item at index %d to exist", i)
-			continue
-		}
-		if gotItem.ID() != item.ID() {
-			t.Errorf("expected item ID %s, got %s", item.ID(), gotItem.ID())
-		}
+	rendered := list.Render()
+	expected := "Item 1\nItem 2\nItem 3"
+	if rendered != expected {
+		t.Errorf("expected rendered output:\n%s\ngot:\n%s", expected, rendered)
 	}
 }
 
 func TestListAppend(t *testing.T) {
-	bounds := image.Rect(0, 0, 10, 5)
-	list := New(bounds)
-
-	newItems := []Item{
-		NewStringItem("1", "Item A"),
-		NewStringItem("2", "Item B"),
-	}
+	var defaultRend DefaultItemRenderer
+	list := New(&defaultRend,
+		NewStringItem("Item 1"),
+	)
 
-	list.Append(newItems...)
+	list.Append(
+		NewStringItem("Item 2"),
+		NewStringItem("Item 3"),
+	)
 
-	if list.Count() != len(newItems) {
-		t.Errorf("expected list count %d, got %d", len(newItems), list.Count())
+	if list.Len() != 3 {
+		t.Errorf("expected list count 3, got %d", list.Len())
 	}
 
-	for i, item := range newItems {
-		gotItem, ok := list.At(i)
-		if !ok {
-			t.Errorf("expected item at index %d to exist", i)
-			continue
-		}
-		if gotItem.ID() != item.ID() {
-			t.Errorf("expected item ID %s, got %s", item.ID(), gotItem.ID())
-		}
+	rendered := list.Render()
+	expected := "Item 1\nItem 2\nItem 3"
+	if rendered != expected {
+		t.Errorf("expected rendered output:\n%s\ngot:\n%s", expected, rendered)
 	}
 }
 
 func TestListUpdate(t *testing.T) {
-	items := []Item{
-		NewStringItem("1", "Old Item 1"),
-		NewStringItem("2", "Old Item 2"),
-	}
-
-	bounds := image.Rect(0, 0, 10, 5)
-	list := New(bounds, items...)
-
-	updatedItem := NewStringItem("1", "New Item 1")
-	success := list.Update(0, updatedItem)
-	if !success {
+	var defaultRend DefaultItemRenderer
+	list := New(&defaultRend,
+		NewStringItem("Item 1"),
+		NewStringItem("Item 2"),
+	)
+
+	updated := list.Update(1, NewStringItem("Updated Item 2"))
+	if !updated {
 		t.Errorf("expected update to succeed")
 	}
 
-	gotItem, ok := list.At(0)
-	if !ok {
-		t.Errorf("expected item at index 0 to exist")
-	} else if gotItem.ID() != updatedItem.ID() {
-		t.Errorf("expected item ID %s, got %s", updatedItem.ID(), gotItem.ID())
-	}
-}
-
-func TestListDelete(t *testing.T) {
-	items := []Item{
-		NewStringItem("1", "Item 1"),
-		NewStringItem("2", "Item 2"),
-		NewStringItem("3", "Item 3"),
-	}
-
-	bounds := image.Rect(0, 0, 10, 5)
-	list := New(bounds, items...)
-
-	success := list.Delete(1)
-	if !success {
-		t.Errorf("expected delete to succeed")
-	}
-
-	if list.Count() != 2 {
-		t.Errorf("expected list count 2, got %d", list.Count())
-	}
-
-	expectedItems := []Item{
-		NewStringItem("1", "Item 1"),
-		NewStringItem("3", "Item 3"),
-	}
-
-	for i, item := range expectedItems {
-		gotItem, ok := list.At(i)
-		if !ok {
-			t.Errorf("expected item at index %d to exist", i)
-			continue
-		}
-		if gotItem.ID() != item.ID() {
-			t.Errorf("expected item ID %s, got %s", item.ID(), gotItem.ID())
-		}
-	}
-}
-
-func TestListRender(t *testing.T) {
-	items := []Item{
-		NewStringItem("1", "Line 1\nLine 2"),
-		NewStringItem("2", "Line 3"),
-		NewStringItem("3", "Line 4\nLine 5\nLine 6"),
-	}
-
-	bounds := image.Rect(0, 0, 10, 4)
-	list := New(bounds, items...)
-
 	rendered := list.Render()
-	expected := "Line 1\nLine 2\nLine 3\n"
-
+	expected := "Item 1\nUpdated Item 2"
 	if rendered != expected {
 		t.Errorf("expected rendered output:\n%s\ngot:\n%s", expected, rendered)
 	}
 }
 
-func TestListRenderReverse(t *testing.T) {
-	items := []Item{
-		NewStringItem("1", "Line 1\nLine 2"),
-		NewStringItem("2", "Line 3"),
-		NewStringItem("3", "Line 4\nLine 5\nLine 6"),
+func TestListDelete(t *testing.T) {
+	var defaultRend DefaultItemRenderer
+	list := New(&defaultRend,
+		NewStringItem("Item 1"),
+		NewStringItem("Item 2"),
+		NewStringItem("Item 3"),
+	)
+
+	deleted := list.Delete(1)
+	if !deleted {
+		t.Errorf("expected delete to succeed")
 	}
 
-	bounds := image.Rect(0, 0, 10, 4)
-	list := New(bounds, items...)
-	list.SetReverse(true)
+	if list.Len() != 2 {
+		t.Errorf("expected list count 2, got %d", list.Len())
+	}
 
 	rendered := list.Render()
-	expected := "Line 4\nLine 5\nLine 6\n"
-
+	expected := "Item 1\nItem 3"
 	if rendered != expected {
 		t.Errorf("expected rendered output:\n%s\ngot:\n%s", expected, rendered)
 	}

internal/ui/list/styles.go 🔗

@@ -1,25 +0,0 @@
-package list
-
-import (
-	"charm.land/lipgloss/v2"
-	"github.com/charmbracelet/x/exp/charmtone"
-)
-
-// Styles holds the styles for the List component.
-type Styles struct {
-	NormalItem  lipgloss.Style
-	FocusedItem lipgloss.Style
-}
-
-// DefaultStyles returns the default styles for the List component.
-func DefaultStyles() (s Styles) {
-	s.NormalItem = lipgloss.NewStyle().
-		MarginLeft(1).
-		PaddingLeft(1)
-	s.FocusedItem = lipgloss.NewStyle().
-		Border(lipgloss.ThickBorder(), false, false, false, true).
-		PaddingLeft(1).
-		BorderForeground(charmtone.Guac)
-
-	return s
-}