Skip to main content

tablo_core/schema/
layouts.rs

1//! Holds the `Section`, `Group`, and `Grid` containers that compose form layout.
2
3use tablo_ui::{card_content, card_header, card_title, field_group as ui_field_group};
4use topcoat::{
5    Result,
6    context::Cx,
7    view::{StaticClass, class, *},
8};
9
10use super::{
11    Schema,
12    fields::Field,
13    tree::{IntoSchema, Source, render_nodes},
14};
15
16/// Renders a `Section`'s titled panel.
17const PANEL: StaticClass = class!(
18    "flex flex-col gap-5 rounded-xl border border-border bg-card py-6 text-card-foreground shadow-sm"
19);
20
21/// Holds a titled container with an optional child `Schema`.
22#[derive(Debug)]
23pub struct Section {
24    title: String,
25    pub(crate) children: Schema,
26    extra_class: Option<String>,
27}
28
29impl Section {
30    pub fn new(title: impl Into<String>) -> Self {
31        Self {
32            title: title.into(),
33            children: Schema::empty(),
34            extra_class: None,
35        }
36    }
37
38    pub fn schema(mut self, children: impl IntoSchema) -> Self {
39        self.children = children.into_schema();
40        self
41    }
42
43    /// Adds an additive `class` hook on the panel container.
44    pub fn class(mut self, class: impl Into<String>) -> Self {
45        self.extra_class = Some(class.into());
46        self
47    }
48
49    pub(crate) async fn render<'a>(
50        &self,
51        cx: &'a Cx,
52        fields: &[Field],
53        source: &Source<'_>,
54    ) -> Result<BoxView<'a>> {
55        let title = self.title.clone();
56        let extra = self.extra_class.clone();
57        if !self.children.nodes.is_empty() {
58            let child_view = render_nodes(cx, &self.children.nodes, fields, source).await?;
59            Ok(view! {
60                cx =>
61                <div class=(class!(PANEL, extra.clone()))>
62                    card_header(card_title((title)))
63                    card_content(
64                        attrs: attributes! { class="flex flex-col gap-6" },
65                        (child_view)
66                    )
67                </div>
68            }
69            .boxed())
70        } else {
71            Ok(view! {
72                cx =>
73                <div class=(class!(PANEL, extra.clone()))>
74                    card_header(card_title((title)))
75                </div>
76            }
77            .boxed())
78        }
79    }
80}
81
82/// Holds an unlabelled container for grouping fields.
83#[derive(Debug, Default)]
84pub struct Group {
85    pub(crate) children: Schema,
86}
87
88impl Group {
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    pub fn schema(mut self, children: impl IntoSchema) -> Self {
94        self.children = children.into_schema();
95        self
96    }
97
98    pub(crate) async fn render<'a>(
99        &self,
100        cx: &'a Cx,
101        fields: &[Field],
102        source: &Source<'_>,
103    ) -> Result<BoxView<'a>> {
104        let child_view = render_nodes(cx, &self.children.nodes, fields, source).await?;
105        Ok(view! { cx => ui_field_group((child_view)) }.boxed())
106    }
107}
108
109/// Holds a column container with `cols` clamped to 1..12.
110#[derive(Debug)]
111pub struct Grid {
112    cols: u8,
113    pub(crate) children: Schema,
114}
115
116impl Grid {
117    pub fn new(cols: u8) -> Self {
118        Self {
119            cols: cols.clamp(1, 12),
120            children: Schema::empty(),
121        }
122    }
123
124    pub fn schema(mut self, children: impl IntoSchema) -> Self {
125        self.children = children.into_schema();
126        self
127    }
128
129    pub(crate) async fn render<'a>(
130        &self,
131        cx: &'a Cx,
132        fields: &[Field],
133        source: &Source<'_>,
134    ) -> Result<BoxView<'a>> {
135        // Static literals for Tailwind scanner — `format!("grid grid-cols-{}")` would be
136        // purged because Tailwind only sees literal substrings.
137        let class: &'static str = match self.cols {
138            1 => "grid grid-cols-1 gap-4",
139            2 => "grid grid-cols-2 gap-4",
140            3 => "grid grid-cols-3 gap-4",
141            4 => "grid grid-cols-4 gap-4",
142            5 => "grid grid-cols-5 gap-4",
143            6 => "grid grid-cols-6 gap-4",
144            7 => "grid grid-cols-7 gap-4",
145            8 => "grid grid-cols-8 gap-4",
146            9 => "grid grid-cols-9 gap-4",
147            10 => "grid grid-cols-10 gap-4",
148            11 => "grid grid-cols-11 gap-4",
149            _ => "grid grid-cols-12 gap-4",
150        };
151        let child_view = render_nodes(cx, &self.children.nodes, fields, source).await?;
152        Ok(view! { cx => <div class=(class)>(child_view)</div> }.boxed())
153    }
154}
155
156#[cfg(test)]
157mod tests;