From f7db3333c5714d5d3ec3606c682b7c3f4cc3595a Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 05:51:22 +0100
Subject: [PATCH 01/11] add filters

---
 src/lib.rs                   |   2 +
 src/ui/mod.rs                |   3 +
 src/ui/primitives/filter.rs  | 265 +++++++++++++++++++++++++++++++++++
 src/ui/primitives/mod.rs     |   3 +
 src/ui/primitives/rounded.rs |   2 +-
 5 files changed, 274 insertions(+), 1 deletion(-)
 create mode 100644 src/ui/primitives/filter.rs

diff --git a/src/lib.rs b/src/lib.rs
index 1985369..809b519 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,5 @@
+#![feature(const_vec_string_slice)]
+
 use tokio::sync::OnceCell;
 
 pub mod auth;
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 602b811..90bb54f 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -26,6 +26,9 @@ pub mod prelude {
     pub use super::primitives::container::Container;
     pub use super::primitives::cursor::Cursor;
     pub use super::primitives::div::Div;
+    pub use super::primitives::filter::{
+        Blur, Brightness, Contrast, Grayscale, HueRotate, Invert, Saturate, Sepia,
+    };
     pub use super::primitives::flex::{
         Direction, Flex, FlexBasis, FlexGrow, Justify, Order, Strategy, Wrap,
     };
diff --git a/src/ui/primitives/filter.rs b/src/ui/primitives/filter.rs
new file mode 100644
index 0000000..7598bc9
--- /dev/null
+++ b/src/ui/primitives/filter.rs
@@ -0,0 +1,265 @@
+use crate::ui::UIWidget;
+use maud::{Markup, Render, html};
+
+use super::Size;
+
+#[allow(non_snake_case)]
+pub fn Blur<T: UIWidget + 'static>(amount: Size, inner: T) -> BlurWidget {
+    BlurWidget(Box::new(inner), amount, false)
+}
+
+pub struct BlurWidget(Box<dyn UIWidget>, Size, bool);
+
+impl BlurWidget {
+    pub fn backdrop(mut self) -> Self {
+        self.2 = true;
+        self
+    }
+}
+
+impl Render for BlurWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for BlurWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let class = match &self.1 {
+            Size::Custom(s) => &format!(" blur-[{s}]"),
+            Size::None => "blur-none",
+            Size::Small => "blur-sm",
+            Size::Regular => "blur",
+            Size::Medium => "blur-md",
+            Size::Large => "blur-lg",
+            Size::XL => "blur-xl",
+            Size::_2XL => "blur-2xl",
+            Size::_3XL => "blur-3xl",
+            Size::Full => "blur-3xl",
+        };
+
+        if self.2 {
+            return vec![format!("backdrop-{class}")];
+        }
+
+        vec![class.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 {
+        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())
+                }
+            }
+        }
+    }
+}
+
+macro_rules! build_value_widget {
+    ($constr:ident, $widget:ident, $class:literal) => {
+        #[allow(non_snake_case)]
+        pub fn $constr<T: UIWidget + 'static>(value: f64, inner: T) -> $widget {
+            $widget(Box::new(inner), value, false)
+        }
+
+        pub struct $widget(Box<dyn UIWidget>, f64, bool);
+
+        impl $widget {
+            pub fn backdrop(mut self) -> Self {
+                self.2 = true;
+                self
+            }
+        }
+
+        impl Render for $widget {
+            fn render(&self) -> Markup {
+                self.render_with_class("")
+            }
+        }
+
+        impl UIWidget for $widget {
+            fn can_inherit(&self) -> bool {
+                true
+            }
+
+            fn base_class(&self) -> Vec<String> {
+                let mut ret = $class.to_string();
+                ret.push_str(&format!("-[{:.2}]", self.1));
+
+                if self.2 {
+                    return vec![format!("backdrop-{ret}")];
+                }
+
+                vec![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())
+                        }
+                    }
+                }
+            }
+        }
+    };
+}
+
+build_value_widget!(Brightness, BrightnessWidget, "brightness");
+build_value_widget!(Contrast, ConstrastWidget, "contrast");
+build_value_widget!(Saturate, SaturationWidget, "saturate");
+
+macro_rules! build_on_off_widget {
+    ($constr:ident, $widget:ident, $class:literal) => {
+        #[allow(non_snake_case)]
+        pub fn $constr<T: UIWidget + 'static>(inner: T) -> $widget {
+            $widget(Box::new(inner), true, false)
+        }
+
+        pub struct $widget(Box<dyn UIWidget>, bool, bool);
+
+        impl $widget {
+            pub fn none(mut self) -> Self {
+                self.1 = false;
+                self
+            }
+
+            pub fn backdrop(mut self) -> Self {
+                self.2 = true;
+                self
+            }
+        }
+
+        impl Render for $widget {
+            fn render(&self) -> Markup {
+                self.render_with_class("")
+            }
+        }
+
+        impl UIWidget for $widget {
+            fn can_inherit(&self) -> bool {
+                true
+            }
+
+            fn base_class(&self) -> Vec<String> {
+                let class = if self.1 {
+                    $class.to_string()
+                } else {
+                    concat!($class, "-0").to_string()
+                };
+
+                if self.2 {
+                    return vec![format!("backdrop-{class}")];
+                }
+
+                vec![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 {
+                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())
+                        }
+                    }
+                }
+            }
+        }
+    };
+}
+
+build_on_off_widget!(Grayscale, GrayscaleWidget, "grayscale");
+build_on_off_widget!(Invert, InvertWidget, "invert");
+build_on_off_widget!(Sepia, SepiaWidget, "sepia");
+
+#[allow(non_snake_case)]
+pub fn HueRotate<T: UIWidget + 'static>(deg: u32, inner: T) -> HueRotateWidget {
+    HueRotateWidget(Box::new(inner), deg, false)
+}
+
+pub struct HueRotateWidget(Box<dyn UIWidget>, u32, bool);
+
+impl HueRotateWidget {
+    pub fn backdrop(mut self) -> Self {
+        self.2 = true;
+        self
+    }
+}
+
+impl Render for HueRotateWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for HueRotateWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let class = format!("hue-rotate-[{:.2}deg]", self.1);
+
+        if self.2 {
+            return vec![format!("backdrop-{class}")];
+        }
+
+        vec![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 {
+        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())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index 0e1c096..25fa8b7 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -8,6 +8,7 @@ pub mod background;
 pub mod container;
 pub mod cursor;
 pub mod div;
+pub mod filter;
 pub mod flex;
 pub mod header;
 pub mod height;
@@ -62,6 +63,7 @@ pub fn script(script: &str) -> PreEscaped<String> {
 }
 
 pub enum Size {
+    Custom(String),
     None,
     Small,
     Regular,
@@ -77,6 +79,7 @@ impl Size {
     #[must_use]
     pub const fn to_value(&self) -> &str {
         match self {
+            Self::Custom(str) => str.as_str(),
             Self::None => "none",
             Self::Small => "sm",
             Self::Regular => "",
diff --git a/src/ui/primitives/rounded.rs b/src/ui/primitives/rounded.rs
index f716814..d448fe3 100644
--- a/src/ui/primitives/rounded.rs
+++ b/src/ui/primitives/rounded.rs
@@ -12,7 +12,7 @@ pub struct RoundedWidget(Box<dyn UIWidget>, Option<Size>, Option<Side>);
 
 impl RoundedWidget {
     #[must_use]
-    pub const fn size(mut self, size: Size) -> Self {
+    pub fn size(mut self, size: Size) -> Self {
         self.1 = Some(size);
         self
     }

From 5d4aa21eddde93f31e52c2b44e5b72b7ca364f9a Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 06:09:56 +0100
Subject: [PATCH 02/11] update

---
 src/ui/primitives/filter.rs | 214 ++++++++++++++++++++++++++++++++++++
 src/ui/primitives/shadow.rs |  49 ++++++---
 2 files changed, 247 insertions(+), 16 deletions(-)

diff --git a/src/ui/primitives/filter.rs b/src/ui/primitives/filter.rs
index 7598bc9..3218387 100644
--- a/src/ui/primitives/filter.rs
+++ b/src/ui/primitives/filter.rs
@@ -263,3 +263,217 @@ impl UIWidget for HueRotateWidget {
         }
     }
 }
+
+#[allow(non_snake_case)]
+pub fn Opacity<T: UIWidget + 'static>(value: f64, inner: T) -> OpacityWidget {
+    OpacityWidget(Box::new(inner), value, false)
+}
+
+pub struct OpacityWidget(Box<dyn UIWidget>, f64, bool);
+
+impl OpacityWidget {
+    pub fn backdrop(mut self) -> Self {
+        self.2 = true;
+        self
+    }
+}
+
+impl Render for OpacityWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for OpacityWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let class = match self.1 {
+            0.0 => "opacity-0",
+            0.05 => "opacity-5",
+            0.1 => "opacity-10",
+            0.15 => "opacity-15",
+            0.2 => "opacity-20",
+            0.25 => "opacity-25",
+            0.3 => "opacity-30",
+            0.35 => "opacity-35",
+            0.4 => "opacity-40",
+            0.45 => "opacity-45",
+            0.5 => "opacity-50",
+            0.55 => "opacity-55",
+            0.6 => "opacity-60",
+            0.65 => "opacity-65",
+            0.7 => "opacity-70",
+            0.75 => "opacity-75",
+            0.8 => "opacity-80",
+            0.85 => "opacity-85",
+            0.9 => "opacity-90",
+            0.95 => "opacity-95",
+            1.0 => "opacity-100",
+            _ => &format!("opacity-[{:.2}]", self.1),
+        };
+
+        if self.2 {
+            return vec![format!("backdrop-{class}")];
+        }
+
+        vec![class.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 {
+        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())
+                }
+            }
+        }
+    }
+}
+
+pub enum BlendMode {
+    Normal,
+    Multiply,
+    Screen,
+    Overlay,
+    Darken,
+    Lighten,
+    ColorDodge,
+    ColorBurn,
+    HardLight,
+    SoftLight,
+    Difference,
+    Exclusion,
+    Hue,
+    Saturation,
+    Color,
+    Luminosity,
+    PlusDarker,
+    PlusLighter,
+}
+
+impl BlendMode {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BlendMode::Normal => "normal",
+            BlendMode::Multiply => "multiply",
+            BlendMode::Screen => "screen",
+            BlendMode::Overlay => "overlay",
+            BlendMode::Darken => "darken",
+            BlendMode::Lighten => "lighten",
+            BlendMode::ColorDodge => "color-dodge",
+            BlendMode::ColorBurn => "color-burn",
+            BlendMode::HardLight => "hard-light",
+            BlendMode::SoftLight => "soft-light",
+            BlendMode::Difference => "difference",
+            BlendMode::Exclusion => "exclusion",
+            BlendMode::Hue => "hue",
+            BlendMode::Saturation => "saturation",
+            BlendMode::Color => "color",
+            BlendMode::Luminosity => "luminosity",
+            BlendMode::PlusDarker => "plus-darker",
+            BlendMode::PlusLighter => "plus-lighter",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn MixBlendMode<T: UIWidget + 'static>(mode: BlendMode, inner: T) -> MixBlendModeWidget {
+    MixBlendModeWidget(Box::new(inner), mode)
+}
+
+pub struct MixBlendModeWidget(Box<dyn UIWidget>, BlendMode);
+
+impl Render for MixBlendModeWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for MixBlendModeWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![format!("mix-blend-{}", self.1.to_value())]
+    }
+
+    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_snake_case)]
+pub fn BackgroundBlendMode<T: UIWidget + 'static>(
+    mode: BlendMode,
+    inner: T,
+) -> BackgroundBlendModeWidget {
+    BackgroundBlendModeWidget(Box::new(inner), mode)
+}
+
+pub struct BackgroundBlendModeWidget(Box<dyn UIWidget>, BlendMode);
+
+impl Render for BackgroundBlendModeWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for BackgroundBlendModeWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![format!("bg-blend-{}", self.1.to_value())]
+    }
+
+    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())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/primitives/shadow.rs b/src/ui/primitives/shadow.rs
index 92f2323..5e3cc64 100644
--- a/src/ui/primitives/shadow.rs
+++ b/src/ui/primitives/shadow.rs
@@ -1,35 +1,46 @@
-use crate::ui::UIWidget;
+use crate::ui::{UIWidget, color::UIColor};
 use maud::{Markup, Render, html};
 
-pub struct Shadow(Box<dyn UIWidget>, String);
+pub struct Shadow(Box<dyn UIWidget>, String, Option<Box<dyn UIColor>>);
 
 impl Shadow {
-    pub fn medium<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), "md".to_owned())
-    }
-
     pub fn small<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), "sm".to_owned())
+        Self(Box::new(inner), "sm".to_owned(), None)
     }
 
     pub fn regular<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), String::new())
+        Self(Box::new(inner), String::new(), None)
+    }
+
+    pub fn medium<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "md".to_owned(), None)
     }
 
     pub fn large<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), "lg".to_owned())
