✨ (pagetop): Añade Grid y extrae Flex de Container

This commit is contained in:
Manuel Cillero 2026-09-18 16:49:09 +02:00
parent dfff12881f
commit 307ca6f62c
44 changed files with 2554 additions and 735 deletions

View file

@ -30,10 +30,16 @@ pub mod dropdown;
#[doc(inline)]
pub use dropdown::Dropdown;
mod flex;
pub use flex::Flex;
pub mod form;
#[doc(inline)]
pub use form::Form;
mod grid;
pub use grid::Grid;
mod html;
pub use html::Html;

View file

@ -47,9 +47,6 @@ pub struct Container {
props: Props,
/// Devuelve el tipo semántico del contenedor.
kind: Kind,
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
#[getters(copy)]
flex: Flex,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
@ -70,7 +67,7 @@ impl Component for Container {
if output.is_empty() {
return Ok(html! {});
}
let container_props = self.props().unpack_with_flex(cx, self.flex());
let container_props = self.props().unpack(cx);
Ok(match self.kind() {
Kind::Default => html! { div (container_props) { (output) } },
Kind::Main => html! { main (container_props) { (output) } },
@ -138,12 +135,6 @@ impl Container {
self
}
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
self.flex = self.flex.merge(flex);
self
}
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
/// una operación [`ChildOp`].
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {

299
src/base/component/flex.rs Normal file
View file

@ -0,0 +1,299 @@
use crate::html::align;
use crate::html::flex::{Behavior, ContentJustify, Direction};
use crate::prelude::*;
// **< DisplayFlex >********************************************************************************
// Posicionamiento Flexbox del contenedor `Flex`. La API pública sólo expone los constructores
// `Flex::new()`, `Flex::at()`, `Flex::inline()` y `Flex::inline_at()`, nunca la variante en sí.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
enum DisplayFlex {
#[default]
Always,
AlwaysInline,
At(Breakpoint),
InlineAt(Breakpoint),
}
// **< Flex >***************************************************************************************
/// Componente que crea un **contenedor Flexbox** para posicionar componentes.
///
/// Es el único componente de PageTop que ofrece posicionamiento Flexbox. Sus hijos pueden ser
/// **cualquier componente**, no sólo otro `Flex`. Para colocarlos dentro del contenedor
/// (crecimiento, reducción, alineación individual, orden, tamaño, desplazamiento) se usa
/// [`flex::FlexItem`] vía `with_prop()`, exactamente igual que en cualquier otro contenedor.
/// `FlexItem` es una característica de [`PropsOp`], no exige ningún envoltorio propio.
///
/// Se aplica con clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`,
/// `justify-content`, `align-items`, `align-content`, `gap`, `row-gap`, `column-gap`), registradas
/// vía [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en
/// el `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de
/// los temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
///
/// `align`/`align_content`/`gap` usan tipos de [`pagetop::html::align`] ([`align::Items`],
/// [`align::Content`], [`align::Gap`]), compartidos con [`Grid`], mismo CSS y mismo catálogo de
/// valores en los dos. Ver la documentación de [`align`](crate::html::align) para el criterio
/// completo.
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
/// [`pagetop::html::align`]: crate::html::align
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
/// [`flex::FlexItem`]: crate::html::flex::FlexItem
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let actions = Flex::new()
/// .with_justify(flex::ContentJustify::End)
/// .with_align(align::Items::Center)
/// .with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
/// .with_child(Button::submit(Lc::n("Save")))
/// .with_child(Button::plain(Lc::n("Cancel")));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Flex {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
// Determina si esta configuración debe aplicarse (y con qué variante de `display`) o si `Flex`
// no está en absoluto configurado. `None` es el estado real de ausencia: lo que tiene un
// componente que nunca ha llamado a ninguno de sus constructores explícitos. Sin getter
// público; `new()`, `at()`, `inline()` e `inline_at()` son la única forma de activarlo.
#[getters(skip)]
display: Option<DisplayFlex>,
/// Devuelve la dirección del eje principal, por punto de corte.
#[getters(copy)]
direction: Responsive<Direction>,
/// Devuelve el comportamiento cuando los elementos no caben en una sola línea, por punto de
/// corte.
#[getters(copy)]
wrap: Responsive<Behavior>,
/// Devuelve la alineación de los elementos en el eje principal, por punto de corte.
#[getters(copy)]
justify: Responsive<ContentJustify>,
/// Devuelve la alineación de los elementos en el eje transversal, por punto de corte.
#[getters(copy)]
align: Responsive<align::Items>,
/// Devuelve la alineación de las líneas cuando hay más de una, por punto de corte.
#[getters(copy)]
align_content: Responsive<align::Content>,
/// Devuelve el espaciado entre elementos, por punto de corte.
#[getters(copy)]
gap: Responsive<align::Gap>,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
#[async_trait]
impl Component for Flex {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let output = self.children().render(cx).await;
if output.is_empty() {
return Ok(html! {});
}
let mut classes = String::new();
self.flex_classes(cx, &mut classes);
let container_props = self.props().unpack_with_classes(cx, classes);
Ok(html! { div (container_props) { (output) } })
}
}
#[builder_impl]
impl Flex {
/// Define una configuración con `display: flex`, sin punto de corte: se aplica siempre.
pub fn new() -> Self {
Self {
display: Some(DisplayFlex::Always),
..Default::default()
}
}
/// Define una configuración con `display: flex` que se aplica a partir del punto de corte
/// indicado.
pub fn at(bp: Breakpoint) -> Self {
Self {
display: Some(DisplayFlex::At(bp)),
..Default::default()
}
}
/// Define una configuración con `display: inline-flex`, sin punto de corte: se aplica
/// siempre.
pub fn inline() -> Self {
Self {
display: Some(DisplayFlex::AlwaysInline),
..Default::default()
}
}
/// Define una configuración con `display: inline-flex` que se aplica a partir del punto de
/// corte indicado.
pub fn inline_at(bp: Breakpoint) -> Self {
Self {
display: Some(DisplayFlex::InlineAt(bp)),
..Default::default()
}
}
// **< Flex BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
self.props.alter_prop(op);
self
}
/// Establece la dirección del eje principal.
pub fn with_direction(mut self, dir: Direction) -> Self {
self.direction = self.direction.set(dir);
self
}
/// Establece la dirección del eje principal a partir del punto de corte indicado.
pub fn with_direction_at(mut self, bp: Breakpoint, dir: Direction) -> Self {
self.direction = self.direction.set_at(bp, dir);
self
}
/// Establece el comportamiento cuando los elementos no caben en una sola línea.
pub fn with_wrap(mut self, wrap: Behavior) -> Self {
self.wrap = self.wrap.set(wrap);
self
}
/// Establece el comportamiento cuando los elementos no caben en una sola línea, a partir del
/// punto de corte indicado.
pub fn with_wrap_at(mut self, bp: Breakpoint, wrap: Behavior) -> Self {
self.wrap = self.wrap.set_at(bp, wrap);
self
}
/// Establece la alineación de los elementos en el eje principal.
pub fn with_justify(mut self, justify: ContentJustify) -> Self {
self.justify = self.justify.set(justify);
self
}
/// Establece la alineación de los elementos en el eje principal, a partir del punto de corte
/// indicado.
pub fn with_justify_at(mut self, bp: Breakpoint, justify: ContentJustify) -> Self {
self.justify = self.justify.set_at(bp, justify);
self
}
/// Establece la alineación de los elementos en el eje transversal.
pub fn with_align(mut self, align: align::Items) -> Self {
self.align = self.align.set(align);
self
}
/// Establece la alineación de los elementos en el eje transversal, a partir del punto de corte
/// indicado.
pub fn with_align_at(mut self, bp: Breakpoint, align: align::Items) -> Self {
self.align = self.align.set_at(bp, align);
self
}
/// Establece la alineación de las líneas cuando hay más de una (ver
/// [`Content`](crate::html::align::Content)).
pub fn with_align_content(mut self, align_content: align::Content) -> Self {
self.align_content = self.align_content.set(align_content);
self
}
/// Establece la alineación de las líneas cuando hay más de una (ver
/// [`Content`](crate::html::align::Content)), a partir del punto de corte indicado.
pub fn with_align_content_at(mut self, bp: Breakpoint, align_content: align::Content) -> Self {
self.align_content = self.align_content.set_at(bp, align_content);
self
}
/// Establece el espaciado entre elementos.
pub fn with_gap(mut self, gap: align::Gap) -> Self {
self.gap = self.gap.set(gap);
self
}
/// Establece el espaciado entre elementos, a partir del punto de corte indicado.
pub fn with_gap_at(mut self, bp: Breakpoint, gap: align::Gap) -> Self {
self.gap = self.gap.set_at(bp, gap);
self
}
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
/// una operación [`ChildOp`].
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.children.alter_child(op.into());
self
}
}
impl Flex {
// Calcula las clases CSS *responsive* de este contenedor Flex y las añade a `classes`,
// separadas con un espacio de las que ya hubiera, para poder compartir un único acumulador
// con `FlexItem::apply()` (ver `Props::unpack_with_classes()`) sin cadenas intermedias.
#[rustfmt::skip]
fn flex_classes(&self, cx: &mut Context, classes: &mut String) {
// Sin `display` no hay contenedor Flex: el resto de propiedades (`flex-direction`, `gap`,
// etc.) no tienen ningún efecto en CSS sin `display: flex`/`inline-flex`, así que ni se
// generan.
let Some(display) = self.display else {
return;
};
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
let (prefix, value) = match display {
DisplayFlex::Always
| DisplayFlex::At(_) => ("_flex_", "flex"),
DisplayFlex::AlwaysInline
| DisplayFlex::InlineAt(_) => ("_inline-flex_", "inline-flex"),
};
let entry = match display {
DisplayFlex::At(bp) | DisplayFlex::InlineAt(bp) => bp.resolved(cx),
_ => None,
};
let class = match entry {
None => prefix.into(),
Some(entry) => util::join!(prefix, entry.name, "_").into(),
};
styles(cx, classes, entry, class, "display", value.into());
apply!(cx, classes, self.direction, "_flex-direction_", "flex-direction");
apply!(cx, classes, self.wrap, "_flex-wrap_", "flex-wrap");
apply!(cx, classes, self.justify, "_flex-justify_", "justify-content");
apply!(cx, classes, self.align, "_flex-align-items_", "align-items");
apply!(cx, classes, self.align_content, "_flex-align-content_", "align-content");
for (bp, gap) in self.gap.by_breakpoint() {
let entry = bp.resolved(cx);
for (property, value) in gap.styles().into_iter().flatten() {
// El prefijo de `gap` no es literal (depende de la propiedad), así que se compone
// aquí en un único `join!` en vez de pasar por `responsive_class!`.
let token = value_to_token(&value);
let class = match entry {
None => util::join!("_flex-", property, "_", token, "_"),
Some(e) => util::join!("_flex-", property, "_", token, "_", e.name, "_"),
};
styles(cx, classes, entry, class.into(), property, value);
}
}
}
}

376
src/base/component/grid.rs Normal file
View file

@ -0,0 +1,376 @@
use crate::html::align::{Content, Gap, Items};
use crate::html::grid::{AutoFlow, AxisTrack, ContentJustify, DefaultJustify, Tracks};
use crate::prelude::*;
// **< DisplayGrid >********************************************************************************
// Posicionamiento CSS Grid del contenedor `Grid`. La API pública sólo expone los constructores
// `Grid::new()`, `Grid::at()`, `Grid::inline()` e `Grid::inline_at()`, nunca la variante en sí.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
enum DisplayGrid {
#[default]
Always,
AlwaysInline,
At(Breakpoint),
InlineAt(Breakpoint),
}
// **< Grid >***************************************************************************************
/// Componente que crea una **rejilla CSS Grid** para posicionar componentes.
///
/// Es el único componente de PageTop que ofrece posicionamiento CSS Grid (con el mismo criterio que
/// [`Flex`] para Flexbox). Sus hijos pueden ser **cualquier componente**, no sólo otro `Grid`. Para
/// colocarlos dentro de la rejilla (columna, fila, alineación individual) se usa [`grid::GridItem`]
/// vía `with_prop()`, exactamente igual que en cualquier otro contenedor. `GridItem` es una
/// característica de [`PropsOp`], no exige ningún envoltorio propio.
///
/// Se aplica con clases CSS generadas dinámicamente (`display`, `grid-template-columns`,
/// `grid-template-rows`, `grid-auto-columns`, `grid-auto-rows`, `grid-auto-flow`, `justify-items`,
/// `align-items`, `justify-content`, `align-content`, `gap`, `row-gap`, `column-gap`), registradas
/// vía [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en
/// el `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de
/// los temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
///
/// `align_items`/`align_content`/`gap` usan tipos de [`pagetop::html::align`] ([`align::Items`],
/// [`align::Content`], [`align::Gap`]), compartidos con [`Flex`], mismo CSS y mismo catálogo de
/// valores en los dos. Ver la documentación de [`align`](crate::html::align) para el criterio
/// completo.
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
/// [`pagetop::html::align`]: crate::html::align
/// [`grid::GridItem`]: crate::html::grid::GridItem
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
/// [`Flex`]: crate::base::component::Flex
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let cards = Grid::new()
/// .with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)))
/// .with_gap(align::Gap::Both(UnitValue::RelRem(1.0)))
/// .with_child(Container::new())
/// .with_child(Container::new())
/// .with_child(Container::new());
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Grid {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
// Determina si esta configuración debe aplicarse (y con qué variante de `display`) o si `Grid`
// no está en absoluto configurado. `None` es el estado real de ausencia: lo que tiene un
// componente que nunca ha llamado a ninguno de sus constructores explícitos. Sin getter
// público; `new()`, `at()`, `inline()` e `inline_at()` son la única forma de activarlo.
#[getters(skip)]
display: Option<DisplayGrid>,
/// Devuelve las pistas de columna (`grid-template-columns`), por punto de corte.
#[getters(copy)]
columns: Responsive<Tracks>,
/// Devuelve las pistas de fila (`grid-template-rows`), por punto de corte.
#[getters(copy)]
rows: Responsive<Tracks>,
/// Devuelve el tamaño de las columnas generadas implícitamente (`grid-auto-columns`), por
/// punto de corte.
#[getters(copy)]
auto_columns: Responsive<AxisTrack>,
/// Devuelve el tamaño de las filas generadas implícitamente (`grid-auto-rows`), por punto de
/// corte.
#[getters(copy)]
auto_rows: Responsive<AxisTrack>,
/// Devuelve el algoritmo de colocación automática, por punto de corte.
#[getters(copy)]
auto_flow: Responsive<AutoFlow>,
/// Devuelve la alineación de los elementos en el eje de columnas, por punto de corte.
#[getters(copy)]
justify_items: Responsive<DefaultJustify>,
/// Devuelve la alineación de los elementos en el eje de filas, por punto de corte.
#[getters(copy)]
align_items: Responsive<Items>,
/// Devuelve la alineación de las pistas en el eje de columnas, por punto de corte.
#[getters(copy)]
justify_content: Responsive<ContentJustify>,
/// Devuelve la alineación de las pistas en el eje de filas, por punto de corte.
#[getters(copy)]
align_content: Responsive<Content>,
/// Devuelve el espaciado entre pistas, por punto de corte.
#[getters(copy)]
gap: Responsive<Gap>,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
#[async_trait]
impl Component for Grid {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let output = self.children().render(cx).await;
if output.is_empty() {
return Ok(html! {});
}
let mut classes = String::new();
self.grid_classes(cx, &mut classes);
let container_props = self.props().unpack_with_classes(cx, classes);
Ok(html! { div (container_props) { (output) } })
}
}
#[builder_impl]
impl Grid {
/// Define una configuración con `display: grid`, sin punto de corte: se aplica siempre.
pub fn new() -> Self {
Self {
display: Some(DisplayGrid::Always),
..Default::default()
}
}
/// Define una configuración con `display: grid` que se aplica a partir del punto de corte
/// indicado.
pub fn at(bp: Breakpoint) -> Self {
Self {
display: Some(DisplayGrid::At(bp)),
..Default::default()
}
}
/// Define una configuración con `display: inline-grid`, sin punto de corte: se aplica
/// siempre.
pub fn inline() -> Self {
Self {
display: Some(DisplayGrid::AlwaysInline),
..Default::default()
}
}
/// Define una configuración con `display: inline-grid` que se aplica a partir del punto de
/// corte indicado.
pub fn inline_at(bp: Breakpoint) -> Self {
Self {
display: Some(DisplayGrid::InlineAt(bp)),
..Default::default()
}
}
// **< Grid BUILDER >***************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
self.props.alter_prop(op);
self
}
/// Establece las pistas de columna.
pub fn with_columns(mut self, tracks: Tracks) -> Self {
self.columns = self.columns.set(tracks);
self
}
/// Establece las pistas de columna a partir del punto de corte indicado.
pub fn with_columns_at(mut self, bp: Breakpoint, tracks: Tracks) -> Self {
self.columns = self.columns.set_at(bp, tracks);
self
}
/// Establece las pistas de fila.
pub fn with_rows(mut self, tracks: Tracks) -> Self {
self.rows = self.rows.set(tracks);
self
}
/// Establece las pistas de fila a partir del punto de corte indicado.
pub fn with_rows_at(mut self, bp: Breakpoint, tracks: Tracks) -> Self {
self.rows = self.rows.set_at(bp, tracks);
self
}
/// Establece el tamaño de las columnas generadas implícitamente por la colocación automática.
pub fn with_auto_columns(mut self, track: AxisTrack) -> Self {
self.auto_columns = self.auto_columns.set(track);
self
}
/// Establece el tamaño de las columnas generadas implícitamente, a partir del punto de corte
/// indicado.
pub fn with_auto_columns_at(mut self, bp: Breakpoint, track: AxisTrack) -> Self {
self.auto_columns = self.auto_columns.set_at(bp, track);
self
}
/// Establece el tamaño de las filas generadas implícitamente por la colocación automática.
pub fn with_auto_rows(mut self, track: AxisTrack) -> Self {
self.auto_rows = self.auto_rows.set(track);
self
}
/// Establece el tamaño de las filas generadas implícitamente, a partir del punto de corte
/// indicado.
pub fn with_auto_rows_at(mut self, bp: Breakpoint, track: AxisTrack) -> Self {
self.auto_rows = self.auto_rows.set_at(bp, track);
self
}
/// Establece el algoritmo de colocación automática.
pub fn with_auto_flow(mut self, auto_flow: AutoFlow) -> Self {
self.auto_flow = self.auto_flow.set(auto_flow);
self
}
/// Establece el algoritmo de colocación automática, a partir del punto de corte indicado.
pub fn with_auto_flow_at(mut self, bp: Breakpoint, auto_flow: AutoFlow) -> Self {
self.auto_flow = self.auto_flow.set_at(bp, auto_flow);
self
}
/// Establece la alineación de los elementos en el eje de columnas.
pub fn with_justify_items(mut self, justify_items: DefaultJustify) -> Self {
self.justify_items = self.justify_items.set(justify_items);
self
}
/// Establece la alineación de los elementos en el eje de columnas, a partir del punto de corte
/// indicado.
pub fn with_justify_items_at(mut self, bp: Breakpoint, justify_items: DefaultJustify) -> Self {
self.justify_items = self.justify_items.set_at(bp, justify_items);
self
}
/// Establece la alineación de los elementos en el eje de filas.
pub fn with_align_items(mut self, align_items: Items) -> Self {
self.align_items = self.align_items.set(align_items);
self
}
/// Establece la alineación de los elementos en el eje de filas, a partir del punto de corte
/// indicado.
pub fn with_align_items_at(mut self, bp: Breakpoint, align_items: Items) -> Self {
self.align_items = self.align_items.set_at(bp, align_items);
self
}
/// Establece la alineación de las pistas en el eje de columnas (ver [`ContentJustify`]).
pub fn with_justify_content(mut self, justify_content: ContentJustify) -> Self {
self.justify_content = self.justify_content.set(justify_content);
self
}
/// Establece la alineación de las pistas en el eje de columnas, a partir del punto de corte
/// indicado.
pub fn with_justify_content_at(
mut self,
bp: Breakpoint,
justify_content: ContentJustify,
) -> Self {
self.justify_content = self.justify_content.set_at(bp, justify_content);
self
}
/// Establece la alineación de las pistas en el eje de filas (ver
/// [`Content`](crate::html::align::Content)).
pub fn with_align_content(mut self, align_content: Content) -> Self {
self.align_content = self.align_content.set(align_content);
self
}
/// Establece la alineación de las pistas en el eje de filas, a partir del punto de corte
/// indicado.
pub fn with_align_content_at(mut self, bp: Breakpoint, align_content: Content) -> Self {
self.align_content = self.align_content.set_at(bp, align_content);
self
}
/// Establece el espaciado entre pistas.
pub fn with_gap(mut self, gap: Gap) -> Self {
self.gap = self.gap.set(gap);
self
}
/// Establece el espaciado entre pistas, a partir del punto de corte indicado.
pub fn with_gap_at(mut self, bp: Breakpoint, gap: Gap) -> Self {
self.gap = self.gap.set_at(bp, gap);
self
}
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
/// una operación [`ChildOp`].
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.children.alter_child(op.into());
self
}
}
impl Grid {
// Calcula las clases CSS *responsive* de este contenedor Grid y las añade a `classes`,
// separadas con un espacio de las que ya hubiera, para poder compartir un único acumulador con
// `GridItem::apply()` (ver `Props::unpack_with_classes()`) sin cadenas intermedias.
#[rustfmt::skip]
fn grid_classes(&self, cx: &mut Context, classes: &mut String) {
// Sin `display` no hay contenedor Grid: el resto de propiedades (`grid-template-columns`,
// `gap`, etc.) no tienen ningún efecto en CSS sin `display: grid`/`inline-grid`, así que ni
// se generan. Éste es el estado de un `Grid` que nunca ha llamado a ninguno de sus
// constructores explícitos.
let Some(display) = self.display else {
return;
};
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
let (prefix, value) = match display {
DisplayGrid::Always
| DisplayGrid::At(_) => ("_grid_", "grid"),
DisplayGrid::AlwaysInline
| DisplayGrid::InlineAt(_) => ("_inline-grid_", "inline-grid"),
};
let entry = match display {
DisplayGrid::At(bp) | DisplayGrid::InlineAt(bp) => bp.resolved(cx),
_ => None,
};
let class = match entry {
None => prefix.into(),
Some(entry) => util::join!(prefix, entry.name, "_").into(),
};
styles(cx, classes, entry, class, "display", value.into());
apply!(cx, classes, self.columns, "_grid-columns_", "grid-template-columns", val);
apply!(cx, classes, self.rows, "_grid-rows_", "grid-template-rows", val);
apply!(cx, classes, self.auto_columns, "_grid-auto-columns_", "grid-auto-columns", val);
apply!(cx, classes, self.auto_rows, "_grid-auto-rows_", "grid-auto-rows", val);
// `val`: a diferencia de `flex::Behavior` (una sola palabra en cualquier variante),
// `AutoFlow::RowDense`/`ColumnDense` valen "row dense"/"column dense" (con espacio), así
// que necesitan pasar por `value_to_token()` igual que `Tracks`/`ItemPlacement`.
apply!(cx, classes, self.auto_flow, "_grid-auto-flow_", "grid-auto-flow", val);
apply!(cx, classes, self.justify_items, "_grid-justify-items_", "justify-items");
apply!(cx, classes, self.align_items, "_grid-align-items_", "align-items");
apply!(cx, classes, self.justify_content, "_grid-justify-content_", "justify-content");
apply!(cx, classes, self.align_content, "_grid-align-content_", "align-content");
for (bp, gap) in self.gap.by_breakpoint() {
let entry = bp.resolved(cx);
for (property, value) in gap.styles().into_iter().flatten() {
// El prefijo de `gap` no es literal (depende de la propiedad), así que se compone
// aquí en un único `join!` en vez de pasar por `responsive_class!`.
let token = value_to_token(&value);
let class = match entry {
None => util::join!("_grid-", property, "_", token, "_"),
Some(e) => util::join!("_grid-", property, "_", token, "_", e.name, "_"),
};
styles(cx, classes, entry, class.into(), property, value);
}
}
}
}

