update
This commit is contained in:
parent
8208fa8899
commit
ed739d792f
35 changed files with 1675 additions and 447 deletions
|
@ -1,8 +1,7 @@
|
||||||
|
use based::get_pg;
|
||||||
use based::request::{RequestContext, StringResponse};
|
use based::request::{RequestContext, StringResponse};
|
||||||
use based::{
|
use based::ui::components::Shell;
|
||||||
get_pg,
|
use based::ui::render_page;
|
||||||
ui::{Shell, render_page},
|
|
||||||
};
|
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use rocket::get;
|
use rocket::get;
|
||||||
use rocket::routes;
|
use rocket::routes;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use based::request::{RequestContext, StringResponse};
|
use based::request::{RequestContext, StringResponse};
|
||||||
use based::ui::{Shell, render_page};
|
use based::ui::components::{AppBar, Shell};
|
||||||
|
use based::ui::htmx::{Event, HTMXAttributes};
|
||||||
|
use based::ui::{prelude::*, render_page};
|
||||||
use maud::Render;
|
use maud::Render;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use rocket::get;
|
use rocket::get;
|
||||||
use rocket::routes;
|
use rocket::routes;
|
||||||
|
|
||||||
use based::ui::appbar::AppBar;
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn index_page(ctx: RequestContext) -> StringResponse {
|
pub async fn index_page(ctx: RequestContext) -> StringResponse {
|
||||||
let content = AppBar("MyApp", None).render();
|
let content = AppBar("MyApp", None).render();
|
||||||
|
@ -14,6 +14,14 @@ pub async fn index_page(ctx: RequestContext) -> StringResponse {
|
||||||
let content = html!(
|
let content = html!(
|
||||||
h1 { "Hello World!" };
|
h1 { "Hello World!" };
|
||||||
|
|
||||||
|
(Hover(
|
||||||
|
Padding(Text("").color(Gray::_400)).x(10),
|
||||||
|
Link("/test", Text("Hello")).hx_get("/test").hx_get("/test").hx_trigger(
|
||||||
|
Event::on_load().delay("2s")
|
||||||
|
.and(Event::on_revealed())
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
(content)
|
(content)
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
129
src/ui/color.rs
Normal file
129
src/ui/color.rs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/// UI Color
|
||||||
|
pub trait UIColor {
|
||||||
|
fn color_class(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ColorCircle {
|
||||||
|
fn previous(&self) -> Self;
|
||||||
|
fn next(&self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! color_map {
|
||||||
|
($name:ident, $id:literal) => {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum $name {
|
||||||
|
_50,
|
||||||
|
_100,
|
||||||
|
_200,
|
||||||
|
_300,
|
||||||
|
_400,
|
||||||
|
_500,
|
||||||
|
_600,
|
||||||
|
_700,
|
||||||
|
_800,
|
||||||
|
_900,
|
||||||
|
_950,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIColor for $name {
|
||||||
|
fn color_class(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
$name::_50 => concat!($id, "-50"),
|
||||||
|
$name::_100 => concat!($id, "-100"),
|
||||||
|
$name::_200 => concat!($id, "-200"),
|
||||||
|
$name::_300 => concat!($id, "-300"),
|
||||||
|
$name::_400 => concat!($id, "-400"),
|
||||||
|
$name::_500 => concat!($id, "-500"),
|
||||||
|
$name::_600 => concat!($id, "-600"),
|
||||||
|
$name::_700 => concat!($id, "-700"),
|
||||||
|
$name::_800 => concat!($id, "-800"),
|
||||||
|
$name::_900 => concat!($id, "-900"),
|
||||||
|
$name::_950 => concat!($id, "-950"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorCircle for $name {
|
||||||
|
fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
$name::_50 => $name::_100,
|
||||||
|
$name::_100 => $name::_200,
|
||||||
|
$name::_200 => $name::_300,
|
||||||
|
$name::_300 => $name::_400,
|
||||||
|
$name::_400 => $name::_500,
|
||||||
|
$name::_500 => $name::_600,
|
||||||
|
$name::_600 => $name::_700,
|
||||||
|
$name::_700 => $name::_800,
|
||||||
|
$name::_800 => $name::_900,
|
||||||
|
$name::_900 => $name::_950,
|
||||||
|
$name::_950 => $name::_50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
$name::_50 => $name::_950,
|
||||||
|
$name::_100 => $name::_50,
|
||||||
|
$name::_200 => $name::_100,
|
||||||
|
$name::_300 => $name::_200,
|
||||||
|
$name::_400 => $name::_300,
|
||||||
|
$name::_500 => $name::_400,
|
||||||
|
$name::_600 => $name::_500,
|
||||||
|
$name::_700 => $name::_600,
|
||||||
|
$name::_800 => $name::_700,
|
||||||
|
$name::_900 => $name::_800,
|
||||||
|
$name::_950 => $name::_900,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
color_map!(Slate, "slate");
|
||||||
|
color_map!(Gray, "gray");
|
||||||
|
color_map!(Zinc, "zinc");
|
||||||
|
color_map!(Neutral, "neutral");
|
||||||
|
color_map!(Stone, "stone");
|
||||||
|
color_map!(Red, "red");
|
||||||
|
color_map!(Orange, "orange");
|
||||||
|
color_map!(Amber, "amber");
|
||||||
|
color_map!(Yellow, "yellow");
|
||||||
|
color_map!(Lime, "lime");
|
||||||
|
color_map!(Green, "green");
|
||||||
|
color_map!(Emerald, "emerald");
|
||||||
|
color_map!(Teal, "teal");
|
||||||
|
color_map!(Cyan, "cyan");
|
||||||
|
color_map!(Sky, "sky");
|
||||||
|
color_map!(Blue, "blue");
|
||||||
|
color_map!(Indigo, "indigo");
|
||||||
|
color_map!(Violet, "violet");
|
||||||
|
color_map!(Purple, "purple");
|
||||||
|
color_map!(Fuchsia, "fuchsia");
|
||||||
|
color_map!(Pink, "pink");
|
||||||
|
color_map!(Rose, "rose");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Colors {
|
||||||
|
/// Inherit a color
|
||||||
|
Inherit,
|
||||||
|
/// Use current color
|
||||||
|
Current,
|
||||||
|
/// Transparency
|
||||||
|
Transparent,
|
||||||
|
/// Black
|
||||||
|
Black,
|
||||||
|
/// White
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIColor for Colors {
|
||||||
|
fn color_class(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Colors::Inherit => "inherit",
|
||||||
|
Colors::Current => "current",
|
||||||
|
Colors::Transparent => "transparent",
|
||||||
|
Colors::Black => "black",
|
||||||
|
Colors::White => "white",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,7 @@ use maud::{Markup, Render};
|
||||||
|
|
||||||
use crate::auth::User;
|
use crate::auth::User;
|
||||||
|
|
||||||
use crate::ui::basic::*;
|
use crate::ui::{UIWidget, prelude::*};
|
||||||
|
|
||||||
use super::UIWidget;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn AppBar(name: &str, user: Option<User>) -> AppBarWidget {
|
pub fn AppBar(name: &str, user: Option<User>) -> AppBarWidget {
|
||||||
|
@ -30,6 +28,14 @@ impl UIWidget for AppBarWidget {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, _: &str) -> Markup {
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
Padding(Shadow::medium(Background(
|
Padding(Shadow::medium(Background(
|
||||||
Gray::_800,
|
Gray::_800,
|
||||||
|
@ -39,6 +45,7 @@ impl UIWidget for AppBarWidget {
|
||||||
Div()
|
Div()
|
||||||
.vanish()
|
.vanish()
|
||||||
.add(
|
.add(
|
||||||
|
SpaceBetween(
|
||||||
Flex(Link(
|
Flex(Link(
|
||||||
"/",
|
"/",
|
||||||
Div()
|
Div()
|
||||||
|
@ -46,12 +53,14 @@ impl UIWidget for AppBarWidget {
|
||||||
.add(Sized(
|
.add(Sized(
|
||||||
10,
|
10,
|
||||||
10,
|
10,
|
||||||
RoundedMedium(Image("/favicon").alt("Logo")),
|
Rounded(Image("/favicon").alt("Logo"))
|
||||||
|
.size(Size::Medium),
|
||||||
))
|
))
|
||||||
.add(Span(&self.name).semibold().xl().white()),
|
.add(Span(&self.name).semibold().xl().white()),
|
||||||
))
|
))
|
||||||
.items_center()
|
.items_center(),
|
||||||
.space_x(2),
|
)
|
||||||
|
.x(ScreenValue::_2),
|
||||||
)
|
)
|
||||||
.add_some(self.user.as_ref(), |user| Text(&user.username).white()),
|
.add_some(self.user.as_ref(), |user| Text(&user.username).white()),
|
||||||
)
|
)
|
7
src/ui/components/mod.rs
Normal file
7
src/ui/components/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
mod appbar;
|
||||||
|
mod search;
|
||||||
|
mod shell;
|
||||||
|
|
||||||
|
pub use appbar::AppBar;
|
||||||
|
pub use search::Search;
|
||||||
|
pub use shell::Shell;
|
|
@ -1,6 +1,7 @@
|
||||||
use maud::{PreEscaped, html};
|
|
||||||
|
|
||||||
use crate::request::{RequestContext, api::Pager};
|
use crate::request::{RequestContext, api::Pager};
|
||||||
|
use crate::ui::htmx::{Event, HTMXAttributes, SwapStrategy};
|
||||||
|
use crate::ui::prelude::*;
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
/// Represents a search form with configurable options such as heading, placeholder, and CSS class.
|
/// Represents a search form with configurable options such as heading, placeholder, and CSS class.
|
||||||
pub struct Search {
|
pub struct Search {
|
||||||
|
@ -94,9 +95,12 @@ impl Search {
|
||||||
}
|
}
|
||||||
|
|
||||||
@if reslen as u64 == pager.items_per_page {
|
@if reslen as u64 == pager.items_per_page {
|
||||||
div hx-get=(format!("{}?query={}&page={}", self.post_url, query, page+1))
|
(Div()
|
||||||
hx-trigger="revealed"
|
.hx_get(
|
||||||
hx-swap="outerHTML" {};
|
&format!("{}?query={}&page={}", self.post_url, query, page+1)
|
||||||
|
).hx_trigger(
|
||||||
|
Event::on_revealed()
|
||||||
|
).hx_swap(SwapStrategy::outerHTML))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,7 +144,7 @@ impl Search {
|
||||||
/// The HTML string containing the entire search form and results UI.
|
/// The HTML string containing the entire search form and results UI.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(&self, query: &str, first_page: PreEscaped<String>) -> PreEscaped<String> {
|
pub fn build(&self, query: &str, first_page: PreEscaped<String>) -> PreEscaped<String> {
|
||||||
let no_html = PreEscaped(String::new());
|
let no_html = Nothing();
|
||||||
html! {
|
html! {
|
||||||
(self.heading.as_ref().unwrap_or_else(|| &no_html))
|
(self.heading.as_ref().unwrap_or_else(|| &no_html))
|
||||||
input type="search" name="query"
|
input type="search" name="query"
|
74
src/ui/components/shell.rs
Normal file
74
src/ui/components/shell.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use maud::{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.
|
||||||
|
pub struct Shell {
|
||||||
|
/// The HTML content for the `<head>` section of the page.
|
||||||
|
head: PreEscaped<String>,
|
||||||
|
/// An optional class attribute for the `<body>` element.
|
||||||
|
body_class: Option<String>,
|
||||||
|
/// The HTML content for the static body portion.
|
||||||
|
body_content: PreEscaped<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shell {
|
||||||
|
/// Constructs a new `Shell` instance with the given head content, body content, and body class.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `head` - The HTML content for the page's head.
|
||||||
|
/// * `body_content` - The HTML content for the body of the page.
|
||||||
|
/// * `body_class` - An optional class to apply to the `<body>` element.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `Shell` instance encapsulating the provided HTML content and attributes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
|
head: PreEscaped<String>,
|
||||||
|
body_content: PreEscaped<String>,
|
||||||
|
body_class: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
head,
|
||||||
|
body_class,
|
||||||
|
body_content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the full HTML page using the shell structure, with additional content and a title.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `content` - The additional HTML content to render inside the main content div.
|
||||||
|
/// * `title` - The title of the page, rendered inside the `<title>` element.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `PreEscaped<String>` containing the full HTML page content.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render(&self, content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
title { (title) };
|
||||||
|
(self.head)
|
||||||
|
};
|
||||||
|
@if self.body_class.is_some() {
|
||||||
|
body class=(self.body_class.as_ref().unwrap()) {
|
||||||
|
(self.body_content);
|
||||||
|
|
||||||
|
div id="main_content" {
|
||||||
|
(content)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} @else {
|
||||||
|
body {
|
||||||
|
(self.body_content);
|
||||||
|
|
||||||
|
div id="main_content" {
|
||||||
|
(content)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
use maud::{Markup, Render, html};
|
|
||||||
|
|
||||||
use super::UIWidget;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn Div() -> DivWidget {
|
|
||||||
DivWidget(Vec::new(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DivWidget(Vec<Box<dyn UIWidget>>, bool);
|
|
||||||
|
|
||||||
impl DivWidget {
|
|
||||||
pub fn add<T: UIWidget + 'static>(mut self, element: T) -> Self {
|
|
||||||
self.0.push(Box::new(element));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_some<T: UIWidget + 'static, X, U: Fn(&X) -> 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
179
src/ui/htmx/mod.rs
Normal file
179
src/ui/htmx/mod.rs
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
mod selector;
|
||||||
|
mod swap;
|
||||||
|
mod trigger;
|
||||||
|
|
||||||
|
pub use selector::Selector;
|
||||||
|
use swap::ModifiedSwapStrategy;
|
||||||
|
pub use swap::SwapStrategy;
|
||||||
|
pub use trigger::{Event, QueueOption, Trigger};
|
||||||
|
|
||||||
|
use super::AttrExtendable;
|
||||||
|
|
||||||
|
pub trait HTMXAttributes: AttrExtendable + std::marker::Sized {
|
||||||
|
/// Issues a `GET` request to the specified URL
|
||||||
|
fn hx_get(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-get", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issues a `POST` request to the specified URL
|
||||||
|
fn hx_post(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-post", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a URL into the browser location bar to create history
|
||||||
|
fn hx_push_url(self) -> Self {
|
||||||
|
self.add_attr("hx-push-url", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select content to swap in from a response
|
||||||
|
fn hx_select(self, element: &str) -> Self {
|
||||||
|
self.add_attr("hx-select", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select content to swap in from a response, somewhere other than the target (out of band).
|
||||||
|
/// Select `element` from response and replace `element` in the DOM.
|
||||||
|
fn hx_select_oob(self, element: &str) -> Self {
|
||||||
|
self.add_attr("hx-select-oob", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-boost attribute allows you to “boost” normal anchors and form tags to use AJAX instead.
|
||||||
|
fn hx_boost(self) -> Self {
|
||||||
|
self.add_attr("hx-boost", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-confirm attribute allows you to confirm an action before issuing a request.
|
||||||
|
fn hx_confirm(self, msg: &str) -> Self {
|
||||||
|
self.add_attr("hx-confirm", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-delete attribute will cause an element to issue a `DELETE` request to the specified URL and swap the HTML into the DOM using a swap strategy.
|
||||||
|
fn hx_delete(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-delete", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-disable attribute will disable htmx processing for a given element and all its children.
|
||||||
|
fn hx_disable(self) -> Self {
|
||||||
|
self.add_attr("hx-disable", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-disabled-elt attribute allows you to specify elements that will have the disabled attribute added to them for the duration of the request.
|
||||||
|
fn hx_disabled_elt(self, element: Selector) -> Self {
|
||||||
|
self.add_attr("hx-disabled-elt", &element.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-disinherit attribute allows you to control automatic attribute inheritance.
|
||||||
|
fn hx_disinherit(self, attrs: &str) -> Self {
|
||||||
|
self.add_attr("hx-disinherit", attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-encoding attribute allows you to switch the request encoding from the usual `application/x-www-form-urlencoded` encoding to `multipart/form-data`, usually to support file uploads in an ajax request.
|
||||||
|
fn hx_encoding(self) -> Self {
|
||||||
|
self.add_attr("hx-encoding", "multipart/form-data")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-headers attribute allows you to add to the headers that will be submitted with an AJAX request.
|
||||||
|
fn hx_headers(self, headers: HashMap<String, String>) -> Self {
|
||||||
|
let json = serde_json::to_value(headers).unwrap();
|
||||||
|
let json_str = serde_json::to_string(&json).unwrap();
|
||||||
|
self.add_attr("hx-headers", &json_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-vals attribute allows you to add to the parameters that will be submitted with an AJAX request.
|
||||||
|
fn hx_vals(self, vals: HashMap<String, String>) -> Self {
|
||||||
|
let json = serde_json::to_value(vals).unwrap();
|
||||||
|
let json_str = serde_json::to_string(&json).unwrap();
|
||||||
|
self.add_attr("hx-vals", &json_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the hx-history attribute to false on any element in the current document, or any html fragment loaded into the current document by htmx, to prevent sensitive data being saved to the localStorage cache when htmx takes a snapshot of the page state.
|
||||||
|
///
|
||||||
|
/// History navigation will work as expected, but on restoration the URL will be requested from the server instead of the history cache.
|
||||||
|
fn hx_history(self) -> Self {
|
||||||
|
self.add_attr("hx-history", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-history-elt attribute allows you to specify the element that will be used to snapshot and restore page state during navigation. By default, the body tag is used. This is typically good enough for most setups, but you may want to narrow it down to a child element. Just make sure that the element is always visible in your application, or htmx will not be able to restore history navigation properly.
|
||||||
|
fn hx_history_elt(self) -> Self {
|
||||||
|
self.add_attr("hx-history-elt", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-include attribute allows you to include additional element values in an AJAX request.
|
||||||
|
fn hx_include(self, element: Selector) -> Self {
|
||||||
|
self.add_attr("hx-include", &element.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-indicator attribute allows you to specify the element that will have the htmx-request class added to it for the duration of the request. This can be used to show spinners or progress indicators while the request is in flight.
|
||||||
|
///
|
||||||
|
/// Note: This attribute only supports CSS queries and `closest` match.
|
||||||
|
fn hx_indicator(self, indicator: Selector) -> Self {
|
||||||
|
self.add_attr("hx-indicator", &indicator.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-params attribute allows you to filter the parameters that will be submitted with an AJAX request.
|
||||||
|
///
|
||||||
|
/// The possible values of this attribute are:
|
||||||
|
/// `*` - Include all parameters (default)
|
||||||
|
/// `none` - Include no parameters
|
||||||
|
/// `not <param-list>` - Include all except the comma separated list of parameter names
|
||||||
|
/// `<param-list>` - Include all the comma separated list of parameter names
|
||||||
|
fn hx_params(self, params: &str) -> Self {
|
||||||
|
self.add_attr("hx-params", params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-patch attribute will cause an element to issue a PATCH to the specified URL and swap the HTML into the DOM using a swap strategy.
|
||||||
|
fn hx_patch(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-patch", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-put attribute will cause an element to issue a PUT to the specified URL and swap the HTML into the DOM using a swap strategy
|
||||||
|
fn hx_put(self, url: &str) -> Self {
|
||||||
|
self.add_attr("hx-put", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-replace-url attribute allows you to replace the current url of the browser location history.
|
||||||
|
///
|
||||||
|
/// The possible values of this attribute are:
|
||||||
|
/// `true`, which replaces the fetched URL in the browser navigation bar.
|
||||||
|
/// `false`, which disables replacing the fetched URL if it would otherwise be replaced due to inheritance.
|
||||||
|
/// A URL to be replaced into the location bar. This may be relative or absolute, as per history.replaceState().
|
||||||
|
fn hx_replace_url(self, value: &str) -> Self {
|
||||||
|
self.add_attr("hx-replace-url", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-validate attribute will cause an element to validate itself by way of the HTML5 Validation API before it submits a request.
|
||||||
|
fn hx_validate(self) -> Self {
|
||||||
|
self.add_attr("hx-validte", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-preserve attribute allows you to keep an element unchanged during HTML replacement. Elements with hx-preserve set are preserved by id when htmx updates any ancestor element. You must set an unchanging id on elements for hx-preserve to work. The response requires an element with the same id, but its type and other attributes are ignored.
|
||||||
|
fn hx_preserve(self) -> Self {
|
||||||
|
self.add_attr("hx-preserve", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-prompt attribute allows you to show a prompt before issuing a request. The value of the prompt will be included in the request in the HX-Prompt header.
|
||||||
|
fn hx_prompt(self, msg: &str) -> Self {
|
||||||
|
self.add_attr("hx-prompt", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-swap attribute allows you to specify how the response will be swapped in relative to the target of an AJAX request.
|
||||||
|
fn hx_swap<T: Into<ModifiedSwapStrategy>>(self, swap: T) -> Self {
|
||||||
|
self.add_attr("hx-swap", &swap.into().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-swap-oob attribute allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target, that is “Out of Band”. This allows you to piggy back updates to other element updates on a response.
|
||||||
|
fn hx_swap_oob(self) -> Self {
|
||||||
|
self.add_attr("hx-swap-oob", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-target attribute allows you to target a different element for swapping than the one issuing the AJAX request.
|
||||||
|
fn hx_target(self, element: Selector) -> Self {
|
||||||
|
self.add_attr("hx-target", &element.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hx-trigger attribute allows you to specify what triggers an AJAX request.
|
||||||
|
fn hx_trigger<T: Into<Trigger>>(self, trigger: T) -> Self {
|
||||||
|
self.add_attr("hx-trigger", &trigger.into().to_string())
|
||||||
|
}
|
||||||
|
}
|
33
src/ui/htmx/selector.rs
Normal file
33
src/ui/htmx/selector.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
pub enum Selector {
|
||||||
|
/// A CSS query selector of the element
|
||||||
|
Query(String),
|
||||||
|
/// this element itself
|
||||||
|
This,
|
||||||
|
/// closest <CSS selector> which will find the closest ancestor element or itself, that matches the given CSS selector.
|
||||||
|
Closest(String),
|
||||||
|
/// find <CSS selector> which will find the first child descendant element that matches the given CSS selector
|
||||||
|
Find(String),
|
||||||
|
/// next which resolves to element.nextElementSibling
|
||||||
|
Next,
|
||||||
|
/// next <CSS selector> which will scan the DOM forward for the first element that matches the given CSS selector.
|
||||||
|
NextQuery(String),
|
||||||
|
/// previous which resolves to element.previousElementSibling
|
||||||
|
Previous,
|
||||||
|
/// previous <CSS selector> which will scan the DOM backwards for the first element that matches the given CSS selector.
|
||||||
|
PreviousQuery(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selector {
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Selector::Query(query) => query.clone(),
|
||||||
|
Selector::This => "this".to_owned(),
|
||||||
|
Selector::Closest(css) => format!("closest {css}"),
|
||||||
|
Selector::Find(css) => format!("find {css}"),
|
||||||
|
Selector::Next => "next".to_owned(),
|
||||||
|
Selector::NextQuery(css) => format!("next {css}"),
|
||||||
|
Selector::Previous => "previous".to_owned(),
|
||||||
|
Selector::PreviousQuery(css) => format!("previous {css}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158
src/ui/htmx/swap.rs
Normal file
158
src/ui/htmx/swap.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum SwapStrategy {
|
||||||
|
/// Replace the inner html of the target element
|
||||||
|
innerHTML,
|
||||||
|
/// Replace the entire target element with the response
|
||||||
|
outerHTML,
|
||||||
|
/// Replace the text content of the target element, without parsing the response as HTML
|
||||||
|
textContent,
|
||||||
|
/// Insert the response before the target element
|
||||||
|
beforebegin,
|
||||||
|
/// Insert the response before the first child of the target element
|
||||||
|
afterbegin,
|
||||||
|
/// Insert the response after the last child of the target element
|
||||||
|
beforeend,
|
||||||
|
/// Insert the response after the target element
|
||||||
|
afterend,
|
||||||
|
/// Deletes the target element regardless of the response
|
||||||
|
delete,
|
||||||
|
/// Does not append content from response (out of band items will still be processed).
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SwapStrategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::innerHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwapStrategy {
|
||||||
|
pub fn to_string(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
SwapStrategy::innerHTML => "innerHTML",
|
||||||
|
SwapStrategy::outerHTML => "outerHTML",
|
||||||
|
SwapStrategy::textContent => "textContent",
|
||||||
|
SwapStrategy::beforebegin => "beforebegin",
|
||||||
|
SwapStrategy::afterbegin => "afterbegin",
|
||||||
|
SwapStrategy::beforeend => "beforeend",
|
||||||
|
SwapStrategy::afterend => "afterend",
|
||||||
|
SwapStrategy::delete => "delete",
|
||||||
|
SwapStrategy::none => "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If you want to use the new View Transitions API when a swap occurs, you can use the transition:true option for your swap.
|
||||||
|
pub fn transition(self) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = "transition".to_owned();
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the amount of time that htmx will wait after receiving a response to swap the content by including a swap modifier.
|
||||||
|
pub fn swap(self, duration: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("swap:{duration}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the time between the swap and the settle logic by including a settle modifier.
|
||||||
|
pub fn settle(self, duration: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("settle:{duration}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, htmx will update the title of the page if it finds a `<title>` tag in the response content. You can turn off this behavior.
|
||||||
|
pub fn ignore_title(self) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = "ignoreTitle:true";
|
||||||
|
ModifiedSwapStrategy::new(self, modifier.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// htmx preserves focus between requests for inputs that have a defined id attribute. By default htmx prevents auto-scrolling to focused inputs between requests which can be unwanted behavior on longer requests when the user has already scrolled away.
|
||||||
|
pub fn focus_scroll(self, enable: bool) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("focus-scroll:{enable}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure visibility
|
||||||
|
pub fn show(self, e: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("show:{e}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll to this location after load
|
||||||
|
pub fn scroll(self, e: &str) -> ModifiedSwapStrategy {
|
||||||
|
let modifier = format!("scroll:{e}");
|
||||||
|
ModifiedSwapStrategy::new(self, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModifiedSwapStrategy {
|
||||||
|
pub strategy: SwapStrategy,
|
||||||
|
pub modifiers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwapStrategy> for ModifiedSwapStrategy {
|
||||||
|
fn from(value: SwapStrategy) -> Self {
|
||||||
|
Self::new(value, String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModifiedSwapStrategy {
|
||||||
|
fn new(strategy: SwapStrategy, modifier: String) -> Self {
|
||||||
|
ModifiedSwapStrategy {
|
||||||
|
strategy,
|
||||||
|
modifiers: vec![modifier],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If you want to use the new View Transitions API when a swap occurs, you can use the transition:true option for your swap.
|
||||||
|
pub fn transition(mut self) -> Self {
|
||||||
|
let modifier = "transition:true".to_owned();
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the amount of time that htmx will wait after receiving a response to swap the content by including a swap modifier.
|
||||||
|
pub fn swap(mut self, duration: &str) -> Self {
|
||||||
|
let modifier = format!("swap:{duration}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can modify the time between the swap and the settle logic by including a settle modifier.
|
||||||
|
pub fn settle(mut self, duration: &str) -> Self {
|
||||||
|
let modifier = format!("settle:{duration}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, htmx will update the title of the page if it finds a `<title>` tag in the response content. You can turn off this behavior.
|
||||||
|
pub fn ignore_title(mut self) -> Self {
|
||||||
|
let modifier = "ignoreTitle:true";
|
||||||
|
self.modifiers.push(modifier.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// htmx preserves focus between requests for inputs that have a defined id attribute. By default htmx prevents auto-scrolling to focused inputs between requests which can be unwanted behavior on longer requests when the user has already scrolled away.
|
||||||
|
pub fn focus_scroll(mut self, enable: bool) -> Self {
|
||||||
|
let modifier = format!("focus-scroll:{enable}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure visibility
|
||||||
|
pub fn show(mut self, e: &str) -> Self {
|
||||||
|
let modifier = format!("show:{e}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll to this location after load
|
||||||
|
pub fn scroll(mut self, e: &str) -> Self {
|
||||||
|
let modifier = format!("scroll:{e}");
|
||||||
|
self.modifiers.push(modifier);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
format!("{} {}", self.strategy.to_string(), self.modifiers.join(" "))
|
||||||
|
}
|
||||||
|
}
|
179
src/ui/htmx/trigger.rs
Normal file
179
src/ui/htmx/trigger.rs
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
use super::Selector;
|
||||||
|
|
||||||
|
pub struct Event {
|
||||||
|
modifiers: Vec<String>,
|
||||||
|
kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
if self.kind.starts_with("every") {
|
||||||
|
return self.kind.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{} {}", self.kind, self.modifiers.join(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a second event trigger
|
||||||
|
pub fn and(self, event: Event) -> Trigger {
|
||||||
|
Trigger {
|
||||||
|
triggers: vec![self, event],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periodically poll
|
||||||
|
///
|
||||||
|
/// Value can be something like `1s [someConditional]`
|
||||||
|
pub fn poll(value: &str) -> Self {
|
||||||
|
Event {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: format!("every {value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard events refer to web API events (e.g. `click`, `keydown`, `mouseup`, `load`).
|
||||||
|
pub fn event(event: &str) -> Self {
|
||||||
|
Event {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: event.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// triggered on load (useful for lazy-loading something)
|
||||||
|
pub fn on_load() -> Self {
|
||||||
|
Event {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: "load".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// triggered when an element is scrolled into the viewport (also useful for lazy-loading). If you are using overflow in css like `overflow-y: scroll` you should use `intersect once` instead of `revealed`.
|
||||||
|
pub fn on_revealed() -> Self {
|
||||||
|
Event {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: "revealed".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fires once when an element first intersects the viewport. This supports two additional options:
|
||||||
|
/// `root:<selector>` - a CSS selector of the root element for intersection
|
||||||
|
/// `threshold:<float>` - a floating point number between 0.0 and 1.0, indicating what amount of intersection to fire the event on
|
||||||
|
pub fn on_intersect(value: &str) -> Self {
|
||||||
|
Event {
|
||||||
|
modifiers: vec![],
|
||||||
|
kind: format!("intersect:{value}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the event will only trigger once (e.g. the first click)
|
||||||
|
pub fn once(mut self) -> Self {
|
||||||
|
self.modifiers.push("once".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the event will only change if the value of the element has changed.
|
||||||
|
pub fn changed(mut self) -> Self {
|
||||||
|
self.modifiers.push("changed".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a delay will occur before an event triggers a request. If the event is seen again it will reset the delay.
|
||||||
|
pub fn delay(mut self, delay: &str) -> Self {
|
||||||
|
self.modifiers.push(format!("delay:{delay}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a throttle will occur after an event triggers a request. If the event is seen again before the delay completes, it is ignored, the element will trigger at the end of the delay.
|
||||||
|
pub fn throttle(mut self, delay: &str) -> Self {
|
||||||
|
self.modifiers.push(format!("throttle:{delay}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// allows the event that triggers a request to come from another element in the document (e.g. listening to a key event on the body, to support hot keys)
|
||||||
|
pub fn from(mut self, element: Selector) -> Self {
|
||||||
|
self.modifiers.push(format!("from:{}", element.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// allows you to filter via a CSS selector on the target of the event. This can be useful when you want to listen for triggers from elements that might not be in the DOM at the point of initialization, by, for example, listening on the body, but with a target filter for a child element
|
||||||
|
pub fn target(mut self, selector: &str) -> Self {
|
||||||
|
self.modifiers.push(format!("target:{selector}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if this option is included the event will not trigger any other htmx requests on parents (or on elements listening on parents)
|
||||||
|
pub fn consume(mut self) -> Self {
|
||||||
|
self.modifiers.push("consume".to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// determines how events are queued if an event occurs while a request for another event is in flight.
|
||||||
|
pub fn queue(mut self, opt: QueueOption) -> Self {
|
||||||
|
self.modifiers.push(format!("queue:{}", opt.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum QueueOption {
|
||||||
|
/// queue the first event
|
||||||
|
first,
|
||||||
|
/// queue the last event (default)
|
||||||
|
last,
|
||||||
|
/// queue all events (issue a request for each event)
|
||||||
|
all,
|
||||||
|
/// do not queue new events
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueueOption {
|
||||||
|
pub fn to_string(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
QueueOption::first => "first",
|
||||||
|
QueueOption::last => "last",
|
||||||
|
QueueOption::all => "all",
|
||||||
|
QueueOption::none => "none",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for QueueOption {
|
||||||
|
fn default() -> Self {
|
||||||
|
QueueOption::last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Trigger {
|
||||||
|
triggers: Vec<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Event> for Trigger {
|
||||||
|
fn from(value: Event) -> Self {
|
||||||
|
Trigger {
|
||||||
|
triggers: vec![value],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trigger {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { triggers: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(mut self, event: Event) -> Self {
|
||||||
|
self.triggers.push(event);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn and(self, event: Event) -> Self {
|
||||||
|
self.add(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.triggers
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
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<T: UIWidget + 'static>(reference: &str, inner: T) -> LinkWidget {
|
|
||||||
LinkWidget(Box::new(inner), reference.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LinkWidget(Box<dyn UIWidget>, 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
232
src/ui/mod.rs
232
src/ui/mod.rs
|
@ -1,134 +1,46 @@
|
||||||
use maud::{Markup, PreEscaped, Render, html};
|
use components::Shell;
|
||||||
|
use maud::{Markup, PreEscaped, Render};
|
||||||
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
|
// UI
|
||||||
|
|
||||||
// Preludes
|
|
||||||
|
|
||||||
// Basic Primitives
|
// Basic Primitives
|
||||||
pub mod basic {
|
pub mod color;
|
||||||
pub use super::aspect::Aspect;
|
pub mod htmx;
|
||||||
pub use super::background::Background;
|
pub mod primitives;
|
||||||
pub use super::background::{Blue, Gray};
|
pub mod wrapper;
|
||||||
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
|
// Stacked Components
|
||||||
pub mod extended {
|
pub mod components;
|
||||||
pub use super::appbar::AppBar;
|
|
||||||
|
// Preludes
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::color::*;
|
||||||
|
pub use super::primitives::Nothing;
|
||||||
|
pub use super::primitives::Side;
|
||||||
|
pub use super::primitives::Size;
|
||||||
|
pub use super::primitives::aspect::Aspect;
|
||||||
|
pub use super::primitives::background::Background;
|
||||||
|
pub use super::primitives::container::Container;
|
||||||
|
pub use super::primitives::div::Div;
|
||||||
|
pub use super::primitives::flex::{Flex, Justify};
|
||||||
|
pub use super::primitives::header::Header;
|
||||||
|
pub use super::primitives::image::Image;
|
||||||
|
pub use super::primitives::link::Link;
|
||||||
|
pub use super::primitives::padding::Padding;
|
||||||
|
pub use super::primitives::rounded::Rounded;
|
||||||
|
pub use super::primitives::script;
|
||||||
|
pub use super::primitives::shadow::Shadow;
|
||||||
|
pub use super::primitives::sized::Sized;
|
||||||
|
pub use super::primitives::space::{ScreenValue, SpaceBetween};
|
||||||
|
pub use super::primitives::text::{Paragraph, Span, Text};
|
||||||
|
pub use super::primitives::width::FitWidth;
|
||||||
|
pub use super::wrapper::Hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::request::{RequestContext, StringResponse};
|
use crate::request::{RequestContext, StringResponse};
|
||||||
|
|
||||||
use rocket::http::{ContentType, Status};
|
use rocket::http::{ContentType, Status};
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn Nothing() -> PreEscaped<String> {
|
|
||||||
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.
|
|
||||||
pub struct Shell {
|
|
||||||
/// The HTML content for the `<head>` section of the page.
|
|
||||||
head: PreEscaped<String>,
|
|
||||||
/// An optional class attribute for the `<body>` element.
|
|
||||||
body_class: Option<String>,
|
|
||||||
/// The HTML content for the static body portion.
|
|
||||||
body_content: PreEscaped<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shell {
|
|
||||||
/// Constructs a new `Shell` instance with the given head content, body content, and body class.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `head` - The HTML content for the page's head.
|
|
||||||
/// * `body_content` - The HTML content for the body of the page.
|
|
||||||
/// * `body_class` - An optional class to apply to the `<body>` element.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `Shell` instance encapsulating the provided HTML content and attributes.
|
|
||||||
#[must_use]
|
|
||||||
pub const fn new(
|
|
||||||
head: PreEscaped<String>,
|
|
||||||
body_content: PreEscaped<String>,
|
|
||||||
body_class: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
head,
|
|
||||||
body_class,
|
|
||||||
body_content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders the full HTML page using the shell structure, with additional content and a title.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `content` - The additional HTML content to render inside the main content div.
|
|
||||||
/// * `title` - The title of the page, rendered inside the `<title>` element.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `PreEscaped<String>` containing the full HTML page content.
|
|
||||||
#[must_use]
|
|
||||||
pub fn render(&self, content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
|
||||||
html! {
|
|
||||||
html {
|
|
||||||
head {
|
|
||||||
title { (title) };
|
|
||||||
(self.head)
|
|
||||||
};
|
|
||||||
@if self.body_class.is_some() {
|
|
||||||
body class=(self.body_class.as_ref().unwrap()) {
|
|
||||||
(self.body_content);
|
|
||||||
|
|
||||||
div id="main_content" {
|
|
||||||
(content)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} @else {
|
|
||||||
body {
|
|
||||||
(self.body_content);
|
|
||||||
|
|
||||||
div id="main_content" {
|
|
||||||
(content)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a full page or an HTMX-compatible fragment based on the request context.
|
/// Renders a full page or an HTMX-compatible fragment based on the request context.
|
||||||
///
|
///
|
||||||
/// If the request is not an HTMX request, this function uses the provided shell to generate
|
/// If the request is not an HTMX request, this function uses the provided shell to generate
|
||||||
|
@ -161,61 +73,6 @@ pub async fn render_page(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates an HTML link with HTMX attributes for dynamic behavior.
|
|
||||||
///
|
|
||||||
/// This function creates an `<a>` element with attributes that enable HTMX behavior for navigation without reload.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `url` - The URL to link to.
|
|
||||||
/// * `class` - The CSS class for styling the link.
|
|
||||||
/// * `onclick` - The JavaScript `onclick` handler for the link.
|
|
||||||
/// * `content` - The content inside the link element.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `PreEscaped<String>` containing the rendered HTML link element.
|
|
||||||
#[must_use]
|
|
||||||
pub fn htmx_link(
|
|
||||||
url: &str,
|
|
||||||
class: &str,
|
|
||||||
onclick: &str,
|
|
||||||
content: PreEscaped<String>,
|
|
||||||
) -> PreEscaped<String> {
|
|
||||||
html!(
|
|
||||||
a class=(class) onclick=(onclick) href=(url) hx-get=(url) hx-target="#main_content" hx-push-url="true" hx-swap="innerHTML" {
|
|
||||||
(content);
|
|
||||||
};
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a `<script>` element containing the provided JavaScript code.
|
|
||||||
///
|
|
||||||
/// This function wraps the provided JavaScript code in a `<script>` tag,
|
|
||||||
/// allowing for easy inclusion of custom scripts in the rendered HTML.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `script` - The JavaScript code to include.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `PreEscaped<String>` containing the rendered `<script>` element.
|
|
||||||
#[must_use]
|
|
||||||
pub fn script(script: &str) -> PreEscaped<String> {
|
|
||||||
html!(
|
|
||||||
script {
|
|
||||||
(PreEscaped(script))
|
|
||||||
};
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Row(PreEscaped<String>);
|
|
||||||
|
|
||||||
impl Render for Row {
|
|
||||||
fn render(&self) -> maud::Markup {
|
|
||||||
html! {
|
|
||||||
div class="flex" { (self.0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grids
|
// Grids
|
||||||
|
|
||||||
// ListViews
|
// ListViews
|
||||||
|
@ -224,23 +81,40 @@ impl Render for Row {
|
||||||
|
|
||||||
// Cards
|
// Cards
|
||||||
|
|
||||||
|
/// Generic UI Widget
|
||||||
pub trait UIWidget: Render {
|
pub trait UIWidget: Render {
|
||||||
|
/// Indicating if the widget supports inheriting classes
|
||||||
fn can_inherit(&self) -> bool;
|
fn can_inherit(&self) -> bool;
|
||||||
|
/// Returning the base classes for this widget
|
||||||
|
fn base_class(&self) -> Vec<String>;
|
||||||
|
fn extended_class(&self) -> Vec<String>;
|
||||||
|
/// Render the widget with additional classes
|
||||||
fn render_with_class(&self, class: &str) -> Markup;
|
fn render_with_class(&self, class: &str) -> Markup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implementation for raw HTML with html! macro
|
||||||
impl UIWidget for PreEscaped<String> {
|
impl UIWidget for PreEscaped<String> {
|
||||||
fn can_inherit(&self) -> bool {
|
fn can_inherit(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, _: &str) -> Markup {
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
self.render()
|
self.render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO :
|
/// Trait for an element which can add new `attrs`
|
||||||
// hover focus
|
pub trait AttrExtendable {
|
||||||
// responsive media
|
fn add_attr(self, key: &str, val: &str) -> Self;
|
||||||
// more elements
|
|
||||||
// htmx builder trait?
|
/// Set the `id` attribute of an element.
|
||||||
|
fn id(self, id: &str) -> Self;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
use super::UIWidget;
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
pub struct Aspect {
|
pub struct Aspect {
|
||||||
kind: u8,
|
kind: u8,
|
||||||
|
@ -32,20 +32,39 @@ impl Aspect {
|
||||||
|
|
||||||
impl Render for Aspect {
|
impl Render for Aspect {
|
||||||
fn render(&self) -> Markup {
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for Aspect {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
let class = match self.kind {
|
let class = match self.kind {
|
||||||
0 => "aspect-auto",
|
0 => "aspect-auto",
|
||||||
1 => "aspect-square",
|
1 => "aspect-square",
|
||||||
2 => "aspect-video",
|
2 => "aspect-video",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
|
vec![class.to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.inner.as_ref().can_inherit() {
|
if self.inner.as_ref().can_inherit() {
|
||||||
html! {
|
html! {
|
||||||
(self.inner.as_ref().render_with_class(class))
|
(self.inner.as_ref().render_with_class(&format!("{class} {}", self.base_class().join(" "))))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
div class=(class) {
|
div class=(format!("{class} {}", self.base_class().join(" "))) {
|
||||||
(self.inner.as_ref())
|
(self.inner.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,33 +1,6 @@
|
||||||
use super::UIWidget;
|
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
pub trait UIColor {
|
use crate::ui::{UIWidget, color::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)]
|
#[allow(non_snake_case)]
|
||||||
pub fn Background<T: UIWidget + 'static, C: UIColor + 'static>(
|
pub fn Background<T: UIWidget + 'static, C: UIColor + 'static>(
|
||||||
|
@ -50,14 +23,24 @@ impl UIWidget for BackgroundWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![format!("bg-{}", self.1.color_class())]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.0.as_ref().can_inherit() {
|
if self.0.as_ref().can_inherit() {
|
||||||
self.0
|
self.0
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.render_with_class(&format!("bg-{} {class}", self.1.color_class()))
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
div class=(format!("bg-{} {class}", self.1.color_class())) {
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
(self.0.as_ref())
|
(self.0.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
use super::UIWidget;
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
/// A component for fixing an element's width to the current breakpoint.
|
/// A component for fixing an element's width to the current breakpoint.
|
||||||
|
@ -21,6 +21,16 @@ impl UIWidget for ContainerWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec!["container".to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.0.as_ref().can_inherit() {
|
if self.0.as_ref().can_inherit() {
|
||||||
self.0
|
self.0
|
108
src/ui/primitives/div.rs
Normal file
108
src/ui/primitives/div.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::{AttrExtendable, UIWidget, htmx::HTMXAttributes};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// `<div>` element
|
||||||
|
///
|
||||||
|
/// Useful for grouping values together
|
||||||
|
pub fn Div() -> DivWidget {
|
||||||
|
DivWidget(Vec::new(), false, HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DivWidget(Vec<Box<dyn UIWidget>>, bool, HashMap<String, String>);
|
||||||
|
|
||||||
|
impl AttrExtendable for DivWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.2.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(self, id: &str) -> Self {
|
||||||
|
self.add_attr("id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivWidget {
|
||||||
|
/// Add an element to the `<div>`
|
||||||
|
pub fn add<T: UIWidget + 'static>(mut self, element: T) -> Self {
|
||||||
|
self.0.push(Box::new(element));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an optional element to the `<div>`
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use based::ui::basic::*;
|
||||||
|
///
|
||||||
|
/// let div = Div().add_some(Some("hello"), |value| Text(value));
|
||||||
|
/// ```
|
||||||
|
pub fn add_some<T: UIWidget + 'static, X, U: Fn(&X) -> T>(
|
||||||
|
mut self,
|
||||||
|
option: Option<&X>,
|
||||||
|
then: U,
|
||||||
|
) -> Self {
|
||||||
|
if let Some(val) = option {
|
||||||
|
self.0.push(Box::new(then(val)));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the `<div>`s innerHTML
|
||||||
|
///
|
||||||
|
/// This will render `<content>` instead of `<div> <content> </div>`
|
||||||
|
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 base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
for e in &self.0 {
|
||||||
|
c.extend_from_slice(&e.extended_class());
|
||||||
|
}
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, _: &str) -> Markup {
|
||||||
|
let inner = html! {
|
||||||
|
@for e in &self.0 {
|
||||||
|
(e.as_ref())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if self.1 {
|
||||||
|
inner
|
||||||
|
} else {
|
||||||
|
let attrs = self
|
||||||
|
.2
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{k}='{v}'"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
PreEscaped(format!("<div {attrs}> {} </a>", inner.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTMXAttributes for DivWidget {}
|
|
@ -1,4 +1,4 @@
|
||||||
use super::UIWidget;
|
use crate::ui::UIWidget;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
@ -41,11 +41,6 @@ impl FlexWidget {
|
||||||
self
|
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 {
|
pub fn items_center(mut self) -> Self {
|
||||||
self.1.push("items-center".to_owned());
|
self.1.push("items-center".to_owned());
|
||||||
self
|
self
|
||||||
|
@ -62,14 +57,26 @@ impl UIWidget for FlexWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut res = vec!["flex".to_string()];
|
||||||
|
res.extend_from_slice(&self.1);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.0.as_ref().can_inherit() && !self.2 {
|
if self.0.as_ref().can_inherit() && !self.2 {
|
||||||
self.0
|
self.0
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.render_with_class(&format!("flex {} {class}", self.1.join(" ")))
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
div class=(format!("flex {} {class}", self.1.join(" "))) {
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
(self.0.as_ref())
|
(self.0.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
use super::UIWidget;
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn Header<T: UIWidget + 'static>(inner: T) -> HeaderWidget {
|
pub fn Header<T: UIWidget + 'static>(inner: T) -> HeaderWidget {
|
||||||
|
@ -20,6 +20,16 @@ impl UIWidget for HeaderWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
header class=(class) {
|
header class=(class) {
|
|
@ -1,4 +1,4 @@
|
||||||
use super::UIWidget;
|
use crate::ui::UIWidget;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
@ -32,6 +32,14 @@ impl UIWidget for ImageWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
img src=(self.src) alt=(self.alt) class=(class) {};
|
img src=(self.src) alt=(self.alt) class=(class) {};
|
1
src/ui/primitives/input.rs
Normal file
1
src/ui/primitives/input.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// TODO : Implement input types
|
77
src/ui/primitives/link.rs
Normal file
77
src/ui/primitives/link.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use maud::{Markup, PreEscaped, Render};
|
||||||
|
|
||||||
|
use crate::ui::{
|
||||||
|
AttrExtendable, UIWidget,
|
||||||
|
htmx::{HTMXAttributes, Selector, SwapStrategy},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// A component for fixing an element's width to the current breakpoint.
|
||||||
|
pub fn Link<T: UIWidget + 'static>(reference: &str, inner: T) -> LinkWidget {
|
||||||
|
LinkWidget(Box::new(inner), reference.to_owned(), HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LinkWidget(Box<dyn UIWidget>, String, HashMap<String, String>);
|
||||||
|
|
||||||
|
impl AttrExtendable for LinkWidget {
|
||||||
|
fn add_attr(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.2.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(self, id: &str) -> Self {
|
||||||
|
self.add_attr("id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTMXAttributes for LinkWidget {}
|
||||||
|
|
||||||
|
impl Render for LinkWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for LinkWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
let attrs = self
|
||||||
|
.2
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{k}='{v}'"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
PreEscaped(format!(
|
||||||
|
"<a href='{}' class='{class}' {attrs}> {} </a>",
|
||||||
|
self.1,
|
||||||
|
self.0.render().0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinkWidget {
|
||||||
|
/// Enable HTMX link capabilities
|
||||||
|
pub fn use_htmx(self) -> Self {
|
||||||
|
let url = self.1.clone();
|
||||||
|
self.hx_get(&url)
|
||||||
|
.hx_target(Selector::Query("#main_content".to_string()))
|
||||||
|
.hx_push_url()
|
||||||
|
.hx_swap(SwapStrategy::innerHTML)
|
||||||
|
}
|
||||||
|
}
|
108
src/ui/primitives/mod.rs
Normal file
108
src/ui/primitives/mod.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
|
pub mod aspect;
|
||||||
|
pub mod background;
|
||||||
|
pub mod container;
|
||||||
|
pub mod div;
|
||||||
|
pub mod flex;
|
||||||
|
pub mod header;
|
||||||
|
pub mod image;
|
||||||
|
pub mod input;
|
||||||
|
pub mod link;
|
||||||
|
pub mod padding;
|
||||||
|
pub mod rounded;
|
||||||
|
pub mod shadow;
|
||||||
|
pub mod sized;
|
||||||
|
pub mod space;
|
||||||
|
pub mod text;
|
||||||
|
pub mod width;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Nothing() -> PreEscaped<String> {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a `<script>` element containing the provided JavaScript code.
|
||||||
|
///
|
||||||
|
/// This function wraps the provided JavaScript code in a `<script>` tag,
|
||||||
|
/// allowing for easy inclusion of custom scripts in the rendered HTML.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `script` - The JavaScript code to include.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `PreEscaped<String>` containing the rendered `<script>` element.
|
||||||
|
#[must_use]
|
||||||
|
pub fn script(script: &str) -> PreEscaped<String> {
|
||||||
|
html!(
|
||||||
|
script {
|
||||||
|
(PreEscaped(script))
|
||||||
|
};
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Size {
|
||||||
|
None,
|
||||||
|
Small,
|
||||||
|
Regular,
|
||||||
|
Medium,
|
||||||
|
Large,
|
||||||
|
XL,
|
||||||
|
_2XL,
|
||||||
|
_3XL,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
pub fn to_string(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::None => "none",
|
||||||
|
Self::Small => "sm",
|
||||||
|
Self::Regular => "",
|
||||||
|
Self::Medium => "md",
|
||||||
|
Self::Large => "lg",
|
||||||
|
Self::XL => "xl",
|
||||||
|
Self::_2XL => "2xl",
|
||||||
|
Self::_3XL => "3xl",
|
||||||
|
Self::Full => "full",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Side {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Top,
|
||||||
|
Right,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
StartStart,
|
||||||
|
StartEnd,
|
||||||
|
EndEnd,
|
||||||
|
EndStart,
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Side {
|
||||||
|
pub fn to_string(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Side::Start => "s",
|
||||||
|
Side::End => "e",
|
||||||
|
Side::Top => "t",
|
||||||
|
Side::Right => "r",
|
||||||
|
Side::Bottom => "b",
|
||||||
|
Side::Left => "l",
|
||||||
|
Side::StartStart => "ss",
|
||||||
|
Side::StartEnd => "se",
|
||||||
|
Side::EndEnd => "ee",
|
||||||
|
Side::EndStart => "es",
|
||||||
|
Side::TopLeft => "tl",
|
||||||
|
Side::TopRight => "tr",
|
||||||
|
Side::BottomRight => "br",
|
||||||
|
Side::BottomLeft => "bl",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
use super::UIWidget;
|
|
||||||
|
|
||||||
pub struct PaddingInfo {
|
pub struct PaddingInfo {
|
||||||
pub right: Option<u32>,
|
pub right: Option<u32>,
|
||||||
}
|
}
|
||||||
|
@ -51,7 +50,7 @@ impl UIWidget for PaddingWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn base_class(&self) -> Vec<String> {
|
||||||
let mut our_class = Vec::new();
|
let mut our_class = Vec::new();
|
||||||
|
|
||||||
if let Some(r) = self.right {
|
if let Some(r) = self.right {
|
||||||
|
@ -66,15 +65,23 @@ impl UIWidget for PaddingWidget {
|
||||||
our_class.push(format!("px-{x}"));
|
our_class.push(format!("px-{x}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let our_class = our_class.join(" ");
|
our_class
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.inner.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.inner.as_ref().can_inherit() {
|
if self.inner.as_ref().can_inherit() {
|
||||||
self.inner
|
self.inner
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.render_with_class(&format!("{our_class} {class}"))
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
div class=(format!("{our_class} {class}")) {
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
(self.inner.as_ref())
|
(self.inner.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
68
src/ui/primitives/rounded.rs
Normal file
68
src/ui/primitives/rounded.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use super::{Side, Size};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Rounded<T: UIWidget + 'static>(inner: T) -> RoundedWidget {
|
||||||
|
RoundedWidget(Box::new(inner), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RoundedWidget(Box<dyn UIWidget>, Option<Size>, Option<Side>);
|
||||||
|
|
||||||
|
impl RoundedWidget {
|
||||||
|
pub fn size(mut self, size: Size) -> Self {
|
||||||
|
self.1 = Some(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn side(mut self, side: Side) -> Self {
|
||||||
|
self.2 = Some(side);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RoundedWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for RoundedWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
if let Some(side) = &self.2 {
|
||||||
|
if let Some(size) = &self.1 {
|
||||||
|
return vec![format!("rounded-{}-{}", side.to_string(), size.to_string())];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(size) = &self.1 {
|
||||||
|
return vec![format!("rounded-{}", size.to_string())];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec!["rounded".to_owned()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
src/ui/primitives/shadow.rs
Normal file
78
src/ui/primitives/shadow.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
pub struct Shadow(Box<dyn UIWidget>, String);
|
||||||
|
|
||||||
|
impl Shadow {
|
||||||
|
pub fn medium<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "md".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "sm".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regular<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn large<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "lg".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn none<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "none".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xl<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "xl".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _2xl<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "2xl".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner<T: UIWidget + 'static>(inner: T) -> Shadow {
|
||||||
|
Shadow(Box::new(inner), "inner".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 base_class(&self) -> Vec<String> {
|
||||||
|
if self.1.is_empty() {
|
||||||
|
vec!["shadow".to_string()]
|
||||||
|
} else {
|
||||||
|
vec![format!("shadow-{}", self.1)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use super::UIWidget;
|
use crate::ui::UIWidget;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
@ -19,14 +19,24 @@ impl UIWidget for SizedWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![format!("h-{}", self.1), format!("w-{}", self.2)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.0.as_ref().can_inherit() {
|
if self.0.as_ref().can_inherit() {
|
||||||
self.0
|
self.0
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.render_with_class(&format!("h-{} w-{} {class}", self.1, self.2))
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
div class=(format!("h-{} w-{} {class}", self.1, self.2)) {
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
(self.0.as_ref())
|
(self.0.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
151
src/ui/primitives/space.rs
Normal file
151
src/ui/primitives/space.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Controlling the space between child elements.
|
||||||
|
pub fn SpaceBetween<T: UIWidget + 'static>(inner: T) -> SpaceBetweenWidget {
|
||||||
|
SpaceBetweenWidget(Box::new(inner), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SpaceBetweenWidget(Box<dyn UIWidget>, Option<ScreenValue>, Option<ScreenValue>);
|
||||||
|
|
||||||
|
impl Render for SpaceBetweenWidget {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpaceBetweenWidget {
|
||||||
|
pub fn x(mut self, x: ScreenValue) -> Self {
|
||||||
|
self.1 = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y(mut self, y: ScreenValue) -> Self {
|
||||||
|
self.2 = Some(y);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for SpaceBetweenWidget {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
let mut ret = Vec::new();
|
||||||
|
|
||||||
|
if let Some(x) = &self.1 {
|
||||||
|
ret.push(format!("space-x-{}", x.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = &self.2 {
|
||||||
|
ret.push(format!("space-y-{}", y.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum ScreenValue {
|
||||||
|
_0,
|
||||||
|
_0p5,
|
||||||
|
_1,
|
||||||
|
_1p5,
|
||||||
|
_2,
|
||||||
|
_2p5,
|
||||||
|
_3,
|
||||||
|
_3p5,
|
||||||
|
_4,
|
||||||
|
_5,
|
||||||
|
_6,
|
||||||
|
_7,
|
||||||
|
_8,
|
||||||
|
_9,
|
||||||
|
_10,
|
||||||
|
_11,
|
||||||
|
_12,
|
||||||
|
_14,
|
||||||
|
_16,
|
||||||
|
_20,
|
||||||
|
_24,
|
||||||
|
_28,
|
||||||
|
_32,
|
||||||
|
_36,
|
||||||
|
_40,
|
||||||
|
_44,
|
||||||
|
_48,
|
||||||
|
_52,
|
||||||
|
_56,
|
||||||
|
_60,
|
||||||
|
_64,
|
||||||
|
_72,
|
||||||
|
_80,
|
||||||
|
_90,
|
||||||
|
px,
|
||||||
|
reverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenValue {
|
||||||
|
pub fn to_string(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ScreenValue::_0 => "0",
|
||||||
|
ScreenValue::_0p5 => "0.5",
|
||||||
|
ScreenValue::_1 => "1",
|
||||||
|
ScreenValue::_1p5 => "1.5",
|
||||||
|
ScreenValue::_2 => "2",
|
||||||
|
ScreenValue::_2p5 => "2.5",
|
||||||
|
ScreenValue::_3 => "3",
|
||||||
|
ScreenValue::_3p5 => "3.5",
|
||||||
|
ScreenValue::_4 => "4",
|
||||||
|
ScreenValue::_5 => "5",
|
||||||
|
ScreenValue::_6 => "6",
|
||||||
|
ScreenValue::_7 => "7",
|
||||||
|
ScreenValue::_8 => "8",
|
||||||
|
ScreenValue::_9 => "9",
|
||||||
|
ScreenValue::_10 => "10",
|
||||||
|
ScreenValue::_11 => "11",
|
||||||
|
ScreenValue::_12 => "12",
|
||||||
|
ScreenValue::_14 => "14",
|
||||||
|
ScreenValue::_16 => "16",
|
||||||
|
ScreenValue::_20 => "20",
|
||||||
|
ScreenValue::_24 => "24",
|
||||||
|
ScreenValue::_28 => "28",
|
||||||
|
ScreenValue::_32 => "32",
|
||||||
|
ScreenValue::_36 => "36",
|
||||||
|
ScreenValue::_40 => "40",
|
||||||
|
ScreenValue::_44 => "44",
|
||||||
|
ScreenValue::_48 => "48",
|
||||||
|
ScreenValue::_52 => "52",
|
||||||
|
ScreenValue::_56 => "56",
|
||||||
|
ScreenValue::_60 => "60",
|
||||||
|
ScreenValue::_64 => "64",
|
||||||
|
ScreenValue::_72 => "72",
|
||||||
|
ScreenValue::_80 => "80",
|
||||||
|
ScreenValue::_90 => "90",
|
||||||
|
ScreenValue::px => "px",
|
||||||
|
ScreenValue::reverse => "reverse",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
use super::UIWidget;
|
use crate::ui::{UIWidget, color::UIColor};
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
/// Text UI Widget
|
||||||
pub fn Text(txt: &str) -> TextWidget {
|
pub fn Text(txt: &str) -> TextWidget {
|
||||||
TextWidget {
|
TextWidget {
|
||||||
inner: None,
|
inner: None,
|
||||||
|
@ -14,6 +15,7 @@ pub fn Text(txt: &str) -> TextWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
/// HTML `<p>` Paragraph
|
||||||
pub fn Paragraph<T: UIWidget + 'static>(inner: T) -> TextWidget {
|
pub fn Paragraph<T: UIWidget + 'static>(inner: T) -> TextWidget {
|
||||||
TextWidget {
|
TextWidget {
|
||||||
inner: Some(Box::new(inner)),
|
inner: Some(Box::new(inner)),
|
||||||
|
@ -26,6 +28,7 @@ pub fn Paragraph<T: UIWidget + 'static>(inner: T) -> TextWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
/// `<span>` element
|
||||||
pub fn Span(txt: &str) -> TextWidget {
|
pub fn Span(txt: &str) -> TextWidget {
|
||||||
TextWidget {
|
TextWidget {
|
||||||
inner: None,
|
inner: None,
|
||||||
|
@ -47,43 +50,61 @@ pub struct TextWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextWidget {
|
impl TextWidget {
|
||||||
|
/// Turn `Text` semibold.
|
||||||
|
///
|
||||||
|
/// Adds the class `font-semibold`
|
||||||
pub fn semibold(mut self) -> Self {
|
pub fn semibold(mut self) -> Self {
|
||||||
self.font = "font-semibold".to_owned();
|
self.font = "font-semibold".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn `Text` bold.
|
||||||
|
///
|
||||||
|
/// Adds the class `font-bold`
|
||||||
pub fn bold(mut self) -> Self {
|
pub fn bold(mut self) -> Self {
|
||||||
self.font = "font-bold".to_owned();
|
self.font = "font-bold".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn `Text` medium.
|
||||||
|
///
|
||||||
|
/// Adds the class `font-medium`
|
||||||
pub fn medium(mut self) -> Self {
|
pub fn medium(mut self) -> Self {
|
||||||
self.font = "font-medium".to_owned();
|
self.font = "font-medium".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn `Text` size to 2XL.
|
||||||
|
///
|
||||||
|
/// Adds the class `text-2xl`
|
||||||
pub fn _2xl(mut self) -> Self {
|
pub fn _2xl(mut self) -> Self {
|
||||||
self.size = "text-2xl".to_owned();
|
self.size = "text-2xl".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn `Text` size to xl.
|
||||||
|
///
|
||||||
|
/// Adds the class `text-xl`
|
||||||
pub fn xl(mut self) -> Self {
|
pub fn xl(mut self) -> Self {
|
||||||
self.size = "text-xl".to_owned();
|
self.size = "text-xl".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turn `Text` size to small.
|
||||||
|
///
|
||||||
|
/// Adds the class `text-sm`
|
||||||
pub fn sm(mut self) -> Self {
|
pub fn sm(mut self) -> Self {
|
||||||
self.size = "text-sm".to_owned();
|
self.size = "text-sm".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gray(mut self, i: u32) -> Self {
|
pub fn color<T: UIColor>(mut self, color: T) -> Self {
|
||||||
self.color = format!("text-gray-{}", i);
|
self.color = format!("text-{}", color.color_class());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slate(mut self, i: u32) -> Self {
|
pub fn black(mut self) -> Self {
|
||||||
self.color = format!("text-slate-{}", i);
|
self.color = "text-black".to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,26 +125,37 @@ impl UIWidget for TextWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![self.color.clone(), self.font.clone(), self.size.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
if let Some(inner) = &self.inner {
|
||||||
|
c.extend_from_slice(&inner.extended_class());
|
||||||
|
}
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
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 let Some(inner) = &self.inner {
|
||||||
if self.span {
|
if self.span {
|
||||||
html! {
|
html! {
|
||||||
span class=(format!("{} {}", class, our_class)) { (inner) }
|
span class=(format!("{} {}", class, self.base_class().join(" "))) { (inner) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
p class=(format!("{} {}", class, our_class)) { (inner) }
|
p class=(format!("{} {}", class, self.base_class().join(" "))) { (inner) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.span {
|
if self.span {
|
||||||
html! {
|
html! {
|
||||||
span class=(format!("{} {}", class, our_class)) { (self.txt) }
|
span class=(format!("{} {}", class, self.base_class().join(" "))) { (self.txt) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
p class=(format!("{} {}", class, our_class)) { (self.txt) }
|
p class=(format!("{} {}", class, self.base_class().join(" "))) { (self.txt) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
|
use crate::ui::UIWidget;
|
||||||
use maud::{Markup, Render, html};
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
use super::UIWidget;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn FitWidth<T: UIWidget + 'static>(inner: T) -> FitWidthWidget {
|
pub fn FitWidth<T: UIWidget + 'static>(inner: T) -> FitWidthWidget {
|
||||||
FitWidthWidget(Box::new(inner))
|
FitWidthWidget(Box::new(inner))
|
||||||
|
@ -20,14 +19,24 @@ impl UIWidget for FitWidthWidget {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec!["max-w-fit".to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
let mut c = self.base_class();
|
||||||
|
c.extend_from_slice(&self.0.extended_class());
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
fn render_with_class(&self, class: &str) -> Markup {
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
if self.0.as_ref().can_inherit() {
|
if self.0.as_ref().can_inherit() {
|
||||||
self.0
|
self.0
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.render_with_class(&format!("max-w-fit {class}"))
|
.render_with_class(&format!("{} {class}", self.base_class().join(" ")))
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
div class=(format!("max-w-fit {class}")) {
|
div class=(format!("{} {class}", self.base_class().join(" "))) {
|
||||||
(self.0.as_ref())
|
(self.0.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
use maud::{Markup, Render, html};
|
|
||||||
|
|
||||||
use super::UIWidget;
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn Rounded<T: UIWidget + 'static>(inner: T) -> RoundedWidget {
|
|
||||||
RoundedWidget(Box::new(inner), "full".to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn RoundedMedium<T: UIWidget + 'static>(inner: T) -> RoundedWidget {
|
|
||||||
RoundedWidget(Box::new(inner), "md".to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RoundedWidget(Box<dyn UIWidget>, 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use maud::{Markup, Render, html};
|
|
||||||
|
|
||||||
use super::UIWidget;
|
|
||||||
|
|
||||||
pub struct Shadow(Box<dyn UIWidget>, String);
|
|
||||||
|
|
||||||
impl Shadow {
|
|
||||||
pub fn medium<T: UIWidget + 'static>(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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
57
src/ui/wrapper/hover.rs
Normal file
57
src/ui/wrapper/hover.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use maud::{Markup, Render, html};
|
||||||
|
|
||||||
|
use crate::ui::UIWidget;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Hover<T: UIWidget + 'static, I: UIWidget + 'static>(inherit: I, inner: T) -> HoverWrapper {
|
||||||
|
HoverWrapper(Box::new(inner), Box::new(inherit))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HoverWrapper(Box<dyn UIWidget>, Box<dyn UIWidget>);
|
||||||
|
|
||||||
|
impl HoverWrapper {
|
||||||
|
pub fn hovered_class(&self) -> String {
|
||||||
|
self.1
|
||||||
|
.extended_class()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|x| !x.is_empty())
|
||||||
|
.map(|x| format!("hover:{x}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for HoverWrapper {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
self.render_with_class("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UIWidget for HoverWrapper {
|
||||||
|
fn can_inherit(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_class(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extended_class(&self) -> Vec<String> {
|
||||||
|
self.base_class()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_with_class(&self, class: &str) -> Markup {
|
||||||
|
// TODO : Replace lol
|
||||||
|
if self.0.as_ref().can_inherit() {
|
||||||
|
self.0
|
||||||
|
.as_ref()
|
||||||
|
.render_with_class(&format!("{} {class}", self.hovered_class()))
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
div class=(format!("{} {class}", self.hovered_class())) {
|
||||||
|
(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
src/ui/wrapper/mod.rs
Normal file
4
src/ui/wrapper/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod hover;
|
||||||
|
pub use hover::Hover;
|
||||||
|
|
||||||
|
// TODO : responsive media
|
Loading…
Add table
Reference in a new issue