Swap Bulma for DaisyUI (Tailwind) (#111)

This commit is contained in:
Mike Stefanello 2025-06-17 20:19:58 -04:00 committed by GitHub
parent fc5db0e95a
commit c1e9baabe6
53 changed files with 1124 additions and 632 deletions

View file

@ -3,62 +3,64 @@ package components
import (
"github.com/mikestefanello/pagoda/pkg/msg"
"github.com/mikestefanello/pagoda/pkg/ui"
"github.com/mikestefanello/pagoda/pkg/ui/icons"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func FlashMessages(r *ui.Request) Node {
var g Group
var color Color
for _, typ := range []msg.Type{
msg.TypeSuccess,
msg.TypeInfo,
msg.TypeWarning,
msg.TypeDanger,
msg.TypeError,
} {
for _, str := range msg.Get(r.Context, typ) {
g = append(g, Notification(typ, str))
switch typ {
case msg.TypeSuccess:
color = ColorSuccess
case msg.TypeInfo:
color = ColorInfo
case msg.TypeWarning:
color = ColorWarning
case msg.TypeError:
color = ColorError
}
g = append(g, Alert(color, str))
}
}
return g
}
func Notification(typ msg.Type, text string) Node {
func Alert(color Color, text string) Node {
var class string
switch typ {
case msg.TypeSuccess:
class = "success"
case msg.TypeInfo:
class = "info"
case msg.TypeWarning:
class = "warning"
case msg.TypeDanger:
class = "danger"
switch color {
case ColorSuccess:
class = "alert-success"
case ColorInfo:
class = "alert-info"
case ColorWarning:
class = "alert-warning"
case ColorError:
class = "alert-error"
}
return Div(
Class("notification is-"+class),
Role("alert"),
Class("alert mb-2 "+class),
Attr("x-data", "{show: true}"),
Attr("x-show", "show"),
Button(
Class("delete"),
Span(
Attr("@click", "show = false"),
Class("cursor-pointer"),
icons.XCircle(),
),
Text(text),
)
}
func Message(class, header string, body Node) Node {
return Article(
Class("message "+class),
If(header != "", Div(
Class("message-header"),
P(Text(header)),
)),
Div(
Class("message-body"),
body,
),
Span(Text(text)),
)
}

121
pkg/ui/components/data.go Normal file
View file

@ -0,0 +1,121 @@
package components
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type (
CardParams struct {
Title string
Body Group
Footer Group
Color Color
Size Size
}
Stat struct {
Title string
Value string
Description string
Icon Node
}
)
func Badge(color Color, text string) Node {
var class string
switch color {
case ColorSuccess:
class = "badge-success"
case ColorWarning:
class = "badge-warning"
}
return Div(
Class("badge "+class),
Text(text),
)
}
func Divider(text string) Node {
return Div(
Class("divider"),
Text(text),
)
}
func Card(params CardParams) Node {
var colorClass, sizeClass string
switch params.Color {
case ColorSuccess:
colorClass = "bg-success text-success-content"
case ColorPrimary:
colorClass = "bg-primary text-primary-content"
case ColorAccent:
colorClass = "bg-accent text-accent-content"
case ColorNeutral:
colorClass = "bg-neutral text-neutral-content"
case ColorWarning:
colorClass = "bg-warning text-warning-content"
case ColorInfo:
colorClass = "bg-info text-info-content"
}
switch params.Size {
case SizeSmall:
sizeClass = "card-sm"
case SizeMedium:
sizeClass = "card-md"
case SizeLarge:
sizeClass = "card-lg"
}
return Div(
Class("cards mb-2 "+colorClass+" "+sizeClass),
Div(
Class("card-body"),
If(len(params.Title) > 0, Span(
Class("card-title"),
Text(params.Title),
)),
params.Body,
If(params.Footer != nil, Div(
Class("card-actions justify-end"),
params.Footer,
)),
),
)
}
func Stats(stats ...Stat) Node {
g := make(Group, 0, len(stats))
for _, stat := range stats {
g = append(g, Div(
Class("stat"),
Iff(stat.Icon != nil, func() Node {
return Div(
Class("stat-figure text-secondary"),
stat.Icon,
)
}),
Div(
Class("stat-title"),
Text(stat.Title),
),
Div(
Class("stat-value"),
Text(stat.Value),
),
Div(
Class("stat-desc"),
Text(stat.Description),
),
))
}
return Div(
Class("stats shadow"),
g,
)
}

View file

@ -19,6 +19,12 @@ type (
Help string
}
FileFieldParams struct {
Name string
Label string
Help string
}
OptionsParams struct {
Form form.Form
FormField string
@ -26,6 +32,7 @@ type (
Label string
Value string
Options []Choice
Help string
}
Choice struct {
@ -52,38 +59,22 @@ type (
)
func ControlGroup(controls ...Node) Node {
g := make(Group, len(controls))
for i, control := range controls {
g[i] = Div(
Class("control"),
control,
)
}
return Div(
Class("field is-grouped"),
g,
Class("mt-2 flex gap-2"),
Group(controls),
)
}
func TextareaField(el TextareaFieldParams) Node {
return Div(
Class("field"),
Label(
For("name"),
Class("label"),
Text(el.Label),
return Fieldset(
el.Label,
Textarea(
Class("textarea h-24 w-2/3 "+formFieldStatusClass(el.Form, el.FormField)),
ID(el.Name),
Name(el.Name),
Text(el.Value),
),
Div(
Class("control"),
Textarea(
ID(el.Name),
Name(el.Name),
Class("textarea "+formFieldStatusClass(el.Form, el.FormField)),
Text(el.Value),
),
),
If(el.Help != "", P(Class("help"), Text(el.Help))),
Help(el.Help),
formFieldErrors(el.Form, el.FormField),
)
}
@ -91,25 +82,27 @@ func TextareaField(el TextareaFieldParams) Node {
func Radios(el OptionsParams) Node {
buttons := make(Group, len(el.Options))
for i, opt := range el.Options {
buttons[i] = Label(
Class("radio"),
id := "radio-" + el.Name + "-" + opt.Value
buttons[i] = Div(
Class("mb-2"),
Input(
ID(id),
Type("radio"),
Name(el.Name),
Value(opt.Value),
Class("radio mr-1 "+formFieldStatusClass(el.Form, el.FormField)),
If(el.Value == opt.Value, Checked()),
),
Text(" "+opt.Label),
Label(
Text(opt.Label),
For(id),
),
)
}
return Div(
Class("control field"),
Label(Class("label"), Text(el.Label)),
Div(
Class("radios"),
buttons,
),
return Fieldset(
el.Label,
buttons,
formFieldErrors(el.Form, el.FormField),
)
}
@ -124,82 +117,77 @@ func SelectList(el OptionsParams) Node {
)
}
return Div(
Class("control field"),
Label(Class("label"), Text(el.Label)),
Div(
Class("select"),
Select(
Name(el.Name),
buttons,
),
return Fieldset(
el.Label,
Select(
Class("select "+formFieldStatusClass(el.Form, el.FormField)),
buttons,
),
Help(el.Help),
formFieldErrors(el.Form, el.FormField),
)
}
func Checkbox(el CheckboxParams) Node {
return Div(
Class("field"),
Div(
Class("control"),
Label(
Label(
Class("label"),
Input(
Class("checkbox"),
Input(
Type("checkbox"),
Name(el.Name),
If(el.Checked, Checked()),
Value("true"),
),
Text(" "+el.Label),
Type("checkbox"),
Name(el.Name),
If(el.Checked, Checked()),
Value("true"),
),
Text(" "+el.Label),
),
formFieldErrors(el.Form, el.FormField),
)
}
func InputField(el InputFieldParams) Node {
return Div(
Class("field"),
Label(
Class("label"),
For(el.Name),
Text(el.Label),
return Fieldset(
el.Label,
Input(
ID(el.Name),
Name(el.Name),
Type(el.InputType),
Class("input "+formFieldStatusClass(el.Form, el.FormField)),
Value(el.Value),
If(el.Placeholder != "", Placeholder(el.Placeholder)),
),
Div(
Class("control"),
Input(
ID(el.Name),
Name(el.Name),
Type(el.InputType),
If(el.Placeholder != "", Placeholder(el.Placeholder)),
Class("input "+formFieldStatusClass(el.Form, el.FormField)),
Value(el.Value),
),
),
If(el.Help != "", P(Class("help"), Text(el.Help))),
Help(el.Help),
formFieldErrors(el.Form, el.FormField),
)
}
func FileField(name, label string) Node {
return Div(
Class("field file"),
Label(
Class("file-label"),
Input(
Class("file-input"),
Type("file"),
Name(name),
),
Span(
Class("file-cta"),
Span(
Class("file-label"),
Text(label),
),
),
func Help(text string) Node {
return If(len(text) > 0, Div(
Class("label"),
Text(text),
))
}
func Fieldset(label string, els ...Node) Node {
return FieldSet(
Class("fieldset"),
If(len(label) > 0, Legend(
Class("fieldset-legend"),
Text(label),
)),
Group(els),
)
}
func FileField(el FileFieldParams) Node {
return Fieldset(
el.Label,
Input(
Type("file"),
Class("file-input"),
Name(el.Name),
),
Help(el.Help),
)
}
@ -210,9 +198,9 @@ func formFieldStatusClass(fm form.Form, formField string) string {
case !fm.IsSubmitted():
return ""
case fm.FieldHasErrors(formField):
return "is-danger"
return "input-error"
default:
return "is-success"
return "input-success"
}
}
@ -228,8 +216,8 @@ func formFieldErrors(fm form.Form, field string) Node {
g := make(Group, len(errs))
for i, err := range errs {
g[i] = P(
Class("help is-danger"),
g[i] = Div(
Class("text-error"),
Text(err),
)
}
@ -245,17 +233,35 @@ func CSRF(r *ui.Request) Node {
)
}
func FormButton(class, label string) Node {
func FormButton(color Color, label string) Node {
return Button(
Class("button "+class),
Class("btn "+buttonColor(color)),
Text(label),
)
}
func ButtonLink(href, class, label string) Node {
func ButtonLink(color Color, href, label string) Node {
return A(
Href(href),
Class("button "+class),
Class("btn "+buttonColor(color)),
Text(label),
)
}
func buttonColor(color Color) string {
// Only colors being used are included so unused styles are not compiled.
switch color {
case ColorPrimary:
return "btn-primary"
case ColorInfo:
return "btn-info"
case ColorAccent:
return "btn-accent"
case ColorError:
return "btn-error"
case ColorLink:
return "btn-link"
default:
return ""
}
}

View file

@ -8,17 +8,18 @@ import (
. "maragu.dev/gomponents/html"
)
func JS(r *ui.Request) Node {
func JS() Node {
return Group{
Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js")),
Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"), Defer()),
Script(Src("https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"), Defer()),
}
}
func CSS() Node {
return Link(
Href("https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"),
Href(ui.StaticFile("main.css")),
Rel("stylesheet"),
Type("text/css"),
)
}
@ -26,7 +27,7 @@ func Metatags(r *ui.Request) Node {
return Group{
Meta(Charset("utf-8")),
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
Link(Rel("icon"), Href(ui.File("favicon.png"))),
Link(Rel("icon"), Href(ui.StaticFile("favicon.png"))),
TitleEl(Text(r.Config.App.Name), If(r.Title != "", Text(" | "+r.Title))),
If(r.Metatags.Description != "", Meta(Name("description"), Content(r.Metatags.Description))),
If(len(r.Metatags.Keywords) > 0, Meta(Name("keywords"), Content(strings.Join(r.Metatags.Keywords, ", ")))),

View file

@ -1,19 +1,72 @@
package components
import (
"fmt"
"github.com/mikestefanello/pagoda/pkg/pager"
"github.com/mikestefanello/pagoda/pkg/ui"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
func MenuLink(r *ui.Request, title, routeName string, routeParams ...any) Node {
func MenuLink(r *ui.Request, icon Node, title, routeName string, routeParams ...any) Node {
href := r.Path(routeName, routeParams...)
return Li(
Class("ml-2"),
A(
Href(href),
icon,
Text(title),
If(href == r.CurrentPath, Class("is-active")),
Classes{
"menu-active": href == r.CurrentPath,
"p-2": true,
},
),
)
}
func Pager(page int, path string, hasNext bool, hxTarget string) Node {
href := func(page int) string {
return fmt.Sprintf("%s?%s=%d",
path,
pager.QueryKey,
page,
)
}
return Div(
Class("join"),
A(
Class("join-item btn"),
Text("«"),
If(page <= 1, Disabled()),
Href(href(page-1)),
Iff(len(hxTarget) > 0, func() Node {
return Group{
Attr("hx-get", href(page-1)),
Attr("hx-swap", "outerHTML"),
Attr("hx-target", hxTarget),
}
}),
),
Button(
Class("join-item btn"),
Textf("Page %d", page),
),
A(
Class("join-item btn"),
Text("»"),
If(!hasNext, Disabled()),
Href(href(page+1)),
Iff(len(hxTarget) > 0, func() Node {
return Group{
Attr("hx-get", href(page+1)),
Attr("hx-swap", "outerHTML"),
Attr("hx-target", hxTarget),
}
}),
),
)
}

View file

@ -0,0 +1,27 @@
package components
type (
Color int
Size int
)
const (
ColorNone Color = iota
ColorNeutral
ColorPrimary
ColorSecondary
ColorAccent
ColorInfo
ColorSuccess
ColorWarning
ColorError
ColorLink
)
const (
SizeExtraSmall Size = iota
SizeSmall
SizeMedium
SizeLarge
SizeExtraLarge
)

View file

@ -2,6 +2,7 @@ package components
import (
"fmt"
"math/rand"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
@ -11,46 +12,27 @@ type Tab struct {
Title, Body string
}
func Tabs(heading, description string, items []Tab) Node {
renderTitles := func() Node {
g := make(Group, len(items))
for i, item := range items {
g[i] = Li(
Attr(":class", fmt.Sprintf("{'is-active': tab === %d}", i)),
Attr("@click", fmt.Sprintf("tab = %d", i)),
A(Text(item.Title)),
)
}
return g
}
func Tabs(tabs []Tab) Node {
g := make(Group, 0, len(tabs)*2)
id := fmt.Sprintf("tabs-%d", rand.Int())
renderBodies := func() Node {
g := make(Group, len(items))
for i, item := range items {
g[i] = Div(
Attr("x-show", fmt.Sprintf("tab == %d", i)),
P(Raw(" "+item.Body)),
)
}
return g
for i, tab := range tabs {
g = append(g,
Input(
Type("radio"),
Name(id),
Class("tab"),
Aria("label", tab.Title),
If(i == 0, Checked()),
),
Div(
Class("tab-content bg-base-100 border-base-300 p-6"),
Raw(tab.Body),
))
}
return Div(
P(
Class("subtitle mt-5"),
Text(heading),
),
P(
Class("mb-4"),
Text(description),
),
Div(
Attr("x-data", "{tab: 0}"),
Div(
Class("tabs"),
Ul(renderTitles()),
),
renderBodies(),
),
Class("tabs tabs-lift"),
g,
)
}