use tablo_ui::field_group as ui_field_group;
use topcoat::{
Result,
context::Cx,
icon::{IconData, icon},
view::{StaticClass, class, *},
};
use super::{
super::Color,
Schema,
condition::{Condition, Watched},
fields::Field,
tree::{IntoSchema, Node, Retype, Source, render_nodes},
};
mod section;
mod span;
mod wizard;
pub use section::Section;
pub(crate) use section::titled;
pub(crate) use span::{Span, spanned};
pub use wizard::{Step, Wizard};
#[derive(Debug)]
pub struct Group<C = Schema> {
pub(crate) children: C,
pub(crate) condition: Option<Condition>,
span: Option<Span>,
}
impl Default for Group<()> {
fn default() -> Self {
Self {
children: (),
condition: None,
span: None,
}
}
}
impl Group<()> {
pub fn new() -> Self {
Self::default()
}
pub fn schema<F>(self, children: impl IntoSchema<F>) -> Group<Schema<F>> {
self.holding(children.into_schema())
}
}
impl<C> Group<C> {
pub(crate) fn holding<D>(self, children: D) -> Group<D> {
self.map(|_| children)
}
pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Group<D> {
Group {
children: f(self.children),
condition: self.condition,
span: self.span,
}
}
pub(crate) fn chrome<'a>(&self, cx: &'a Cx, body: BoxView<'a>) -> BoxView<'a> {
group_chrome(cx, self.span, body)
}
}
fn group_chrome<'a>(cx: &'a Cx, span: Option<Span>, body: BoxView<'a>) -> BoxView<'a> {
spanned(cx, span, view! { ui_field_group((body)) }.boxed())
}
impl Group<Schema> {
pub(crate) async fn render<'a>(
&self,
cx: &'a Cx,
fields: &[Field],
source: &Source<'_>,
) -> Result<BoxView<'a>> {
let body = render_nodes(cx, &self.children.nodes, fields, source).await?;
Ok(self.chrome(cx, body))
}
}
#[derive(Debug)]
pub struct Grid<C = Schema> {
cols: u8,
pub(crate) children: C,
pub(crate) condition: Option<Condition>,
span: Option<Span>,
}
impl Grid<()> {
pub fn new(cols: u8) -> Self {
Self {
cols: cols.clamp(1, 12),
children: (),
condition: None,
span: None,
}
}
pub fn schema<F>(self, children: impl IntoSchema<F>) -> Grid<Schema<F>> {
self.holding(children.into_schema())
}
}
impl<C> Grid<C> {
pub(crate) fn holding<D>(self, children: D) -> Grid<D> {
self.map(|_| children)
}
pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Grid<D> {
Grid {
cols: self.cols,
children: f(self.children),
condition: self.condition,
span: self.span,
}
}
pub(crate) fn chrome<'a>(&self, cx: &'a Cx, body: BoxView<'a>) -> BoxView<'a> {
grid_chrome(cx, self.cols, self.span, body)
}
}
fn grid_chrome<'a>(cx: &'a Cx, cols: u8, span: Option<Span>, body: BoxView<'a>) -> BoxView<'a> {
let class: &'static str = match cols {
1 => "grid grid-cols-1 gap-4 [--tablo-cols:1]",
2 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @md:grid-cols-2 @md:[--tablo-cols:2]",
3 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @2xl:grid-cols-3 @2xl:[--tablo-cols:3]",
4 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-4 @4xl:[--tablo-cols:4]",
5 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-5 @4xl:[--tablo-cols:5]",
6 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-6 @4xl:[--tablo-cols:6]",
7 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-7 @4xl:[--tablo-cols:7]",
8 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-8 @4xl:[--tablo-cols:8]",
9 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-9 @4xl:[--tablo-cols:9]",
10 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-10 @4xl:[--tablo-cols:10]",
11 => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-11 @4xl:[--tablo-cols:11]",
_ => "grid grid-cols-1 gap-4 [--tablo-cols:1] @4xl:grid-cols-12 @4xl:[--tablo-cols:12]",
};
let grid = view! { <div class="@container"><div class=(class)>(body)</div></div> }.boxed();
spanned(cx, span, grid)
}
impl Grid<Schema> {
pub(crate) async fn render<'a>(
&self,
cx: &'a Cx,
fields: &[Field],
source: &Source<'_>,
) -> Result<BoxView<'a>> {
let body = render_nodes(cx, &self.children.nodes, fields, source).await?;
Ok(self.chrome(cx, body))
}
}
macro_rules! conditional_blocks {
($($ty:ident),+ $(,)?) => {
$(
impl<F> $ty<Schema<F>> {
pub fn visible_when(
mut self,
watched: &impl Watched<F>,
values: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.condition = Some(Condition::new(watched, values));
self
}
}
)+
};
}
conditional_blocks!(Section, Group, Grid, Tabs, Tab, Wizard, Step);
macro_rules! spanning_blocks {
($($ty:ident),+ $(,)?) => {
$(
impl<C> $ty<C> {
pub fn span(mut self, columns: u8) -> Self {
self.span = Some(Span::columns(columns));
self
}
pub fn span_full(mut self) -> Self {
self.span = Some(Span::Full);
self
}
}
)+
};
}
spanning_blocks!(Section, Group, Grid, Tabs, Wizard);
const TAB_LIST: StaticClass = class!(
"inline-flex h-9 w-fit max-w-full items-center overflow-x-auto overflow-y-hidden rounded-lg \
bg-muted p-[3px] text-muted-foreground"
);
const TAB_TRIGGER: StaticClass = class!(
"inline-flex h-[calc(100%-1px)] items-center justify-center gap-1.5 rounded-md border \
border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap text-foreground \
transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] \
focus-visible:ring-ring/50 aria-selected:bg-background aria-selected:shadow-sm \
dark:text-muted-foreground dark:aria-selected:border-input dark:aria-selected:bg-input/30 \
dark:aria-selected:text-foreground data-tab-error:text-destructive"
);
#[derive(Debug)]
pub struct Tabs<C = Schema> {
pub(crate) children: C,
pub(crate) condition: Option<Condition>,
span: Option<Span>,
}
impl<C: HoldsTabs> Tabs<C> {
pub fn new(tabs: impl IntoIterator<Item = Tab<C>>) -> Self {
let mut children = C::default();
for tab in tabs {
children.push_tab(tab);
}
Self {
children,
condition: None,
span: None,
}
}
}
impl<C> Tabs<C> {
pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Tabs<D> {
Tabs {
children: f(self.children),
condition: self.condition,
span: self.span,
}
}
pub(crate) fn chrome<'a>(&self, cx: &'a Cx, tabs: Vec<TabView<'a>>) -> BoxView<'a> {
spanned(cx, self.span, tabs_chrome(cx, tabs))
}
}
#[derive(Debug)]
pub struct Tab<C = Schema> {
face: TabFace,
pub(crate) children: C,
pub(crate) condition: Option<Condition>,
}
#[derive(Debug)]
pub(crate) struct TabFace {
label: crate::Text,
icon: Option<IconData>,
badge: Option<crate::Text>,
badge_color: Option<Color>,
}
impl Tab<()> {
pub fn new(label: impl Into<crate::Text>) -> Self {
Self {
face: TabFace {
label: label.into(),
icon: None,
badge: None,
badge_color: None,
},
children: (),
condition: None,
}
}
pub fn schema<F>(self, children: impl IntoSchema<F>) -> Tab<Schema<F>> {
self.holding(children.into_schema())
}
}
impl<C> Tab<C> {
pub(crate) fn holding<D>(self, children: D) -> Tab<D> {
self.map(|_| children)
}
pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Tab<D> {
Tab {
face: self.face,
children: f(self.children),
condition: self.condition,
}
}
pub fn icon(mut self, data: IconData) -> Self {
self.face.icon = Some(data);
self
}
pub fn badge(mut self, text: impl Into<crate::Text>) -> Self {
self.face.badge = Some(text.into());
self
}
pub fn badge_color(mut self, color: Color) -> Self {
self.face.badge_color = Some(color);
self
}
pub(crate) fn face(&self) -> &TabFace {
&self.face
}
}
#[doc(hidden)]
pub trait HoldsTabs: Default + Sized {
fn push_tab(&mut self, tab: Tab<Self>);
}
impl<F> HoldsTabs for Schema<F> {
fn push_tab(&mut self, tab: Tab<Self>) {
let mut fields = Vec::new();
let tab = tab.map(|mut children| {
fields = std::mem::take(&mut children.fields);
children.retype()
});
self.append(Schema::from_parts(vec![Node::Tab(Box::new(tab))], fields));
}
}
pub(crate) fn tab_id(id: &str, depth: usize) -> String {
format!("{id}-{depth}")
}
pub(crate) struct TabView<'a> {
pub(crate) trigger: BoxView<'a>,
pub(crate) panel: BoxView<'a>,
}
pub(crate) fn tab_view<'a>(
cx: &'a Cx,
id: &str,
face: &TabFace,
active: bool,
errored: bool,
body: BoxView<'a>,
) -> TabView<'a> {
let tab = format!("{id}-tab");
let panel = format!("{id}-tabpanel");
let (labelled_by, panel_id) = (tab.clone(), panel.clone());
let label = face.label.localize(cx);
let glyph = face.icon.clone();
let badge = face.badge.as_ref().map(|text| text.localize(cx));
let dot = face.badge_color.map(Color::dot_class);
let selected = if active { "true" } else { "false" };
let tabindex = if active { "0" } else { "-1" };
let trigger = view! {
<button
type="button"
role="tab"
id=(tab)
aria-controls=(panel)
aria-selected=(selected)
tabindex=(tabindex)
data-tab-error=(errored)
class=(TAB_TRIGGER)
>
if let Some(data) = glyph {
icon(
data: data,
attrs: attributes! { class="size-4 shrink-0" aria-hidden="true" }
)
}
(label)
if let Some(text) = badge {
tablo_ui::badge(
variant: tablo_ui::BadgeVariant::Secondary,
attrs: attributes! { data-tab-badge="" class="tabular-nums" },
if let Some(dot) = dot {
<span class=(dot) aria-hidden="true"></span>
}
(text)
)
}
</button>
}
.boxed();
let panel = view! {
<div
role="tabpanel"
id=(panel_id)
aria-labelledby=(labelled_by)
tabindex="0"
hidden=(!active)
data-tab-panel=""
class="flex flex-col gap-6 outline-none"
>
(body)
</div>
}
.boxed();
TabView { trigger, panel }
}
fn tabs_chrome<'a>(cx: &'a Cx, tabs: Vec<TabView<'a>>) -> BoxView<'a> {
let (triggers, panels): (Vec<_>, Vec<_>) =
tabs.into_iter().map(|tab| (tab.trigger, tab.panel)).unzip();
view! {
<div data-tabs="" class="flex flex-col gap-4">
<div role="tablist" class=(TAB_LIST)>
for trigger in triggers {
(trigger)
}
</div>
for panel in panels {
(panel)
}
</div>
}
.boxed()
}
impl Tabs<Schema> {
pub(crate) async fn render<'a>(
&self,
cx: &'a Cx,
fields: &[Field],
source: &Source<'_>,
) -> Result<BoxView<'a>> {
let tabs: Vec<(&Node, &Tab)> = self
.children
.nodes
.iter()
.filter_map(|node| match node {
Node::Tab(tab) if node.shows_any(fields, source) => Some((node, &**tab)),
_ => None,
})
.collect();
let errored: Vec<bool> = tabs
.iter()
.map(|(node, _)| node.has_error(fields, source))
.collect();
let active = errored.iter().position(|e| *e).unwrap_or_else(|| {
tabs.iter()
.position(|(_, tab)| source.holds(tab.condition.as_ref()))
.unwrap_or(0)
});
let depth = source.tab_depth();
let mut views = Vec::with_capacity(tabs.len());
for (index, (node, tab)) in tabs.iter().enumerate() {
let id = node
.first_field()
.map(|field| tab_id(&source.id(fields[field].name()), depth))
.unwrap_or_default();
source.set_tab_depth(depth + 1);
let body = render_nodes(cx, &tab.children.nodes, fields, source).await;
source.set_tab_depth(depth);
let body = body?;
let view = tab_view(cx, &id, tab.face(), index == active, errored[index], body);
views.push(TabView {
trigger: source.guard(cx, tab.condition.as_ref(), view.trigger),
panel: source.guard(cx, tab.condition.as_ref(), view.panel),
});
}
Ok(self.chrome(cx, views))
}
}