-    }
-
-    pub fn none<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), "none".to_owned())
+        Self(Box::new(inner), "lg".to_owned(), None)
     }
 
     pub fn xl<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), "xl".to_owned())
+        Self(Box::new(inner), "xl".to_owned(), None)
     }
 
     pub fn _2xl<T: UIWidget + 'static>(inner: T) -> Self {
-        Self(Box::new(inner), "2xl".to_owned())
+        Self(Box::new(inner), "2xl".to_owned(), None)
+    }
+
+    pub fn inner<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "inner".to_owned(), None)
+    }
+
+    pub fn none<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "none".to_owned(), None)
+    }
+}
+
+impl Shadow {
+    pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.2 = Some(Box::new(color));
+        self
     }
 }
 
@@ -45,11 +56,17 @@ impl UIWidget for Shadow {
     }
 
     fn base_class(&self) -> Vec<String> {
-        if self.1.is_empty() {
+        let mut ret = if self.1.is_empty() {
             vec!["shadow".to_string()]
         } else {
             vec![format!("shadow-{}", self.1)]
+        };
+
+        if let Some(color) = &self.2 {
+            ret.push(format!("shadow-{}", color.color_class()));
         }
+
+        ret
     }
 
     fn extended_class(&self) -> Vec<String> {

From 35669c423cb0007d2fa577519cbbd9a7dfbfcd3d Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 06:44:06 +0100
Subject: [PATCH 03/11] add border

---
 src/ui/mod.rs               |   1 +
 src/ui/primitives/border.rs | 166 ++++++++++++++++++++++++++++++++++++
 src/ui/primitives/flex.rs   | 109 ++++++++++++++++++++++-
 src/ui/primitives/mod.rs    |   1 +
 4 files changed, 273 insertions(+), 4 deletions(-)
 create mode 100644 src/ui/primitives/border.rs

diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 90bb54f..3bce85f 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -23,6 +23,7 @@ pub mod prelude {
     pub use super::primitives::animation::{Animated, Animation, Delay, Duration, Scope, Timing};
     pub use super::primitives::aspect::Aspect;
     pub use super::primitives::background::Background;
+    pub use super::primitives::border::{Border, BorderSide, BorderSize, BorderStyle};
     pub use super::primitives::container::Container;
     pub use super::primitives::cursor::Cursor;
     pub use super::primitives::div::Div;
diff --git a/src/ui/primitives/border.rs b/src/ui/primitives/border.rs
new file mode 100644
index 0000000..12bfd56
--- /dev/null
+++ b/src/ui/primitives/border.rs
@@ -0,0 +1,166 @@
+use maud::{Markup, Render, html};
+
+use crate::ui::{UIWidget, color::UIColor};
+
+pub enum BorderSize {
+    _0,
+    _2,
+    _4,
+    _8,
+}
+
+impl BorderSize {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BorderSize::_0 => "0",
+            BorderSize::_2 => "2",
+            BorderSize::_4 => "4",
+            BorderSize::_8 => "8",
+        }
+    }
+}
+
+pub enum BorderSide {
+    X,
+    Y,
+    Start,
+    End,
+    Top,
+    Right,
+    Bottom,
+    Left,
+}
+
+impl BorderSide {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BorderSide::X => "x",
+            BorderSide::Y => "y",
+            BorderSide::Start => "s",
+            BorderSide::End => "e",
+            BorderSide::Top => "t",
+            BorderSide::Right => "r",
+            BorderSide::Bottom => "b",
+            BorderSide::Left => "l",
+        }
+    }
+}
+
+pub enum BorderStyle {
+    Solid,
+    Dashed,
+    Dotted,
+    Double,
+    Hidden,
+    None,
+}
+
+impl BorderStyle {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BorderStyle::Solid => "border-solid",
+            BorderStyle::Dashed => "border-dashed",
+            BorderStyle::Dotted => "border-dotted",
+            BorderStyle::Double => "border-double",
+            BorderStyle::Hidden => "border-hidden",
+            BorderStyle::None => "border-none",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn Border<T: UIWidget + 'static>(inner: T) -> BorderWidget {
+    BorderWidget(Box::new(inner), None, None, None, None)
+}
+
+pub struct BorderWidget(
+    Box<dyn UIWidget>,
+    Option<BorderSize>,
+    Option<BorderSide>,
+    Option<Box<dyn UIColor>>,
+    Option<BorderStyle>,
+);
+
+impl BorderWidget {
+    #[must_use]
+    pub fn size(mut self, size: BorderSize) -> Self {
+        self.1 = Some(size);
+        self
+    }
+
+    #[must_use]
+    pub const fn side(mut self, side: BorderSide) -> Self {
+        self.2 = Some(side);
+        self
+    }
+
+    #[must_use]
+    pub const fn style(mut self, style: BorderStyle) -> Self {
+        self.4 = Some(style);
+        self
+    }
+
+    #[must_use]
+    pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.3 = Some(Box::new(color));
+        self
+    }
+
+    fn border_class(&self) -> String {
+        if let Some(side) = &self.2 {
+            if let Some(size) = &self.1 {
+                return format!("border-{}-{}", side.to_value(), size.to_value());
+            }
+        } else if let Some(size) = &self.1 {
+            return format!("border-{}", size.to_value());
+        }
+
+        "border".to_owned()
+    }
+}
+
+impl Render for BorderWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for BorderWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let mut ret = vec![self.border_class()];
+
+        if let Some(color) = &self.3 {
+            ret.push(format!("border-{}", color.color_class()));
+        }
+
+        if let Some(style) = &self.4 {
+            ret.push(style.to_value().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())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/primitives/flex.rs b/src/ui/primitives/flex.rs
index b662362..2c9013a 100644
--- a/src/ui/primitives/flex.rs
+++ b/src/ui/primitives/flex.rs
@@ -1,11 +1,11 @@
-use crate::ui::UIWidget;
+use crate::ui::{UIWidget, color::UIColor};
 use maud::{Markup, Render, html};
 
 use super::space::{Fraction, ScreenValue};
 
 #[allow(non_snake_case)]
 pub fn Flex<T: UIWidget + 'static>(inner: T) -> FlexWidget {
-    FlexWidget(Box::new(inner), vec![], false)
+    FlexWidget(Box::new(inner), vec![], false, None)
 }
 
 pub enum Justify {
@@ -19,7 +19,7 @@ pub enum Justify {
     Stretch,
 }
 
-pub struct FlexWidget(Box<dyn UIWidget>, Vec<String>, bool);
+pub struct FlexWidget(Box<dyn UIWidget>, Vec<String>, bool, Option<Direction>);
 
 impl Render for FlexWidget {
     fn render(&self) -> Markup {
@@ -41,9 +41,65 @@ impl FlexWidget {
         self
     }
 
+    #[must_use]
+    pub fn divide_style(mut self, style: DivideStyle) -> Self {
+        self.1.push(style.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn divide_color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.1.push(format!("divide-{}", color.color_class()));
+        self
+    }
+
+    #[must_use]
+    pub fn divide_x(mut self, width: DivideWidth) -> Self {
+        let reversed = self
+            .3
+            .as_ref()
+            .map(|x| match x {
+                Direction::Row => false,
+                Direction::RowReverse => true,
+                Direction::Column => false,
+                Direction::ColumnReverse => true,
+            })
+            .unwrap_or_default();
+
+        self.1.push(format!("divide-x-{}", width.to_value()));
+
+        if reversed {
+            self.1.push("divide-x-reverse".to_string());
+        }
+
+        self
+    }
+
+    #[must_use]
+    pub fn divide_y(mut self, width: DivideWidth) -> Self {
+        let reversed = self
+            .3
+            .as_ref()
+            .map(|x| match x {
+                Direction::Row => false,
+                Direction::RowReverse => true,
+                Direction::Column => false,
+                Direction::ColumnReverse => true,
+            })
+            .unwrap_or_default();
+
+        self.1.push(format!("divide-y-{}", width.to_value()));
+
+        if reversed {
+            self.1.push("divide-y-reverse".to_string());
+        }
+
+        self
+    }
+
     #[must_use]
     pub fn direction(mut self, direction: Direction) -> Self {
-        self.1.push(format!("flex-{}", direction.to_value()));
+        self.3 = Some(direction);
         self
     }
 
@@ -95,6 +151,26 @@ impl FlexWidget {
     }
 }
 
+pub enum DivideWidth {
+    Custom(u64),
+    _0,
+    _2,
+    _4,
+    _8,
+}
+
+impl DivideWidth {
+    pub fn to_value(&self) -> String {
+        match self {
+            DivideWidth::Custom(s) => format!("[{s}px]"),
+            DivideWidth::_0 => "0".to_string(),
+            DivideWidth::_2 => "2".to_string(),
+            DivideWidth::_4 => "4".to_string(),
+            DivideWidth::_8 => "8".to_string(),
+        }
+    }
+}
+
 pub enum Direction {
     Row,
     RowReverse,
@@ -136,6 +212,11 @@ impl UIWidget for FlexWidget {
 
     fn base_class(&self) -> Vec<String> {
         let mut res = vec!["flex".to_string()];
+
+        if let Some(direction) = &self.3 {
+            res.push(format!("flex-{}", direction.to_value()));
+        }
+
         res.extend_from_slice(&self.1);
         res
     }
@@ -399,3 +480,23 @@ impl UIWidget for OrderWidget {
         }
     }
 }
+
+pub enum DivideStyle {
+    Solid,
+    Dashed,
+    Dotted,
+    Double,
+    None,
+}
+
+impl DivideStyle {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            DivideStyle::Solid => "divide-solid",
+            DivideStyle::Dashed => "divide-dashed",
+            DivideStyle::Dotted => "divide-dotted",
+            DivideStyle::Double => "divide-double",
+            DivideStyle::None => "divide-none",
+        }
+    }
+}
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index 25fa8b7..c2ae3dc 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -5,6 +5,7 @@ use super::UIWidget;
 pub mod animation;
 pub mod aspect;
 pub mod background;
+pub mod border;
 pub mod container;
 pub mod cursor;
 pub mod div;

From bc27b457ea69eaef41c4c2dd77b668de5cb7423e Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 08:41:20 +0100
Subject: [PATCH 04/11] add svg

---
 src/ui/components/shell.rs |  2 +
 src/ui/mod.rs              |  1 +
 src/ui/primitives/mod.rs   |  1 +
 src/ui/primitives/svg.rs   | 76 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 80 insertions(+)
 create mode 100644 src/ui/primitives/svg.rs

diff --git a/src/ui/components/shell.rs b/src/ui/components/shell.rs
index bdcca62..2bc0396 100644
--- a/src/ui/components/shell.rs
+++ b/src/ui/components/shell.rs
@@ -1,5 +1,7 @@
 use maud::{PreEscaped, html};
 
+// TODO : refactor shell
+
 /// 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.
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 3bce85f..9b06d13 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -44,6 +44,7 @@ pub mod prelude {
     pub use super::primitives::shadow::Shadow;
     pub use super::primitives::sized::Sized;
     pub use super::primitives::space::{ScreenValue, SpaceBetween};
+    pub use super::primitives::svg::SVG;
     pub use super::primitives::text::{
         Code, DecorationKind, DecorationStyle, DecorationThickness, LetterSpacing, LineClamp,
         LineHeight, ListStyle, NumberStyle, Paragraph, Span, Text, TextAlignment, TextContent,
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index c2ae3dc..1ddc144 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -22,6 +22,7 @@ pub mod rounded;
 pub mod shadow;
 pub mod sized;
 pub mod space;
+pub mod svg;
 pub mod text;
 pub mod transform;
 pub mod visibility;
diff --git a/src/ui/primitives/svg.rs b/src/ui/primitives/svg.rs
new file mode 100644
index 0000000..79de1cf
--- /dev/null
+++ b/src/ui/primitives/svg.rs
@@ -0,0 +1,76 @@
+use maud::{Markup, Render, html};
+
+use crate::ui::{UIWidget, color::UIColor};
+
+#[allow(non_snake_case)]
+pub fn SVG<T: UIWidget + 'static>(inner: T) -> SVGWidget {
+    SVGWidget(Box::new(inner), None, None, None)
+}
+
+pub struct SVGWidget(
+    Box<dyn UIWidget>,
+    Option<Box<dyn UIColor>>,
+    Option<Box<dyn UIColor>>,
+    Option<u32>,
+);
+
+impl SVGWidget {
+    pub fn fill<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.1 = Some(Box::new(color));
+        self
+    }
+
+    pub fn stroke<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.2 = Some(Box::new(color));
+        self
+    }
+
+    pub fn stroke_width(mut self, width: u32) -> Self {
+        self.3 = Some(width);
+        self
+    }
+}
+
+impl Render for SVGWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for SVGWidget {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let mut ret = vec![];
+
+        if let Some(fill) = &self.1 {
+            ret.push(format!("fill-{}", fill.color_class()));
+        }
+
+        if let Some(stroke) = &self.2 {
+            ret.push(format!("stroke-{}", stroke.color_class()));
+        }
+
+        if let Some(stroke_width) = &self.3 {
+            ret.push(format!("stroke-[{stroke_width}px]"));
+        }
+
+        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, _: &str) -> Markup {
+        html! {
+            svg class=(self.base_class().join(" ")) {
+                (self.0.as_ref())
+            }
+        }
+    }
+}

