This commit is contained in:
2025-08-17 17:04:27 -04:00
parent f9f23f2d3f
commit 2eed0d9021
72 changed files with 2713 additions and 100 deletions

View File

@@ -0,0 +1,99 @@
package ui
import (
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
"reichard.io/antholume/pkg/sliceutils"
"reichard.io/antholume/pkg/utils"
)
type ButtonVariant string
const (
ButtonVariantPrimary ButtonVariant = "primary"
ButtonVariantSecondary ButtonVariant = "secondary"
ButtonVariantGhost ButtonVariant = "ghost"
)
type buttonAs int
const (
buttonAsLink buttonAs = iota
buttonAsForm
buttonAsSpan
)
type ButtonConfig struct {
Variant ButtonVariant
Disabled bool
as buttonAs
value string
}
// LinkButton creates a button that links to a url. The default variant is ButtonVariantPrimary.
func LinkButton(content g.Node, url string, cfg ...ButtonConfig) g.Node {
config := buildButtonConfig(cfg, buttonAsLink, url)
return button(content, config)
}
// FormButton creates a button that is a form. The default variant is ButtonVariantPrimary.
func FormButton(content g.Node, formName string, cfg ...ButtonConfig) g.Node {
config := buildButtonConfig(cfg, buttonAsForm, formName)
return button(content, config)
}
// SpanButton creates a button that has no target (i.e. span). The default variant is ButtonVariantPrimary.
func SpanButton(content g.Node, cfg ...ButtonConfig) g.Node {
config := buildButtonConfig(cfg, buttonAsSpan, "")
return button(content, config)
}
func button(content g.Node, config ButtonConfig) g.Node {
classes := config.getClasses()
if config.as == buttonAsSpan || config.Disabled {
return h.Span(content, h.Class(classes))
} else if config.as == buttonAsLink {
return h.A(h.Class(classes), h.Href(config.value), content)
}
return h.Button(
content,
h.Type("submit"),
h.Class(classes),
g.If(config.value != "", h.FormAttr(config.value)),
)
}
func (c *ButtonConfig) getClasses() string {
baseClass := "transition duration-100 ease-in font-medium text-center inline-block"
var variantClass string
switch c.Variant {
case ButtonVariantPrimary:
variantClass = "h-full w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
case ButtonVariantSecondary:
variantClass = "h-full w-full px-2 py-1 text-white bg-black shadow-md hover:text-black hover:bg-white"
case ButtonVariantGhost:
variantClass = "text-gray-500 hover:text-gray-800 dark:hover:text-gray-100"
}
classes := baseClass + " " + variantClass
if c.Disabled {
classes += " opacity-40 pointer-events-none"
}
return classes
}
func buildButtonConfig(cfg []ButtonConfig, as buttonAs, val string) ButtonConfig {
c, found := sliceutils.First(cfg)
if !found {
c = ButtonConfig{Variant: ButtonVariantPrimary}
}
c.Variant = utils.FirstNonZero(c.Variant, ButtonVariantPrimary)
c.as = as
c.value = val
return c
}

24
web/components/ui/kv.go Normal file
View File

@@ -0,0 +1,24 @@
package ui
import (
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
)
// KeyValue is a basic vertical key/value pair component
func KeyValue(key, val g.Node) g.Node {
return h.Div(
h.Class("flex flex-col"),
h.Div(h.Class("text-gray-500"), key),
h.Div(h.Class("font-medium text-black dark:text-white"), val),
)
}
// HKeyValue is a basic horizontal key/value pair component
func HKeyValue(key, val g.Node) g.Node {
return h.Div(
h.Class("flex gap-2"),
h.Div(h.Class("text-gray-500"), key),
h.Div(h.Class("font-medium text-black dark:text-white"), val),
)
}

View File

