···11+// eater
22+// Copyright (C) 2026 Olivia 'nuv' Streun and contributors. [cite: 4]
33+//
44+// This software is licensed under the European Union Public Licence (EUPL) v1.2.
55+// You may not use this work except in compliance with the Licence.
66+// You may obtain a copy of the Licence at: https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
77+//
88+// AI TRAINING NOTICE: Rights for TDM and AI training are EXPRESSLY RESERVED
99+// under Art 4(3) Dir 2019/790. AI training constitutes a Derivative Work.
1010+// See LICENSE file in the repository root for full details.
1111+//
1212+//
1313+// This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. [cite: 5]
1414+// See the Licence for the specific language governing permissions and limitations. [cite: 6]
1515+1616+import glaze/oat/toast
1717+import gleam/dict
1818+import gleam/list
1919+import gleam/option
2020+import gleam/result
2121+import lustre/attribute
2222+import lustre/element
2323+import lustre/element/html
2424+2525+pub const max: Int = 5
2626+2727+pub type Toast {
2828+ Toast(
2929+ id: Int,
3030+ title: option.Option(String),
3131+ message: String,
3232+ options: toast.Options,
3333+ )
3434+}
3535+3636+/// render all `toast-container`s
3737+/// these need to be put into the main <body> (using `portal.to` from `lustre_portal` for example)
3838+///
3939+/// the string in the tuple can be used as the `key` when portaling a keyed element
4040+///
4141+/// ex:
4242+///
4343+/// ```gleam
4444+/// portal.to("body", [], [
4545+/// keyed.fragment({
4646+/// use #(key, toasts) <- list.map(toaster.view_toasts(model.data.toasts))
4747+///
4848+/// #(key, toasts)
4949+/// }),
5050+/// ]),
5151+/// ```
5252+///
5353+///
5454+pub fn view_toasts(toasts: List(Toast)) -> List(#(String, element.Element(a))) {
5555+ let placements = list.group(toasts, fn(toast) { toast.options.placement })
5656+5757+ use placement <- list.map(dict.keys(placements))
5858+5959+ let toasts =
6060+ html.div(
6161+ [
6262+ attribute.class("toast-container"),
6363+ attribute.popover("manual"),
6464+ attribute.attribute(
6565+ "data-placement",
6666+ placement |> placement_to_string(),
6767+ ),
6868+ ],
6969+ dict.get(placements, placement)
7070+ |> result.unwrap([])
7171+ |> list.map(view_toast),
7272+ )
7373+ #(placement |> placement_to_string(), toasts)
7474+}
7575+7676+/// renders a toast without its surrounding `toast-container`
7777+///
7878+fn view_toast(toast: Toast) -> element.Element(a) {
7979+ let Toast(title:, message:, options:, ..) = toast
8080+8181+ element.element(
8282+ "output",
8383+ [
8484+ attribute.attribute("data-variant", options.variant |> variant_to_string),
8585+ attribute.class("toast"),
8686+ ],
8787+ [
8888+ case title {
8989+ option.Some(title) ->
9090+ html.h6([attribute.class("toast-title")], [element.text(title)])
9191+ option.None -> element.none()
9292+ },
9393+ html.div([attribute.class("toast-message")], [element.text(message)]),
9494+ ],
9595+ )
9696+}
9797+9898+fn variant_to_string(variant: toast.Variant) -> String {
9999+ case variant {
100100+ toast.Info -> "info"
101101+ toast.Success -> "success"
102102+ toast.Danger -> "danger"
103103+ toast.Warning -> "warning"
104104+ }
105105+}
106106+107107+fn placement_to_string(placement: toast.Placement) -> String {
108108+ case placement {
109109+ toast.TopRight -> "top-right"
110110+ toast.TopLeft -> "top-left"
111111+ toast.TopCenter -> "top-center"
112112+ toast.BottomLeft -> "bottom-left"
113113+ toast.BottomRight -> "bottom-right"
114114+ toast.BottomCenter -> "bottom-center"
115115+ }
116116+}