feat: naming dialog view (centered bordered box)
c722d38c9615fc5ddb44650db262174cad2d16c7
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-07-30 19:04
parent d94299a4
feat: naming dialog view (centered bordered box) Title, target-folder hint, input, validation line, and key footer in the same visual family as the help overlay (TASK-046).
2 files changed
internal/dialog/dialog.go +33 −0
@@ -11,6 +11,7 @@ "glint/internal/theme"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
)
// Result is what a key press did to the dialog.
@@ -83,3 +84,35 @@ var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
return ResultNone, cmd
}
+
+// boxMin is the narrowest the dialog box gets before it just takes the column.
+const boxMin = 24
+
+// View renders the dialog as a centered bordered box: title, the folder the
+// name lands in, the input, an optional validation line, then the key footer.
+func (m *Model) View() string {
+ inner := lipgloss.NewStyle().Foreground(m.th.Heading).Bold(true).Render(m.title)
+ if m.dirHint != "" {
+ inner += "\n" + lipgloss.NewStyle().Foreground(m.th.Muted).Render("in "+m.dirHint)
+ }
+ inner += "\n\n" + m.input.View()
+ if m.err != "" {
+ inner += "\n\n" + lipgloss.NewStyle().Foreground(m.th.Spell).Render(m.err)
+ }
+ inner += "\n\n" + lipgloss.NewStyle().Foreground(m.th.Muted).
+ Render("Enter to confirm · Esc to cancel")
+
+ w := m.width - 2 // the border takes one cell each side
+ if w < boxMin {
+ w = boxMin
+ }
+ box := lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(m.th.Heading).
+ BorderBackground(m.th.Background).
+ Background(m.th.Background).
+ Foreground(m.th.Text).
+ Padding(0, 1).
+ Width(w)
+ return box.Render(inner)
+}
internal/dialog/dialog_test.go +38 −0
@@ -1,12 +1,14 @@
package dialog
import (
+ "strings"
"testing"
"glint/internal/theme"
"github.com/charmbracelet/bubbles/cursor"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
)
func newModel() *Model { return New(theme.FlexokiDark()) }
@@ -106,3 +108,39 @@ if m.err == "" {
t.Error("err cleared by a cursor blink; only typing should clear it")
}
}
+
+func TestViewShowsTitleDirAndFooter(t *testing.T) {
+ m := newModel()
+ m.SetWidth(60)
+ m.Open("New note", "~/Humdrum/Inbox/", "")
+ out := m.View()
+ for _, want := range []string{"New note", "~/Humdrum/Inbox/", "Enter", "Esc"} {
+ if !strings.Contains(out, want) {
+ t.Errorf("View() missing %q:\n%s", want, out)
+ }
+ }
+}
+
+func TestViewShowsErrorWhenSet(t *testing.T) {
+ m := newModel()
+ m.SetWidth(60)
+ m.Open("New note", "~/Notes/", "")
+ if strings.Contains(m.View(), "Name taken") {
+ t.Fatal("View() shows the error before SetError")
+ }
+ m.SetError("Name taken")
+ if !strings.Contains(m.View(), "Name taken") {
+ t.Errorf("View() missing the error line:\n%s", m.View())
+ }
+}
+
+func TestViewFitsContentWidth(t *testing.T) {
+ m := newModel()
+ m.SetWidth(40)
+ m.Open("New note", "~/Notes/", "")
+ for i, ln := range strings.Split(m.View(), "\n") {
+ if w := lipgloss.Width(ln); w > 40 {
+ t.Errorf("line %d width = %d, want <= 40", i, w)
+ }
+ }
+}