From ddd2e363c2f1a22c15527404dc986bc0e01910ff Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 09:20:15 +0100
Subject: [PATCH 05/11] add more borders

---
 src/ui/mod.rs               |   4 +-
 src/ui/primitives/border.rs | 231 ++++++++++++++++++++++++++++++++++++
 2 files changed, 234 insertions(+), 1 deletion(-)

diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 9b06d13..bb8a803 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -23,7 +23,9 @@ pub mod prelude {
     pub use super::primitives::animation::{Animated, Animation, Delay, Duration, Scope, Timing};
     pub use super::primitives::aspect::Aspect;
     pub use super::primitives::background::Background;
-    pub use super::primitives::border::{Border, BorderSide, BorderSize, BorderStyle};
+    pub use super::primitives::border::{
+        Border, BorderSide, BorderSize, BorderStyle, Outline, OutlineStyle, Ring,
+    };
     pub use super::primitives::container::Container;
     pub use super::primitives::cursor::Cursor;
     pub use super::primitives::div::Div;
diff --git a/src/ui/primitives/border.rs b/src/ui/primitives/border.rs
index 12bfd56..c7e14c3 100644
--- a/src/ui/primitives/border.rs
+++ b/src/ui/primitives/border.rs
@@ -164,3 +164,234 @@ impl UIWidget for BorderWidget {
         }
     }
 }
+
+pub enum OutlineStyle {
+    Solid,
+    Dashed,
+    Dotted,
+    Double,
+    None,
+}
+
+impl OutlineStyle {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            OutlineStyle::Solid => "outline",
+            OutlineStyle::Dashed => "outline-dashed",
+            OutlineStyle::Dotted => "outline-dotted",
+            OutlineStyle::Double => "outline-double",
+            OutlineStyle::None => "outline-none",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn Outline<T: UIWidget + 'static>(width: u32, inner: T) -> OutlineWidget {
+    OutlineWidget(Box::new(inner), width, None, None, 0)
+}
+
+pub struct OutlineWidget(
+    Box<dyn UIWidget>,
+    u32,
+    Option<Box<dyn UIColor>>,
+    Option<OutlineStyle>,
+    u32,
+);
+
+impl OutlineWidget {
+    #[must_use]
+    pub const fn offset(mut self, offset: u32) -> Self {
+        self.4 = offset;
+        self
+    }
+
+    #[must_use]
+    pub const fn style(mut self, style: OutlineStyle) -> Self {
+        self.3 = Some(style);
+        self
+    }
+
+    #[must_use]
+    pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.2 = Some(Box::new(color));
+        self
+    }
+}
+
+impl Render for OutlineWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for OutlineWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let class = match self.1 {
+            0 => "outline-0",
+            1 => "outline-1",
+            2 => "outline-2",
+            4 => "outline-4",
+            8 => "outline-8",
+            _ => &format!("outline-[{}px]", self.1),
+        };
+
+        let mut ret = vec![class.to_string()];
+
+        if let Some(color) = &self.2 {
+            ret.push(format!("outline-{}", color.color_class()));
+        }
+
+        if let Some(style) = &self.3 {
+            ret.push(style.to_value().to_string());
+        }
+
+        ret.push(match self.4 {
+            0 => "outline-offset-0".to_string(),
+            1 => "outline-offset-1".to_string(),
+            2 => "outline-offset-2".to_string(),
+            4 => "outline-offset-4".to_string(),
+            8 => "outline-offset-8".to_string(),
+            _ => format!("outline-offset-[{}px]", self.4),
+        });
+
+        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_snake_case)]
+pub fn Ring<T: UIWidget + 'static>(width: u32, inner: T) -> RingWidget {
+    RingWidget(Box::new(inner), width, None, false, 0, None)
+}
+
+pub struct RingWidget(
+    // Inner
+    Box<dyn UIWidget>,
+    // Size
+    u32,
+    // Color
+    Option<Box<dyn UIColor>>,
+    // Inset
+    bool,
+    // Offset Width
+    u32,
+    // Offset Color
+    Option<Box<dyn UIColor>>,
+);
+
+impl RingWidget {
+    #[must_use]
+    pub const fn inset(mut self) -> Self {
+        self.3 = true;
+        self
+    }
+
+    #[must_use]
+    pub const fn offset_width(mut self, offset: u32) -> Self {
+        self.4 = offset;
+        self
+    }
+
+    #[must_use]
+    pub fn offset_color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.5 = Some(Box::new(color));
+        self
+    }
+
+    #[must_use]
+    pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.2 = Some(Box::new(color));
+        self
+    }
+}
+
+impl Render for RingWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for RingWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let class = match self.1 {
+            0 => "ring-0",
+            1 => "ring-1",
+            2 => "ring-2",
+            4 => "ring-4",
+            8 => "ring-8",
+            _ => &format!("ring-[{}px]", self.1),
+        };
+
+        let mut ret = vec![class.to_string()];
+
+        if let Some(color) = &self.2 {
+            ret.push(format!("ring-{}", color.color_class()));
+        }
+
+        if self.3 {
+            ret.push("ring-inset".to_string());
+        }
+
+        ret.push(match self.4 {
+            0 => "ring-offset-0".to_string(),
+            1 => "ring-offset-1".to_string(),
+            2 => "ring-offset-2".to_string(),
+            4 => "ring-offset-4".to_string(),
+            8 => "ring-offset-8".to_string(),
+            _ => format!("ring-offset-[{}px]", self.4),
+        });
+
+        if let Some(color) = &self.5 {
+            ret.push(format!("ring-offset-{}", color.color_class()));
+        }
+
+        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())
+                }
+            }
+        }
+    }
+}

From 01e33afd938bb0553ec4aba0bc7fa42b9cabff47 Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 12:18:27 +0100
Subject: [PATCH 06/11] background

---
 src/ui/color.rs                 |  78 +++++++++
 src/ui/components/appbar.rs     |  10 +-
 src/ui/primitives/background.rs | 288 +++++++++++++++++++++++++++++++-
 3 files changed, 363 insertions(+), 13 deletions(-)

diff --git a/src/ui/color.rs b/src/ui/color.rs
index c42e898..4671712 100644
--- a/src/ui/color.rs
+++ b/src/ui/color.rs
@@ -133,3 +133,81 @@ impl UIColor for Colors {
         }
     }
 }
+
+// TODO : Gradient
+
+pub struct Gradient {
+    start: Box<dyn UIColor>,
+    middle: Option<Box<dyn UIColor>>,
+    end: Option<Box<dyn UIColor>>,
+    pos_start: Option<u8>,
+    pos_middle: Option<u8>,
+    pos_end: Option<u8>,
+}
+
+impl Gradient {
+    pub fn from<C: UIColor + 'static>(start: C) -> Self {
+        Self {
+            start: Box::new(start),
+            middle: None,
+            end: None,
+            pos_end: None,
+            pos_middle: None,
+            pos_start: None,
+        }
+    }
+
+    pub fn via<C: UIColor + 'static>(mut self, middle: C) -> Self {
+        self.middle = Some(Box::new(middle));
+        self
+    }
+
+    pub fn to<C: UIColor + 'static>(mut self, end: C) -> Self {
+        self.end = Some(Box::new(end));
+        self
+    }
+
+    pub fn step_start(mut self, percentage: u8) -> Self {
+        assert!(percentage <= 100, "Percentage should be under 100%");
+        self.pos_start = Some(percentage);
+        self
+    }
+
+    pub fn step_middle(mut self, percentage: u8) -> Self {
+        assert!(percentage <= 100, "Percentage should be under 100%");
+        self.pos_middle = Some(percentage);
+        self
+    }
+
+    pub fn step_end(mut self, percentage: u8) -> Self {
+        assert!(percentage <= 100, "Percentage should be under 100%");
+        self.pos_end = Some(percentage);
+        self
+    }
+
+    pub fn color_class(&self) -> Vec<String> {
+        let mut classes = vec![format!("from-{}", self.start.color_class())];
+
+        if let Some(via) = &self.middle {
+            classes.push(format!("via-{}", via.color_class()));
+        }
+
+        if let Some(end) = &self.end {
+            classes.push(format!("to-{}", end.color_class()));
+        }
+
+        if let Some(step) = &self.pos_start {
+            classes.push(format!("from-{step}%"));
+        }
+
+        if let Some(step) = &self.pos_middle {
+            classes.push(format!("via-{step}%"));
+        }
+
+        if let Some(step) = &self.pos_end {
+            classes.push(format!("to-{step}%"));
+        }
+
+        classes
+    }
+}
diff --git a/src/ui/components/appbar.rs b/src/ui/components/appbar.rs
index 50fbbb8..8145568 100644
--- a/src/ui/components/appbar.rs
+++ b/src/ui/components/appbar.rs
@@ -38,9 +38,8 @@ impl UIWidget for AppBarWidget {
     }
 
     fn render_with_class(&self, _: &str) -> Markup {
-        Padding(Shadow::medium(Background(
-            Gray::_800,
-            Header(
+        Padding(Shadow::medium(
+            Background(Header(
                 Padding(
                     Flex(
                         Div()
@@ -70,8 +69,9 @@ impl UIWidget for AppBarWidget {
                     .items_center(),
                 )
                 .x(ScreenValue::_6),
-            ),
-        )))
+            ))
+            .color(Gray::_800),
+        ))
         .y(ScreenValue::_2)
         .render()
     }
diff --git a/src/ui/primitives/background.rs b/src/ui/primitives/background.rs
index e07503e..dbd000f 100644
--- a/src/ui/primitives/background.rs
+++ b/src/ui/primitives/background.rs
@@ -1,16 +1,98 @@
 use maud::{Markup, Render, html};
 
-use crate::ui::{UIWidget, color::UIColor};
+use crate::ui::{
+    UIWidget,
+    color::{Gradient, UIColor},
+};
 
 #[allow(non_snake_case)]
-pub fn Background<T: UIWidget + 'static, C: UIColor + 'static>(
-    color: C,
-    inner: T,
-) -> BackgroundWidget {
-    BackgroundWidget(Box::new(inner), Box::new(color))
+pub fn Background<T: UIWidget + 'static>(inner: T) -> BackgroundWidget {
+    BackgroundWidget(
+        Box::new(inner),
+        None,
+        None,
+        None,
+        None,
+        None,
+        None,
+        None,
+        None,
+        None,
+        None,
+    )
 }
 
-pub struct BackgroundWidget(Box<dyn UIWidget>, Box<dyn UIColor>);
+pub struct BackgroundWidget(
+    // Inner
+    Box<dyn UIWidget>,
+    // Background Color
+    Option<Box<dyn UIColor>>,
+    // Background Attachment
+    Option<BackgroundScrollAttachment>,
+    Option<BackgroundClip>,
+    Option<BackgroundOrigin>,
+    Option<BackgroundRepeat>,
+    Option<BackgroundSize>,
+    // Background Image URL
+    Option<String>,
+    // Gradient
+    Option<BackgroundGradient>,
+    Option<Gradient>,
+    Option<BackgroundPosition>,
+);
+
+impl BackgroundWidget {
+    pub fn color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.1 = Some(Box::new(color));
+        self
+    }
+
+    pub fn image(mut self, url: &str) -> Self {
+        self.7 = Some(url.to_string());
+        self
+    }
+
+    pub fn none(mut self) -> Self {
+        self.8 = Some(BackgroundGradient::None);
+        self
+    }
+
+    pub fn gradient(mut self, direction: BackgroundGradient, gradient: Gradient) -> Self {
+        self.8 = Some(direction);
+        self.9 = Some(gradient);
+        self
+    }
+
+    pub fn position(mut self, position: BackgroundPosition) -> Self {
+        self.10 = Some(position);
+        self
+    }
+
+    pub fn scroll(mut self, attachment: BackgroundScrollAttachment) -> Self {
+        self.2 = Some(attachment);
+        self
+    }
+
+    pub fn clip(mut self, clip: BackgroundClip) -> Self {
+        self.3 = Some(clip);
+        self
+    }
+
+    pub fn origin(mut self, origin: BackgroundOrigin) -> Self {
+        self.4 = Some(origin);
+        self
+    }
+
+    pub fn repeat(mut self, repeat: BackgroundRepeat) -> Self {
+        self.5 = Some(repeat);
+        self
+    }
+
+    pub fn size(mut self, size: BackgroundSize) -> Self {
+        self.6 = Some(size);
+        self
+    }
+}
 
 impl Render for BackgroundWidget {
     fn render(&self) -> Markup {
@@ -24,7 +106,49 @@ impl UIWidget for BackgroundWidget {
     }
 
     fn base_class(&self) -> Vec<String> {
-        vec![format!("bg-{}", self.1.color_class())]
+        let mut ret = Vec::new();
+
+        if let Some(color) = &self.1 {
+            ret.push(format!("bg-{}", color.color_class()));
+        }
+
+        if let Some(attachment) = &self.2 {
+            ret.push(attachment.to_value().to_string());
+        }
+
+        if let Some(clip) = &self.3 {
+            ret.push(clip.to_value().to_string());
+        }
+
+        if let Some(origin) = &self.4 {
+            ret.push(origin.to_value().to_string());
+        }
+
+        if let Some(repeat) = &self.5 {
+            ret.push(repeat.to_value().to_string());
+        }
+
+        if let Some(size) = &self.6 {
+            ret.push(size.to_value().to_string());
+        }
+
+        if let Some(image) = &self.7 {
+            ret.push(format!("bg-[url('{image}')]"));
+        }
+
+        if let Some(gradient) = &self.8 {
+            ret.push(gradient.to_value().to_string());
+        }
+
+        if let Some(gradient) = &self.9 {
+            ret.extend_from_slice(&gradient.color_class());
+        }
+
+        if let Some(position) = &self.10 {
+            ret.push(position.to_value().to_string());
+        }
+
+        ret
     }
 
     fn extended_class(&self) -> Vec<String> {
@@ -47,3 +171,151 @@ impl UIWidget for BackgroundWidget {
         }
     }
 }
+
+/// Controlling how a background image behaves when scrolling.
+pub enum BackgroundScrollAttachment {
+    /// Fix the background image relative to the viewport.
+    Fixed,
+    /// Scroll the background image with the container and the viewport.
+    Local,
+    /// Scroll the background image with the viewport, but not with the container.
+    Scroll,
+}
+
+impl BackgroundScrollAttachment {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundScrollAttachment::Fixed => "bg-fixed",
+            BackgroundScrollAttachment::Local => "bg-local",
+            BackgroundScrollAttachment::Scroll => "bg-scroll",
+        }
+    }
+}
+
+pub enum BackgroundClip {
+    Border,
+    Padding,
+    Content,
+    Text,
+}
+
+impl BackgroundClip {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundClip::Border => "bg-clip-border",
+            BackgroundClip::Padding => "bg-clip-padding",
+            BackgroundClip::Content => "bg-clip-content",
+            BackgroundClip::Text => "bg-clip-text",
+        }
+    }
+}
+
+pub enum BackgroundOrigin {
+    Border,
+    Padding,
+    Content,
+}
+
+impl BackgroundOrigin {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundOrigin::Border => "bg-origin-border",
+            BackgroundOrigin::Padding => "bg-origin-padding",
+            BackgroundOrigin::Content => "bg-origin-content",
+        }
+    }
+}
+
+pub enum BackgroundRepeat {
+    Repeat,
+    NoRepeat,
+    RepeatX,
+    RepeatY,
+    Round,
+    Space,
+}
+
+impl BackgroundRepeat {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundRepeat::Repeat => "bg-repeat",
+            BackgroundRepeat::NoRepeat => "bg-no-repeat",
+            BackgroundRepeat::RepeatX => "bg-repeat-x",
+            BackgroundRepeat::RepeatY => "bg-repeat-y",
+            BackgroundRepeat::Round => "bg-repeat-round",
+            BackgroundRepeat::Space => "bg-repeat-space",
+        }
+    }
+}
+
+pub enum BackgroundSize {
+    Auto,
+    Cover,
+    Contain,
+}
+
+impl BackgroundSize {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundSize::Auto => "bg-auto",
+            BackgroundSize::Cover => "bg-cover",
+            BackgroundSize::Contain => "bg-contain",
+        }
+    }
+}
+
+pub enum BackgroundGradient {
+    None,
+    ToTop,
+    ToTopRight,
+    ToRight,
+    ToBottomRight,
+    ToBottom,
+    ToBottomLeft,
+    ToLeft,
+    ToTopLeft,
+}
+
+impl BackgroundGradient {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundGradient::None => "bg-none",
+            BackgroundGradient::ToTop => "bg-gradient-to-t",
+            BackgroundGradient::ToTopRight => "bg-gradient-to-tr",
+            BackgroundGradient::ToRight => "bg-gradient-to-r",
+            BackgroundGradient::ToBottomRight => "bg-gradient-to-br",
+            BackgroundGradient::ToBottom => "bg-gradient-to-b",
+            BackgroundGradient::ToBottomLeft => "bg-gradient-to-bl",
+            BackgroundGradient::ToLeft => "bg-gradient-to-l",
+            BackgroundGradient::ToTopLeft => "bg-gradient-to-tl",
+        }
+    }
+}
+
+pub enum BackgroundPosition {
+    Bottom,
+    Center,
+    Left,
+    LeftBottom,
+    LeftTop,
+    Right,
+    RightBottom,
+    RightTop,
+    Top,
+}
+
+impl BackgroundPosition {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BackgroundPosition::Bottom => "bg-bottom",
+            BackgroundPosition::Center => "bg-center",
+            BackgroundPosition::Left => "bg-left",
+            BackgroundPosition::LeftBottom => "bg-left-bottom",
+            BackgroundPosition::LeftTop => "bg-left-top",
+            BackgroundPosition::Right => "bg-right",
+            BackgroundPosition::RightBottom => "bg-right-bottom",
+            BackgroundPosition::RightTop => "bg-right-top",
+            BackgroundPosition::Top => "bg-top",
+        }
+    }
+}

