diff --git a/fasthtml/toaster.py b/fasthtml/toaster.py index d0ae6c16..5856643b 100644 --- a/fasthtml/toaster.py +++ b/fasthtml/toaster.py @@ -13,9 +13,17 @@ } .fh-toast { background-color: #333; color: white; - padding: 12px 20px; border-radius: 4px; margin-bottom: 10px; + padding: 12px 28px 12px 20px; border-radius: 4px; margin-bottom: 10px; max-width: 80%; width: auto; text-align: center; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + position: relative; +} +.fh-toast-dismiss { + position:absolute; top:0.2em; right:0.4em; + line-height:1em; padding: 0 0.2em 0.2em 0.2em; border-radius:inherit; + transform:scaleY(0.8); transform:scaleX(1.); + pointer-events:auto; cursor:pointer; + background-color:inherit; color:inherit; filter:brightness(0.85); } .fh-toast-info { background-color: #2196F3; } .fh-toast-success { background-color: #4CAF50; } @@ -24,21 +32,25 @@ """ toast_js = """ -export function proc_htmx(sel, func) { - htmx.onLoad(elt => { +export function proc_htmx(sel, func) {{ + htmx.onLoad(elt => {{ const elements = any(sel, elt, false); if (elt.matches && elt.matches(sel)) elements.unshift(elt); elements.forEach(func); - }); -} -proc_htmx('.fh-toast-container', async function(toast) { + }}); +}} +proc_htmx('.fh-toast-container', async function(toast) {{ await sleep(100); toast.style.opacity = '0.8'; - await sleep(3000); + await sleep({duration}); toast.style.opacity = '0'; await sleep(300); toast.remove(); -}); +}}); + +proc_htmx('.fh-toast-dismiss', function(elem) {{ + elem.addEventListener('click', (e) => {{ e.target.parentElement.remove() }}); +}}); """ def add_toast(sess, message, typ="info"): @@ -46,14 +58,12 @@ def add_toast(sess, message, typ="info"): sess.setdefault(sk, []).append((message, typ)) def render_toasts(sess): - toasts = [Div(msg, cls=f"fh-toast fh-toast-{typ}") for msg,typ in sess.pop(sk, [])] - return Div(Div(*toasts, cls="fh-toast-container"), - hx_swap_oob="afterbegin:body") + toasts = [Div(msg, Span('x', cls='fh-toast-dismiss'), cls=f"fh-toast fh-toast-{typ}") for msg,typ in sess.pop(sk, [])] + return Div(Div(*toasts, cls="fh-toast-container"), hx_swap_oob="afterbegin:body") def toast_after(resp, req, sess): if sk in sess and (not resp or isinstance(resp, (tuple,FT,FtResponse))): req.injects.append(render_toasts(sess)) -def setup_toasts(app): - app.hdrs += (Style(toast_css), Script(toast_js, type="module")) +def setup_toasts(app, duration:float=10.): + app.hdrs += (Style(toast_css), Script(toast_js.format(duration=1000*duration), type="module")) app.after.append(toast_after) - diff --git a/nbs/tutorials/quickstart_for_web_devs.ipynb b/nbs/tutorials/quickstart_for_web_devs.ipynb index 5b981a84..a12398d5 100644 --- a/nbs/tutorials/quickstart_for_web_devs.ipynb +++ b/nbs/tutorials/quickstart_for_web_devs.ipynb @@ -1280,7 +1280,9 @@ "\n", "1. `setup_toasts` is a helper function that adds toast dependencies. Usually this would be declared right after `fast_app()`\n", "2. Toasts require sessions\n", - "3. Views with Toasts must return FT or FtResponse components." + "3. Views with Toasts must return FT or FtResponse components.\n", + "\n", + "💡 `setup_toasts` takes a `duration` input that allows you to specify how long a toast will be visible before disappearing. For example `setup_toasts(duration=5)` sets the toasts duration to 5 seconds. By default toasts disappear after 10 seconds." ] }, {