initial port of the UI kit
This commit is contained in:
171
go/webui/toast.go
Normal file
171
go/webui/toast.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package webui
|
||||
|
||||
import "kjol/vdom"
|
||||
|
||||
// Port of web/kit/Toast.tsx.
|
||||
//
|
||||
// NOTE: Solid's context API (useToast + addToast/success/error/warning/info/
|
||||
// generic) is dropped — there is no context or portal in the neutral runtime.
|
||||
// Callers own the []Toast list and its removal instead: build the toasts, pass
|
||||
// them to ToastProvider, and handle OnDismiss to drop one by ID. The
|
||||
// auto-generated toast IDs (generateId) become the caller's responsibility.
|
||||
// NOTE: auto-dismiss timers, the requestAnimationFrame progress countdown, and
|
||||
// the exit (fade/slide) animation are dropped (no timers/rAF here). The progress
|
||||
// bar, when shown, renders full-width and static; the dismiss button removes the
|
||||
// toast immediately.
|
||||
// NOTE: the "circle-xmark" and "circle-info" icons are not in the default icon
|
||||
// registry and render as empty boxes until an app registers them.
|
||||
|
||||
// ToastType selects a toast's accent color and leading icon.
|
||||
type ToastType string
|
||||
|
||||
const (
|
||||
ToastSuccess ToastType = "success"
|
||||
ToastError ToastType = "error"
|
||||
ToastWarning ToastType = "warning"
|
||||
ToastInfo ToastType = "info"
|
||||
ToastGeneric ToastType = "generic"
|
||||
)
|
||||
|
||||
// ToastPosition is where the container is anchored on screen.
|
||||
type ToastPosition string
|
||||
|
||||
const (
|
||||
ToastTopRight ToastPosition = "top-right"
|
||||
ToastTopLeft ToastPosition = "top-left"
|
||||
ToastBottomRight ToastPosition = "bottom-right"
|
||||
ToastBottomLeft ToastPosition = "bottom-left"
|
||||
ToastTopCenter ToastPosition = "top-center"
|
||||
ToastBottomCenter ToastPosition = "bottom-center"
|
||||
)
|
||||
|
||||
// Toast is one notification. Duration is in ms (0 = no auto-dismiss and no
|
||||
// progress bar). Dismissible shows the close button; ShowProgress shows the
|
||||
// (static) progress bar when Duration > 0. Type defaults to ToastInfo.
|
||||
type Toast struct {
|
||||
ID string
|
||||
Message string
|
||||
Type ToastType
|
||||
Duration int
|
||||
Dismissible bool
|
||||
ShowProgress bool
|
||||
}
|
||||
|
||||
var toastTypeIcons = map[ToastType]string{
|
||||
ToastSuccess: "circle-check",
|
||||
ToastError: "circle-xmark",
|
||||
ToastWarning: "triangle-exclamation",
|
||||
ToastInfo: "circle-info",
|
||||
ToastGeneric: "",
|
||||
}
|
||||
|
||||
const toastContainerBase = "fixed z-[200] flex flex-col gap-2"
|
||||
|
||||
var toastContainerPositions = map[ToastPosition]string{
|
||||
ToastTopRight: "top-4 right-4",
|
||||
ToastTopLeft: "top-4 left-4",
|
||||
ToastBottomRight: "bottom-4 right-4 flex-col-reverse",
|
||||
ToastBottomLeft: "bottom-4 left-4 flex-col-reverse",
|
||||
ToastTopCenter: "top-4 left-1/2 -translate-x-1/2",
|
||||
ToastBottomCenter: "bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse",
|
||||
}
|
||||
|
||||
const toastBase = "relative overflow-hidden rounded-default shadow-lg border border-neutral-200 border-l-4 bg-white min-w-72 max-w-md transition-[opacity,transform] duration-150 ease-out"
|
||||
|
||||
var toastTypeBorder = map[ToastType]string{
|
||||
ToastSuccess: "border-l-green-700",
|
||||
ToastError: "border-l-red-700",
|
||||
ToastWarning: "border-l-yellow-500",
|
||||
ToastInfo: "border-l-sky-800",
|
||||
ToastGeneric: "border-l-neutral-400",
|
||||
}
|
||||
|
||||
var toastIconColor = map[ToastType]string{
|
||||
ToastSuccess: "text-green-600",
|
||||
ToastError: "text-red-600",
|
||||
ToastWarning: "text-yellow-600",
|
||||
ToastInfo: "text-sky-700",
|
||||
ToastGeneric: "",
|
||||
}
|
||||
|
||||
// ToastItem renders a single toast. onDismiss receives the toast's ID when the
|
||||
// close button is pressed.
|
||||
func ToastItem(t Toast, onDismiss func(string)) *vdom.VNode {
|
||||
typ := t.Type
|
||||
if typ == "" {
|
||||
typ = ToastInfo
|
||||
}
|
||||
icon := toastTypeIcons[typ]
|
||||
showProgress := t.ShowProgress && t.Duration > 0
|
||||
|
||||
row := []vdom.Mod{vdom.Attr("class", "flex items-start gap-3 p-4")}
|
||||
if icon != "" {
|
||||
row = append(row, Icon(icon, 20, cx("shrink-0 mt-0.5", toastIconColor[typ])))
|
||||
}
|
||||
row = append(row, vdom.El("div", vdom.Attr("class", "flex-1 text-sm text-neutral-800"), vdom.Text(t.Message)))
|
||||
if t.Dismissible {
|
||||
dismiss := []vdom.Mod{
|
||||
vdom.Attr("class", "shrink-0 cursor-pointer text-neutral-400 hover:text-neutral-600 bg-transparent border-0 p-0 transition-colors"),
|
||||
vdom.Attr("aria-label", "Dismiss"),
|
||||
}
|
||||
if onDismiss != nil {
|
||||
id := t.ID
|
||||
dismiss = append(dismiss, vdom.On(vdom.EVENT_CLICK, func() { onDismiss(id) }))
|
||||
}
|
||||
dismiss = append(dismiss, Icon("xmark", 16, ""))
|
||||
row = append(row, vdom.El("button", dismiss...))
|
||||
}
|
||||
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("class", cx(toastBase, toastTypeBorder[typ])),
|
||||
vdom.Attr("role", "alert"),
|
||||
vdom.El("div", row...),
|
||||
}
|
||||
if showProgress {
|
||||
mods = append(mods, vdom.El("div", vdom.Attr("class", "h-1 w-full bg-neutral-100"),
|
||||
vdom.El("div", vdom.Attr("class", "h-full bg-neutral-300"), vdom.Attr("style", "width:100%")),
|
||||
))
|
||||
}
|
||||
return vdom.El("div", mods...)
|
||||
}
|
||||
|
||||
// ToastProviderProps configures ToastProvider. Position defaults to
|
||||
// ToastBottomRight; MaxToasts defaults to 5 (only the most recent are kept).
|
||||
type ToastProviderProps struct {
|
||||
Position ToastPosition
|
||||
MaxToasts int
|
||||
Toasts []Toast
|
||||
OnDismiss func(string)
|
||||
}
|
||||
|
||||
// ToastProvider renders its children followed by the fixed toast container. It
|
||||
// replaces the TSX ToastContext.Provider; the wrapper uses display:contents so
|
||||
// it introduces no layout box of its own.
|
||||
func ToastProvider(p ToastProviderProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
position := p.Position
|
||||
if position == "" {
|
||||
position = ToastBottomRight
|
||||
}
|
||||
max := p.MaxToasts
|
||||
if max <= 0 {
|
||||
max = 5
|
||||
}
|
||||
toasts := p.Toasts
|
||||
if len(toasts) > max {
|
||||
toasts = toasts[len(toasts)-max:]
|
||||
}
|
||||
|
||||
container := []vdom.Mod{
|
||||
vdom.Attr("class", cx(toastContainerBase, toastContainerPositions[position])),
|
||||
vdom.Attr("aria-live", "polite"),
|
||||
vdom.Attr("aria-label", "Notifications"),
|
||||
}
|
||||
for _, t := range toasts {
|
||||
container = append(container, ToastItem(t, p.OnDismiss))
|
||||
}
|
||||
|
||||
mods := []vdom.Mod{vdom.Attr("class", "contents")}
|
||||
mods = kids(mods, children)
|
||||
mods = append(mods, vdom.El("div", container...))
|
||||
return vdom.El("div", mods...)
|
||||
}
|
||||
Reference in New Issue
Block a user