///|
/// Edge from which a [`sheet_content`] surface enters the viewport.
pub(all) enum SheetSide {
SheetTop
SheetRight
SheetBottom
SheetLeft
} derive(Debug, Eq)
///|
/// Explicit compound-component scope created by [`sheet`].
struct SheetScope(DialogScope)
///|
const SheetPositionerStyle : String = "position:fixed;inset:0;z-index:51;display:flex;overflow:hidden;pointer-events:none"
///|
const SheetSurfaceStyle : String = "position:relative;display:flex;min-width:0;min-height:0;flex-direction:column;gap:1rem;overflow:auto;background:var(--rui-popover,var(--rui-background,oklch(1 0 0)));color:var(--rui-popover-foreground,var(--rui-foreground,oklch(0.145 0 0)));padding:1.5rem;box-shadow:0 16px 48px rgb(0 0 0 / 0.2);opacity:var(--rui-sheet-opacity,1);transform:var(--rui-sheet-transform,translate3d(0,0,0));will-change:opacity,transform;pointer-events:auto"
///|
const SheetCloseButtonStyle : String = "position:absolute;top:1rem;inset-inline-end:1rem;background:var(--rui-close-bg,var(--rui-button-bg,var(--rui-button-base-bg,transparent)));color:var(--rui-close-fg,var(--rui-button-fg,var(--rui-button-base-fg,currentColor)));box-shadow:var(--rui-close-shadow,var(--rui-button-shadow,var(--rui-button-base-shadow,none)));opacity:var(--rui-close-opacity,var(--rui-close-base-opacity,0.7))"
///|
fn sheet_side_value(side : SheetSide) -> String {
match side {
SheetTop => "top"
SheetRight => "right"
SheetBottom => "bottom"
SheetLeft => "left"
}
}
///|
fn sheet_positioner_side_style(side : SheetSide) -> String {
match side {
SheetTop => "align-items:flex-start"
SheetRight => "justify-content:flex-end"
SheetBottom => "align-items:flex-end"
SheetLeft => "justify-content:flex-start"
}
}
///|
fn sheet_surface_side_style(side : SheetSide) -> String {
match side {
SheetTop =>
"width:100%;max-height:85vh;border-bottom:1px solid var(--rui-border,oklch(0.922 0 0))"
SheetRight =>
"width:min(85vw,24rem);height:100%;border-left:1px solid var(--rui-border,oklch(0.922 0 0))"
SheetBottom =>
"width:100%;max-height:85vh;border-top:1px solid var(--rui-border,oklch(0.922 0 0))"
SheetLeft =>
"width:min(85vw,24rem);height:100%;border-right:1px solid var(--rui-border,oklch(0.922 0 0))"
}
}
///|
fn sheet_closed_transform(side : SheetSide) -> String {
match side {
SheetTop => "translate3d(0,-2.5rem,0)"
SheetRight => "translate3d(2.5rem,0,0)"
SheetBottom => "translate3d(0,2.5rem,0)"
SheetLeft => "translate3d(-2.5rem,0,0)"
}
}
///|
fn sheet_motion_style(side : SheetSide, open : Bool) -> String {
let closed_transform = sheet_closed_transform(side)
if open {
"--rui-sheet-closed-transform:\{closed_transform};--rui-sheet-opacity:1;--rui-sheet-transform:translate3d(0,0,0);animation:rui-sheet-enter 200ms ease-in-out"
} else {
"--rui-sheet-closed-transform:\{closed_transform};--rui-sheet-opacity:0;--rui-sheet-transform:\{closed_transform};animation:rui-sheet-exit 200ms ease-in-out;pointer-events:none"
}
}
///|
fn sheet_close_icon(scope : DialogScope, label : String) -> @html.Html {
let close_attrs = @html.Attrs::build()
.data_set("rui-close-icon", "")
.data_set("state", dialog_state_value(scope.open))
dialog_control_button(
slot="sheet-close",
command=scope.close_command,
variant=Ghost,
size=IconSm,
disabled=false,
id=None,
class=None,
title=None,
aria_label=Some(label),
on_click=None,
attrs=Some(close_attrs),
style=[SheetCloseButtonStyle],
ui_x_icon(),
)
}
///|
/// Create a stateful edge sheet backed by native dialog behavior. Modal sheets
/// enter the browser top layer; `modal=false` leaves the page interactive. The
/// returned value participates in Rabbita's incremental renderer.
pub fn sheet(
id~ : String,
default_open? : Bool = false,
modal? : Bool = true,
close_on_escape? : Bool = true,
on_open_change? : @cmd.Emit[Bool],
on_close? : @cmd.Emit[String],
children : (SheetScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
dialog(
id~,
default_open~,
modal~,
close_on_escape~,
on_open_change?,
on_close?,
scope => children(SheetScope(scope)),
)
}
///|
pub fn[C : @html.IsChildren] sheet_trigger(
scope : SheetScope,
variant? : ButtonVariant = Outline,
size? : ButtonSize = Default,
disabled? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
on_click? : @cmd.Cmd,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let dialog_scope = scope.0
let trigger_attrs = dialog_state_attrs(
attrs,
"sheet-trigger",
dialog_scope.open,
)
.aria_haspopup("dialog")
.aria_controls(dialog_scope.id)
.aria_expanded(ui_bool(dialog_scope.open))
dialog_control_button(
slot="sheet-trigger",
command=dialog_scope.open_command,
variant~,
size~,
disabled~,
id~,
class~,
title~,
aria_label~,
on_click~,
attrs=Some(trigger_attrs),
style~,
children,
)
}
///|
/// Render an edge-anchored sheet surface. Caller styles are appended after the
/// built-in Vega recipe, so CSS variables or individual properties can be
/// overridden without copying the component.
pub fn[C : @html.IsChildren] sheet_content(
scope : SheetScope,
side? : SheetSide = SheetRight,
close_on_overlay? : Bool = true,
show_close? : Bool = true,
close_label? : String = "Close",
described? : Bool = true,
aria_label? : String,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
overlay_attrs? : @html.Attrs,
overlay_style? : Array[String] = [],
native_attrs? : @html.Attrs,
native_style? : Array[String] = [],
children : C,
) -> @html.Html {
let dialog_scope = scope.0
let native_element_attrs = dialog_state_attrs(
native_attrs,
"sheet-native",
dialog_scope.open,
)
.data_set("modal", ui_bool(dialog_scope.modal))
.data_set("close-on-outside", ui_bool(close_on_overlay))
.data_set("close-on-escape", ui_bool(dialog_scope.close_on_escape))
if dialog_scope.modal {
ignore(native_element_attrs.aria_modal("true"))
}
if aria_label is Some(label) {
ignore(native_element_attrs.aria_label(label))
} else {
ignore(native_element_attrs.aria_labelledby(dialog_scope.title_id()))
}
if described {
ignore(native_element_attrs.aria_describedby(dialog_scope.description_id()))
}
let overlay_element_attrs = dialog_state_attrs(
overlay_attrs,
"sheet-overlay",
dialog_scope.open,
)
if close_on_overlay {
ignore(
overlay_element_attrs.on_click(_ => {
if ui_dialog_should_close_outside() {
dialog_scope.close_command
} else {
@cmd.none
}
}),
)
}
let content_attrs = dialog_state_attrs(
attrs,
"sheet-content",
dialog_scope.open,
).data_set("side", sheet_side_value(side))
let positioner_attrs = dialog_state_attrs(
None,
"sheet-positioner",
dialog_scope.open,
).data_set("side", sheet_side_value(side))
let content = @html.div(
style=ui_styles(
[
UiBoxSizing,
SheetSurfaceStyle,
sheet_surface_side_style(side),
sheet_motion_style(side, dialog_scope.open),
],
style,
),
id?,
class?,
title?,
hidden?,
attrs=content_attrs,
[
@html.div(style=[UiBoxSizing, "display:contents"], children),
if show_close {
sheet_close_icon(dialog_scope, close_label)
} else {
@html.nothing
},
],
)
let open = dialog_scope.native_open
let on_close = dialog_scope.on_close
let on_cancel = dialog_scope.on_cancel
let closedby = if !dialog_scope.modal {
"none"
} else if close_on_overlay {
"any"
} else if dialog_scope.close_on_escape {
"closerequest"
} else {
"none"
}
@html.dialog(
style=ui_styles([UiBoxSizing, DialogNativeStyle], native_style),
id=dialog_scope.id,
open?,
closedby~,
on_close?,
on_cancel?,
attrs=native_element_attrs,
[
if dialog_scope.modal {
@html.div(
style=ui_styles(
[
UiBoxSizing,
DialogOverlayStyle,
dialog_overlay_motion_style(dialog_scope.open),
],
overlay_style,
),
attrs=overlay_element_attrs,
@html.nothing,
)
} else {
@html.nothing
},
@html.div(
style=[
UiBoxSizing,
SheetPositionerStyle,
sheet_positioner_side_style(side),
],
attrs=positioner_attrs,
content,
),
],
)
}
///|
pub fn[C : @html.IsChildren] sheet_header(
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
@html.div(
style=ui_styles([UiBoxSizing, DialogHeaderStyle], style),
id?,
class?,
title?,
attrs=ui_attrs(attrs).data_set("slot", "sheet-header"),
children,
)
}
///|
pub fn[C : @html.IsChildren] sheet_footer(
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
@html.div(
style=ui_styles([UiBoxSizing, DialogFooterStyle], style),
id?,
class?,
title?,
attrs=ui_attrs(attrs).data_set("slot", "sheet-footer"),
children,
)
}
///|
pub fn[C : @html.IsChildren] sheet_title(
scope : SheetScope,
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let element_id = id.unwrap_or(scope.0.title_id())
@html.h2(
style=ui_styles([UiBoxSizing, DialogTitleStyle], style),
id=element_id,
class?,
title?,
attrs=ui_attrs(attrs).data_set("slot", "sheet-title"),
children,
)
}
///|
pub fn[C : @html.IsChildren] sheet_description(
scope : SheetScope,
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let element_id = id.unwrap_or(scope.0.description_id())
@html.p(
style=ui_styles([UiBoxSizing, DialogDescriptionStyle], style),
id=element_id,
class?,
title?,
attrs=ui_attrs(attrs).data_set("slot", "sheet-description"),
children,
)
}
///|
pub fn[C : @html.IsChildren] sheet_close(
scope : SheetScope,
variant? : ButtonVariant = Outline,
size? : ButtonSize = Default,
disabled? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
on_click? : @cmd.Cmd,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
dialog_control_button(
slot="sheet-close",
command=scope.0.close_command,
variant~,
size~,
disabled~,
id~,
class~,
title~,
aria_label~,
on_click~,
attrs~,
style~,
children,
)
}