diff --git a/.gitignore b/.gitignore
index e0e82dec..d311e0cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,3 @@
**/log/*.log*
**/local.*.toml
**/local.toml
-workdir
diff --git a/Cargo.lock b/Cargo.lock
index 27c4a241..ad273146 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1560,7 +1560,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pagetop"
-version = "0.0.57"
+version = "0.0.56"
dependencies = [
"actix-files",
"actix-session",
@@ -1590,7 +1590,7 @@ dependencies = [
[[package]]
name = "pagetop-build"
-version = "0.0.12"
+version = "0.0.11"
dependencies = [
"grass",
"static-files",
@@ -1598,7 +1598,7 @@ dependencies = [
[[package]]
name = "pagetop-macros"
-version = "0.0.14"
+version = "0.0.13"
dependencies = [
"proc-macro-crate",
"proc-macro-error",
diff --git a/Cargo.toml b/Cargo.toml
index fa090e35..8c5912b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pagetop"
-version = "0.0.57"
+version = "0.0.56"
edition = "2021"
description = """\
diff --git a/README.md b/README.md
index 117da733..1dba1342 100644
--- a/README.md
+++ b/README.md
@@ -30,20 +30,6 @@ requirements and application scenarios through actions, components, packages, an
# ⚡️ Quick start
-The simplest PageTop application looks like this:
-
-```rust
-use pagetop::prelude::*;
-
-#[pagetop::main]
-async fn main() -> std::io::Result<()> {
- Application::new().run()?.await
-}
-```
-
-This provides a default homepage at `http://localhost:8088` using the default configuration. To
-customize the service, you can define a PageTop package like this:
-
```rust
use pagetop::prelude::*;
@@ -67,8 +53,8 @@ async fn main() -> std::io::Result<()> {
}
```
-This program defines a custom `HelloWorld` package to serve a page at the root path (`/`) displaying
-a "Hello World!" message inside an HTML `
` element.
+This program features a `HelloWorld` package, providing a service that serves a greeting web page
+accessible via `http://localhost:8088` under default settings.
# 📂 Helpers
diff --git a/build.rs b/build.rs
index 85e02e02..8151d84c 100644
--- a/build.rs
+++ b/build.rs
@@ -1,7 +1,7 @@
use pagetop_build::StaticFilesBundle;
fn main() -> std::io::Result<()> {
- StaticFilesBundle::from_dir("./static", None)
+ StaticFilesBundle::from_dir("./static/assets", None)
.with_name("assets")
.build()
}
diff --git a/config/common.toml b/config/common.toml
index d6b30e57..900872d6 100644
--- a/config/common.toml
+++ b/config/common.toml
@@ -1,6 +1,5 @@
[app]
name = "Samples"
-#language = "es-ES"
[log]
tracing = "Debug"
diff --git a/config/predefined-settings.toml b/docs/predefined-settings.toml
similarity index 100%
rename from config/predefined-settings.toml
rename to docs/predefined-settings.toml
diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml
index 94324cec..3ec289c1 100644
--- a/helpers/pagetop-build/Cargo.toml
+++ b/helpers/pagetop-build/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pagetop-build"
-version = "0.0.12"
+version = "0.0.11"
edition = "2021"
description = """\
diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml
index 978c8316..6289eb51 100644
--- a/helpers/pagetop-macros/Cargo.toml
+++ b/helpers/pagetop-macros/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pagetop-macros"
-version = "0.0.14"
+version = "0.0.13"
edition = "2021"
description = """\
diff --git a/src/base/action/page.rs b/src/base/action/page.rs
index 9c89e2d1..fdbff4ac 100644
--- a/src/base/action/page.rs
+++ b/src/base/action/page.rs
@@ -1,5 +1,5 @@
-mod before_render_body;
-pub use before_render_body::*;
+mod before_prepare_body;
+pub use before_prepare_body::*;
-mod after_render_body;
-pub use after_render_body::*;
+mod after_prepare_body;
+pub use after_prepare_body::*;
diff --git a/src/base/action/page/after_render_body.rs b/src/base/action/page/after_prepare_body.rs
similarity index 65%
rename from src/base/action/page/after_render_body.rs
rename to src/base/action/page/after_prepare_body.rs
index 81a89d87..9eb2f397 100644
--- a/src/base/action/page/after_render_body.rs
+++ b/src/base/action/page/after_prepare_body.rs
@@ -1,21 +1,21 @@
use crate::prelude::*;
-pub type FnAfterRenderBody = fn(page: &mut Page);
+pub type FnAfterPrepareBody = fn(page: &mut Page);
-pub struct AfterRenderBody {
- f: FnAfterRenderBody,
+pub struct AfterPrepareBody {
+ f: FnAfterPrepareBody,
weight: Weight,
}
-impl ActionTrait for AfterRenderBody {
+impl ActionTrait for AfterPrepareBody {
fn weight(&self) -> Weight {
self.weight
}
}
-impl AfterRenderBody {
- pub fn new(f: FnAfterRenderBody) -> Self {
- AfterRenderBody { f, weight: 0 }
+impl AfterPrepareBody {
+ pub fn new(f: FnAfterPrepareBody) -> Self {
+ AfterPrepareBody { f, weight: 0 }
}
pub fn with_weight(mut self, value: Weight) -> Self {
diff --git a/src/base/action/page/before_render_body.rs b/src/base/action/page/before_prepare_body.rs
similarity index 64%
rename from src/base/action/page/before_render_body.rs
rename to src/base/action/page/before_prepare_body.rs
index e0a9d770..c1ea5beb 100644
--- a/src/base/action/page/before_render_body.rs
+++ b/src/base/action/page/before_prepare_body.rs
@@ -1,21 +1,21 @@
use crate::prelude::*;
-pub type FnBeforeRenderBody = fn(page: &mut Page);
+pub type FnBeforePrepareBody = fn(page: &mut Page);
-pub struct BeforeRenderBody {
- f: FnBeforeRenderBody,
+pub struct BeforePrepareBody {
+ f: FnBeforePrepareBody,
weight: Weight,
}
-impl ActionTrait for BeforeRenderBody {
+impl ActionTrait for BeforePrepareBody {
fn weight(&self) -> Weight {
self.weight
}
}
-impl BeforeRenderBody {
- pub fn new(f: FnBeforeRenderBody) -> Self {
- BeforeRenderBody { f, weight: 0 }
+impl BeforePrepareBody {
+ pub fn new(f: FnBeforePrepareBody) -> Self {
+ BeforePrepareBody { f, weight: 0 }
}
pub fn with_weight(mut self, value: Weight) -> Self {
diff --git a/src/base/component.rs b/src/base/component.rs
index 93b22fa1..14bfd84d 100644
--- a/src/base/component.rs
+++ b/src/base/component.rs
@@ -1,11 +1,201 @@
-mod html;
-pub use html::Html;
+use crate::core::component::{AssetsOp, Context};
+use crate::html::{JavaScript, StyleSheet};
+use crate::{AutoDefault, Weight};
-mod fluent;
-pub use fluent::Fluent;
+use std::fmt;
+
+// Context parameters.
+pub const PARAM_BASE_WEIGHT: &str = "base.weight";
+pub const PARAM_BASE_INCLUDE_ICONS: &str = "base.include.icon";
+pub const PARAM_BASE_INCLUDE_FLEX_ASSETS: &str = "base.include.flex";
+pub const PARAM_BASE_INCLUDE_MENU_ASSETS: &str = "base.include.menu";
+
+pub(crate) fn add_base_assets(cx: &mut Context) {
+ let weight = cx.get_param::(PARAM_BASE_WEIGHT).unwrap_or(-90);
+
+ cx.set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/root.css")
+ .with_version("0.0.1")
+ .with_weight(weight),
+ ))
+ .set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/looks.css")
+ .with_version("0.0.2")
+ .with_weight(weight),
+ ))
+ .set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/buttons.css")
+ .with_version("0.0.2")
+ .with_weight(weight),
+ ));
+
+ if let Ok(true) = cx.get_param::(PARAM_BASE_INCLUDE_ICONS) {
+ cx.set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/icons.min.css")
+ .with_version("1.11.1")
+ .with_weight(weight),
+ ));
+ }
+
+ if let Ok(true) = cx.get_param::(PARAM_BASE_INCLUDE_FLEX_ASSETS) {
+ cx.set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/flex.css")
+ .with_version("0.0.1")
+ .with_weight(weight),
+ ));
+ }
+
+ if let Ok(true) = cx.get_param::(PARAM_BASE_INCLUDE_MENU_ASSETS) {
+ cx.set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/menu.css")
+ .with_version("0.0.1")
+ .with_weight(weight),
+ ))
+ .set_assets(AssetsOp::AddJavaScript(
+ JavaScript::defer("/base/js/menu.js")
+ .with_version("0.0.1")
+ .with_weight(weight),
+ ));
+ }
+}
+
+// *************************************************************************************************
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub enum BreakPoint {
+ #[default]
+ None, // Does not apply. Rest initially assume 1 pixel = 0.0625rem
+ SM, // @media screen and [ (max-width: 35.5rem) <= 568px < (min-width: 35.5625rem) ]
+ MD, // @media screen and [ (max-width: 48rem) <= 768px < (min-width: 48.0625rem) ]
+ LG, // @media screen and [ (max-width: 62rem) <= 992px < (min-width: 62.0625rem) ]
+ XL, // @media screen and [ (max-width: 80rem) <= 1280px < (min-width: 80.0625rem) ]
+ X2L, // @media screen and [ (max-width: 90rem) <= 1440px < (min-width: 90.0625rem) ]
+ X3L, // @media screen and [ (max-width: 120rem) <= 1920px < (min-width: 120.0625rem) ]
+ X2K, // @media screen and [ (max-width: 160rem) <= 2560px < (min-width: 160.0625rem) ]
+}
+
+#[rustfmt::skip]
+impl fmt::Display for BreakPoint {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ BreakPoint::None => write!(f, "bp__none"),
+ BreakPoint::SM => write!(f, "bp__sm"),
+ BreakPoint::MD => write!(f, "bp__md"),
+ BreakPoint::LG => write!(f, "bp__lg"),
+ BreakPoint::XL => write!(f, "bp__xl"),
+ BreakPoint::X2L => write!(f, "bp__x2l"),
+ BreakPoint::X3L => write!(f, "bp__x3l"),
+ BreakPoint::X2K => write!(f, "bp__x2k"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum StyleBase {
+ #[default]
+ Default,
+ Info,
+ Success,
+ Warning,
+ Danger,
+ Light,
+ Dark,
+ Link,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for StyleBase {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ StyleBase::Default => write!(f, "style__default"),
+ StyleBase::Info => write!(f, "style__info"),
+ StyleBase::Success => write!(f, "style__success"),
+ StyleBase::Warning => write!(f, "style__warning"),
+ StyleBase::Danger => write!(f, "style__danger"),
+ StyleBase::Light => write!(f, "style__light"),
+ StyleBase::Dark => write!(f, "style__dark"),
+ StyleBase::Link => write!(f, "style__link"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum FontSize {
+ ExtraLarge,
+ XxLarge,
+ XLarge,
+ Large,
+ Medium,
+ #[default]
+ Normal,
+ Small,
+ XSmall,
+ XxSmall,
+ ExtraSmall,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for FontSize {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ FontSize::ExtraLarge => write!(f, "fs__x3l"),
+ FontSize::XxLarge => write!(f, "fs__x2l"),
+ FontSize::XLarge => write!(f, "fs__xl"),
+ FontSize::Large => write!(f, "fs__l"),
+ FontSize::Medium => write!(f, "fs__m"),
+ FontSize::Normal => write!(f, ""),
+ FontSize::Small => write!(f, "fs__s"),
+ FontSize::XSmall => write!(f, "fs__xs"),
+ FontSize::XxSmall => write!(f, "fs__x2s"),
+ FontSize::ExtraSmall => write!(f, "fs__x3s"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+pub mod flex;
+
+mod basic;
+pub use basic::*;
mod error403;
pub use error403::Error403;
mod error404;
pub use error404::Error404;
+
+mod heading;
+pub use heading::{Heading, HeadingSize, HeadingType};
+
+mod paragraph;
+pub use paragraph::Paragraph;
+
+mod icon;
+pub use icon::Icon;
+
+mod button;
+pub use button::{Button, ButtonTarget};
+
+mod image;
+pub use image::{Image, ImageSize};
+
+mod block;
+pub use block::Block;
+
+mod branding;
+pub use branding::Branding;
+
+mod powered_by;
+pub use powered_by::{PoweredBy, PoweredByLogo};
+
+pub mod menu;
+pub use menu::Menu;
+
+pub mod form;
+pub use form::{Form, FormMethod};
diff --git a/src/base/component/basic.rs b/src/base/component/basic.rs
new file mode 100644
index 00000000..51ce6c9d
--- /dev/null
+++ b/src/base/component/basic.rs
@@ -0,0 +1,5 @@
+mod html;
+pub use html::Html;
+
+mod fluent;
+pub use fluent::Fluent;
diff --git a/src/base/component/fluent.rs b/src/base/component/basic/fluent.rs
similarity index 100%
rename from src/base/component/fluent.rs
rename to src/base/component/basic/fluent.rs
diff --git a/src/base/component/html.rs b/src/base/component/basic/html.rs
similarity index 100%
rename from src/base/component/html.rs
rename to src/base/component/basic/html.rs
diff --git a/src/base/component/block.rs b/src/base/component/block.rs
new file mode 100644
index 00000000..59d22ed8
--- /dev/null
+++ b/src/base/component/block.rs
@@ -0,0 +1,95 @@
+use crate::prelude::*;
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Block {
+ id : OptionId,
+ classes: OptionClasses,
+ style : StyleBase,
+ title : OptionTranslated,
+ mixed : MixedComponents,
+}
+
+impl ComponentTrait for Block {
+ fn new() -> Self {
+ Block::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn setup_before_prepare(&mut self, _cx: &mut Context) {
+ self.set_classes(
+ ClassesOp::Prepend,
+ ["block__container".to_string(), self.style().to_string()].join(" "),
+ );
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let block_body = self.components().render(cx);
+
+ if block_body.is_empty() {
+ return PrepareMarkup::None;
+ }
+
+ let id = cx.required_id::(self.id());
+
+ PrepareMarkup::With(html! {
+ div id=(id) class=[self.classes().get()] {
+ @if let Some(title) = self.title().using(cx.langid()) {
+ h2 class="block__title" { (title) }
+ }
+ div class="block__content" { (block_body) }
+ }
+ })
+ }
+}
+
+impl Block {
+ // Block BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_style(&mut self, style: StyleBase) -> &mut Self {
+ self.style = style;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_title(&mut self, title: L10n) -> &mut Self {
+ self.title.set_value(title);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_components(&mut self, op: AnyOp) -> &mut Self {
+ self.mixed.set_value(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
+ self.mixed.set_value(AnyOp::Add(AnyComponent::with(component)));
+ self
+ }
+
+ // Block GETTERS.
+
+ pub fn style(&self) -> &StyleBase {
+ &self.style
+ }
+
+ pub fn title(&self) -> &OptionTranslated {
+ &self.title
+ }
+
+ pub fn components(&self) -> &MixedComponents {
+ &self.mixed
+ }
+}
diff --git a/src/base/component/branding.rs b/src/base/component/branding.rs
new file mode 100644
index 00000000..08ba8b01
--- /dev/null
+++ b/src/base/component/branding.rs
@@ -0,0 +1,102 @@
+use crate::prelude::*;
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Branding {
+ id : OptionId,
+ #[default(_code = "global::SETTINGS.app.name.to_owned()")]
+ app_name : String,
+ slogan : OptionTranslated,
+ logo : OptionComponent,
+ #[default(_code = "|_| \"/\"")]
+ frontpage: FnContextualPath,
+}
+
+impl ComponentTrait for Branding {
+ fn new() -> Self {
+ Branding::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let logo = self.logo().render(cx);
+ let home = self.frontpage()(cx);
+ let title = &L10n::l("site_home").using(cx.langid());
+ PrepareMarkup::With(html! {
+ div id=[self.id()] class="branding__container" {
+ div class="branding__content" {
+ @if !logo.is_empty() {
+ a class="branding__logo" href=(home) title=[title] rel="home" {
+ (logo)
+ }
+ }
+ div class="branding__text" {
+ a class="branding__name" href=(home) title=[title] rel="home" {
+ (self.app_name())
+ }
+ @if let Some(slogan) = self.slogan().using(cx.langid()) {
+ div class="branding__slogan" {
+ (slogan)
+ }
+ }
+ }
+ }
+ }
+ })
+ }
+}
+
+impl Branding {
+ // Branding BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_app_name(&mut self, app_name: impl Into) -> &mut Self {
+ self.app_name = app_name.into();
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_slogan(&mut self, slogan: L10n) -> &mut Self {
+ self.slogan.set_value(slogan);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_logo(&mut self, logo: Option) -> &mut Self {
+ self.logo.set_value(logo);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_frontpage(&mut self, frontpage: FnContextualPath) -> &mut Self {
+ self.frontpage = frontpage;
+ self
+ }
+
+ // Branding GETTERS.
+
+ pub fn app_name(&self) -> &String {
+ &self.app_name
+ }
+
+ pub fn slogan(&self) -> &OptionTranslated {
+ &self.slogan
+ }
+
+ pub fn logo(&self) -> &OptionComponent {
+ &self.logo
+ }
+
+ pub fn frontpage(&self) -> &FnContextualPath {
+ &self.frontpage
+ }
+}
diff --git a/src/base/component/button.rs b/src/base/component/button.rs
new file mode 100644
index 00000000..2215a4bb
--- /dev/null
+++ b/src/base/component/button.rs
@@ -0,0 +1,156 @@
+use crate::prelude::*;
+
+#[derive(AutoDefault)]
+pub enum ButtonTarget {
+ #[default]
+ Default,
+ Blank,
+ Parent,
+ Top,
+ Context(String),
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Button {
+ id : OptionId,
+ classes : OptionClasses,
+ style : StyleBase,
+ font_size : FontSize,
+ left_icon : OptionComponent,
+ right_icon: OptionComponent,
+ href : OptionString,
+ html : OptionTranslated,
+ target : ButtonTarget,
+}
+
+impl ComponentTrait for Button {
+ fn new() -> Self {
+ Button::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn setup_before_prepare(&mut self, _cx: &mut Context) {
+ self.set_classes(
+ ClassesOp::Prepend,
+ [
+ "button__tap".to_string(),
+ self.style().to_string(),
+ self.font_size().to_string(),
+ ]
+ .join(" "),
+ );
+ }
+
+ #[rustfmt::skip]
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let target = match &self.target() {
+ ButtonTarget::Default => None,
+ ButtonTarget::Blank => Some("_blank"),
+ ButtonTarget::Parent => Some("_parent"),
+ ButtonTarget::Top => Some("_top"),
+ ButtonTarget::Context(name) => Some(name.as_str()),
+ };
+ PrepareMarkup::With(html! {
+ a
+ id=[self.id()]
+ class=[self.classes().get()]
+ href=[self.href().get()]
+ target=[target]
+ {
+ (self.left_icon().render(cx))
+ span { (self.html().escaped(cx.langid())) }
+ (self.right_icon().render(cx))
+ }
+ })
+ }
+}
+
+impl Button {
+ pub fn anchor(href: impl Into, html: L10n) -> Self {
+ Button::default().with_href(href).with_html(html)
+ }
+
+ // Button BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_style(&mut self, style: StyleBase) -> &mut Self {
+ self.style = style;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_font_size(&mut self, font_size: FontSize) -> &mut Self {
+ self.font_size = font_size;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_left_icon(&mut self, icon: Option) -> &mut Self {
+ self.left_icon.set_value(icon);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_right_icon(&mut self, icon: Option) -> &mut Self {
+ self.right_icon.set_value(icon);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_href(&mut self, href: impl Into) -> &mut Self {
+ self.href.set_value(href);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_html(&mut self, html: L10n) -> &mut Self {
+ self.html.set_value(html);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_target(&mut self, target: ButtonTarget) -> &mut Self {
+ self.target = target;
+ self
+ }
+
+ // Button GETTERS.
+
+ pub fn style(&self) -> &StyleBase {
+ &self.style
+ }
+
+ pub fn font_size(&self) -> &FontSize {
+ &self.font_size
+ }
+
+ pub fn left_icon(&self) -> &OptionComponent {
+ &self.left_icon
+ }
+
+ pub fn right_icon(&self) -> &OptionComponent {
+ &self.right_icon
+ }
+
+ pub fn href(&self) -> &OptionString {
+ &self.href
+ }
+
+ pub fn html(&self) -> &OptionTranslated {
+ &self.html
+ }
+
+ pub fn target(&self) -> &ButtonTarget {
+ &self.target
+ }
+}
diff --git a/src/base/component/flex.rs b/src/base/component/flex.rs
new file mode 100644
index 00000000..0b732dcd
--- /dev/null
+++ b/src/base/component/flex.rs
@@ -0,0 +1,311 @@
+mod container;
+pub use container::Container;
+
+mod item;
+pub use item::Item;
+
+use crate::prelude::*;
+
+use std::fmt;
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Direction {
+ #[default]
+ Default,
+ Row(BreakPoint),
+ RowReverse(BreakPoint),
+ Column(BreakPoint),
+ ColumnReverse(BreakPoint),
+}
+
+#[rustfmt::skip]
+impl fmt::Display for Direction {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Direction::Default => write!(f, "flex__row {}", BreakPoint::default()),
+ Direction::Row(bp) => write!(f, "flex__row {bp}"),
+ Direction::RowReverse(bp) => write!(f, "flex__row flex__reverse {bp}"),
+ Direction::Column(bp) => write!(f, "flex__col {bp}"),
+ Direction::ColumnReverse(bp) => write!(f, "flex__col flex__reverse {bp}"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Wrap {
+ #[default]
+ Default,
+ NoWrap,
+ Wrap(ContentAlign),
+ WrapReverse(ContentAlign),
+}
+
+#[rustfmt::skip]
+impl fmt::Display for Wrap {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Wrap::Default => write!(f, ""),
+ Wrap::NoWrap => write!(f, "flex__nowrap"),
+ Wrap::Wrap(a) => write!(f, "flex__wrap {a}"),
+ Wrap::WrapReverse(a) => write!(f, "flex__wrap-reverse {a}"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum ContentAlign {
+ #[default]
+ Default,
+ Start,
+ End,
+ Center,
+ Stretch,
+ SpaceBetween,
+ SpaceAround,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for ContentAlign {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ContentAlign::Default => write!(f, ""),
+ ContentAlign::Start => write!(f, "flex__align-start"),
+ ContentAlign::End => write!(f, "flex__align-end"),
+ ContentAlign::Center => write!(f, "flex__align-center"),
+ ContentAlign::Stretch => write!(f, "flex__align-stretch"),
+ ContentAlign::SpaceBetween => write!(f, "flex__align-space-between"),
+ ContentAlign::SpaceAround => write!(f, "flex__align-space-around"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Justify {
+ #[default]
+ Default,
+ Start,
+ End,
+ Center,
+ SpaceBetween,
+ SpaceAround,
+ SpaceEvenly,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for Justify {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Justify::Default => write!(f, ""),
+ Justify::Start => write!(f, "flex__justify-start"),
+ Justify::End => write!(f, "flex__justify-end"),
+ Justify::Center => write!(f, "flex__justify-center"),
+ Justify::SpaceBetween => write!(f, "flex__justify-space-between"),
+ Justify::SpaceAround => write!(f, "flex__justify-space-around"),
+ Justify::SpaceEvenly => write!(f, "flex__justify-space-evenly"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Align {
+ #[default]
+ Default,
+ Start,
+ End,
+ Center,
+ Stretch,
+ Baseline,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for Align {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Align::Default => write!(f, ""),
+ Align::Start => write!(f, "flex__start"),
+ Align::End => write!(f, "flex__end"),
+ Align::Center => write!(f, "flex__center"),
+ Align::Stretch => write!(f, "flex__stretch"),
+ Align::Baseline => write!(f, "flex__baseline"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Gap {
+ #[default]
+ Default,
+ Row(unit::Value),
+ Column(unit::Value),
+ Distinct(unit::Value, unit::Value),
+ Both(unit::Value),
+}
+
+#[rustfmt::skip]
+impl fmt::Display for Gap {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Gap::Default => write!(f, ""),
+ Gap::Row(r) => write!(f, "row-gap: {r};"),
+ Gap::Column(c) => write!(f, "column-gap: {c};"),
+ Gap::Distinct(r, c) => write!(f, "gap: {r} {c};"),
+ Gap::Both(v) => write!(f, "gap: {v};"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Grow {
+ #[default]
+ Default,
+ Is1,
+ Is2,
+ Is3,
+ Is4,
+ Is5,
+ Is6,
+ Is7,
+ Is8,
+ Is9,
+}
+
+impl fmt::Display for Grow {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Grow::Default => write!(f, ""),
+ Grow::Is1 => write!(f, "flex__grow-1"),
+ Grow::Is2 => write!(f, "flex__grow-2"),
+ Grow::Is3 => write!(f, "flex__grow-3"),
+ Grow::Is4 => write!(f, "flex__grow-4"),
+ Grow::Is5 => write!(f, "flex__grow-5"),
+ Grow::Is6 => write!(f, "flex__grow-6"),
+ Grow::Is7 => write!(f, "flex__grow-7"),
+ Grow::Is8 => write!(f, "flex__grow-8"),
+ Grow::Is9 => write!(f, "flex__grow-9"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Shrink {
+ #[default]
+ Default,
+ Is1,
+ Is2,
+ Is3,
+ Is4,
+ Is5,
+ Is6,
+ Is7,
+ Is8,
+ Is9,
+}
+
+impl fmt::Display for Shrink {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Shrink::Default => write!(f, ""),
+ Shrink::Is1 => write!(f, "flex__shrink-1"),
+ Shrink::Is2 => write!(f, "flex__shrink-2"),
+ Shrink::Is3 => write!(f, "flex__shrink-3"),
+ Shrink::Is4 => write!(f, "flex__shrink-4"),
+ Shrink::Is5 => write!(f, "flex__shrink-5"),
+ Shrink::Is6 => write!(f, "flex__shrink-6"),
+ Shrink::Is7 => write!(f, "flex__shrink-7"),
+ Shrink::Is8 => write!(f, "flex__shrink-8"),
+ Shrink::Is9 => write!(f, "flex__shrink-9"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Size {
+ #[default]
+ Default,
+ Percent10,
+ Percent20,
+ Percent25,
+ Percent33,
+ Percent40,
+ Percent50,
+ Percent60,
+ Percent66,
+ Percent75,
+ Percent80,
+ Percent90,
+}
+
+impl fmt::Display for Size {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Size::Default => write!(f, ""),
+ Size::Percent10 => write!(f, "flex__size-10"),
+ Size::Percent20 => write!(f, "flex__size-20"),
+ Size::Percent25 => write!(f, "flex__size-25"),
+ Size::Percent33 => write!(f, "flex__size-33"),
+ Size::Percent40 => write!(f, "flex__size-40"),
+ Size::Percent50 => write!(f, "flex__size-50"),
+ Size::Percent60 => write!(f, "flex__size-60"),
+ Size::Percent66 => write!(f, "flex__size-66"),
+ Size::Percent75 => write!(f, "flex__size-75"),
+ Size::Percent80 => write!(f, "flex__size-80"),
+ Size::Percent90 => write!(f, "flex__size-90"),
+ }
+ }
+}
+
+// *************************************************************************************************
+
+#[derive(AutoDefault)]
+pub enum Offset {
+ #[default]
+ Default,
+ Offset10,
+ Offset20,
+ Offset25,
+ Offset33,
+ Offset40,
+ Offset50,
+ Offset60,
+ Offset66,
+ Offset75,
+ Offset80,
+ Offset90,
+}
+
+impl fmt::Display for Offset {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Offset::Default => write!(f, ""),
+ Offset::Offset10 => write!(f, "flex__offset-10"),
+ Offset::Offset20 => write!(f, "flex__offset-20"),
+ Offset::Offset25 => write!(f, "flex__offset-25"),
+ Offset::Offset33 => write!(f, "flex__offset-33"),
+ Offset::Offset40 => write!(f, "flex__offset-40"),
+ Offset::Offset50 => write!(f, "flex__offset-50"),
+ Offset::Offset60 => write!(f, "flex__offset-60"),
+ Offset::Offset66 => write!(f, "flex__offset-66"),
+ Offset::Offset75 => write!(f, "flex__offset-75"),
+ Offset::Offset80 => write!(f, "flex__offset-80"),
+ Offset::Offset90 => write!(f, "flex__offset-90"),
+ }
+ }
+}
diff --git a/src/base/component/flex/container.rs b/src/base/component/flex/container.rs
new file mode 100644
index 00000000..0b095c30
--- /dev/null
+++ b/src/base/component/flex/container.rs
@@ -0,0 +1,212 @@
+use crate::prelude::*;
+
+#[derive(AutoDefault)]
+pub enum ContainerType {
+ #[default]
+ Default,
+ Header,
+ Main,
+ Section,
+ Article,
+ Footer,
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Container {
+ id : OptionId,
+ classes : OptionClasses,
+ container_type: ContainerType,
+ direction : flex::Direction,
+ flex_wrap : flex::Wrap,
+ flex_justify : flex::Justify,
+ flex_align : flex::Align,
+ flex_gap : flex::Gap,
+ items : MixedComponents,
+}
+
+impl ComponentTrait for Container {
+ fn new() -> Self {
+ Container::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn setup_before_prepare(&mut self, cx: &mut Context) {
+ self.set_classes(
+ ClassesOp::Prepend,
+ [
+ "flex__container".to_string(),
+ self.direction().to_string(),
+ self.wrap().to_string(),
+ self.justify().to_string(),
+ self.align().to_string(),
+ ]
+ .join(" "),
+ );
+
+ cx.set_param::(PARAM_BASE_INCLUDE_FLEX_ASSETS, &true);
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let output = self.items().render(cx);
+ if output.is_empty() {
+ return PrepareMarkup::None;
+ }
+
+ let gap = match self.gap() {
+ flex::Gap::Default => None,
+ _ => Some(self.gap().to_string()),
+ };
+ match self.container_type() {
+ ContainerType::Default => PrepareMarkup::With(html! {
+ div id=[self.id()] class=[self.classes().get()] style=[gap] {
+ (output)
+ }
+ }),
+ ContainerType::Header => PrepareMarkup::With(html! {
+ header id=[self.id()] class=[self.classes().get()] style=[gap] {
+ (output)
+ }
+ }),
+ ContainerType::Main => PrepareMarkup::With(html! {
+ main id=[self.id()] class=[self.classes().get()] style=[gap] {
+ (output)
+ }
+ }),
+ ContainerType::Section => PrepareMarkup::With(html! {
+ section id=[self.id()] class=[self.classes().get()] style=[gap] {
+ (output)
+ }
+ }),
+ ContainerType::Article => PrepareMarkup::With(html! {
+ article id=[self.id()] class=[self.classes().get()] style=[gap] {
+ (output)
+ }
+ }),
+ ContainerType::Footer => PrepareMarkup::With(html! {
+ footer id=[self.id()] class=[self.classes().get()] style=[gap] {
+ (output)
+ }
+ }),
+ }
+ }
+}
+
+impl Container {
+ pub fn header() -> Self {
+ Container {
+ container_type: ContainerType::Header,
+ ..Default::default()
+ }
+ }
+
+ pub fn main() -> Self {
+ Container {
+ container_type: ContainerType::Main,
+ ..Default::default()
+ }
+ }
+
+ pub fn section() -> Self {
+ Container {
+ container_type: ContainerType::Section,
+ ..Default::default()
+ }
+ }
+
+ pub fn article() -> Self {
+ Container {
+ container_type: ContainerType::Article,
+ ..Default::default()
+ }
+ }
+
+ pub fn footer() -> Self {
+ Container {
+ container_type: ContainerType::Footer,
+ ..Default::default()
+ }
+ }
+
+ // Container BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_direction(&mut self, direction: flex::Direction) -> &mut Self {
+ self.direction = direction;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_wrap(&mut self, wrap: flex::Wrap) -> &mut Self {
+ self.flex_wrap = wrap;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_justify(&mut self, justify: flex::Justify) -> &mut Self {
+ self.flex_justify = justify;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_align(&mut self, align: flex::Align) -> &mut Self {
+ self.flex_align = align;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_gap(&mut self, gap: flex::Gap) -> &mut Self {
+ self.flex_gap = gap;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_items(&mut self, op: TypedOp) -> &mut Self {
+ self.items.set_typed(op);
+ self
+ }
+
+ pub fn add_item(mut self, item: flex::Item) -> Self {
+ self.items.set_value(AnyOp::Add(AnyComponent::with(item)));
+ self
+ }
+
+ // Container GETTERS.
+
+ pub fn container_type(&self) -> &ContainerType {
+ &self.container_type
+ }
+
+ pub fn direction(&self) -> &flex::Direction {
+ &self.direction
+ }
+
+ pub fn wrap(&self) -> &flex::Wrap {
+ &self.flex_wrap
+ }
+
+ pub fn justify(&self) -> &flex::Justify {
+ &self.flex_justify
+ }
+
+ pub fn align(&self) -> &flex::Align {
+ &self.flex_align
+ }
+
+ pub fn gap(&self) -> &flex::Gap {
+ &self.flex_gap
+ }
+
+ pub fn items(&self) -> &MixedComponents {
+ &self.items
+ }
+}
diff --git a/src/base/component/flex/item.rs b/src/base/component/flex/item.rs
new file mode 100644
index 00000000..b1ec3073
--- /dev/null
+++ b/src/base/component/flex/item.rs
@@ -0,0 +1,200 @@
+use crate::prelude::*;
+
+#[derive(AutoDefault)]
+pub enum ItemType {
+ #[default]
+ Default,
+ Region,
+ Wrapper,
+ Bundle,
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Item {
+ id : OptionId,
+ classes : OptionClasses,
+ item_type : ItemType,
+ flex_grow : flex::Grow,
+ flex_shrink: flex::Shrink,
+ flex_size : flex::Size,
+ flex_offset: flex::Offset,
+ flex_align : flex::Align,
+ mixed : MixedComponents,
+}
+
+impl ComponentTrait for Item {
+ fn new() -> Self {
+ Item::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn setup_before_prepare(&mut self, _cx: &mut Context) {
+ self.set_classes(
+ ClassesOp::Prepend,
+ [
+ "flex__item".to_string(),
+ self.grow().to_string(),
+ self.shrink().to_string(),
+ self.size().to_string(),
+ self.offset().to_string(),
+ self.align().to_string(),
+ ]
+ .join(" "),
+ );
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let (output, region) = match self.item_type() {
+ ItemType::Region => (
+ self.components().render(cx),
+ if let Some(id) = self.id() {
+ cx.prepare_region(id)
+ } else {
+ Markup::default()
+ },
+ ),
+ _ => (self.components().render(cx), Markup::default()),
+ };
+ if output.is_empty() && region.is_empty() {
+ return PrepareMarkup::None;
+ }
+ match self.item_type() {
+ ItemType::Default => PrepareMarkup::With(html! {
+ div id=[self.id()] class=[self.classes().get()] {
+ div class="flex__content" {
+ (output)
+ }
+ }
+ }),
+ ItemType::Region => PrepareMarkup::With(html! {
+ div id=[self.id()] class=[self.classes().get()] {
+ div class="flex__content flex__region" {
+ (region)
+ (output)
+ }
+ }
+ }),
+ ItemType::Wrapper => PrepareMarkup::With(html! {
+ div id=[self.id()] class=[self.classes().get()] {
+ (output)
+ }
+ }),
+ ItemType::Bundle => PrepareMarkup::With(html! {
+ (output)
+ }),
+ }
+ }
+}
+
+impl Item {
+ pub fn region() -> Self {
+ Item {
+ item_type: ItemType::Region,
+ ..Default::default()
+ }
+ }
+
+ pub fn wrapper() -> Self {
+ Item {
+ item_type: ItemType::Wrapper,
+ ..Default::default()
+ }
+ }
+
+ pub fn bundle() -> Self {
+ Item {
+ item_type: ItemType::Bundle,
+ ..Default::default()
+ }
+ }
+
+ pub fn with(component: impl ComponentTrait) -> Self {
+ Item::default().add_component(component)
+ }
+
+ // Item BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_grow(&mut self, grow: flex::Grow) -> &mut Self {
+ self.flex_grow = grow;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_shrink(&mut self, shrink: flex::Shrink) -> &mut Self {
+ self.flex_shrink = shrink;
+ self
+ }
+
+ #[fn_builder]
+ // Ensures the item occupies the exact specified width, neither growing nor shrinking,
+ // regardless of the available space in the container or the size of other items.
+ pub fn set_size(&mut self, size: flex::Size) -> &mut Self {
+ self.flex_size = size;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_offset(&mut self, offset: flex::Offset) -> &mut Self {
+ self.flex_offset = offset;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_align(&mut self, align: flex::Align) -> &mut Self {
+ self.flex_align = align;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_components(&mut self, op: AnyOp) -> &mut Self {
+ self.mixed.set_value(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
+ self.mixed.set_value(AnyOp::Add(AnyComponent::with(component)));
+ self
+ }
+
+ // Item GETTERS.
+
+ pub fn item_type(&self) -> &ItemType {
+ &self.item_type
+ }
+
+ pub fn grow(&self) -> &flex::Grow {
+ &self.flex_grow
+ }
+
+ pub fn shrink(&self) -> &flex::Shrink {
+ &self.flex_shrink
+ }
+
+ pub fn size(&self) -> &flex::Size {
+ &self.flex_size
+ }
+
+ pub fn offset(&self) -> &flex::Offset {
+ &self.flex_offset
+ }
+
+ pub fn align(&self) -> &flex::Align {
+ &self.flex_align
+ }
+
+ pub fn components(&self) -> &MixedComponents {
+ &self.mixed
+ }
+}
diff --git a/src/base/component/form.rs b/src/base/component/form.rs
new file mode 100644
index 00000000..bb5dd943
--- /dev/null
+++ b/src/base/component/form.rs
@@ -0,0 +1,14 @@
+mod form_main;
+pub use form_main::{Form, FormMethod};
+
+mod input;
+pub use input::{Input, InputType};
+
+mod hidden;
+pub use hidden::Hidden;
+
+mod date;
+pub use date::Date;
+
+mod action_button;
+pub use action_button::{ActionButton, ActionButtonType};
diff --git a/src/base/component/form/action_button.rs b/src/base/component/form/action_button.rs
new file mode 100644
index 00000000..cadbe391
--- /dev/null
+++ b/src/base/component/form/action_button.rs
@@ -0,0 +1,182 @@
+use crate::prelude::*;
+
+use std::fmt;
+
+#[derive(AutoDefault)]
+pub enum ActionButtonType {
+ #[default]
+ Submit,
+ Reset,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for ActionButtonType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ActionButtonType::Submit => write!(f, "submit"),
+ ActionButtonType::Reset => write!(f, "reset"),
+ }
+ }
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct ActionButton {
+ classes : OptionClasses,
+ button_type: ActionButtonType,
+ style : StyleBase,
+ font_size : FontSize,
+ left_icon : OptionComponent,
+ right_icon : OptionComponent,
+ name : OptionString,
+ value : OptionTranslated,
+ autofocus : OptionString,
+ disabled : OptionString,
+}
+
+impl ComponentTrait for ActionButton {
+ fn new() -> Self {
+ ActionButton::submit()
+ }
+
+ fn setup_before_prepare(&mut self, _cx: &mut Context) {
+ self.set_classes(
+ ClassesOp::Prepend,
+ [
+ "button__tap".to_string(),
+ self.style().to_string(),
+ self.font_size().to_string(),
+ ]
+ .join(" "),
+ );
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let id = self.name().get().map(|name| concat_string!("edit-", name));
+ PrepareMarkup::With(html! {
+ button
+ type=(self.button_type().to_string())
+ id=[id]
+ class=[self.classes().get()]
+ name=[self.name().get()]
+ value=[self.value().using(cx.langid())]
+ autofocus=[self.autofocus().get()]
+ disabled=[self.disabled().get()]
+ {
+ (self.left_icon().render(cx))
+ span { (self.value().escaped(cx.langid())) }
+ (self.right_icon().render(cx))
+ }
+ })
+ }
+}
+
+impl ActionButton {
+ pub fn submit() -> Self {
+ ActionButton {
+ button_type: ActionButtonType::Submit,
+ style: StyleBase::Default,
+ value: OptionTranslated::new(L10n::l("button_submit")),
+ ..Default::default()
+ }
+ }
+
+ pub fn reset() -> Self {
+ ActionButton {
+ button_type: ActionButtonType::Reset,
+ style: StyleBase::Info,
+ value: OptionTranslated::new(L10n::l("button_reset")),
+ ..Default::default()
+ }
+ }
+
+ // Button BUILDER.
+
+ #[fn_builder]
+ pub fn set_style(&mut self, style: StyleBase) -> &mut Self {
+ self.style = style;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_font_size(&mut self, font_size: FontSize) -> &mut Self {
+ self.font_size = font_size;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_left_icon(&mut self, icon: Option) -> &mut Self {
+ self.left_icon.set_value(icon);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_right_icon(&mut self, icon: Option) -> &mut Self {
+ self.right_icon.set_value(icon);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_name(&mut self, name: &str) -> &mut Self {
+ self.name.set_value(name);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_value(&mut self, value: L10n) -> &mut Self {
+ self.value.set_value(value);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_autofocus(&mut self, toggle: bool) -> &mut Self {
+ self.autofocus
+ .set_value(if toggle { "autofocus" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_disabled(&mut self, toggle: bool) -> &mut Self {
+ self.disabled
+ .set_value(if toggle { "disabled" } else { "" });
+ self
+ }
+
+ // Button GETTERS.
+
+ pub fn button_type(&self) -> &ActionButtonType {
+ &self.button_type
+ }
+
+ pub fn style(&self) -> &StyleBase {
+ &self.style
+ }
+
+ pub fn font_size(&self) -> &FontSize {
+ &self.font_size
+ }
+
+ pub fn left_icon(&self) -> &OptionComponent {
+ &self.left_icon
+ }
+
+ pub fn right_icon(&self) -> &OptionComponent {
+ &self.right_icon
+ }
+
+ pub fn name(&self) -> &OptionString {
+ &self.name
+ }
+
+ pub fn value(&self) -> &OptionTranslated {
+ &self.value
+ }
+
+ pub fn autofocus(&self) -> &OptionString {
+ &self.autofocus
+ }
+
+ pub fn disabled(&self) -> &OptionString {
+ &self.disabled
+ }
+}
diff --git a/src/base/component/form/date.rs b/src/base/component/form/date.rs
new file mode 100644
index 00000000..859a2e86
--- /dev/null
+++ b/src/base/component/form/date.rs
@@ -0,0 +1,166 @@
+use crate::prelude::*;
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Date {
+ classes : OptionClasses,
+ name : OptionString,
+ value : OptionString,
+ label : OptionString,
+ placeholder : OptionString,
+ autofocus : OptionString,
+ autocomplete: OptionString,
+ disabled : OptionString,
+ readonly : OptionString,
+ required : OptionString,
+ help_text : OptionString,
+}
+
+impl ComponentTrait for Date {
+ fn new() -> Self {
+ Date::default().with_classes(ClassesOp::Add, "form-item form-type-date")
+ }
+
+ fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
+ let id = self.name().get().map(|name| concat_string!("edit-", name));
+ PrepareMarkup::With(html! {
+ div class=[self.classes().get()] {
+ @if let Some(label) = self.label().get() {
+ label class="form-label" for=[&id] {
+ (label) " "
+ @if self.required().get().is_some() {
+ span
+ class="form-required"
+ title="Este campo es obligatorio." { "*" } " "
+ }
+ }
+ }
+ input
+ type="date"
+ id=[id]
+ class="form-control"
+ name=[self.name().get()]
+ value=[self.value().get()]
+ placeholder=[self.placeholder().get()]
+ autofocus=[self.autofocus().get()]
+ autocomplete=[self.autocomplete().get()]
+ readonly=[self.readonly().get()]
+ required=[self.required().get()]
+ disabled=[self.disabled().get()] {}
+ @if let Some(help_text) = self.help_text().get() {
+ div class="form-text" { (help_text) }
+ }
+ }
+ })
+ }
+}
+
+impl Date {
+ // Date BUILDER.
+
+ #[fn_builder]
+ pub fn set_name(&mut self, name: &str) -> &mut Self {
+ self.name.set_value(name);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_value(&mut self, value: &str) -> &mut Self {
+ self.value.set_value(value);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_label(&mut self, label: &str) -> &mut Self {
+ self.label.set_value(label);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_placeholder(&mut self, placeholder: &str) -> &mut Self {
+ self.placeholder.set_value(placeholder);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_autofocus(&mut self, toggle: bool) -> &mut Self {
+ self.autofocus
+ .set_value(if toggle { "autofocus" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_autocomplete(&mut self, toggle: bool) -> &mut Self {
+ self.autocomplete.set_value(if toggle { "" } else { "off" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_disabled(&mut self, toggle: bool) -> &mut Self {
+ self.disabled
+ .set_value(if toggle { "disabled" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_readonly(&mut self, toggle: bool) -> &mut Self {
+ self.readonly
+ .set_value(if toggle { "readonly" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_required(&mut self, toggle: bool) -> &mut Self {
+ self.required
+ .set_value(if toggle { "required" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_help_text(&mut self, help_text: &str) -> &mut Self {
+ self.help_text.set_value(help_text);
+ self
+ }
+
+ // Date GETTERS.
+
+ pub fn name(&self) -> &OptionString {
+ &self.name
+ }
+
+ pub fn value(&self) -> &OptionString {
+ &self.value
+ }
+
+ pub fn label(&self) -> &OptionString {
+ &self.label
+ }
+
+ pub fn placeholder(&self) -> &OptionString {
+ &self.placeholder
+ }
+
+ pub fn autofocus(&self) -> &OptionString {
+ &self.autofocus
+ }
+
+ pub fn autocomplete(&self) -> &OptionString {
+ &self.autocomplete
+ }
+
+ pub fn disabled(&self) -> &OptionString {
+ &self.disabled
+ }
+
+ pub fn readonly(&self) -> &OptionString {
+ &self.readonly
+ }
+
+ pub fn required(&self) -> &OptionString {
+ &self.required
+ }
+
+ pub fn help_text(&self) -> &OptionString {
+ &self.help_text
+ }
+}
diff --git a/src/base/component/form/form_main.rs b/src/base/component/form/form_main.rs
new file mode 100644
index 00000000..b630571c
--- /dev/null
+++ b/src/base/component/form/form_main.rs
@@ -0,0 +1,107 @@
+use crate::prelude::*;
+
+#[derive(AutoDefault)]
+pub enum FormMethod {
+ #[default]
+ Post,
+ Get,
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Form {
+ id : OptionId,
+ classes: OptionClasses,
+ action : OptionString,
+ charset: OptionString,
+ method : FormMethod,
+ mixed : MixedComponents,
+}
+
+impl ComponentTrait for Form {
+ fn new() -> Self {
+ Form::default()
+ .with_classes(ClassesOp::Add, "form")
+ .with_charset("UTF-8")
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let method = match self.method() {
+ FormMethod::Post => Some("post".to_owned()),
+ FormMethod::Get => None,
+ };
+ PrepareMarkup::With(html! {
+ form
+ id=[self.id()]
+ class=[self.classes().get()]
+ action=[self.action().get()]
+ method=[method]
+ accept-charset=[self.charset().get()]
+ {
+ div { (self.elements().render(cx)) }
+ }
+ })
+ }
+}
+
+impl Form {
+ // Form BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_action(&mut self, action: &str) -> &mut Self {
+ self.action.set_value(action);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_charset(&mut self, charset: &str) -> &mut Self {
+ self.charset.set_value(charset);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_method(&mut self, method: FormMethod) -> &mut Self {
+ self.method = method;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_elements(&mut self, op: AnyOp) -> &mut Self {
+ self.mixed.set_value(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_element(mut self, element: impl ComponentTrait) -> Self {
+ self.mixed.set_value(AnyOp::Add(AnyComponent::with(element)));
+ self
+ }
+
+ // Form GETTERS.
+
+ pub fn action(&self) -> &OptionString {
+ &self.action
+ }
+
+ pub fn charset(&self) -> &OptionString {
+ &self.charset
+ }
+
+ pub fn method(&self) -> &FormMethod {
+ &self.method
+ }
+
+ pub fn elements(&self) -> &MixedComponents {
+ &self.mixed
+ }
+}
diff --git a/src/base/component/form/hidden.rs b/src/base/component/form/hidden.rs
new file mode 100644
index 00000000..66bfccb9
--- /dev/null
+++ b/src/base/component/form/hidden.rs
@@ -0,0 +1,51 @@
+use crate::prelude::*;
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Hidden {
+ name : OptionName,
+ value : OptionString,
+}
+
+impl ComponentTrait for Hidden {
+ fn new() -> Self {
+ Hidden::default()
+ }
+
+ fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
+ let id = self.name().get().map(|name| concat_string!("value-", name));
+ PrepareMarkup::With(html! {
+ input type="hidden" id=[id] name=[self.name().get()] value=[self.value().get()] {}
+ })
+ }
+}
+
+impl Hidden {
+ pub fn set(name: &str, value: &str) -> Self {
+ Hidden::default().with_name(name).with_value(value)
+ }
+
+ // Hidden BUILDER.
+
+ #[fn_builder]
+ pub fn set_name(&mut self, name: &str) -> &mut Self {
+ self.name.set_value(name);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_value(&mut self, value: &str) -> &mut Self {
+ self.value.set_value(value);
+ self
+ }
+
+ // Hidden GETTERS.
+
+ pub fn name(&self) -> &OptionName {
+ &self.name
+ }
+
+ pub fn value(&self) -> &OptionString {
+ &self.value
+ }
+}
diff --git a/src/base/component/form/input.rs b/src/base/component/form/input.rs
new file mode 100644
index 00000000..a59d77c8
--- /dev/null
+++ b/src/base/component/form/input.rs
@@ -0,0 +1,283 @@
+use crate::prelude::*;
+
+#[derive(AutoDefault)]
+pub enum InputType {
+ #[default]
+ Textfield,
+ Password,
+ Search,
+ Email,
+ Telephone,
+ Url,
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Input {
+ classes : OptionClasses,
+ input_type : InputType,
+ name : OptionName,
+ value : OptionString,
+ label : OptionTranslated,
+ size : Option,
+ minlength : Option,
+ maxlength : Option,
+ placeholder : OptionString,
+ autofocus : OptionString,
+ autocomplete: OptionString,
+ disabled : OptionString,
+ readonly : OptionString,
+ required : OptionString,
+ help_text : OptionTranslated,
+}
+
+impl ComponentTrait for Input {
+ fn new() -> Self {
+ Input::default()
+ .with_classes(ClassesOp::Add, "form-item form-type-textfield")
+ .with_size(Some(60))
+ .with_maxlength(Some(128))
+ }
+
+ #[rustfmt::skip]
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let type_input = match self.input_type() {
+ InputType::Textfield => "text",
+ InputType::Password => "password",
+ InputType::Search => "search",
+ InputType::Email => "email",
+ InputType::Telephone => "tel",
+ InputType::Url => "url",
+ };
+ let id = self.name().get().map(|name| concat_string!("edit-", name));
+ PrepareMarkup::With(html! {
+ div class=[self.classes().get()] {
+ @if let Some(label) = self.label().using(cx.langid()) {
+ label class="form-label" for=[&id] {
+ (label) " "
+ @if self.required().get().is_some() {
+ span
+ class="form-required"
+ title="Este campo es obligatorio." { "*" } " "
+ }
+ }
+ }
+ input
+ type=(type_input)
+ id=[id]
+ class="form-control"
+ name=[self.name().get()]
+ value=[self.value().get()]
+ size=[self.size()]
+ minlength=[self.minlength()]
+ maxlength=[self.maxlength()]
+ placeholder=[self.placeholder().get()]
+ autofocus=[self.autofocus().get()]
+ autocomplete=[self.autocomplete().get()]
+ readonly=[self.readonly().get()]
+ required=[self.required().get()]
+ disabled=[self.disabled().get()] {}
+ @if let Some(description) = self.help_text().using(cx.langid()) {
+ div class="form-text" { (description) }
+ }
+ }
+ })
+ }
+}
+
+impl Input {
+ pub fn textfield() -> Self {
+ Input::default()
+ }
+
+ pub fn password() -> Self {
+ let mut input = Input::default().with_classes(
+ ClassesOp::Replace("form-type-textfield".to_owned()),
+ "form-type-password",
+ );
+ input.input_type = InputType::Password;
+ input
+ }
+
+ pub fn search() -> Self {
+ let mut input = Input::default().with_classes(
+ ClassesOp::Replace("form-type-textfield".to_owned()),
+ "form-type-search",
+ );
+ input.input_type = InputType::Search;
+ input
+ }
+
+ pub fn email() -> Self {
+ let mut input = Input::default().with_classes(
+ ClassesOp::Replace("form-type-textfield".to_owned()),
+ "form-type-email",
+ );
+ input.input_type = InputType::Email;
+ input
+ }
+
+ pub fn telephone() -> Self {
+ let mut input = Input::default().with_classes(
+ ClassesOp::Replace("form-type-textfield".to_owned()),
+ "form-type-telephone",
+ );
+ input.input_type = InputType::Telephone;
+ input
+ }
+
+ pub fn url() -> Self {
+ let mut input = Input::default().with_classes(
+ ClassesOp::Replace("form-type-textfield".to_owned()),
+ "form-type-url",
+ );
+ input.input_type = InputType::Url;
+ input
+ }
+
+ // Input BUILDER.
+
+ #[fn_builder]
+ pub fn set_name(&mut self, name: &str) -> &mut Self {
+ if let Some(previous) = self.name.get() {
+ self.set_classes(ClassesOp::Remove, concat_string!("form-item-", previous));
+ }
+ self.set_classes(ClassesOp::Add, concat_string!("form-item-", name));
+ self.name.set_value(name);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_value(&mut self, value: &str) -> &mut Self {
+ self.value.set_value(value);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_label(&mut self, label: L10n) -> &mut Self {
+ self.label.set_value(label);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_size(&mut self, size: Option) -> &mut Self {
+ self.size = size;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_minlength(&mut self, minlength: Option) -> &mut Self {
+ self.minlength = minlength;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_maxlength(&mut self, maxlength: Option) -> &mut Self {
+ self.maxlength = maxlength;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_placeholder(&mut self, placeholder: &str) -> &mut Self {
+ self.placeholder.set_value(placeholder);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_autofocus(&mut self, toggle: bool) -> &mut Self {
+ self.autofocus
+ .set_value(if toggle { "autofocus" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_autocomplete(&mut self, toggle: bool) -> &mut Self {
+ self.autocomplete.set_value(if toggle { "" } else { "off" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_disabled(&mut self, toggle: bool) -> &mut Self {
+ self.disabled
+ .set_value(if toggle { "disabled" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_readonly(&mut self, toggle: bool) -> &mut Self {
+ self.readonly
+ .set_value(if toggle { "readonly" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_required(&mut self, toggle: bool) -> &mut Self {
+ self.required
+ .set_value(if toggle { "required" } else { "" });
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_help_text(&mut self, help_text: L10n) -> &mut Self {
+ self.help_text.set_value(help_text);
+ self
+ }
+
+ // Input GETTERS.
+
+ pub fn input_type(&self) -> &InputType {
+ &self.input_type
+ }
+
+ pub fn name(&self) -> &OptionName {
+ &self.name
+ }
+
+ pub fn value(&self) -> &OptionString {
+ &self.value
+ }
+
+ pub fn label(&self) -> &OptionTranslated {
+ &self.label
+ }
+
+ pub fn size(&self) -> Option {
+ self.size
+ }
+
+ pub fn minlength(&self) -> Option {
+ self.minlength
+ }
+
+ pub fn maxlength(&self) -> Option {
+ self.maxlength
+ }
+
+ pub fn placeholder(&self) -> &OptionString {
+ &self.placeholder
+ }
+
+ pub fn autofocus(&self) -> &OptionString {
+ &self.autofocus
+ }
+
+ pub fn autocomplete(&self) -> &OptionString {
+ &self.autocomplete
+ }
+
+ pub fn disabled(&self) -> &OptionString {
+ &self.disabled
+ }
+
+ pub fn readonly(&self) -> &OptionString {
+ &self.readonly
+ }
+
+ pub fn required(&self) -> &OptionString {
+ &self.required
+ }
+
+ pub fn help_text(&self) -> &OptionTranslated {
+ &self.help_text
+ }
+}
diff --git a/src/base/component/heading.rs b/src/base/component/heading.rs
new file mode 100644
index 00000000..7cb3b594
--- /dev/null
+++ b/src/base/component/heading.rs
@@ -0,0 +1,157 @@
+use crate::prelude::*;
+
+use std::fmt;
+
+#[derive(AutoDefault)]
+pub enum HeadingType {
+ #[default]
+ H1,
+ H2,
+ H3,
+ H4,
+ H5,
+ H6,
+}
+
+#[derive(AutoDefault)]
+pub enum HeadingSize {
+ ExtraLarge,
+ XxLarge,
+ XLarge,
+ Large,
+ Medium,
+ #[default]
+ Normal,
+ Subtitle,
+}
+
+#[rustfmt::skip]
+impl fmt::Display for HeadingSize {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ HeadingSize::ExtraLarge => write!(f, "heading__title-x3l"),
+ HeadingSize::XxLarge => write!(f, "heading__title-x2l"),
+ HeadingSize::XLarge => write!(f, "heading__title-xl"),
+ HeadingSize::Large => write!(f, "heading__title-l"),
+ HeadingSize::Medium => write!(f, "heading__title-m"),
+ HeadingSize::Normal => write!(f, ""),
+ HeadingSize::Subtitle => write!(f, "heading__subtitle"),
+ }
+ }
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Heading {
+ id : OptionId,
+ classes : OptionClasses,
+ heading_type: HeadingType,
+ size : HeadingSize,
+ text : OptionTranslated,
+}
+
+impl ComponentTrait for Heading {
+ fn new() -> Self {
+ Heading::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn setup_before_prepare(&mut self, _cx: &mut Context) {
+ self.set_classes(ClassesOp::Add, self.size().to_string());
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let id = self.id();
+ let classes = self.classes().get();
+ let text = self.text().escaped(cx.langid());
+ PrepareMarkup::With(html! { @match &self.heading_type() {
+ HeadingType::H1 => h1 id=[id] class=[classes] { (text) },
+ HeadingType::H2 => h2 id=[id] class=[classes] { (text) },
+ HeadingType::H3 => h3 id=[id] class=[classes] { (text) },
+ HeadingType::H4 => h4 id=[id] class=[classes] { (text) },
+ HeadingType::H5 => h5 id=[id] class=[classes] { (text) },
+ HeadingType::H6 => h6 id=[id] class=[classes] { (text) },
+ }})
+ }
+}
+
+impl Heading {
+ pub fn h1(text: L10n) -> Self {
+ Heading::default()
+ .with_heading_type(HeadingType::H1)
+ .with_text(text)
+ }
+
+ pub fn h2(text: L10n) -> Self {
+ Heading::default()
+ .with_heading_type(HeadingType::H2)
+ .with_text(text)
+ }
+
+ pub fn h3(text: L10n) -> Self {
+ Heading::default()
+ .with_heading_type(HeadingType::H3)
+ .with_text(text)
+ }
+
+ pub fn h4(text: L10n) -> Self {
+ Heading::default()
+ .with_heading_type(HeadingType::H4)
+ .with_text(text)
+ }
+
+ pub fn h5(text: L10n) -> Self {
+ Heading::default()
+ .with_heading_type(HeadingType::H5)
+ .with_text(text)
+ }
+
+ pub fn h6(text: L10n) -> Self {
+ Heading::default()
+ .with_heading_type(HeadingType::H6)
+ .with_text(text)
+ }
+
+ // Heading BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_heading_type(&mut self, heading_type: HeadingType) -> &mut Self {
+ self.heading_type = heading_type;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_size(&mut self, size: HeadingSize) -> &mut Self {
+ self.size = size;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_text(&mut self, text: L10n) -> &mut Self {
+ self.text.set_value(text);
+ self
+ }
+
+ // Paragraph GETTERS.
+
+ pub fn heading_type(&self) -> &HeadingType {
+ &self.heading_type
+ }
+
+ pub fn size(&self) -> &HeadingSize {
+ &self.size
+ }
+
+ pub fn text(&self) -> &OptionTranslated {
+ &self.text
+ }
+}
diff --git a/src/base/component/icon.rs b/src/base/component/icon.rs
new file mode 100644
index 00000000..7e0a6a1e
--- /dev/null
+++ b/src/base/component/icon.rs
@@ -0,0 +1,62 @@
+use crate::prelude::*;
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Icon {
+ classes : OptionClasses,
+ icon_name: OptionString,
+ font_size: FontSize,
+}
+
+impl ComponentTrait for Icon {
+ fn new() -> Self {
+ Icon::default()
+ }
+
+ #[rustfmt::skip]
+ fn setup_before_prepare(&mut self, cx: &mut Context) {
+ if let Some(icon_name) = self.icon_name().get() {
+ self.set_classes(ClassesOp::Prepend,
+ concat_string!("bi-", icon_name, " ", self.font_size().to_string()),
+ );
+ cx.set_param::(PARAM_BASE_INCLUDE_ICONS, &true);
+ }
+ }
+
+ fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
+ match self.icon_name().get() {
+ None => PrepareMarkup::None,
+ _ => PrepareMarkup::With(html! { i class=[self.classes().get()] {} }),
+ }
+ }
+}
+
+impl Icon {
+ pub fn with(icon_name: impl Into) -> Self {
+ Icon::default().with_icon_name(icon_name)
+ }
+
+ // Icon BUILDER.
+
+ #[fn_builder]
+ pub fn set_icon_name(&mut self, name: impl Into) -> &mut Self {
+ self.icon_name.set_value(name);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_font_size(&mut self, font_size: FontSize) -> &mut Self {
+ self.font_size = font_size;
+ self
+ }
+
+ // Icon GETTERS.
+
+ pub fn icon_name(&self) -> &OptionString {
+ &self.icon_name
+ }
+
+ pub fn font_size(&self) -> &FontSize {
+ &self.font_size
+ }
+}
diff --git a/src/base/component/image.rs b/src/base/component/image.rs
new file mode 100644
index 00000000..dcf2c625
--- /dev/null
+++ b/src/base/component/image.rs
@@ -0,0 +1,102 @@
+use crate::prelude::*;
+
+const IMG_FLUID: &str = "img__fluid";
+const IMG_FIXED: &str = "img__fixed";
+
+#[derive(AutoDefault)]
+pub enum ImageSize {
+ #[default]
+ Auto,
+ Size(u16, u16),
+ Width(u16),
+ Height(u16),
+ Both(u16),
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Image {
+ id : OptionId,
+ classes: OptionClasses,
+ source : OptionString,
+ size : ImageSize,
+}
+
+impl ComponentTrait for Image {
+ fn new() -> Self {
+ Image::default().with_classes(ClassesOp::Add, IMG_FLUID)
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
+ let (width, height) = match self.size() {
+ ImageSize::Auto => (None, None),
+ ImageSize::Size(width, height) => (Some(width), Some(height)),
+ ImageSize::Width(width) => (Some(width), None),
+ ImageSize::Height(height) => (None, Some(height)),
+ ImageSize::Both(value) => (Some(value), Some(value)),
+ };
+ PrepareMarkup::With(html! {
+ img
+ src=[self.source().get()]
+ id=[self.id()]
+ class=[self.classes().get()]
+ width=[width]
+ height=[height] {}
+ })
+ }
+}
+
+impl Image {
+ pub fn with(source: &str) -> Self {
+ Image::default()
+ .with_source(source)
+ .with_classes(ClassesOp::Add, IMG_FLUID)
+ }
+
+ pub fn fixed(source: &str) -> Self {
+ Image::default()
+ .with_source(source)
+ .with_classes(ClassesOp::Add, IMG_FIXED)
+ }
+
+ pub fn pagetop() -> Self {
+ Image::default()
+ .with_source("/base/pagetop-logo.svg")
+ .with_classes(ClassesOp::Add, IMG_FIXED)
+ .with_size(ImageSize::Size(64, 64))
+ }
+
+ // Image BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_source(&mut self, source: &str) -> &mut Self {
+ self.source.set_value(source);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_size(&mut self, size: ImageSize) -> &mut Self {
+ self.size = size;
+ self
+ }
+
+ // Image GETTERS.
+
+ pub fn source(&self) -> &OptionString {
+ &self.source
+ }
+
+ pub fn size(&self) -> &ImageSize {
+ &self.size
+ }
+}
diff --git a/src/base/component/menu.rs b/src/base/component/menu.rs
new file mode 100644
index 00000000..34ab59fe
--- /dev/null
+++ b/src/base/component/menu.rs
@@ -0,0 +1,17 @@
+mod menu_main;
+pub use menu_main::Menu;
+
+mod item;
+pub use item::{Item, ItemType};
+
+mod submenu;
+pub use submenu::Submenu;
+
+mod megamenu;
+pub use megamenu::Megamenu;
+
+mod group;
+pub use group::Group;
+
+mod element;
+pub use element::{Element, ElementType};
diff --git a/src/base/component/menu/element.rs b/src/base/component/menu/element.rs
new file mode 100644
index 00000000..5c39f7ea
--- /dev/null
+++ b/src/base/component/menu/element.rs
@@ -0,0 +1,60 @@
+use crate::prelude::*;
+
+use super::Submenu;
+
+type Content = TypedComponent;
+type SubmenuItems = TypedComponent;
+
+#[derive(AutoDefault)]
+pub enum ElementType {
+ #[default]
+ Void,
+ Html(Content),
+ Submenu(SubmenuItems),
+}
+
+// Element.
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Element {
+ element_type: ElementType,
+}
+
+impl ComponentTrait for Element {
+ fn new() -> Self {
+ Element::default()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ match self.element_type() {
+ ElementType::Void => PrepareMarkup::None,
+ ElementType::Html(content) => PrepareMarkup::With(html! {
+ (content.render(cx))
+ }),
+ ElementType::Submenu(submenu) => PrepareMarkup::With(html! {
+ (submenu.render(cx))
+ }),
+ }
+ }
+}
+
+impl Element {
+ pub fn html(content: Html) -> Self {
+ Element {
+ element_type: ElementType::Html(Content::with(content)),
+ }
+ }
+
+ pub fn submenu(submenu: Submenu) -> Self {
+ Element {
+ element_type: ElementType::Submenu(SubmenuItems::with(submenu)),
+ }
+ }
+
+ // Element GETTERS.
+
+ pub fn element_type(&self) -> &ElementType {
+ &self.element_type
+ }
+}
diff --git a/src/base/component/menu/group.rs b/src/base/component/menu/group.rs
new file mode 100644
index 00000000..d3b8a33d
--- /dev/null
+++ b/src/base/component/menu/group.rs
@@ -0,0 +1,56 @@
+use crate::prelude::*;
+
+use super::Element;
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Group {
+ id : OptionId,
+ elements: MixedComponents,
+}
+
+impl ComponentTrait for Group {
+ fn new() -> Self {
+ Group::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ PrepareMarkup::With(html! {
+ div id=[self.id()] class="menu-group" {
+ (self.elements().render(cx))
+ }
+ })
+ }
+}
+
+impl Group {
+ // Group BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_elements(&mut self, op: TypedOp) -> &mut Self {
+ self.elements.set_typed(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_element(mut self, element: Element) -> Self {
+ self.elements.set_value(AnyOp::Add(AnyComponent::with(element)));
+ self
+ }
+
+ // Group GETTERS.
+
+ pub fn elements(&self) -> &MixedComponents {
+ &self.elements
+ }
+}
diff --git a/src/base/component/menu/item.rs b/src/base/component/menu/item.rs
new file mode 100644
index 00000000..e59899b0
--- /dev/null
+++ b/src/base/component/menu/item.rs
@@ -0,0 +1,184 @@
+use crate::prelude::*;
+
+use super::{Megamenu, Submenu};
+
+type Label = L10n;
+type Content = TypedComponent;
+type SubmenuItems = TypedComponent;
+type MegamenuGroups = TypedComponent;
+
+#[derive(AutoDefault)]
+pub enum ItemType {
+ #[default]
+ Void,
+ Label(Label),
+ Link(Label, FnContextualPath),
+ LinkBlank(Label, FnContextualPath),
+ Html(Content),
+ Submenu(Label, SubmenuItems),
+ Megamenu(Label, MegamenuGroups),
+}
+
+// Item.
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Item {
+ item_type : ItemType,
+ description: OptionTranslated,
+ left_icon : OptionComponent,
+ right_icon : OptionComponent,
+}
+
+impl ComponentTrait for Item {
+ fn new() -> Self {
+ Item::default()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let description = self.description.using(cx.langid());
+
+ let left_icon = self.left_icon().render(cx);
+ let right_icon = self.right_icon().render(cx);
+
+ match self.item_type() {
+ ItemType::Void => PrepareMarkup::None,
+ ItemType::Label(label) => PrepareMarkup::With(html! {
+ li class="menu__label" {
+ span title=[description] {
+ (left_icon)
+ (label.escaped(cx.langid()))
+ (right_icon)
+ }
+ }
+ }),
+ ItemType::Link(label, path) => PrepareMarkup::With(html! {
+ li class="menu__link" {
+ a href=(path(cx)) title=[description] {
+ (left_icon)
+ (label.escaped(cx.langid()))
+ (right_icon)
+ }
+ }
+ }),
+ ItemType::LinkBlank(label, path) => PrepareMarkup::With(html! {
+ li class="menu__link" {
+ a href=(path(cx)) title=[description] target="_blank" {
+ (left_icon)
+ (label.escaped(cx.langid()))
+ (right_icon)
+ }
+ }
+ }),
+ ItemType::Html(content) => PrepareMarkup::With(html! {
+ li class="menu__html" {
+ (content.render(cx))
+ }
+ }),
+ ItemType::Submenu(label, submenu) => PrepareMarkup::With(html! {
+ li class="menu__children" {
+ a href="#" title=[description] {
+ (left_icon)
+ (label.escaped(cx.langid())) i class="menu__icon bi-chevron-down" {}
+ }
+ div class="menu__subs" {
+ (submenu.render(cx))
+ }
+ }
+ }),
+ ItemType::Megamenu(label, megamenu) => PrepareMarkup::With(html! {
+ li class="menu__children" {
+ a href="#" title=[description] {
+ (left_icon)
+ (label.escaped(cx.langid())) i class="menu__icon bi-chevron-down" {}
+ }
+ div class="menu__subs menu__mega" {
+ (megamenu.render(cx))
+ }
+ }
+ }),
+ }
+ }
+}
+
+impl Item {
+ pub fn label(label: L10n) -> Self {
+ Item {
+ item_type: ItemType::Label(label),
+ ..Default::default()
+ }
+ }
+
+ pub fn link(label: L10n, path: FnContextualPath) -> Self {
+ Item {
+ item_type: ItemType::Link(label, path),
+ ..Default::default()
+ }
+ }
+
+ pub fn link_blank(label: L10n, path: FnContextualPath) -> Self {
+ Item {
+ item_type: ItemType::LinkBlank(label, path),
+ ..Default::default()
+ }
+ }
+
+ pub fn html(content: Html) -> Self {
+ Item {
+ item_type: ItemType::Html(Content::with(content)),
+ ..Default::default()
+ }
+ }
+
+ pub fn submenu(label: L10n, submenu: Submenu) -> Self {
+ Item {
+ item_type: ItemType::Submenu(label, SubmenuItems::with(submenu)),
+ ..Default::default()
+ }
+ }
+
+ pub fn megamenu(label: L10n, megamenu: Megamenu) -> Self {
+ Item {
+ item_type: ItemType::Megamenu(label, MegamenuGroups::with(megamenu)),
+ ..Default::default()
+ }
+ }
+
+ // Item BUILDER.
+
+ #[fn_builder]
+ pub fn set_description(&mut self, text: L10n) -> &mut Self {
+ self.description.set_value(text);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_left_icon(&mut self, icon: Option) -> &mut Self {
+ self.left_icon.set_value(icon);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_right_icon(&mut self, icon: Option) -> &mut Self {
+ self.right_icon.set_value(icon);
+ self
+ }
+
+ // Item GETTERS.
+
+ pub fn item_type(&self) -> &ItemType {
+ &self.item_type
+ }
+
+ pub fn description(&self) -> &OptionTranslated {
+ &self.description
+ }
+
+ pub fn left_icon(&self) -> &OptionComponent {
+ &self.left_icon
+ }
+
+ pub fn right_icon(&self) -> &OptionComponent {
+ &self.right_icon
+ }
+}
diff --git a/src/base/component/menu/megamenu.rs b/src/base/component/menu/megamenu.rs
new file mode 100644
index 00000000..5a21ddd5
--- /dev/null
+++ b/src/base/component/menu/megamenu.rs
@@ -0,0 +1,56 @@
+use crate::prelude::*;
+
+use super::Group;
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Megamenu {
+ id : OptionId,
+ groups: MixedComponents,
+}
+
+impl ComponentTrait for Megamenu {
+ fn new() -> Self {
+ Megamenu::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ PrepareMarkup::With(html! {
+ div id=[self.id()] class="menu__groups" {
+ (self.groups().render(cx))
+ }
+ })
+ }
+}
+
+impl Megamenu {
+ // Megamenu BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_groups(&mut self, op: TypedOp) -> &mut Self {
+ self.groups.set_typed(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_group(mut self, group: Group) -> Self {
+ self.groups.set_value(AnyOp::Add(AnyComponent::with(group)));
+ self
+ }
+
+ // Megamenu GETTERS.
+
+ pub fn groups(&self) -> &MixedComponents {
+ &self.groups
+ }
+}
diff --git a/src/base/component/menu/menu_main.rs b/src/base/component/menu/menu_main.rs
new file mode 100644
index 00000000..94d51d8d
--- /dev/null
+++ b/src/base/component/menu/menu_main.rs
@@ -0,0 +1,84 @@
+use crate::prelude::*;
+
+use super::Item;
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Menu {
+ id : OptionId,
+ items: MixedComponents,
+}
+
+impl ComponentTrait for Menu {
+ fn new() -> Self {
+ Menu::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ cx.set_param::(PARAM_BASE_INCLUDE_MENU_ASSETS, &true);
+ cx.set_param::(PARAM_BASE_INCLUDE_ICONS, &true);
+
+ PrepareMarkup::With(html! {
+ div id=[self.id()] class="menu__container" {
+ div class="menu__content" {
+ div class="menu__main" {
+ div class="menu__overlay" {}
+ nav class="menu__nav" {
+ div class="menu__header" {
+ button type="button" class="menu__arrow" {
+ i class="bi-chevron-left" {}
+ }
+ div class="menu__title" {}
+ button type="button" class="menu__close" {
+ i class="bi-x" {}
+ }
+ }
+ ul class="menu__section" {
+ (self.items().render(cx))
+ }
+ }
+ }
+ button
+ type="button"
+ class="menu__trigger"
+ title=[L10n::l("menu_toggle").using(cx.langid())]
+ {
+ span {} span {} span {}
+ }
+ }
+ }
+ })
+ }
+}
+
+impl Menu {
+ // Menu BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_items(&mut self, op: TypedOp- ) -> &mut Self {
+ self.items.set_typed(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_item(mut self, item: Item) -> Self {
+ self.items.set_value(AnyOp::Add(AnyComponent::with(item)));
+ self
+ }
+
+ // Menu GETTERS.
+
+ pub fn items(&self) -> &MixedComponents {
+ &self.items
+ }
+}
diff --git a/src/base/component/menu/submenu.rs b/src/base/component/menu/submenu.rs
new file mode 100644
index 00000000..4e5e91df
--- /dev/null
+++ b/src/base/component/menu/submenu.rs
@@ -0,0 +1,72 @@
+use crate::prelude::*;
+
+use super::Item;
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct Submenu {
+ id : OptionId,
+ title: OptionTranslated,
+ items: MixedComponents,
+}
+
+impl ComponentTrait for Submenu {
+ fn new() -> Self {
+ Submenu::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ PrepareMarkup::With(html! {
+ div id=[self.id()] class="menu__items" {
+ @if let Some(title) = self.title().using(cx.langid()) {
+ h4 class="menu__title" { (title) }
+ }
+ ul {
+ (self.items().render(cx))
+ }
+ }
+ })
+ }
+}
+
+impl Submenu {
+ // Submenu BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_title(&mut self, title: L10n) -> &mut Self {
+ self.title.set_value(title);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_items(&mut self, op: TypedOp
- ) -> &mut Self {
+ self.items.set_typed(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_item(mut self, item: Item) -> Self {
+ self.items.set_value(AnyOp::Add(AnyComponent::with(item)));
+ self
+ }
+
+ // Submenu GETTERS.
+
+ pub fn title(&self) -> &OptionTranslated {
+ &self.title
+ }
+
+ pub fn items(&self) -> &MixedComponents {
+ &self.items
+ }
+}
diff --git a/src/base/component/paragraph.rs b/src/base/component/paragraph.rs
new file mode 100644
index 00000000..9beeefa8
--- /dev/null
+++ b/src/base/component/paragraph.rs
@@ -0,0 +1,81 @@
+use crate::prelude::*;
+
+#[rustfmt::skip]
+#[derive(AutoDefault, ComponentClasses)]
+pub struct Paragraph {
+ id : OptionId,
+ classes : OptionClasses,
+ font_size: FontSize,
+ mixed : MixedComponents,
+}
+
+impl ComponentTrait for Paragraph {
+ fn new() -> Self {
+ Paragraph::default()
+ }
+
+ fn id(&self) -> Option {
+ self.id.get()
+ }
+
+ fn setup_before_prepare(&mut self, _cx: &mut Context) {
+ self.set_classes(ClassesOp::Prepend, self.font_size().to_string());
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ PrepareMarkup::With(html! {
+ p
+ id=[self.id()]
+ class=[self.classes().get()]
+ {
+ (self.components().render(cx))
+ }
+ })
+ }
+}
+
+impl Paragraph {
+ pub fn with(component: impl ComponentTrait) -> Self {
+ Paragraph::default().add_component(component)
+ }
+
+ pub fn fluent(l10n: L10n) -> Self {
+ Paragraph::default().add_component(Fluent::with(l10n))
+ }
+
+ // Paragraph BUILDER.
+
+ #[fn_builder]
+ pub fn set_id(&mut self, id: impl Into) -> &mut Self {
+ self.id.set_value(id);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_font_size(&mut self, font_size: FontSize) -> &mut Self {
+ self.font_size = font_size;
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_components(&mut self, op: AnyOp) -> &mut Self {
+ self.mixed.set_value(op);
+ self
+ }
+
+ #[rustfmt::skip]
+ pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
+ self.mixed.set_value(AnyOp::Add(AnyComponent::with(component)));
+ self
+ }
+
+ // Paragraph GETTERS.
+
+ pub fn font_size(&self) -> &FontSize {
+ &self.font_size
+ }
+
+ pub fn components(&self) -> &MixedComponents {
+ &self.mixed
+ }
+}
diff --git a/src/base/component/powered_by.rs b/src/base/component/powered_by.rs
new file mode 100644
index 00000000..62c7b76b
--- /dev/null
+++ b/src/base/component/powered_by.rs
@@ -0,0 +1,108 @@
+use crate::prelude::*;
+
+use std::convert::Into;
+
+#[derive(Default, Eq, PartialEq)]
+pub enum PoweredByLogo {
+ #[default]
+ None,
+ Color,
+ LineDark,
+ LineLight,
+ LineRGB(u8, u8, u8),
+}
+
+#[rustfmt::skip]
+#[derive(AutoDefault)]
+pub struct PoweredBy {
+ copyright: Option,
+ logo : PoweredByLogo,
+}
+
+impl ComponentTrait for PoweredBy {
+ fn new() -> Self {
+ let year = Utc::now().format("%Y").to_string();
+ let c = concat_string!(year, " © ", global::SETTINGS.app.name);
+ PoweredBy {
+ copyright: Some(c),
+ ..Default::default()
+ }
+ }
+
+ fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
+ let poweredby_pagetop = L10n::l("poweredby_pagetop")
+ .with_arg(
+ "pagetop_link",
+ "PageTop",
+ )
+ .escaped(cx.langid());
+
+ let pagetop_logo = match self.logo() {
+ PoweredByLogo::None => html! {},
+ PoweredByLogo::Color => self.logo_color(cx),
+ PoweredByLogo::LineDark => self.logo_line(10, 11, 9, cx),
+ PoweredByLogo::LineLight => self.logo_line(255, 255, 255, cx),
+ PoweredByLogo::LineRGB(r, g, b) => self.logo_line(*r, *g, *b, cx),
+ };
+
+ PrepareMarkup::With(html! {
+ div id=[self.id()] class="poweredby__container" {
+ @if let Some(c) = self.copyright() {
+ span class="poweredby__copyright" { (c) "." } " "
+ }
+ span class="poweredby__pagetop" { (poweredby_pagetop) " " (pagetop_logo) }
+ }
+ })
+ }
+}
+
+impl PoweredBy {
+ // PoweredBy BUILDER.
+
+ #[fn_builder]
+ pub fn set_copyright(&mut self, copyright: Option>) -> &mut Self {
+ self.copyright = copyright.map(Into::into);
+ self
+ }
+
+ #[fn_builder]
+ pub fn set_logo(&mut self, logo: PoweredByLogo) -> &mut Self {
+ self.logo = logo;
+ self
+ }
+
+ // PoweredBy GETTERS.
+
+ pub fn copyright(&self) -> &Option {
+ &self.copyright
+ }
+
+ pub fn logo(&self) -> &PoweredByLogo {
+ &self.logo
+ }
+
+ // PoweredBy PRIVATE.
+
+ fn logo_color(&self, cx: &mut Context) -> Markup {
+ let logo_txt = &L10n::l("pagetop_logo").using(cx.langid());
+ html! {
+ span class="poweredby__logo" aria-label=[logo_txt] {
+ img src="/base/pagetop-logo.svg" alt=[logo_txt] {}
+ }
+ }
+ }
+
+ fn logo_line(&self, r: u8, g: u8, b: u8, cx: &mut Context) -> Markup {
+ let logo_txt = L10n::l("pagetop_logo").using(cx.langid());
+ let logo_rgb = format!("rgb({r},{g},{b})");
+ html! {
+ span class="poweredby__logo" aria-label=[logo_txt] {
+ svg viewBox="0 0 1614 1614" xmlns="http://www.w3.org/2000/svg" role="img" {
+ path fill=(logo_rgb) d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
+ path fill=(logo_rgb) d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
+ path fill=(logo_rgb) d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {}
+ }
+ }
+ }
+ }
+}
diff --git a/src/base/package.rs b/src/base/package.rs
index 7b633f33..1b0fa82b 100644
--- a/src/base/package.rs
+++ b/src/base/package.rs
@@ -1,133 +1,2 @@
-use crate::prelude::*;
-
-pub struct Welcome;
-
-impl PackageTrait for Welcome {
- fn name(&self) -> L10n {
- L10n::l("welcome_package_name")
- }
-
- fn description(&self) -> L10n {
- L10n::l("welcome_package_description")
- }
-
- fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
- scfg.route("/", service::web::get().to(homepage));
- }
-}
-
-async fn homepage(request: HttpRequest) -> ResultPage {
- Page::new(request)
- .with_title(L10n::l("welcome_page"))
- .with_assets(AssetsOp::Theme("Basic"))
- .with_assets(AssetsOp::AddStyleSheet(StyleSheet::inline("styles", r##"
- body {
- background-color: #f3d060;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- font-size: 20px;
- }
- .skip__to_content {
- display: none;
- }
- .wrapper {
- max-width: 1200px;
- width: 100%;
- margin: 0 auto;
- padding: 0;
- }
- .container {
- padding: 0 16px;
- }
- .title {
- font-size: clamp(3rem, 10vw, 10rem);
- letter-spacing: -0.05em;
- line-height: 1.2;
- margin: 0;
- }
- .subtitle {
- font-size: clamp(1.8rem, 2vw, 3rem);
- letter-spacing: -0.02em;
- line-height: 1.2;
- margin: 0;
- }
- .powered {
- margin: .5em 0 1em;
- }
- .box-container {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- align-items: stretch;
- gap: 1.5em;
- }
- .box {
- flex: 1 1 280px;
- border: 3px solid #25282a;
- box-shadow: 5px 5px 0px #25282a;
- box-sizing: border-box;
- padding: 0 16px;
- }
- footer {
- margin-top: 5em;
- font-size: 14px;
- font-weight: 500;
- color: #a5282c;
- }
- "##)))
- .with_component(Html::with(html! {
- div class="wrapper" {
- div class="container" {
- h1 class="title" { (L10n::l("welcome_title").markup()) }
-
- p class="subtitle" {
- (L10n::l("welcome_intro").with_arg("app", format!(
- "{}",
- &global::SETTINGS.app.name
- )).markup())
- }
- p class="powered" {
- (L10n::l("welcome_powered").with_arg("pagetop", format!(
- "{}",
- "https://crates.io/crates/pagetop", "PageTop"
- )).markup())
- }
-
- h2 { (L10n::l("welcome_page").markup()) }
-
- div class="box-container" {
- section class="box" style="background-color: #5eb0e5;" {
- h3 {
- (L10n::l("welcome_subtitle")
- .with_arg("app", &global::SETTINGS.app.name)
- .markup())
- }
- p { (L10n::l("welcome_text1").markup()) }
- p { (L10n::l("welcome_text2").markup()) }
- }
- section class="box" style="background-color: #aee1cd;" {
- h3 {
- (L10n::l("welcome_pagetop_title").markup())
- }
- p { (L10n::l("welcome_pagetop_text1").markup()) }
- p { (L10n::l("welcome_pagetop_text2").markup()) }
- p { (L10n::l("welcome_pagetop_text3").markup()) }
- }
- section class="box" style="background-color: #ebebe3;" {
- h3 {
- (L10n::l("welcome_issues_title").markup())
- }
- p { (L10n::l("welcome_issues_text1").markup()) }
- p {
- (L10n::l("welcome_issues_text2")
- .with_arg("app", &global::SETTINGS.app.name)
- .markup())
- }
- }
- }
-
- footer { "[ " (L10n::l("welcome_have_fun").markup()) " ]" }
- }
- }
- }))
- .render()
-}
+mod welcome;
+pub use welcome::Welcome;
diff --git a/src/base/package/welcome.rs b/src/base/package/welcome.rs
new file mode 100644
index 00000000..7b633f33
--- /dev/null
+++ b/src/base/package/welcome.rs
@@ -0,0 +1,133 @@
+use crate::prelude::*;
+
+pub struct Welcome;
+
+impl PackageTrait for Welcome {
+ fn name(&self) -> L10n {
+ L10n::l("welcome_package_name")
+ }
+
+ fn description(&self) -> L10n {
+ L10n::l("welcome_package_description")
+ }
+
+ fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
+ scfg.route("/", service::web::get().to(homepage));
+ }
+}
+
+async fn homepage(request: HttpRequest) -> ResultPage {
+ Page::new(request)
+ .with_title(L10n::l("welcome_page"))
+ .with_assets(AssetsOp::Theme("Basic"))
+ .with_assets(AssetsOp::AddStyleSheet(StyleSheet::inline("styles", r##"
+ body {
+ background-color: #f3d060;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ font-size: 20px;
+ }
+ .skip__to_content {
+ display: none;
+ }
+ .wrapper {
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 0;
+ }
+ .container {
+ padding: 0 16px;
+ }
+ .title {
+ font-size: clamp(3rem, 10vw, 10rem);
+ letter-spacing: -0.05em;
+ line-height: 1.2;
+ margin: 0;
+ }
+ .subtitle {
+ font-size: clamp(1.8rem, 2vw, 3rem);
+ letter-spacing: -0.02em;
+ line-height: 1.2;
+ margin: 0;
+ }
+ .powered {
+ margin: .5em 0 1em;
+ }
+ .box-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: stretch;
+ gap: 1.5em;
+ }
+ .box {
+ flex: 1 1 280px;
+ border: 3px solid #25282a;
+ box-shadow: 5px 5px 0px #25282a;
+ box-sizing: border-box;
+ padding: 0 16px;
+ }
+ footer {
+ margin-top: 5em;
+ font-size: 14px;
+ font-weight: 500;
+ color: #a5282c;
+ }
+ "##)))
+ .with_component(Html::with(html! {
+ div class="wrapper" {
+ div class="container" {
+ h1 class="title" { (L10n::l("welcome_title").markup()) }
+
+ p class="subtitle" {
+ (L10n::l("welcome_intro").with_arg("app", format!(
+ "{}",
+ &global::SETTINGS.app.name
+ )).markup())
+ }
+ p class="powered" {
+ (L10n::l("welcome_powered").with_arg("pagetop", format!(
+ "{}",
+ "https://crates.io/crates/pagetop", "PageTop"
+ )).markup())
+ }
+
+ h2 { (L10n::l("welcome_page").markup()) }
+
+ div class="box-container" {
+ section class="box" style="background-color: #5eb0e5;" {
+ h3 {
+ (L10n::l("welcome_subtitle")
+ .with_arg("app", &global::SETTINGS.app.name)
+ .markup())
+ }
+ p { (L10n::l("welcome_text1").markup()) }
+ p { (L10n::l("welcome_text2").markup()) }
+ }
+ section class="box" style="background-color: #aee1cd;" {
+ h3 {
+ (L10n::l("welcome_pagetop_title").markup())
+ }
+ p { (L10n::l("welcome_pagetop_text1").markup()) }
+ p { (L10n::l("welcome_pagetop_text2").markup()) }
+ p { (L10n::l("welcome_pagetop_text3").markup()) }
+ }
+ section class="box" style="background-color: #ebebe3;" {
+ h3 {
+ (L10n::l("welcome_issues_title").markup())
+ }
+ p { (L10n::l("welcome_issues_text1").markup()) }
+ p {
+ (L10n::l("welcome_issues_text2")
+ .with_arg("app", &global::SETTINGS.app.name)
+ .markup())
+ }
+ }
+ }
+
+ footer { "[ " (L10n::l("welcome_have_fun").markup()) " ]" }
+ }
+ }
+ }))
+ .render()
+}
diff --git a/src/base/theme.rs b/src/base/theme.rs
index c07ffc56..7b9b442f 100644
--- a/src/base/theme.rs
+++ b/src/base/theme.rs
@@ -1,11 +1,8 @@
-use crate::prelude::*;
+mod basic;
+pub use basic::Basic;
-pub struct Basic;
+mod chassis;
+pub use chassis::Chassis;
-impl PackageTrait for Basic {
- fn theme(&self) -> Option {
- Some(&Basic)
- }
-}
-
-impl ThemeTrait for Basic {}
+mod inception;
+pub use inception::Inception;
diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs
new file mode 100644
index 00000000..c07ffc56
--- /dev/null
+++ b/src/base/theme/basic.rs
@@ -0,0 +1,11 @@
+use crate::prelude::*;
+
+pub struct Basic;
+
+impl PackageTrait for Basic {
+ fn theme(&self) -> Option {
+ Some(&Basic)
+ }
+}
+
+impl ThemeTrait for Basic {}
diff --git a/src/base/theme/chassis.rs b/src/base/theme/chassis.rs
new file mode 100644
index 00000000..b59b1ce9
--- /dev/null
+++ b/src/base/theme/chassis.rs
@@ -0,0 +1,29 @@
+use crate::prelude::*;
+
+pub struct Chassis;
+
+impl PackageTrait for Chassis {
+ fn name(&self) -> L10n {
+ L10n::n("Chassis")
+ }
+
+ fn theme(&self) -> Option {
+ Some(&Chassis)
+ }
+}
+
+impl ThemeTrait for Chassis {
+ fn after_prepare_body(&self, page: &mut Page) {
+ page.set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/normalize.min.css")
+ .with_version("8.0.1")
+ .with_weight(-90),
+ ))
+ .set_assets(AssetsOp::AddBaseAssets)
+ .set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/chassis.css")
+ .with_version("0.0.1")
+ .with_weight(-90),
+ ));
+ }
+}
diff --git a/src/base/theme/inception.rs b/src/base/theme/inception.rs
new file mode 100644
index 00000000..13f257fc
--- /dev/null
+++ b/src/base/theme/inception.rs
@@ -0,0 +1,29 @@
+use crate::prelude::*;
+
+pub struct Inception;
+
+impl PackageTrait for Inception {
+ fn name(&self) -> L10n {
+ L10n::n("Inception")
+ }
+
+ fn theme(&self) -> Option {
+ Some(&Inception)
+ }
+}
+
+impl ThemeTrait for Inception {
+ fn after_prepare_body(&self, page: &mut Page) {
+ page.set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/normalize.min.css")
+ .with_version("8.0.1")
+ .with_weight(-90),
+ ))
+ .set_assets(AssetsOp::AddBaseAssets)
+ .set_assets(AssetsOp::AddStyleSheet(
+ StyleSheet::from("/base/css/inception.css")
+ .with_version("0.0.1")
+ .with_weight(-90),
+ ));
+ }
+}
diff --git a/src/core/component.rs b/src/core/component.rs
index 5a02ee0e..af116771 100644
--- a/src/core/component.rs
+++ b/src/core/component.rs
@@ -1,5 +1,5 @@
mod context;
-pub use context::{AssetsOp, Context, ErrorParam};
+pub use context::{AssetsOp, Context, ParamError};
pub type FnContextualPath = fn(cx: &Context) -> &str;
mod definition;
@@ -8,7 +8,7 @@ pub use definition::{ComponentBase, ComponentTrait};
mod classes;
pub use classes::{ComponentClasses, ComponentClassesOp};
-mod children;
-pub use children::Children;
-pub use children::{ChildComponent, ChildOp};
-pub use children::{TypedComponent, TypedOp};
+mod mixed;
+pub use mixed::MixedComponents;
+pub use mixed::{AnyComponent, AnyOp};
+pub use mixed::{TypedComponent, TypedOp};
diff --git a/src/core/component/context.rs b/src/core/component/context.rs
index 45bdcb87..439afb05 100644
--- a/src/core/component/context.rs
+++ b/src/core/component/context.rs
@@ -1,7 +1,8 @@
+use crate::base::component::add_base_assets;
use crate::concat_string;
-use crate::core::component::ChildOp;
+use crate::core::component::AnyOp;
use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME};
-use crate::core::theme::{ChildrenInRegions, ThemeRef};
+use crate::core::theme::{ComponentsInRegions, ThemeRef};
use crate::html::{html, Markup};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
@@ -27,24 +28,26 @@ pub enum AssetsOp {
// JavaScripts.
AddJavaScript(JavaScript),
RemoveJavaScript(&'static str),
+ // Add assets to properly use base components.
+ AddBaseAssets,
}
#[derive(Debug)]
-pub enum ErrorParam {
+pub enum ParamError {
NotFound,
ParseError(String),
}
-impl fmt::Display for ErrorParam {
+impl fmt::Display for ParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- ErrorParam::NotFound => write!(f, "Parameter not found"),
- ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"),
+ ParamError::NotFound => write!(f, "Parameter not found"),
+ ParamError::ParseError(e) => write!(f, "Parse error: {e}"),
}
}
}
-impl Error for ErrorParam {}
+impl Error for ParamError {}
#[rustfmt::skip]
pub struct Context {
@@ -55,7 +58,7 @@ pub struct Context {
favicon : Option,
stylesheet: Assets,
javascript: Assets,
- regions : ChildrenInRegions,
+ regions : ComponentsInRegions,
params : HashMap<&'static str, String>,
id_counter: usize,
}
@@ -71,7 +74,7 @@ impl Context {
favicon : None,
stylesheet: Assets::::new(),
javascript: Assets::::new(),
- regions : ChildrenInRegions::default(),
+ regions : ComponentsInRegions::default(),
params : HashMap::<&str, String>::new(),
id_counter: 0,
}
@@ -111,12 +114,16 @@ impl Context {
AssetsOp::RemoveJavaScript(path) => {
self.javascript.remove(path);
}
+ // Add assets to properly use base components.
+ AssetsOp::AddBaseAssets => {
+ add_base_assets(self);
+ }
}
self
}
- pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self {
- self.regions.set_in_region(region, op);
+ pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self {
+ self.regions.set_components(region, op);
self
}
@@ -143,32 +150,32 @@ impl Context {
self.layout
}
- pub fn regions(&self) -> &ChildrenInRegions {
+ pub fn regions(&self) -> &ComponentsInRegions {
&self.regions
}
- pub fn get_param(&self, key: &'static str) -> Result {
+ pub fn get_param(&self, key: &'static str) -> Result {
self.params
.get(key)
- .ok_or(ErrorParam::NotFound)
- .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
+ .ok_or(ParamError::NotFound)
+ .and_then(|v| T::from_str(v).map_err(|_| ParamError::ParseError(v.clone())))
}
- // Context RENDER.
+ // Context PREPARE.
- pub fn render_assets(&mut self) -> Markup {
+ pub(crate) fn prepare_assets(&mut self) -> Markup {
html! {
@if let Some(favicon) = &self.favicon {
- (favicon.render())
+ (favicon.prepare())
}
- (self.stylesheet.render())
- (self.javascript.render())
+ (self.stylesheet.prepare())
+ (self.javascript.prepare())
}
}
- pub fn render_region(&mut self, region: impl Into) -> Markup {
+ pub(crate) fn prepare_region(&mut self, region: impl Into) -> Markup {
self.regions
- .all_in_region(self.theme, ®ion.into())
+ .all_components(self.theme, region.into().as_str())
.render(self)
}
diff --git a/src/core/component/children.rs b/src/core/component/mixed.rs
similarity index 60%
rename from src/core/component/children.rs
rename to src/core/component/mixed.rs
index 9a5db05b..e93444bb 100644
--- a/src/core/component/children.rs
+++ b/src/core/component/mixed.rs
@@ -5,20 +5,20 @@ use crate::{fn_builder, TypeId};
use std::sync::{Arc, RwLock};
#[derive(Clone)]
-pub struct ChildComponent(Arc>);
+pub struct AnyComponent(Arc>);
-impl ChildComponent {
+impl AnyComponent {
pub fn with(component: impl ComponentTrait) -> Self {
- ChildComponent(Arc::new(RwLock::new(component)))
+ AnyComponent(Arc::new(RwLock::new(component)))
}
- // ChildComponent RENDER.
+ // AnyComponent RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
self.0.write().unwrap().render(cx)
}
- // ChildComponent HELPERS.
+ // AnyComponent HELPERS.
fn type_id(&self) -> TypeId {
self.0.read().unwrap().type_id()
@@ -52,20 +52,20 @@ impl TypedComponent {
// TypedComponent HELPERS.
- fn to_child(&self) -> ChildComponent {
- ChildComponent(self.0.clone())
+ fn to_any(&self) -> AnyComponent {
+ AnyComponent(self.0.clone())
}
}
// *************************************************************************************************
-pub enum ChildOp {
- Add(ChildComponent),
- InsertAfterId(&'static str, ChildComponent),
- InsertBeforeId(&'static str, ChildComponent),
- Prepend(ChildComponent),
+pub enum AnyOp {
+ Add(AnyComponent),
+ InsertAfterId(&'static str, AnyComponent),
+ InsertBeforeId(&'static str, AnyComponent),
+ Prepend(AnyComponent),
RemoveById(&'static str),
- ReplaceById(&'static str, ChildComponent),
+ ReplaceById(&'static str, AnyComponent),
Reset,
}
@@ -80,37 +80,37 @@ pub enum TypedOp {
}
#[derive(Clone, Default)]
-pub struct Children(Vec);
+pub struct MixedComponents(Vec);
-impl Children {
+impl MixedComponents {
pub fn new() -> Self {
- Children::default()
+ MixedComponents::default()
}
- pub fn with(child: ChildComponent) -> Self {
- Children::default().with_value(ChildOp::Add(child))
+ pub fn with(any: AnyComponent) -> Self {
+ MixedComponents::default().with_value(AnyOp::Add(any))
}
- pub(crate) fn merge(mixes: &[Option<&Children>]) -> Self {
- let mut opt = Children::default();
+ pub(crate) fn merge(mixes: &[Option<&MixedComponents>]) -> Self {
+ let mut opt = MixedComponents::default();
for m in mixes.iter().flatten() {
opt.0.append(&mut m.0.clone());
}
opt
}
- // Children BUILDER.
+ // MixedComponents BUILDER.
#[fn_builder]
- pub fn set_value(&mut self, op: ChildOp) -> &mut Self {
+ pub fn set_value(&mut self, op: AnyOp) -> &mut Self {
match op {
- ChildOp::Add(any) => self.add(any),
- ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any),
- ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any),
- ChildOp::Prepend(any) => self.prepend(any),
- ChildOp::RemoveById(id) => self.remove_by_id(id),
- ChildOp::ReplaceById(id, any) => self.replace_by_id(id, any),
- ChildOp::Reset => self.reset(),
+ AnyOp::Add(any) => self.add(any),
+ AnyOp::InsertAfterId(id, any) => self.insert_after_id(id, any),
+ AnyOp::InsertBeforeId(id, any) => self.insert_before_id(id, any),
+ AnyOp::Prepend(any) => self.prepend(any),
+ AnyOp::RemoveById(id) => self.remove_by_id(id),
+ AnyOp::ReplaceById(id, any) => self.replace_by_id(id, any),
+ AnyOp::Reset => self.reset(),
};
self
}
@@ -118,41 +118,41 @@ impl Children {
#[fn_builder]
pub fn set_typed(&mut self, op: TypedOp) -> &mut Self {
match op {
- TypedOp::Add(typed) => self.add(typed.to_child()),
- TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.to_child()),
- TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.to_child()),
- TypedOp::Prepend(typed) => self.prepend(typed.to_child()),
+ TypedOp::Add(typed) => self.add(typed.to_any()),
+ TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.to_any()),
+ TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.to_any()),
+ TypedOp::Prepend(typed) => self.prepend(typed.to_any()),
TypedOp::RemoveById(id) => self.remove_by_id(id),
- TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.to_child()),
+ TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.to_any()),
TypedOp::Reset => self.reset(),
};
self
}
#[inline]
- fn add(&mut self, child: ChildComponent) {
- self.0.push(child);
+ fn add(&mut self, any: AnyComponent) {
+ self.0.push(any);
}
#[inline]
- fn insert_after_id(&mut self, id: &str, child: ChildComponent) {
+ fn insert_after_id(&mut self, id: &str, any: AnyComponent) {
match self.0.iter().position(|c| c.id() == id) {
- Some(index) => self.0.insert(index + 1, child),
- _ => self.0.push(child),
+ Some(index) => self.0.insert(index + 1, any),
+ _ => self.0.push(any),
};
}
#[inline]
- fn insert_before_id(&mut self, id: &str, child: ChildComponent) {
+ fn insert_before_id(&mut self, id: &str, any: AnyComponent) {
match self.0.iter().position(|c| c.id() == id) {
- Some(index) => self.0.insert(index, child),
- _ => self.0.insert(0, child),
+ Some(index) => self.0.insert(index, any),
+ _ => self.0.insert(0, any),
};
}
#[inline]
- fn prepend(&mut self, child: ChildComponent) {
- self.0.insert(0, child);
+ fn prepend(&mut self, any: AnyComponent) {
+ self.0.insert(0, any);
}
#[inline]
@@ -163,10 +163,10 @@ impl Children {
}
#[inline]
- fn replace_by_id(&mut self, id: &str, child: ChildComponent) {
+ fn replace_by_id(&mut self, id: &str, any: AnyComponent) {
for c in &mut self.0 {
if c.id() == id {
- *c = child;
+ *c = any;
break;
}
}
@@ -177,7 +177,7 @@ impl Children {
self.0.clear();
}
- // Children GETTERS.
+ // MixedComponents GETTERS.
pub fn len(&self) -> usize {
self.0.len()
@@ -187,21 +187,21 @@ impl Children {
self.0.is_empty()
}
- pub fn get_by_id(&self, id: impl Into) -> Option<&ChildComponent> {
+ pub fn get_by_id(&self, id: impl Into) -> Option<&AnyComponent> {
let id = id.into();
self.0.iter().find(|c| c.id() == id)
}
- pub fn iter_by_id(&self, id: impl Into) -> impl Iterator
- {
+ pub fn iter_by_id(&self, id: impl Into) -> impl Iterator
- {
let id = id.into();
self.0.iter().filter(move |&c| c.id() == id)
}
- pub fn iter_by_type_id(&self, type_id: TypeId) -> impl Iterator
- {
+ pub fn iter_by_type_id(&self, type_id: TypeId) -> impl Iterator
- {
self.0.iter().filter(move |&c| c.type_id() == type_id)
}
- // Children RENDER.
+ // MixedComponents RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
html! {
diff --git a/src/core/package/all.rs b/src/core/package/all.rs
index 301d3a02..e41d62df 100644
--- a/src/core/package/all.rs
+++ b/src/core/package/all.rs
@@ -129,6 +129,8 @@ pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
m.configure_service(scfg);
}
include_files_service!(
- scfg, assets => "/", [&global::SETTINGS.dev.pagetop_project_dir, "static"]
+ scfg,
+ assets => "/",
+ [&global::SETTINGS.dev.pagetop_project_dir, "static/assets"]
);
}
diff --git a/src/core/theme.rs b/src/core/theme.rs
index adb99d59..1925071a 100644
--- a/src/core/theme.rs
+++ b/src/core/theme.rs
@@ -2,7 +2,7 @@ mod definition;
pub use definition::{ThemeRef, ThemeTrait};
mod regions;
-pub(crate) use regions::ChildrenInRegions;
+pub(crate) use regions::ComponentsInRegions;
pub use regions::InRegion;
pub(crate) mod all;
diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs
index 01e1cb04..6f474516 100644
--- a/src/core/theme/definition.rs
+++ b/src/core/theme/definition.rs
@@ -1,20 +1,80 @@
+use crate::base::component::*;
+use crate::core::component::{ComponentBase, ComponentTrait};
use crate::core::package::PackageTrait;
-use crate::global;
-use crate::html::{html, Markup};
+use crate::html::{html, PrepareMarkup};
use crate::locale::L10n;
use crate::response::page::Page;
+use crate::{concat_string, global};
pub type ThemeRef = &'static dyn ThemeTrait;
/// Los temas deben implementar este "trait".
pub trait ThemeTrait: PackageTrait + Send + Sync {
+ #[rustfmt::skip]
fn regions(&self) -> Vec<(&'static str, L10n)> {
- vec![("content", L10n::l("content"))]
+ vec![
+ ("header", L10n::l("header")),
+ ("pagetop", L10n::l("pagetop")),
+ ("sidebar_left", L10n::l("sidebar_left")),
+ ("content", L10n::l("content")),
+ ("sidebar_right", L10n::l("sidebar_right")),
+ ("footer", L10n::l("footer")),
+ ]
}
- fn render_head(&self, page: &mut Page) -> Markup {
+ #[allow(unused_variables)]
+ fn before_prepare_body(&self, page: &mut Page) {}
+
+ fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
+ let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
+
+ PrepareMarkup::With(html! {
+ body id=[page.body_id().get()] class=[page.body_classes().get()] {
+ @if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
+ div class="skip__to_content" {
+ a href=(concat_string!("#", skip_to_id)) { (skip) }
+ }
+ }
+ (flex::Container::new()
+ .with_id("body__wrapper")
+ .with_direction(flex::Direction::Column(BreakPoint::None))
+ .with_align(flex::Align::Center)
+ .add_item(flex::Item::region().with_id("header"))
+ .add_item(flex::Item::region().with_id("pagetop"))
+ .add_item(
+ flex::Item::with(
+ flex::Container::new()
+ .with_direction(flex::Direction::Row(BreakPoint::None))
+ .add_item(
+ flex::Item::region()
+ .with_id("sidebar_left")
+ .with_grow(flex::Grow::Is1),
+ )
+ .add_item(
+ flex::Item::region()
+ .with_id("content")
+ .with_grow(flex::Grow::Is3),
+ )
+ .add_item(
+ flex::Item::region()
+ .with_id("sidebar_right")
+ .with_grow(flex::Grow::Is1),
+ ),
+ )
+ .with_id("flex__wrapper"),
+ )
+ .add_item(flex::Item::region().with_id("footer"))
+ .render(page.context()))
+ }
+ })
+ }
+
+ #[allow(unused_variables)]
+ fn after_prepare_body(&self, page: &mut Page) {}
+
+ fn prepare_head(&self, page: &mut Page) -> PrepareMarkup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
- html! {
+ PrepareMarkup::With(html! {
head {
meta charset="utf-8";
@@ -38,22 +98,37 @@ pub trait ThemeTrait: PackageTrait + Send + Sync {
meta property=(property) content=(content) {}
}
- (page.context().render_assets())
+ (page.context().prepare_assets())
}
- }
+ })
}
-
- #[allow(unused_variables)]
- fn before_render_body(&self, page: &mut Page) {}
-
- fn render_body(&self, page: &mut Page) -> Markup {
- html! {
+ /*
+ fn prepare_page_body(&self, page: &mut Page) -> PrepareMarkup {
+ PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
- (page.context().render_region("content"))
+ (page.body_content().render())
}
- }
+ })
}
- #[allow(unused_variables)]
- fn after_render_body(&self, page: &mut Page) {}
+ fn error_403(&self, request: service::HttpRequest) -> Page {
+ Page::new(request)
+ .with_title(L10n::n("Error FORBIDDEN"))
+ .with_body(PrepareMarkup::With(html! {
+ div {
+ h1 { ("FORBIDDEN ACCESS") }
+ }
+ }))
+ }
+
+ fn error_404(&self, request: service::HttpRequest) -> Page {
+ Page::new(request)
+ .with_title(L10n::n("Error RESOURCE NOT FOUND"))
+ .with_body(PrepareMarkup::With(html! {
+ div {
+ h1 { ("RESOURCE NOT FOUND") }
+ }
+ }))
+ }
+ */
}
diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs
index 7e8128c5..da2e24ff 100644
--- a/src/core/theme/regions.rs
+++ b/src/core/theme/regions.rs
@@ -1,44 +1,40 @@
-use crate::core::component::{ChildComponent, ChildOp, Children};
+use crate::core::component::{AnyComponent, AnyOp, MixedComponents};
use crate::core::theme::ThemeRef;
use crate::{fn_builder, AutoDefault, TypeId};
use std::collections::HashMap;
use std::sync::{LazyLock, RwLock};
-static THEME_REGIONS: LazyLock>> =
+static THEME_REGIONS: LazyLock>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
-static COMMON_REGIONS: LazyLock> =
- LazyLock::new(|| RwLock::new(ChildrenInRegions::default()));
+static COMMON_REGIONS: LazyLock> =
+ LazyLock::new(|| RwLock::new(ComponentsInRegions::default()));
#[derive(AutoDefault)]
-pub struct ChildrenInRegions(HashMap<&'static str, Children>);
+pub struct ComponentsInRegions(HashMap<&'static str, MixedComponents>);
-impl ChildrenInRegions {
- pub fn new() -> Self {
- ChildrenInRegions::default()
- }
-
- pub fn with(region: &'static str, child: ChildComponent) -> Self {
- ChildrenInRegions::default().with_in_region(region, ChildOp::Add(child))
+impl ComponentsInRegions {
+ pub fn new(region: &'static str, any: AnyComponent) -> Self {
+ ComponentsInRegions::default().with_components(region, AnyOp::Add(any))
}
#[fn_builder]
- pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self {
+ pub fn set_components(&mut self, region: &'static str, op: AnyOp) -> &mut Self {
if let Some(region) = self.0.get_mut(region) {
region.set_value(op);
} else {
- self.0.insert(region, Children::new().with_value(op));
+ self.0.insert(region, MixedComponents::new().with_value(op));
}
self
}
- pub fn all_in_region(&self, theme: ThemeRef, region: &str) -> Children {
+ pub fn all_components(&self, theme: ThemeRef, region: &str) -> MixedComponents {
let common = COMMON_REGIONS.read().unwrap();
if let Some(r) = THEME_REGIONS.read().unwrap().get(&theme.type_id()) {
- Children::merge(&[common.0.get(region), self.0.get(region), r.0.get(region)])
+ MixedComponents::merge(&[common.0.get(region), self.0.get(region), r.0.get(region)])
} else {
- Children::merge(&[common.0.get(region), self.0.get(region)])
+ MixedComponents::merge(&[common.0.get(region), self.0.get(region)])
}
}
}
@@ -50,26 +46,26 @@ pub enum InRegion {
}
impl InRegion {
- pub fn add(&self, child: ChildComponent) -> &Self {
+ pub fn add(&self, any: AnyComponent) -> &Self {
match self {
InRegion::Content => {
COMMON_REGIONS
.write()
.unwrap()
- .set_in_region("content", ChildOp::Add(child));
+ .set_components("content", AnyOp::Add(any));
}
InRegion::Named(name) => {
COMMON_REGIONS
.write()
.unwrap()
- .set_in_region(name, ChildOp::Add(child));
+ .set_components(name, AnyOp::Add(any));
}
InRegion::OfTheme(region, theme) => {
let mut regions = THEME_REGIONS.write().unwrap();
if let Some(r) = regions.get_mut(&theme.type_id()) {
- r.set_in_region(region, ChildOp::Add(child));
+ r.set_components(region, AnyOp::Add(any));
} else {
- regions.insert(theme.type_id(), ChildrenInRegions::with(region, child));
+ regions.insert(theme.type_id(), ComponentsInRegions::new(region, any));
}
}
}
diff --git a/src/html/assets.rs b/src/html/assets.rs
index c8b8f1b9..4c8f27ce 100644
--- a/src/html/assets.rs
+++ b/src/html/assets.rs
@@ -10,7 +10,7 @@ pub trait AssetsTrait {
fn weight(&self) -> Weight;
- fn render(&self) -> Markup;
+ fn prepare(&self) -> Markup;
}
#[derive(AutoDefault)]
@@ -41,12 +41,12 @@ impl Assets {
self
}
- pub fn render(&mut self) -> Markup {
+ pub fn prepare(&mut self) -> Markup {
let assets = &mut self.0;
assets.sort_by_key(AssetsTrait::weight);
html! {
@for a in assets {
- (a.render())
+ (a.prepare())
}
}
}
diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs
index 366da4ff..068efcb4 100644
--- a/src/html/assets/favicon.rs
+++ b/src/html/assets/favicon.rs
@@ -83,7 +83,7 @@ impl Favicon {
// Favicon PREPARE.
- pub(crate) fn render(&self) -> Markup {
+ pub(crate) fn prepare(&self) -> Markup {
html! {
@for item in &self.0 {
(item)
diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs
index 76d84673..672ab3e0 100644
--- a/src/html/assets/javascript.rs
+++ b/src/html/assets/javascript.rs
@@ -36,7 +36,7 @@ impl AssetsTrait for JavaScript {
self.weight
}
- fn render(&self) -> Markup {
+ fn prepare(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) {};
diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs
index 5dd65a97..11dde4ef 100644
--- a/src/html/assets/stylesheet.rs
+++ b/src/html/assets/stylesheet.rs
@@ -38,7 +38,7 @@ impl AssetsTrait for StyleSheet {
self.weight
}
- fn render(&self) -> Markup {
+ fn prepare(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
link
diff --git a/src/lib.rs b/src/lib.rs
index a5369718..ad7b24f7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -53,7 +53,7 @@
//! ```
//! This program implements a package named `HelloWorld` with one service that returns a web page
//! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using
-//! the [default configuration settings](`global::Server`)). You can find this code in the `PageTop`
+//! the [default configuration settings](`config::Server`)). You can find this code in the `PageTop`
//! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples).
//!
//! # 🧩 Dependency Management
@@ -79,14 +79,22 @@ pub use concat_string::concat_string;
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
pub use paste::paste;
-pub use pagetop_macros::{fn_builder, html, main, test, AutoDefault, ComponentClasses};
+pub use pagetop_macros::{fn_builder, main, test, AutoDefault, ComponentClasses};
-pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;
+// *************************************************************************************************
+// GLOBAL.
+// *************************************************************************************************
+
+pub use static_files::Resource as StaticResource;
+
+pub type HashMapResources = std::collections::HashMap<&'static str, StaticResource>;
pub use std::any::TypeId;
pub type Weight = i8;
+include_locales!(LOCALES_PAGETOP);
+
// API *********************************************************************************************
// Useful functions and macros.
diff --git a/src/locale.rs b/src/locale.rs
index 6cc79c9f..513c7f70 100644
--- a/src/locale.rs
+++ b/src/locale.rs
@@ -1,19 +1,21 @@
//! Localization (L10n).
//!
-//! PageTop uses the [Fluent](https://www.projectfluent.org/) specifications for application
-//! localization, leveraging the [fluent-templates](https://docs.rs/fluent-templates/) crate to
-//! integrate translation resources directly into the application binary.
+//! PageTop uses the [Fluent](https://www.projectfluent.org/) set of specifications for application
+//! localization.
//!
//! # Fluent Syntax (FTL)
//!
//! The format used to describe the translation resources used by Fluent is called
-//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be both readable and
-//! expressive, enabling complex natural language constructs like gender, plurals, and conjugations.
+//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be easy to read while
+//! simultaneously allowing the representation of complex natural language concepts to address
+//! gender, plurals, conjugations, and others.
//!
//! # Fluent Resources
//!
-//! Localization resources are organized in the *src/locale* directory, with subdirectories for
-//! each valid [Unicode Language Identifier](https://docs.rs/unic-langid/):
+//! PageTop utilizes [fluent-templates](https://docs.rs/fluent-templates/) to integrate localization
+//! resources into the application binary. The following example groups files and subfolders from
+//! *src/locale* that have a valid [Unicode Language Identifier](https://docs.rs/unic-langid/) and
+//! assigns them to their corresponding identifier:
//!
//! ```text
//! src/locale/
@@ -67,13 +69,13 @@
//! # How to apply localization in your code
//!
//! Once you have created your FTL resource directory, use the
-//! [`include_locales!`](crate::include_locales) macro to integrate them into your module or
+//! [`static_locales!`](crate::static_locales) macro to integrate them into your module or
//! application. If your resources are located in the `"src/locale"` directory, simply declare:
//!
//! ```
//! use pagetop::prelude::*;
//!
-//! include_locales!(LOCALES_SAMPLE);
+//! static_locales!(LOCALES_SAMPLE);
//! ```
//!
//! But if they are in another directory, then you can use:
@@ -81,14 +83,14 @@
//! ```
//! use pagetop::prelude::*;
//!
-//! include_locales!(LOCALES_SAMPLE from "path/to/locale");
+//! static_locales!(LOCALES_SAMPLE in "path/to/locale");
//! ```
use crate::html::{Markup, PreEscaped};
-use crate::{global, kv, AutoDefault};
+use crate::{global, kv, AutoDefault, LOCALES_PAGETOP};
pub use fluent_templates;
-pub use unic_langid::{CharacterDirection, LanguageIdentifier};
+pub use unic_langid::LanguageIdentifier;
use fluent_templates::Loader;
use fluent_templates::StaticLoader as Locales;
@@ -100,61 +102,45 @@ use std::sync::LazyLock;
use std::fmt;
-/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
-/// and locale key names.
+const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
+
static LANGUAGES: LazyLock> = LazyLock::new(|| {
kv![
- "en" => ( langid!("en-US"), "english" ),
- "en-GB" => ( langid!("en-GB"), "english_british" ),
- "en-US" => ( langid!("en-US"), "english_united_states" ),
- "es" => ( langid!("es-ES"), "spanish" ),
- "es-ES" => ( langid!("es-ES"), "spanish_spain" ),
+ "en" => (langid!("en-US"), "English"),
+ "en-GB" => (langid!("en-GB"), "English (British)"),
+ "en-US" => (langid!("en-US"), "English (United States)"),
+ "es" => (langid!("es-ES"), "Spanish"),
+ "es-ES" => (langid!("es-ES"), "Spanish (Spain)"),
]
});
-pub static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!("en-US"));
+static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!("en-US"));
/// Sets the application's default
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
/// through `SETTINGS.app.language`.
-pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
- LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&FALLBACK_LANGID));
+pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = LazyLock::new(|| {
+ langid_for(global::SETTINGS.app.language.as_str()).unwrap_or(&FALLBACK_LANGID)
+});
-pub enum LangError {
- EmptyLang,
- UnknownLang(String),
-}
-
-impl fmt::Display for LangError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- LangError::EmptyLang => write!(f, "The language identifier is empty."),
- LangError::UnknownLang(lang) => write!(f, "Unknown language identifier: {lang}"),
- }
- }
-}
-
-pub fn langid_for(language: impl Into) -> Result<&'static LanguageIdentifier, LangError> {
+pub fn langid_for(language: impl Into) -> Result<&'static LanguageIdentifier, String> {
let language = language.into();
- if language.is_empty() {
- return Err(LangError::EmptyLang);
- }
- // Attempt to match the full language code (e.g., "es-MX").
- if let Some(langid) = LANGUAGES.get(&language).map(|(langid, _)| langid) {
- return Ok(langid);
- }
- // Fallback to the base language if no sublocale is found (e.g., "es").
- if let Some((base_lang, _)) = language.split_once('-') {
- if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) {
- return Ok(langid);
+ match LANGUAGES.get(language.as_str()) {
+ Some((langid, _)) => Ok(langid),
+ None => {
+ if language.is_empty() {
+ Ok(&FALLBACK_LANGID)
+ } else {
+ Err(format!(
+ "No langid for Unicode Language Identifier \"{language}\".",
+ ))
+ }
}
}
- Err(LangError::UnknownLang(language))
}
#[macro_export]
-/// Defines a set of localization elements and local translation texts, removing Unicode isolating
-/// marks around arguments to improve readability and compatibility in certain rendering contexts.
+/// Defines a set of localization elements and local translation texts.
macro_rules! include_locales {
( $LOCALES:ident $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
@@ -167,7 +153,7 @@ macro_rules! include_locales {
};
}
};
- ( $LOCALES:ident from $dir_locales:literal $(, $core_locales:literal)? ) => {
+ ( $LOCALES:ident in $dir_locales:literal $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
static $LOCALES = {
locales: $dir_locales,
@@ -180,8 +166,6 @@ macro_rules! include_locales {
};
}
-include_locales!(LOCALES_PAGETOP);
-
#[derive(AutoDefault)]
enum L10nOp {
#[default]
@@ -193,8 +177,7 @@ enum L10nOp {
#[derive(AutoDefault)]
pub struct L10n {
op: L10nOp,
- #[default(&LOCALES_PAGETOP)]
- locales: &'static Locales,
+ locales: Option<&'static Locales>,
args: HashMap,
}
@@ -209,6 +192,7 @@ impl L10n {
pub fn l(key: impl Into) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
+ locales: Some(&LOCALES_PAGETOP),
..Default::default()
}
}
@@ -216,7 +200,7 @@ impl L10n {
pub fn t(key: impl Into, locales: &'static Locales) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
- locales,
+ locales: Some(locales),
..Default::default()
}
}
@@ -226,44 +210,37 @@ impl L10n {
self
}
- pub fn with_args(mut self, args: HashMap) -> Self {
- for (k, v) in args {
- self.args.insert(k, v);
- }
- self
- }
-
- pub fn get(&self) -> Option {
- self.using(&DEFAULT_LANGID)
- }
-
pub fn using(&self, langid: &LanguageIdentifier) -> Option {
match &self.op {
L10nOp::None => None,
L10nOp::Text(text) => Some(text.to_owned()),
- L10nOp::Translate(key) => {
- if self.args.is_empty() {
- self.locales.try_lookup(langid, key)
- } else {
- self.locales.try_lookup_with_args(
- langid,
- key,
- &self.args.iter().fold(HashMap::new(), |mut args, (k, v)| {
- args.insert(k.to_string(), v.to_owned().into());
- args
- }),
- )
+ L10nOp::Translate(key) => match self.locales {
+ Some(locales) => {
+ if self.args.is_empty() {
+ locales.try_lookup(langid, key)
+ } else {
+ locales.try_lookup_with_args(
+ langid,
+ key,
+ &self
+ .args
+ .iter()
+ .fold(HashMap::new(), |mut args, (key, value)| {
+ args.insert(key.to_string(), value.to_owned().into());
+ args
+ }),
+ )
+ }
}
- }
+ None => None,
+ },
}
}
- /// Escapes translated text using the default language identifier.
pub fn markup(&self) -> Markup {
- PreEscaped(self.get().unwrap_or_default())
+ PreEscaped(self.using(&DEFAULT_LANGID).unwrap_or_default())
}
- /// Escapes translated text using the specified language identifier.
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
PreEscaped(self.using(langid).unwrap_or_default())
}
@@ -271,11 +248,43 @@ impl L10n {
impl fmt::Display for L10n {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let content = match &self.op {
- L10nOp::None => "".to_string(),
- L10nOp::Text(text) => text.clone(),
- L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("No <{}>", key)),
- };
- write!(f, "{content}")
+ match &self.op {
+ L10nOp::None => write!(f, ""),
+ L10nOp::Text(text) => write!(f, "{text}"),
+ L10nOp::Translate(key) => {
+ if let Some(locales) = self.locales {
+ write!(
+ f,
+ "{}",
+ if self.args.is_empty() {
+ locales.lookup(
+ match key.as_str() {
+ LANGUAGE_SET_FAILURE => &FALLBACK_LANGID,
+ _ => &DEFAULT_LANGID,
+ },
+ key,
+ )
+ } else {
+ locales.lookup_with_args(
+ match key.as_str() {
+ LANGUAGE_SET_FAILURE => &FALLBACK_LANGID,
+ _ => &DEFAULT_LANGID,
+ },
+ key,
+ &self
+ .args
+ .iter()
+ .fold(HashMap::new(), |mut args, (key, value)| {
+ args.insert(key.to_string(), value.to_owned().into());
+ args
+ }),
+ )
+ }
+ )
+ } else {
+ write!(f, "Unknown localization {key}")
+ }
+ }
+ }
}
}
diff --git a/src/locale/en-US/base.ftl b/src/locale/en-US/base.ftl
new file mode 100644
index 00000000..9ec4803b
--- /dev/null
+++ b/src/locale/en-US/base.ftl
@@ -0,0 +1,13 @@
+# Branding component.
+site_home = Home
+
+# PoweredBy component.
+poweredby_pagetop = Powered by {$pagetop_link}
+pagetop_logo = PageTop logo
+
+# Menu component.
+menu_toggle = Toggle menu visibility
+
+# Form components.
+button_submit = Submit
+button_reset = Reset
diff --git a/src/locale/en-US/theme.ftl b/src/locale/en-US/theme.ftl
index fd7f228d..6b3cb0e8 100644
--- a/src/locale/en-US/theme.ftl
+++ b/src/locale/en-US/theme.ftl
@@ -1 +1,8 @@
+header = Header
+pagetop = Page Top
content = Content
+sidebar_left = Sidebar Left
+sidebar_right = Sidebar Right
+footer = Footer
+
+skip_to_content = Skip to main content (Press Enter)
diff --git a/src/locale/es-ES/base.ftl b/src/locale/es-ES/base.ftl
new file mode 100644
index 00000000..953d891c
--- /dev/null
+++ b/src/locale/es-ES/base.ftl
@@ -0,0 +1,13 @@
+# Branding component.
+site_home = Inicio
+
+# PoweredBy component.
+poweredby_pagetop = Funciona con {$pagetop_link}
+pagetop_logo = Logotipo de PageTop
+
+# Menu component.
+menu_toggle = Alternar visibilidad del menú
+
+# Form components.
+button_submit = Enviar
+button_reset = Reiniciar
diff --git a/src/locale/es-ES/theme.ftl b/src/locale/es-ES/theme.ftl
index c2026c6f..fb5caacd 100644
--- a/src/locale/es-ES/theme.ftl
+++ b/src/locale/es-ES/theme.ftl
@@ -1 +1,8 @@
+header = Cabecera
+pagetop = Superior
content = Contenido
+sidebar_left = Barra lateral izquierda
+sidebar_right = Barra lateral derecha
+footer = Pie
+
+skip_to_content = Ir al contenido principal (Pulsar Intro)
diff --git a/src/prelude.rs b/src/prelude.rs
index 9d0d465f..1a3efc92 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -4,7 +4,7 @@
pub use crate::{concat_string, fn_builder, html, main, paste, test};
-pub use crate::{AutoDefault, ComponentClasses, StaticResources, TypeId, Weight};
+pub use crate::{AutoDefault, ComponentClasses, HashMapResources, TypeId, Weight};
// MACROS.
diff --git a/src/response/page.rs b/src/response/page.rs
index 340bd0c7..c3e268b5 100644
--- a/src/response/page.rs
+++ b/src/response/page.rs
@@ -4,8 +4,8 @@ pub use error::ErrorPage;
pub use actix_web::Result as ResultPage;
use crate::base::action;
+use crate::core::component::{AnyComponent, AnyOp, ComponentTrait};
use crate::core::component::{AssetsOp, Context};
-use crate::core::component::{ChildComponent, ChildOp, ComponentTrait};
use crate::fn_builder;
use crate::html::{html, Markup, DOCTYPE};
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
@@ -98,14 +98,14 @@ impl Page {
}
#[fn_builder]
- pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self {
- self.context.set_in_region(region, op);
+ pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self {
+ self.context.set_regions(region, op);
self
}
pub fn with_component(mut self, component: impl ComponentTrait) -> Self {
self.context
- .set_in_region("content", ChildOp::Add(ChildComponent::with(component)));
+ .set_regions("content", AnyOp::Add(AnyComponent::with(component)));
self
}
@@ -115,7 +115,7 @@ impl Page {
component: impl ComponentTrait,
) -> Self {
self.context
- .set_in_region(region, ChildOp::Add(ChildComponent::with(component)));
+ .set_regions(region, AnyOp::Add(AnyComponent::with(component)));
self
}
@@ -156,26 +156,26 @@ impl Page {
// Page RENDER.
pub fn render(&mut self) -> ResultPage {
- // Theme-specific operations before rendering the page body.
- self.context.theme().before_render_body(self);
+ // Theme operations before preparing the page body.
+ self.context.theme().before_prepare_body(self);
- // Execute package actions before rendering the page body.
- action::page::BeforeRenderBody::dispatch(self);
+ // Packages actions before preparing the page body.
+ action::page::BeforePrepareBody::dispatch(self);
- // Render the page body.
- let body = self.context.theme().render_body(self);
+ // Prepare page body.
+ let body = self.context.theme().prepare_body(self);
- // Theme-specific operations after rendering the page body.
- self.context.theme().after_render_body(self);
+ // Theme operations after preparing the page body.
+ self.context.theme().after_prepare_body(self);
- // Execute package actions after rendering the page body.
- action::page::AfterRenderBody::dispatch(self);
+ // Packages actions after preparing the page body.
+ action::page::AfterPrepareBody::dispatch(self);
- // Render the page head.
- let head = self.context.theme().render_head(self);
+ // Prepare page head.
+ let head = self.context.theme().prepare_head(self);
- // Render the full page with language and direction attributes.
- let lang = &self.context.langid().language;
+ // Render the page.
+ let lang = self.context.langid().language.as_str();
let dir = match self.context.langid().character_direction() {
CharacterDirection::LTR => "ltr",
CharacterDirection::RTL => "rtl",
@@ -184,8 +184,8 @@ impl Page {
Ok(html! {
(DOCTYPE)
html lang=(lang) dir=(dir) {
- (head)
- (body)
+ (head.render())
+ (body.render())
}
})
}
diff --git a/static/favicon.ico b/static/assets/favicon.ico
similarity index 100%
rename from static/favicon.ico
rename to static/assets/favicon.ico
diff --git a/static/base/css/basic.css b/static/base/css/basic.css
new file mode 100644
index 00000000..caccc2c9
--- /dev/null
+++ b/static/base/css/basic.css
@@ -0,0 +1,3 @@
+.skip__to_content {
+ display: none;
+}
diff --git a/static/base/css/buttons.css b/static/base/css/buttons.css
new file mode 100644
index 00000000..b53588d6
--- /dev/null
+++ b/static/base/css/buttons.css
@@ -0,0 +1,1265 @@
+.button__tap {
+ cursor: pointer;
+ text-align: center;
+ display: inline-block;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: var(--val-border-radius);
+ padding: var(--val-gap-0-35) var(--val-gap-0-75);
+ transition: background-color .15s ease-in-out;
+ white-space: nowrap;
+ user-select: none;
+}
+.button__tap > span {
+ margin: 0 var(--val-gap-0-5);
+}
+
+.button__tap.style__default {
+ color: var(--val-color--white);
+ background-color: var(--val-color--primary);
+}
+.button__tap.style__default:hover {
+ color: var(--val-color--white);
+ background-color: var(--val-color--primary-dark);
+}
+.button__tap.style__default:active,
+.button__tap.style__default:disabled {
+ color: var(--val-color--white);
+ background-color: var(--val-color--primary-light);
+}
+
+.button__tap.style__info:hover {
+ color: var(--val-color--white);
+ background-color: var(--val-color--info-dark);
+}
+.button__tap.style__info:active,
+.button__tap.style__info:disabled {
+ color: var(--val-color--white);
+ background-color: var(--val-color--info-light);
+}
+
+.button__tap.style__success:hover {
+ color: var(--val-color--white);
+ background-color: var(--val-color--success-dark);
+}
+.button__tap.style__success:active,
+.button__tap.style__success:disabled {
+ color: var(--val-color--white);
+ background-color: var(--val-color--success-light);
+}
+
+.button__tap.style__warning:hover {
+ color: var(--val-color--white);
+ background-color: var(--val-color--warning-dark);
+}
+.button__tap.style__warning:active,
+.button__tap.style__warning:disabled {
+ color: var(--val-color--white);
+ background-color: var(--val-color--warning-light);
+}
+
+.button__tap.style__danger:hover {
+ color: var(--val-color--white);
+ background-color: var(--val-color--danger-dark);
+}
+.button__tap.style__danger:active,
+.button__tap.style__danger:disabled {
+ color: var(--val-color--white);
+ background-color: var(--val-color--danger-light);
+}
+
+.button__tap.style__light:hover {
+ color: var(--val-color--text);
+ background-color: var(--val-color--light-dark);
+}
+.button__tap.style__light:active,
+.button__tap.style__light:disabled {
+ color: var(--val-color--text);
+ background-color: var(--val-color--light-light);
+}
+
+.button__tap.style__dark:hover {
+ color: var(--val-color--white);
+ background-color: var(--val-color--dark-dark);
+}
+.button__tap.style__dark:active,
+.button__tap.style__dark:disabled {
+ color: var(--val-color--white);
+ background-color: var(--val-color--dark-light);
+}
+
+/*
+.button strong {
+ color: inherit
+}
+
+.button .icon,.button .icon.is-large,.button .icon.is-medium,.button .icon.is-small {
+ height: 1.5em;
+ width: 1.5em
+}
+
+.button .icon:first-child:not(:last-child) {
+ margin-left: calc(-.5em - 1px);
+ margin-right: .25em
+}
+
+.button .icon:last-child:not(:first-child) {
+ margin-left: .25em;
+ margin-right: calc(-.5em - 1px)
+}
+
+.button .icon:first-child:last-child {
+ margin-left: calc(-.5em - 1px);
+ margin-right: calc(-.5em - 1px)
+}
+
+.button__tap.is-hovered,.button:hover {
+ border-color: #b5b5b5;
+ color: #363636
+}
+
+.button__tap.is-focused,.button:focus {
+ border-color: #485fc7;
+ color: #363636
+}
+
+.button__tap.is-focused:not(:active),.button:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(72,95,199,.25)
+}
+
+.button__tap.is-active,.button:active {
+ border-color: #4a4a4a;
+ color: #363636
+}
+
+.button__tap.is-text {
+ background-color: transparent;
+ border-color: transparent;
+ color: #4a4a4a;
+ text-decoration: underline
+}
+
+.button__tap.is-text.is-focused,.button__tap.is-text.is-hovered,.button__tap.is-text:focus,.button__tap.is-text:hover {
+ background-color: #f5f5f5;
+ color: #363636
+}
+
+.button__tap.is-text.is-active,.button__tap.is-text:active {
+ background-color: #e8e8e8;
+ color: #363636
+}
+
+.button__tap.is-text[disabled],fieldset[disabled] .button__tap.is-text {
+ background-color: transparent;
+ border-color: transparent;
+ box-shadow: none
+}
+
+.button__tap.is-ghost {
+ background: 0 0;
+ border-color: transparent;
+ color: #485fc7;
+ text-decoration: none
+}
+
+.button__tap.is-ghost.is-hovered,.button__tap.is-ghost:hover {
+ color: #485fc7;
+ text-decoration: underline
+}
+
+.button__tap.is-white {
+ background-color: #fff;
+ border-color: transparent;
+ color: #0a0a0a
+}
+
+.button__tap.is-white.is-hovered,.button__tap.is-white:hover {
+ background-color: #f9f9f9;
+ border-color: transparent;
+ color: #0a0a0a
+}
+
+.button__tap.is-white.is-focused,.button__tap.is-white:focus {
+ border-color: transparent;
+ color: #0a0a0a
+}
+
+.button__tap.is-white.is-focused:not(:active),.button__tap.is-white:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(255,255,255,.25)
+}
+
+.button__tap.is-white.is-active,.button__tap.is-white:active {
+ background-color: #f2f2f2;
+ border-color: transparent;
+ color: #0a0a0a
+}
+
+.button__tap.is-white[disabled],fieldset[disabled] .button__tap.is-white {
+ background-color: #fff;
+ border-color: #fff;
+ box-shadow: none
+}
+
+.button__tap.is-white.is-inverted {
+ background-color: #0a0a0a;
+ color: #fff
+}
+
+.button__tap.is-white.is-inverted.is-hovered,.button__tap.is-white.is-inverted:hover {
+ background-color: #000
+}
+
+.button__tap.is-white.is-inverted[disabled],fieldset[disabled] .button__tap.is-white.is-inverted {
+ background-color: #0a0a0a;
+ border-color: transparent;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-white.is-loading::after {
+ border-color: transparent transparent #0a0a0a #0a0a0a!important
+}
+
+.button__tap.is-white.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-white.is-outlined.is-focused,.button__tap.is-white.is-outlined.is-hovered,.button__tap.is-white.is-outlined:focus,.button__tap.is-white.is-outlined:hover {
+ background-color: #fff;
+ border-color: #fff;
+ color: #0a0a0a
+}
+
+.button__tap.is-white.is-outlined.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-white.is-outlined.is-loading.is-focused::after,.button__tap.is-white.is-outlined.is-loading.is-hovered::after,.button__tap.is-white.is-outlined.is-loading:focus::after,.button__tap.is-white.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #0a0a0a #0a0a0a!important
+}
+
+.button__tap.is-white.is-outlined[disabled],fieldset[disabled] .button__tap.is-white.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-white.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #0a0a0a;
+ color: #0a0a0a
+}
+
+.button__tap.is-white.is-inverted.is-outlined.is-focused,.button__tap.is-white.is-inverted.is-outlined.is-hovered,.button__tap.is-white.is-inverted.is-outlined:focus,.button__tap.is-white.is-inverted.is-outlined:hover {
+ background-color: #0a0a0a;
+ color: #fff
+}
+
+.button__tap.is-white.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-white.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-white.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-white.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-white.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-white.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #0a0a0a;
+ box-shadow: none;
+ color: #0a0a0a
+}
+
+.button__tap.is-black {
+ background-color: #0a0a0a;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-black.is-hovered,.button__tap.is-black:hover {
+ background-color: #040404;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-black.is-focused,.button__tap.is-black:focus {
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-black.is-focused:not(:active),.button__tap.is-black:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(10,10,10,.25)
+}
+
+.button__tap.is-black.is-active,.button__tap.is-black:active {
+ background-color: #000;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-black[disabled],fieldset[disabled] .button__tap.is-black {
+ background-color: #0a0a0a;
+ border-color: #0a0a0a;
+ box-shadow: none
+}
+
+.button__tap.is-black.is-inverted {
+ background-color: #fff;
+ color: #0a0a0a
+}
+
+.button__tap.is-black.is-inverted.is-hovered,.button__tap.is-black.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.button__tap.is-black.is-inverted[disabled],fieldset[disabled] .button__tap.is-black.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #0a0a0a
+}
+
+.button__tap.is-black.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-black.is-outlined {
+ background-color: transparent;
+ border-color: #0a0a0a;
+ color: #0a0a0a
+}
+
+.button__tap.is-black.is-outlined.is-focused,.button__tap.is-black.is-outlined.is-hovered,.button__tap.is-black.is-outlined:focus,.button__tap.is-black.is-outlined:hover {
+ background-color: #0a0a0a;
+ border-color: #0a0a0a;
+ color: #fff
+}
+
+.button__tap.is-black.is-outlined.is-loading::after {
+ border-color: transparent transparent #0a0a0a #0a0a0a!important
+}
+
+.button__tap.is-black.is-outlined.is-loading.is-focused::after,.button__tap.is-black.is-outlined.is-loading.is-hovered::after,.button__tap.is-black.is-outlined.is-loading:focus::after,.button__tap.is-black.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-black.is-outlined[disabled],fieldset[disabled] .button__tap.is-black.is-outlined {
+ background-color: transparent;
+ border-color: #0a0a0a;
+ box-shadow: none;
+ color: #0a0a0a
+}
+
+.button__tap.is-black.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-black.is-inverted.is-outlined.is-focused,.button__tap.is-black.is-inverted.is-outlined.is-hovered,.button__tap.is-black.is-inverted.is-outlined:focus,.button__tap.is-black.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #0a0a0a
+}
+
+.button__tap.is-black.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-black.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-black.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-black.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #0a0a0a #0a0a0a!important
+}
+
+.button__tap.is-black.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-black.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-light {
+ background-color: #f5f5f5;
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light.is-hovered,.button__tap.is-light:hover {
+ background-color: #eee;
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light.is-focused,.button__tap.is-light:focus {
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light.is-focused:not(:active),.button__tap.is-light:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(245,245,245,.25)
+}
+
+.button__tap.is-light.is-active,.button__tap.is-light:active {
+ background-color: #e8e8e8;
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light[disabled],fieldset[disabled] .button__tap.is-light {
+ background-color: #f5f5f5;
+ border-color: #f5f5f5;
+ box-shadow: none
+}
+
+.button__tap.is-light.is-inverted {
+ background-color: rgba(0,0,0,.7);
+ color: #f5f5f5
+}
+
+.button__tap.is-light.is-inverted.is-hovered,.button__tap.is-light.is-inverted:hover {
+ background-color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light.is-inverted[disabled],fieldset[disabled] .button__tap.is-light.is-inverted {
+ background-color: rgba(0,0,0,.7);
+ border-color: transparent;
+ box-shadow: none;
+ color: #f5f5f5
+}
+
+.button__tap.is-light.is-loading::after {
+ border-color: transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important
+}
+
+.button__tap.is-light.is-outlined {
+ background-color: transparent;
+ border-color: #f5f5f5;
+ color: #f5f5f5
+}
+
+.button__tap.is-light.is-outlined.is-focused,.button__tap.is-light.is-outlined.is-hovered,.button__tap.is-light.is-outlined:focus,.button__tap.is-light.is-outlined:hover {
+ background-color: #f5f5f5;
+ border-color: #f5f5f5;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light.is-outlined.is-loading::after {
+ border-color: transparent transparent #f5f5f5 #f5f5f5!important
+}
+
+.button__tap.is-light.is-outlined.is-loading.is-focused::after,.button__tap.is-light.is-outlined.is-loading.is-hovered::after,.button__tap.is-light.is-outlined.is-loading:focus::after,.button__tap.is-light.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important
+}
+
+.button__tap.is-light.is-outlined[disabled],fieldset[disabled] .button__tap.is-light.is-outlined {
+ background-color: transparent;
+ border-color: #f5f5f5;
+ box-shadow: none;
+ color: #f5f5f5
+}
+
+.button__tap.is-light.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: rgba(0,0,0,.7);
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-light.is-inverted.is-outlined.is-focused,.button__tap.is-light.is-inverted.is-outlined.is-hovered,.button__tap.is-light.is-inverted.is-outlined:focus,.button__tap.is-light.is-inverted.is-outlined:hover {
+ background-color: rgba(0,0,0,.7);
+ color: #f5f5f5
+}
+
+.button__tap.is-light.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-light.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-light.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-light.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #f5f5f5 #f5f5f5!important
+}
+
+.button__tap.is-light.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-light.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: rgba(0,0,0,.7);
+ box-shadow: none;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-dark {
+ background-color: #363636;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-dark.is-hovered,.button__tap.is-dark:hover {
+ background-color: #2f2f2f;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-dark.is-focused,.button__tap.is-dark:focus {
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-dark.is-focused:not(:active),.button__tap.is-dark:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(54,54,54,.25)
+}
+
+.button__tap.is-dark.is-active,.button__tap.is-dark:active {
+ background-color: #292929;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-dark[disabled],fieldset[disabled] .button__tap.is-dark {
+ background-color: #363636;
+ border-color: #363636;
+ box-shadow: none
+}
+
+.button__tap.is-dark.is-inverted {
+ background-color: #fff;
+ color: #363636
+}
+
+.button__tap.is-dark.is-inverted.is-hovered,.button__tap.is-dark.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.button__tap.is-dark.is-inverted[disabled],fieldset[disabled] .button__tap.is-dark.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #363636
+}
+
+.button__tap.is-dark.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-dark.is-outlined {
+ background-color: transparent;
+ border-color: #363636;
+ color: #363636
+}
+
+.button__tap.is-dark.is-outlined.is-focused,.button__tap.is-dark.is-outlined.is-hovered,.button__tap.is-dark.is-outlined:focus,.button__tap.is-dark.is-outlined:hover {
+ background-color: #363636;
+ border-color: #363636;
+ color: #fff
+}
+
+.button__tap.is-dark.is-outlined.is-loading::after {
+ border-color: transparent transparent #363636 #363636!important
+}
+
+.button__tap.is-dark.is-outlined.is-loading.is-focused::after,.button__tap.is-dark.is-outlined.is-loading.is-hovered::after,.button__tap.is-dark.is-outlined.is-loading:focus::after,.button__tap.is-dark.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-dark.is-outlined[disabled],fieldset[disabled] .button__tap.is-dark.is-outlined {
+ background-color: transparent;
+ border-color: #363636;
+ box-shadow: none;
+ color: #363636
+}
+
+.button__tap.is-dark.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-dark.is-inverted.is-outlined.is-focused,.button__tap.is-dark.is-inverted.is-outlined.is-hovered,.button__tap.is-dark.is-inverted.is-outlined:focus,.button__tap.is-dark.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #363636
+}
+
+.button__tap.is-dark.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-dark.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-dark.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-dark.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #363636 #363636!important
+}
+
+.button__tap.is-dark.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-dark.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+*/
+/*
+.style__default[disabled],fieldset[disabled] .style__default {
+ background-color: #00d1b2;
+ border-color: #00d1b2;
+ box-shadow: none
+}
+
+.style__default.is-inverted {
+ background-color: #fff;
+ color: #00d1b2
+}
+
+.style__default.is-inverted.is-hovered,.style__default.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.style__default.is-inverted[disabled],fieldset[disabled] .style__default.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #00d1b2
+}
+
+.style__default.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.style__default.is-outlined {
+ background-color: transparent;
+ border-color: #00d1b2;
+ color: #00d1b2
+}
+
+.style__default.is-outlined.is-focused,.style__default.is-outlined.is-hovered,.style__default.is-outlined:focus,.style__default.is-outlined:hover {
+ background-color: #00d1b2;
+ border-color: #00d1b2;
+ color: #fff
+}
+
+.style__default.is-outlined.is-loading::after {
+ border-color: transparent transparent #00d1b2 #00d1b2!important
+}
+
+.style__default.is-outlined.is-loading.is-focused::after,.style__default.is-outlined.is-loading.is-hovered::after,.style__default.is-outlined.is-loading:focus::after,.style__default.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.style__default.is-outlined[disabled],fieldset[disabled] .style__default.is-outlined {
+ background-color: transparent;
+ border-color: #00d1b2;
+ box-shadow: none;
+ color: #00d1b2
+}
+
+.style__default.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.style__default.is-inverted.is-outlined.is-focused,.style__default.is-inverted.is-outlined.is-hovered,.style__default.is-inverted.is-outlined:focus,.style__default.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #00d1b2
+}
+
+.style__default.is-inverted.is-outlined.is-loading.is-focused::after,.style__default.is-inverted.is-outlined.is-loading.is-hovered::after,.style__default.is-inverted.is-outlined.is-loading:focus::after,.style__default.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #00d1b2 #00d1b2!important
+}
+
+.style__default.is-inverted.is-outlined[disabled],fieldset[disabled] .style__default.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.style__default.is-light {
+ background-color: #ebfffc;
+ color: #00947e
+}
+
+.style__default.is-light.is-hovered,.style__default.is-light:hover {
+ background-color: #defffa;
+ border-color: transparent;
+ color: #00947e
+}
+
+.style__default.is-light.is-active,.style__default.is-light:active {
+ background-color: #d1fff8;
+ border-color: transparent;
+ color: #00947e
+}
+
+.button__tap.is-link {
+ background-color: #485fc7;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-link.is-hovered,.button__tap.is-link:hover {
+ background-color: #3e56c4;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-link.is-focused,.button__tap.is-link:focus {
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-link.is-focused:not(:active),.button__tap.is-link:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(72,95,199,.25)
+}
+
+.button__tap.is-link.is-active,.button__tap.is-link:active {
+ background-color: #3a51bb;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-link[disabled],fieldset[disabled] .button__tap.is-link {
+ background-color: #485fc7;
+ border-color: #485fc7;
+ box-shadow: none
+}
+
+.button__tap.is-link.is-inverted {
+ background-color: #fff;
+ color: #485fc7
+}
+
+.button__tap.is-link.is-inverted.is-hovered,.button__tap.is-link.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.button__tap.is-link.is-inverted[disabled],fieldset[disabled] .button__tap.is-link.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #485fc7
+}
+
+.button__tap.is-link.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-link.is-outlined {
+ background-color: transparent;
+ border-color: #485fc7;
+ color: #485fc7
+}
+
+.button__tap.is-link.is-outlined.is-focused,.button__tap.is-link.is-outlined.is-hovered,.button__tap.is-link.is-outlined:focus,.button__tap.is-link.is-outlined:hover {
+ background-color: #485fc7;
+ border-color: #485fc7;
+ color: #fff
+}
+
+.button__tap.is-link.is-outlined.is-loading::after {
+ border-color: transparent transparent #485fc7 #485fc7!important
+}
+
+.button__tap.is-link.is-outlined.is-loading.is-focused::after,.button__tap.is-link.is-outlined.is-loading.is-hovered::after,.button__tap.is-link.is-outlined.is-loading:focus::after,.button__tap.is-link.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-link.is-outlined[disabled],fieldset[disabled] .button__tap.is-link.is-outlined {
+ background-color: transparent;
+ border-color: #485fc7;
+ box-shadow: none;
+ color: #485fc7
+}
+
+.button__tap.is-link.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-link.is-inverted.is-outlined.is-focused,.button__tap.is-link.is-inverted.is-outlined.is-hovered,.button__tap.is-link.is-inverted.is-outlined:focus,.button__tap.is-link.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #485fc7
+}
+
+.button__tap.is-link.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-link.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-link.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-link.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #485fc7 #485fc7!important
+}
+
+.button__tap.is-link.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-link.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-link.is-light {
+ background-color: #eff1fa;
+ color: #3850b7
+}
+
+.button__tap.is-link.is-light.is-hovered,.button__tap.is-link.is-light:hover {
+ background-color: #e6e9f7;
+ border-color: transparent;
+ color: #3850b7
+}
+
+.button__tap.is-link.is-light.is-active,.button__tap.is-link.is-light:active {
+ background-color: #dce0f4;
+ border-color: transparent;
+ color: #3850b7
+}
+
+.button__tap.is-info {
+ background-color: #3e8ed0;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-info.is-hovered,.button__tap.is-info:hover {
+ background-color: #3488ce;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-info.is-focused,.button__tap.is-info:focus {
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-info.is-focused:not(:active),.button__tap.is-info:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(62,142,208,.25)
+}
+
+.button__tap.is-info.is-active,.button__tap.is-info:active {
+ background-color: #3082c5;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-info[disabled],fieldset[disabled] .button__tap.is-info {
+ background-color: #3e8ed0;
+ border-color: #3e8ed0;
+ box-shadow: none
+}
+
+.button__tap.is-info.is-inverted {
+ background-color: #fff;
+ color: #3e8ed0
+}
+
+.button__tap.is-info.is-inverted.is-hovered,.button__tap.is-info.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.button__tap.is-info.is-inverted[disabled],fieldset[disabled] .button__tap.is-info.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #3e8ed0
+}
+
+.button__tap.is-info.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-info.is-outlined {
+ background-color: transparent;
+ border-color: #3e8ed0;
+ color: #3e8ed0
+}
+
+.button__tap.is-info.is-outlined.is-focused,.button__tap.is-info.is-outlined.is-hovered,.button__tap.is-info.is-outlined:focus,.button__tap.is-info.is-outlined:hover {
+ background-color: #3e8ed0;
+ border-color: #3e8ed0;
+ color: #fff
+}
+
+.button__tap.is-info.is-outlined.is-loading::after {
+ border-color: transparent transparent #3e8ed0 #3e8ed0!important
+}
+
+.button__tap.is-info.is-outlined.is-loading.is-focused::after,.button__tap.is-info.is-outlined.is-loading.is-hovered::after,.button__tap.is-info.is-outlined.is-loading:focus::after,.button__tap.is-info.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-info.is-outlined[disabled],fieldset[disabled] .button__tap.is-info.is-outlined {
+ background-color: transparent;
+ border-color: #3e8ed0;
+ box-shadow: none;
+ color: #3e8ed0
+}
+
+.button__tap.is-info.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-info.is-inverted.is-outlined.is-focused,.button__tap.is-info.is-inverted.is-outlined.is-hovered,.button__tap.is-info.is-inverted.is-outlined:focus,.button__tap.is-info.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #3e8ed0
+}
+
+.button__tap.is-info.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-info.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-info.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-info.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #3e8ed0 #3e8ed0!important
+}
+
+.button__tap.is-info.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-info.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-info.is-light {
+ background-color: #eff5fb;
+ color: #296fa8
+}
+
+.button__tap.is-info.is-light.is-hovered,.button__tap.is-info.is-light:hover {
+ background-color: #e4eff9;
+ border-color: transparent;
+ color: #296fa8
+}
+
+.button__tap.is-info.is-light.is-active,.button__tap.is-info.is-light:active {
+ background-color: #dae9f6;
+ border-color: transparent;
+ color: #296fa8
+}
+
+.button__tap.is-success {
+ background-color: #48c78e;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-success.is-hovered,.button__tap.is-success:hover {
+ background-color: #3ec487;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-success.is-focused,.button__tap.is-success:focus {
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-success.is-focused:not(:active),.button__tap.is-success:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(72,199,142,.25)
+}
+
+.button__tap.is-success.is-active,.button__tap.is-success:active {
+ background-color: #3abb81;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-success[disabled],fieldset[disabled] .button__tap.is-success {
+ background-color: #48c78e;
+ border-color: #48c78e;
+ box-shadow: none
+}
+
+.button__tap.is-success.is-inverted {
+ background-color: #fff;
+ color: #48c78e
+}
+
+.button__tap.is-success.is-inverted.is-hovered,.button__tap.is-success.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.button__tap.is-success.is-inverted[disabled],fieldset[disabled] .button__tap.is-success.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #48c78e
+}
+
+.button__tap.is-success.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-success.is-outlined {
+ background-color: transparent;
+ border-color: #48c78e;
+ color: #48c78e
+}
+
+.button__tap.is-success.is-outlined.is-focused,.button__tap.is-success.is-outlined.is-hovered,.button__tap.is-success.is-outlined:focus,.button__tap.is-success.is-outlined:hover {
+ background-color: #48c78e;
+ border-color: #48c78e;
+ color: #fff
+}
+
+.button__tap.is-success.is-outlined.is-loading::after {
+ border-color: transparent transparent #48c78e #48c78e!important
+}
+
+.button__tap.is-success.is-outlined.is-loading.is-focused::after,.button__tap.is-success.is-outlined.is-loading.is-hovered::after,.button__tap.is-success.is-outlined.is-loading:focus::after,.button__tap.is-success.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-success.is-outlined[disabled],fieldset[disabled] .button__tap.is-success.is-outlined {
+ background-color: transparent;
+ border-color: #48c78e;
+ box-shadow: none;
+ color: #48c78e
+}
+
+.button__tap.is-success.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-success.is-inverted.is-outlined.is-focused,.button__tap.is-success.is-inverted.is-outlined.is-hovered,.button__tap.is-success.is-inverted.is-outlined:focus,.button__tap.is-success.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #48c78e
+}
+
+.button__tap.is-success.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-success.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-success.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-success.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #48c78e #48c78e!important
+}
+
+.button__tap.is-success.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-success.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-success.is-light {
+ background-color: #effaf5;
+ color: #257953
+}
+
+.button__tap.is-success.is-light.is-hovered,.button__tap.is-success.is-light:hover {
+ background-color: #e6f7ef;
+ border-color: transparent;
+ color: #257953
+}
+
+.button__tap.is-success.is-light.is-active,.button__tap.is-success.is-light:active {
+ background-color: #dcf4e9;
+ border-color: transparent;
+ color: #257953
+}
+
+.button__tap.is-warning {
+ background-color: #ffe08a;
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-hovered,.button__tap.is-warning:hover {
+ background-color: #ffdc7d;
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-focused,.button__tap.is-warning:focus {
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-focused:not(:active),.button__tap.is-warning:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(255,224,138,.25)
+}
+
+.button__tap.is-warning.is-active,.button__tap.is-warning:active {
+ background-color: #ffd970;
+ border-color: transparent;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning[disabled],fieldset[disabled] .button__tap.is-warning {
+ background-color: #ffe08a;
+ border-color: #ffe08a;
+ box-shadow: none
+}
+
+.button__tap.is-warning.is-inverted {
+ background-color: rgba(0,0,0,.7);
+ color: #ffe08a
+}
+
+.button__tap.is-warning.is-inverted.is-hovered,.button__tap.is-warning.is-inverted:hover {
+ background-color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-inverted[disabled],fieldset[disabled] .button__tap.is-warning.is-inverted {
+ background-color: rgba(0,0,0,.7);
+ border-color: transparent;
+ box-shadow: none;
+ color: #ffe08a
+}
+
+.button__tap.is-warning.is-loading::after {
+ border-color: transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important
+}
+
+.button__tap.is-warning.is-outlined {
+ background-color: transparent;
+ border-color: #ffe08a;
+ color: #ffe08a
+}
+
+.button__tap.is-warning.is-outlined.is-focused,.button__tap.is-warning.is-outlined.is-hovered,.button__tap.is-warning.is-outlined:focus,.button__tap.is-warning.is-outlined:hover {
+ background-color: #ffe08a;
+ border-color: #ffe08a;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-outlined.is-loading::after {
+ border-color: transparent transparent #ffe08a #ffe08a!important
+}
+
+.button__tap.is-warning.is-outlined.is-loading.is-focused::after,.button__tap.is-warning.is-outlined.is-loading.is-hovered::after,.button__tap.is-warning.is-outlined.is-loading:focus::after,.button__tap.is-warning.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important
+}
+
+.button__tap.is-warning.is-outlined[disabled],fieldset[disabled] .button__tap.is-warning.is-outlined {
+ background-color: transparent;
+ border-color: #ffe08a;
+ box-shadow: none;
+ color: #ffe08a
+}
+
+.button__tap.is-warning.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: rgba(0,0,0,.7);
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-inverted.is-outlined.is-focused,.button__tap.is-warning.is-inverted.is-outlined.is-hovered,.button__tap.is-warning.is-inverted.is-outlined:focus,.button__tap.is-warning.is-inverted.is-outlined:hover {
+ background-color: rgba(0,0,0,.7);
+ color: #ffe08a
+}
+
+.button__tap.is-warning.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-warning.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-warning.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-warning.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #ffe08a #ffe08a!important
+}
+
+.button__tap.is-warning.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-warning.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: rgba(0,0,0,.7);
+ box-shadow: none;
+ color: rgba(0,0,0,.7)
+}
+
+.button__tap.is-warning.is-light {
+ background-color: #fffaeb;
+ color: #946c00
+}
+
+.button__tap.is-warning.is-light.is-hovered,.button__tap.is-warning.is-light:hover {
+ background-color: #fff6de;
+ border-color: transparent;
+ color: #946c00
+}
+
+.button__tap.is-warning.is-light.is-active,.button__tap.is-warning.is-light:active {
+ background-color: #fff3d1;
+ border-color: transparent;
+ color: #946c00
+}
+
+.button__tap.is-danger {
+ background-color: #f14668;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-danger.is-hovered,.button__tap.is-danger:hover {
+ background-color: #f03a5f;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-danger.is-focused,.button__tap.is-danger:focus {
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-danger.is-focused:not(:active),.button__tap.is-danger:focus:not(:active) {
+ box-shadow: 0 0 0 .125em rgba(241,70,104,.25)
+}
+
+.button__tap.is-danger.is-active,.button__tap.is-danger:active {
+ background-color: #ef2e55;
+ border-color: transparent;
+ color: #fff
+}
+
+.button__tap.is-danger[disabled],fieldset[disabled] .button__tap.is-danger {
+ background-color: #f14668;
+ border-color: #f14668;
+ box-shadow: none
+}
+
+.button__tap.is-danger.is-inverted {
+ background-color: #fff;
+ color: #f14668
+}
+
+.button__tap.is-danger.is-inverted.is-hovered,.button__tap.is-danger.is-inverted:hover {
+ background-color: #f2f2f2
+}
+
+.button__tap.is-danger.is-inverted[disabled],fieldset[disabled] .button__tap.is-danger.is-inverted {
+ background-color: #fff;
+ border-color: transparent;
+ box-shadow: none;
+ color: #f14668
+}
+
+.button__tap.is-danger.is-loading::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-danger.is-outlined {
+ background-color: transparent;
+ border-color: #f14668;
+ color: #f14668
+}
+
+.button__tap.is-danger.is-outlined.is-focused,.button__tap.is-danger.is-outlined.is-hovered,.button__tap.is-danger.is-outlined:focus,.button__tap.is-danger.is-outlined:hover {
+ background-color: #f14668;
+ border-color: #f14668;
+ color: #fff
+}
+
+.button__tap.is-danger.is-outlined.is-loading::after {
+ border-color: transparent transparent #f14668 #f14668!important
+}
+
+.button__tap.is-danger.is-outlined.is-loading.is-focused::after,.button__tap.is-danger.is-outlined.is-loading.is-hovered::after,.button__tap.is-danger.is-outlined.is-loading:focus::after,.button__tap.is-danger.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #fff #fff!important
+}
+
+.button__tap.is-danger.is-outlined[disabled],fieldset[disabled] .button__tap.is-danger.is-outlined {
+ background-color: transparent;
+ border-color: #f14668;
+ box-shadow: none;
+ color: #f14668
+}
+
+.button__tap.is-danger.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ color: #fff
+}
+
+.button__tap.is-danger.is-inverted.is-outlined.is-focused,.button__tap.is-danger.is-inverted.is-outlined.is-hovered,.button__tap.is-danger.is-inverted.is-outlined:focus,.button__tap.is-danger.is-inverted.is-outlined:hover {
+ background-color: #fff;
+ color: #f14668
+}
+
+.button__tap.is-danger.is-inverted.is-outlined.is-loading.is-focused::after,.button__tap.is-danger.is-inverted.is-outlined.is-loading.is-hovered::after,.button__tap.is-danger.is-inverted.is-outlined.is-loading:focus::after,.button__tap.is-danger.is-inverted.is-outlined.is-loading:hover::after {
+ border-color: transparent transparent #f14668 #f14668!important
+}
+
+.button__tap.is-danger.is-inverted.is-outlined[disabled],fieldset[disabled] .button__tap.is-danger.is-inverted.is-outlined {
+ background-color: transparent;
+ border-color: #fff;
+ box-shadow: none;
+ color: #fff
+}
+
+.button__tap.is-danger.is-light {
+ background-color: #feecf0;
+ color: #cc0f35
+}
+
+.button__tap.is-danger.is-light.is-hovered,.button__tap.is-danger.is-light:hover {
+ background-color: #fde0e6;
+ border-color: transparent;
+ color: #cc0f35
+}
+
+.button__tap.is-danger.is-light.is-active,.button__tap.is-danger.is-light:active {
+ background-color: #fcd4dc;
+ border-color: transparent;
+ color: #cc0f35
+}
+*/
\ No newline at end of file
diff --git a/static/base/css/chassis.css b/static/base/css/chassis.css
new file mode 100644
index 00000000..1cc2f5dc
--- /dev/null
+++ b/static/base/css/chassis.css
@@ -0,0 +1,356 @@
+html {
+ background-color: white;
+ padding: 1px 3px;
+}
+body {
+ padding: 1px 3px;
+}
+div {
+ padding: 1px 3px;
+ margin: 5px;
+}
+h1, h2, h3, h4,h5, h6, p {
+ background-color: snow;
+}
+* * {
+ outline: 5px solid rgba(255,0,0,.1);
+}
+* * * {
+ outline: 3px dashed rgba(255,0,0,.4);
+}
+* * * * {
+ outline: 2px dotted rgba(255,0,0,.6);
+}
+* * * * * {
+ outline: 1px dotted rgba(255,0,0,.9);
+}
+* * * * * * {
+ outline-color: gray;
+}
+
+*::before, *::after {
+ background: #faa;
+ border-radius: 3px;
+ font: normal normal 400 10px/1.2 monospace;
+ vertical-align: middle;
+ padding: 1px 3px;
+ margin: 0 3px;
+}
+*::before {
+ content: "(";
+}
+*::after {
+ content: ")";
+}
+
+a::before { content: ""; }
+a::after { content: ""; }
+abbr::before { content: ""; }
+abbr::after { content: ""; }
+acronym::before { content: ""; }
+acronym::after { content: ""; }
+address::before { content: ""; }
+address::after { content: ""; }
+applet::before { content: ""; }
+area::before { content: ""; }
+area::after { content: ""; }
+article::before { content: ""; }
+article::after { content: ""; }
+aside::before { content: ""; }
+audio::before { content: ""; }
+
+b::before { content: ""; }
+b::after { content: ""; }
+base::before { content: ""; }
+base::after { content: ""; }
+basefont::before { content: ""; }
+basefont::after { content: ""; }
+bdi::before { content: ""; }
+bdi::after { content: ""; }
+bdo::before { content: ""; }
+bdo::after { content: ""; }
+bgsound::before { content: ""; }
+bgsound::after { content: ""; }
+big::before { content: ""; }
+big::after { content: ""; }
+blink::before { content: ""; }
+blockquote::before { content: "
"; }
+blockquote::after { content: "
"; }
+body::before { content: ""; }
+body::after { content: ""; }
+br::before { content: "
"; }
+br::after { content: ""; }
+button::before { content: ""; }
+
+caption::before { content: ""; }
+caption::after { content: ""; }
+canvas::before { content: ""; }
+center::before { content: ""; }
+center::after { content: ""; }
+cite::before { content: ""; }
+cite::after { content: ""; }
+code::before { content: ""; }
+code::after { content: ""; }
+col::before { content: ""; }
+col::after { content: ""; }
+colgroup::before { content: ""; }
+colgroup::after { content: ""; }
+command::before { content: ""; }
+command::after { content: ""; }
+content::before { content: ""; }
+content::after { content: ""; }
+
+data::before { content: ""; }
+data::after { content: ""; }
+datalist::before { content: ""; }
+dd::before { content: ""; }
+dd::after { content: ""; }
+del::before { content: ""; }
+del::after { content: ""; }
+details::before { content: ""; }
+details::after { content: " "; }
+dfn::before { content: ""; }
+dfn::after { content: ""; }
+dialog::before { content: ""; }
+dir::before { content: ""; }
+dir::after { content: ""; }
+div::before { content: ""; }
+div::after { content: "
"; }
+dl::before { content: ""; }
+dl::after { content: "
"; }
+dt::before { content: ""; }
+dt::after { content: ""; }
+
+element::before { content: ""; }
+element::after { content: ""; }
+em::before { content: ""; }
+em::after { content: ""; }
+embed::before { content: ""; }
+
+fieldset::before { content: ""; }
+figcaption::before { content: ""; }
+figcaption::after { content: ""; }
+figure::before { content: ""; }
+figure::after { content: ""; }
+font::before { content: ""; }
+font::after { content: ""; }
+footer::before { content: ""; }
+form::before { content: ""; }
+frame::before { content: ""; }
+frame::after { content: ""; }
+frameset::before { content: ""; }
+
+h1::before { content: ""; }
+h1::after { content: "
"; }
+h2::before { content: ""; }
+h2::after { content: "
"; }
+h3::before { content: ""; }
+h3::after { content: "
"; }
+h4::before { content: ""; }
+h4::after { content: "
"; }
+h5::before { content: ""; }
+h5::after { content: "
"; }
+h6::before { content: ""; }
+h6::after { content: "
"; }
+head::before { content: ""; }
+head::after { content: ""; }
+header::before { content: ""; }
+header::after { content: ""; }
+hgroup::before { content: ""; }
+hgroup::after { content: ""; }
+hr::before { content: "
"; }
+hr::after { content: ""; }
+html::before { content: ""; }
+html::after { content: ""; }
+
+i::before { content: ""; }
+i::after { content: ""; }
+iframe::before { content: ""; }
+image::before { content: ""; }
+image::after { content: ""; }
+img::before { content: "
"; }
+img::after { content: ""; }
+input::before { content: ""; }
+input::after { content: ""; }
+ins::before { content: ""; }
+ins::after { content: ""; }
+isindex::before { content: ""; }
+isindex::after { content: ""; }
+
+kbd::before { content: ""; }
+kbd::after { content: ""; }
+keygen::before { content: ""; }
+keygen::after { content: ""; }
+
+label::before { content: ""; }
+legend::before { content: ""; }
+li::before { content: ""; }
+li::after { content: ""; }
+link::before { content: ""; }
+link::after { content: ""; }
+listing::before { content: ""; }
+listing::after { content: ""; }
+
+main::before { content: ""; }
+main::after { content: ""; }
+map::before { content: ""; }
+mark::before { content: ""; }
+mark::after { content: ""; }
+marquee::before { content: ""; }
+menu::before { content: ""; }
+menuitem::before { content: ""; }
+meta::before { content: ""; }
+meta::after { content: ""; }
+meter::before { content: ""; }
+meter::after { content: ""; }
+multicol::before { content: ""; }
+multicol::after { content: ""; }
+
+nav::before { content: ""; }
+nextid::before { content: ""; }
+nextid::after { content: ""; }
+nobr::before { content: ""; }
+nobr::after { content: ""; }
+noembed::before { content: ""; }
+noembed::after { content: ""; }
+noframes::before { content: ""; }
+noframes::after { content: ""; }
+noscript::before { content: ""; }
+
+object::before { content: ""; }
+ol::before { content: ""; }
+ol::after { content: "
"; }
+optgroup::before { content: ""; }
+option::before { content: ""; }
+output::before { content: ""; }
+
+p::before { content: ""; }
+p::after { content: "
"; }
+param::before { content: ""; }
+param::after { content: ""; }
+picture::before { content: ""; }
+picture::after { content: ""; }
+plaintext::before { content: ""; }
+plaintext::after { content: ""; }
+pre::before { content: ""; }
+pre::after { content: ""; }
+progress::before { content: ""; }
+
+q::before { content: ""; }
+q::after { content: "
"; }
+
+rb::before { content: ""; }
+rb::after { content: ""; }
+rp::before { content: ""; }
+rt::before { content: ""; }
+rtc::before { content: ""; }
+ruby::before { content: ""; }
+ruby::after { content: ""; }
+
+s::before { content: ""; }
+s::after { content: ""; }
+samp::before { content: ""; }
+samp::after { content: ""; }
+script::before { content: ""; }
+section::before { content: ""; }
+section::after { content: ""; }
+select::before { content: ""; }
+shadow::before { content: ""; }
+shadow::after { content: ""; }
+slot::before { content: ""; }
+slot::after { content: ""; }
+small::before { content: ""; }
+small::after { content: ""; }
+source::before { content: ""; }
+source::after { content: ""; }
+spacer::before { content: ""; }
+spacer::after { content: ""; }
+span::before { content: ""; }
+span::after { content: ""; }
+strike::before { content: ""; }
+strike::after { content: ""; }
+strong::before { content: ""; }
+strong::after { content: ""; }
+style::before { content: "details-1
\ No newline at end of file
diff --git a/static/base/images/header.svg b/static/base/images/header.svg
new file mode 100644
index 00000000..060757ee
--- /dev/null
+++ b/static/base/images/header.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/base/images/issues.jpg b/static/base/images/issues.jpg
new file mode 100644
index 00000000..ab0ee60a
Binary files /dev/null and b/static/base/images/issues.jpg differ
diff --git a/static/base/images/pagetop.png b/static/base/images/pagetop.png
new file mode 100644
index 00000000..71242a86
Binary files /dev/null and b/static/base/images/pagetop.png differ
diff --git a/static/base/images/welcome.jpg b/static/base/images/welcome.jpg
new file mode 100644
index 00000000..05d89e33
Binary files /dev/null and b/static/base/images/welcome.jpg differ
diff --git a/static/base/js/menu.js b/static/base/js/menu.js
new file mode 100644
index 00000000..0565f194
--- /dev/null
+++ b/static/base/js/menu.js
@@ -0,0 +1,94 @@
+function menu__showChildren(nav, children) {
+ let submenu = children[0].querySelector('.menu__subs');
+ submenu.classList.add('active');
+ submenu.style.animation = 'slideLeft 0.5s ease forwards';
+
+ let title = children[0].querySelector('i').parentNode.childNodes[0].textContent;
+ nav.querySelector('.menu__title').innerHTML = title;
+ nav.querySelector('.menu__header').classList.add('active');
+}
+
+function menu__hideChildren(nav, children) {
+ let submenu = children[0].querySelector('.menu__subs');
+ submenu.style.animation = 'slideRight 0.5s ease forwards';
+ setTimeout(() => {
+ submenu.classList.remove('active');
+ submenu.style.removeProperty('animation');
+ }, 300);
+
+ children.shift();
+ if (children.length > 0) {
+ let title = children[0].querySelector('i').parentNode.childNodes[0].textContent;
+ nav.querySelector('.menu__title').innerHTML = title;
+ } else {
+ nav.querySelector('.menu__header').classList.remove('active');
+ nav.querySelector('.menu__title').innerHTML = '';
+ }
+}
+
+function menu__toggle(nav, overlay) {
+ nav.classList.toggle('active');
+ overlay.classList.toggle('active');
+}
+
+function menu__reset(menu, nav, overlay) {
+ menu__toggle(nav, overlay);
+ setTimeout(() => {
+ nav.querySelector('.menu__header').classList.remove('active');
+ nav.querySelector('.menu__title').innerHTML = '';
+ menu.querySelectorAll('.menu__subs').forEach(submenu => {
+ submenu.classList.remove('active');
+ submenu.style.removeProperty('animation');
+ });
+ }, 300);
+ return [];
+}
+
+document.querySelectorAll('.menu__container').forEach(menu => {
+
+ let menuChildren = [];
+ const menuNav = menu.querySelector('.menu__nav');
+ const menuOverlay = menu.querySelector('.menu__overlay');
+
+ menu.querySelector('.menu__section').addEventListener('click', (e) => {
+ if (menuNav.classList.contains('active')) {
+ let target = e.target.closest('.menu__children');
+ if (target && target != menuChildren[0]) {
+ menuChildren.unshift(target);
+ menu__showChildren(menuNav, menuChildren);
+ }
+ }
+ });
+
+ menu.querySelector('.menu__arrow').addEventListener('click', () => {
+ menu__hideChildren(menuNav, menuChildren);
+ });
+
+ menu.querySelector('.menu__close').addEventListener('click', () => {
+ menuChildren = menu__reset(menu, menuNav, menuOverlay);
+ });
+
+ menu.querySelectorAll('.menu__link > a[target="_blank"]').forEach(link => {
+ link.addEventListener('click', (e) => {
+ menuChildren = menu__reset(menu, menuNav, menuOverlay);
+ e.target.blur();
+ });
+ });
+
+ menu.querySelector('.menu__trigger').addEventListener('click', () => {
+ menu__toggle(menuNav, menuOverlay);
+ });
+
+ menuOverlay.addEventListener('click', () => {
+ menu__toggle(menuNav, menuOverlay);
+ });
+
+ window.onresize = function () {
+ if (menuNav.classList.contains('active')) {
+ var fontSizeRoot = parseFloat(getComputedStyle(document.documentElement).fontSize);
+ if (this.innerWidth >= 62 * fontSizeRoot) {
+ menuChildren = menu__reset(menu, menuNav, menuOverlay);
+ }
+ }
+ };
+});
diff --git a/static/base/pagetop-logo.svg b/static/base/pagetop-logo.svg
new file mode 100644
index 00000000..3666bf3a
--- /dev/null
+++ b/static/base/pagetop-logo.svg
@@ -0,0 +1,14 @@
+