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}