Packages

A shadcn/ui-inspired UI library for Gleam and Lustre

Current section

Files

Jump to
glizzy src ui modal.gleam
Raw

src/ui/modal.gleam

//// Modal component with focus trap support.
////
//// ## Usage with Focus Trap
////
//// ```gleam
//// import glizzy/ui/modal
//// import glizzy/ui/focus
//// import lustre/effect.{type Effect, batch}
////
//// type Model {
//// Model(is_open: Bool, focus_scope_id: String)
//// }
////
//// type Msg {
//// Open
//// Close
//// FocusScopeCreated(String)
//// }
////
//// // In init:
//// fn init() -> #(Model, Effect(Msg)) {
//// #(
//// Model(is_open: False, focus_scope_id: ""),
//// focus.create_scope("modal-content", FocusScopeCreated),
//// )
//// }
////
//// // In update:
//// fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
//// case msg {
//// Open -> #(
//// Model(..model, is_open: True),
//// batch([
//// focus.trap_focus(model.focus_scope_id, True),
//// focus.focus_first(model.focus_scope_id),
//// ])
//// )
//// Close -> #(
//// Model(..model, is_open: False),
//// focus.trap_focus(model.focus_scope_id, False)
//// )
//// FocusScopeCreated(scope_id) -> #(
//// Model(..model, focus_scope_id: scope_id),
//// effect.none()
//// )
//// }
//// }
////
//// // In view:
//// fn view(model: Model) {
//// modal.modal([
//// attribute("id", "modal-container"),
//// ], [
//// modal.underlay([]),
//// modal.content([
//// attribute("id", "modal-content"),
//// attribute("data-state", case model.is_open {
//// True -> "open"
//// False -> "closed"
//// }),
//// ], [
//// // ... modal content
//// ])
//// ])
//// }
//// ```
// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import gleam/string
import lustre/attribute.{type Attribute, attribute, class}
import lustre/element.{type Element}
import lustre/element/html
import ui/size as size_helpers
pub type Variant {
Default
Muted
}
pub type Size {
Small
Medium
Large
}
pub fn modal(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class("fixed inset-0 z-50 flex items-center justify-center"),
attribute("role", "dialog"),
attribute("aria-modal", "true"),
..attributes
],
children,
)
}
pub fn underlay(attributes: List(Attribute(a))) -> Element(a) {
html.div([class("fixed inset-0 bg-black/70 z-40"), ..attributes], [])
}
pub fn content(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.div(
[
class(
[
"relative z-50 w-full rounded-md border bg-background p-6 shadow-lg",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
]
|> string.join(" "),
),
attribute("role", "document"),
..attributes
],
children,
)
}
pub fn title(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.h2(
[
class(
"text-lg font-semibold leading-none tracking-tight text-foreground mb-2",
),
attribute("id", "modal-title"),
..attributes
],
children,
)
}
pub fn description(
attributes: List(Attribute(a)),
children: List(Element(a)),
) -> Element(a) {
html.p(
[
class("text-sm text-muted-foreground mt-2"),
attribute("id", "modal-description"),
..attributes
],
children,
)
}
pub fn variant(v: Variant) -> Attribute(a) {
case v {
Default -> class("bg-background text-foreground")
Muted -> class("bg-muted text-muted-foreground")
}
}
pub fn size(s: Size) -> Attribute(a) {
case s {
Small -> class(size_helpers.container_class(size_helpers.Small))
Medium -> class(size_helpers.container_class(size_helpers.Medium))
Large -> class(size_helpers.container_class(size_helpers.Large))
}
}
pub fn aria_label(label: String) -> Attribute(a) {
attribute("aria-label", label)
}
pub fn aria_describedby(id: String) -> Attribute(a) {
attribute("aria-describedby", id)
}
pub fn aria_labelledby(id: String) -> Attribute(a) {
attribute("aria-labelledby", id)
}