214 lines
6.9 KiB
Go
214 lines
6.9 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-white text-black border border-neutral-300 hover:bg-neutral-50",
|
|
"light-neutral": "shadow-xs bg-neutral-50 text-black border border-neutral-300 hover:bg-neutral-100",
|
|
"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-neutral-100 text-neutral-700 border border-neutral-300 hover:bg-neutral-200",
|
|
"ghost": "shadow-none bg-transparent text-neutral-600 border-none hover:bg-neutral-100",
|
|
}
|
|
|
|
var btnOutlineColors = map[string]string{
|
|
"neutral": "text-neutral-700",
|
|
"white": "text-neutral-300",
|
|
"light-neutral": "text-neutral-300",
|
|
"blue": "text-sky-700",
|
|
"dark-blue": "text-sky-900",
|
|
"green": "text-green-700",
|
|
"dark-green": "text-green-900",
|
|
"red": "text-red-700",
|
|
"dark-red": "text-red-900",
|
|
"yellow": "text-yellow-700",
|
|
"orange": "text-orange-600",
|
|
"primary": "text-primary",
|
|
}
|
|
|
|
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 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 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-xs"
|
|
}
|
|
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-white text-text-heading shadow-sm"
|
|
const inactiveCls = "text-neutral-500 hover:text-neutral-700"
|
|
|
|
mods := []vdom.Mod{vdom.Attr("class", cx("flex items-center gap-0.5 rounded-md bg-neutral-100 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-neutral-600 hover:text-neutral-900"
|
|
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),
|
|
)
|
|
}
|