Skip to main content

tablo_ui/components/primitives/
sheet.rs

1// SYNC: topcoat-ui-registry@0.10.0 sha256:b1149fd0c5b263dc73d511ee056b56416aad7f570cfdac13a430ccb6d4047132 — do not hand-edit. Sync via `cargo xtask sync-topcoat-ui` (ADR-0007).
2use topcoat::{
3    Result,
4    runtime::Expr,
5    view::{Attributes, Child, StaticClass, View, class, component, view},
6};
7
8/// Classes for a viewport overlay with no edge padding. Set `display` only for the open
9/// state so the native closed state remains hidden.
10const OVERLAY: StaticClass = class!(
11    "fixed inset-0 z-50 size-full max-h-none max-w-none overflow-hidden \
12     bg-background/80 text-foreground backdrop-blur-sm open:flex",
13);
14
15/// Classes that fade the overlay in and out. `allow-discrete` keeps it displayed
16/// through the exit transition, and `@starting-style` supplies the entry opacity.
17const FADE: StaticClass = class!(
18    "opacity-0 open:opacity-100 starting:open:opacity-0 \
19     [transition:opacity_200ms_ease-out,display_200ms_allow-discrete]",
20);
21
22/// A panel that slides in from an edge of the page.
23///
24/// Pass a boolean to `open` for a fixed state, or a runtime expression to control it in
25/// the browser. Like [`dialog`](super::dialog::dialog), it needs application scripting
26/// for focus trapping and Escape dismissal.
27///
28/// Pass a `sheet_content` panel as children and choose its edge with `side`. Use dialog
29/// components to arrange its contents. `attrs` are forwarded to the `<dialog>`, with
30/// extra classes added to its classes.
31///
32/// ```ignore
33/// view! {
34///     sheet(
35///         open: filtering,
36///         sheet_content(
37///             dialog_header(
38///                 dialog_title("Filters")
39///                 dialog_description("Narrow the deployments below.")
40///             )
41///             (fields)
42///         )
43///     )
44/// }
45/// ```
46#[component]
47pub async fn sheet(
48    /// Whether the sheet shows.
49    #[into]
50    open: Expr<bool>,
51    /// Extra attributes for the `<dialog>` element.
52    #[default]
53    mut attrs: Attributes,
54    /// The sheet's content.
55    #[default]
56    child: Child<'_>,
57) -> Result<impl View> {
58    Ok(view! {
59        <dialog
60            :open=(open)
61            class=(class!(OVERLAY, FADE, attrs.remove("class")))
62            (attrs)
63        >
64            (child)
65        </dialog>
66    })
67}
68
69/// The edge a [`sheet_content`] lies against.
70///
71/// [`Default`] is `SheetSide::Right`, used when no side is given.
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73#[allow(dead_code)]
74pub enum SheetSide {
75    /// Along the left edge, full height.
76    Left,
77    /// Along the right edge at full height.
78    #[default]
79    Right,
80    /// Across the top edge, full width.
81    Top,
82    /// Across the bottom edge, full width.
83    Bottom,
84}
85
86impl SheetSide {
87    /// Classes that position the panel at the selected edge.
88    fn classes(self) -> StaticClass {
89        match self {
90            Self::Left => class!("mr-auto h-full w-full max-w-sm border-r"),
91            Self::Right => class!("ml-auto h-full w-full max-w-sm border-l"),
92            Self::Top => class!("mb-auto max-h-full w-full border-b"),
93            Self::Bottom => class!("mt-auto max-h-full w-full border-t"),
94        }
95    }
96
97    /// Classes that slide the panel in and out from the selected edge.
98    fn motion(self) -> StaticClass {
99        match self {
100            Self::Left => class!(
101                "-translate-x-full in-[[open]]:translate-x-0 \
102                 starting:in-[[open]]:-translate-x-full",
103            ),
104            Self::Right => class!(
105                "translate-x-full in-[[open]]:translate-x-0 \
106                 starting:in-[[open]]:translate-x-full",
107            ),
108            Self::Top => class!(
109                "-translate-y-full in-[[open]]:translate-y-0 \
110                 starting:in-[[open]]:-translate-y-full",
111            ),
112            Self::Bottom => class!(
113                "translate-y-full in-[[open]]:translate-y-0 \
114                 starting:in-[[open]]:translate-y-full",
115            ),
116        }
117    }
118}
119
120/// Classes for a sheet panel with vertically stacked content and internal scrolling.
121const CONTENT: StaticClass = class!(
122    "flex flex-col gap-4 overflow-y-auto border-border bg-card p-6 \
123     text-card-foreground shadow-sm [transition:translate_200ms_ease-out]",
124);
125
126/// The content panel inside a sheet.
127///
128/// `side` chooses its edge and defaults to `Right`. `attrs` are forwarded to the
129/// `<div>`, with extra classes added to its classes. Use them to adjust the panel's
130/// dimensions.
131#[component]
132pub async fn sheet_content(
133    /// The edge the panel lies against.
134    #[default]
135    side: SheetSide,
136    /// Extra attributes for the `<div>` element.
137    #[default]
138    mut attrs: Attributes,
139    /// The panel's sections.
140    #[default]
141    child: Child<'_>,
142) -> Result<impl View> {
143    Ok(view! {
144        <div
145            class=(class!(CONTENT, side.classes(), side.motion(), attrs.remove("class")))
146            (attrs)
147        >
148            (child)
149        </div>
150    })
151}