@@ -0,0 +1,99 @@
package ui
import (
"strings"
"github.com/google/uuid"
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
"reichard.io/antholume/pkg/ptr"
"reichard.io/antholume/pkg/sliceutils"
"reichard.io/antholume/pkg/utils"
)
type PopoverPosition string
const (
// ---- Cornered ----
// PopoverTopLeft PopoverPosition = "left-0 top-0 origin-bottom-right -translate-x-full -translate-y-full"
// PopoverTopRight PopoverPosition = "right-0 top-0 origin-bottom-left translate-x-full -translate-y-full"
// PopoverBottomLeft PopoverPosition = "left-0 bottom-0 origin-top-right -translate-x-full translate-y-full"
// PopoverBottomRight PopoverPosition = "right-0 bottom-0 origin-top-left translate-x-full translate-y-full"
// ---- Flush ----
PopoverTopLeft PopoverPosition = "right-0 -top-1 origin-bottom-right -translate-y-full"
PopoverTopRight PopoverPosition = "left-0 -top-1 origin-bottom-left -translate-y-full"
PopoverBottomLeft PopoverPosition = "right-0 -bottom-1 origin-top-right translate-y-full"
PopoverBottomRight PopoverPosition = "left-0 -bottom-1 origin-top-left translate-y-full"
// ---- Centered ----
PopoverTopCenter PopoverPosition = "left-1/2 top-0 origin-bottom -translate-x-1/2 -translate-y-full"
PopoverBottomCenter PopoverPosition = "left-1/2 bottom-0 origin-top -translate-x-1/2 translate-y-full"
PopoverLeftCenter PopoverPosition = "left-0 top-1/2 origin-right -translate-x-full -translate-y-1/2"
PopoverRightCenter PopoverPosition = "right-0 top-1/2 origin-left translate-x-full -translate-y-1/2"
PopoverCenter PopoverPosition = "left-1/2 top-1/2 origin-center -translate-x-1/2 -translate-y-1/2"
)
type PopoverConfig struct {
Position PopoverPosition
Classes string
Dim *bool
}
// AnchoredPopover creates a popover with content anchored to the anchor node.
// The default position is PopoverBottomRight.
func AnchoredPopover(anchor, content g.Node, cfg ...PopoverConfig) g.Node {
// Get Popover Config
c, _ := sliceutils.First(cfg)
c.Position = utils.FirstNonZero(c.Position, PopoverBottomRight)
if c.Dim == nil {
c.Dim = ptr.Of(false)
}
popoverID := uuid.NewString()
return h.Div(
h.Class("relative"),
h.Label(
h.Class("cursor-pointer"),
h.For(popoverID),
anchor,
),
h.Input(
h.ID(popoverID),
h.Class("hidden css-button"),
h.Type("checkbox"),
),
Popover(content, c),
)
}
func Popover(content g.Node, cfg ...PopoverConfig) g.Node {
// Get Popover Config
c, _ := sliceutils.First(cfg)
c.Position = utils.FirstNonZero(c.Position, PopoverCenter)
if c.Dim == nil {
c.Dim = ptr.Of(true)
}
wrappedContent := h.Div(h.Class(c.getClasses()), content)
if !ptr.Deref(c.Dim) {
return wrappedContent
}
return h.Div(
h.Div(h.Class("fixed top-0 left-0 bg-black z-40 opacity-50 w-screen h-screen")),
wrappedContent,
)
}
func (c *PopoverConfig) getClasses() string {
return strings.Join([]string{
"absolute z-50 p-2 transition-all duration-200 rounded shadow-lg",
"bg-gray-200 dark:bg-gray-600 shadow-gray-500 dark:shadow-gray-900",
c.Classes,
string(c.Position),
}, " ")
}

View File

@@ -0,0 +1,64 @@
package ui
import (
"fmt"
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
)
type TableRow map[string]TableCell
type TableCell struct {
String string
Value g.Node
}
type TableConfig struct {
Columns []string
Rows []TableRow
}
func Table(cfg TableConfig) g.Node {
return h.Table(
h.Class("min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"),
h.THead(
h.Class("text-gray-800 dark:text-gray-400"),
h.Tr(
g.Map(cfg.Columns, func(col string) g.Node {
return h.Th(
h.Class("p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"),
g.Text(col),
)
})...,
),
),
h.TBody(
h.Class("text-black dark:text-white"),
g.If(len(cfg.Rows) == 0,
h.Tr(
h.Td(
h.Class("text-center p-3"),
g.Attr("colspan", fmt.Sprintf("%d", len(cfg.Columns))),
g.Text("No Results"),
),
),
),
g.Map(cfg.Rows, func(row TableRow) g.Node {
return h.Tr(
g.Map(cfg.Columns, func(col string) g.Node {
cell, ok := row[col]
content := cell.Value
if !ok || content == nil {
content = g.Text(cell.String)
}
return h.Td(
h.Class("p-3 border-b border-gray-200"),
content,
)
})...,
)
}),
),
)
}