diff --git a/examples/basic.rs b/examples/basic.rs index cd167c3..1b217ea 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,7 +1,7 @@ use based::request::{RequestContext, StringResponse}; use based::{ get_pg, - page::{Shell, render_page}, + ui::{Shell, render_page}, }; use maud::html; use rocket::get; diff --git a/examples/ui.rs b/examples/ui.rs new file mode 100644 index 0000000..9683986 --- /dev/null +++ b/examples/ui.rs @@ -0,0 +1,42 @@ +use based::request::{RequestContext, StringResponse}; +use based::ui::{Shell, render_page}; +use maud::Render; +use maud::html; +use rocket::get; +use rocket::routes; + +use based::ui::appbar::AppBar; + +#[get("/")] +pub async fn index_page(ctx: RequestContext) -> StringResponse { + let content = AppBar("MyApp", None).render(); + + let content = html!( + h1 { "Hello World!" }; + + (content) + + ); + + render_page( + content, + "Hello World", + ctx, + &Shell::new( + html! { + script src="https://cdn.tailwindcss.com" {}; + }, + html! {}, + Some(String::new()), + ), + ) + .await +} + +#[rocket::launch] +async fn launch() -> _ { + // Logging + env_logger::init(); + + rocket::build().mount("/", routes![index_page]) +} diff --git a/src/lib.rs b/src/lib.rs index 44842e3..bfe93a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,9 @@ pub mod auth; pub mod format; #[cfg(feature = "htmx")] pub mod htmx; -pub mod page; pub mod request; pub mod result; +pub mod ui; // TODO : CORS? diff --git a/src/ui/appbar.rs b/src/ui/appbar.rs new file mode 100644 index 0000000..10f20e1 --- /dev/null +++ b/src/ui/appbar.rs @@ -0,0 +1,68 @@ +use maud::{Markup, Render}; + +use crate::auth::User; + +use crate::ui::basic::*; + +use super::UIWidget; + +#[allow(non_snake_case)] +pub fn AppBar(name: &str, user: Option) -> AppBarWidget { + AppBarWidget { + name: name.to_owned(), + user, + } +} + +pub struct AppBarWidget { + name: String, + user: Option, +} + +impl Render for AppBarWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for AppBarWidget { + fn can_inherit(&self) -> bool { + false + } + + fn render_with_class(&self, _: &str) -> Markup { + Padding(Shadow::medium(Background( + Gray::_800, + Header( + Padding( + Flex( + Div() + .vanish() + .add( + Flex(Link( + "/", + Div() + .vanish() + .add(Sized( + 10, + 10, + RoundedMedium(Image("/favicon").alt("Logo")), + )) + .add(Span(&self.name).semibold().xl().white()), + )) + .items_center() + .space_x(2), + ) + .add_some(self.user.as_ref(), |user| Text(&user.username).white()), + ) + .group() + .justify(Justify::Between) + .items_center(), + ) + .x(6), + ), + ))) + .y(2) + .render() + } +} diff --git a/src/ui/aspect.rs b/src/ui/aspect.rs new file mode 100644 index 0000000..0158951 --- /dev/null +++ b/src/ui/aspect.rs @@ -0,0 +1,54 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +pub struct Aspect { + kind: u8, + inner: Box, +} + +impl Aspect { + pub fn auto(inner: T) -> Self { + Self { + kind: 0, + inner: Box::new(inner), + } + } + + pub fn square(inner: T) -> Self { + Self { + kind: 1, + inner: Box::new(inner), + } + } + + pub fn video(inner: T) -> Self { + Self { + kind: 2, + inner: Box::new(inner), + } + } +} + +impl Render for Aspect { + fn render(&self) -> Markup { + let class = match self.kind { + 0 => "aspect-auto", + 1 => "aspect-square", + 2 => "aspect-video", + _ => "", + }; + + if self.inner.as_ref().can_inherit() { + html! { + (self.inner.as_ref().render_with_class(class)) + } + } else { + html! { + div class=(class) { + (self.inner.as_ref()) + } + } + } + } +} diff --git a/src/ui/background.rs b/src/ui/background.rs new file mode 100644 index 0000000..5a431c4 --- /dev/null +++ b/src/ui/background.rs @@ -0,0 +1,66 @@ +use super::UIWidget; +use maud::{Markup, Render, html}; + +pub trait UIColor { + fn color_class(&self) -> &str; +} + +pub enum Blue { + _500, +} + +impl UIColor for Blue { + fn color_class(&self) -> &str { + match self { + Blue::_500 => "blue-500", + } + } +} + +pub enum Gray { + _800, +} + +impl UIColor for Gray { + fn color_class(&self) -> &str { + match self { + Gray::_800 => "gray-800", + } + } +} + +#[allow(non_snake_case)] +pub fn Background( + color: C, + inner: T, +) -> BackgroundWidget { + BackgroundWidget(Box::new(inner), Box::new(color)) +} + +pub struct BackgroundWidget(Box, Box); + +impl Render for BackgroundWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for BackgroundWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("bg-{} {class}", self.1.color_class())) + } else { + html! { + div class=(format!("bg-{} {class}", self.1.color_class())) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/container.rs b/src/ui/container.rs new file mode 100644 index 0000000..5220c77 --- /dev/null +++ b/src/ui/container.rs @@ -0,0 +1,37 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +#[allow(non_snake_case)] +/// A component for fixing an element's width to the current breakpoint. +pub fn Container(inner: T) -> ContainerWidget { + ContainerWidget(Box::new(inner)) +} + +pub struct ContainerWidget(Box); + +impl Render for ContainerWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for ContainerWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("container {class}")) + } else { + html! { + div class=(format!("container {class}")) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/div.rs b/src/ui/div.rs new file mode 100644 index 0000000..8a40ad1 --- /dev/null +++ b/src/ui/div.rs @@ -0,0 +1,63 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +#[allow(non_snake_case)] +pub fn Div() -> DivWidget { + DivWidget(Vec::new(), false) +} + +pub struct DivWidget(Vec>, bool); + +impl DivWidget { + pub fn add(mut self, element: T) -> Self { + self.0.push(Box::new(element)); + self + } + + pub fn add_some T>( + mut self, + option: Option<&X>, + then: U, + ) -> Self { + if let Some(val) = option { + self.0.push(Box::new(then(val))); + } + self + } + + pub fn vanish(mut self) -> Self { + self.1 = true; + self + } +} + +impl Render for DivWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for DivWidget { + fn can_inherit(&self) -> bool { + false + } + + fn render_with_class(&self, _: &str) -> Markup { + if self.1 { + html! { + @for e in &self.0 { + (e.as_ref()) + } + } + } else { + html! { + div { + @for e in &self.0 { + (e.as_ref()) + } + } + } + } + } +} diff --git a/src/ui/flex.rs b/src/ui/flex.rs new file mode 100644 index 0000000..6ed2aa1 --- /dev/null +++ b/src/ui/flex.rs @@ -0,0 +1,78 @@ +use super::UIWidget; +use maud::{Markup, Render, html}; + +#[allow(non_snake_case)] +pub fn Flex(inner: T) -> FlexWidget { + FlexWidget(Box::new(inner), vec![], false) +} + +pub enum Justify { + Center, + Between, +} + +pub struct FlexWidget(Box, Vec, bool); + +impl Render for FlexWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl FlexWidget { + pub fn full_center(mut self) -> Self { + self.1.push("items-center".to_owned()); + self.1.push("justify-center".to_owned()); + self + } + + pub fn group(mut self) -> Self { + self.2 = true; + self + } + + pub fn justify(mut self, value: Justify) -> Self { + let class = match value { + Justify::Center => "justify-center".to_owned(), + Justify::Between => "justify-between".to_owned(), + }; + + self.1.push(class); + self + } + + pub fn space_x(mut self, x: u32) -> Self { + self.1.push(format!("space-x-{x}")); + self + } + + pub fn items_center(mut self) -> Self { + self.1.push("items-center".to_owned()); + self + } + + pub fn gap(mut self, amount: u32) -> Self { + self.1.push(format!("gap-{amount}")); + self + } +} + +impl UIWidget for FlexWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() && !self.2 { + self.0 + .as_ref() + .render_with_class(&format!("flex {} {class}", self.1.join(" "))) + } else { + html! { + div class=(format!("flex {} {class}", self.1.join(" "))) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/header.rs b/src/ui/header.rs new file mode 100644 index 0000000..2751bb1 --- /dev/null +++ b/src/ui/header.rs @@ -0,0 +1,30 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +#[allow(non_snake_case)] +pub fn Header(inner: T) -> HeaderWidget { + HeaderWidget(Box::new(inner)) +} + +pub struct HeaderWidget(Box); + +impl Render for HeaderWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for HeaderWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + html! { + header class=(class) { + (self.0.as_ref()) + } + } + } +} diff --git a/src/ui/image.rs b/src/ui/image.rs new file mode 100644 index 0000000..da0eb5e --- /dev/null +++ b/src/ui/image.rs @@ -0,0 +1,40 @@ +use super::UIWidget; +use maud::{Markup, Render, html}; + +#[allow(non_snake_case)] +pub fn Image(src: &str) -> ImageWidget { + ImageWidget { + src: src.to_owned(), + alt: String::new(), + } +} + +pub struct ImageWidget { + src: String, + alt: String, +} + +impl Render for ImageWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl ImageWidget { + pub fn alt(mut self, alt: &str) -> Self { + self.alt = alt.to_owned(); + self + } +} + +impl UIWidget for ImageWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + html! { + img src=(self.src) alt=(self.alt) class=(class) {}; + } + } +} diff --git a/src/ui/link.rs b/src/ui/link.rs new file mode 100644 index 0000000..f5d4883 --- /dev/null +++ b/src/ui/link.rs @@ -0,0 +1,31 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +#[allow(non_snake_case)] +/// A component for fixing an element's width to the current breakpoint. +pub fn Link(reference: &str, inner: T) -> LinkWidget { + LinkWidget(Box::new(inner), reference.to_owned()) +} + +pub struct LinkWidget(Box, String); + +impl Render for LinkWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for LinkWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + html! { + a class=(class) href=(self.1) { + (self.0.as_ref()) + } + } + } +} diff --git a/src/page/mod.rs b/src/ui/mod.rs similarity index 77% rename from src/page/mod.rs rename to src/ui/mod.rs index a93a2df..3114828 100644 --- a/src/page/mod.rs +++ b/src/ui/mod.rs @@ -1,11 +1,61 @@ -use maud::{html, PreEscaped, Render}; +use maud::{Markup, PreEscaped, Render, html}; +pub mod appbar; +pub mod aspect; +pub mod background; +pub mod container; +pub mod div; +pub mod flex; +pub mod header; +pub mod image; +pub mod link; +pub mod padding; +pub mod rounded; pub mod search; +pub mod shadow; +pub mod sized; +pub mod text; +pub mod width; + +// UI + +// Preludes + +// Basic Primitives +pub mod basic { + pub use super::aspect::Aspect; + pub use super::background::Background; + pub use super::background::{Blue, Gray}; + pub use super::container::Container; + pub use super::div::Div; + pub use super::flex::Flex; + pub use super::flex::Justify; + pub use super::header::Header; + pub use super::image::Image; + pub use super::link::Link; + pub use super::padding::Padding; + pub use super::rounded::Rounded; + pub use super::rounded::RoundedMedium; + pub use super::shadow::Shadow; + pub use super::sized::Sized; + pub use super::text::{Paragraph, Span, Text}; + pub use super::width::FitWidth; +} + +// Stacked Components +pub mod extended { + pub use super::appbar::AppBar; +} use crate::request::{RequestContext, StringResponse}; use rocket::http::{ContentType, Status}; +#[allow(non_snake_case)] +pub fn Nothing() -> PreEscaped { + html! {} +} + /// Represents the HTML structure of a page shell, including the head, body class, and body content. /// /// This structure is used to construct the overall HTML structure of a page, making it easier to generate consistent HTML pages dynamically. @@ -156,11 +206,6 @@ pub fn script(script: &str) -> PreEscaped { ) } - -// TODO : More UI primitives like flutter - -// Row -> flex - pub struct Row(PreEscaped); impl Render for Row { @@ -171,12 +216,6 @@ impl Render for Row { } } -pub fn test() { - html! { - (Row(PreEscaped("".to_string()))) - }; -} - // Grids // ListViews @@ -185,5 +224,23 @@ pub fn test() { // Cards -// FlowBite? +pub trait UIWidget: Render { + fn can_inherit(&self) -> bool; + fn render_with_class(&self, class: &str) -> Markup; +} +impl UIWidget for PreEscaped { + fn can_inherit(&self) -> bool { + false + } + + fn render_with_class(&self, _: &str) -> Markup { + self.render() + } +} + +// TODO : +// hover focus +// responsive media +// more elements +// htmx builder trait? diff --git a/src/ui/padding.rs b/src/ui/padding.rs new file mode 100644 index 0000000..4962109 --- /dev/null +++ b/src/ui/padding.rs @@ -0,0 +1,83 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +pub struct PaddingInfo { + pub right: Option, +} + +#[allow(non_snake_case)] +pub fn Padding(inner: T) -> PaddingWidget { + PaddingWidget { + inner: Box::new(inner), + right: None, + y: None, + x: None, + } +} + +pub struct PaddingWidget { + pub inner: Box, + pub right: Option, + pub y: Option, + pub x: Option, +} + +impl PaddingWidget { + pub fn right(mut self, right: u32) -> Self { + self.right = Some(right); + self + } + + pub fn y(mut self, y: u32) -> Self { + self.y = Some(y); + self + } + + pub fn x(mut self, x: u32) -> Self { + self.x = Some(x); + self + } +} + +impl Render for PaddingWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for PaddingWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + let mut our_class = Vec::new(); + + if let Some(r) = self.right { + our_class.push(format!("pr-{r}")); + } + + if let Some(y) = self.y { + our_class.push(format!("py-{y}")); + } + + if let Some(x) = self.x { + our_class.push(format!("px-{x}")); + } + + let our_class = our_class.join(" "); + + if self.inner.as_ref().can_inherit() { + self.inner + .as_ref() + .render_with_class(&format!("{our_class} {class}")) + } else { + html! { + div class=(format!("{our_class} {class}")) { + (self.inner.as_ref()) + } + } + } + } +} diff --git a/src/ui/rounded.rs b/src/ui/rounded.rs new file mode 100644 index 0000000..1632274 --- /dev/null +++ b/src/ui/rounded.rs @@ -0,0 +1,41 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +#[allow(non_snake_case)] +pub fn Rounded(inner: T) -> RoundedWidget { + RoundedWidget(Box::new(inner), "full".to_owned()) +} + +#[allow(non_snake_case)] +pub fn RoundedMedium(inner: T) -> RoundedWidget { + RoundedWidget(Box::new(inner), "md".to_owned()) +} + +pub struct RoundedWidget(Box, String); + +impl Render for RoundedWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for RoundedWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("rounded-{} {class}", self.1)) + } else { + html! { + div class=(format!("rounded-{} {class}", self.1)) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/page/search.rs b/src/ui/search.rs similarity index 100% rename from src/page/search.rs rename to src/ui/search.rs diff --git a/src/ui/shadow.rs b/src/ui/shadow.rs new file mode 100644 index 0000000..5585d3b --- /dev/null +++ b/src/ui/shadow.rs @@ -0,0 +1,37 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +pub struct Shadow(Box, String); + +impl Shadow { + pub fn medium(inner: T) -> Shadow { + Shadow(Box::new(inner), "md".to_owned()) + } +} + +impl Render for Shadow { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for Shadow { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("shadow-{} {class}", self.1)) + } else { + html! { + div class=(format!("shadow-{} {class}", self.1)) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/sized.rs b/src/ui/sized.rs new file mode 100644 index 0000000..dc473d6 --- /dev/null +++ b/src/ui/sized.rs @@ -0,0 +1,35 @@ +use super::UIWidget; +use maud::{Markup, Render, html}; + +#[allow(non_snake_case)] +pub fn Sized(height: u32, width: u32, inner: T) -> SizedWidget { + SizedWidget(Box::new(inner), height, width) +} + +pub struct SizedWidget(Box, u32, u32); + +impl Render for SizedWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for SizedWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("h-{} w-{} {class}", self.1, self.2)) + } else { + html! { + div class=(format!("h-{} w-{} {class}", self.1, self.2)) { + (self.0.as_ref()) + } + } + } + } +} diff --git a/src/ui/text.rs b/src/ui/text.rs new file mode 100644 index 0000000..0a7c7f9 --- /dev/null +++ b/src/ui/text.rs @@ -0,0 +1,131 @@ +use super::UIWidget; +use maud::{Markup, Render, html}; + +#[allow(non_snake_case)] +pub fn Text(txt: &str) -> TextWidget { + TextWidget { + inner: None, + txt: txt.to_string(), + font: String::new(), + color: String::new(), + size: String::new(), + span: false, + } +} + +#[allow(non_snake_case)] +pub fn Paragraph(inner: T) -> TextWidget { + TextWidget { + inner: Some(Box::new(inner)), + font: String::new(), + color: String::new(), + txt: String::new(), + size: String::new(), + span: false, + } +} + +#[allow(non_snake_case)] +pub fn Span(txt: &str) -> TextWidget { + TextWidget { + inner: None, + txt: txt.to_string(), + font: String::new(), + color: String::new(), + size: String::new(), + span: true, + } +} + +pub struct TextWidget { + inner: Option>, + txt: String, + font: String, + color: String, + size: String, + span: bool, +} + +impl TextWidget { + pub fn semibold(mut self) -> Self { + self.font = "font-semibold".to_owned(); + self + } + + pub fn bold(mut self) -> Self { + self.font = "font-bold".to_owned(); + self + } + + pub fn medium(mut self) -> Self { + self.font = "font-medium".to_owned(); + self + } + + pub fn _2xl(mut self) -> Self { + self.size = "text-2xl".to_owned(); + self + } + + pub fn xl(mut self) -> Self { + self.size = "text-xl".to_owned(); + self + } + + pub fn sm(mut self) -> Self { + self.size = "text-sm".to_owned(); + self + } + + pub fn gray(mut self, i: u32) -> Self { + self.color = format!("text-gray-{}", i); + self + } + + pub fn slate(mut self, i: u32) -> Self { + self.color = format!("text-slate-{}", i); + self + } + + pub fn white(mut self) -> Self { + self.color = "text-white".to_owned(); + self + } +} + +impl Render for TextWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for TextWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + let our_class = format!("{} {} {}", self.color, self.font, self.size); + if let Some(inner) = &self.inner { + if self.span { + html! { + span class=(format!("{} {}", class, our_class)) { (inner) } + } + } else { + html! { + p class=(format!("{} {}", class, our_class)) { (inner) } + } + } + } else { + if self.span { + html! { + span class=(format!("{} {}", class, our_class)) { (self.txt) } + } + } else { + html! { + p class=(format!("{} {}", class, our_class)) { (self.txt) } + } + } + } + } +} diff --git a/src/ui/width.rs b/src/ui/width.rs new file mode 100644 index 0000000..9c7a390 --- /dev/null +++ b/src/ui/width.rs @@ -0,0 +1,36 @@ +use maud::{Markup, Render, html}; + +use super::UIWidget; + +#[allow(non_snake_case)] +pub fn FitWidth(inner: T) -> FitWidthWidget { + FitWidthWidget(Box::new(inner)) +} + +pub struct FitWidthWidget(Box); + +impl Render for FitWidthWidget { + fn render(&self) -> Markup { + self.render_with_class("") + } +} + +impl UIWidget for FitWidthWidget { + fn can_inherit(&self) -> bool { + true + } + + fn render_with_class(&self, class: &str) -> Markup { + if self.0.as_ref().can_inherit() { + self.0 + .as_ref() + .render_with_class(&format!("max-w-fit {class}")) + } else { + html! { + div class=(format!("max-w-fit {class}")) { + (self.0.as_ref()) + } + } + } + } +}