Compare commits
9 commits
main
...
add-menu-c
Author | SHA1 | Date | |
---|---|---|---|
8912bbc8ec | |||
284d157931 | |||
de06afce65 | |||
423b30475b | |||
c0577a0773 | |||
744bd700fc | |||
f5fb4b7a1d | |||
f5290b477f | |||
476ea7de7f |
43 changed files with 1611 additions and 226 deletions
|
@ -1,5 +1,53 @@
|
||||||
//! Componentes nativos proporcionados por PageTop.
|
//! Componentes nativos proporcionados por PageTop.
|
||||||
|
|
||||||
|
use crate::AutoDefault;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
// **< FontSize >***********************************************************************************
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum FontSize {
|
||||||
|
ExtraLarge,
|
||||||
|
XxLarge,
|
||||||
|
XLarge,
|
||||||
|
Large,
|
||||||
|
Medium,
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Small,
|
||||||
|
XSmall,
|
||||||
|
XxSmall,
|
||||||
|
ExtraSmall,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl FontSize {
|
||||||
|
#[inline]
|
||||||
|
pub const fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FontSize::ExtraLarge => "fs__x3l",
|
||||||
|
FontSize::XxLarge => "fs__x2l",
|
||||||
|
FontSize::XLarge => "fs__xl",
|
||||||
|
FontSize::Large => "fs__l",
|
||||||
|
FontSize::Medium => "fs__m",
|
||||||
|
FontSize::Normal => "",
|
||||||
|
FontSize::Small => "fs__s",
|
||||||
|
FontSize::XSmall => "fs__xs",
|
||||||
|
FontSize::XxSmall => "fs__x2s",
|
||||||
|
FontSize::ExtraSmall => "fs__x3s",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FontSize {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
mod html;
|
mod html;
|
||||||
pub use html::Html;
|
pub use html::Html;
|
||||||
|
|
||||||
|
@ -8,3 +56,8 @@ pub use block::Block;
|
||||||
|
|
||||||
mod poweredby;
|
mod poweredby;
|
||||||
pub use poweredby::PoweredBy;
|
pub use poweredby::PoweredBy;
|
||||||
|
|
||||||
|
mod icon;
|
||||||
|
pub use icon::{Icon, IconKind};
|
||||||
|
|
||||||
|
pub mod menu;
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl Component for Block {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
// Block BUILDER *******************************************************************************
|
// **< Block BUILDER >**************************************************************************
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del bloque.
|
/// Establece el identificador único (`id`) del bloque.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -77,14 +77,14 @@ impl Block {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifica la lista de hijos (`children`) aplicando una operación.
|
/// Modifica la lista de hijos (`children`) aplicando una operación [`ChildOp`].
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: ChildOp) -> Self {
|
||||||
self.children.alter_child(op);
|
self.children.alter_child(op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block GETTERS *******************************************************************************
|
// **< Block GETTERS >**************************************************************************
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al bloque.
|
/// Devuelve las clases CSS asociadas al bloque.
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
pub fn classes(&self) -> &AttrClasses {
|
||||||
|
|
|
@ -49,7 +49,7 @@ impl Component for Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Html {
|
impl Html {
|
||||||
// Html BUILDER ********************************************************************************
|
// **< Html BUILDER >***************************************************************************
|
||||||
|
|
||||||
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
/// Crea una instancia que generará el `Markup`, con acceso opcional al contexto.
|
||||||
///
|
///
|
||||||
|
@ -77,7 +77,7 @@ impl Html {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Html GETTERS ********************************************************************************
|
// **< Html GETTERS >***************************************************************************
|
||||||
|
|
||||||
/// Aplica la función interna de renderizado con el [`Context`] proporcionado.
|
/// Aplica la función interna de renderizado con el [`Context`] proporcionado.
|
||||||
///
|
///
|
||||||
|
|
134
src/base/component/icon.rs
Normal file
134
src/base/component/icon.rs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum IconKind {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Font(FontSize),
|
||||||
|
Svg {
|
||||||
|
shapes: Markup,
|
||||||
|
viewbox: AttrValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Icon {
|
||||||
|
classes : AttrClasses,
|
||||||
|
icon_kind : IconKind,
|
||||||
|
aria_label: AttrL10n,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Icon {
|
||||||
|
fn new() -> Self {
|
||||||
|
Icon::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
if !matches!(self.icon_kind(), IconKind::None) {
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "icon");
|
||||||
|
}
|
||||||
|
if let IconKind::Font(font_size) = self.icon_kind() {
|
||||||
|
self.alter_classes(ClassesOp::Add, font_size.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
match self.icon_kind() {
|
||||||
|
IconKind::None => PrepareMarkup::None,
|
||||||
|
IconKind::Font(_) => {
|
||||||
|
let aria_label = self.aria_label().lookup(cx);
|
||||||
|
let has_label = aria_label.is_some();
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
i
|
||||||
|
class=[self.classes().get()]
|
||||||
|
role=[has_label.then_some("img")]
|
||||||
|
aria-label=[aria_label]
|
||||||
|
aria-hidden=[(!has_label).then_some("true")]
|
||||||
|
{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
IconKind::Svg { shapes, viewbox } => {
|
||||||
|
let aria_label = self.aria_label().lookup(cx);
|
||||||
|
let has_label = aria_label.is_some();
|
||||||
|
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox=(viewbox)
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
class=[self.classes().get()]
|
||||||
|
role=[has_label.then_some("img")]
|
||||||
|
aria-label=[aria_label]
|
||||||
|
aria-hidden=[(!has_label).then_some("true")]
|
||||||
|
{
|
||||||
|
(shapes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Icon {
|
||||||
|
pub fn font() -> Self {
|
||||||
|
Icon::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font_sized(font_size: FontSize) -> Self {
|
||||||
|
Icon::default().with_icon_kind(IconKind::Font(font_size))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn svg(shapes: Markup) -> Self {
|
||||||
|
Icon::default().with_icon_kind(IconKind::Svg {
|
||||||
|
shapes,
|
||||||
|
viewbox: AttrValue::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
|
||||||
|
Icon::default().with_icon_kind(IconKind::Svg {
|
||||||
|
shapes,
|
||||||
|
viewbox: AttrValue::new(viewbox),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Icon BUILDER >***************************************************************************
|
||||||
|
|
||||||
|
/// Modifica la lista de clases CSS aplicadas al icono.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_icon_kind(mut self, icon_kind: IconKind) -> Self {
|
||||||
|
self.icon_kind = icon_kind;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_aria_label(mut self, label: L10n) -> Self {
|
||||||
|
self.aria_label.alter_value(label);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Icon GETTERS >***************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve las clases CSS asociadas al icono.
|
||||||
|
pub fn classes(&self) -> &AttrClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_kind(&self) -> &IconKind {
|
||||||
|
&self.icon_kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn aria_label(&self) -> &AttrL10n {
|
||||||
|
&self.aria_label
|
||||||
|
}
|
||||||
|
}
|
17
src/base/component/menu.rs
Normal file
17
src/base/component/menu.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
mod menu_menu;
|
||||||
|
pub use menu_menu::Menu;
|
||||||
|
|
||||||
|
mod item;
|
||||||
|
pub use item::{Item, ItemKind};
|
||||||
|
|
||||||
|
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};
|
56
src/base/component/menu/element.rs
Normal file
56
src/base/component/menu/element.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
type Content = Typed<Html>;
|
||||||
|
type SubmenuItems = Typed<menu::Submenu>;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ElementType {
|
||||||
|
#[default]
|
||||||
|
Void,
|
||||||
|
Html(Content),
|
||||||
|
Submenu(SubmenuItems),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Element {
|
||||||
|
element_type: ElementType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component 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: menu::Submenu) -> Self {
|
||||||
|
Element {
|
||||||
|
element_type: ElementType::Submenu(SubmenuItems::with(submenu)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Element GETTERS >************************************************************************
|
||||||
|
|
||||||
|
pub fn element_type(&self) -> &ElementType {
|
||||||
|
&self.element_type
|
||||||
|
}
|
||||||
|
}
|
58
src/base/component/menu/group.rs
Normal file
58
src/base/component/menu/group.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Group {
|
||||||
|
id : AttrId,
|
||||||
|
elements: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Group {
|
||||||
|
fn new() -> Self {
|
||||||
|
Group::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
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 >**************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del grupo.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo elemento al menú.
|
||||||
|
pub fn add_element(mut self, element: menu::Element) -> Self {
|
||||||
|
self.elements
|
||||||
|
.alter_typed(TypedOp::Add(Typed::with(element)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_elements(mut self, op: TypedOp<menu::Element>) -> Self {
|
||||||
|
self.elements.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Group GETTERS >**************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve la lista de elementos (`children`) del grupo.
|
||||||
|
pub fn elements(&self) -> &Children {
|
||||||
|
&self.elements
|
||||||
|
}
|
||||||
|
}
|
185
src/base/component/menu/item.rs
Normal file
185
src/base/component/menu/item.rs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
type Label = L10n;
|
||||||
|
type Content = Typed<Html>;
|
||||||
|
type SubmenuItems = Typed<menu::Submenu>;
|
||||||
|
type MegamenuGroups = Typed<menu::Megamenu>;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemKind {
|
||||||
|
#[default]
|
||||||
|
Void,
|
||||||
|
Label(Label),
|
||||||
|
Link(Label, FnPathByContext),
|
||||||
|
LinkBlank(Label, FnPathByContext),
|
||||||
|
Html(Content),
|
||||||
|
Submenu(Label, SubmenuItems),
|
||||||
|
Megamenu(Label, MegamenuGroups),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Item {
|
||||||
|
item_kind : ItemKind,
|
||||||
|
description: AttrL10n,
|
||||||
|
left_icon : Typed<Icon>,
|
||||||
|
right_icon : Typed<Icon>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Item {
|
||||||
|
fn new() -> Self {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let description = self.description().lookup(cx);
|
||||||
|
let left_icon = self.left_icon().render(cx);
|
||||||
|
let right_icon = self.right_icon().render(cx);
|
||||||
|
|
||||||
|
match self.item_kind() {
|
||||||
|
ItemKind::Void => PrepareMarkup::None,
|
||||||
|
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||||
|
li class="menu__item menu__item--label" {
|
||||||
|
span title=[description] {
|
||||||
|
(left_icon)
|
||||||
|
span class="menu__label" { (label.using(cx)) }
|
||||||
|
(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemKind::Link(label, path) => PrepareMarkup::With(html! {
|
||||||
|
li class="menu__item menu__item--link" {
|
||||||
|
a href=(path(cx)) title=[description] {
|
||||||
|
(left_icon)
|
||||||
|
span class="menu__label" { (label.using(cx)) }
|
||||||
|
(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemKind::LinkBlank(label, path) => PrepareMarkup::With(html! {
|
||||||
|
li class="menu__item menu__item--link" {
|
||||||
|
a href=(path(cx)) title=[description] target="_blank" {
|
||||||
|
(left_icon)
|
||||||
|
span class="menu__label" { (label.using(cx)) }
|
||||||
|
(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemKind::Html(content) => PrepareMarkup::With(html! {
|
||||||
|
li class="menu__item menu__item--html" {
|
||||||
|
(content.render(cx))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemKind::Submenu(label, submenu) => PrepareMarkup::With(html! {
|
||||||
|
li class="menu__item menu__item--children" {
|
||||||
|
a href="#" title=[description] {
|
||||||
|
(left_icon)
|
||||||
|
span class="menu__label" { (label.using(cx)) }
|
||||||
|
(Icon::svg(html! {
|
||||||
|
path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708" {}
|
||||||
|
}).render(cx))
|
||||||
|
}
|
||||||
|
div class="menu__children menu__children--submenu" {
|
||||||
|
(submenu.render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemKind::Megamenu(label, megamenu) => PrepareMarkup::With(html! {
|
||||||
|
li class="menu__item menu__item--children" {
|
||||||
|
a href="#" title=[description] {
|
||||||
|
(left_icon)
|
||||||
|
span class="menu__label" { (label.using(cx)) }
|
||||||
|
(Icon::svg(html! {
|
||||||
|
path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708" {}
|
||||||
|
}).render(cx))
|
||||||
|
}
|
||||||
|
div class="menu__children menu__children--mega" {
|
||||||
|
(megamenu.render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn label(label: L10n) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Label(label),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Link(label, path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::LinkBlank(label, path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn html(content: Html) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Html(Content::with(content)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn submenu(label: L10n, submenu: menu::Submenu) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Submenu(label, SubmenuItems::with(submenu)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
pub fn megamenu(label: L10n, megamenu: Megamenu) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Megamenu(label, MegamenuGroups::with(megamenu)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// **< Item BUILDER >***************************************************************************
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_description(mut self, text: L10n) -> Self {
|
||||||
|
self.description.alter_value(text);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_left_icon<I: Into<Icon>>(mut self, icon: Option<I>) -> Self {
|
||||||
|
self.left_icon.alter_component(icon.map(Into::into));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_right_icon<I: Into<Icon>>(mut self, icon: Option<I>) -> Self {
|
||||||
|
self.right_icon.alter_component(icon.map(Into::into));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Item GETTERS >***************************************************************************
|
||||||
|
|
||||||
|
pub fn item_kind(&self) -> &ItemKind {
|
||||||
|
&self.item_kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> &AttrL10n {
|
||||||
|
&self.description
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left_icon(&self) -> &Typed<Icon> {
|
||||||
|
&self.left_icon
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_icon(&self) -> &Typed<Icon> {
|
||||||
|
&self.right_icon
|
||||||
|
}
|
||||||
|
}
|
57
src/base/component/menu/megamenu.rs
Normal file
57
src/base/component/menu/megamenu.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Megamenu {
|
||||||
|
id : AttrId,
|
||||||
|
groups: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Megamenu {
|
||||||
|
fn new() -> Self {
|
||||||
|
Megamenu::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class="menu__mega" {
|
||||||
|
(self.groups().render(cx))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Megamenu {
|
||||||
|
// **< Megamenu BUILDER >***********************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del megamenú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo grupo al menú.
|
||||||
|
pub fn add_group(mut self, group: menu::Group) -> Self {
|
||||||
|
self.groups.alter_typed(TypedOp::Add(Typed::with(group)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de grupos (`children`) aplicando una operación [`TypedOp`].
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_groups(mut self, op: TypedOp<menu::Group>) -> Self {
|
||||||
|
self.groups.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Megamenu GETTERS >***********************************************************************
|
||||||
|
|
||||||
|
/// Devuelve la lista de grupos (`children`) del megamenú.
|
||||||
|
pub fn groups(&self) -> &Children {
|
||||||
|
&self.groups
|
||||||
|
}
|
||||||
|
}
|
108
src/base/component/menu/menu_menu.rs
Normal file
108
src/base/component/menu/menu_menu.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Menu {
|
||||||
|
id : AttrId,
|
||||||
|
classes: AttrClasses,
|
||||||
|
items : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Menu {
|
||||||
|
fn new() -> Self {
|
||||||
|
Menu::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "menu");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
// cx.set_param::<bool>(PARAM_BASE_INCLUDE_MENU_ASSETS, &true);
|
||||||
|
// cx.set_param::<bool>(PARAM_BASE_INCLUDE_ICONS, &true);
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class=[self.classes().get()] {
|
||||||
|
div class="menu__wrapper" {
|
||||||
|
div class="menu__panel" {
|
||||||
|
div class="menu__overlay" {}
|
||||||
|
nav class="menu__nav" {
|
||||||
|
div class="menu__header" {
|
||||||
|
button type="button" class="menu__back" {
|
||||||
|
(Icon::svg(html! {
|
||||||
|
path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0" {}
|
||||||
|
}).render(cx))
|
||||||
|
}
|
||||||
|
div class="menu__title" {}
|
||||||
|
button type="button" class="menu__close" {
|
||||||
|
(Icon::svg(html! {
|
||||||
|
path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z" {}
|
||||||
|
}).render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul class="menu__list" {
|
||||||
|
(self.items().render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button
|
||||||
|
type="button"
|
||||||
|
class="menu__trigger"
|
||||||
|
title=[L10n::l("menu_toggle").lookup(cx)]
|
||||||
|
{
|
||||||
|
(Icon::svg(html! {
|
||||||
|
path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5" {}
|
||||||
|
}).render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Menu {
|
||||||
|
// **< Menu BUILDER >***************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del menú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de clases CSS aplicadas al menú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo ítem al menú.
|
||||||
|
pub fn add_item(mut self, item: menu::Item) -> Self {
|
||||||
|
self.items.alter_typed(TypedOp::Add(Typed::with(item)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de ítems (`children`) aplicando una operación [`TypedOp`].
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_items(mut self, op: TypedOp<menu::Item>) -> Self {
|
||||||
|
self.items.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Menu GETTERS >***************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve las clases CSS asociadas al menú.
|
||||||
|
pub fn classes(&self) -> &AttrClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la lista de ítems (`children`) del menú.
|
||||||
|
pub fn items(&self) -> &Children {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
73
src/base/component/menu/submenu.rs
Normal file
73
src/base/component/menu/submenu.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Submenu {
|
||||||
|
id : AttrId,
|
||||||
|
title: AttrL10n,
|
||||||
|
items: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Submenu {
|
||||||
|
fn new() -> Self {
|
||||||
|
Submenu::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class="menu__submenu" {
|
||||||
|
@if let Some(title) = self.title().lookup(cx) {
|
||||||
|
h4 class="menu__submenu-title" { (title) }
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
(self.items().render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Submenu {
|
||||||
|
// **< Submenu BUILDER >************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del submenú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_title(mut self, title: L10n) -> Self {
|
||||||
|
self.title.alter_value(title);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo ítem al submenú.
|
||||||
|
pub fn add_item(mut self, item: menu::Item) -> Self {
|
||||||
|
self.items.alter_typed(TypedOp::Add(Typed::with(item)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de ítems (`children`) aplicando una operación [`TypedOp`].
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_items(mut self, op: TypedOp<menu::Item>) -> Self {
|
||||||
|
self.items.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Submenu GETTERS >************************************************************************
|
||||||
|
|
||||||
|
pub fn title(&self) -> &AttrL10n {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la lista de ítems (`children`) del submenú.
|
||||||
|
pub fn items(&self) -> &Children {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ impl Component for PoweredBy {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PoweredBy {
|
impl PoweredBy {
|
||||||
// PoweredBy BUILDER ***************************************************************************
|
// **< PoweredBy BUILDER >**********************************************************************
|
||||||
|
|
||||||
/// Establece el texto de copyright que mostrará el componente.
|
/// Establece el texto de copyright que mostrará el componente.
|
||||||
///
|
///
|
||||||
|
@ -58,7 +58,7 @@ impl PoweredBy {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// PoweredBy GETTERS ***************************************************************************
|
// **< PoweredBy GETTERS >**********************************************************************
|
||||||
|
|
||||||
/// Devuelve el texto de copyright actual, si existe.
|
/// Devuelve el texto de copyright actual, si existe.
|
||||||
pub fn copyright(&self) -> Option<&str> {
|
pub fn copyright(&self) -> Option<&str> {
|
||||||
|
|
|
@ -51,14 +51,35 @@ impl Theme for Basic {
|
||||||
"PageTopIntro" => "/css/intro.css",
|
"PageTopIntro" => "/css/intro.css",
|
||||||
_ => "/css/basic.css",
|
_ => "/css/basic.css",
|
||||||
};
|
};
|
||||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
let pkg_version = env!("CARGO_PKG_VERSION");
|
||||||
|
page.alter_assets(ContextOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/normalize.css")
|
StyleSheet::from("/css/normalize.css")
|
||||||
.with_version("8.0.1")
|
.with_version("8.0.1")
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_assets(AssetsOp::AddStyleSheet(
|
.alter_assets(ContextOp::AddStyleSheet(
|
||||||
|
StyleSheet::from("/css/root.css")
|
||||||
|
.with_version(pkg_version)
|
||||||
|
.with_weight(-99),
|
||||||
|
))
|
||||||
|
.alter_assets(ContextOp::AddStyleSheet(
|
||||||
|
StyleSheet::from("/css/components.css")
|
||||||
|
.with_version(pkg_version)
|
||||||
|
.with_weight(-99),
|
||||||
|
))
|
||||||
|
.alter_assets(ContextOp::AddStyleSheet(
|
||||||
|
StyleSheet::from("/css/menu.css")
|
||||||
|
.with_version(pkg_version)
|
||||||
|
.with_weight(-99),
|
||||||
|
))
|
||||||
|
.alter_assets(ContextOp::AddStyleSheet(
|
||||||
StyleSheet::from(styles)
|
StyleSheet::from(styles)
|
||||||
.with_version(env!("CARGO_PKG_VERSION"))
|
.with_version(pkg_version)
|
||||||
|
.with_weight(-99),
|
||||||
|
))
|
||||||
|
.alter_assets(ContextOp::AddJavaScript(
|
||||||
|
JavaScript::defer("/js/menu.js")
|
||||||
|
.with_version(pkg_version)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -148,7 +169,7 @@ fn render_intro(page: &mut Page) -> Markup {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pagetop_intro(page: &mut Page) -> Markup {
|
fn render_pagetop_intro(page: &mut Page) -> Markup {
|
||||||
page.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
page.alter_assets(ContextOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||||
util::indoc!(r#"
|
util::indoc!(r#"
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
|
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
|
||||||
|
|
|
@ -5,12 +5,12 @@ use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// ACCIONES ****************************************************************************************
|
// **< ACCIONES >***********************************************************************************
|
||||||
|
|
||||||
static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
|
static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
|
||||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
// AÑADIR ACCIONES *********************************************************************************
|
// **< AÑADIR ACCIONES >****************************************************************************
|
||||||
|
|
||||||
// Registra una nueva acción en el sistema.
|
// Registra una nueva acción en el sistema.
|
||||||
//
|
//
|
||||||
|
@ -36,7 +36,7 @@ pub fn add_action(action: ActionBox) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DESPLEGAR ACCIONES ******************************************************************************
|
// **< DESPLEGAR ACCIONES >*************************************************************************
|
||||||
|
|
||||||
/// Despacha y ejecuta las funciones asociadas a una [`ActionKey`].
|
/// Despacha y ejecuta las funciones asociadas a una [`ActionKey`].
|
||||||
///
|
///
|
||||||
|
|
|
@ -8,5 +8,6 @@ pub use children::Children;
|
||||||
pub use children::{Child, ChildOp};
|
pub use children::{Child, ChildOp};
|
||||||
pub use children::{Typed, TypedOp};
|
pub use children::{Typed, TypedOp};
|
||||||
|
|
||||||
mod slot;
|
mod context;
|
||||||
pub use slot::TypedSlot;
|
pub use context::{Context, ContextError, ContextOp, Contextual};
|
||||||
|
pub type FnPathByContext = fn(cx: &Context) -> &str;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::core::component::Component;
|
use crate::core::component::{Component, Context};
|
||||||
use crate::html::{html, Context, Markup};
|
use crate::html::{html, Markup};
|
||||||
use crate::{builder_fn, UniqueId};
|
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
@ -11,76 +11,105 @@ use std::vec::IntoIter;
|
||||||
///
|
///
|
||||||
/// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y
|
/// Esta estructura permite manipular y renderizar un componente que implemente [`Component`], y
|
||||||
/// habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
/// habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
||||||
#[derive(Clone)]
|
#[derive(AutoDefault, Clone)]
|
||||||
pub struct Child(Arc<RwLock<dyn Component>>);
|
pub struct Child(Option<Arc<RwLock<dyn Component>>>);
|
||||||
|
|
||||||
impl Child {
|
impl Child {
|
||||||
/// Crea un nuevo `Child` a partir de un componente.
|
/// Crea un nuevo `Child` a partir de un componente.
|
||||||
pub fn with(component: impl Component) -> Self {
|
pub fn with(component: impl Component) -> Self {
|
||||||
Child(Arc::new(RwLock::new(component)))
|
Child(Some(Arc::new(RwLock::new(component))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child GETTERS *******************************************************************************
|
// **< Child BUILDER >**************************************************************************
|
||||||
|
|
||||||
/// Devuelve el identificador del componente, si está definido.
|
/// Establece un componente nuevo, o lo vacía.
|
||||||
|
///
|
||||||
|
/// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_component<C: Component>(mut self, component: Option<C>) -> Self {
|
||||||
|
if let Some(c) = component {
|
||||||
|
self.0 = Some(Arc::new(RwLock::new(c)));
|
||||||
|
} else {
|
||||||
|
self.0 = None;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Child GETTERS >**************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve el identificador del componente, si existe y está definido.
|
||||||
|
#[inline]
|
||||||
pub fn id(&self) -> Option<String> {
|
pub fn id(&self) -> Option<String> {
|
||||||
self.0.read().id()
|
self.0.as_ref().and_then(|c| c.read().id())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child RENDER ********************************************************************************
|
// **< Child RENDER >***************************************************************************
|
||||||
|
|
||||||
/// Renderiza el componente con el contexto proporcionado.
|
/// Renderiza el componente con el contexto proporcionado.
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
pub fn render(&self, cx: &mut Context) -> Markup {
|
||||||
self.0.write().render(cx)
|
self.0.as_ref().map_or(html! {}, |c| c.write().render(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child HELPERS *******************************************************************************
|
// **< Child HELPERS >**************************************************************************
|
||||||
|
|
||||||
// Devuelve el [`UniqueId`] del tipo del componente.
|
// Devuelve el [`UniqueId`] del tipo del componente, si existe.
|
||||||
fn type_id(&self) -> UniqueId {
|
#[inline]
|
||||||
self.0.read().type_id()
|
fn type_id(&self) -> Option<UniqueId> {
|
||||||
|
self.0.as_ref().map(|c| c.read().type_id())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
/// Variante tipada de [`Child`] para evitar conversiones durante el uso.
|
/// Variante tipada de [`Child`] para evitar conversiones de tipo durante el uso.
|
||||||
///
|
///
|
||||||
/// Esta estructura permite manipular y renderizar un componente concreto que implemente
|
/// Esta estructura permite manipular y renderizar un componente concreto que implemente
|
||||||
/// [`Component`], y habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
/// [`Component`], y habilita acceso concurrente mediante [`Arc<RwLock<_>>`].
|
||||||
pub struct Typed<C: Component>(Arc<RwLock<C>>);
|
#[derive(AutoDefault, Clone)]
|
||||||
|
pub struct Typed<C: Component>(Option<Arc<RwLock<C>>>);
|
||||||
impl<C: Component> Clone for Typed<C> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Component> Typed<C> {
|
impl<C: Component> Typed<C> {
|
||||||
/// Crea un nuevo `Typed` a partir de un componente.
|
/// Crea un nuevo `Typed` a partir de un componente.
|
||||||
pub fn with(component: C) -> Self {
|
pub fn with(component: C) -> Self {
|
||||||
Typed(Arc::new(RwLock::new(component)))
|
Typed(Some(Arc::new(RwLock::new(component))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typed GETTERS *******************************************************************************
|
// **< Typed BUILDER >**************************************************************************
|
||||||
|
|
||||||
/// Devuelve el identificador del componente, si está definido.
|
/// Establece un componente nuevo, o lo vacía.
|
||||||
|
///
|
||||||
|
/// Si se proporciona `Some(component)`, se encapsula como [`Typed`]; y si es `None`, se limpia.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_component(mut self, component: Option<C>) -> Self {
|
||||||
|
self.0 = component.map(|c| Arc::new(RwLock::new(c)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Typed GETTERS >**************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve el identificador del componente, si existe y está definido.
|
||||||
|
#[inline]
|
||||||
pub fn id(&self) -> Option<String> {
|
pub fn id(&self) -> Option<String> {
|
||||||
self.0.read().id()
|
self.0.as_ref().and_then(|c| c.read().id())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typed RENDER ********************************************************************************
|
// **< Typed RENDER >***************************************************************************
|
||||||
|
|
||||||
/// Renderiza el componente con el contexto proporcionado.
|
/// Renderiza el componente con el contexto proporcionado.
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
pub fn render(&self, cx: &mut Context) -> Markup {
|
||||||
self.0.write().render(cx)
|
self.0.as_ref().map_or(html! {}, |c| c.write().render(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typed HELPERS *******************************************************************************
|
// **< Typed HELPERS >**************************************************************************
|
||||||
|
|
||||||
// Convierte el componente tipado en un [`Child`].
|
// Convierte el componente tipado en un [`Child`].
|
||||||
|
#[inline]
|
||||||
fn into_child(self) -> Child {
|
fn into_child(self) -> Child {
|
||||||
Child(self.0.clone())
|
if let Some(c) = &self.0 {
|
||||||
|
Child(Some(c.clone()))
|
||||||
|
} else {
|
||||||
|
Child(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +165,7 @@ impl Children {
|
||||||
opt
|
opt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children BUILDER ****************************************************************************
|
// **< Children BUILDER >***********************************************************************
|
||||||
|
|
||||||
/// Ejecuta una operación con [`ChildOp`] en la lista.
|
/// Ejecuta una operación con [`ChildOp`] en la lista.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -175,7 +204,7 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children GETTERS ****************************************************************************
|
// **< Children GETTERS >***********************************************************************
|
||||||
|
|
||||||
/// Devuelve el número de componentes hijo de la lista.
|
/// Devuelve el número de componentes hijo de la lista.
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
|
@ -201,10 +230,10 @@ impl Children {
|
||||||
/// Devuelve un iterador sobre los componentes hijo con el identificador de tipo ([`UniqueId`])
|
/// Devuelve un iterador sobre los componentes hijo con el identificador de tipo ([`UniqueId`])
|
||||||
/// indicado.
|
/// indicado.
|
||||||
pub fn iter_by_type_id(&self, type_id: UniqueId) -> impl Iterator<Item = &Child> {
|
pub fn iter_by_type_id(&self, type_id: UniqueId) -> impl Iterator<Item = &Child> {
|
||||||
self.0.iter().filter(move |&c| c.type_id() == type_id)
|
self.0.iter().filter(move |&c| c.type_id() == Some(type_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children RENDER *****************************************************************************
|
// **< Children RENDER >************************************************************************
|
||||||
|
|
||||||
/// Renderiza todos los componentes hijo, en orden.
|
/// Renderiza todos los componentes hijo, en orden.
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
pub fn render(&self, cx: &mut Context) -> Markup {
|
||||||
|
@ -215,7 +244,7 @@ impl Children {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Children HELPERS ****************************************************************************
|
// **< Children HELPERS >***********************************************************************
|
||||||
|
|
||||||
// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -10,8 +10,8 @@ use crate::{builder_fn, join};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Operaciones para modificar el contexto ([`Context`]) de un documento.
|
/// Operaciones para modificar recursos asociados al contexto ([`Context`]) de un documento.
|
||||||
pub enum AssetsOp {
|
pub enum ContextOp {
|
||||||
// Favicon.
|
// Favicon.
|
||||||
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
|
||||||
SetFavicon(Option<Favicon>),
|
SetFavicon(Option<Favicon>),
|
||||||
|
@ -33,14 +33,14 @@ pub enum AssetsOp {
|
||||||
|
|
||||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||||
///
|
///
|
||||||
/// - [`ErrorParam::NotFound`]: la clave no existe.
|
/// - [`ContextError::ParamNotFound`]: la clave no existe.
|
||||||
/// - [`ErrorParam::TypeMismatch`]: la clave existe, pero el valor guardado no coincide con el tipo
|
/// - [`ContextError::ParamTypeMismatch`]: la clave existe, pero el valor guardado no coincide con
|
||||||
/// solicitado. Incluye nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente
|
/// el tipo solicitado. Incluye nombre de la clave (`key`), tipo esperado (`expected`) y tipo
|
||||||
/// guardado (`saved`) para facilitar el diagnóstico.
|
/// realmente guardado (`saved`) para facilitar el diagnóstico.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorParam {
|
pub enum ContextError {
|
||||||
NotFound,
|
ParamNotFound,
|
||||||
TypeMismatch {
|
ParamTypeMismatch {
|
||||||
key: &'static str,
|
key: &'static str,
|
||||||
expected: &'static str,
|
expected: &'static str,
|
||||||
saved: &'static str,
|
saved: &'static str,
|
||||||
|
@ -55,12 +55,12 @@ pub enum ErrorParam {
|
||||||
/// - Almacenar la **solicitud HTTP** de origen.
|
/// - Almacenar la **solicitud HTTP** de origen.
|
||||||
/// - Seleccionar **tema** y **composición** (*layout*) de renderizado.
|
/// - Seleccionar **tema** y **composición** (*layout*) de renderizado.
|
||||||
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
||||||
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
|
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`ContextOp`].
|
||||||
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
||||||
/// - Generar **identificadores únicos** por tipo de componente.
|
/// - Generar **identificadores únicos** por tipo de componente.
|
||||||
///
|
///
|
||||||
/// Lo implementan, típicamente, estructuras que representan el contexto de renderizado, como
|
/// Lo implementan, típicamente, estructuras que representan el contexto de renderizado, como
|
||||||
/// [`Context`](crate::html::Context) o [`Page`](crate::response::page::Page).
|
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::page::Page).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
@ -71,14 +71,14 @@ pub enum ErrorParam {
|
||||||
/// cx.with_langid(&LangMatch::resolve("es-ES"))
|
/// cx.with_langid(&LangMatch::resolve("es-ES"))
|
||||||
/// .with_theme("aliner")
|
/// .with_theme("aliner")
|
||||||
/// .with_layout("default")
|
/// .with_layout("default")
|
||||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||||
/// .with_param("usuario_id", 42_i32)
|
/// .with_param("usuario_id", 42_i32)
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait Contextual: LangId {
|
pub trait Contextual: LangId {
|
||||||
// Contextual BUILDER **************************************************************************
|
// **< Contextual BUILDER >*********************************************************************
|
||||||
|
|
||||||
/// Establece el idioma del documento.
|
/// Establece el idioma del documento.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -100,11 +100,11 @@ pub trait Contextual: LangId {
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
|
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
|
||||||
|
|
||||||
/// Define los recursos del contexto usando [`AssetsOp`].
|
/// Define los recursos del contexto usando [`ContextOp`].
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_assets(self, op: AssetsOp) -> Self;
|
fn with_assets(self, op: ContextOp) -> Self;
|
||||||
|
|
||||||
// Contextual GETTERS **************************************************************************
|
// **< Contextual GETTERS >*********************************************************************
|
||||||
|
|
||||||
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
|
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
|
||||||
fn request(&self) -> Option<&HttpRequest>;
|
fn request(&self) -> Option<&HttpRequest>;
|
||||||
|
@ -142,7 +142,7 @@ pub trait Contextual: LangId {
|
||||||
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
||||||
fn javascripts(&self) -> &Assets<JavaScript>;
|
fn javascripts(&self) -> &Assets<JavaScript>;
|
||||||
|
|
||||||
// Contextual HELPERS **************************************************************************
|
// **< Contextual HELPERS >*********************************************************************
|
||||||
|
|
||||||
/// Genera un identificador único por tipo (`<tipo>-<n>`) cuando no se aporta uno explícito.
|
/// Genera un identificador único por tipo (`<tipo>-<n>`) cuando no se aporta uno explícito.
|
||||||
///
|
///
|
||||||
|
@ -172,11 +172,11 @@ pub trait Contextual: LangId {
|
||||||
/// // Selecciona un tema (por su nombre corto).
|
/// // Selecciona un tema (por su nombre corto).
|
||||||
/// .with_theme("aliner")
|
/// .with_theme("aliner")
|
||||||
/// // Asigna un favicon.
|
/// // Asigna un favicon.
|
||||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||||
/// // Añade una hoja de estilo externa.
|
/// // Añade una hoja de estilo externa.
|
||||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
|
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
|
||||||
/// // Añade un script JavaScript.
|
/// // Añade un script JavaScript.
|
||||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
||||||
/// // Añade un parámetro dinámico al contexto.
|
/// // Añade un parámetro dinámico al contexto.
|
||||||
/// .with_param("usuario_id", 42)
|
/// .with_param("usuario_id", 42)
|
||||||
/// }
|
/// }
|
||||||
|
@ -255,7 +255,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context RENDER ******************************************************************************
|
// **< Context RENDER >*************************************************************************
|
||||||
|
|
||||||
/// Renderiza los recursos del contexto.
|
/// Renderiza los recursos del contexto.
|
||||||
pub fn render_assets(&mut self) -> Markup {
|
pub fn render_assets(&mut self) -> Markup {
|
||||||
|
@ -283,15 +283,15 @@ impl Context {
|
||||||
markup
|
markup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context PARAMS ******************************************************************************
|
// **< Context PARAMS >*************************************************************************
|
||||||
|
|
||||||
/// Recupera una *referencia tipada* al parámetro solicitado.
|
/// Recupera una *referencia tipada* al parámetro solicitado.
|
||||||
///
|
///
|
||||||
/// Devuelve:
|
/// Devuelve:
|
||||||
///
|
///
|
||||||
/// - `Ok(&T)` si la clave existe y el tipo coincide.
|
/// - `Ok(&T)` si la clave existe y el tipo coincide.
|
||||||
/// - `Err(ErrorParam::NotFound)` si la clave no existe.
|
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
|
||||||
/// - `Err(ErrorParam::TypeMismatch)` si la clave existe pero el tipo no coincide.
|
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
|
@ -308,10 +308,10 @@ impl Context {
|
||||||
/// // Error de tipo:
|
/// // Error de tipo:
|
||||||
/// assert!(cx.get_param::<String>("usuario_id").is_err());
|
/// assert!(cx.get_param::<String>("usuario_id").is_err());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get_param<T: 'static>(&self, key: &'static str) -> Result<&T, ErrorParam> {
|
pub fn get_param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
|
||||||
let (any, type_name) = self.params.get(key).ok_or(ErrorParam::NotFound)?;
|
let (any, type_name) = self.params.get(key).ok_or(ContextError::ParamNotFound)?;
|
||||||
any.downcast_ref::<T>()
|
any.downcast_ref::<T>()
|
||||||
.ok_or_else(|| ErrorParam::TypeMismatch {
|
.ok_or_else(|| ContextError::ParamTypeMismatch {
|
||||||
key,
|
key,
|
||||||
expected: TypeInfo::FullName.of::<T>(),
|
expected: TypeInfo::FullName.of::<T>(),
|
||||||
saved: type_name,
|
saved: type_name,
|
||||||
|
@ -323,8 +323,8 @@ impl Context {
|
||||||
/// Devuelve:
|
/// Devuelve:
|
||||||
///
|
///
|
||||||
/// - `Ok(T)` si la clave existía y el tipo coincide.
|
/// - `Ok(T)` si la clave existía y el tipo coincide.
|
||||||
/// - `Err(ErrorParam::NotFound)` si la clave no existe.
|
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
|
||||||
/// - `Err(ErrorParam::TypeMismatch)` si el tipo no coincide.
|
/// - `Err(ContextError::ParamTypeMismatch)` si el tipo no coincide.
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
|
@ -341,12 +341,12 @@ impl Context {
|
||||||
/// // Error de tipo:
|
/// // Error de tipo:
|
||||||
/// assert!(cx.take_param::<i32>("titulo").is_err());
|
/// assert!(cx.take_param::<i32>("titulo").is_err());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn take_param<T: 'static>(&mut self, key: &'static str) -> Result<T, ErrorParam> {
|
pub fn take_param<T: 'static>(&mut self, key: &'static str) -> Result<T, ContextError> {
|
||||||
let (boxed, saved) = self.params.remove(key).ok_or(ErrorParam::NotFound)?;
|
let (boxed, saved) = self.params.remove(key).ok_or(ContextError::ParamNotFound)?;
|
||||||
boxed
|
boxed
|
||||||
.downcast::<T>()
|
.downcast::<T>()
|
||||||
.map(|b| *b)
|
.map(|b| *b)
|
||||||
.map_err(|_| ErrorParam::TypeMismatch {
|
.map_err(|_| ContextError::ParamTypeMismatch {
|
||||||
key,
|
key,
|
||||||
expected: TypeInfo::FullName.of::<T>(),
|
expected: TypeInfo::FullName.of::<T>(),
|
||||||
saved,
|
saved,
|
||||||
|
@ -371,7 +371,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permite a [`Context`](crate::html::Context) actuar como proveedor de idioma.
|
/// Permite a [`Context`](crate::core::component::Context) actuar como proveedor de idioma.
|
||||||
///
|
///
|
||||||
/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad:
|
/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad:
|
||||||
///
|
///
|
||||||
|
@ -389,7 +389,7 @@ impl LangId for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Contextual for Context {
|
impl Contextual for Context {
|
||||||
// Contextual BUILDER **************************************************************************
|
// **< Contextual BUILDER >*********************************************************************
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
||||||
|
@ -442,36 +442,36 @@ impl Contextual for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_assets(mut self, op: AssetsOp) -> Self {
|
fn with_assets(mut self, op: ContextOp) -> Self {
|
||||||
match op {
|
match op {
|
||||||
// Favicon.
|
// Favicon.
|
||||||
AssetsOp::SetFavicon(favicon) => {
|
ContextOp::SetFavicon(favicon) => {
|
||||||
self.favicon = favicon;
|
self.favicon = favicon;
|
||||||
}
|
}
|
||||||
AssetsOp::SetFaviconIfNone(icon) => {
|
ContextOp::SetFaviconIfNone(icon) => {
|
||||||
if self.favicon.is_none() {
|
if self.favicon.is_none() {
|
||||||
self.favicon = Some(icon);
|
self.favicon = Some(icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Stylesheets.
|
// Stylesheets.
|
||||||
AssetsOp::AddStyleSheet(css) => {
|
ContextOp::AddStyleSheet(css) => {
|
||||||
self.stylesheets.add(css);
|
self.stylesheets.add(css);
|
||||||
}
|
}
|
||||||
AssetsOp::RemoveStyleSheet(path) => {
|
ContextOp::RemoveStyleSheet(path) => {
|
||||||
self.stylesheets.remove(path);
|
self.stylesheets.remove(path);
|
||||||
}
|
}
|
||||||
// JavaScripts.
|
// JavaScripts.
|
||||||
AssetsOp::AddJavaScript(js) => {
|
ContextOp::AddJavaScript(js) => {
|
||||||
self.javascripts.add(js);
|
self.javascripts.add(js);
|
||||||
}
|
}
|
||||||
AssetsOp::RemoveJavaScript(path) => {
|
ContextOp::RemoveJavaScript(path) => {
|
||||||
self.javascripts.remove(path);
|
self.javascripts.remove(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contextual GETTERS **************************************************************************
|
// **< Contextual GETTERS >*********************************************************************
|
||||||
|
|
||||||
fn request(&self) -> Option<&HttpRequest> {
|
fn request(&self) -> Option<&HttpRequest> {
|
||||||
self.request.as_ref()
|
self.request.as_ref()
|
||||||
|
@ -530,7 +530,7 @@ impl Contextual for Context {
|
||||||
&self.javascripts
|
&self.javascripts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contextual HELPERS **************************************************************************
|
// **< Contextual HELPERS >*********************************************************************
|
||||||
|
|
||||||
/// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona
|
/// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona
|
||||||
/// un `id` explícito.
|
/// un `id` explícito.
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
|
use crate::core::component::Context;
|
||||||
use crate::core::{AnyInfo, TypeInfo};
|
use crate::core::{AnyInfo, TypeInfo};
|
||||||
use crate::html::{html, Context, Markup, PrepareMarkup};
|
use crate::html::{html, Markup, PrepareMarkup};
|
||||||
|
|
||||||
/// Define la función de renderizado para todos los componentes.
|
/// Define la función de renderizado para todos los componentes.
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
use crate::builder_fn;
|
|
||||||
use crate::core::component::{Component, Typed};
|
|
||||||
use crate::html::{html, Context, Markup};
|
|
||||||
|
|
||||||
/// Contenedor para un componente [`Typed`] opcional.
|
|
||||||
///
|
|
||||||
/// Un `TypedSlot` actúa como un contenedor dentro de otro componente para incluir o no un
|
|
||||||
/// subcomponente. Internamente encapsula `Option<Typed<C>>`, pero proporciona una API más sencilla
|
|
||||||
/// para construir estructuras jerárquicas.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// let comp = MyComponent::new();
|
|
||||||
/// let opt = TypedSlot::new(comp);
|
|
||||||
/// assert!(opt.get().is_some());
|
|
||||||
/// ```
|
|
||||||
pub struct TypedSlot<C: Component>(Option<Typed<C>>);
|
|
||||||
|
|
||||||
impl<C: Component> Default for TypedSlot<C> {
|
|
||||||
fn default() -> Self {
|
|
||||||
TypedSlot(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Component> TypedSlot<C> {
|
|
||||||
/// Crea un nuevo [`TypedSlot`].
|
|
||||||
///
|
|
||||||
/// El componente se envuelve automáticamente en un [`Typed`] y se almacena.
|
|
||||||
pub fn new(component: C) -> Self {
|
|
||||||
TypedSlot(Some(Typed::with(component)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypedSlot BUILDER *********************************************************************
|
|
||||||
|
|
||||||
/// Establece un componente nuevo, o lo vacía.
|
|
||||||
///
|
|
||||||
/// Si se proporciona `Some(component)`, se guarda en [`Typed`]; y si es `None`, se limpia.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_value(mut self, component: Option<C>) -> Self {
|
|
||||||
self.0 = component.map(Typed::with);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypedSlot GETTERS *********************************************************************
|
|
||||||
|
|
||||||
/// Devuelve un clon (incrementa el contador `Arc`) de [`Typed<C>`], si existe.
|
|
||||||
pub fn get(&self) -> Option<Typed<C>> {
|
|
||||||
self.0.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypedSlot RENDER ************************************************************************
|
|
||||||
|
|
||||||
/// Renderiza el componente, si existe.
|
|
||||||
pub fn render(&self, cx: &mut Context) -> Markup {
|
|
||||||
if let Some(component) = &self.0 {
|
|
||||||
component.render(cx)
|
|
||||||
} else {
|
|
||||||
html! {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@ use parking_lot::RwLock;
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// EXTENSIONES *************************************************************************************
|
// **< EXTENSIONES >********************************************************************************
|
||||||
|
|
||||||
static ENABLED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
|
static ENABLED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
|
||||||
LazyLock::new(|| RwLock::new(Vec::new()));
|
LazyLock::new(|| RwLock::new(Vec::new()));
|
||||||
|
@ -15,7 +15,7 @@ static ENABLED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
|
||||||
static DROPPED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
|
static DROPPED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
|
||||||
LazyLock::new(|| RwLock::new(Vec::new()));
|
LazyLock::new(|| RwLock::new(Vec::new()));
|
||||||
|
|
||||||
// REGISTRO DE LAS EXTENSIONES *********************************************************************
|
// **< REGISTRO DE LAS EXTENSIONES >****************************************************************
|
||||||
|
|
||||||
pub fn register_extensions(root_extension: Option<ExtensionRef>) {
|
pub fn register_extensions(root_extension: Option<ExtensionRef>) {
|
||||||
// Prepara la lista de extensiones habilitadas.
|
// Prepara la lista de extensiones habilitadas.
|
||||||
|
@ -104,7 +104,7 @@ fn add_to_dropped(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// REGISTRO DE LAS ACCIONES ************************************************************************
|
// **< REGISTRO DE LAS ACCIONES >*******************************************************************
|
||||||
|
|
||||||
pub fn register_actions() {
|
pub fn register_actions() {
|
||||||
for extension in ENABLED_EXTENSIONS.read().iter() {
|
for extension in ENABLED_EXTENSIONS.read().iter() {
|
||||||
|
@ -114,7 +114,7 @@ pub fn register_actions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// INICIALIZA LAS EXTENSIONES **********************************************************************
|
// **< INICIALIZA LAS EXTENSIONES >*****************************************************************
|
||||||
|
|
||||||
pub fn initialize_extensions() {
|
pub fn initialize_extensions() {
|
||||||
trace::info!("Calling application bootstrap");
|
trace::info!("Calling application bootstrap");
|
||||||
|
@ -123,7 +123,7 @@ pub fn initialize_extensions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONFIGURA LOS SERVICIOS *************************************************************************
|
// **< CONFIGURA LOS SERVICIOS >********************************************************************
|
||||||
|
|
||||||
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
|
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
|
||||||
// Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos.
|
// Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos.
|
||||||
|
|
|
@ -5,11 +5,11 @@ use parking_lot::RwLock;
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// TEMAS *******************************************************************************************
|
// **< TEMAS >**************************************************************************************
|
||||||
|
|
||||||
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
|
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
|
||||||
|
|
||||||
// TEMA PREDETERMINADO *****************************************************************************
|
// **< TEMA PREDETERMINADO >************************************************************************
|
||||||
|
|
||||||
pub static DEFAULT_THEME: LazyLock<ThemeRef> =
|
pub static DEFAULT_THEME: LazyLock<ThemeRef> =
|
||||||
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
|
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
|
||||||
|
@ -17,7 +17,7 @@ pub static DEFAULT_THEME: LazyLock<ThemeRef> =
|
||||||
None => &crate::base::theme::Basic,
|
None => &crate::base::theme::Basic,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TEMA POR NOMBRE *********************************************************************************
|
// **< TEMA POR NOMBRE >****************************************************************************
|
||||||
|
|
||||||
// Devuelve el tema identificado por su [`short_name()`](AnyInfo::short_name).
|
// Devuelve el tema identificado por su [`short_name()`](AnyInfo::short_name).
|
||||||
pub fn theme_by_short_name(short_name: &'static str) -> Option<ThemeRef> {
|
pub fn theme_by_short_name(short_name: &'static str) -> Option<ThemeRef> {
|
||||||
|
|
|
@ -50,11 +50,11 @@ pub struct App {
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
/// Idioma por defecto para la aplicación.
|
/// Idioma por defecto para la aplicación.
|
||||||
///
|
///
|
||||||
/// Si no está definido o no es válido, el idioma efectivo para el renderizado se resolverá
|
/// Si no está definido o no es válido, [`LangId`](crate::locale::LangId) determinará el idioma
|
||||||
/// según la implementación de [`LangId`](crate::locale::LangId) en este orden: primero intenta
|
/// efectivo para el renderizado en este orden: primero intentará usar el establecido mediante
|
||||||
/// con el establecido en [`Contextual::with_langid()`](crate::html::Contextual::with_langid);
|
/// [`Contextual::with_langid()`](crate::core::component::Contextual::with_langid); si no se ha
|
||||||
/// pero si no se ha definido explícitamente, usará el indicado en la cabecera `Accept-Language`
|
/// definido explícitamente, probará el indicado en la cabecera `Accept-Language` del navegador;
|
||||||
/// del navegador; y, si ninguno aplica, se empleará el idioma de respaldo ("en-US").
|
/// y, si ninguno aplica, se empleará el idioma de respaldo ("en-US").
|
||||||
pub language: String,
|
pub language: String,
|
||||||
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
|
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
|
||||||
/// *"Starwars"*.
|
/// *"Starwars"*.
|
||||||
|
|
43
src/html.rs
43
src/html.rs
|
@ -3,7 +3,7 @@
|
||||||
mod maud;
|
mod maud;
|
||||||
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE};
|
pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE};
|
||||||
|
|
||||||
// HTML DOCUMENT ASSETS ****************************************************************************
|
// **< HTML DOCUMENT ASSETS >***********************************************************************
|
||||||
|
|
||||||
mod assets;
|
mod assets;
|
||||||
pub use assets::favicon::Favicon;
|
pub use assets::favicon::Favicon;
|
||||||
|
@ -11,12 +11,38 @@ pub use assets::javascript::JavaScript;
|
||||||
pub use assets::stylesheet::{StyleSheet, TargetMedia};
|
pub use assets::stylesheet::{StyleSheet, TargetMedia};
|
||||||
pub use assets::{Asset, Assets};
|
pub use assets::{Asset, Assets};
|
||||||
|
|
||||||
// HTML DOCUMENT CONTEXT ***************************************************************************
|
// **< HTML DOCUMENT CONTEXT >**********************************************************************
|
||||||
|
|
||||||
mod context;
|
/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::Context`] en su lugar.
|
||||||
pub use context::{AssetsOp, Context, Contextual, ErrorParam};
|
#[deprecated(since = "0.5.0", note = "Moved to `pagetop::core::component::Context`")]
|
||||||
|
pub type Context = crate::core::component::Context;
|
||||||
|
|
||||||
// HTML ATTRIBUTES *********************************************************************************
|
/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::ContextOp`] en su lugar.
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.5.0",
|
||||||
|
note = "Moved to `pagetop::core::component::ContextOp`"
|
||||||
|
)]
|
||||||
|
pub type ContextOp = crate::core::component::ContextOp;
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::Contextual`] en su lugar.
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.5.0",
|
||||||
|
note = "Moved to `pagetop::core::component::Contextual`"
|
||||||
|
)]
|
||||||
|
pub trait Contextual: crate::core::component::Contextual {}
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::ContextError`] en su lugar.
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.5.0",
|
||||||
|
note = "Moved to `pagetop::core::component::ContextError`"
|
||||||
|
)]
|
||||||
|
pub type ContextError = crate::core::component::ContextError;
|
||||||
|
|
||||||
|
/// **Obsoleto desde la versión 0.5.0**: usar [`ContextOp`] en su lugar.
|
||||||
|
#[deprecated(since = "0.5.0", note = "Use `ContextOp` instead")]
|
||||||
|
pub type AssetsOp = crate::core::component::ContextOp;
|
||||||
|
|
||||||
|
// **< HTML ATTRIBUTES >****************************************************************************
|
||||||
|
|
||||||
mod attr_id;
|
mod attr_id;
|
||||||
pub use attr_id::AttrId;
|
pub use attr_id::AttrId;
|
||||||
|
@ -50,14 +76,13 @@ pub type OptionClasses = AttrClasses;
|
||||||
|
|
||||||
use crate::{core, AutoDefault};
|
use crate::{core, AutoDefault};
|
||||||
|
|
||||||
/// **Obsoleto desde la versión 0.4.0**: usar [`TypedSlot`](crate::core::component::TypedSlot) en su
|
/// **Obsoleto desde la versión 0.4.0**: usar [`Typed`](crate::core::component::Typed) en su lugar.
|
||||||
/// lugar.
|
|
||||||
#[deprecated(
|
#[deprecated(
|
||||||
since = "0.4.0",
|
since = "0.4.0",
|
||||||
note = "Use `pagetop::core::component::TypedSlot` instead"
|
note = "Use `pagetop::core::component::Typed` instead"
|
||||||
)]
|
)]
|
||||||
#[allow(type_alias_bounds)]
|
#[allow(type_alias_bounds)]
|
||||||
pub type OptionComponent<C: core::component::Component> = core::component::TypedSlot<C>;
|
pub type OptionComponent<C: core::component::Component> = core::component::Typed<C>;
|
||||||
|
|
||||||
/// Prepara contenido HTML para su conversión a [`Markup`].
|
/// Prepara contenido HTML para su conversión a [`Markup`].
|
||||||
///
|
///
|
||||||
|
|
|
@ -2,7 +2,8 @@ pub mod favicon;
|
||||||
pub mod javascript;
|
pub mod javascript;
|
||||||
pub mod stylesheet;
|
pub mod stylesheet;
|
||||||
|
|
||||||
use crate::html::{html, Context, Markup};
|
use crate::core::component::Context;
|
||||||
|
use crate::html::{html, Markup};
|
||||||
use crate::{AutoDefault, Weight};
|
use crate::{AutoDefault, Weight};
|
||||||
|
|
||||||
/// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de
|
/// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::html::{html, Context, Markup};
|
use crate::core::component::Context;
|
||||||
|
use crate::html::{html, Markup};
|
||||||
use crate::AutoDefault;
|
use crate::AutoDefault;
|
||||||
|
|
||||||
/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio.
|
/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio.
|
||||||
|
@ -52,7 +53,7 @@ impl Favicon {
|
||||||
Favicon::default()
|
Favicon::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favicon BUILDER *****************************************************************************
|
// **< Favicon BUILDER >************************************************************************
|
||||||
|
|
||||||
/// Le añade un icono genérico apuntando a `image`. El tipo MIME se infiere automáticamente a
|
/// Le añade un icono genérico apuntando a `image`. El tipo MIME se infiere automáticamente a
|
||||||
/// partir de la extensión.
|
/// partir de la extensión.
|
||||||
|
@ -152,6 +153,8 @@ impl Favicon {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< Favicon RENDER >*************************************************************************
|
||||||
|
|
||||||
/// Renderiza el **Favicon** completo con todas las etiquetas declaradas.
|
/// Renderiza el **Favicon** completo con todas las etiquetas declaradas.
|
||||||
///
|
///
|
||||||
/// El parámetro `Context` se acepta por coherencia con el resto de *assets*, aunque en este
|
/// El parámetro `Context` se acepta por coherencia con el resto de *assets*, aunque en este
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use crate::core::component::Context;
|
||||||
use crate::html::assets::Asset;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{html, Context, Markup, PreEscaped};
|
use crate::html::{html, Markup, PreEscaped};
|
||||||
use crate::{join, join_pair, AutoDefault, Weight};
|
use crate::{join, join_pair, AutoDefault, Weight};
|
||||||
|
|
||||||
// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
|
// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
|
||||||
|
@ -171,7 +172,7 @@ impl JavaScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript BUILDER **************************************************************************
|
// **< JavaScript BUILDER >*********************************************************************
|
||||||
|
|
||||||
/// Asocia una **versión** al recurso (usada para control de la caché del navegador).
|
/// Asocia una **versión** al recurso (usada para control de la caché del navegador).
|
||||||
///
|
///
|
||||||
|
@ -210,6 +211,8 @@ impl Asset for JavaScript {
|
||||||
self.weight
|
self.weight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< JavaScript RENDER >**********************************************************************
|
||||||
|
|
||||||
fn render(&self, cx: &mut Context) -> Markup {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => html! {
|
Source::From(path) => html! {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use crate::core::component::Context;
|
||||||
use crate::html::assets::Asset;
|
use crate::html::assets::Asset;
|
||||||
use crate::html::{html, Context, Markup, PreEscaped};
|
use crate::html::{html, Markup, PreEscaped};
|
||||||
use crate::{join_pair, AutoDefault, Weight};
|
use crate::{join_pair, AutoDefault, Weight};
|
||||||
|
|
||||||
// Define el origen del recurso CSS y cómo se incluye en el documento.
|
// Define el origen del recurso CSS y cómo se incluye en el documento.
|
||||||
|
@ -113,7 +114,7 @@ impl StyleSheet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StyleSheet BUILDER **************************************************************************
|
// **< StyleSheet BUILDER >*********************************************************************
|
||||||
|
|
||||||
/// Asocia una versión al recurso (usada para control de la caché del navegador).
|
/// Asocia una versión al recurso (usada para control de la caché del navegador).
|
||||||
///
|
///
|
||||||
|
@ -132,7 +133,7 @@ impl StyleSheet {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// StyleSheet EXTRAS ***************************************************************************
|
// **< StyleSheet HELPERS >*********************************************************************
|
||||||
|
|
||||||
/// Especifica el medio donde se aplican los estilos.
|
/// Especifica el medio donde se aplican los estilos.
|
||||||
///
|
///
|
||||||
|
@ -163,6 +164,8 @@ impl Asset for StyleSheet {
|
||||||
self.weight
|
self.weight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< StyleSheet RENDER >**********************************************************************
|
||||||
|
|
||||||
fn render(&self, cx: &mut Context) -> Markup {
|
fn render(&self, cx: &mut Context) -> Markup {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
Source::From(path) => html! {
|
Source::From(path) => html! {
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl AttrClasses {
|
||||||
AttrClasses::default().with_value(ClassesOp::Prepend, classes)
|
AttrClasses::default().with_value(ClassesOp::Prepend, classes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrClasses BUILDER *************************************************************************
|
// **< AttrClasses BUILDER >********************************************************************
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
pub fn with_value(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
|
@ -114,7 +114,7 @@ impl AttrClasses {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrClasses GETTERS *************************************************************************
|
// **< AttrClasses GETTERS >********************************************************************
|
||||||
|
|
||||||
/// Devuelve la cadena de clases, si existe.
|
/// Devuelve la cadena de clases, si existe.
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl AttrId {
|
||||||
AttrId::default().with_value(value)
|
AttrId::default().with_value(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrId BUILDER ******************************************************************************
|
// **< AttrId BUILDER >*************************************************************************
|
||||||
|
|
||||||
/// Establece un identificador nuevo normalizando el valor.
|
/// Establece un identificador nuevo normalizando el valor.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -39,7 +39,7 @@ impl AttrId {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrId GETTERS ******************************************************************************
|
// **< AttrId GETTERS >*************************************************************************
|
||||||
|
|
||||||
/// Devuelve el identificador normalizado, si existe.
|
/// Devuelve el identificador normalizado, si existe.
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl AttrL10n {
|
||||||
AttrL10n(value)
|
AttrL10n(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrL10n BUILDER ****************************************************************************
|
// **< AttrL10n BUILDER >***********************************************************************
|
||||||
|
|
||||||
/// Establece una traducción nueva.
|
/// Establece una traducción nueva.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -48,7 +48,7 @@ impl AttrL10n {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrL10n GETTERS ****************************************************************************
|
// **< AttrL10n GETTERS >***********************************************************************
|
||||||
|
|
||||||
/// Devuelve la traducción para `language`, si existe.
|
/// Devuelve la traducción para `language`, si existe.
|
||||||
pub fn lookup(&self, language: &impl LangId) -> Option<String> {
|
pub fn lookup(&self, language: &impl LangId) -> Option<String> {
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl AttrName {
|
||||||
AttrName::default().with_value(value)
|
AttrName::default().with_value(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrName BUILDER ****************************************************************************
|
// **< AttrName BUILDER >***********************************************************************
|
||||||
|
|
||||||
/// Establece un nombre nuevo normalizando el valor.
|
/// Establece un nombre nuevo normalizando el valor.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -39,7 +39,7 @@ impl AttrName {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrName GETTERS ****************************************************************************
|
// **< AttrName GETTERS >***********************************************************************
|
||||||
|
|
||||||
/// Devuelve el nombre normalizado, si existe.
|
/// Devuelve el nombre normalizado, si existe.
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
|
|
|
@ -27,7 +27,7 @@ impl AttrValue {
|
||||||
AttrValue::default().with_value(value)
|
AttrValue::default().with_value(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrValue BUILDER ***************************************************************************
|
// **< AttrValue BUILDER >**********************************************************************
|
||||||
|
|
||||||
/// Establece una cadena nueva normalizando el valor.
|
/// Establece una cadena nueva normalizando el valor.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -41,7 +41,7 @@ impl AttrValue {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttrValue GETTERS ***************************************************************************
|
// **< AttrValue GETTERS >**********************************************************************
|
||||||
|
|
||||||
/// Devuelve la cadena normalizada, si existe.
|
/// Devuelve la cadena normalizada, si existe.
|
||||||
pub fn get(&self) -> Option<String> {
|
pub fn get(&self) -> Option<String> {
|
||||||
|
|
|
@ -97,7 +97,7 @@ extern crate self as pagetop;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
// RE-EXPORTED *************************************************************************************
|
// **< RE-EXPORTED >********************************************************************************
|
||||||
|
|
||||||
pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault};
|
pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault};
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ pub type UniqueId = std::any::TypeId;
|
||||||
/// antes en la ordenación.
|
/// antes en la ordenación.
|
||||||
pub type Weight = i8;
|
pub type Weight = i8;
|
||||||
|
|
||||||
// API *********************************************************************************************
|
// **< API >****************************************************************************************
|
||||||
|
|
||||||
// Macros y funciones útiles.
|
// Macros y funciones útiles.
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
@ -163,6 +163,6 @@ pub mod base;
|
||||||
// Prepara y ejecuta la aplicación.
|
// Prepara y ejecuta la aplicación.
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
// PRELUDE *****************************************************************************************
|
// **< PRELUDE >************************************************************************************
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
|
@ -28,7 +28,14 @@ pub use crate::global;
|
||||||
|
|
||||||
pub use crate::trace;
|
pub use crate::trace;
|
||||||
|
|
||||||
pub use crate::html::*;
|
// No se usa `pub use crate::html::*;` para evitar duplicar alias marcados como obsoletos
|
||||||
|
// (*deprecated*) porque han sido trasladados a `crate::core::component`. Cuando se retiren estos
|
||||||
|
// alias obsoletos se volverá a declarar como `pub use crate::html::*;`.
|
||||||
|
pub use crate::html::{
|
||||||
|
display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue,
|
||||||
|
ClassesOp, Escaper, Favicon, JavaScript, Markup, PreEscaped, PrepareMarkup, StyleSheet,
|
||||||
|
TargetMedia, DOCTYPE,
|
||||||
|
};
|
||||||
|
|
||||||
pub use crate::locale::*;
|
pub use crate::locale::*;
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@ pub use error::ErrorPage;
|
||||||
pub use actix_web::Result as ResultPage;
|
pub use actix_web::Result as ResultPage;
|
||||||
|
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::core::component::{Child, ChildOp, Component};
|
use crate::core::component::{Child, ChildOp, Component, Context, ContextOp, Contextual};
|
||||||
use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT};
|
use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT};
|
||||||
use crate::html::{html, Markup, DOCTYPE};
|
use crate::html::{html, Markup, DOCTYPE};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
use crate::html::{AssetsOp, Context, Contextual};
|
|
||||||
use crate::html::{AttrClasses, ClassesOp};
|
use crate::html::{AttrClasses, ClassesOp};
|
||||||
use crate::html::{AttrId, AttrL10n};
|
use crate::html::{AttrId, AttrL10n};
|
||||||
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
||||||
|
@ -52,7 +51,7 @@ impl Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page BUILDER ********************************************************************************
|
// **< Page BUILDER >***************************************************************************
|
||||||
|
|
||||||
/// Establece el título de la página como un valor traducible.
|
/// Establece el título de la página como un valor traducible.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
@ -151,7 +150,7 @@ impl Page {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page GETTERS ********************************************************************************
|
// **< Page GETTERS >***************************************************************************
|
||||||
|
|
||||||
/// Devuelve el título traducido para el idioma de la página, si existe.
|
/// Devuelve el título traducido para el idioma de la página, si existe.
|
||||||
pub fn title(&mut self) -> Option<String> {
|
pub fn title(&mut self) -> Option<String> {
|
||||||
|
@ -192,7 +191,7 @@ impl Page {
|
||||||
&mut self.context
|
&mut self.context
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page RENDER *********************************************************************************
|
// **< Page RENDER >****************************************************************************
|
||||||
|
|
||||||
/// Renderiza los componentes de una región (`region_name`) de la página.
|
/// Renderiza los componentes de una región (`region_name`) de la página.
|
||||||
pub fn render_region(&mut self, region_name: &'static str) -> Markup {
|
pub fn render_region(&mut self, region_name: &'static str) -> Markup {
|
||||||
|
@ -253,7 +252,7 @@ impl LangId for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Contextual for Page {
|
impl Contextual for Page {
|
||||||
// Contextual BUILDER **************************************************************************
|
// **< Contextual BUILDER >*********************************************************************
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
|
||||||
|
@ -286,12 +285,12 @@ impl Contextual for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_assets(mut self, op: AssetsOp) -> Self {
|
fn with_assets(mut self, op: ContextOp) -> Self {
|
||||||
self.context.alter_assets(op);
|
self.context.alter_assets(op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contextual GETTERS **************************************************************************
|
// **< Contextual GETTERS >*********************************************************************
|
||||||
|
|
||||||
fn request(&self) -> Option<&HttpRequest> {
|
fn request(&self) -> Option<&HttpRequest> {
|
||||||
self.context.request()
|
self.context.request()
|
||||||
|
@ -321,7 +320,7 @@ impl Contextual for Page {
|
||||||
self.context.javascripts()
|
self.context.javascripts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contextual HELPERS **************************************************************************
|
// **< Contextual HELPERS >*********************************************************************
|
||||||
|
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||||
self.context.required_id::<T>(id)
|
self.context.required_id::<T>(id)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::base::component::Html;
|
use crate::base::component::Html;
|
||||||
use crate::html::Contextual;
|
use crate::core::component::Contextual;
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::response::ResponseError;
|
use crate::response::ResponseError;
|
||||||
use crate::service::http::{header::ContentType, StatusCode};
|
use crate::service::http::{header::ContentType, StatusCode};
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::env;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
// MACROS INTEGRADAS *******************************************************************************
|
// **< MACROS INTEGRADAS >**************************************************************************
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use paste::paste;
|
pub use paste::paste;
|
||||||
|
@ -16,7 +16,7 @@ pub use concat_string::concat_string;
|
||||||
|
|
||||||
pub use indoc::{concatdoc, formatdoc, indoc};
|
pub use indoc::{concatdoc, formatdoc, indoc};
|
||||||
|
|
||||||
// MACROS ÚTILES ***********************************************************************************
|
// **< MACROS ÚTILES >******************************************************************************
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
/// Macro para construir una colección de pares clave-valor.
|
/// Macro para construir una colección de pares clave-valor.
|
||||||
|
@ -198,7 +198,7 @@ macro_rules! join_strict {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUNCIONES ÚTILES ********************************************************************************
|
// **< FUNCIONES ÚTILES >***************************************************************************
|
||||||
|
|
||||||
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
|
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
|
||||||
///
|
///
|
||||||
|
|
|
@ -3,9 +3,3 @@
|
||||||
.region--footer {
|
.region--footer {
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PoweredBy component */
|
|
||||||
|
|
||||||
.poweredby {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
12
static/css/components.css
Normal file
12
static/css/components.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/* Icon component */
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PoweredBy component */
|
||||||
|
|
||||||
|
.poweredby {
|
||||||
|
text-align: center;
|
||||||
|
}
|
300
static/css/menu.css
Normal file
300
static/css/menu.css
Normal file
|
@ -0,0 +1,300 @@
|
||||||
|
.menu {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: var(--val-menu--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__wrapper {
|
||||||
|
padding-right: var(--val-gap);
|
||||||
|
}
|
||||||
|
.menu__wrapper a,
|
||||||
|
.menu__wrapper button {
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__nav ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.menu__nav li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0 0 1.5rem;
|
||||||
|
padding: 0;
|
||||||
|
line-height: var(--val-menu--item-height);
|
||||||
|
list-style: none;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__item--label,
|
||||||
|
.menu__nav li > a {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--val-color--text);
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
.menu__nav li > a {
|
||||||
|
border: none;
|
||||||
|
transition: color 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.menu__nav li:hover > a,
|
||||||
|
.menu__nav li > a:focus {
|
||||||
|
color: var(--val-menu--color-highlight);
|
||||||
|
}
|
||||||
|
.menu__nav li > a > svg.icon {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__children {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: var(--val-gap-0-5) var(--val-gap-1-5);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: var(--val-menu--color-bg);
|
||||||
|
border-top: 3px solid var(--val-menu--color-highlight);
|
||||||
|
z-index: 500;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
box-shadow: 0 4px 6px -1px var(--val-menu--color-border), 0 2px 4px -1px var(--val-menu--color-shadow);
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__item--children:hover > .menu__children,
|
||||||
|
.menu__item--children > a:focus + .menu__children,
|
||||||
|
.menu__item--children .menu__children:focus-within {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__submenu {
|
||||||
|
min-width: var(--val-menu--item-width-min);
|
||||||
|
max-width: var(--val-menu--item-width-max);
|
||||||
|
}
|
||||||
|
.menu__submenu-title {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--val-menu--line-padding) 0;
|
||||||
|
line-height: var(--val-menu--line-height);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--val-menu--color-highlight);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
.menu__submenu li {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__children--mega {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__mega {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__header,
|
||||||
|
.menu__trigger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Applies <= 992px */
|
||||||
|
@media only screen and (max-width: 62rem) {
|
||||||
|
.menu__wrapper {
|
||||||
|
padding-right: var(--val-gap-0-5);
|
||||||
|
}
|
||||||
|
.menu__trigger {
|
||||||
|
cursor: pointer;
|
||||||
|
width: var(--val-menu--trigger-width);
|
||||||
|
height: var(--val-menu--item-height);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.menu__trigger svg.icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
.menu__nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: var(--val-menu--side-width);
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9099;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--val-menu--color-bg);
|
||||||
|
transform: translate(-100%);
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
.menu__nav.active {
|
||||||
|
transform: translate(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__nav li {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
line-height: var(--val-menu--line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__item--label,
|
||||||
|
.menu__nav li > a {
|
||||||
|
display: block;
|
||||||
|
padding: var(--val-menu--line-padding) var(--val-menu--item-height) var(--val-menu--line-padding) var(--val-menu--item-gap);
|
||||||
|
border-bottom: 1px solid var(--val-menu--color-border);
|
||||||
|
}
|
||||||
|
.menu__nav li ul li.menu__item--label,
|
||||||
|
.menu__nav li ul li > a {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
.menu__nav li > a > svg.icon {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--val-menu--line-padding);
|
||||||
|
right: var(--val-menu--line-padding);
|
||||||
|
height: var(--val-menu--line-height);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__children {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
max-width: none;
|
||||||
|
min-width: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 0;
|
||||||
|
opacity: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateX(0%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.menu__children.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.menu__children > :first-child {
|
||||||
|
margin-top: 2.675rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__submenu-title {
|
||||||
|
padding: var(--val-menu--line-padding) var(--val-menu--item-height) var(--val-menu--line-padding) var(--val-menu--item-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__mega {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__header {
|
||||||
|
position: sticky;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
top: 0;
|
||||||
|
height: var(--val-menu--item-height);
|
||||||
|
border-bottom: 1px solid var(--val-menu--color-border);
|
||||||
|
background: var(--val-menu--color-bg);
|
||||||
|
z-index: 501;
|
||||||
|
}
|
||||||
|
.menu__title {
|
||||||
|
padding: var(--val-menu--line-padding);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.menu__close,
|
||||||
|
.menu__back {
|
||||||
|
width: var(--val-menu--item-height);
|
||||||
|
min-width: var(--val-menu--item-height);
|
||||||
|
height: var(--val-menu--item-height);
|
||||||
|
line-height: var(--val-menu--item-height);
|
||||||
|
color: var(--val-color--text);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.menu__close {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
border-left: 1px solid var(--val-menu--color-border) !important;
|
||||||
|
}
|
||||||
|
.menu__back {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
border-right: 1px solid var(--val-menu--color-border) !important;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.menu__header.active .menu__back {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__list {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9098;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
.menu__overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATIONS */
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRight {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
212
static/css/root.css
Normal file
212
static/css/root.css
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
:root {
|
||||||
|
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
||||||
|
--val-font-serif: "Lora","georgia",serif;
|
||||||
|
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||||
|
--val-font-family: var(--val-font-sans);
|
||||||
|
|
||||||
|
/* Font size */
|
||||||
|
--val-fs--x3l: 2.5rem;
|
||||||
|
--val-fs--x2l: 2rem;
|
||||||
|
--val-fs--xl: 1.75rem;
|
||||||
|
--val-fs--l: 1.5rem;
|
||||||
|
--val-fs--m: 1.25rem;
|
||||||
|
--val-fs--base: 1rem;
|
||||||
|
--val-fs--s: 0.875rem;
|
||||||
|
--val-fs--xs: 0.75rem;
|
||||||
|
--val-fs--x2s: 0.5625rem;
|
||||||
|
--val-fs--x3s: 0.375rem;
|
||||||
|
|
||||||
|
/* Font weight */
|
||||||
|
--val-fw--light: 300;
|
||||||
|
--val-fw--base: 400;
|
||||||
|
--val-fw--bold: 500;
|
||||||
|
|
||||||
|
/* Line height */
|
||||||
|
--val-lh--base: 1.5;
|
||||||
|
--val-lh--header: 1.2;
|
||||||
|
|
||||||
|
--val-max-width: 90rem;
|
||||||
|
/*
|
||||||
|
--val-color-rgb: 33,37,41;
|
||||||
|
--val-main--bg-rgb: 255,255,255;
|
||||||
|
--val-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
|
||||||
|
--line-height-base: 1.6875rem;
|
||||||
|
--line-height-s: 1.125rem;
|
||||||
|
--max-bg-color: 98.125rem;
|
||||||
|
*/
|
||||||
|
--val-gap: 1.125rem;
|
||||||
|
/*
|
||||||
|
--content-left: 5.625rem;
|
||||||
|
--site-header-height-wide: var(--val-gap10);
|
||||||
|
--container-padding: var(--val-gap);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
@media (min-width: 75rem) {
|
||||||
|
:root {
|
||||||
|
--container-padding:var(--val-gap2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--scrollbar-width: 0px;
|
||||||
|
--grid-col-count: 6;
|
||||||
|
--grid-gap: var(--val-gap);
|
||||||
|
--grid-gap-count: calc(var(--grid-col-count) - 1);
|
||||||
|
--grid-full-width: calc(100vw - var(--val-gap2) - var(--scrollbar-width));
|
||||||
|
--grid-col-width: calc((var(--grid-full-width) - (var(--grid-gap-count) * var(--grid-gap))) / var(--grid-col-count));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 43.75rem) {
|
||||||
|
:root {
|
||||||
|
--grid-col-count:14;
|
||||||
|
--grid-gap: var(--val-gap2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 62.5rem) {
|
||||||
|
:root {
|
||||||
|
--scrollbar-width:0.9375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 75rem) {
|
||||||
|
:root {
|
||||||
|
--grid-full-width:calc(100vw - var(--scrollbar-width) - var(--content-left) - var(--val-gap4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 90rem) {
|
||||||
|
:root {
|
||||||
|
--grid-full-width:calc(var(--max-width) - var(--val-gap4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--val-gap-0-15: calc(0.15 * var(--val-gap));
|
||||||
|
--val-gap-0-25: calc(0.25 * var(--val-gap));
|
||||||
|
--val-gap-0-35: calc(0.35 * var(--val-gap));
|
||||||
|
--val-gap-0-5: calc(0.5 * var(--val-gap));
|
||||||
|
--val-gap-0-75: calc(0.75 * var(--val-gap));
|
||||||
|
--val-gap-1-5: calc(1.5 * var(--val-gap));
|
||||||
|
--val-gap-2: calc(2 * var(--val-gap));
|
||||||
|
|
||||||
|
--primary-hue: 216;
|
||||||
|
--primary-sat: 60%;
|
||||||
|
--val-color--primary: hsl(var(--primary-hue), var(--primary-sat), 50%);
|
||||||
|
--val-color--primary-light: hsl(var(--primary-hue), var(--primary-sat), 60%);
|
||||||
|
--val-color--primary-dark: hsl(var(--primary-hue), var(--primary-sat), 40%);
|
||||||
|
--val-color--primary-link: hsl(var(--primary-hue), var(--primary-sat), 55%);
|
||||||
|
--val-color--primary-link-hover: hsl(var(--primary-hue), var(--primary-sat), 30%);
|
||||||
|
--val-color--primary-link-active: hsl(var(--primary-hue), var(--primary-sat), 70%);
|
||||||
|
|
||||||
|
--info-hue: 190;
|
||||||
|
--info-sat: 90%;
|
||||||
|
--val-color--info: hsl(var(--info-hue), var(--info-sat), 54%);
|
||||||
|
--val-color--info-light: hsl(var(--info-hue), var(--info-sat), 70%);
|
||||||
|
--val-color--info-dark: hsl(var(--info-hue), var(--info-sat), 45%);
|
||||||
|
--val-color--info-link: hsl(var(--info-hue), var(--info-sat), 30%);
|
||||||
|
--val-color--info-link-hover: hsl(var(--info-hue), var(--info-sat), 20%);
|
||||||
|
--val-color--info-link-active: hsl(var(--info-hue), var(--info-sat), 40%);
|
||||||
|
|
||||||
|
--success-hue: 150;
|
||||||
|
--success-sat: 50%;
|
||||||
|
--val-color--success: hsl(var(--success-hue), var(--success-sat), 50%);
|
||||||
|
--val-color--success-light: hsl(var(--success-hue), var(--success-sat), 68%);
|
||||||
|
--val-color--success-dark: hsl(var(--success-hue), var(--success-sat), 38%);
|
||||||
|
--val-color--success-link: hsl(var(--success-hue), var(--success-sat), 26%);
|
||||||
|
--val-color--success-link-hover: hsl(var(--success-hue), var(--success-sat), 18%);
|
||||||
|
--val-color--success-link-active: hsl(var(--success-hue), var(--success-sat), 36%);
|
||||||
|
|
||||||
|
--warning-hue: 44;
|
||||||
|
--warning-sat: 100%;
|
||||||
|
--val-color--warning: hsl(var(--warning-hue), var(--warning-sat), 50%);
|
||||||
|
--val-color--warning-light: hsl(var(--warning-hue), var(--warning-sat), 60%);
|
||||||
|
--val-color--warning-dark: hsl(var(--warning-hue), var(--warning-sat), 40%);
|
||||||
|
--val-color--warning-link: hsl(var(--warning-hue), var(--warning-sat), 30%);
|
||||||
|
--val-color--warning-link-hover: hsl(var(--warning-hue), var(--warning-sat), 20%);
|
||||||
|
--val-color--warning-link-active: hsl(var(--warning-hue), var(--warning-sat), 38%);
|
||||||
|
|
||||||
|
--danger-hue: 348;
|
||||||
|
--danger-sat: 86%;
|
||||||
|
--val-color--danger: hsl(var(--danger-hue), var(--danger-sat), 50%);
|
||||||
|
--val-color--danger-light: hsl(var(--danger-hue), var(--danger-sat), 60%);
|
||||||
|
--val-color--danger-dark: hsl(var(--danger-hue), var(--danger-sat), 35%);
|
||||||
|
--val-color--danger-link: hsl(var(--danger-hue), var(--danger-sat), 25%);
|
||||||
|
--val-color--danger-link-hover: hsl(var(--danger-hue), var(--danger-sat), 10%);
|
||||||
|
--val-color--danger-link-active: hsl(var(--danger-hue), var(--danger-sat), 30%);
|
||||||
|
|
||||||
|
--light-hue: 0;
|
||||||
|
--light-sat: 0%;
|
||||||
|
--val-color--light: hsl(var(--light-hue), var(--light-sat), 96%);
|
||||||
|
--val-color--light-light: hsl(var(--light-hue), var(--light-sat), 98%);
|
||||||
|
--val-color--light-dark: hsl(var(--light-hue), var(--light-sat), 92%);
|
||||||
|
|
||||||
|
--dark-hue: 0;
|
||||||
|
--dark-sat: 0%;
|
||||||
|
--val-color--dark: hsl(var(--dark-hue), var(--dark-sat), 25%);
|
||||||
|
--val-color--dark-light: hsl(var(--dark-hue), var(--dark-sat), 40%);
|
||||||
|
--val-color--dark-dark: hsl(var(--dark-hue), var(--dark-sat), 8%);
|
||||||
|
--val-color--dark-link: hsl(var(--dark-hue), var(--dark-sat), 90%);
|
||||||
|
--val-color--dark-link-hover: hsl(var(--dark-hue), var(--dark-sat), 100%);
|
||||||
|
--val-color--dark-link-active: hsl(var(--dark-hue), var(--dark-sat), 70%);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--gray-hue: 201;
|
||||||
|
--gray-sat: 15%;
|
||||||
|
--val-color--gray-5: hsl(var(--gray-hue), var(--gray-sat), 5%);
|
||||||
|
--val-color--gray-10: hsl(var(--gray-hue), var(--gray-sat) ,11%);
|
||||||
|
--val-color--gray-20: hsl(var(--gray-hue), var(--gray-sat),20%);
|
||||||
|
--val-color--gray-45: hsl(var(--gray-hue), var(--gray-sat), 44%);
|
||||||
|
--val-color--gray-60: hsl(var(--gray-hue), var(--gray-sat), 57%);
|
||||||
|
--val-color--gray-65: hsl(var(--gray-hue), var(--gray-sat), 63%);
|
||||||
|
--val-color--gray-70: hsl(var(--gray-hue), var(--gray-sat), 72%);
|
||||||
|
--val-color--gray-90: hsl(var(--gray-hue), var(--gray-sat), 88%);
|
||||||
|
--val-color--gray-95: hsl(var(--gray-hue), var(--gray-sat), 93%);
|
||||||
|
--val-color--gray-100: hsl(var(--gray-hue), var(--gray-sat), 97%);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--val-color--bg: #fafafa;
|
||||||
|
--val-color--text: #212529;
|
||||||
|
--val-color--white: #fff;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
--color-text-neutral-soft: var(--color--gray-45);
|
||||||
|
--color-text-neutral-medium: var(--color--gray-20);
|
||||||
|
--color-text-neutral-loud: var(--color--gray-5);
|
||||||
|
--color-text-primary-medium: var(--val-color--primary-40);
|
||||||
|
--color-text-primary-loud: var(--val-color--primary-30);
|
||||||
|
--color--black: #000;
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
--color--red: #e33f1e;
|
||||||
|
--color--gold: #fdca40;
|
||||||
|
--color--green: #3fa21c;
|
||||||
|
--header-height-wide-when-fixed: calc(6 * var(--val-gap));
|
||||||
|
--mobile-nav-width: 31.25rem;
|
||||||
|
|
||||||
|
--val-menu--border-radius: 0.625rem;
|
||||||
|
*/
|
||||||
|
--val-border-radius: 0.375rem;
|
||||||
|
|
||||||
|
/* Menu component */
|
||||||
|
--val-menu--color-bg: var(--val-color--bg);
|
||||||
|
--val-menu--color-highlight: #e91e63;
|
||||||
|
--val-menu--color-border: rgba(0, 0, 0, 0.1);
|
||||||
|
--val-menu--color-shadow: rgba(0, 0, 0, 0.06);
|
||||||
|
--val-menu--line-padding: 0.625rem;
|
||||||
|
--val-menu--line-height: calc(1.875rem + 1px);
|
||||||
|
--val-menu--item-height: calc(var(--val-menu--line-padding) + var(--val-menu--line-height));
|
||||||
|
--val-menu--item-width-min: 14rem;
|
||||||
|
--val-menu--item-width-max: 20rem;
|
||||||
|
--val-menu--item-gap: 1rem;
|
||||||
|
--val-menu--trigger-width: 2.675rem;
|
||||||
|
--val-menu--side-width: 20rem;
|
||||||
|
}
|
97
static/js/menu.js
Normal file
97
static/js/menu.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
function menu__showChildren(nav, children) {
|
||||||
|
const li = children[0];
|
||||||
|
const submenu = li.querySelector('.menu__children');
|
||||||
|
submenu.classList.add('active');
|
||||||
|
submenu.style.animation = 'slideLeft 0.5s ease forwards';
|
||||||
|
|
||||||
|
const labelEl = li.querySelector('.menu__label');
|
||||||
|
const title = labelEl ? labelEl.textContent.trim() : (li.querySelector('a')?.textContent?.trim() ?? '');
|
||||||
|
nav.querySelector('.menu__title').innerHTML = title;
|
||||||
|
nav.querySelector('.menu__header').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function menu__hideChildren(nav, children) {
|
||||||
|
const submenu = children[0].querySelector('.menu__children');
|
||||||
|
submenu.style.animation = 'slideRight 0.5s ease forwards';
|
||||||
|
setTimeout(() => {
|
||||||
|
submenu.classList.remove('active');
|
||||||
|
submenu.style.removeProperty('animation');
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
children.shift();
|
||||||
|
if (children.length > 0) {
|
||||||
|
const a = children[0].querySelector('a');
|
||||||
|
const title = (a && a.textContent ? a.textContent.trim() : '');
|
||||||
|
nav.querySelector('.menu__title').textContent = title;
|
||||||
|
} else {
|
||||||
|
nav.querySelector('.menu__header').classList.remove('active');
|
||||||
|
nav.querySelector('.menu__title').textContent = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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__children').forEach(submenu => {
|
||||||
|
submenu.classList.remove('active');
|
||||||
|
submenu.style.removeProperty('animation');
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.menu').forEach(menu => {
|
||||||
|
|
||||||
|
let menuChildren = [];
|
||||||
|
const menuNav = menu.querySelector('.menu__nav');
|
||||||
|
const menuOverlay = menu.querySelector('.menu__overlay');
|
||||||
|
|
||||||
|
menu.querySelector('.menu__list').addEventListener('click', (e) => {
|
||||||
|
if (menuNav.classList.contains('active')) {
|
||||||
|
let target = e.target.closest('.menu__item--children');
|
||||||
|
if (target && target != menuChildren[0]) {
|
||||||
|
menuChildren.unshift(target);
|
||||||
|
menu__showChildren(menuNav, menuChildren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelector('.menu__back').addEventListener('click', () => {
|
||||||
|
menu__hideChildren(menuNav, menuChildren);
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelector('.menu__close').addEventListener('click', () => {
|
||||||
|
menuChildren = menu__reset(menu, menuNav, menuOverlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelectorAll('.menu__item--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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -90,7 +90,7 @@ async fn poweredby_getter_reflects_internal_state() {
|
||||||
assert!(c1.contains(&global::SETTINGS.app.name));
|
assert!(c1.contains(&global::SETTINGS.app.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// HELPERS *****************************************************************************************
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
fn render_component<C: Component>(c: &C) -> Markup {
|
fn render_component<C: Component>(c: &C) -> Markup {
|
||||||
let mut cx = Context::default();
|
let mut cx = Context::default();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue