Files
kjol/go/webui/buttons.go

214 lines
7.0 KiB
Go

package webui
import "kjol/vdom"
// Port of web/kit/Buttons.tsx.
const (
ButtonNeutral = "neutral"
ButtonWhite = "white"
ButtonLightNeutral = "light-neutral"
ButtonBlue = "blue"
ButtonDarkBlue = "dark-blue"
ButtonGreen = "green"
ButtonDarkGreen = "dark-green"
ButtonRed = "red"
ButtonDarkRed = "dark-red"
ButtonYellow = "yellow"
ButtonOrange = "orange"
ButtonPrimary = "primary"
ButtonSecondary = "secondary"
ButtonGhost = "ghost"
)
const btnBase = "inline-flex items-center justify-center gap-2 cursor-pointer text-sm font-normal rounded-default transition disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2"
var btnColors = map[string]string{
"neutral": "shadow-xs bg-neutral-700 text-white hover:bg-neutral-800",
"white": "shadow-xs bg-surface text-ink border border-line-strong hover:bg-surface-muted",
"light-neutral": "shadow-xs bg-surface-muted text-ink border border-line-strong hover:bg-surface-raised",
"blue": "shadow-xs bg-sky-700 text-white hover:bg-sky-800",
"dark-blue": "shadow-xs bg-sky-900 text-white hover:bg-sky-950",
"green": "shadow-xs bg-green-700 text-white hover:bg-green-800",
"dark-green": "shadow-xs bg-green-900 text-white hover:bg-green-950",
"red": "shadow-xs bg-red-700 text-white hover:bg-red-800",
"dark-red": "shadow-xs bg-red-900 text-white hover:bg-red-950",
"yellow": "shadow-xs bg-yellow-700 text-white hover:bg-yellow-800",
"orange": "shadow-xs bg-orange-600 text-white hover:bg-orange-700",
"primary": "shadow-xs bg-primary text-white hover:bg-primary-hover",
"secondary": "shadow-none bg-surface-raised text-ink-soft border border-line-strong hover:bg-surface-strong",
"ghost": "shadow-none bg-transparent text-ink-soft border-none hover:bg-surface-raised",
}
var btnOutlineColors = map[string]string{
"neutral": "text-ink-soft",
"white": "text-ink-faint",
"light-neutral": "text-ink-faint",
"blue": "text-sky-700 dark:text-sky-400",
"dark-blue": "text-sky-900",
"green": "text-green-700 dark:text-green-400",
"dark-green": "text-green-900",
"red": "text-red-700",
"dark-red": "text-red-900",
"yellow": "text-yellow-700",
"orange": "text-orange-600",
"primary": "text-accent",
}
const btnOutlineBase = "bg-transparent shadow-[inset_0_0_0_1px_currentColor] hover:shadow-[inset_0_0_0_2px_currentColor]"
// ButtonProps configures Button. Icon is an icon name (see webui.Icon); Text is
// the label. If neither Text nor children are given the button is icon-only.
type ButtonProps struct {
Color string
Outline bool
Small bool
Icon string
Text string
Type string
Disabled bool
Title string
Class string
OnClick func()
}
func buttonClass(p ButtonProps) string {
c := btnBase
if p.Outline {
oc := btnOutlineColors[p.Color]
if oc == "" {
oc = btnOutlineColors["neutral"]
}
c = cx(c, btnOutlineBase, oc)
} else {
cc := btnColors[p.Color]
if cc == "" {
cc = btnColors["neutral"]
}
c = cx(c, cc)
}
hasText := p.Text != ""
switch {
case p.Small && p.Icon != "":
c = cx(c, "py-1 px-3")
case p.Small:
c = cx(c, "py-1 px-4")
case p.Icon != "" && !hasText:
c = cx(c, "py-2 px-3")
case p.Icon != "":
c = cx(c, "py-2 px-5")
default:
c = cx(c, "py-2 px-8")
}
return cx(c, p.Class)
}
// Button renders a styled <button>. Extra children render after any Icon/Text.
func Button(p ButtonProps, children ...*vdom.VNode) *vdom.VNode {
mods := []vdom.Mod{
vdom.Attr("type", pick(p.Type, "button")),
vdom.Attr("class", buttonClass(p)),
}
if p.Disabled {
mods = append(mods, vdom.Attr("disabled", "disabled"))
}
if p.Title != "" {
mods = append(mods, vdom.Attr("title", p.Title))
}
if p.OnClick != nil {
mods = append(mods, vdom.On(vdom.EVENT_CLICK, p.OnClick))
}
if p.Icon != "" {
mods = append(mods, Icon(p.Icon, iconSize(p.Small), ""))
}
if p.Text != "" {
mods = append(mods, vdom.Text(p.Text))
}
mods = kids(mods, children)
return vdom.Button(mods...)
}
func iconSize(small bool) int {
if small {
return 14
}
return 16
}
// ButtonLink is a text-styled link button (blue).
func ButtonLink(onClick func(), children ...*vdom.VNode) *vdom.VNode {
mods := []vdom.Mod{
vdom.Attr("type", "button"),
vdom.Attr("class", "cursor-pointer bg-transparent border-none p-0 font-[inherit] text-sky-700 dark:text-sky-400 hover:underline"),
}
if onClick != nil {
mods = append(mods, vdom.On(vdom.EVENT_CLICK, onClick))
}
return vdom.Button(kids(mods, children)...)
}
// ButtonLinkRed is the red variant of ButtonLink.
func ButtonLinkRed(onClick func(), children ...*vdom.VNode) *vdom.VNode {
mods := []vdom.Mod{
vdom.Attr("type", "button"),
vdom.Attr("class", "cursor-pointer bg-transparent border-none p-0 font-[inherit] text-red-600 dark:text-red-400 hover:underline"),
}
if onClick != nil {
mods = append(mods, vdom.On(vdom.EVENT_CLICK, onClick))
}
return vdom.Button(kids(mods, children)...)
}
// SegmentedButtonOption is one option in a SegmentedButtons group.
type SegmentedButtonOption struct {
Value string
Label string
Icon string
}
// SegmentedButtons renders a pill group of mutually-exclusive options; `value`
// is the selected option's value, `onChange` receives the clicked value.
func SegmentedButtons(options []SegmentedButtonOption, value string, onChange func(string), small bool, class string) *vdom.VNode {
sizeCls := "py-1 px-3 text-sm"
if small {
sizeCls = "py-0.5 px-2 text-ss"
}
const baseCls = "inline-flex items-center justify-center gap-1.5 flex-1 cursor-pointer font-medium transition-colors whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
const activeCls = "bg-surface text-text-heading shadow-sm"
const inactiveCls = "text-ink-muted hover:text-ink-soft"
mods := []vdom.Mod{vdom.Attr("class", cx("flex items-center gap-0.5 rounded-md bg-surface-raised p-0.5", class))}
for _, opt := range options {
state := inactiveCls
if opt.Value == value {
state = activeCls
}
v := opt.Value
btnMods := []vdom.Mod{
vdom.Attr("type", "button"),
vdom.Attr("class", cx(baseCls, "rounded-default", sizeCls, state)),
vdom.Attr("title", opt.Label),
vdom.On(vdom.EVENT_CLICK, func() { onChange(v) }),
}
if opt.Icon != "" {
btnMods = append(btnMods, Icon(opt.Icon, 12, ""))
}
btnMods = append(btnMods, vdom.Span(vdom.Text(opt.Label)))
mods = append(mods, vdom.Button(btnMods...))
}
return vdom.Div(mods...)
}
// BackLink is an inline chevron-left anchor. onDark switches to on-dark colors.
func BackLink(href, text string, onDark bool) *vdom.VNode {
color := "text-ink-soft hover:text-ink"
if onDark {
color = "text-text-on-dark-muted hover:text-text-on-dark"
}
return vdom.A(vdom.Attr("href", href),
vdom.Attr("class", cx("inline-flex items-center gap-1 text-sm no-underline", color)),
Icon("chevron-left", 16, ""),
vdom.Text(text),
)
}