172 lines
5.6 KiB
Go
172 lines
5.6 KiB
Go
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...)
|
|
}
|