mod appbar; mod avatar; mod htmx; mod icon; mod indicator; mod modal; mod overlay; mod pagination; mod placeholder; mod search; mod shell; mod timeline; pub mod prelude { pub use super::appbar::AppBar; pub use super::avatar::{Avatar, AvatarStack}; pub use super::htmx::{ClickToLoad, InfinityScroll, LazyLoad}; pub use super::icon::MaterialIcon; pub use super::indicator::{BadgeIndicator, Indicator, IndicatorLegend, NumberIndicator}; pub use super::modal::{Modal, ModalCloseButton, ModalOpenButton}; pub use super::overlay::{ DropDown, DropdownMenu, DropdownMenuEntry, Placement, Popover, PopoverTrigger, Tooltip, }; pub use super::pagination::{Pagination, PaginationButtonsOnly}; pub use super::placeholder::{ CardPlaceholder, ImagePlaceholder, ListPlaceholder, Placeholder, TextPlaceholder, VideoPlaceholder, }; pub use super::search::Search; pub use super::shell::{ Alignment, BottomNavigation, BottomNavigationTile, Classic, ClassicWidget, FetchToast, NavBar, Position, Shell, Toast, }; pub use super::{ Accordion, Alert, Banner, Breadcrumb, Card, Carousel, CarouselMode, ColoredAlert, ColoredSpinner, CopyText, FetchAlert, FnKey, HelpIcon, HorizontalLine, IconStepper, InfoIcon, ProgressBar, Spinner, Stepper, Tabs, X_Icon, }; } use maud::{PreEscaped, Render, html}; use crate::ui::prelude::script; use super::{ UIWidget, color::{ColorCircle, Gray, UIColor}, htmx::{Event, HTMXAttributes, SwapStrategy}, prelude::Div, }; #[allow(non_snake_case)] pub fn HorizontalLine() -> PreEscaped { html! { hr class="h-px my-8 bg-gray-200 border-0 dark:bg-gray-700" {}; } } pub fn FnKey(key: &str) -> PreEscaped { html! { kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500" { (key) }; } } pub fn ColoredSpinner(color: T) -> PreEscaped { let col = color.color_class(); html! { div role="status" { svg aria-hidden="true" class=(format!("w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-{col}")) viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg" { path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" {}; path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" {}; }; span class="sr-only" { "Loading..." } }; } } #[allow(non_snake_case)] pub fn Spinner() -> PreEscaped { ColoredSpinner(super::color::Blue::_600) } pub fn CopyText(txt: &str) -> PreEscaped { let id = uuid::Uuid::new_v4().to_string(); let gscript = format!( " window.addEventListener('load', function () {{ const clipboard = FlowbiteInstances.getInstance('CopyClipboard', '{id}-copy'); const tooltip = FlowbiteInstances.getInstance('Tooltip', '{id}-copy-tooltip'); const $defaultIcon = document.getElementById('default-icon'); const $successIcon = document.getElementById('success-icon'); const $defaultTooltipMessage = document.getElementById('default-tooltip-message'); const $successTooltipMessage = document.getElementById('success-tooltip-message'); clipboard.updateOnCopyCallback((clipboard) => {{ showSuccess(); // reset to default state setTimeout(() => {{ resetToDefault(); }}, 2000); }}) const showSuccess = () => {{ $defaultIcon.classList.add('hidden'); $successIcon.classList.remove('hidden'); $defaultTooltipMessage.classList.add('hidden'); $successTooltipMessage.classList.remove('hidden'); tooltip.show(); }} const resetToDefault = () => {{ $defaultIcon.classList.remove('hidden'); $successIcon.classList.add('hidden'); $defaultTooltipMessage.classList.remove('hidden'); $successTooltipMessage.classList.add('hidden'); tooltip.hide(); }} }}) " ); html! { (script(&gscript)) div class="w-full max-w-[16rem]" { div class="relative" { input id=(format!("{id}-copy")) type="text" class="col-span-6 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500" value=(txt) disabled readonly {}; button data-copy-to-clipboard-target=(format!("{id}-copy")) data-tooltip-target=(format!("{id}-copy-tooltip")) class="absolute end-2 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg p-2 inline-flex items-center justify-center" { span id="default-icon" { svg class="w-3.5 h-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20" { path d="M16 1h-3.278A1.992 1.992 0 0 0 11 0H7a1.993 1.993 0 0 0-1.722 1H2a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2Zm-3 14H5a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2Zm0-4H5a1 1 0 0 1 0-2h8a1 1 0 1 1 0 2Zm0-5H5a1 1 0 0 1 0-2h2V2h4v2h2a1 1 0 1 1 0 2Z" {}; }; }; span id="success-icon" class="hidden inline-flex items-center" { svg class="w-3.5 h-3.5 text-blue-700 dark:text-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12" { path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5" {}; }; }; }; div id=(format!("{id}-copy-tooltip")) role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700" { span id="default-tooltip-message" { "Copy to clipboard" }; span id="success-tooltip-message" class="hidden" { "Copied!" }; div class="tooltip-arrow" data-popper-arrow {}; }; }; }; } } pub struct AccordionWidget { collapsed: bool, title: Box, body: Box, } impl AccordionWidget { pub fn expanded(mut self) -> Self { self.collapsed = false; self } } impl Render for AccordionWidget { fn render(&self) -> maud::Markup { self.render_with_class("") } } impl UIWidget for AccordionWidget { fn can_inherit(&self) -> bool { false } fn base_class(&self) -> Vec { Vec::new() } fn extended_class(&self) -> Vec { Vec::new() } fn render_with_class(&self, _: &str) -> maud::Markup { let id = uuid::Uuid::new_v4().to_string(); html! { div id=(format!("accordion-collapse-{id}")) data-accordion="collapse" { h2 id=(format!("accordion-collapse-heading-{id}")) { button type="button" class="flex items-center justify-between w-full p-5 font-medium rtl:text-right rounded-t-xl gap-3" data-accordion-target=(format!("#accordion-collapse-body-{id}")) aria-expanded=(if self.collapsed { "false" } else { "true" }) aria-controls=(format!("accordion-collapse-body-{id}")) { (self.title) svg data-accordion-icon class="w-3 h-3 rotate-180 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6" { path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5 5 1 1 5" {}; }; }; }; div id=(format!("accordion-collapse-body-{id}")) class="hidden" aria-labelledby=(format!("accordion-collapse-heading-{id}")) { div class="p-5 border border-b-0 border-gray-200 dark:border-gray-700 dark:bg-gray-900 rounded-b-md" { (self.body) } }; }; } } } #[allow(non_snake_case)] pub fn Accordion( title: H, body: B, ) -> AccordionWidget { AccordionWidget { collapsed: true, title: Box::new(title), body: Box::new(body), } } #[allow(non_snake_case)] pub fn InfoIcon() -> PreEscaped { html! { svg class="shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" { path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" {}; }; } } #[allow(non_snake_case)] pub fn X_Icon() -> PreEscaped { html! { svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" { path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" {}; }; } } #[allow(non_snake_case)] pub fn ColoredAlert( color: C, inner: T, ) -> PreEscaped { let dark_color = color.previous().previous().previous().previous(); let dark_color = dark_color.color_class(); let bg_light = color.start(); let bg_light = bg_light.color_class(); let btn_bg_light = color .previous() .previous() .previous() .previous() .previous() .previous(); let btn_bg_light = btn_bg_light.color_class(); let ring_light = color.previous().previous().previous().previous(); let ring_light = ring_light.color_class(); let btn_light = color.previous().previous().previous(); let btn_light = btn_light.color_class(); let color = color.color_class(); let id = format!("alert-{}", uuid::Uuid::new_v4().to_string()); html! { div id=(id) class=(format!("flex items-center p-4 mb-4 text-{color} rounded-lg bg-{bg_light} dark:bg-gray-800 dark:text-{dark_color}")) role="alert" { (inner) button type="button" class=(format!("ms-auto -mx-1.5 -my-1.5 bg-{bg_light} text-{btn_light} rounded-lg focus:ring-2 focus:ring-{ring_light} p-1.5 hover:bg-{btn_bg_light} inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-{dark_color} dark:hover:bg-gray-700")) data-dismiss-target=(format!("#{id}")) aria-label="Close" { span class="sr-only" { "Close" }; (X_Icon()) }; }; } } #[allow(non_snake_case)] pub fn Alert(inner: T) -> PreEscaped { ColoredAlert(Gray::_800, inner) } #[allow(non_snake_case)] pub fn FetchAlert(reference: &str) -> PreEscaped { Div() .hx_get(reference) .hx_target(super::htmx::Selector::Query( "#notification_area".to_string(), )) .hx_swap(SwapStrategy::beforeend) .hx_trigger(Event::on_load()) .render() } pub struct BreadcrumbWidget { elements: Vec<(String, String)>, seperator: Option>, } impl BreadcrumbWidget { pub fn seperator(mut self, seperator: T) -> Self { self.seperator = Some(Box::new(seperator)); self } fn arrow_seperator() -> PreEscaped { html! { svg class="rtl:rotate-180 w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" { path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" {}; }; } } } impl Render for BreadcrumbWidget { fn render(&self) -> maud::Markup { self.render_with_class("") } } impl UIWidget for BreadcrumbWidget { fn can_inherit(&self) -> bool { false } fn base_class(&self) -> Vec { Vec::new() } fn extended_class(&self) -> Vec { Vec::new() } fn render_with_class(&self, _: &str) -> maud::Markup { html! { nav class="flex" aria-label="Breadcrumb" { ol class="inline-flex items-center space-x-1 md:space-x-2 rtl:space-x-reverse p-2" { @for (index, (name, url)) in self.elements.iter().enumerate() { @if index == 0 { li class="inline-flex items-center" { a href=(url) class="inline-flex items-center text-sm font-medium hover:text-blue-600" { (PreEscaped(name)) }; }; } @else if index == (self.elements.len()-1) { li aria-current="page" { div class="flex items-center" { @if let Some(s) = self.seperator.as_ref() { (s) } @else { (Self::arrow_seperator()) } span class="ms-1 text-sm font-medium text-gray-500 md:ms-2 dark:text-gray-400" { (PreEscaped(name)) }; }; }; } @else { li { div class="flex items-center" { @if let Some(s) = self.seperator.as_ref() { (s) } @else { (Self::arrow_seperator()) } a href=(url) class="ms-1 text-sm font-medium hover:text-blue-600 md:ms-2" { (PreEscaped(name)) }; }; }; } }; }; }; } } } #[allow(non_snake_case)] pub fn Breadcrumb(paths: Vec<(String, String)>) -> BreadcrumbWidget { BreadcrumbWidget { elements: paths, seperator: None, } } #[allow(non_snake_case)] pub fn Card(inner: T) -> PreEscaped { html! { div class="w-fit p-4 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700" { (inner) } } } #[allow(non_snake_case)] pub fn Banner(inner: T) -> PreEscaped { let id = uuid::Uuid::new_v4().to_string(); html! { div id=(format!("banner-{id}")) tabindex="-1" class="fixed top-0 start-0 z-40 flex justify-between w-full p-4 border-b border-gray-200 bg-gray-50 dark:bg-gray-700 dark:border-gray-600" { div class="flex items-center mx-auto" { p class="flex items-center text-sm font-normal text-gray-500 dark:text-gray-400" { (inner) }; }; div class="flex items-center" { button data-dismiss-target=(format!("#banner-{id}")) type="button" class="flex-shrink-0 inline-flex justify-center w-7 h-7 items-center text-gray-400 hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 dark:hover:bg-gray-600 dark:hover:text-white" { (X_Icon()) span class="sr-only" { "Close banner" }; }; }; }; } } pub enum CarouselMode { Slide, Static, } #[allow(non_snake_case)] pub fn Carousel(elements: Vec) -> CarouselWidget { let mut boxed_elements: Vec> = Vec::with_capacity(elements.len()); for e in elements { boxed_elements.push(Box::new(e)); } CarouselWidget { elements: boxed_elements, mode: CarouselMode::Slide, controls: false, indicators: false, } } pub struct CarouselWidget { elements: Vec>, mode: CarouselMode, controls: bool, indicators: bool, } impl CarouselWidget { pub fn mode(mut self, mode: CarouselMode) -> Self { self.mode = mode; self } pub fn with_controls(mut self) -> Self { self.controls = true; self } pub fn with_indicators(mut self) -> Self { self.indicators = true; self } } impl Render for CarouselWidget { fn render(&self) -> maud::Markup { self.render_with_class("") } } impl UIWidget for CarouselWidget { fn can_inherit(&self) -> bool { false } fn base_class(&self) -> Vec { Vec::new() } fn extended_class(&self) -> Vec { Vec::new() } fn render_with_class(&self, _: &str) -> maud::Markup { let mode = match self.mode { CarouselMode::Slide => "slide", CarouselMode::Static => "static", }; html! { div id="default-carousel" class="relative w-full" data-carousel=(mode) { // Carousel wrapper div class="relative h-56 overflow-hidden rounded-lg md:h-96" { @for element in &self.elements { div class="hidden duration-700 ease-in-out absolute block w-full flex justify-center items-center" data-carousel-item { (element) }; }; } @if self.indicators { // Slider indicators div class="absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse" { @for i in 0..self.elements.len() { @if i == 0 { button type="button" class="w-3 h-3 rounded-full" aria-current="true" aria-label=(format!("Slide {i}")) data-carousel-slide-to=(i) {}; } @else { button type="button" class="w-3 h-3 rounded-full" aria-current="false" aria-label=(format!("Slide {i}")) data-carousel-slide-to=(i) {}; } } } } @if self.controls { // Slider controls button type="button" class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none" data-carousel-prev { span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none" { svg class="w-4 h-4 text-white dark:text-gray-800 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" { path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" {}; }; span class="sr-only" { "Previous" }; }; }; button type="button" class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none" data-carousel-next { span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none" { svg class="w-4 h-4 text-white dark:text-gray-800 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10" { path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" {}; }; span class="sr-only" { "Next" }; }; }; } } } } } #[allow(non_snake_case)] pub fn HelpIcon() -> PreEscaped { html! { svg class="w-4 h-4 text-gray-400 hover:text-gray-500" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" { path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" {}; } } } #[allow(non_snake_case)] pub fn ProgressBar(percentage: u8, label: bool) -> PreEscaped { assert!(percentage < 100, "Percentage must be less than 100"); html! { @if label { div class="w-full bg-gray-200 rounded-full dark:bg-gray-700" { div class="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full" style=(format!("width: {percentage}%")) { (format!("{percentage}%")) }; }; } @else { div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700" { div class="bg-blue-600 h-2.5 rounded-full" style=(format!("width: {percentage}%")) {}; }; } } } #[allow(non_snake_case)] pub fn Stepper>(steps: Vec) -> StepperWidget { let mut boxed_elements: Vec> = Vec::with_capacity(steps.len()); for e in steps { boxed_elements.push(Box::new(e.into())); } StepperWidget { elements: boxed_elements, icons: false, progress: 0, } } #[allow(non_snake_case)] pub fn IconStepper(elements: Vec) -> StepperWidget { let mut boxed_elements: Vec> = Vec::with_capacity(elements.len()); for e in elements { boxed_elements.push(Box::new(e)); } StepperWidget { elements: boxed_elements, icons: true, progress: 0, } } pub struct StepperWidget { elements: Vec>, icons: bool, progress: u8, } impl StepperWidget { pub fn step(mut self, step: u8) -> Self { if step != 0 { self.progress = step - 1; } else { self.progress = step; } self } pub fn check_icon() -> PreEscaped { html! { svg class="w-3.5 h-3.5 sm:w-4 sm:h-4 me-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" { path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" {}; }; } } pub fn build_final(index: usize, element: &Box) -> PreEscaped { html! { li class="flex items-center" { span class="me-2" { (index+1) }; (element) } } } pub fn build_middle(index: usize, element: &Box) -> PreEscaped { html! { li class="flex md:w-full items-center after:content-[''] after:w-full after:h-1 after:border-b after:border-gray-200 after:border-1 after:hidden sm:after:inline-block after:mx-6 xl:after:mx-10 dark:after:border-gray-700" { span class="flex items-center after:content-['/'] sm:after:hidden after:mx-2 after:text-gray-200 dark:after:text-gray-500" { span class="me-2" { (index+1) }; (element) }; }; } } pub fn build_done(element: &Box) -> PreEscaped { html! { li class="flex md:w-full items-center text-blue-600 dark:text-blue-500 sm:after:content-[''] after:w-full after:h-1 after:border-b after:border-gray-200 after:border-1 after:hidden sm:after:inline-block after:mx-6 xl:after:mx-10 dark:after:border-gray-700" { span class="flex items-center after:content-['/'] sm:after:hidden after:mx-2 after:text-gray-200 dark:after:text-gray-500" { (Self::check_icon()) (element) }; }; } } pub fn build_final_icon(element: &Box) -> PreEscaped { html! { li class="flex items-center w-full" { span class="flex items-center justify-center w-10 h-10 bg-gray-100 rounded-full lg:h-12 lg:w-12 dark:bg-gray-700 shrink-0" { (element) } } } } pub fn build_middle_icon(element: &Box) -> PreEscaped { html! { li class="flex w-full items-center after:content-[''] after:w-full after:h-1 after:border-b after:border-gray-100 after:border-4 after:inline-block dark:after:border-gray-700" { span class="flex items-center justify-center w-10 h-10 bg-gray-100 rounded-full lg:h-12 lg:w-12 dark:bg-gray-700 shrink-0" { (element) } }; } } pub fn build_done_icon(element: &Box) -> PreEscaped { html! { li class="flex w-full items-center text-blue-600 dark:text-blue-500 after:content-[''] after:w-full after:h-1 after:border-b after:border-blue-100 after:border-4 after:inline-block dark:after:border-blue-800" { span class="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-full lg:h-12 lg:w-12 dark:bg-blue-800 shrink-0" { (element) }; }; } } } impl Render for StepperWidget { fn render(&self) -> maud::Markup { self.render_with_class("") } } impl UIWidget for StepperWidget { fn can_inherit(&self) -> bool { false } fn base_class(&self) -> Vec { Vec::new() } fn extended_class(&self) -> Vec { Vec::new() } fn render_with_class(&self, _: &str) -> maud::Markup { html! { @if self.icons { ol class="flex items-center w-full" { @for (index, e) in self.elements.iter().enumerate() { @if index == (self.elements.len()-1) { (Self::build_final_icon(e)) } @else if index <= self.progress as usize { (Self::build_done_icon(e)) } @else { (Self::build_middle_icon(e)) } } }; } @else { ol class="flex items-center w-full text-sm font-medium text-center text-gray-500 dark:text-gray-400 sm:text-base" { @for (index, e) in self.elements.iter().enumerate() { @if index == (self.elements.len()-1) { (Self::build_final(index, e)) } @else if index <= self.progress as usize { (Self::build_done(e)) } @else { (Self::build_middle(index, e)) } } } } } } } pub struct TabWidget { pub content: Vec<(PreEscaped, String, Box)>, } #[allow(non_snake_case)] pub fn Tabs() -> TabWidget { TabWidget { content: Vec::new(), } } impl TabWidget { pub fn add_tab( mut self, id: &str, tab: T, body: B, ) -> Self { self.content .push((tab.render(), id.to_string(), Box::new(body))); self } } impl Render for TabWidget { fn render(&self) -> maud::Markup { self.render_with_class("") } } impl UIWidget for TabWidget { fn can_inherit(&self) -> bool { false } fn base_class(&self) -> Vec { Vec::new() } fn extended_class(&self) -> Vec { Vec::new() } fn render_with_class(&self, _: &str) -> maud::Markup { let tab_id = uuid::Uuid::new_v4().to_string(); html! { div class="mb-4 border-b border-gray-200 dark:border-gray-700" { ul class="flex flex-wrap -mb-px text-sm font-medium text-center" id=(format!("tab_{tab_id}")) data-tabs-toggle=(format!("#tab_content_{tab_id}")) role="tablist" { @for (i, (head, id, _)) in self.content.iter().enumerate() { li class=(if i == self.content.len() { "" } else { "me-2" }) role="presentation" { button class="inline-block p-4 border-b-2 rounded-t-lg" data-tabs-target=(format!("#{id}")) type="button" role="tab" aria-controls=(id) aria-selected="false" { (head) }; } }; } }; div id=(format!("#tab_content_{tab_id}")) { @for (_, id, body) in &self.content { div class="hidden rounded-lg" id=(id) role="tabpanel" aria-labelledby=(format!("{id}_tab")) { (body) } } }; } } }