From e98242addf2055c2bd48265e696a96c3d8864a76 Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 22:18:21 +0100
Subject: [PATCH 07/11] update modifiers

---
 src/ui/wrapper/mod.rs | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/src/ui/wrapper/mod.rs b/src/ui/wrapper/mod.rs
index 4fac146..06654a3 100644
--- a/src/ui/wrapper/mod.rs
+++ b/src/ui/wrapper/mod.rs
@@ -74,6 +74,29 @@ macro_rules! wrapper {
 }
 
 wrapper!(Hover, HoverWrapper, "hover");
+wrapper!(DarkMode, DarkModeWrapper, "dark");
+wrapper!(Active, ActiveWrapper, "active");
+wrapper!(Focus, FocusWrapper, "focus");
+wrapper!(First, FirstWrapper, "first");
+wrapper!(Odd, OddWrapper, "odd");
+wrapper!(Even, EvenWrapper, "even");
+
+wrapper!(Required, RequiredWrapper, "required");
+wrapper!(Invalid, InvalidWrapper, "invalid");
+wrapper!(Disabled, DisabledWrapper, "disabled");
+wrapper!(Placeholder, PlaceholderWrapper, "placeholder");
+wrapper!(FileButton, FileButtonWrapper, "file");
+wrapper!(Marker, MarkerWrapper, "marker");
+wrapper!(Selection, SelectionWrapper, "selection");
+wrapper!(FirstLine, FirstLineWrapper, "first-line");
+wrapper!(FirstLetter, FirstLetterWrapper, "first-letter");
+
+wrapper!(Portrait, PortraitWrapper, "portrait");
+wrapper!(Landscape, LandscapeWrapper, "landscape");
+wrapper!(Print, PrintWrapper, "print");
+wrapper!(LeftToRight, LeftToRightWrapper, "ltr");
+wrapper!(RightToLeft, RightToLeftWrapper, "rtl");
+wrapper!(Opened, OpenWrapper, "open");
 
 wrapper!(SmallScreen, SmallScreenWrapper, "sm");
 wrapper!(MediumScreen, MediumScreenWrapper, "md");

From b752d77815a4ac44343e2c12aa1553164b81d0e9 Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Mon, 20 Jan 2025 23:09:36 +0100
Subject: [PATCH 08/11] add position

---
 src/ui/mod.rs                 |   1 +
 src/ui/primitives/mod.rs      |   4 +-
 src/ui/primitives/position.rs | 210 ++++++++++++++++++++++++++++++++++
 3 files changed, 213 insertions(+), 2 deletions(-)
 create mode 100644 src/ui/primitives/position.rs

diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index bb8a803..3e37b3b 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -41,6 +41,7 @@ pub mod prelude {
     pub use super::primitives::link::Link;
     pub use super::primitives::margin::Margin;
     pub use super::primitives::padding::Padding;
+    pub use super::primitives::position::{Position, PositionKind};
     pub use super::primitives::rounded::Rounded;
     pub use super::primitives::script;
     pub use super::primitives::shadow::Shadow;
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index 1ddc144..d0acf64 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -1,6 +1,5 @@
-use maud::{PreEscaped, html};
-
 use super::UIWidget;
+use maud::{PreEscaped, html};
 
 pub mod animation;
 pub mod aspect;
@@ -18,6 +17,7 @@ pub mod input;
 pub mod link;
 pub mod margin;
 pub mod padding;
+pub mod position;
 pub mod rounded;
 pub mod shadow;
 pub mod sized;
diff --git a/src/ui/primitives/position.rs b/src/ui/primitives/position.rs
new file mode 100644
index 0000000..75531ce
--- /dev/null
+++ b/src/ui/primitives/position.rs
@@ -0,0 +1,210 @@
+use maud::{Markup, Render, html};
+
+use crate::ui::UIWidget;
+
+#[allow(non_snake_case)]
+pub fn Position<T: UIWidget + 'static>(kind: PositionKind, inner: T) -> Positioned {
+    Positioned {
+        inner: Box::new(inner),
+        kind,
+        inset: None,
+        inset_x: None,
+        inset_y: None,
+        start: None,
+        end: None,
+        top: None,
+        right: None,
+        bottom: None,
+        left: None,
+    }
+}
+
+pub struct Positioned {
+    inner: Box<dyn UIWidget>,
+    kind: PositionKind,
+    inset: Option<i64>,
+    inset_x: Option<i64>,
+    inset_y: Option<i64>,
+    start: Option<i64>,
+    end: Option<i64>,
+    top: Option<i64>,
+    right: Option<i64>,
+    bottom: Option<i64>,
+    left: Option<i64>,
+}
+
+impl Positioned {
+    pub fn inset(mut self, value: i64) -> Self {
+        self.inset = Some(value);
+        self
+    }
+    pub fn inset_x(mut self, value: i64) -> Self {
+        self.inset_x = Some(value);
+        self
+    }
+
+    pub fn inset_y(mut self, value: i64) -> Self {
+        self.inset_y = Some(value);
+        self
+    }
+
+    pub fn start(mut self, value: i64) -> Self {
+        self.start = Some(value);
+        self
+    }
+
+    pub fn end(mut self, value: i64) -> Self {
+        self.end = Some(value);
+        self
+    }
+
+    pub fn top(mut self, value: i64) -> Self {
+        self.top = Some(value);
+        self
+    }
+
+    pub fn right(mut self, value: i64) -> Self {
+        self.right = Some(value);
+        self
+    }
+
+    pub fn bottom(mut self, value: i64) -> Self {
+        self.bottom = Some(value);
+        self
+    }
+
+    pub fn left(mut self, value: i64) -> Self {
+        self.left = Some(value);
+        self
+    }
+}
+
+impl Render for Positioned {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for Positioned {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let mut ret = vec![self.kind.to_value().to_string()];
+
+        if let Some(inset) = &self.inset {
+            if inset.is_negative() {
+                ret.push(format!("-inset-[{inset}px]"));
+            } else {
+                ret.push(format!("inset-[{inset}px]"));
+            }
+        }
+
+        if let Some(inset) = &self.inset_x {
+            if inset.is_negative() {
+                ret.push(format!("-inset-x-[{inset}px]"));
+            } else {
+                ret.push(format!("inset-x-[{inset}px]"));
+            }
+        }
+
+        if let Some(inset) = &self.inset_y {
+            if inset.is_negative() {
+                ret.push(format!("-inset-y-[{inset}px]"));
+            } else {
+                ret.push(format!("inset-y-[{inset}px]"));
+            }
+        }
+
+        if let Some(start) = &self.start {
+            if start.is_negative() {
+                ret.push(format!("-start-[{start}px]"));
+            } else {
+                ret.push(format!("start-[{start}px]"));
+            }
+        }
+
+        if let Some(end) = &self.end {
+            if end.is_negative() {
+                ret.push(format!("-end-[{end}px]"));
+            } else {
+                ret.push(format!("end-[{end}px]"));
+            }
+        }
+
+        if let Some(value) = &self.top {
+            if value.is_negative() {
+                ret.push(format!("-top-[{value}px]"));
+            } else {
+                ret.push(format!("top-[{value}px]"));
+            }
+        }
+
+        if let Some(value) = &self.right {
+            if value.is_negative() {
+                ret.push(format!("-right-[{value}px]"));
+            } else {
+                ret.push(format!("right-[{value}px]"));
+            }
+        }
+
+        if let Some(value) = &self.bottom {
+            if value.is_negative() {
+                ret.push(format!("-bottom-[{value}px]"));
+            } else {
+                ret.push(format!("bottom-[{value}px]"));
+            }
+        }
+
+        if let Some(value) = &self.left {
+            if value.is_negative() {
+                ret.push(format!("-left-[{value}px]"));
+            } else {
+                ret.push(format!("left-[{value}px]"));
+            }
+        }
+
+        ret
+    }
+
+    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() {
+            self.inner
+                .as_ref()
+                .render_with_class(&format!("{} {class}", self.base_class().join(" ")))
+        } else {
+            html! {
+                div class=(format!("{} {class}", self.base_class().join(" "))) {
+                    (self.inner.as_ref())
+                }
+            }
+        }
+    }
+}
+
+pub enum PositionKind {
+    Static,
+    Fixed,
+    Absolute,
+    Relative,
+    Sticky,
+}
+
+impl PositionKind {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            PositionKind::Static => "static",
+            PositionKind::Fixed => "fixed",
+            PositionKind::Absolute => "absolute",
+            PositionKind::Relative => "relative",
+            PositionKind::Sticky => "sticky",
+        }
+    }
+}

