1use tablo_ui::{card_content, card_header, card_title, field_group as ui_field_group};
5use topcoat::{
6 Result,
7 context::Cx,
8 view::{StaticClass, class, *},
9};
10
11use super::{
12 Schema,
13 condition::{Condition, Watched},
14 fields::Field,
15 tree::{IntoSchema, Source, render_nodes},
16};
17use crate::detail::{Detail, IntoDetail};
18
19const PANEL: StaticClass = class!(
21 "flex flex-col gap-5 rounded-xl border border-border bg-card py-6 text-card-foreground shadow-sm"
22);
23
24#[derive(Debug)]
27pub struct Section<C = Schema> {
28 title: String,
29 pub(crate) children: C,
30 extra_class: Option<String>,
31 pub(crate) condition: Option<Condition>,
32}
33
34impl Section<()> {
35 pub fn new(title: impl Into<String>) -> Self {
36 Self {
37 title: title.into(),
38 children: (),
39 extra_class: None,
40 condition: None,
41 }
42 }
43
44 pub fn schema<F>(self, children: impl IntoSchema<F>) -> Section<Schema<F>> {
46 self.holding(children.into_schema())
47 }
48
49 pub fn columns<M>(self, children: impl IntoDetail<M>) -> Section<Detail<M>> {
51 self.holding(children.into_detail())
52 }
53}
54
55impl<C> Section<C> {
56 pub fn class(mut self, class: impl Into<String>) -> Self {
58 self.extra_class = Some(class.into());
59 self
60 }
61
62 pub(crate) fn holding<D>(self, children: D) -> Section<D> {
63 self.map(|_| children)
64 }
65
66 pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Section<D> {
67 Section {
68 title: self.title,
69 children: f(self.children),
70 extra_class: self.extra_class,
71 condition: self.condition,
72 }
73 }
74
75 pub(crate) fn chrome<'a>(&self, cx: &'a Cx, body: Option<BoxView<'a>>) -> BoxView<'a> {
77 let title = self.title.clone();
78 let extra = self.extra_class.clone();
79 match body {
80 Some(body) => view! {
81 cx =>
82 <div class=(class!(PANEL, extra.clone()))>
83 card_header(card_title((title)))
84 card_content(
85 attrs: attributes! { class="flex flex-col gap-6" },
86 (body)
87 )
88 </div>
89 }
90 .boxed(),
91 None => view! {
92 cx =>
93 <div class=(class!(PANEL, extra.clone()))>
94 card_header(card_title((title)))
95 </div>
96 }
97 .boxed(),
98 }
99 }
100}
101
102impl Section<Schema> {
103 pub(crate) async fn render<'a>(
104 &self,
105 cx: &'a Cx,
106 fields: &[Field],
107 source: &Source<'_>,
108 ) -> Result<BoxView<'a>> {
109 let body = if self.children.nodes.is_empty() {
110 None
111 } else {
112 Some(render_nodes(cx, &self.children.nodes, fields, source).await?)
113 };
114 Ok(self.chrome(cx, body))
115 }
116}
117
118#[derive(Debug)]
120pub struct Group<C = Schema> {
121 pub(crate) children: C,
122 pub(crate) condition: Option<Condition>,
123}
124
125impl Default for Group<()> {
126 fn default() -> Self {
127 Self {
128 children: (),
129 condition: None,
130 }
131 }
132}
133
134impl Group<()> {
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn schema<F>(self, children: impl IntoSchema<F>) -> Group<Schema<F>> {
141 self.holding(children.into_schema())
142 }
143
144 pub fn columns<M>(self, children: impl IntoDetail<M>) -> Group<Detail<M>> {
146 self.holding(children.into_detail())
147 }
148}
149
150impl<C> Group<C> {
151 pub(crate) fn holding<D>(self, children: D) -> Group<D> {
152 self.map(|_| children)
153 }
154
155 pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Group<D> {
156 Group {
157 children: f(self.children),
158 condition: self.condition,
159 }
160 }
161
162 pub(crate) fn chrome<'a>(&self, cx: &'a Cx, body: BoxView<'a>) -> BoxView<'a> {
164 view! { cx => ui_field_group((body)) }.boxed()
165 }
166}
167
168impl Group<Schema> {
169 pub(crate) async fn render<'a>(
170 &self,
171 cx: &'a Cx,
172 fields: &[Field],
173 source: &Source<'_>,
174 ) -> Result<BoxView<'a>> {
175 let body = render_nodes(cx, &self.children.nodes, fields, source).await?;
176 Ok(self.chrome(cx, body))
177 }
178}
179
180#[derive(Debug)]
183pub struct Grid<C = Schema> {
184 cols: u8,
185 pub(crate) children: C,
186 pub(crate) condition: Option<Condition>,
187}
188
189impl Grid<()> {
190 pub fn new(cols: u8) -> Self {
191 Self {
192 cols: cols.clamp(1, 12),
193 children: (),
194 condition: None,
195 }
196 }
197
198 pub fn schema<F>(self, children: impl IntoSchema<F>) -> Grid<Schema<F>> {
200 self.holding(children.into_schema())
201 }
202
203 pub fn columns<M>(self, children: impl IntoDetail<M>) -> Grid<Detail<M>> {
205 self.holding(children.into_detail())
206 }
207}
208
209impl<C> Grid<C> {
210 pub(crate) fn holding<D>(self, children: D) -> Grid<D> {
211 self.map(|_| children)
212 }
213
214 pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Grid<D> {
215 Grid {
216 cols: self.cols,
217 children: f(self.children),
218 condition: self.condition,
219 }
220 }
221
222 pub(crate) fn chrome<'a>(&self, cx: &'a Cx, body: BoxView<'a>) -> BoxView<'a> {
224 let class: &'static str = match self.cols {
227 1 => "grid grid-cols-1 gap-4",
228 2 => "grid grid-cols-2 gap-4",
229 3 => "grid grid-cols-3 gap-4",
230 4 => "grid grid-cols-4 gap-4",
231 5 => "grid grid-cols-5 gap-4",
232 6 => "grid grid-cols-6 gap-4",
233 7 => "grid grid-cols-7 gap-4",
234 8 => "grid grid-cols-8 gap-4",
235 9 => "grid grid-cols-9 gap-4",
236 10 => "grid grid-cols-10 gap-4",
237 11 => "grid grid-cols-11 gap-4",
238 _ => "grid grid-cols-12 gap-4",
239 };
240 view! { cx => <div class=(class)>(body)</div> }.boxed()
241 }
242}
243
244impl Grid<Schema> {
245 pub(crate) async fn render<'a>(
246 &self,
247 cx: &'a Cx,
248 fields: &[Field],
249 source: &Source<'_>,
250 ) -> Result<BoxView<'a>> {
251 let body = render_nodes(cx, &self.children.nodes, fields, source).await?;
252 Ok(self.chrome(cx, body))
253 }
254}
255
256macro_rules! conditional_blocks {
258 ($($ty:ident),+ $(,)?) => {
259 $(
260 impl<F> $ty<Schema<F>> {
261 pub fn visible_when(
265 mut self,
266 watched: &impl Watched<F>,
267 values: impl IntoIterator<Item = impl Into<String>>,
268 ) -> Self {
269 self.condition = Some(Condition::new(watched, values));
270 self
271 }
272 }
273 )+
274 };
275}
276
277conditional_blocks!(Section, Group, Grid);