Source code for duck.html.components.snackbar

"""
Snackbar component for displaying brief messages at the top of the screen.
"""

from duck.html.components.container import FlexContainer
from duck.html.components.script import Script


SNACKBAR_SCRIPT = """
if (!window._snackbarTimers) window._snackbarTimers = new WeakMap();

function showSnackbar(snackbar, type, timeout) {
    const colors = {
        error: snackbar.dataset.errorColor,
        info: snackbar.dataset.infoColor,
        success: snackbar.dataset.successColor,
        warning: snackbar.dataset.warningColor,
    };

    const color = colors[type] || colors.info;

    if (color) {
        if (snackbar.dataset.variant === "glacier") {
            snackbar.style.borderColor = color;
            snackbar.style.color = color;
        } else {
            snackbar.style.background = color;
        }
    }

    const prevTimer = window._snackbarTimers.get(snackbar);
    if (prevTimer) {
        clearTimeout(prevTimer);
    }

    snackbar.style.display = "flex";
    snackbar.style.transform = "translateY(0)";
    snackbar.style.opacity = "1";

    if (typeof timeout === "undefined" || timeout === null) {
        timeout = Number(snackbar.dataset.timeout) || null;
    }

    if (timeout) {
        const timer = setTimeout(function() {
            hideSnackbar(snackbar);
            window._snackbarTimers.delete(snackbar);
        }, timeout);

        window._snackbarTimers.set(snackbar, timer);
    }
}

function hideSnackbar(snackbar) {
    snackbar.style.transform = "translateY(-100%)";
    snackbar.style.opacity = "0";

    function onTransitionEnd(event) {
        if (event.propertyName === "opacity") {
            snackbar.style.display = "none";
            snackbar.removeEventListener("transitionend", onTransitionEnd);
        }
    }

    snackbar.addEventListener("transitionend", onTransitionEnd);

    const prevTimer = window._snackbarTimers.get(snackbar);
    if (prevTimer) {
        clearTimeout(prevTimer);
        window._snackbarTimers.delete(snackbar);
    }
}
"""


[docs] class Snackbar(FlexContainer): """ Snackbar component for showing brief notifications. """ allowed_types = {"info", "success", "error", "warning"} allowed_variants = {"filled", "glacier"} def __init__( self, text: str | None = None, type: str = "info", variant: str = "filled", timeout: int | None = None, **kwargs, ): """ Initialize the snackbar. Args: text: Snackbar message text. type: Snackbar type. Must be ``info``, ``success``, ``error``, or ``warning``. variant: Visual style. Must be ``filled`` (solid background) or ``glacier`` (frosted, outlined glass look). timeout: Auto-hide timeout in milliseconds. **kwargs: Additional component keyword arguments. Raises: ValueError: If the snackbar type or variant is invalid. """ if type not in self.allowed_types: raise ValueError( f"Snackbar type must be one of {sorted(self.allowed_types)}." ) if variant not in self.allowed_variants: raise ValueError( f"Snackbar variant must be one of {sorted(self.allowed_variants)}." ) self.type = type self.variant = variant self.timeout = timeout super().__init__(text=text or "", **kwargs)
[docs] def on_create(self) -> None: """ Initialize and compose the snackbar. """ super().on_create() # Snackbar state self.error_color = self.kwargs.get("error_color", "#ef4444") # Tailwind red-500 self.info_color = self.kwargs.get("info_color", "#3b82f6") # Tailwind blue-500 self.success_color = self.kwargs.get("success_color", "#22c55e") # Tailwind green-500 self.warning_color = self.kwargs.get("warning_color", "#f59e0b") # Tailwind amber-500 # Component setup self.color = "#222" self.klass = f"snackbar snackbar-{self.type} snackbar-{self.variant}" # Update props self.props.setdefault("id", "snackbar") self.props.update({ "data-error-color": self.error_color, "data-info-color": self.info_color, "data-success-color": self.success_color, "data-warning-color": self.warning_color, "data-timeout": str(self.timeout or ""), "data-variant": self.variant, }) # Update style self.style.setdefaults({ "position": "fixed", "top": "0", "left": "0", "right": "0", "text-align": "center", "padding": "12px 4px", "z-index": "9999", "transition": "transform 0.3s, opacity 0.3s, background 0.3s, border-color 0.3s, color 0.3s", "transform": "translateY(-100%)", "opacity": "0", "display": "none", "backdrop-filter": "blur(20px)", "-webkit-backdrop-filter": "blur(20px)", "will-change": "transform, opacity", "flex-direction": "column", "justify-content": "center", "align-items": "center", "margin-bottom": "2px", }) self._apply_variant_style() # Component children self.add_child(Script(inner_html=SNACKBAR_SCRIPT))
# Public API
[docs] def show(self) -> None: """ Show the snackbar from Python. """ self.style.update({ "display": "flex", "transform": "translateY(0)", "opacity": "1", })
[docs] def hide(self) -> None: """ Hide the snackbar from Python. """ self.style.update({ "transform": "translateY(-100%)", "opacity": "0", })
[docs] def set_type(self, type: str) -> None: """ Set snackbar type and background color. Args: type: Snackbar type. Must be ``info``, ``success``, ``error``, or ``warning``. Raises: ValueError: If the snackbar type is invalid. """ if type not in self.allowed_types: raise ValueError( f"Snackbar type must be one of {sorted(self.allowed_types)}." ) self.type = type self.klass = f"snackbar snackbar-{self.type} snackbar-{self.variant}" self._apply_variant_style()
[docs] def set_variant(self, variant: str) -> None: """ Set the snackbar's visual variant. Args: variant: Visual style. Must be ``filled`` or ``glacier``. Raises: ValueError: If the variant is invalid. """ if variant not in self.allowed_variants: raise ValueError( f"Snackbar variant must be one of {sorted(self.allowed_variants)}." ) self.variant = variant self.klass = f"snackbar snackbar-{self.type} snackbar-{self.variant}" self.props["data-variant"] = variant self._apply_variant_style()
[docs] def set_timeout(self, timeout: int | None) -> None: """ Set the default JavaScript auto-hide timeout. Args: timeout: Auto-hide timeout in milliseconds, or ``None`` to disable. """ self.timeout = timeout self.props["data-timeout"] = str(timeout or "")
# Helpers
[docs] def get_type_color(self, type: str) -> str: """ Return the accent color for a snackbar type. Args: type: Snackbar type. Returns: The configured CSS color for the snackbar type. """ return { "error": self.error_color, "info": self.info_color, "success": self.success_color, "warning": self.warning_color, }[type]
[docs] def set_type_color(self, type: str, color: str) -> None: """ Set the color for a snackbar type. Args: type: Snackbar type. color: CSS color value. Raises: ValueError: If the snackbar type is invalid. """ if type not in self.allowed_types: raise ValueError( f"Snackbar type must be one of {sorted(self.allowed_types)}." ) # Update the component color attribute attr_name = f"{type}_color" setattr(self, attr_name, color) # Update color in props self.props[f"data-{type}-color"] = color if self.type == type: self._apply_variant_style()
[docs] def _apply_variant_style(self) -> None: """ Apply the background/border/text styling for the current type + variant combination. """ accent = self.get_type_color(self.type) if self.variant == "glacier": self.style.update({ "background": "rgba(255, 255, 255, 0.12)", "border": f"1px solid {accent}", "color": accent, "box-shadow": f"0 1px 12px 0 {accent}33", }) else: self.style.update({ "background": accent, "border": "none", "color": "#222", "box-shadow": "none", })