This commit is contained in:
JMARyA 2025-02-08 17:05:06 +01:00
parent 5ce50b76f5
commit 86fc33916e
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
24 changed files with 3920 additions and 104 deletions

View file

@ -0,0 +1,394 @@
use std::{collections::HashMap, fmt::Write};
use maud::{Markup, PreEscaped, Render, html};
use crate::ui::{
AttrExtendable, UIWidget,
color::{Blue, UIColor},
prelude::Nothing,
};
use super::InputAttr;
#[allow(non_snake_case)]
pub fn Range() -> RangeWidget {
RangeWidget {
attrs: HashMap::new(),
}
}
pub struct RangeWidget {
attrs: HashMap<String, String>,
}
impl AttrExtendable for RangeWidget {
fn add_attr(mut self, key: &str, val: &str) -> Self {
self.attrs.insert(key.into(), val.into());
self
}
}
impl RangeWidget {
pub fn min(self, min: u32) -> Self {
self.add_attr("min", &min.to_string())
}
pub fn max(self, max: u32) -> Self {
self.add_attr("max", &max.to_string())
}
pub fn step(self, step: u32) -> Self {
self.add_attr("step", &step.to_string())
}
}
impl InputAttr for RangeWidget {}
impl Render for RangeWidget {
fn render(&self) -> Markup {
self.render_with_class("")
}
}
impl UIWidget for RangeWidget {
fn can_inherit(&self) -> bool {
true
}
fn base_class(&self) -> Vec<String> {
vec![
"w-full".into(),
"h-2".into(),
"bg-gray-200".into(),
"rounded-lg".into(),
"appearence-none".into(),
"cursor-pointer".into(),
"dark:bg-gray-700".into(),
]
}
fn extended_class(&self) -> Vec<String> {
self.base_class()
}
fn render_with_class(&self, class: &str) -> Markup {
let mut attrs = self.attrs.clone();
attrs.insert(
"class".into(),
format!("{class} {}", self.base_class().join(" ")),
);
attrs.insert("type".into(), "range".into());
build_element("input", &attrs, PreEscaped(String::new()))
}
}
pub fn build_element(
element: &str,
attrs: &HashMap<String, String>,
inner: PreEscaped<String>,
) -> PreEscaped<String> {
let mut ret = String::with_capacity(256);
write!(&mut ret, "<{element}").unwrap();
for (key, value) in attrs {
if value.is_empty() {
write!(&mut ret, " {key}").unwrap();
} else {
write!(&mut ret, " {key}='{}'", value.replace("'", "\\'")).unwrap();
};
}
if inner.0.is_empty() {
if element == "textarea" {
write!(&mut ret, "></textarea>").unwrap();
} else {
write!(&mut ret, ">").unwrap();
}
} else {
write!(&mut ret, ">{}</{element}>", inner.0).unwrap();
}
PreEscaped(ret)
}
#[allow(non_snake_case)]
pub fn Toggle(title: &str) -> ToggleWidget {
ToggleWidget {
color: Box::new(Blue::_600),
checked: false,
attrs: HashMap::new(),
title: title.to_string(),
}
}
pub struct ToggleWidget {
color: Box<dyn UIColor>,
checked: bool,
attrs: HashMap<String, String>,
title: String,
}
impl ToggleWidget {
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
self.color = Box::new(color);
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
}
impl AttrExtendable for ToggleWidget {
fn add_attr(mut self, key: &str, val: &str) -> Self {
self.attrs.insert(key.to_string(), val.to_string());
self
}
}
impl InputAttr for ToggleWidget {}
impl Render for ToggleWidget {
fn render(&self) -> Markup {
self.render_with_class("")
}
}
impl UIWidget for ToggleWidget {
fn can_inherit(&self) -> bool {
false
}
fn base_class(&self) -> Vec<String> {
Vec::new()
}
fn extended_class(&self) -> Vec<String> {
Vec::new()
}
fn render_with_class(&self, _: &str) -> Markup {
let color = self.color.color_class();
let class = format!(
"relative w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-{color} dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-{color} dark:peer-checked:bg-{color}"
);
let mut attrs = self.attrs.clone();
attrs.insert("class".into(), "sr-only peer".into());
attrs.insert("type".into(), "checkbox".into());
if self.checked {
attrs.insert("checked".into(), "".into());
}
let input = build_element("input", &attrs, Nothing());
html! {
label class="inline-flex items-center cursor-pointer" {
(input)
div class=(class) {};
span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300" { (self.title) };
};
}
}
}
#[allow(non_snake_case)]
pub fn CustomRadio<T: UIWidget + 'static>(inner: T, group_name: &str) -> RadioWidget {
RadioWidget {
color: Box::new(Blue::_600),
checked: false,
attrs: HashMap::new(),
title: inner.render().0,
custom: true,
}
.name(group_name)
.id(&format!("radio-{}", uuid::Uuid::new_v4().to_string()))
}
#[allow(non_snake_case)]
pub fn Radio(title: &str, group_name: &str) -> RadioWidget {
RadioWidget {
color: Box::new(Blue::_600),
checked: false,
attrs: HashMap::new(),
title: title.to_string(),
custom: false,
}
.name(group_name)
}
pub struct RadioWidget {
color: Box<dyn UIColor>,
checked: bool,
attrs: HashMap<String, String>,
title: String,
custom: bool,
}
impl RadioWidget {
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
self.color = Box::new(color);
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
}
impl AttrExtendable for RadioWidget {
fn add_attr(mut self, key: &str, val: &str) -> Self {
self.attrs.insert(key.to_string(), val.to_string());
self
}
}
impl InputAttr for RadioWidget {
fn name(self, name: &str) -> Self {
self.add_attr("name", name)
}
}
impl Render for RadioWidget {
fn render(&self) -> Markup {
self.render_with_class("")
}
}
impl UIWidget for RadioWidget {
fn can_inherit(&self) -> bool {
false
}
fn base_class(&self) -> Vec<String> {
Vec::new()
}
fn extended_class(&self) -> Vec<String> {
Vec::new()
}
fn render_with_class(&self, _: &str) -> Markup {
let mut attrs = self.attrs.clone();
let color = self.color.color_class();
if self.custom {
attrs.insert("class".into(), "hidden peer".into());
} else {
attrs.insert("class".into(), format!("w-4 h-4 text-{color} bg-gray-100 border-gray-300 focus:ring-{color} dark:focus:ring-{color} dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"));
}
attrs.insert("type".into(), "radio".into());
let input = build_element("input", &attrs, Nothing());
let input_id = attrs.get("id").map(|x| x.as_str()).unwrap_or_default();
let label_class = if self.custom {
format!(
"inline-flex items-center justify-between w-full p-5 text-gray-500 bg-white border border-gray-200 rounded-lg cursor-pointer dark:hover:text-gray-300 dark:border-gray-700 dark:peer-checked:text-{color} peer-checked:border-{color} dark:peer-checked:border-{color} peer-checked:text-{color} hover:text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-700"
)
} else {
"ms-2 text-sm font-medium text-gray-900 dark:text-gray-300".to_string()
};
html! {
div class="flex items-center me-4" {
(input)
label for=(input_id) class=(label_class) {
(PreEscaped(self.title.clone()))
};
};
}
}
}
#[allow(non_snake_case)]
pub fn Checkbox(title: &str) -> CheckboxWidget {
CheckboxWidget {
color: Box::new(Blue::_600),
checked: false,
attrs: HashMap::new(),
title: title.to_string(),
}
}
pub struct CheckboxWidget {
color: Box<dyn UIColor>,
checked: bool,
attrs: HashMap<String, String>,
title: String,
}
impl CheckboxWidget {
pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
self.color = Box::new(color);
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
}
impl AttrExtendable for CheckboxWidget {
fn add_attr(mut self, key: &str, val: &str) -> Self {
self.attrs.insert(key.to_string(), val.to_string());
self
}
}
impl InputAttr for CheckboxWidget {}
impl Render for CheckboxWidget {
fn render(&self) -> Markup {
self.render_with_class("")
}
}
impl UIWidget for CheckboxWidget {
fn can_inherit(&self) -> bool {
false
}
fn base_class(&self) -> Vec<String> {
Vec::new()
}
fn extended_class(&self) -> Vec<String> {
Vec::new()
}
fn render_with_class(&self, _: &str) -> Markup {
let color = self.color.color_class();
let class = format!(
"w-4 h-4 text-{color} bg-gray-100 border-gray-300 rounded-sm focus:ring-{color} dark:focus:ring-{color} dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
);
let mut attrs = self.attrs.clone();
attrs.insert("class".into(), class);
attrs.insert("type".into(), "checkbox".into());
if self.checked {
attrs.insert("checked".into(), "".into());
}
let input = build_element("input", &attrs, Nothing());
let input_id = attrs.get("id").map(|x| x.as_str()).unwrap_or_default();
html! {
div class="flex items-center me-4" {
(input)
label for=(input_id) class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300" { (self.title) };
};
}
}
}