View file

@ -58,7 +58,7 @@ use crate::prelude::*;
/// ));
/// ```
///
/// Barra con **botón de despliegue** y **marca de identidad**, en ese orden:
/// Barra con **botón de despliegue**, contenido y **marca de identidad**:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
@ -81,9 +81,6 @@ pub struct Navbar {
props: Props,
/// Devuelve la disposición configurada para la barra de navegación.
layout: navbar::Layout,
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
#[getters(copy)]
flex: Flex,
/// Devuelve la lista de contenidos.
items: Children,
}
@ -99,8 +96,10 @@ impl Component for Navbar {
}
fn setup(&mut self, cx: &mut Context) {
// Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de
// despliegue para referenciar el contenido colapsable con `aria-controls`.
// Asegura que la barra de navegación tiene un identificador único: siempre se usa para
// derivar el `id` del área de contenido (`id_content`, ver `prepare()`); además, el botón
// de despliegue lo necesita para referenciar ese contenido colapsable con `aria-controls`
// si el *layout* lo incluye.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
self.alter_prop(PropsOp::prepend_classes("navbar"));
}
@ -124,12 +123,9 @@ impl Component for Navbar {
aria-expanded="false"
aria-controls=(&id_content)
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
{
span class="navbar-toggle-icon" {}
}
{}
};
// Posicionamiento Flexbox opcional (no del `<nav>`, cuya estructura la fija `layout()`).
let content_props = Props::classes("navbar-content").with_id(id_content);
Ok(html! {
@ -137,32 +133,32 @@ impl Component for Navbar {
@match self.layout() {
// Barra más sencilla: sólo contenido, siempre visible.
navbar::Layout::Simple => {
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
div (content_props.unpack(cx)) { (items) }
},
// Barra sencilla que se puede contraer/expandir.
navbar::Layout::SimpleToggle => {
(button)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
div (content_props.unpack(cx)) { (items) }
},
// Barra con marca, siempre visible, sin botón.
navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx).await)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
div (content_props.unpack(cx)) { (items) }
},
// Barra con marca y botón, en ese orden.
navbar::Layout::BrandLeft(brand) => {
(brand.render(cx).await)
(button)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
div (content_props.unpack(cx)) { (items) }
},
// Barra con botón y marca, en ese orden.
// Barra con botón, contenido y marca (en ese orden).
navbar::Layout::BrandRight(brand) => {
(button)
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
div (content_props.unpack(cx)) { (items) }
(brand.render(cx).await)
},
}
@ -194,8 +190,10 @@ impl Navbar {
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **botón de despliegue** y **marca de identidad**, en ese
/// orden.
/// Crea una barra de navegación con **botón de despliegue**, el contenido y, en último lugar,
/// la **marca de identidad**. A diferencia de [`brand_left()`](Self::brand_left), aquí no queda
/// junto al botón, sino que se renderiza tras el contenido, para acabar en el extremo derecho
/// de la barra (el botón sólo es visible en viewports estrechos).
pub fn brand_right(brand: Brand) -> Self {
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
}
@ -220,15 +218,6 @@ impl Navbar {
self
}
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
///
/// No afecta a la posición de la marca ni del botón de despliegue, que quedan fijados con
/// [`with_layout()`](Self::with_layout).
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
self.flex = self.flex.merge(flex);
self
}
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
/// barra con una operación [`TypedOp`].
///

View file

@ -451,9 +451,9 @@ impl Pager {
// páginas ya cabe sin necesidad de truncar (ver más abajo), devuelve todas las páginas sin
// elipsis. En caso contrario, siempre incluye la primera y la última página, la ventana de
// páginas alrededor de la actual (`window` antes y después, recortada a los límites del
// listado), y sustituye por una elipsis cualquier tramo oculto de dos o más páginas -- un
// tramo de una sola página se muestra directamente, ya que ocultarla tras una elipsis no
// ahorra espacio.
// listado), y sustituye por una elipsis cualquier tramo oculto de dos o más páginas. Un tramo
// de una sola página se muestra directamente, ya que ocultarla tras una elipsis no ahorra
// espacio.
fn page_items(&self, page: u64, total_pages: u64) -> Vec<PageItem> {
// Acotado a `total_pages`: una ventana mayor no aporta nada (ya se mostrarían todas las
// páginas) y evita operar con un valor arbitrariamente grande más abajo.