ev_lib 0.3.1

EV-invest shared Rust libraries, one per feature
Documentation
use dioxus::prelude::*;
use tailwind_fuse::{AsTailwindClass, TwVariant};

use crate::cn;
#[component]
pub fn Empty(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!(
		"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
		class
	);
	rsx! {
		div { class: cls, "data-slot": "empty", {children} }
	}
}

#[component]
pub fn EmptyHeader(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!("flex max-w-sm flex-col items-center gap-2 text-center", class);
	rsx! {
		div { class: cls, "data-slot": "empty-header", {children} }
	}
}

#[derive(strum::AsRefStr, PartialEq, TwVariant)]
#[strum(serialize_all = "kebab-case")]
#[tw(class = "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0")]
pub enum EmptyMediaVariant {
	#[tw(default, class = "bg-transparent")]
	Default,
	#[tw(class = "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6")]
	Icon,
}

#[component]
pub fn EmptyMedia(#[props(default)] variant: EmptyMediaVariant, #[props(default)] class: String, children: Element) -> Element {
	let cls = cn!(variant.as_class(), class);
	rsx! {
		div {
			class: cls,
			"data-slot": "empty-icon",
			"data-variant": variant.as_ref(),
			{children}
		}
	}
}

#[component]
pub fn EmptyTitle(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!("text-lg font-medium tracking-tight", class);
	rsx! {
		div { class: cls, "data-slot": "empty-title", {children} }
	}
}

#[component]
pub fn EmptyDescription(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!("text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", class);
	rsx! {
		div { class: cls, "data-slot": "empty-description", {children} }
	}
}

#[component]
pub fn EmptyContent(#[props(default)] class: String, children: Element) -> Element {
	let cls = cn!("flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance", class);
	rsx! {
		div { class: cls, "data-slot": "empty-content", {children} }
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::uikit::test_util::render;

	#[test]
	fn empty_renders_base_and_slot() {
		fn app() -> Element {
			rsx! {
				Empty { "x" }
			}
		}
		let html = render(app);
		assert!(html.contains("border-dashed"), "{html}");
		assert!(html.contains("data-slot=\"empty\""), "{html}");
	}

	#[test]
	fn media_default_variant() {
		fn app() -> Element {
			rsx! {
				EmptyMedia { "m" }
			}
		}
		let html = render(app);
		assert!(html.contains("data-slot=\"empty-icon\""), "{html}");
		assert!(html.contains("data-variant=\"default\""), "{html}");
		assert!(html.contains("bg-transparent"), "{html}");
	}

	#[test]
	fn media_icon_variant_is_canon() {
		fn app() -> Element {
			rsx! {
				EmptyMedia { variant: EmptyMediaVariant::Icon, "m" }
			}
		}
		let html = render(app);
		assert!(html.contains("size-10"), "{html}");
		assert!(html.contains("data-variant=\"icon\""), "{html}");
	}

	#[test]
	fn parts_carry_their_slots() {
		fn app() -> Element {
			rsx! {
				EmptyHeader { "h" }
				EmptyTitle { "t" }
				EmptyDescription { "d" }
				EmptyContent { "c" }
			}
		}
		let html = render(app);
		assert!(html.contains("data-slot=\"empty-header\""), "{html}");
		assert!(html.contains("data-slot=\"empty-title\""), "{html}");
		assert!(html.contains("data-slot=\"empty-description\""), "{html}");
		assert!(html.contains("data-slot=\"empty-content\""), "{html}");
	}
}