From 95ceaa8231ea89b648880cc71cdb6dc5f6d47623 Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Tue, 21 Jan 2025 00:52:29 +0100
Subject: [PATCH 09/11] update

---
 src/ui/color.rs               |   2 +-
 src/ui/mod.rs                 |  14 ++--
 src/ui/primitives/cursor.rs   |  73 +++++++++++++++++++
 src/ui/primitives/mod.rs      |  45 +++++++++++-
 src/ui/primitives/position.rs | 131 ++++++++++++++++++++++++++++++++++
 src/ui/primitives/text.rs     |  84 +++++++++++++++++++++-
 6 files changed, 340 insertions(+), 9 deletions(-)

diff --git a/src/ui/color.rs b/src/ui/color.rs
index 4671712..e7baa05 100644
--- a/src/ui/color.rs
+++ b/src/ui/color.rs
@@ -12,7 +12,7 @@ pub trait ColorCircle {
     fn next(&self) -> Self;
 }
 
-// todo : specific colors rgb
+// todo : specific colors rgb -[#50d71e]
 
 macro_rules! color_map {
     ($name:ident, $id:literal) => {
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 3e37b3b..af4fedf 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -17,6 +17,7 @@ pub mod components;
 pub mod prelude {
     pub use super::color::*;
     pub use super::primitives::Context;
+    pub use super::primitives::NoBrowserAppearance;
     pub use super::primitives::Nothing;
     pub use super::primitives::Side;
     pub use super::primitives::Size;
@@ -27,7 +28,7 @@ pub mod prelude {
         Border, BorderSide, BorderSize, BorderStyle, Outline, OutlineStyle, Ring,
     };
     pub use super::primitives::container::Container;
-    pub use super::primitives::cursor::Cursor;
+    pub use super::primitives::cursor::{Action, Cursor, TouchAction};
     pub use super::primitives::div::Div;
     pub use super::primitives::filter::{
         Blur, Brightness, Contrast, Grayscale, HueRotate, Invert, Saturate, Sepia,
@@ -41,7 +42,7 @@ pub mod prelude {
     pub use super::primitives::link::Link;
     pub use super::primitives::margin::Margin;
     pub use super::primitives::padding::Padding;
-    pub use super::primitives::position::{Position, PositionKind};
+    pub use super::primitives::position::{Position, PositionKind, Resize, Resizeable};
     pub use super::primitives::rounded::Rounded;
     pub use super::primitives::script;
     pub use super::primitives::shadow::Shadow;
@@ -49,10 +50,11 @@ pub mod prelude {
     pub use super::primitives::space::{ScreenValue, SpaceBetween};
     pub use super::primitives::svg::SVG;
     pub use super::primitives::text::{
-        Code, DecorationKind, DecorationStyle, DecorationThickness, LetterSpacing, LineClamp,
-        LineHeight, ListStyle, NumberStyle, Paragraph, Span, Text, TextAlignment, TextContent,
-        TextDecoration, TextHyphens, TextOverflow, TextTransform, TextWhitespace, TextWordBreak,
-        TextWrap, UnderlineOffset, VerticalTextAlignment,
+        AccentColor, Code, DecorationKind, DecorationStyle, DecorationThickness, LetterSpacing,
+        LineClamp, LineHeight, ListStyle, NumberStyle, Paragraph, Span, Text, TextAlignment,
+        TextContent, TextCursorColor, TextDecoration, TextHyphens, TextOverflow, TextSelection,
+        TextTransform, TextWhitespace, TextWordBreak, TextWrap, UnderlineOffset,
+        VerticalTextAlignment,
     };
     pub use super::primitives::transform::{
         RenderTransformCPU, RenderTransformGPU, Rotate, Scale, Skew, Transform, TransformOrigin,
diff --git a/src/ui/primitives/cursor.rs b/src/ui/primitives/cursor.rs
index 1ab521e..d6d5141 100644
--- a/src/ui/primitives/cursor.rs
+++ b/src/ui/primitives/cursor.rs
@@ -122,3 +122,76 @@ impl UIWidget for CursorWidget {
         }
     }
 }
+
+#[allow(non_snake_case)]
+pub fn TouchAction<T: UIWidget + 'static>(action: Action, inner: T) -> TouchActionWidget {
+    TouchActionWidget(Box::new(inner), action)
+}
+
+pub struct TouchActionWidget(Box<dyn UIWidget>, Action);
+
+impl Render for TouchActionWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for TouchActionWidget {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![self.1.to_value().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 {
+        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())
+                }
+            }
+        }
+    }
+}
+
+pub enum Action {
+    Auto,
+    None,
+    PanX,
+    PanLeft,
+    PanRight,
+    PanY,
+    PanUp,
+    PanDown,
+    PinchZoom,
+    Manipulation,
+}
+
+impl Action {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            Action::Auto => "touch-auto",
+            Action::None => "touch-none",
+            Action::PanX => "touch-pan-x",
+            Action::PanLeft => "touch-pan-left",
+            Action::PanRight => "touch-pan-right",
+            Action::PanY => "touch-pan-y",
+            Action::PanUp => "touch-pan-up",
+            Action::PanDown => "touch-pan-down",
+            Action::PinchZoom => "touch-pinch-zoom",
+            Action::Manipulation => "touch-manipulation",
+        }
+    }
+}
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index d0acf64..89bdd20 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -1,5 +1,5 @@
 use super::UIWidget;
-use maud::{PreEscaped, html};
+use maud::{Markup, PreEscaped, Render, html};
 
 pub mod animation;
 pub mod aspect;
@@ -135,3 +135,46 @@ impl Side {
         }
     }
 }
+
+#[allow(non_snake_case)]
+pub fn NoBrowserAppearance<T: UIWidget + 'static>(inner: T) -> NoBrowserAppearanceWidget {
+    NoBrowserAppearanceWidget(Box::new(inner))
+}
+
+pub struct NoBrowserAppearanceWidget(Box<dyn UIWidget>);
+
+impl Render for NoBrowserAppearanceWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for NoBrowserAppearanceWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec!["appearance-none".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 {
+        if self.0.as_ref().can_inherit() {
+            self.0
+                .as_ref()
+                .render_with_class(&format!("appearance-none {class}"))
+        } else {
+            html! {
+                div class=(format!("appearance-none {class}")) {
+                    (self.0.as_ref())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/primitives/position.rs b/src/ui/primitives/position.rs
index 75531ce..2019981 100644
--- a/src/ui/primitives/position.rs
+++ b/src/ui/primitives/position.rs
@@ -2,6 +2,8 @@ use maud::{Markup, Render, html};
 
 use crate::ui::UIWidget;
 
+use super::Side;
+
 #[allow(non_snake_case)]
 pub fn Position<T: UIWidget + 'static>(kind: PositionKind, inner: T) -> Positioned {
     Positioned {
@@ -208,3 +210,132 @@ impl PositionKind {
         }
     }
 }
+
+#[allow(non_snake_case)]
+pub fn ObjectPosition<T: UIWidget + 'static>(side: Side, inner: T) -> ObjectPositioned {
+    ObjectPositioned {
+        inner: Box::new(inner),
+        side,
+    }
+}
+
+pub struct ObjectPositioned {
+    inner: Box<dyn UIWidget>,
+    side: Side,
+}
+
+impl Render for ObjectPositioned {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for ObjectPositioned {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![
+            match self.side {
+                Side::Start => "object-top",
+                Side::End => "object-bottom",
+                Side::Top => "object-top",
+                Side::Right => "object-right",
+                Side::Bottom => "object-bottom",
+                Side::Left => "object-left",
+                Side::StartStart => "object-left-top",
+                Side::StartEnd => "object-right-top",
+                Side::EndEnd => "object-right-bottom",
+                Side::EndStart => "object-left-bottom",
+                Side::TopLeft => "object-left-top",
+                Side::TopRight => "object-right-top",
+                Side::BottomRight => "object-right-bottom",
+                Side::BottomLeft => "object-left-bottom",
+                Side::Center => "object-center",
+            }
+            .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() {
+            self.inner
+                .as_ref()
+                .render_with_class(&format!("{} {class}", self.base_class().join(" ")))
+        } else {
+            html! {
+                div class=(format!("{} {class}", self.base_class().join(" "))) {
+                    (self.inner.as_ref())
+                }
+            }
+        }
+    }
+}
+
+pub enum Resize {
+    None,
+    Y,
+    X,
+    Both,
+}
+
+impl Resize {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            Resize::None => "resize-none",
+            Resize::Y => "resize-y",
+            Resize::X => "resize-x",
+            Resize::Both => "resize",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn Resizeable<T: UIWidget + 'static>(mode: Resize, inner: T) -> ResizeableWidget {
+    ResizeableWidget(Box::new(inner), mode)
+}
+
+pub struct ResizeableWidget(Box<dyn UIWidget>, Resize);
+
+impl Render for ResizeableWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for ResizeableWidget {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![self.1.to_value().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 {
+        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())
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/primitives/text.rs b/src/ui/primitives/text.rs
index 5230a1f..43befe7 100644
--- a/src/ui/primitives/text.rs
+++ b/src/ui/primitives/text.rs
@@ -1,5 +1,5 @@
 use crate::ui::{UIWidget, color::UIColor};
-use maud::{Markup, PreEscaped, Render};
+use maud::{Markup, PreEscaped, Render, html};
 
 use super::{Nothing, space::ScreenValue};
 
@@ -31,6 +31,7 @@ pub fn Text(txt: &str) -> TextWidget {
         align: None,
         vert_align: None,
         list_style: None,
+        select: None,
         kind: TextKind::Paragraph,
     }
 }
@@ -68,6 +69,7 @@ pub fn Paragraph<T: UIWidget + 'static>(inner: T) -> TextWidget {
         list_style: None,
         clamp: None,
         align: None,
+        select: None,
         kind: TextKind::Paragraph,
     }
 }
@@ -100,6 +102,7 @@ pub fn Span(txt: &str) -> TextWidget {
         clamp: None,
         align: None,
         pseudo: None,
+        select: None,
         kind: TextKind::Span,
     }
 }
@@ -132,6 +135,7 @@ pub fn Code(txt: &str) -> TextWidget {
         clamp: None,
         align: None,
         pseudo: None,
+        select: None,
         kind: TextKind::Pre,
     }
 }
@@ -160,10 +164,17 @@ pub struct TextWidget {
     pseudo: Option<TextContent>,
     align: Option<TextAlignment>,
     clamp: Option<LineClamp>,
+    select: Option<TextSelection>,
     title: Option<String>,
 }
 
 impl TextWidget {
+    #[must_use]
+    pub fn select(mut self, select: TextSelection) -> Self {
+        self.select = Some(select);
+        self
+    }
+
     #[must_use]
     pub fn whitespace(mut self, whitespace: TextWhitespace) -> Self {
         self.whitespace = Some(whitespace);
@@ -527,6 +538,7 @@ impl UIWidget for TextWidget {
         add_option!(list_style, ret);
         add_option!(pseudo, ret);
         add_option!(line_height, ret);
+        add_option!(select, ret);
 
         if let Some(decoration) = &self.decoration {
             ret.extend_from_slice(&decoration.base_class());
@@ -1061,3 +1073,73 @@ impl TextHyphens {
         }
     }
 }
+
+pub enum TextSelection {
+    None,
+    Text,
+    All,
+    Auto,
+}
+
+impl TextSelection {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            TextSelection::None => "select-none",
+            TextSelection::Text => "select-text",
+            TextSelection::All => "select-all",
+            TextSelection::Auto => "select-auto	",
+        }
+    }
+}
+
+macro_rules! color_widget {
+    ($constr:ident, $widget:ident, $class:literal) => {
+        #[allow(non_snake_case)]
+        pub fn $constr<T: UIWidget + 'static, C: UIColor + 'static>(color: C, inner: T) -> $widget {
+            $widget(Box::new(inner), Box::new(color))
+        }
+
+        pub struct $widget(Box<dyn UIWidget>, Box<dyn UIColor>);
+
+        impl Render for $widget {
+            fn render(&self) -> Markup {
+                self.render_with_class("")
+            }
+        }
+
+        impl UIWidget for $widget {
+            fn can_inherit(&self) -> bool {
+                false
+            }
+
+            fn base_class(&self) -> Vec<String> {
+                let mut class = $class.to_string();
+                class.push_str(&format!("-{}", self.1.color_class()));
+                vec![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 {
+                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())
+                        }
+                    }
+                }
+            }
+        }
+    };
+}
+
+color_widget!(TextCursorColor, CaretColorWidget, "caret");
+color_widget!(AccentColor, AccentColorWidget, "accent");

