use tablo_ui::{card_content, card_description, card_header, card_title};
use topcoat::{
Result,
context::Cx,
icon::{IconData, icon},
view::{StaticClass, class, *},
};
use super::{
super::{
Schema,
condition::Condition,
fields::Field,
tree::{IntoSchema, Source, render_nodes},
},
Span, spanned,
};
const PANEL: StaticClass = class!(
"flex flex-col rounded-xl border border-border bg-card text-card-foreground shadow-xs",
"[&>[data-section-title]:not(:last-child)]:border-b [&>[data-section-title]]:border-border",
);
const SUMMARY: StaticClass = class!(
"flex cursor-pointer list-none items-center gap-2 px-6 py-4 outline-none",
"focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-details-marker]:hidden",
"[[open]>&]:border-b [[open]>&]:border-border",
);
const FLAT: StaticClass = class!(
"flex flex-col gap-4 [&:not(:first-of-type)]:border-t [&:not(:first-of-type)]:border-border",
"[&:not(:first-of-type)]:pt-6 [&:only-of-type>[data-section-title]]:hidden",
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Collapse {
Never,
Open,
Closed,
}
#[derive(Debug)]
struct Look {
title: crate::Text,
description: Option<crate::Text>,
icon: Option<IconData>,
collapse: Collapse,
aside: bool,
extra_class: Option<String>,
}
#[derive(Debug)]
pub struct Section<C = Schema> {
look: Look,
pub(crate) children: C,
pub(crate) condition: Option<Condition>,
pub(super) span: Option<Span>,
}
impl Section<()> {
pub fn new(title: impl Into<crate::Text>) -> Self {
Self {
look: Look {
title: title.into(),
description: None,
icon: None,
collapse: Collapse::Never,
aside: false,
extra_class: None,
},
children: (),
condition: None,
span: None,
}
}
pub fn schema<F>(self, children: impl IntoSchema<F>) -> Section<Schema<F>> {
self.holding(children.into_schema())
}
}
impl<C> Section<C> {
pub fn class(mut self, class: impl Into<String>) -> Self {
self.look.extra_class = Some(class.into());
self
}
pub fn description(mut self, description: impl Into<crate::Text>) -> Self {
self.look.description = Some(description.into());
self
}
pub fn icon(mut self, data: IconData) -> Self {
self.look.icon = Some(data);
self
}
pub fn collapsible(mut self) -> Self {
self.look.collapse = Collapse::Open;
self
}
pub fn collapsed(mut self) -> Self {
self.look.collapse = Collapse::Closed;
self
}
pub fn aside(mut self) -> Self {
self.look.aside = true;
self
}
pub(crate) fn holding<D>(self, children: D) -> Section<D> {
self.map(|_| children)
}
pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Section<D> {
Section {
look: self.look,
children: f(self.children),
condition: self.condition,
span: self.span,
}
}
pub(crate) fn chrome<'a>(&self, cx: &'a Cx, body: Option<BoxView<'a>>) -> BoxView<'a> {
section_chrome(cx, &self.look, self.span, body, false)
}
}
fn section_chrome<'a>(
cx: &'a Cx,
look: &Look,
span: Option<Span>,
body: Option<BoxView<'a>>,
errored: bool,
) -> BoxView<'a> {
spanned(cx, span, panel(cx, look, body, errored))
}
fn panel<'a>(cx: &'a Cx, look: &Look, body: Option<BoxView<'a>>, errored: bool) -> BoxView<'a> {
let extra = look.extra_class.clone();
let heading = heading(cx, look);
let description = look.description.clone();
match body {
Some(body) if look.aside => view! {
<div
class=(class!("grid gap-x-8 gap-y-4 md:grid-cols-3", extra.clone()))
data-section=""
>
<div class="flex flex-col gap-1.5" data-section-title="">
(heading)
if let Some(description) = description {
card_description((description))
}
</div>
<div class=(class!(PANEL, "md:col-span-2"))>
card_content(
attrs: attributes! { class="flex flex-col gap-6 py-6" },
(body)
)
</div>
</div>
}
.boxed(),
Some(body) if look.collapse != Collapse::Never => {
let open = look.collapse == Collapse::Open || errored;
view! {
<details
class=(class!(PANEL, extra.clone()))
data-section=""
open=(open)
>
<summary class=(SUMMARY) data-section-title="">
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
(heading)
if let Some(description) = description {
card_description((description))
}
</div>
icon(
data: tablo_ui::icons::CHEVRON_DOWN,
attrs: attributes! {
class="shrink-0 text-muted-foreground transition-transform \
[[open]>summary>&]:rotate-180"
}
)
</summary>
card_content(
attrs: attributes! { class="flex flex-col gap-6 py-6" },
(body)
)
</details>
}
.boxed()
}
body => view! {
<div class=(class!(PANEL, extra.clone())) data-section="">
card_header(
attrs: attributes! { class="py-4" data-section-title="" },
(heading)
if let Some(description) = description {
card_description((description))
}
)
if let Some(body) = body {
card_content(
attrs: attributes! { class="flex flex-col gap-6 py-6" },
(body)
)
}
</div>
}
.boxed(),
}
}
fn flat<'a>(cx: &'a Cx, look: &Look, span: Option<Span>, body: Option<BoxView<'a>>) -> BoxView<'a> {
let extra = look.extra_class.clone();
let heading = heading(cx, look);
let description = look.description.clone();
let view = view! {
<div class=(class!(FLAT, extra)) data-section="">
<div data-section-title="">(heading)</div>
if let Some(description) = description {
card_description((description))
}
if let Some(body) = body {
<div class="flex flex-col gap-6">(body)</div>
}
</div>
}
.boxed();
spanned(cx, span, view)
}
pub(crate) fn titled<'a>(cx: &'a Cx, title: crate::Text, body: BoxView<'a>) -> BoxView<'a> {
let look = Look {
title,
description: None,
icon: None,
collapse: Collapse::Never,
aside: false,
extra_class: None,
};
section_chrome(cx, &look, None, Some(body), false)
}
fn heading<'a>(cx: &'a Cx, look: &Look) -> BoxView<'a> {
let title = look.title.clone();
match look.icon.clone() {
Some(data) => view! {
<div class="flex items-center gap-2">
icon(
data: data,
attrs: attributes! { class="shrink-0 text-muted-foreground" }
)
card_title((title))
</div>
}
.boxed(),
None => view! { card_title((title)) }.boxed(),
}
}
impl Section<Schema> {
pub(crate) async fn render<'a>(
&self,
cx: &'a Cx,
fields: &[Field],
source: &Source<'_>,
) -> Result<BoxView<'a>> {
if self.children.nodes.is_empty() {
return Ok(if source.is_flat() {
flat(cx, &self.look, self.span, None)
} else {
section_chrome(cx, &self.look, self.span, None, false)
});
}
let errored = self
.children
.nodes
.iter()
.any(|node| node.has_error(fields, source));
let body = render_nodes(cx, &self.children.nodes, fields, source).await?;
if source.is_flat() {
return Ok(flat(cx, &self.look, self.span, Some(body)));
}
Ok(section_chrome(
cx,
&self.look,
self.span,
Some(body),
errored,
))
}
}