diff --git a/examples/form-controls.rs b/examples/form-controls.rs index e6f8ee29..e7311b21 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -140,12 +140,9 @@ async fn form_controls(request: HttpRequest) -> Result { .with_child(form::Hidden::field("origin", "form-selections")) // Botonera de acciones. .with_child( - Container::new() - .with_flex( - Flex::new() - .with_wrap(flex::Behavior::Wrap) - .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), - ) + Flex::new() + .with_wrap(flex::Behavior::Wrap) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))) .with_child( Button::submit(Lc::t("btn_submit", &LOC)) .with_style(button::Style::Solid(Intent::Primary)), @@ -260,12 +257,9 @@ async fn form_controls(request: HttpRequest) -> Result { .with_child(form::Hidden::field("origin", "form-text")) // Botonera de acciones. .with_child( - Container::new() - .with_flex( - Flex::new() - .with_wrap(flex::Behavior::Wrap) - .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), - ) + Flex::new() + .with_wrap(flex::Behavior::Wrap) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))) .with_child( Button::submit(Lc::t("btn_submit", &LOC)) .with_style(button::Style::Solid(Intent::Primary)), @@ -440,12 +434,9 @@ fn form_lists() -> Form { .with_child(form::Hidden::field("origin", "form-lists")) // Botonera de acciones. .with_child( - Container::new() - .with_flex( - Flex::new() - .with_wrap(flex::Behavior::Wrap) - .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), - ) + Flex::new() + .with_wrap(flex::Behavior::Wrap) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))) .with_child( Button::submit(Lc::t("btn_submit", &LOC)) .with_style(button::Style::Solid(Intent::Primary)), diff --git a/examples/intro-flex.rs b/examples/intro-flex.rs index 1d2aba41..4e07a0bb 100644 --- a/examples/intro-flex.rs +++ b/examples/intro-flex.rs @@ -530,10 +530,8 @@ fn flex_item(n: impl Into) -> Lc { } // Fila de demostracion con fondo gris para visualizar los limites del propio contenedor flex. -fn demo_row(flex: Flex) -> Container { - Container::new() - .with_prop(PropsOp::add_classes("flex-demo-row")) - .with_flex(flex) +fn demo_row(flex: Flex) -> Flex { + flex.with_prop(PropsOp::add_classes("flex-demo-row")) } // Titulo y fragmento de codigo que introducen cada demostracion. diff --git a/examples/intro-grid.rs b/examples/intro-grid.rs new file mode 100644 index 00000000..c22c3912 --- /dev/null +++ b/examples/intro-grid.rs @@ -0,0 +1,544 @@ +use pagetop::prelude::*; + +include_locales!(LOC from "examples/locale"); + +struct IntroGrid; + +#[async_trait] +impl Extension for IntroGrid { + fn dependencies(&self) -> Vec { + vec![&pagetop_bootsier::Bootsier] + } + + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(intro_grid)) + } +} + +async fn intro_grid(request: HttpRequest) -> Result { + Page::new(request) + .with_assets(demo_box_styles()) + .with_assets(demo_row_styles()) + .with_child( + Intro::default() + .with_opening(IntroOpening::Custom) + .with_title(Lc::n("PageTop")) + .with_slogan(Lc::t("grid_slogan", &LOC)) + .with_button(None::<(Lc, Route)>) + .with_child(columns_block()) + .with_child(rows_gap_block()) + .with_child(placement_block()) + .with_child(alignment_block()) + .with_child(content_alignment_block()) + .with_child(item_alignment_block()) + .with_child(auto_flow_block()) + .with_child(auto_tracks_block()) + .with_child(responsive_block()) + .with_child(layout_block()), + ) + .render() + .await +} + +fn columns_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_columns", &LOC)) + .with_child(caption( + Lc::t("grid_title_columns_thirds", &LOC), + Lc::n("Grid::new().with_columns(Tracks::repeat(3, AxisTrack::Fraction(1.0)))"), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0))) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))), + ) + .with_child(caption( + Lc::t("grid_title_columns_sidebar", &LOC), + Lc::n(concat!( + "Grid::new().with_columns(Tracks::new()", + ".with_track(AxisTrack::Fixed(UnitValue::Px(140)))", + ".with_track(AxisTrack::Fraction(1.0)))", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns( + grid::Tracks::new() + .with_track(grid::AxisTrack::Fixed(UnitValue::Px(140))) + .with_track(grid::AxisTrack::Fraction(1.0)), + ) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(Lc::t("grid_box_sidebar", &LOC))) + .with_child(demo_box(Lc::t("grid_box_content", &LOC))), + ) + .with_child(caption( + Lc::t("grid_title_columns_min_max", &LOC), + Lc::n(concat!( + "Grid::new().with_columns(Tracks::new()", + ".with_track(AxisTrack::MinContent)", + ".with_track(AxisTrack::Fraction(1.0))", + ".with_track(AxisTrack::MaxContent))", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns( + grid::Tracks::new() + .with_track(grid::AxisTrack::MinContent) + .with_track(grid::AxisTrack::Fraction(1.0)) + .with_track(grid::AxisTrack::MaxContent), + ) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(Lc::t("grid_box_min_content", &LOC))) + .with_child(demo_box(Lc::t("grid_box_fraction", &LOC))) + .with_child(demo_box(Lc::t("grid_box_max_content", &LOC))), + ) +} + +fn rows_gap_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_rows_gap", &LOC)) + .with_child(caption( + Lc::t("grid_title_rows", &LOC), + Lc::n(concat!( + "Grid::new()", + ".with_columns(Tracks::repeat(2, AxisTrack::Fraction(1.0)))", + ".with_rows(Tracks::repeat(2, AxisTrack::Fixed(UnitValue::RelRem(3.0))))", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(2, grid::AxisTrack::Fraction(1.0))) + .with_rows(grid::Tracks::repeat( + 2, + grid::AxisTrack::Fixed(UnitValue::RelRem(3.0)), + )) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))) + .with_child(demo_box(grid_box("4"))), + ) + .with_child(caption( + Lc::t("grid_title_gap_distinct", &LOC), + Lc::n(concat!( + "Grid::new().with_gap(align::Gap::Distinct {", + " row: UnitValue::RelRem(1.5), column: UnitValue::RelRem(0.25) })", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0))) + .with_gap(align::Gap::Distinct { + row: UnitValue::RelRem(1.5), + column: UnitValue::RelRem(0.25), + }), + ) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))) + .with_child(demo_box(grid_box("4"))) + .with_child(demo_box(grid_box("5"))) + .with_child(demo_box(grid_box("6"))), + ) +} + +fn placement_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_placement", &LOC)) + .with_child(caption( + Lc::t("grid_title_placement", &LOC), + Lc::n(concat!( + "GridItem::new().with_column(ItemPlacement::Span(2))", + " / .with_column(ItemPlacement::Range(2, 4))", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(4, grid::AxisTrack::Fraction(1.0))) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(Lc::t("grid_box_plain", &LOC))) + .with_child( + demo_box(Lc::t("grid_box_span_2", &LOC)) + .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2))), + ) + .with_child(demo_box(Lc::t("grid_box_plain", &LOC))) + .with_child( + demo_box(Lc::t("grid_box_range", &LOC)).with_prop( + GridItem::new() + .with_column(grid::ItemPlacement::Range(2, 4)) + .with_row(grid::ItemPlacement::Line(2)), + ), + ), + ) +} + +fn alignment_block() -> Block { + let mut block = Block::new().with_title(Lc::t("grid_block_title_alignment", &LOC)); + + let variants: [(&str, grid::DefaultJustify, align::Items, &str); 4] = [ + ( + "grid_title_alignment_start", + grid::DefaultJustify::Start, + align::Items::Start, + concat!( + "Grid::new().with_justify_items(DefaultJustify::Start)", + ".with_align_items(align::Items::Start)", + ), + ), + ( + "grid_title_alignment_center", + grid::DefaultJustify::Center, + align::Items::Center, + concat!( + "Grid::new().with_justify_items(DefaultJustify::Center)", + ".with_align_items(align::Items::Center)", + ), + ), + ( + "grid_title_alignment_end", + grid::DefaultJustify::End, + align::Items::End, + concat!( + "Grid::new().with_justify_items(DefaultJustify::End)", + ".with_align_items(align::Items::End)", + ), + ), + ( + "grid_title_alignment_stretch", + grid::DefaultJustify::Stretch, + align::Items::Stretch, + concat!( + "Grid::new().with_justify_items(DefaultJustify::Stretch)", + ".with_align_items(align::Items::Stretch)", + ), + ), + ]; + for (title_key, justify_items, align_items, code) in variants { + block = block + .with_child(caption(Lc::t(title_key, &LOC), Lc::n(code))) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat( + 3, + grid::AxisTrack::Fixed(UnitValue::Px(96)), + )) + .with_rows(grid::Tracks::repeat( + 1, + grid::AxisTrack::Fixed(UnitValue::RelRem(4.5)), + )) + .with_justify_items(justify_items) + .with_align_items(align_items) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))), + ); + } + block +} + +fn content_alignment_block() -> Block { + let mut block = Block::new().with_title(Lc::t("grid_block_title_content_alignment", &LOC)); + + let variants: [(&str, grid::ContentJustify, &str); 4] = [ + ( + "grid_title_content_alignment_start", + grid::ContentJustify::Start, + "Grid::new().with_justify_content(ContentJustify::Start)", + ), + ( + "grid_title_content_alignment_center", + grid::ContentJustify::Center, + "Grid::new().with_justify_content(ContentJustify::Center)", + ), + ( + "grid_title_content_alignment_between", + grid::ContentJustify::SpaceBetween, + "Grid::new().with_justify_content(ContentJustify::SpaceBetween)", + ), + ( + "grid_title_content_alignment_evenly", + grid::ContentJustify::SpaceEvenly, + "Grid::new().with_justify_content(ContentJustify::SpaceEvenly)", + ), + ]; + for (title_key, justify_content, code) in variants { + block = block + .with_child(caption(Lc::t(title_key, &LOC), Lc::n(code))) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat( + 3, + grid::AxisTrack::Fixed(UnitValue::Px(72)), + )) + .with_justify_content(justify_content) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_prop(PropsOp::add_style("max-width", "26rem")) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))), + ); + } + block +} + +fn item_alignment_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_item_alignment", &LOC)) + .with_child(caption( + Lc::t("grid_title_item_alignment", &LOC), + Lc::n(concat!( + "GridItem::new()", + ".with_justify_self(ItemJustify::End)", + ".with_align_self(align::ItemSelf::End)", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat( + 3, + grid::AxisTrack::Fixed(UnitValue::Px(96)), + )) + .with_rows(grid::Tracks::repeat( + 1, + grid::AxisTrack::Fixed(UnitValue::RelRem(4.5)), + )) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(grid_box("1"))) + .with_child( + demo_box(Lc::t("grid_box_overridden", &LOC)).with_prop( + GridItem::new() + .with_justify_self(grid::ItemJustify::End) + .with_align_self(align::ItemSelf::End), + ), + ) + .with_child(demo_box(grid_box("3"))), + ) +} + +fn auto_flow_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_auto_flow", &LOC)) + .with_child(caption( + Lc::t("grid_title_auto_flow_row", &LOC), + Lc::n("Grid::new().with_auto_flow(AutoFlow::Row) (por defecto)"), + )) + .with_child(dense_demo(grid::AutoFlow::Row)) + .with_child(caption( + Lc::t("grid_title_auto_flow_dense", &LOC), + Lc::n("Grid::new().with_auto_flow(AutoFlow::RowDense)"), + )) + .with_child(dense_demo(grid::AutoFlow::RowDense)) +} + +fn dense_demo(auto_flow: grid::AutoFlow) -> Grid { + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(4, grid::AxisTrack::Fraction(1.0))) + .with_auto_flow(auto_flow) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child( + demo_box(Lc::t("grid_box_span_2", &LOC)) + .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2))), + ) + .with_child( + demo_box(Lc::t("grid_box_span_2", &LOC)) + .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2))), + ) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))) +} + +fn auto_tracks_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_auto_tracks", &LOC)) + .with_child(caption( + Lc::t("grid_title_auto_tracks", &LOC), + Lc::n(concat!( + "Grid::new()", + ".with_columns(Tracks::repeat(3, AxisTrack::Fraction(1.0)))", + ".with_auto_rows(AxisTrack::Fixed(UnitValue::RelRem(3.0)))", + )), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0))) + .with_auto_rows(grid::AxisTrack::Fixed(UnitValue::RelRem(3.0))) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_child(demo_box(grid_box("1"))) + .with_child(demo_box(grid_box("2"))) + .with_child(demo_box(grid_box("3"))) + .with_child(demo_box(grid_box("4"))) + .with_child(demo_box(grid_box("5"))) + .with_child(demo_box(grid_box("6"))), + ) +} + +fn responsive_block() -> Block { + let mut row = demo_grid( + Grid::new() + .with_columns(grid::Tracks::repeat(1, grid::AxisTrack::Fraction(1.0))) + .with_columns_at( + Breakpoint::Sm, + grid::Tracks::repeat(2, grid::AxisTrack::Fraction(1.0)), + ) + .with_columns_at( + Breakpoint::Md, + grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)), + ) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ); + for n in 1..=6 { + row = row.with_child(demo_box(card_label(n))); + } + Block::new() + .with_title(Lc::t("grid_block_title_responsive", &LOC)) + .with_child(caption( + Lc::t("grid_title_responsive", &LOC), + Lc::n(concat!( + "Grid::new()", + ".with_columns(Tracks::repeat(1, AxisTrack::Fraction(1.0)))", + ".with_columns_at(Breakpoint::Sm, Tracks::repeat(2, AxisTrack::Fraction(1.0)))", + ".with_columns_at(Breakpoint::Md, Tracks::repeat(3, AxisTrack::Fraction(1.0)))", + )), + )) + .with_child(row) +} + +fn layout_block() -> Block { + Block::new() + .with_title(Lc::t("grid_block_title_layout", &LOC)) + .with_child(caption( + Lc::t("grid_title_layout", &LOC), + Lc::t("grid_desc_layout", &LOC), + )) + .with_child( + demo_grid( + Grid::new() + .with_columns( + grid::Tracks::new() + .with_track(grid::AxisTrack::Fixed(UnitValue::Px(120))) + .with_track(grid::AxisTrack::Fraction(1.0)) + .with_track(grid::AxisTrack::Fixed(UnitValue::Px(120))), + ) + .with_rows( + grid::Tracks::new() + .with_track(grid::AxisTrack::Auto) + .with_track(grid::AxisTrack::Fraction(1.0)) + .with_track(grid::AxisTrack::Auto), + ) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), + ) + .with_prop(PropsOp::add_style("min-height", "18rem")) + .with_child( + demo_box(Lc::t("grid_box_header", &LOC)) + .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(3))), + ) + .with_child(demo_box(Lc::t("grid_box_nav", &LOC))) + .with_child(demo_box(Lc::t("grid_box_content", &LOC))) + .with_child(demo_box(Lc::t("grid_box_aside", &LOC))) + .with_child( + demo_box(Lc::t("grid_box_footer", &LOC)) + .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(3))), + ), + ) +} + +// **< HELPERS >************************************************************************************ + +// Aspecto fijo de las cajas de muestra: esquinas rectas y borde claro, a diferencia de las cajas +// redondeadas de `intro-flex.rs`, para distinguir a simple vista los ejemplos de Grid de los de Flex. +fn demo_box_styles() -> AssetsOp { + AssetsOp::add_responsive_styles( + None, + "grid-demo-box", + [ + ("background-color", "#0d6efd"), + ("color", "#fff"), + ("border", "2px solid #6ea8fe"), + ("min-width", "3rem"), + ("width", "auto"), + ("max-width", "none"), + ("margin", "0"), + ("text-align", "center"), + ], + ) +} + +// Aspecto fijo de las filas de muestra. +fn demo_row_styles() -> AssetsOp { + AssetsOp::add_responsive_styles( + None, + "grid-demo-row", + [ + ("background-color", "#f1f3f5"), + ("width", "100%"), + ("max-width", "none"), + ("margin", "0 0 1.5rem"), + ("padding", "0.75rem"), + ], + ) +} + +// Caja con el relleno vertical estandar del resto de ejemplos. +fn demo_box(label: Lc) -> Container { + Container::new() + .with_prop(PropsOp::add_classes("grid-demo-box")) + .with_prop(PropsOp::add_style("padding", "0.5rem 1rem")) + .with_child(Html::with(move |cx| html! { (label.using(cx)) })) +} + +// Etiqueta "Box N" para las cajas que sólo se distinguen por su posición. +fn grid_box(n: impl Into) -> Lc { + Lc::t("grid_box_label", &LOC).with_arg("n", n) +} + +// Etiqueta "Tarjeta N" para las cajas de la rejilla responsive. +fn card_label(n: usize) -> Lc { + Lc::t("grid_box_card", &LOC).with_arg("n", n.to_string()) +} + +// Rejilla de demostracion con fondo gris para visualizar los limites del propio contenedor grid. +fn demo_grid(grid: Grid) -> Grid { + grid.with_prop(PropsOp::add_classes("grid-demo-row")) +} + +// Titulo y fragmento de codigo que introducen cada demostracion. +fn caption(title: Lc, code: Lc) -> Html { + Html::with(move |cx| { + html! { + h3 { (title.using(cx)) } + p { code { (code.using(cx)) } } + } + }) +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&IntroGrid).await.run().await +} diff --git a/examples/intro-responsive.rs b/examples/intro-responsive.rs index 6bcf1b0e..6306a678 100644 --- a/examples/intro-responsive.rs +++ b/examples/intro-responsive.rs @@ -245,10 +245,8 @@ fn card_label(n: usize) -> Lc { } // Fila de demostracion con fondo gris para visualizar los limites del propio contenedor flex. -fn demo_row(flex: Flex) -> Container { - Container::new() - .with_prop(PropsOp::add_classes("flex-demo-row")) - .with_flex(flex) +fn demo_row(flex: Flex) -> Flex { + flex.with_prop(PropsOp::add_classes("flex-demo-row")) } // Titulo y fragmento de codigo que introducen cada demostracion. diff --git a/examples/intro-spacing.rs b/examples/intro-spacing.rs index cb5b9f4f..eed4c38f 100644 --- a/examples/intro-spacing.rs +++ b/examples/intro-spacing.rs @@ -229,10 +229,8 @@ fn box_sample() -> Lc { } // Fila de demostración con fondo gris para visualizar los límites de cada caja. -fn demo_row(flex: Flex) -> Container { - Container::new() - .with_prop(PropsOp::add_classes("spacing-demo-row")) - .with_flex(flex) +fn demo_row(flex: Flex) -> Flex { + flex.with_prop(PropsOp::add_classes("spacing-demo-row")) } // Título y fragmento de código que introducen cada demostración. diff --git a/examples/locale/en-US/intro-grid.ftl b/examples/locale/en-US/intro-grid.ftl new file mode 100644 index 00000000..c0c3c241 --- /dev/null +++ b/examples/locale/en-US/intro-grid.ftl @@ -0,0 +1,49 @@ +grid_slogan = CSS Grid positioning +grid_block_title_columns = Columns +grid_block_title_rows_gap = Rows and gap +grid_block_title_placement = Item placement +grid_block_title_alignment = Align items +grid_block_title_content_alignment = Justify content +grid_block_title_item_alignment = Individual alignment +grid_block_title_auto_flow = Auto-placement algorithm +grid_block_title_auto_tracks = Implicit tracks +grid_block_title_responsive = Responsive grid +grid_block_title_layout = Composite structure: page layout + +grid_title_columns_thirds = Three equal columns +grid_title_columns_sidebar = Fixed sidebar + flexible content +grid_title_columns_min_max = Min-content, fraction, max-content +grid_title_rows = Fixed-height rows +grid_title_gap_distinct = Different row and column spacing +grid_title_placement = Column spanning and explicit ranges +grid_title_alignment_start = Justify/align items: start +grid_title_alignment_center = Justify/align items: center +grid_title_alignment_end = Justify/align items: end +grid_title_alignment_stretch = Justify/align items: stretch +grid_title_content_alignment_start = Justify content: start +grid_title_content_alignment_center = Justify content: center +grid_title_content_alignment_between = Justify content: space between +grid_title_content_alignment_evenly = Justify content: space evenly +grid_title_item_alignment = Overriding the container's alignment on a single item +grid_title_auto_flow_row = Default flow, leaves a gap +grid_title_auto_flow_dense = Dense flow, fills the gap +grid_title_auto_tracks = Equal-height implicit rows +grid_title_responsive = One column on mobile, two on tablet, three on desktop +grid_title_layout = Header and footer spanning three columns, flexible content area +grid_desc_layout = Grid::new() with fixed side columns, a flexible center column and explicit spans + +grid_box_label = Box { $n } +grid_box_card = Card { $n } +grid_box_sidebar = Sidebar +grid_box_content = Content +grid_box_min_content = Min-content +grid_box_fraction = 1fr +grid_box_max_content = Max-content +grid_box_plain = Plain +grid_box_span_2 = Spans 2 columns +grid_box_range = Columns 2 to 3 +grid_box_overridden = Overridden +grid_box_header = Header +grid_box_nav = Navigation +grid_box_aside = Aside +grid_box_footer = Footer diff --git a/examples/locale/es-ES/intro-grid.ftl b/examples/locale/es-ES/intro-grid.ftl new file mode 100644 index 00000000..5066795a --- /dev/null +++ b/examples/locale/es-ES/intro-grid.ftl @@ -0,0 +1,49 @@ +grid_slogan = Posicionamiento con CSS Grid +grid_block_title_columns = Columnas +grid_block_title_rows_gap = Filas y espaciado +grid_block_title_placement = Colocación de elementos +grid_block_title_alignment = Alineación de elementos +grid_block_title_content_alignment = Justificación de contenido +grid_block_title_item_alignment = Alineación individual +grid_block_title_auto_flow = Algoritmo de colocación automática +grid_block_title_auto_tracks = Pistas implícitas +grid_block_title_responsive = Rejilla responsive +grid_block_title_layout = Estructura compuesta: maquetación de página + +grid_title_columns_thirds = Tres columnas iguales +grid_title_columns_sidebar = Barra lateral fija + contenido flexible +grid_title_columns_min_max = Contenido mínimo, fracción, contenido máximo +grid_title_rows = Filas de altura fija +grid_title_gap_distinct = Espaciado distinto entre filas y columnas +grid_title_placement = Columnas abarcadas y rangos explícitos +grid_title_alignment_start = Justificación/alineación de elementos: inicio +grid_title_alignment_center = Justificación/alineación de elementos: centro +grid_title_alignment_end = Justificación/alineación de elementos: final +grid_title_alignment_stretch = Justificación/alineación de elementos: estirar +grid_title_content_alignment_start = Justificación de contenido: inicio +grid_title_content_alignment_center = Justificación de contenido: centro +grid_title_content_alignment_between = Justificación de contenido: espacio entre elementos +grid_title_content_alignment_evenly = Justificación de contenido: espacio repartido a partes iguales +grid_title_item_alignment = Sobrescribir la alineación del contenedor en un único elemento +grid_title_auto_flow_row = Flujo por defecto, deja un hueco +grid_title_auto_flow_dense = Flujo denso, rellena el hueco +grid_title_auto_tracks = Filas implícitas de igual altura +grid_title_responsive = Una columna en móvil, dos en tableta, tres en escritorio +grid_title_layout = Cabecera y pie abarcando tres columnas, área de contenido flexible +grid_desc_layout = Grid::new() con columnas laterales fijas, una columna central flexible y abarcados explícitos + +grid_box_label = Caja { $n } +grid_box_card = Tarjeta { $n } +grid_box_sidebar = Barra lateral +grid_box_content = Contenido +grid_box_min_content = Contenido mínimo +grid_box_fraction = 1fr +grid_box_max_content = Contenido máximo +grid_box_plain = Normal +grid_box_span_2 = Abarca 2 columnas +grid_box_range = Columnas 2 a 3 +grid_box_overridden = Sobrescrita +grid_box_header = Cabecera +grid_box_nav = Navegación +grid_box_aside = Aparte +grid_box_footer = Pie diff --git a/extensions/pagetop-admin/src/component/admin_frame.rs b/extensions/pagetop-admin/src/component/admin_frame.rs index cbdfd371..380beb47 100644 --- a/extensions/pagetop-admin/src/component/admin_frame.rs +++ b/extensions/pagetop-admin/src/component/admin_frame.rs @@ -9,8 +9,8 @@ use crate::registry; /// Renderiza breadcrumb, encabezado (título + acciones locales), tareas locales (pestañas) y el /// contenido de la página. No incluye ningún menú de navegación: mostrar las secciones del panel /// (el "sidebar" o el "menú superior", según el tema) es responsabilidad del tema que intercepte -/// [`CoreTemplates::Admin`](pagetop::core::theme::CoreTemplates::Admin) -- por ejemplo -/// `pagetop-bootsier` -- leyendo directamente [`crate::registry::global()`]. El tema básico de +/// [`CoreTemplates::Admin`](pagetop::core::theme::CoreTemplates::Admin) (por ejemplo +/// `pagetop-bootsier`) leyendo directamente [`crate::registry::global()`]. El tema básico de /// PageTop no lo hace, y una página de administración sigue siendo completamente navegable sin él, /// a través del *dashboard* (`/admin`), las páginas de sección y este mismo breadcrumb. /// diff --git a/extensions/pagetop-admin/src/lib.rs b/extensions/pagetop-admin/src/lib.rs index dfa7eab5..7e3d3e35 100644 --- a/extensions/pagetop-admin/src/lib.rs +++ b/extensions/pagetop-admin/src/lib.rs @@ -162,7 +162,7 @@ impl Extension for Admin { registry::build(); // Región neutra del core: cualquier tema puede decidir renderizarla (o no) sin que - // `pagetop-admin` dependa de ninguno en concreto -- ver `CoreRegions::Aside`. + // `pagetop-admin` dependa de ninguno en concreto (ver `CoreRegions::Aside`). InRegion::Global(&CoreRegions::Aside).add(component::AdminMenu::new()); } diff --git a/extensions/pagetop-bootsier/src/theme/bs/badge.rs b/extensions/pagetop-bootsier/src/theme/bs/badge.rs index ab058de7..913ad0d3 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/badge.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/badge.rs @@ -9,8 +9,8 @@ const EXTRA_COLOR: &str = "bootsier.badge.color"; /// Extensión de Bootsier para [`Badge`]. /// /// Permite forzar un color de la paleta de Bootsier ([`BootsierColors`]) en vez del que le -/// correspondería por defecto a la [`Intent`] del badge -- por ejemplo, para usar `Light`/`Dark`, -/// que `Intent` no tiene. +/// correspondería por defecto a la [`Intent`] del badge (por ejemplo, para usar `Light`/`Dark`, +/// que `Intent` no tiene). /// /// ```rust,no_run /// use pagetop::prelude::*; @@ -39,7 +39,7 @@ impl BadgeBootsier for Badge { // **< Badge SETUP >******************************************************************************** pub(crate) fn setup(badge: &mut Badge) { - // `Badge::setup()` (core) ya ha traducido la intención con `Theme::intent_color()` -- la clase + // `Badge::setup()` (core) ya ha traducido la intención con `Theme::intent_color()`. La clase // `badge-*` que hay que localizar es siempre la derivada de la `Intent`, con independencia de // que `BadgeBootsier::with_color()` fuerce un color distinto para el destino `text-bg-*`. let intent_color = BootsierColors::from(badge.intent()).as_str(); diff --git a/extensions/pagetop-bootsier/src/theme/bs/button.rs b/extensions/pagetop-bootsier/src/theme/bs/button.rs index 18b80175..9ecd8f05 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/button.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/button.rs @@ -75,10 +75,9 @@ impl ButtonBootsier for Button { pub(crate) fn setup(button: &mut Button) { button.alter_prop(PropsOp::replace_classes("button", "btn")); - // `Button::setup()` (core) ya ha traducido la intención con `Theme::intent_color()` -- aquí - // sólo queda cambiar el prefijo `button-`/`button-outline-` por el equivalente - // `btn-`/`btn-outline-` de Bootstrap, conservando el mismo nombre de color salvo que - // `with_color()` lo sobrescriba. + // `Button::setup()` (core) ya ha traducido la intención con `Theme::intent_color()`. Aquí sólo + // queda cambiar el prefijo `button-`/`button-outline-` por el equivalente `btn-`/`btn-outline-` + // de Bootstrap, conservando el mismo nombre de color salvo que `with_color()` lo sobrescriba. let override_color = button .props() .extra::(EXTRA_COLOR) diff --git a/extensions/pagetop-bootsier/src/theme/bs/layout/region.rs b/extensions/pagetop-bootsier/src/theme/bs/layout/region.rs index 968d7373..3c3f296a 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/layout/region.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/layout/region.rs @@ -97,8 +97,8 @@ impl RegionName for BootsierRegions { // **< Region RENDER >****************************************************************************** // Regiones de Bootsier: se renderizan sin el `
` envolvente que aplica -// `layout::Region::prepare()` por defecto -- sus elementos van directamente dentro del contenedor -// que los gestiona (sidebar-menu o navbar-nav). Devuelve `None` si `component` no envuelve una +// `layout::Region::prepare()` por defecto. Sus elementos van directamente dentro del contenedor que +// los gestiona (sidebar-menu o navbar-nav). Devuelve `None` si `component` no envuelve una // `BootsierRegions`, dejando que el resto de la cadena de temas (o el propio componente) resuelva // el renderizado por defecto. pub(crate) async fn render( diff --git a/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs b/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs index 99a33065..8b80c84b 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/layout/template.rs @@ -5,10 +5,10 @@ use crate::theme::{ContainerBootsier, bs}; use crate::{ADMINLTE_VERSION, LOCALES_BOOTSIER}; // Regiones de Bootsier: se renderiza sin el `
` envolvente que aplica -// `layout::Template::prepare()` por defecto -- delega en `render_standard()`/`render_admin()` -// según la variante de `CoreTemplates` que envuelva el componente. Devuelve `None` si -// `component` no envuelve una `CoreTemplates`, dejando que el resto de la cadena de temas (o -// el propio componente) resuelva el renderizado por defecto. +// `layout::Template::prepare()` por defecto; delega en `render_standard()`/`render_admin()` según +// la variante de `CoreTemplates` que envuelva el componente. Devuelve `None` si `component` no +// envuelve una `CoreTemplates`, dejando que el resto de la cadena de temas (o el propio componente) +// resuelva el renderizado por defecto. pub(crate) async fn render( component: &layout::Template, cx: &mut Context, diff --git a/extensions/pagetop-menu/src/component/menu_block.rs b/extensions/pagetop-menu/src/component/menu_block.rs index 589d8f13..aa6e6555 100644 --- a/extensions/pagetop-menu/src/component/menu_block.rs +++ b/extensions/pagetop-menu/src/component/menu_block.rs @@ -4,8 +4,8 @@ use crate::tree::{MenuKey, MenuNode, TreeOptions, build_tree, try_resolve_menu_u /// Renderiza un menú completo como bloque de navegación HTML. /// -/// Se construye componiendo [`Nav`] y [`nav::Item`] -- con [`Dropdown`]/[`dropdown::Item`] para -/// los nodos con hijos -- a partir del árbol del menú, así que produce el mismo marcado accesible +/// Se construye componiendo [`Nav`] y [`nav::Item`] (con [`Dropdown`]/[`dropdown::Item`] para +/// los nodos con hijos) a partir del árbol del menú, así que produce el mismo marcado accesible /// que cualquier `Nav`/`Dropdown` y se beneficia igual del CSS/JavaScript que aporta el tema /// activo: /// @@ -25,17 +25,17 @@ use crate::tree::{MenuKey, MenuNode, TreeOptions, build_tree, try_resolve_menu_u /// /// # Limitaciones conocidas /// -/// - **Profundidad máxima de 2 niveles.** [`Dropdown`] no admite submenús anidados (como -/// Bootstrap, del que toma su marcado): un nodo de tercer nivel o más profundo nunca se -/// construye -- [`TreeOptions::max_depth`] se acota internamente a `2` con independencia de lo -/// que indique [`with_max_depth()`](Self::with_max_depth), así que no hay pérdida silenciosa de -/// datos, sencillamente no se piden a la base de datos. +/// - **Profundidad máxima de 2 niveles.** [`Dropdown`] no admite submenús anidados (como Bootstrap, +/// del que toma su marcado): un nodo de tercer nivel o más profundo nunca se construye. +/// [`TreeOptions::max_depth`] se acota internamente a `2` con independencia de lo que indique +/// [`with_max_depth()`](Self::with_max_depth), así que no hay pérdida silenciosa de datos, +/// sencillamente no se piden a la base de datos. /// - **Sin colapso responsive propio.** A diferencia del antiguo `Menu::collapsible`, `Nav` es una -/// lista plana sin botón ni JavaScript de colapso; una aplicación que necesite ese -/// comportamiento debe envolver `MenuBlock` en su propia chrome hasta que exista un componente -/// `Navbar` en el core. +/// lista plana sin botón ni JavaScript de colapso; una aplicación que necesite ese comportamiento +/// debe envolver `MenuBlock` en su propia chrome hasta que exista un componente `Navbar` en el +/// core. /// - `MenuNode::in_active_trail` y `MenuNode::expanded` (ver [`crate::tree::MenuNode`]) todavía no -/// se reflejan en el marcado -- ni [`nav::Item`] ni [`dropdown::Item`] tienen hoy una forma +/// se reflejan en el marcado. Ni [`nav::Item`] ni [`dropdown::Item`] tienen hoy una forma /// verificada de pre-abrirse en el servidor sin que la mejora progresiva del tema /// (`accessible-menu` en `Basic`) lo sobrescriba al inicializarse. Sólo se traduce el estado /// `is_active`/`enabled` de cada nodo. @@ -149,7 +149,7 @@ impl MenuBlock { // Convierte un `MenuNode` de nivel 1 (raíz) en un `nav::Item`: sin hijos, enlace (o etiqueta sin // ruta propia); con hijos, activador de un `Dropdown` con sus hijos como `dropdown::Item`. Los -// hijos de un `MenuNode` de nivel 1 nunca tienen a su vez hijos propios -- `TreeOptions::max_depth` +// hijos de un `MenuNode` de nivel 1 nunca tienen a su vez hijos propios. `TreeOptions::max_depth` // se acota a 2 en `MenuBlock::prepare()`, así que no hay un tercer nivel que representar. fn node_to_item(node: &MenuNode, cx: &Context) -> nav::Item { if !node.children.is_empty() { diff --git a/extensions/pagetop-user/src/component/password_confirm.rs b/extensions/pagetop-user/src/component/password_confirm.rs index 480b2ca1..70e3a773 100644 --- a/extensions/pagetop-user/src/component/password_confirm.rs +++ b/extensions/pagetop-user/src/component/password_confirm.rs @@ -7,7 +7,7 @@ use crate::LOCALES_USER; /// Campos de contraseña y confirmación, compuestos a partir de /// [`form::input::Field::password()`] del core. No valida en ningún momento que ambos valores -/// coincidan -- eso ocurre en el servidor, tras el envío del formulario -- sólo renderiza los dos +/// coincidan (eso ocurre en el servidor, tras el envío del formulario), sólo renderiza los dos /// campos, uno junto al otro, sin ningún contenedor propio. #[derive(AutoDefault, Clone, Debug, Getters)] pub(crate) struct PasswordConfirm { diff --git a/extensions/pagetop-user/src/handlers/admin/users.rs b/extensions/pagetop-user/src/handlers/admin/users.rs index b77023c3..ed045684 100644 --- a/extensions/pagetop-user/src/handlers/admin/users.rs +++ b/extensions/pagetop-user/src/handlers/admin/users.rs @@ -330,7 +330,7 @@ fn edit_actions( can_toggle_admin: bool, waypoint: &Waypoint, cx: &mut Context, -) -> Container { +) -> Flex { let (next_status, label_key) = match status { UserStatus::Blocked => ("active", "btn-activate"), _ => ("blocked", "btn-block"), @@ -361,13 +361,10 @@ fn edit_actions( status_form = status_form.with_prop(PropsOp::set(hx::CONFIRM, confirm)); } - let mut container = Container::new() - .with_flex( - Flex::new() - .with_wrap(flex::Behavior::Wrap) - .with_align(flex::Align::Center) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), - ) + let mut container = Flex::new() + .with_wrap(flex::Behavior::Wrap) + .with_align(align::Items::Center) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))) .with_child( Button::submit(Lc::t("btn-save", &LOCALES_USER)) .with_style(button::Style::Solid(Intent::Primary)) diff --git a/src/base/component.rs b/src/base/component.rs index 86409bda..807c57e2 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -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; diff --git a/src/base/component/container.rs b/src/base/component/container.rs index 9fcf344c..a33ad79f 100644 --- a/src/base/component/container.rs +++ b/src/base/component/container.rs @@ -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>) -> 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) -> Self { diff --git a/src/html/flex/container.rs b/src/base/component/flex.rs similarity index 54% rename from src/html/flex/container.rs rename to src/base/component/flex.rs index 52d9ebd9..6419411e 100644 --- a/src/html/flex/container.rs +++ b/src/base/component/flex.rs @@ -1,13 +1,11 @@ -use crate::core::component::Context; -use crate::core::theme::{Breakpoint, Responsive}; -use crate::html::flex::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap}; -use crate::{AutoDefault, Getters, builder_impl, util}; +use crate::html::align; +use crate::html::flex::{Behavior, ContentJustify, Direction}; +use crate::prelude::*; // **< DisplayFlex >******************************************************************************** -// Modo de activación del posicionamiento Flexbox de un contenedor `Flex`. Detalle interno de -// implementación: la API pública sólo expone los constructores `Flex::new()`, `Flex::at()`, -// `Flex::inline()` e `Flex::inline_at()`, nunca esta variante directamente. +// 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] @@ -19,46 +17,55 @@ enum DisplayFlex { // **< Flex >*************************************************************************************** -/// Configuración para el posicionamiento Flexbox en un contenedor. +/// Componente que crea un **contenedor Flexbox** para posicionar componentes. /// -/// Se resuelve como clases CSS generadas dinámicamente (`display`, `flex-direction`, `flex-wrap`, -/// `justify-content`, `align-items`, `align-content`, `gap`), registradas vía -/// [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en el -/// `` 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. +/// 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. /// -/// El nombre de cada clase se deriva de la propiedad y el valor que representa (por ejemplo -/// `_flex-direction_row_`), así que dos contenedores con la misma configuración comparten la misma -/// regla generada en vez de duplicarla, y el nombre generado no coincide por accidente con clases -/// de terceros. +/// 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 `` 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 = Container::new() -/// .with_flex( -/// Flex::new() -/// .with_justify(flex::ContentJustify::End) -/// .with_align(flex::Align::Center) -/// .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), -/// ) +/// 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, Copy, Debug, PartialEq, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Flex { - // 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 contenedor que nunca ha llamado a `with_flex()`. Sin getter público; `new()`, `at()`, - // `inline()` e `inline_at()` son la única forma de activarlo. + /// 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, - /// Devuelve la dirección del eje principal por punto de corte. + /// Devuelve la dirección del eje principal, por punto de corte. #[getters(copy)] direction: Responsive, /// Devuelve el comportamiento cuando los elementos no caben en una sola línea, por punto de @@ -70,18 +77,42 @@ pub struct Flex { justify: Responsive, /// Devuelve la alineación de los elementos en el eje transversal, por punto de corte. #[getters(copy)] - align: Responsive, + align: Responsive, /// 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: Responsive, /// Devuelve el espaciado entre elementos, por punto de corte. #[getters(copy)] - gap: Responsive, + gap: Responsive, + /// 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 { + self.props.get_id() + } + + async fn prepare(&self, cx: &mut Context) -> Result { + 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 Flex con `display: flex`, sin punto de corte: se aplica siempre. + /// Define una configuración con `display: flex`, sin punto de corte: se aplica siempre. pub fn new() -> Self { Self { display: Some(DisplayFlex::Always), @@ -89,7 +120,7 @@ impl Flex { } } - /// Define una configuración Flex con `display: flex` que se aplica a partir del punto de corte + /// Define una configuración con `display: flex` que se aplica a partir del punto de corte /// indicado. pub fn at(bp: Breakpoint) -> Self { Self { @@ -98,7 +129,7 @@ impl Flex { } } - /// Define una configuración Flex con `display: inline-flex`, sin punto de corte: se aplica + /// Define una configuración con `display: inline-flex`, sin punto de corte: se aplica /// siempre. pub fn inline() -> Self { Self { @@ -107,7 +138,7 @@ impl Flex { } } - /// Define una configuración Flex con `display: inline-flex` que se aplica a partir del punto de + /// 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 { @@ -118,6 +149,18 @@ impl Flex { // **< 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) -> 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) -> 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); @@ -157,83 +200,61 @@ impl Flex { } /// Establece la alineación de los elementos en el eje transversal. - pub fn with_align(mut self, align: Align) -> Self { + 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) -> Self { + 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 [`AlignContent`]). - pub fn with_align_content(mut self, align_content: AlignContent) -> 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 [`AlignContent`]), a partir - /// del punto de corte indicado. - pub fn with_align_content_at(mut self, bp: Breakpoint, align_content: AlignContent) -> 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: Gap) -> Self { + 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: Gap) -> Self { + 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) -> Self { + self.children.alter_child(op.into()); + self + } } impl Flex { - /// Combina esta configuración con otra `Flex`, campo a campo, o la resetea a los valores por - /// defecto si se pasa `None`. - /// - /// Cada campo de `flex` que tenga un valor sustituye al correspondiente de `self`; los que - /// estén a `None` dejan intacto el valor ya presente en `self`. Así, sucesivas llamadas pueden - /// ir completando o sobrescribiendo campos concretos sin necesidad de repetir los ya - /// establecidos. Es el método recomendado para que un contenedor propio adopte `Flex` de forma - /// incremental (ver [`Container::with_flex()`](crate::base::component::Container::with_flex) - /// como referencia de uso). - pub fn merge(mut self, flex: impl Into>) -> Self { - let Some(flex) = flex.into() else { - return Flex::default(); - }; - self.display = flex.display.or(self.display); - self.direction = self.direction.merge(flex.direction); - self.wrap = self.wrap.merge(flex.wrap); - self.justify = self.justify.merge(flex.justify); - self.align = self.align.merge(flex.align); - self.align_content = self.align_content.merge(flex.align_content); - self.gap = self.gap.merge(flex.gap); - self - } - - /// Aplica esta configuración a un [`Props`] como declaraciones de estilo en línea. - /// - /// Es el método recomendado para que un componente adopte `Flex`: concentra en un único sitio - /// la traducción de la configuración a estilos, para no repetirla en cada componente que la - /// use. Precedente: [`Container`](crate::base::component::Container) lo aplica sobre su - /// propio `Props`; [`Navbar`](crate::base::component::Navbar), sobre el `Props` de su área de - /// contenido. - /// - /// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera, - /// para poder compartir un único acumulador con [`FlexItem::apply()`](super::FlexItem::apply) - /// sin cadenas intermedias. + // 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] - pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) { - // Sin `display` no hay contenedor Flex: el resto de facetas (`flex-direction`, `gap`...) - // no tienen ningún efecto en CSS sin `display: flex`/`inline-flex`, así que ni se generan. + 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; }; diff --git a/src/base/component/grid.rs b/src/base/component/grid.rs new file mode 100644 index 00000000..502c7362 --- /dev/null +++ b/src/base/component/grid.rs @@ -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 `` 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, + /// Devuelve las pistas de columna (`grid-template-columns`), por punto de corte. + #[getters(copy)] + columns: Responsive, + /// Devuelve las pistas de fila (`grid-template-rows`), por punto de corte. + #[getters(copy)] + rows: Responsive, + /// Devuelve el tamaño de las columnas generadas implícitamente (`grid-auto-columns`), por + /// punto de corte. + #[getters(copy)] + auto_columns: Responsive, + /// Devuelve el tamaño de las filas generadas implícitamente (`grid-auto-rows`), por punto de + /// corte. + #[getters(copy)] + auto_rows: Responsive, + /// Devuelve el algoritmo de colocación automática, por punto de corte. + #[getters(copy)] + auto_flow: Responsive, + /// Devuelve la alineación de los elementos en el eje de columnas, por punto de corte. + #[getters(copy)] + justify_items: Responsive, + /// Devuelve la alineación de los elementos en el eje de filas, por punto de corte. + #[getters(copy)] + align_items: Responsive, + /// Devuelve la alineación de las pistas en el eje de columnas, por punto de corte. + #[getters(copy)] + justify_content: Responsive, + /// Devuelve la alineación de las pistas en el eje de filas, por punto de corte. + #[getters(copy)] + align_content: Responsive, + /// Devuelve el espaciado entre pistas, por punto de corte. + #[getters(copy)] + gap: Responsive, + /// 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 { + self.props.get_id() + } + + async fn prepare(&self, cx: &mut Context) -> Result { + 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) -> 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) -> 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) -> 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); + } + } + } +} diff --git a/src/base/component/navbar/component.rs b/src/base/component/navbar/component.rs index 3b00b6a2..c10a5cb5 100644 --- a/src/base/component/navbar/component.rs +++ b/src/base/component/navbar/component.rs @@ -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::(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 `