From a9a8b8b951d9556b9bcd22bc5002b0d6c9dbb6a0 Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Tue, 21 Jan 2025 09:00:45 +0100
Subject: [PATCH 10/11] update

---
 examples/ui.rs               |   4 +-
 src/ui/mod.rs                |   5 +
 src/ui/primitives/display.rs | 283 +++++++++++++++++++++++++++++++++++
 src/ui/primitives/mod.rs     |   2 +
 src/ui/primitives/scroll.rs  | 234 +++++++++++++++++++++++++++++
 5 files changed, 526 insertions(+), 2 deletions(-)
 create mode 100644 src/ui/primitives/display.rs
 create mode 100644 src/ui/primitives/scroll.rs

diff --git a/examples/ui.rs b/examples/ui.rs
index b2256f7..e2f92d3 100644
--- a/examples/ui.rs
+++ b/examples/ui.rs
@@ -15,8 +15,8 @@ pub async fn index_page(ctx: RequestContext) -> StringResponse {
         h1 { "Hello World!" };
 
         (
-            Screen::medium(Hover(Background(Red::_700, Nothing()))).on(
-                Background(Blue::_700, Text("HELLO!"))
+            Screen::medium(Hover(Background(Nothing()).color(Red::_700))).on(
+                Background(Text("HELLO!")).color(Blue::_700)
             )
         )
 
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index af4fedf..d158a54 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -29,6 +29,10 @@ pub mod prelude {
     };
     pub use super::primitives::container::Container;
     pub use super::primitives::cursor::{Action, Cursor, TouchAction};
+    pub use super::primitives::display::{
+        BoxDecorationBreak, BoxSizing, BreakAfter, BreakBefore, BreakInside, BreakInsideValue,
+        BreakValue, Clear, Display, Float, ObjectFit, Overflow,
+    };
     pub use super::primitives::div::Div;
     pub use super::primitives::filter::{
         Blur, Brightness, Contrast, Grayscale, HueRotate, Invert, Saturate, Sepia,
@@ -45,6 +49,7 @@ pub mod prelude {
     pub use super::primitives::position::{Position, PositionKind, Resize, Resizeable};
     pub use super::primitives::rounded::Rounded;
     pub use super::primitives::script;
+    pub use super::primitives::scroll::{Overscroll, Scroll, SnapAlign, SnapType};
     pub use super::primitives::shadow::Shadow;
     pub use super::primitives::sized::Sized;
     pub use super::primitives::space::{ScreenValue, SpaceBetween};
diff --git a/src/ui/primitives/display.rs b/src/ui/primitives/display.rs
new file mode 100644
index 0000000..14fddee
--- /dev/null
+++ b/src/ui/primitives/display.rs
@@ -0,0 +1,283 @@
+use crate::ui::UIWidget;
+use maud::{Markup, Render, html};
+
+macro_rules! string_class_widget {
+    ($name:ident) => {
+        pub struct $name(Box<dyn UIWidget>, String);
+
+        impl Render for $name {
+            fn render(&self) -> Markup {
+                self.render_with_class("")
+            }
+        }
+
+        impl UIWidget for $name {
+            fn can_inherit(&self) -> bool {
+                true
+            }
+
+            fn base_class(&self) -> Vec<String> {
+                vec![self.1.clone()]
+            }
+
+            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())
+                        }
+                    }
+                }
+            }
+        }
+    };
+}
+
+macro_rules! constructor {
+    ($name:ident, $class:literal) => {
+        #[allow(non_snake_case)]
+        pub fn $name<T: UIWidget + 'static>(inner: T) -> Self {
+            Self(Box::new(inner), $class.to_string())
+        }
+    };
+}
+
+pub enum BreakValue {
+    Auto,
+    Avoid,
+    All,
+    AvoidPage,
+    Page,
+    Left,
+    Right,
+    Column,
+}
+
+impl BreakValue {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BreakValue::Auto => "auto",
+            BreakValue::Avoid => "avoid",
+            BreakValue::All => "all",
+            BreakValue::AvoidPage => "break-page",
+            BreakValue::Page => "page",
+            BreakValue::Left => "left",
+            BreakValue::Right => "right",
+            BreakValue::Column => "column",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn BreakAfter<T: UIWidget + 'static>(value: BreakValue, inner: T) -> BreakWidget {
+    BreakWidget(Box::new(inner), false, value)
+}
+
+#[allow(non_snake_case)]
+pub fn BreakBefore<T: UIWidget + 'static>(value: BreakValue, inner: T) -> BreakWidget {
+    BreakWidget(Box::new(inner), true, value)
+}
+
+pub struct BreakWidget(Box<dyn UIWidget>, bool, BreakValue);
+
+impl Render for BreakWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for BreakWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        if self.1 {
+            vec![format!("break-before-{}", self.2.to_value())]
+        } else {
+            vec![format!("break-after-{}", self.2.to_value())]
+        }
+    }
+
+    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())
+                }
+            }
+        }
+    }
+}
+
+pub enum BreakInsideValue {
+    Auto,
+    Avoid,
+    AvoidPage,
+    AvoidColumn,
+}
+
+impl BreakInsideValue {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            BreakInsideValue::Auto => "break-inside-auto",
+            BreakInsideValue::Avoid => "break-inside-avoid",
+            BreakInsideValue::AvoidPage => "break-inside-avoid-page",
+            BreakInsideValue::AvoidColumn => "break-inside-avoid-column",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn BreakInside<T: UIWidget + 'static>(value: BreakValue, inner: T) -> BreakWidget {
+    BreakWidget(Box::new(inner), true, value)
+}
+
+pub struct BreakInsideWidget(Box<dyn UIWidget>, BreakValue);
+
+impl Render for BreakInsideWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for BreakInsideWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![self.1.to_value().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 {
+        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())
+                }
+            }
+        }
+    }
+}
+
+string_class_widget!(BoxDecorationBreak);
+
+impl BoxDecorationBreak {
+    constructor!(Clone, "box-decoration-clone");
+    constructor!(Slice, "box-decoration-slice");
+}
+
+string_class_widget!(BoxSizing);
+
+impl BoxSizing {
+    constructor!(Border, "box-border");
+    constructor!(Content, "box-content");
+}
+
+string_class_widget!(Display);
+
+impl Display {
+    constructor!(Block, "block");
+    constructor!(InlineBlock, "inline-block");
+    constructor!(Inline, "inline");
+    constructor!(Flex, "flex");
+    constructor!(InlineFlex, "inline-flex");
+    constructor!(Table, "table");
+    constructor!(InlineTable, "inline-table");
+    constructor!(TableCaption, "table-caption");
+    constructor!(TableCell, "table-cell");
+    constructor!(TableColumn, "table-column");
+    constructor!(TableColumnGroup, "table-column-group");
+    constructor!(TableFooterGroup, "table-footer-group");
+    constructor!(TableHeaderGroup, "table-header-group");
+    constructor!(TableRowGroup, "table-row-group");
+    constructor!(TableRow, "table-row");
+    constructor!(FlowRoot, "flow-root");
+    constructor!(Grid, "grid");
+    constructor!(InlineGrid, "inline-grid");
+    constructor!(Contents, "contents");
+    constructor!(ListItem, "list-item");
+    constructor!(Hidden, "hidden");
+}
+
+string_class_widget!(Float);
+
+impl Float {
+    constructor!(Start, "float-start");
+    constructor!(End, "float-end");
+    constructor!(Left, "float-left");
+    constructor!(Right, "float-right");
+    constructor!(None, "float-none");
+}
+
+string_class_widget!(Clear);
+
+impl Clear {
+    constructor!(Start, "clear-start");
+    constructor!(End, "clear-end");
+    constructor!(Left, "clear-left");
+    constructor!(Right, "clear-right");
+    constructor!(Both, "clear-both");
+    constructor!(None, "clear-none");
+}
+
+string_class_widget!(ObjectFit);
+
+impl ObjectFit {
+    constructor!(Contain, "object-contain");
+    constructor!(Cover, "object-cover");
+    constructor!(Fill, "object-fill");
+    constructor!(None, "object-none");
+    constructor!(ScaleDown, "object-scale-down");
+}
+
+string_class_widget!(Overflow);
+
+impl Overflow {
+    constructor!(Auto, "overflow-auto");
+    constructor!(Hidden, "overflow-hidden");
+    constructor!(Clip, "overflow-clip");
+    constructor!(Visible, "overflow-visible");
+    constructor!(Scroll, "overflow-scroll");
+    constructor!(XAuto, "overflow-x-auto");
+    constructor!(YAuto, "overflow-y-auto");
+    constructor!(XHidden, "overflow-x-hidden");
+    constructor!(YHidden, "overflow-y-hidden");
+    constructor!(XClip, "overflow-x-clip");
+    constructor!(YClip, "overflow-y-clip");
+    constructor!(XVisible, "overflow-x-visible");
+    constructor!(YVisible, "overflow-y-visible");
+    constructor!(XScroll, "overflow-x-scroll");
+    constructor!(YScroll, "overflow-y-scroll");
+}
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index 89bdd20..3234ef4 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -7,6 +7,7 @@ pub mod background;
 pub mod border;
 pub mod container;
 pub mod cursor;
+pub mod display;
 pub mod div;
 pub mod filter;
 pub mod flex;
@@ -19,6 +20,7 @@ pub mod margin;
 pub mod padding;
 pub mod position;
 pub mod rounded;
+pub mod scroll;
 pub mod shadow;
 pub mod sized;
 pub mod space;
diff --git a/src/ui/primitives/scroll.rs b/src/ui/primitives/scroll.rs
new file mode 100644
index 0000000..720ffe2
--- /dev/null
+++ b/src/ui/primitives/scroll.rs
@@ -0,0 +1,234 @@
+use super::{margin::Margin, padding::PaddingWidget};
+use crate::ui::UIWidget;
+use maud::{Markup, Render, html};
+
+#[allow(non_snake_case)]
+pub fn Scroll<T: UIWidget + 'static>(inner: T) -> ScrollWidget {
+    ScrollWidget(Box::new(inner), true, None, None, None, None, false)
+}
+
+pub struct ScrollWidget(
+    Box<dyn UIWidget>,
+    bool,
+    Option<Margin>,
+    Option<PaddingWidget>,
+    Option<Overscroll>,
+    Option<SnapType>,
+    bool,
+);
+
+impl ScrollWidget {
+    pub fn smooth(mut self, value: bool) -> Self {
+        self.1 = value;
+        self
+    }
+
+    pub fn scroll_margin(mut self, margin: Margin) -> Self {
+        self.2 = Some(margin);
+        self
+    }
+
+    pub fn scroll_padding(mut self, padding: PaddingWidget) -> Self {
+        self.3 = Some(padding);
+        self
+    }
+
+    pub fn overscroll(mut self, behaviour: Overscroll) -> Self {
+        self.4 = Some(behaviour);
+        self
+    }
+
+    pub fn snap(mut self, kind: SnapType) -> Self {
+        self.5 = Some(kind);
+        self
+    }
+
+    pub fn skip_snap(mut self) -> Self {
+        self.6 = true;
+        self
+    }
+}
+
+impl Render for ScrollWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for ScrollWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let mut ret = Vec::new();
+
+        if self.1 {
+            ret.push("scroll-smooth".to_string());
+        }
+
+        if let Some(margin) = &self.2 {
+            let classes = margin
+                .base_class()
+                .into_iter()
+                .map(|x| format!("scroll-{x}"))
+                .collect::<Vec<_>>();
+            ret.extend_from_slice(&classes);
+        }
+
+        if let Some(padding) = &self.3 {
+            let classes = padding
+                .base_class()
+                .into_iter()
+                .map(|x| format!("scroll-{x}"))
+                .collect::<Vec<_>>();
+            ret.extend_from_slice(&classes);
+        }
+
+        if let Some(overscroll) = &self.4 {
+            ret.push(overscroll.to_value().to_string());
+        }
+
+        if let Some(snap) = &self.5 {
+            ret.push(snap.to_value().to_string());
+        }
+
+        if self.6 {
+            ret.push("snap-normal".to_string());
+        } else {
+            ret.push("snap-always".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())
+                }
+            }
+        }
+    }
+}
+
+pub enum Overscroll {
+    Auto,
+    Contain,
+    None,
+    YAuto,
+    YContain,
+    YNone,
+    XAuto,
+    XContain,
+    XNone,
+}
+
+impl Overscroll {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            Overscroll::Auto => "overscroll-auto",
+            Overscroll::Contain => "overscroll-contain",
+            Overscroll::None => "overscroll-none",
+            Overscroll::YAuto => "overscroll-y-auto",
+            Overscroll::YContain => "overscroll-y-contain",
+            Overscroll::YNone => "overscroll-y-none",
+            Overscroll::XAuto => "overscroll-x-auto",
+            Overscroll::XContain => "overscroll-x-contain",
+            Overscroll::XNone => "overscroll-x-none",
+        }
+    }
+}
+
+pub enum SnapType {
+    None,
+    X,
+    Y,
+    Both,
+    Mandatory,
+    Proximity,
+}
+
+impl SnapType {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            SnapType::None => "snap-none",
+            SnapType::X => "snap-x",
+            SnapType::Y => "snap-y",
+            SnapType::Both => "snap-both",
+            SnapType::Mandatory => "snap-mandatory",
+            SnapType::Proximity => "snap-proximity",
+        }
+    }
+}
+
+pub struct SnapAlign(Box<dyn UIWidget>, String);
+
+impl SnapAlign {
+    #[allow(non_snake_case)]
+    pub fn Start<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "snap-start".to_string())
+    }
+
+    #[allow(non_snake_case)]
+    pub fn End<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "snap-end".to_string())
+    }
+
+    #[allow(non_snake_case)]
+    pub fn Center<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "snap-center".to_string())
+    }
+
+    #[allow(non_snake_case)]
+    pub fn None<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), "snap-align-none".to_string())
+    }
+}
+
+impl Render for SnapAlign {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for SnapAlign {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![self.1.clone()]
+    }
+
+    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())
+                }
+            }
+        }
+    }
+}

From e02def6bc16dfe61937816d669514c9d4300ae1a Mon Sep 17 00:00:00 2001
From: JMARyA <jmarya@hydrar.de>
Date: Tue, 21 Jan 2025 13:40:56 +0100
Subject: [PATCH 11/11] update

---
 src/ui/mod.rs                |  23 +-
 src/ui/primitives/display.rs |  31 +++
 src/ui/primitives/flex.rs    |  84 +++++++
 src/ui/primitives/grid.rs    | 461 +++++++++++++++++++++++++++++++++++
 src/ui/primitives/list.rs    | 103 ++++++++
 src/ui/primitives/mod.rs     |   3 +
 src/ui/primitives/table.rs   | 204 ++++++++++++++++
 7 files changed, 897 insertions(+), 12 deletions(-)
 create mode 100644 src/ui/primitives/grid.rs
 create mode 100644 src/ui/primitives/list.rs
 create mode 100644 src/ui/primitives/table.rs

diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index d158a54..ab1717d 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,6 +1,5 @@
 use components::Shell;
-use maud::{Markup, PreEscaped, Render};
-use prelude::Text;
+use maud::{Markup, PreEscaped, Render, html};
 
 // UI
 
@@ -154,37 +153,37 @@ impl UIWidget for PreEscaped<String> {
 
 impl UIWidget for String {
     fn can_inherit(&self) -> bool {
-        Text(&self).can_inherit()
+        false
     }
 
     fn base_class(&self) -> Vec<String> {
-        Text(&self).base_class()
+        Vec::new()
     }
 
     fn extended_class(&self) -> Vec<String> {
-        Text(&self).extended_class()
+        Vec::new()
     }
 
-    fn render_with_class(&self, class: &str) -> Markup {
-        Text(&self).render_with_class(class)
+    fn render_with_class(&self, _: &str) -> Markup {
+        html!((self))
     }
 }
 
 impl UIWidget for &str {
     fn can_inherit(&self) -> bool {
-        Text(&self).can_inherit()
+        false
     }
 
     fn base_class(&self) -> Vec<String> {
-        Text(&self).base_class()
+        Vec::new()
     }
 
     fn extended_class(&self) -> Vec<String> {
-        Text(&self).extended_class()
+        Vec::new()
     }
 
-    fn render_with_class(&self, class: &str) -> Markup {
-        Text(&self).render_with_class(class)
+    fn render_with_class(&self, _: &str) -> Markup {
+        html!((self))
     }
 }
 
diff --git a/src/ui/primitives/display.rs b/src/ui/primitives/display.rs
index 14fddee..287bd59 100644
--- a/src/ui/primitives/display.rs
+++ b/src/ui/primitives/display.rs
@@ -281,3 +281,34 @@ impl Overflow {
     constructor!(XScroll, "overflow-x-scroll");
     constructor!(YScroll, "overflow-y-scroll");
 }
+
+string_class_widget!(JustifySelf);
+
+impl JustifySelf {
+    constructor!(Auto, "justify-self-auto");
+    constructor!(Start, "justify-self-start");
+    constructor!(End, "justify-self-end");
+    constructor!(Center, "justify-self-center");
+    constructor!(Stretch, "justify-self-stretch");
+}
+
+string_class_widget!(PlaceSelf);
+
+impl PlaceSelf {
+    constructor!(Auto, "place-self-auto");
+    constructor!(Start, "place-self-start");
+    constructor!(End, "place-self-end");
+    constructor!(Center, "place-self-center");
+    constructor!(Stretch, "place-self-stretch");
+}
+
+string_class_widget!(AlignSelf);
+
+impl AlignSelf {
+    constructor!(Auto, "self-auto");
+    constructor!(Start, "self-start");
+    constructor!(End, "self-end");
+    constructor!(Center, "self-center");
+    constructor!(Stretch, "self-stretch");
+    constructor!(Baseline, "self-baseline");
+}
diff --git a/src/ui/primitives/flex.rs b/src/ui/primitives/flex.rs
index 2c9013a..35b231e 100644
--- a/src/ui/primitives/flex.rs
+++ b/src/ui/primitives/flex.rs
@@ -109,6 +109,24 @@ impl FlexWidget {
         self
     }
 
+    #[must_use]
+    pub fn justify_items(mut self, justify: JustifyItems) -> Self {
+        self.1.push(justify.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn align_content(mut self, align: AlignContent) -> Self {
+        self.1.push(align.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn align_items(mut self, align: AlignItems) -> Self {
+        self.1.push(align.to_value().to_string());
+        self
+    }
+
     #[must_use]
     pub fn justify(mut self, value: Justify) -> Self {
         let class = match value {
@@ -500,3 +518,69 @@ impl DivideStyle {
         }
     }
 }
+
+pub enum JustifyItems {
+    Start,
+    End,
+    Center,
+    Stretch,
+}
+
+impl JustifyItems {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            JustifyItems::Start => "justify-items-start",
+            JustifyItems::End => "justify-items-end",
+            JustifyItems::Center => "justify-items-center",
+            JustifyItems::Stretch => "justify-items-stretch",
+        }
+    }
+}
+
+pub enum AlignContent {
+    Normal,
+    Center,
+    Start,
+    End,
+    Between,
+    Around,
+    Evenly,
+    Baseline,
+    Stretch,
+}
+
+impl AlignContent {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            AlignContent::Normal => "content-normal",
+            AlignContent::Center => "content-center",
+            AlignContent::Start => "content-start",
+            AlignContent::End => "content-end",
+            AlignContent::Between => "content-between",
+            AlignContent::Around => "content-around",
+            AlignContent::Evenly => "content-evenly",
+            AlignContent::Baseline => "content-baseline",
+            AlignContent::Stretch => "content-stretch",
+        }
+    }
+}
+
+pub enum AlignItems {
+    Start,
+    End,
+    Center,
+    Baseline,
+    Stretch,
+}
+
+impl AlignItems {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            AlignItems::Start => "items-start",
+            AlignItems::End => "items-end",
+            AlignItems::Center => "items-center",
+            AlignItems::Baseline => "items-baseline",
+            AlignItems::Stretch => "items-stretch",
+        }
+    }
+}
diff --git a/src/ui/primitives/grid.rs b/src/ui/primitives/grid.rs
new file mode 100644
index 0000000..cd1e345
--- /dev/null
+++ b/src/ui/primitives/grid.rs
@@ -0,0 +1,461 @@
+use crate::ui::{UIWidget, color::UIColor};
+use maud::{Markup, Render, html};
+
+use super::{
+    flex::{AlignContent, AlignItems, DivideStyle, DivideWidth, Justify, JustifyItems},
+    space::ScreenValue,
+};
+
+#[allow(non_snake_case)]
+pub fn Grid<T: UIWidget + 'static>(inner: T) -> GridWidget {
+    GridWidget(Box::new(inner), vec![], false)
+}
+
+pub struct GridWidget(Box<dyn UIWidget>, Vec<String>, bool);
+
+impl Render for GridWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+pub enum GridAmount {
+    _1,
+    _2,
+    _3,
+    _4,
+    _5,
+    _6,
+    _7,
+    _8,
+    _9,
+    _10,
+    _11,
+    _12,
+    None,
+    Subgrid,
+}
+
+impl GridAmount {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            GridAmount::_1 => "1",
+            GridAmount::_2 => "2",
+            GridAmount::_3 => "3",
+            GridAmount::_4 => "4",
+            GridAmount::_5 => "5",
+            GridAmount::_6 => "6",
+            GridAmount::_7 => "7",
+            GridAmount::_8 => "8",
+            GridAmount::_9 => "9",
+            GridAmount::_10 => "10",
+            GridAmount::_11 => "11",
+            GridAmount::_12 => "12",
+            GridAmount::None => "none",
+            GridAmount::Subgrid => "subgrid",
+        }
+    }
+}
+
+impl GridWidget {
+    #[must_use]
+    pub fn columns(mut self, amount: GridAmount) -> Self {
+        self.1.push(format!("grid-cols-{}", amount.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn rows(mut self, amount: GridAmount) -> Self {
+        self.1.push(format!("grid-rows-{}", amount.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn auto_flow(mut self, flow: GridAutoFlow) -> Self {
+        self.1.push(flow.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn auto_columns(mut self, size: GridAutoSize) -> Self {
+        self.1.push(format!("auto-cols-{}", size.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn auto_rows(mut self, size: GridAutoSize) -> Self {
+        self.1.push(format!("auto-rows-{}", size.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn full_center(mut self) -> Self {
+        self.1.push("items-center".to_owned());
+        self.1.push("justify-center".to_owned());
+        self
+    }
+
+    #[must_use]
+    pub const fn group(mut self) -> Self {
+        self.2 = true;
+        self
+    }
+
+    #[must_use]
+    pub fn divide_style(mut self, style: DivideStyle) -> Self {
+        self.1.push(style.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn divide_color<C: UIColor + 'static>(mut self, color: C) -> Self {
+        self.1.push(format!("divide-{}", color.color_class()));
+        self
+    }
+
+    #[must_use]
+    pub fn divide_x(mut self, width: DivideWidth) -> Self {
+        self.1.push(format!("divide-x-{}", width.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn divide_y(mut self, width: DivideWidth) -> Self {
+        self.1.push(format!("divide-y-{}", width.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn justify_items(mut self, justify: JustifyItems) -> Self {
+        self.1.push(justify.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn align_content(mut self, align: AlignContent) -> Self {
+        self.1.push(align.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn align_items(mut self, align: AlignItems) -> Self {
+        self.1.push(align.to_value().to_string());
+        self
+    }
+
+    #[must_use]
+    pub fn justify(mut self, value: Justify) -> Self {
+        let class = match value {
+            Justify::Center => "justify-center".to_string(),
+            Justify::Between => "justify-between".to_string(),
+            Justify::Normal => "justify-normal".to_string(),
+            Justify::Start => "justify-start".to_string(),
+            Justify::End => "justify-end".to_string(),
+            Justify::Around => "justify-around".to_string(),
+            Justify::Evenly => "justify-evenly".to_string(),
+            Justify::Stretch => "justify-stretch".to_string(),
+        };
+
+        self.1.push(class);
+        self
+    }
+
+    #[must_use]
+    pub fn items_center(mut self) -> Self {
+        self.1.push("items-center".to_owned());
+        self
+    }
+
+    #[must_use]
+    pub fn gap(mut self, amount: ScreenValue) -> Self {
+        self.1.push(format!("gap-{}", amount.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn gap_x(mut self, amount: ScreenValue) -> Self {
+        self.1.push(format!("gap-x-{}", amount.to_value()));
+        self
+    }
+
+    #[must_use]
+    pub fn gap_y(mut self, amount: ScreenValue) -> Self {
+        self.1.push(format!("gap-y-{}", amount.to_value()));
+        self
+    }
+}
+
+impl UIWidget for GridWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let mut res = vec!["grid".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 {
+        if self.0.as_ref().can_inherit() && !self.2 {
+            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())
+                }
+            }
+        }
+    }
+}
+
+pub enum GridAutoFlow {
+    Row,
+    Column,
+    Dense,
+    RowDense,
+    ColumnDense,
+}
+
+impl GridAutoFlow {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            GridAutoFlow::Row => "grid-flow-row",
+            GridAutoFlow::Column => "grid-flow-col",
+            GridAutoFlow::Dense => "grid-flow-dense",
+            GridAutoFlow::RowDense => "grid-flow-row-dense",
+            GridAutoFlow::ColumnDense => "grid-flow-col-dense",
+        }
+    }
+}
+
+pub enum GridAutoSize {
+    Auto,
+    Min,
+    Max,
+    Fr,
+}
+
+impl GridAutoSize {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            GridAutoSize::Auto => "auto",
+            GridAutoSize::Min => "min",
+            GridAutoSize::Max => "max",
+            GridAutoSize::Fr => "fr",
+        }
+    }
+}
+
+#[allow(non_snake_case)]
+pub fn GridElementColumn<T: UIWidget + 'static>(inner: T) -> GridElement {
+    GridElement(Box::new(inner), Vec::new(), "col".to_string())
+}
+
+#[allow(non_snake_case)]
+pub fn GridElementRow<T: UIWidget + 'static>(inner: T) -> GridElement {
+    GridElement(Box::new(inner), Vec::new(), "row".to_string())
+}
+
+pub struct GridElement(Box<dyn UIWidget>, Vec<String>, String);
+
+impl GridElement {
+    pub fn auto(mut self) -> Self {
+        self.1.push(format!("{}-auto", self.2));
+        self
+    }
+
+    pub fn span(mut self, value: GridElementValue) -> Self {
+        self.1.push(format!("{}-span-{}", self.2, match value {
+            GridElementValue::_1 => "1",
+            GridElementValue::_2 => "2",
+            GridElementValue::_3 => "3",
+            GridElementValue::_4 => "4",
+            GridElementValue::_5 => "5",
+            GridElementValue::_6 => "6",
+            GridElementValue::_7 => "7",
+            GridElementValue::_8 => "8",
+            GridElementValue::_9 => "9",
+            GridElementValue::_10 => "10",
+            GridElementValue::_11 => "11",
+            GridElementValue::_12 => "12",
+            GridElementValue::Auto => "full",
+        }));
+        self
+    }
+
+    pub fn start(mut self, value: GridElementValue) -> Self {
+        self.1
+            .push(format!("{}-start-{}", self.2, value.to_value()));
+        self
+    }
+
+    pub fn end(mut self, value: GridElementValue) -> Self {
+        self.1.push(format!("{}-end-{}", self.2, value.to_value()));
+        self
+    }
+}
+
+impl Render for GridElement {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for GridElement {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        let mut res = vec!["grid".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 {
+        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())
+                }
+            }
+        }
+    }
+}
+
+pub enum GridElementValue {
+    _1,
+    _2,
+    _3,
+    _4,
+    _5,
+    _6,
+    _7,
+    _8,
+    _9,
+    _10,
+    _11,
+    _12,
+    Auto,
+}
+
+impl GridElementValue {
+    pub const fn to_value(&self) -> &str {
+        match self {
+            GridElementValue::_1 => "1",
+            GridElementValue::_2 => "2",
+            GridElementValue::_3 => "3",
+            GridElementValue::_4 => "4",
+            GridElementValue::_5 => "5",
+            GridElementValue::_6 => "6",
+            GridElementValue::_7 => "7",
+            GridElementValue::_8 => "8",
+            GridElementValue::_9 => "9",
+            GridElementValue::_10 => "10",
+            GridElementValue::_11 => "11",
+            GridElementValue::_12 => "12",
+            GridElementValue::Auto => "auto",
+        }
+    }
+}
+
+macro_rules! string_class_widget {
+    ($name:ident) => {
+        pub struct $name(Box<dyn UIWidget>, String);
+
+        impl Render for $name {
+            fn render(&self) -> Markup {
+                self.render_with_class("")
+            }
+        }
+
+        impl UIWidget for $name {
+            fn can_inherit(&self) -> bool {
+                true
+            }
+
+            fn base_class(&self) -> Vec<String> {
+                vec![self.1.clone()]
+            }
+
+            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())
+                        }
+                    }
+                }
+            }
+        }
+    };
+}
+
+macro_rules! constructor {
+    ($name:ident, $class:literal) => {
+        #[allow(non_snake_case)]
+        pub fn $name<T: UIWidget + 'static>(inner: T) -> Self {
+            Self(Box::new(inner), $class.to_string())
+        }
+    };
+}
+
+string_class_widget!(Columns);
+
+impl Columns {
+    constructor!(_1, "columns-1");
+    constructor!(_2, "columns-2");
+    constructor!(_3, "columns-3");
+    constructor!(_4, "columns-4");
+    constructor!(_5, "columns-5");
+    constructor!(_6, "columns-6");
+    constructor!(_7, "columns-7");
+    constructor!(_8, "columns-8");
+    constructor!(_9, "columns-9");
+    constructor!(_10, "columns-10");
+    constructor!(_11, "columns-11");
+    constructor!(_12, "columns-12");
+    constructor!(Auto, "columns-auto");
+    constructor!(_3XS, "columns-3xs");
+    constructor!(_2XS, "columns-2xs");
+    constructor!(XS, "columns-xs");
+    constructor!(Small, "columns-sm");
+    constructor!(Medium, "columns-md");
+    constructor!(Large, "columns-lg");
+    constructor!(XL, "columns-xl");
+    constructor!(_2XL, "columns-2xl");
+    constructor!(_3XL, "columns-3xl");
+    constructor!(_4XL, "columns-4xl");
+    constructor!(_5XL, "columns-5xl");
+    constructor!(_6XL, "columns-6xl");
+    constructor!(_7XL, "columns-7xl");
+}
diff --git a/src/ui/primitives/list.rs b/src/ui/primitives/list.rs
new file mode 100644
index 0000000..630a766
--- /dev/null
+++ b/src/ui/primitives/list.rs
@@ -0,0 +1,103 @@
+use crate::ui::UIWidget;
+use maud::{Markup, Render, html};
+
+#[allow(non_snake_case)]
+#[must_use]
+pub fn OrderedList() -> ListWidget {
+    ListWidget(Vec::new(), true)
+}
+
+#[allow(non_snake_case)]
+#[must_use]
+pub fn UnorderedList() -> ListWidget {
+    ListWidget(Vec::new(), false)
+}
+
+pub struct ListWidget(Vec<Box<dyn UIWidget>>, bool);
+
+impl ListWidget {
+    #[must_use]
+    pub fn push<T: UIWidget + 'static>(mut self, element: T) -> Self {
+        self.0.push(Box::new(element));
+        self
+    }
+
+    #[must_use]
+    pub fn push_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
+    }
+
+    #[must_use]
+    pub fn push_if<T: UIWidget + 'static, U: Fn() -> T>(
+        mut self,
+        condition: bool,
+        then: U,
+    ) -> Self {
+        if condition {
+            self.0.push(Box::new(then()));
+        }
+        self
+    }
+
+    #[must_use]
+    pub fn push_for_each<T, X, F>(mut self, items: &[X], mut action: F) -> Self
+    where
+        T: UIWidget + 'static,
+        F: FnMut(&X) -> T,
+    {
+        for item in items {
+            self.0.push(Box::new(action(item)));
+        }
+
+        self
+    }
+}
+
+impl Render for ListWidget {
+    fn render(&self) -> Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for ListWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        vec![]
+    }
+
+    fn extended_class(&self) -> Vec<String> {
+        vec![]
+    }
+
+    fn render_with_class(&self, class: &str) -> Markup {
+        let inner = html! {
+            @for e in &self.0 {
+                li { (e.as_ref()) };
+            }
+        };
+
+        if self.1 {
+            html! {
+                ol class=(class) {
+                    (inner);
+                }
+            }
+        } else {
+            html! {
+                ul class=(class) {
+                    (inner);
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/primitives/mod.rs b/src/ui/primitives/mod.rs
index 3234ef4..d2d21f5 100644
--- a/src/ui/primitives/mod.rs
+++ b/src/ui/primitives/mod.rs
@@ -11,11 +11,13 @@ pub mod display;
 pub mod div;
 pub mod filter;
 pub mod flex;
+pub mod grid;
 pub mod header;
 pub mod height;
 pub mod image;
 pub mod input;
 pub mod link;
+pub mod list;
 pub mod margin;
 pub mod padding;
 pub mod position;
@@ -25,6 +27,7 @@ pub mod shadow;
 pub mod sized;
 pub mod space;
 pub mod svg;
+pub mod table;
 pub mod text;
 pub mod transform;
 pub mod visibility;
diff --git a/src/ui/primitives/table.rs b/src/ui/primitives/table.rs
new file mode 100644
index 0000000..60bf8a6
--- /dev/null
+++ b/src/ui/primitives/table.rs
@@ -0,0 +1,204 @@
+use maud::{Markup, Render, html};
+
+use crate::ui::UIWidget;
+
+use super::{div::Div, space::ScreenValue};
+
+#[allow(non_snake_case)]
+pub fn Table<T: UIWidget + 'static + Clone>(inner: Vec<Vec<T>>) -> TableWidget {
+    let inner = Div().vanish().push_for_each(&inner, |row| {
+        TableRow(
+            Div()
+                .vanish()
+                .push_for_each(&row, |col| TableData(col.clone())),
+        )
+    });
+
+    TableWidget(Box::new(inner), Vec::new(), None, None)
+}
+
+pub struct TableWidget(
+    Box<dyn UIWidget>,
+    Vec<String>,
+    Option<Box<dyn UIWidget>>,
+    Option<Caption>,
+);
+
+impl TableWidget {
+    pub fn header<T: UIWidget + 'static>(mut self, header: T) -> Self {
+        self.2 = Some(Box::new(header));
+        self
+    }
+
+    pub fn caption(mut self, caption: Caption) -> Self {
+        self.3 = Some(caption);
+        self
+    }
+
+    pub fn border_collapse(mut self) -> Self {
+        self.1.push("border-collapse".to_string());
+        self
+    }
+
+    pub fn border_seperate(mut self) -> Self {
+        self.1.push("border-separate".to_string());
+        self
+    }
+
+    pub fn border_spacing(mut self, spacing: ScreenValue) -> Self {
+        self.1
+            .push(format!("border-spacing-{}", spacing.to_value()));
+        self
+    }
+
+    pub fn border_spacing_x(mut self, spacing: ScreenValue) -> Self {
+        self.1
+            .push(format!("border-spacing-x-{}", spacing.to_value()));
+        self
+    }
+
+    pub fn border_spacing_y(mut self, spacing: ScreenValue) -> Self {
+        self.1
+            .push(format!("border-spacing-y-{}", spacing.to_value()));
+        self
+    }
+
+    pub fn layout_fixed(mut self) -> Self {
+        self.1.push("table-fixed".to_string());
+        self
+    }
+
+    pub fn layout_auto(mut self) -> Self {
+        self.1.push("table-auto".to_string());
+        self
+    }
+}
+
+impl Render for TableWidget {
+    fn render(&self) -> maud::Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for TableWidget {
+    fn can_inherit(&self) -> bool {
+        true
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        self.1.clone()
+    }
+
+    fn extended_class(&self) -> Vec<String> {
+        self.base_class()
+    }
+
+    fn render_with_class(&self, class: &str) -> maud::Markup {
+        html! {
+            table class=(format!("{} {class}", self.base_class().join(" "))) {
+                @if let Some(caption) = &self.3 {
+                    (caption)
+                }
+
+                @if let Some(header) = &self.2 {
+                    thead {
+                        (header)
+                    };
+                };
+
+                (self.0.as_ref())
+            };
+        }
+    }
+}
+
+pub struct Caption(Box<dyn UIWidget>, bool);
+
+impl Caption {
+    #[allow(non_snake_case)]
+    pub fn Top<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), true)
+    }
+
+    #[allow(non_snake_case)]
+    pub fn Bottom<T: UIWidget + 'static>(inner: T) -> Self {
+        Self(Box::new(inner), false)
+    }
+}
+
+impl Render for Caption {
+    fn render(&self) -> maud::Markup {
+        self.render_with_class("")
+    }
+}
+
+impl UIWidget for Caption {
+    fn can_inherit(&self) -> bool {
+        false
+    }
+
+    fn base_class(&self) -> Vec<String> {
+        if self.1 {
+            vec!["caption-top".to_string()]
+        } else {
+            vec!["caption-bottom".to_string()]
+        }
+    }
+
+    fn extended_class(&self) -> Vec<String> {
+        self.base_class()
+    }
+
+    fn render_with_class(&self, _: &str) -> maud::Markup {
+        html! {
+            caption class=(self.base_class().join(" ")) {
+                (self.0.as_ref())
+            };
+        }
+    }
+}
+
+macro_rules! element_widget {
+    ($name:ident, $widget:ident, $element:ident) => {
+        #[allow(non_snake_case)]
+        pub fn $name<T: UIWidget + 'static>(inner: T) -> $widget {
+            $widget(Box::new(inner))
+        }
+
+        pub struct $widget(Box<dyn UIWidget>);
+
+        impl Render for $widget {
+            fn render(&self) -> Markup {
+                self.render_with_class("")
+            }
+        }
+
+        impl UIWidget for $widget {
+            fn can_inherit(&self) -> bool {
+                true
+            }
+
+            fn base_class(&self) -> Vec<String> {
+                Vec::new()
+            }
+
+            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 {
+                html! {
+                        $element class=(class) {
+                            (self.0.as_ref())
+                        }
+                }
+            }
+        }
+    };
+}
+
+element_widget!(TableRow, TableRowWidget, tr);
+element_widget!(TableHead, TableHeadWidget, th);
+element_widget!(TableData, TableDataWidget, td);