diff --git a/assets/css/basic.css b/assets/css/basic.css index 325a34d5..1f92da1f 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -353,11 +353,13 @@ body { display: block; } +/* Options and headers stay on one line, so the menu grows to fit the longest one. */ .dropdown-item { display: block; width: 100%; padding: 0.375rem 1rem; text-align: left; + white-space: nowrap; color: var(--val-color--text); text-decoration: none; background-color: transparent; @@ -387,6 +389,7 @@ body { display: block; padding: 0.5rem 1rem 0.25rem; margin: 0; + white-space: nowrap; font-size: 0.8125rem; font-weight: 600; color: var(--val-color--text--muted); @@ -672,28 +675,182 @@ input:disabled + label { color: var(--val-color--text--muted); } -@media (max-width: 767.98px) { - .navbar { +/* + * A navbar with a toggle button collapses below the breakpoint set by its `navbar-expand-{name}` + * class, added by the `Navbar` component. `navbar-expand` alone (a breakpoint with no minimum + * width) never collapses and needs no rules, since the base rules above are the expanded layout. + * The widths below match the default `Theme::breakpoint_entry()` table, so a child theme that + * renames or resizes the breakpoints must also redefine these classes. + * + * While collapsed, the `.nav` items are stacked in a column, so a dropdown menu inside it is laid + * out in the flow (`position: static`) to push down the following items instead of covering them. + */ +@media (max-width: 575.98px) { + .navbar-expand-sm { flex-wrap: wrap; } - .navbar:has(.navbar-toggle) .navbar-toggle { + .navbar-expand-sm:has(.navbar-toggle) .navbar-toggle { display: inline-block; order: -1; } - .navbar:has(.navbar-toggle) .navbar-content { + .navbar-expand-sm:has(.navbar-toggle) .navbar-content { display: none; flex-direction: column; align-items: stretch; width: 100%; } - .navbar:has(.navbar-toggle) .navbar-content.show { + .navbar-expand-sm:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar .nav { + .navbar-expand-sm .nav { flex-direction: column; align-items: stretch; width: 100%; } + .navbar-expand-sm .nav .dropdown-menu { + position: static; + box-shadow: none; + } +} + +@media (max-width: 767.98px) { + .navbar-expand-md { + flex-wrap: wrap; + } + .navbar-expand-md:has(.navbar-toggle) .navbar-toggle { + display: inline-block; + order: -1; + } + .navbar-expand-md:has(.navbar-toggle) .navbar-content { + display: none; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-md:has(.navbar-toggle) .navbar-content.show { + display: flex; + } + .navbar-expand-md .nav { + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-md .nav .dropdown-menu { + position: static; + box-shadow: none; + } +} + +@media (max-width: 991.98px) { + .navbar-expand-lg { + flex-wrap: wrap; + } + .navbar-expand-lg:has(.navbar-toggle) .navbar-toggle { + display: inline-block; + order: -1; + } + .navbar-expand-lg:has(.navbar-toggle) .navbar-content { + display: none; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-lg:has(.navbar-toggle) .navbar-content.show { + display: flex; + } + .navbar-expand-lg .nav { + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-lg .nav .dropdown-menu { + position: static; + box-shadow: none; + } +} + +@media (max-width: 1199.98px) { + .navbar-expand-xl { + flex-wrap: wrap; + } + .navbar-expand-xl:has(.navbar-toggle) .navbar-toggle { + display: inline-block; + order: -1; + } + .navbar-expand-xl:has(.navbar-toggle) .navbar-content { + display: none; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-xl:has(.navbar-toggle) .navbar-content.show { + display: flex; + } + .navbar-expand-xl .nav { + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-xl .nav .dropdown-menu { + position: static; + box-shadow: none; + } +} + +@media (max-width: 1399.98px) { + .navbar-expand-xxl { + flex-wrap: wrap; + } + .navbar-expand-xxl:has(.navbar-toggle) .navbar-toggle { + display: inline-block; + order: -1; + } + .navbar-expand-xxl:has(.navbar-toggle) .navbar-content { + display: none; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-xxl:has(.navbar-toggle) .navbar-content.show { + display: flex; + } + .navbar-expand-xxl .nav { + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-xxl .nav .dropdown-menu { + position: static; + box-shadow: none; + } +} + +@media (max-width: 1919.98px) { + .navbar-expand-xxxl { + flex-wrap: wrap; + } + .navbar-expand-xxxl:has(.navbar-toggle) .navbar-toggle { + display: inline-block; + order: -1; + } + .navbar-expand-xxxl:has(.navbar-toggle) .navbar-content { + display: none; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-xxxl:has(.navbar-toggle) .navbar-content.show { + display: flex; + } + .navbar-expand-xxxl .nav { + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar-expand-xxxl .nav .dropdown-menu { + position: static; + box-shadow: none; + } } /* diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 8bb3112d..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(flex::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(flex::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(flex::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 b13e362a..4e07a0bb 100644 --- a/examples/intro-flex.rs +++ b/examples/intro-flex.rs @@ -113,7 +113,7 @@ fn justify_block() -> Block { demo_row( Flex::new() .with_justify(justify) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child(demo_box(flex_item("1"))) .with_child(demo_box(flex_item("2"))) @@ -126,26 +126,26 @@ fn justify_block() -> Block { fn align_block() -> Block { let mut block = Block::new().with_title(Lc::t("flex_block_title_align", &LOC)); - let align_variants: [(&str, flex::Align, &str); 4] = [ + let align_variants: [(&str, align::Items, &str); 4] = [ ( "flex_title_align_start", - flex::Align::Start, - "Flex::new().with_align(Align::Start)", + align::Items::Start, + "Flex::new().with_align(align::Items::Start)", ), ( "flex_title_align_center", - flex::Align::Center, - "Flex::new().with_align(Align::Center)", + align::Items::Center, + "Flex::new().with_align(align::Items::Center)", ), ( "flex_title_align_end", - flex::Align::End, - "Flex::new().with_align(Align::End)", + align::Items::End, + "Flex::new().with_align(align::Items::End)", ), ( "flex_title_align_stretch", - flex::Align::Stretch, - "Flex::new().with_align(Align::Stretch)", + align::Items::Stretch, + "Flex::new().with_align(align::Items::Stretch)", ), ]; for (title_key, align, code) in align_variants { @@ -155,7 +155,7 @@ fn align_block() -> Block { demo_row( Flex::new() .with_align(align) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")) .with_child(demo_box(Lc::t("flex_box_medium", &LOC))) @@ -165,13 +165,13 @@ fn align_block() -> Block { block .with_child(caption( Lc::t("flex_title_align_baseline", &LOC), - Lc::n("Flex::new().with_align(Align::Baseline)"), + Lc::n("Flex::new().with_align(align::Items::Baseline)"), )) .with_child( demo_row( Flex::new() - .with_align(flex::Align::Baseline) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_align(align::Items::Baseline) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child( sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem") @@ -185,26 +185,26 @@ fn align_block() -> Block { fn align_self_block() -> Block { let mut block = Block::new().with_title(Lc::t("flex_block_title_align_self", &LOC)); - let align_self_variants: [(&str, flex::ItemAlign, &str); 4] = [ + let align_self_variants: [(&str, align::ItemSelf, &str); 4] = [ ( "flex_title_align_self_start", - flex::ItemAlign::Start, - "FlexItem::new().with_align_self(flex::ItemAlign::Start)", + align::ItemSelf::Start, + "FlexItem::new().with_align_self(align::ItemSelf::Start)", ), ( "flex_title_align_self_end", - flex::ItemAlign::End, - "FlexItem::new().with_align_self(flex::ItemAlign::End)", + align::ItemSelf::End, + "FlexItem::new().with_align_self(align::ItemSelf::End)", ), ( "flex_title_align_self_center", - flex::ItemAlign::Center, - "FlexItem::new().with_align_self(flex::ItemAlign::Center)", + align::ItemSelf::Center, + "FlexItem::new().with_align_self(align::ItemSelf::Center)", ), ( "flex_title_align_self_stretch", - flex::ItemAlign::Stretch, - "FlexItem::new().with_align_self(flex::ItemAlign::Stretch)", + align::ItemSelf::Stretch, + "FlexItem::new().with_align_self(align::ItemSelf::Stretch)", ), ]; for (title_key, align_self, code) in align_self_variants { @@ -213,8 +213,8 @@ fn align_self_block() -> Block { .with_child( demo_row( Flex::new() - .with_align(flex::Align::Start) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_align(align::Items::Start) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")) .with_child( @@ -226,13 +226,13 @@ fn align_self_block() -> Block { block .with_child(caption( Lc::t("flex_title_align_self_baseline", &LOC), - Lc::n("FlexItem::new().with_align_self(flex::ItemAlign::Baseline)"), + Lc::n("FlexItem::new().with_align_self(align::ItemSelf::Baseline)"), )) .with_child( demo_row( Flex::new() - .with_align(flex::Align::Start) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_align(align::Items::Start) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child( sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem") @@ -240,7 +240,7 @@ fn align_self_block() -> Block { ) .with_child( demo_box(flex_item("1")) - .with_prop(FlexItem::new().with_align_self(flex::ItemAlign::Baseline)), + .with_prop(FlexItem::new().with_align_self(align::ItemSelf::Baseline)), ) .with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")), ) @@ -249,41 +249,41 @@ fn align_self_block() -> Block { fn align_content_block() -> Block { let mut block = Block::new().with_title(Lc::t("flex_block_title_align_content", &LOC)); - let align_content_variants: [(&str, flex::AlignContent, &str); 7] = [ + let align_content_variants: [(&str, align::Content, &str); 7] = [ ( "flex_title_align_content_start", - flex::AlignContent::Start, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Start)", + align::Content::Start, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::Start)", ), ( "flex_title_align_content_end", - flex::AlignContent::End, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::End)", + align::Content::End, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::End)", ), ( "flex_title_align_content_center", - flex::AlignContent::Center, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Center)", + align::Content::Center, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::Center)", ), ( "flex_title_align_content_between", - flex::AlignContent::SpaceBetween, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceBetween)", + align::Content::SpaceBetween, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::SpaceBetween)", ), ( "flex_title_align_content_around", - flex::AlignContent::SpaceAround, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceAround)", + align::Content::SpaceAround, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::SpaceAround)", ), ( "flex_title_align_content_evenly", - flex::AlignContent::SpaceEvenly, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::SpaceEvenly)", + align::Content::SpaceEvenly, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::SpaceEvenly)", ), ( "flex_title_align_content_stretch", - flex::AlignContent::Stretch, - "Flex::new().with_wrap(Behavior::Wrap).with_align_content(AlignContent::Stretch)", + align::Content::Stretch, + "Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::Stretch)", ), ]; for (title_key, align_content, code) in align_content_variants { @@ -291,7 +291,7 @@ fn align_content_block() -> Block { Flex::new() .with_wrap(flex::Behavior::Wrap) .with_align_content(align_content) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_prop(PropsOp::add_style("max-width", "21rem")) .with_prop(PropsOp::add_style("min-height", "11rem")); @@ -316,7 +316,7 @@ fn grow_shrink_block() -> Block { Lc::n("FlexItem::new().with_grow(flex::ItemGrow::Is1)"), )) .with_child( - demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_child(demo_box(Lc::t("flex_box_fixed", &LOC))) .with_child( demo_box(Lc::t("flex_box_grows", &LOC)) @@ -329,7 +329,7 @@ fn grow_shrink_block() -> Block { Lc::n("FlexItem::new().with_shrink(flex::ItemShrink::Is0)"), )) .with_child( - demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_prop(PropsOp::add_style("max-width", "31rem")) .with_child( demo_box(flex_item("1")).with_prop(PropsOp::add_style("width", "10.5rem")), @@ -354,7 +354,7 @@ fn other_block() -> Block { Lc::n("FlexItem::push_end()"), )) .with_child( - demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_child(demo_box(Lc::t("flex_box_start_1", &LOC))) .with_child(demo_box(Lc::t("flex_box_start_2", &LOC))) .with_child(demo_box(Lc::t("flex_box_end", &LOC)).with_prop(FlexItem::push_end())), @@ -363,8 +363,8 @@ fn other_block() -> Block { let mut wrap_row = demo_row( Flex::new() .with_wrap(flex::Behavior::Wrap) - .with_align_content(flex::AlignContent::SpaceBetween) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_align_content(align::Content::SpaceBetween) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_prop(PropsOp::add_style("max-width", "20rem")) .with_prop(PropsOp::add_style("min-height", "11rem")); @@ -380,7 +380,7 @@ fn other_block() -> Block { Lc::n(concat!( "Flex::new()", ".with_wrap(Behavior::Wrap)", - ".with_align_content(AlignContent::SpaceBetween)", + ".with_align_content(align::Content::SpaceBetween)", )), )) .with_child(wrap_row) @@ -389,7 +389,7 @@ fn other_block() -> Block { Lc::n("FlexItem::new().with_order(ItemOrder::First) / .with_order(ItemOrder::Last)"), )) .with_child( - demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_child( demo_box(Lc::n("A")) .with_prop(FlexItem::new().with_order(flex::ItemOrder::Last)), @@ -404,7 +404,7 @@ fn other_block() -> Block { ) .with_child(caption( Lc::t("flex_title_gap_none", &LOC), - Lc::n("Flex::new() (Gap::None por defecto)"), + Lc::n("Flex::new() (align::Gap::None por defecto)"), )) .with_child( demo_row(Flex::new()) @@ -414,10 +414,10 @@ fn other_block() -> Block { ) .with_child(caption( Lc::t("flex_title_gap_some", &LOC), - Lc::n("Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5)))"), + Lc::n("Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(1.5)))"), )) .with_child( - demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(1.5)))) + demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(1.5)))) .with_child(demo_box(flex_item("1"))) .with_child(demo_box(flex_item("2"))) .with_child(demo_box(flex_item("3"))), @@ -465,8 +465,8 @@ fn other_block() -> Block { .with_child( demo_row( Flex::new() - .with_align(flex::Align::Center) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_align(align::Items::Center) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child(demo_box(Lc::t("flex_box_file", &LOC))) .with_child(demo_box(Lc::t("flex_box_edit", &LOC))) @@ -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 fd53d4fe..6306a678 100644 --- a/examples/intro-responsive.rs +++ b/examples/intro-responsive.rs @@ -52,7 +52,7 @@ fn activation_block() -> Block { Lc::n("Flex::at(Breakpoint::Md)"), )) .with_child( - demo_row(Flex::at(Breakpoint::Md).with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::at(Breakpoint::Md).with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_child(demo_box(Lc::t("responsive_box_nav_home", &LOC))) .with_child(demo_box(Lc::t("responsive_box_nav_products", &LOC))) .with_child(demo_box(Lc::t("responsive_box_nav_about", &LOC))) @@ -76,7 +76,7 @@ fn direction_block() -> Block { Flex::new() .with_direction(flex::Direction::Column) .with_direction_at(Breakpoint::Md, flex::Direction::RowReverse) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child(sized_box( Lc::t("responsive_box_image", &LOC), @@ -124,7 +124,7 @@ fn justify_align_block() -> Block { "Flex::new()", ".with_justify(ContentJustify::Center)", ".with_justify_at(Breakpoint::Md, ContentJustify::SpaceBetween)", - ".with_align(Align::Center)", + ".with_align(align::Items::Center)", )), )) .with_child( @@ -132,8 +132,8 @@ fn justify_align_block() -> Block { Flex::new() .with_justify(flex::ContentJustify::Center) .with_justify_at(Breakpoint::Md, flex::ContentJustify::SpaceBetween) - .with_align(flex::Align::Center) - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))), + .with_align(align::Items::Center) + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))), ) .with_child(demo_box(Lc::t("responsive_box_logo", &LOC))) .with_child(demo_box(Lc::t("responsive_box_menu", &LOC))), @@ -151,7 +151,7 @@ fn order_block() -> Block { )), )) .with_child( - demo_row(Flex::at(Breakpoint::Lg).with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::at(Breakpoint::Lg).with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_child(demo_box(Lc::t("responsive_box_content", &LOC))) .with_child(demo_box(Lc::t("responsive_box_sidebar", &LOC)).with_prop( FlexItem::new().with_order_at(Breakpoint::Lg, flex::ItemOrder::First), @@ -166,7 +166,7 @@ fn gap_grow_block() -> Block { Lc::t("responsive_title_gap_grow", &LOC), Lc::n(concat!( "Flex::new()", - ".with_gap(Gap::Both(RelRem(0.5)))", + ".with_gap(align::Gap::Both(RelRem(0.5)))", ".with_gap_at(Breakpoint::Md, Gap::Both(RelRem(1.5)))", " + FlexItem::new().with_grow_at(Breakpoint::Md, ItemGrow::Is1)", )), @@ -174,8 +174,8 @@ fn gap_grow_block() -> Block { .with_child( demo_row( Flex::new() - .with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))) - .with_gap_at(Breakpoint::Md, flex::Gap::Both(UnitValue::RelRem(1.5))), + .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))) + .with_gap_at(Breakpoint::Md, align::Gap::Both(UnitValue::RelRem(1.5))), ) .with_child(demo_box(Lc::t("responsive_box_file", &LOC))) .with_child(demo_box(Lc::t("responsive_box_edit", &LOC))) @@ -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 75b9e24b..eed4c38f 100644 --- a/examples/intro-spacing.rs +++ b/examples/intro-spacing.rs @@ -66,7 +66,7 @@ fn padding_block() -> Block { block = block .with_child(caption(Lc::t(title_key, &LOC), Lc::n(code))) .with_child( - demo_row(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5)))) + demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))) .with_child(demo_box(box_sample()).with_prop(Padding::new().with_all(size))), ); } @@ -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/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 7991078d..54528768 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -125,16 +125,33 @@ impl Extension for Bootsier { #[async_trait] impl Theme for Bootsier { + #[rustfmt::skip] + fn breakpoint_entry(&self, bp: Breakpoint) -> BreakpointEntry { + use Breakpoint::*; + + // Anchos mínimos de la rejilla de Bootstrap (`$grid-breakpoints`). Bootstrap no tiene un + // punto de corte por encima de `xxl`, así que `Xxxl` se resuelve como `xxl`. + match bp { + Xs => BreakpointEntry { breakpoint: Xs, name: "xs", min_width: "" }, + Sm => BreakpointEntry { breakpoint: Sm, name: "sm", min_width: "576px" }, + Md => BreakpointEntry { breakpoint: Md, name: "md", min_width: "768px" }, + Lg => BreakpointEntry { breakpoint: Lg, name: "lg", min_width: "992px" }, + Xl => BreakpointEntry { breakpoint: Xl, name: "xl", min_width: "1200px" }, + Xxl => BreakpointEntry { breakpoint: Xxl, name: "xxl", min_width: "1400px" }, + Xxxl => BreakpointEntry { breakpoint: Xxxl, name: "xxl", min_width: "1400px" }, + } + } + fn intent_color(&self, intent: Intent) -> &'static str { theme::BootsierColors::from(intent).as_str() } - fn setup_component(&self, component: &mut dyn Component, _cx: &mut Context) { + fn setup_component(&self, component: &mut dyn Component, cx: &mut Context) { setup_component!(component, { Badge => |c| theme::bs::badge::setup(c), Brand => |c| theme::bs::brand::setup(c), Button => |c| theme::bs::button::setup(c), - Container => |c| theme::bs::container::setup(c), + Container => |c| theme::bs::container::setup(c, cx), Dialog => |c| theme::bs::dialog::setup(c), Dropdown => |c| theme::bs::dropdown::setup(c), Image => |c| theme::bs::image::setup(c), diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index 18b15f8a..f093ac42 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -11,13 +11,10 @@ //! //! # Plantillas //! -//! Bootsier maqueta las dos plantillas de PageTop -//! ([`CoreTemplates`](pagetop::prelude::CoreTemplates)): `Standard`, con -//! cabecera, contenido y pie, que es la plantilla por defecto de cualquier página; y `Admin`, con -//! la shell completa de AdminLTE 4 (barra superior + barra lateral + área de contenido), que se -//! activa creando la página con [`Page::admin()`](pagetop::response::Page::admin) en lugar de -//! [`Page::new()`](pagetop::response::Page::new). No define sus propias variantes de plantilla: -//! intercepta el componente `Template` en `render_component()` (ver `bs::layout`). +//! Bootsier maqueta las dos plantillas de PageTop ([`CoreTemplates`]): `Standard`, con cabecera, +//! contenido y pie, que es la plantilla por defecto de cualquier página; y `Admin`, con la shell +//! completa de administración (barra superior + barra lateral + área de contenido), que se activa +//! creando la página con [`Page::admin()`] en lugar de [`Page::new()`]. //! //! ```rust,no_run //! use pagetop::prelude::*; @@ -32,6 +29,10 @@ //! } //! ``` //! +//! [`CoreTemplates`]: pagetop::prelude::CoreTemplates +//! [`Page::admin()`]: pagetop::response::Page::admin +//! [`Page::new()`]: pagetop::response::Page::new +//! //! # Barra lateral //! //! Registra elementos en [`BootsierRegions::Sidebar`](bs::BootsierRegions::Sidebar) para poblar la @@ -88,6 +89,8 @@ //! })); //! ``` +use pagetop::prelude::*; + pub mod bs; pub mod class; @@ -113,3 +116,35 @@ pub use bs::form::textarea::TextareaBootsier; pub use bs::nav::NavBootsier; #[doc(hidden)] pub use bs::navbar::NavbarBootsier; + +// Añade la clase de un punto de corte con un prefijo y un sufijo (opcional) a la cadena de clases, +// separada con un espacio de las que ya hubiera: `prefix-name-suffix`, sin `-suffix` si no hay +// sufijo, y sin `-name` si el punto de corte aplica siempre. +// +// `name` es el nombre del punto de corte según `Theme::breakpoint_entry()` en el tema activo de +// `cx`. Si el tema no le asocia un ancho mínimo (`Breakpoint::resolved()` devuelve `None`), el +// punto de corte aplica siempre y se omite `name`, igual que Bootstrap con `container` frente a +// `container-lg`, o `container-fluid` frente a `container-lg-fluid`. +pub(crate) fn push_breakpoint_class( + cx: &Context, + bp: Breakpoint, + classes: &mut String, + prefix: &str, + suffix: &str, +) { + if prefix.is_empty() { + return; + } + if !classes.is_empty() { + classes.push(' '); + } + classes.push_str(prefix); + if let Some(entry) = bp.resolved(cx) { + classes.push('-'); + classes.push_str(entry.name); + } + if !suffix.is_empty() { + classes.push('-'); + classes.push_str(suffix); + } +} 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/container.rs b/extensions/pagetop-bootsier/src/theme/bs/container.rs index 67526ab8..2bb08e79 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/container.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/container.rs @@ -26,7 +26,7 @@ const EXTRA_WIDTH: &str = "bootsier.container.width"; /// /// let main = bs::Container::main() /// .with_id("main-page") -/// .with_width(bs::container::Width::From(BreakPoint::LG)) +/// .with_width(bs::container::Width::From(Breakpoint::Lg)) /// .with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Light))) /// .with_prop(PropsOp::add_classes(class::Text::with(BootsierColors::Dark))) /// .with_prop(PropsOp::add_classes(class::Border::with(ScaleSize::One))) @@ -36,9 +36,9 @@ const EXTRA_WIDTH: &str = "bootsier.container.width"; pub trait ContainerBootsier { /// Establece el comportamiento del ancho para el contenedor. /// - /// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de - /// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo - /// explícito. Ver [`Width`] para las variantes disponibles. + /// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de corte, + /// o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo explícito. + /// Ver [`Width`] para las variantes disponibles. fn with_width(self, width: Width) -> Self; } @@ -55,13 +55,13 @@ impl ContainerBootsier for Container { /// Define cómo se comporta el ancho de un contenedor ([`Container`]). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum Width { - /// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de - /// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible. + /// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de corte. + /// Por debajo del menor punto de corte ocupa el 100% del ancho disponible. #[default] Default, - /// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de - /// ese punto de ruptura ocupa el 100% del ancho disponible. - From(BreakPoint), + /// Aplica los anchos máximos predefinidos a partir del punto de corte indicado. Por debajo de + /// ese punto de corte ocupa el 100% del ancho disponible. + From(Breakpoint), /// Ocupa el 100% del ancho disponible siempre. Fluid, /// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito. @@ -72,30 +72,34 @@ impl Width { const CONTAINER: &str = "container"; /// Añade la clase asociada al ancho del contenedor a la cadena de clases. + /// + /// El nombre del punto de corte se resuelve en el tema activo de `cx`. #[inline] - pub fn push_to(self, classes: &mut String) { + pub fn push_to(self, cx: &Context, classes: &mut String) { match self { - Self::Default => BreakPoint::None.push_to(classes, Self::CONTAINER, ""), - Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""), + Self::Default => { + push_breakpoint_class(cx, Breakpoint::Xs, classes, Self::CONTAINER, "") + } + Self::From(bp) => push_breakpoint_class(cx, bp, classes, Self::CONTAINER, ""), Self::Fluid | Self::FluidMax(_) => { - BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid") + push_breakpoint_class(cx, Breakpoint::Xs, classes, Self::CONTAINER, "fluid") } } } /// Devuelve la clase asociada al ancho del contenedor. - pub fn to_class(self) -> String { + pub fn to_class(self, cx: &Context) -> String { let mut class = String::new(); - self.push_to(&mut class); + self.push_to(cx, &mut class); class } } // **< Container SETUP >**************************************************************************** -pub(crate) fn setup(container: &mut Container) { +pub(crate) fn setup(container: &mut Container, cx: &Context) { let width = container.props().extra_or(EXTRA_WIDTH, Width::default()); - container.alter_prop(PropsOp::prepend_classes(width.to_class())); + container.alter_prop(PropsOp::prepend_classes(width.to_class(cx))); if let Width::FluidMax(w) = width && w.is_measurable() { diff --git a/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs b/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs index c071f3fb..829f0f24 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs @@ -168,7 +168,7 @@ pub(crate) async fn render( let auto_close = auto_close.opt_str(); let menu_classes = { let mut classes = "dropdown-menu".to_string(); - menu_align.push_to(&mut classes); + menu_align.push_to(cx, &mut classes); classes }; diff --git a/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs b/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs index d6f76709..c07fcceb 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs @@ -112,65 +112,69 @@ impl Direction { /// Alineación horizontal del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown). /// /// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una -/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]). +/// alineación diferente a partir de un punto de corte ([`Breakpoint`]). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum MenuAlign { /// Alineación al inicio (comportamiento por defecto). #[default] Start, - /// Alineación al inicio a partir del punto de ruptura indicado. - StartAt(BreakPoint), - /// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido. - StartAndEnd(BreakPoint), + /// Alineación al inicio a partir del punto de corte indicado. + StartAt(Breakpoint), + /// Alineación al inicio por defecto, y al final a partir de un punto de corte válido. + StartAndEnd(Breakpoint), /// Alineación al final. End, - /// Alineación al final a partir del punto de ruptura indicado. - EndAt(BreakPoint), - /// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido. - EndAndStart(BreakPoint), + /// Alineación al final a partir del punto de corte indicado. + EndAt(Breakpoint), + /// Alineación al final por defecto, y al inicio a partir de un punto de corte válido. + EndAndStart(Breakpoint), } impl MenuAlign { /// Añade las clases de alineación a la cadena de clases (sin incluir la base `dropdown-menu`). + /// + /// El nombre del punto de corte se resuelve en el tema activo de `cx`. #[inline] - pub fn push_to(self, classes: &mut String) { + pub fn push_to(self, cx: &Context, classes: &mut String) { + const MENU: &str = "dropdown-menu"; + match self { // Alineación por defecto (start), no añade clases extra. Self::Start => {} // `dropdown-menu-{bp}-start` Self::StartAt(bp) => { - bp.push_to(classes, "dropdown-menu", "start"); + push_breakpoint_class(cx, bp, classes, MENU, "start"); } // `dropdown-menu-start` + `dropdown-menu-{bp}-end` Self::StartAndEnd(bp) => { - BreakPoint::None.push_to(classes, "dropdown-menu", "start"); - bp.push_to(classes, "dropdown-menu", "end"); + push_breakpoint_class(cx, Breakpoint::Xs, classes, MENU, "start"); + push_breakpoint_class(cx, bp, classes, MENU, "end"); } // `dropdown-menu-end` Self::End => { - BreakPoint::None.push_to(classes, "dropdown-menu", "end"); + push_breakpoint_class(cx, Breakpoint::Xs, classes, MENU, "end"); } // `dropdown-menu-{bp}-end` Self::EndAt(bp) => { - bp.push_to(classes, "dropdown-menu", "end"); + push_breakpoint_class(cx, bp, classes, MENU, "end"); } // `dropdown-menu-end` + `dropdown-menu-{bp}-start` Self::EndAndStart(bp) => { - BreakPoint::None.push_to(classes, "dropdown-menu", "end"); - bp.push_to(classes, "dropdown-menu", "start"); + push_breakpoint_class(cx, Breakpoint::Xs, classes, MENU, "end"); + push_breakpoint_class(cx, bp, classes, MENU, "start"); } } } /// Devuelve las clases de alineación del menú (sin incluir la base `dropdown-menu`). - pub fn to_class(self) -> String { + pub fn to_class(self, cx: &Context) -> String { let mut classes = String::new(); - self.push_to(&mut classes); + self.push_to(cx, &mut classes); classes } } 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-bootsier/src/theme/bs/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs index ab1d8a33..48059ec7 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs @@ -8,16 +8,18 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; const EXTRA_LAYOUT: &str = "bootsier.navbar.layout"; const EXTRA_POSITION: &str = "bootsier.navbar.position"; -const EXTRA_EXPAND: &str = "bootsier.navbar.expand"; -/// Extensión de Bootsier para [`Navbar`](crate::theme::bs::Navbar). +/// Extensión de Bootsier para [`Navbar`]. /// /// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con -/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por -/// [`navbar::Layout`](crate::theme::bs::navbar::Layout). También puede fijarse en la parte superior -/// o inferior del documento mediante [`navbar::Position`](crate::theme::bs::navbar::Position), y -/// definir a partir de qué punto de ruptura deja de colapsar con -/// [`with_expand()`](Self::with_expand). +/// botón de despliegue o dentro de un [`Offcanvas`]), controladas por [`navbar::Layout`]. También +/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`]. El +/// punto de corte a partir del cual deja de colapsar se define con [`Navbar::with_expand()`]. +/// +/// [`Navbar`]: crate::theme::bs::Navbar +/// [`Offcanvas`]: crate::theme::bs::Offcanvas +/// [`navbar::Layout`]: crate::theme::bs::navbar::Layout +/// [`navbar::Position`]: crate::theme::bs::navbar::Position /// /// # Ejemplos /// @@ -42,7 +44,6 @@ const EXTRA_EXPAND: &str = "bootsier.navbar.expand"; /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let navbar = bs::Navbar::simple_toggle() -/// .with_expand(BreakPoint::MD) /// .with_item(bs::navbar::Item::nav( /// bs::Nav::new() /// .with_item(bs::nav::Item::link(Lc::n("Home"), "/")) @@ -88,7 +89,7 @@ const EXTRA_EXPAND: &str = "bootsier.navbar.expand"; /// .with_route(Route::from("/")); /// /// let navbar = bs::Navbar::brand_right(brand) -/// .with_expand(BreakPoint::LG) +/// .with_expand(Breakpoint::Lg) /// .with_item(bs::navbar::Item::nav( /// bs::Nav::new() /// .with_kind(bs::nav::Kind::Pills) @@ -151,9 +152,6 @@ pub trait NavbarBootsier { /// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**. fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self; - /// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar. - fn with_expand(self, bp: BreakPoint) -> Self; - /// Define dónde se mostrará la barra de navegación dentro del documento. fn with_position(self, position: bs::navbar::Position) -> Self; } @@ -187,11 +185,6 @@ impl NavbarBootsier for Navbar { navbar } - fn with_expand(mut self, bp: BreakPoint) -> Self { - self.alter_prop(PropsOp::set_extra(EXTRA_EXPAND, bp)); - self - } - fn with_position(mut self, position: bs::navbar::Position) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position)); self @@ -201,14 +194,10 @@ impl NavbarBootsier for Navbar { // **< Navbar SETUP >******************************************************************************* pub(crate) fn setup(navbar: &mut Navbar) { - // Sin `with_expand()`, colapsa por debajo de 768px, igual que el tema Basic (que no tiene - // punto de ruptura configurable y siempre usa ese umbral, ver `static/css/basic.css`). - let expand = navbar.props().extra_or(EXTRA_EXPAND, BreakPoint::MD); let position = navbar .props() .extra_or(EXTRA_POSITION, bs::navbar::Position::default()); let mut classes = String::new(); - expand.push_to(&mut classes, "navbar-expand", ""); position.push_to(&mut classes); if !classes.is_empty() { navbar.alter_prop(PropsOp::add_classes(classes)); diff --git a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs index e9eed78e..68709e80 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs @@ -14,7 +14,7 @@ use crate::theme::*; /// - Se puede configurar el borde de la ventana desde el que se desliza el panel /// ([`with_placement()`](Self::with_placement)). /// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado. -/// - Puede cambiar su comportamiento a partir de un punto de ruptura +/// - Puede cambiar su comportamiento a partir de un punto de corte /// ([`with_breakpoint()`](Self::with_breakpoint)). /// - Asocia título y controles de accesibilidad a un identificador único y expone atributos /// adecuados para lectores de pantalla y navegación por teclado. @@ -47,8 +47,8 @@ pub struct Offcanvas { props: Props, /// Devuelve el título del panel. title: Lc, - /// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel. - breakpoint: BreakPoint, + /// Devuelve el punto de corte configurado para cambiar el comportamiento del panel. + breakpoint: Breakpoint, /// Devuelve el comportamiento configurado para la capa de fondo. backdrop: bs::offcanvas::Backdrop, /// Indica si la página principal puede desplazarse mientras el panel está abierto. @@ -77,8 +77,8 @@ impl Component for Offcanvas { // Clases CSS por defecto para el panel. self.alter_prop(PropsOp::prepend_classes({ - let mut classes = "offcanvas".to_string(); - self.breakpoint().push_to(&mut classes, "offcanvas", ""); + let mut classes = String::new(); + push_breakpoint_class(cx, *self.breakpoint(), &mut classes, "offcanvas", ""); self.placement().push_to(&mut classes); self.visibility().push_to(&mut classes); classes @@ -112,16 +112,16 @@ impl Offcanvas { self } - /// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel. + /// Establece el punto de corte a partir del cual cambia el comportamiento del panel. /// /// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante /// ([`Offcanvas`]). /// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página. /// - /// Por ejemplo, con `BreakPoint::Lg`, será *offcanvas* en móviles y tabletas, y visible - /// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea + /// Por ejemplo, con `Breakpoint::Lg`, será *offcanvas* en móviles y tabletas, y visible + /// directamente en pantallas grandes. Por defecto usa `Breakpoint::Xs` para que sea /// *offcanvas* siempre. - pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self { + pub fn with_breakpoint(mut self, bp: Breakpoint) -> Self { self.breakpoint = bp; self } diff --git a/extensions/pagetop-bootsier/src/theme/class.rs b/extensions/pagetop-bootsier/src/theme/class.rs index ee51497b..8ce0a0c8 100644 --- a/extensions/pagetop-bootsier/src/theme/class.rs +++ b/extensions/pagetop-bootsier/src/theme/class.rs @@ -1,7 +1,7 @@ //! Define clases para aplicar en componentes del tema. //! -//! Incluyen puntos de ruptura, colores y niveles de opacidad, escalas de tamaño y lados, necesarios -//! para crear determinadas clases del tema: +//! Incluyen colores y niveles de opacidad, escalas de tamaño y lados, necesarios para crear +//! determinadas clases del tema: //! //! ```rust,no_run //! use pagetop_bootsier::theme::*; diff --git a/extensions/pagetop-bootsier/src/theme/token.rs b/extensions/pagetop-bootsier/src/theme/token.rs index edd0da0a..cf763304 100644 --- a/extensions/pagetop-bootsier/src/theme/token.rs +++ b/extensions/pagetop-bootsier/src/theme/token.rs @@ -1,8 +1,5 @@ //! Tipos enumerados para construir clases del tema. -mod breakpoint; -pub use breakpoint::BreakPoint; - mod color; pub use color::{BootsierColors, OpacityLevel}; diff --git a/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs b/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs deleted file mode 100644 index 31310760..00000000 --- a/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs +++ /dev/null @@ -1,61 +0,0 @@ -use pagetop::prelude::*; - -/// Puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum BreakPoint { - /// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical. - #[default] - None, - /// **576px o más**. Dispositivos pequeños: teléfonos en modo horizontal. - SM, - /// **768px o más**. Dispositivos medianos: tabletas. - MD, - /// **992px o más**. Dispositivos grandes: puestos de escritorio. - LG, - /// **1200px o más**. Dispositivos muy grandes: puestos de escritorio grandes. - XL, - /// **1400px o más**. Dispositivos extragrandes: puestos de escritorio más grandes. - XXL, -} - -impl BreakPoint { - /// Devuelve la identificación del punto de ruptura (`"sm"`, `"md"`, etc.), o `""` para `None`. - #[rustfmt::skip] - #[inline] - pub const fn as_str(self) -> &'static str { - match self { - Self::None => "", - Self::SM => "sm", - Self::MD => "md", - Self::LG => "lg", - Self::XL => "xl", - Self::XXL => "xxl", - } - } - - // Añade el punto de ruptura con un prefijo y un sufijo (opcional) a la cadena de clases. - // - // - Para `None`: `prefix` o `prefix-suffix` (si `suffix` no está vacío). - // - Para `SM..XXL`: `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`. - #[inline] - pub(crate) fn push_to(self, classes: &mut String, prefix: &str, suffix: &str) { - if prefix.is_empty() { - return; - } - if !classes.is_empty() { - classes.push(' '); - } - match self { - Self::None => classes.push_str(prefix), - _ => { - classes.push_str(prefix); - classes.push('-'); - classes.push_str(self.as_str()); - } - } - if !suffix.is_empty() { - classes.push('-'); - classes.push_str(suffix); - } - } -} diff --git a/extensions/pagetop-bootsier/tests/breakpoint_class.rs b/extensions/pagetop-bootsier/tests/breakpoint_class.rs new file mode 100644 index 00000000..659bc926 --- /dev/null +++ b/extensions/pagetop-bootsier/tests/breakpoint_class.rs @@ -0,0 +1,86 @@ +// Verifies that Bootsier builds its breakpoint-based Bootstrap classes from the breakpoint names +// of `Bootsier::breakpoint_entry()`, and that `Xxxl` (absent in Bootstrap) resolves as `xxl`. + +use pagetop::prelude::*; +use pagetop_bootsier::theme::*; + +fn cx() -> Context { + Context::default().with_theme(&pagetop_bootsier::Bootsier) +} + +#[pagetop::test] +async fn container_width_uses_breakpoint_name() { + let cx = cx(); + assert_eq!(bs::container::Width::Default.to_class(&cx), "container"); + assert_eq!(bs::container::Width::Fluid.to_class(&cx), "container-fluid"); + assert_eq!( + bs::container::Width::From(Breakpoint::Lg).to_class(&cx), + "container-lg" + ); +} + +#[pagetop::test] +async fn container_width_xxxl_resolves_as_xxl() { + let cx = cx(); + assert_eq!( + bs::container::Width::From(Breakpoint::Xxxl).to_class(&cx), + "container-xxl" + ); +} + +#[pagetop::test] +async fn dropdown_menu_align_combines_breakpoint_classes() { + let cx = cx(); + assert_eq!(bs::dropdown::MenuAlign::Start.to_class(&cx), ""); + assert_eq!( + bs::dropdown::MenuAlign::EndAt(Breakpoint::Md).to_class(&cx), + "dropdown-menu-md-end" + ); + assert_eq!( + bs::dropdown::MenuAlign::StartAndEnd(Breakpoint::Xl).to_class(&cx), + "dropdown-menu-start dropdown-menu-xl-end" + ); +} + +#[pagetop::test] +async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() { + let item = + || bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/"))); + + let mut navbar = bs::Navbar::simple().with_item(item()); + let html = navbar.render(&mut cx()).await.into_string(); + assert!(html.contains("navbar-expand-md")); + // The expansion class comes from the `Navbar` component only, so it is not duplicated. + assert_eq!(html.matches("navbar-expand").count(), 1); + + let mut navbar = bs::Navbar::simple() + .with_expand(Breakpoint::Xxxl) + .with_item(item()); + let html = navbar.render(&mut cx()).await.into_string(); + assert!(html.contains("navbar-expand-xxl")); + + // Bootstrap needs a plain `navbar-expand` to keep the bar always expanded. + let mut navbar = bs::Navbar::simple() + .with_expand(Breakpoint::Xs) + .with_item(item()); + let html = navbar.render(&mut cx()).await.into_string(); + assert!(html.contains("navbar-expand")); + assert!(!html.contains("navbar-expand-")); +} + +#[pagetop::test] +async fn offcanvas_uses_either_the_plain_or_the_breakpoint_class() { + // Bootstrap's `.offcanvas` is always fixed, so it must not accompany `.offcanvas-{bp}`. + let panel = + |offcanvas: bs::Offcanvas| offcanvas.with_child(Html::with(|_| html! { p { "Content" } })); + + let mut plain = panel(bs::Offcanvas::new()); + let html = plain.render(&mut cx()).await.into_string(); + assert!(html.contains(r#"class="offcanvas "#)); + assert!(!html.contains("offcanvas-lg")); + + let mut responsive = panel(bs::Offcanvas::new().with_breakpoint(Breakpoint::Lg)); + let html = responsive.render(&mut cx()).await.into_string(); + assert!(html.contains(r#"class="offcanvas-lg "#)); + assert!(!html.contains(r#"class="offcanvas "#)); +} 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..7731f8bb 100644 --- a/src/base/component/navbar/component.rs +++ b/src/base/component/navbar/component.rs @@ -3,7 +3,8 @@ use crate::prelude::*; /// Componente para crear una **barra de navegación**. /// /// Permite mostrar enlaces, menús desplegables ([`nav::Item::dropdown()`]) y una marca de -/// identidad, en distintas disposiciones controladas por [`navbar::Layout`]. +/// identidad, en distintas disposiciones controladas por [`navbar::Layout`]. El punto de corte a +/// partir del cual deja de colapsar se define con [`with_expand()`](Self::with_expand). /// /// Si no contiene elementos, el componente **no se renderiza**. /// @@ -23,11 +24,13 @@ use crate::prelude::*; /// )); /// ``` /// -/// Barra **colapsable**, con botón de despliegue: +/// Barra **colapsable**, con botón de despliegue, que muestra su contenido en línea a partir de un +/// punto de corte (por defecto, [`Breakpoint::Md`]): /// /// ```rust,no_run /// # use pagetop::prelude::*; /// let navbar = Navbar::simple_toggle() +/// .with_expand(Breakpoint::Lg) /// .with_item(navbar::Item::nav( /// Nav::new() /// .with_item(nav::Item::link(Lc::n("Home"), "/")) @@ -58,7 +61,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 +84,10 @@ 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. + /// Devuelve el punto de corte a partir del cual la barra deja de colapsar. + #[default(Breakpoint::Md)] #[getters(copy)] - flex: Flex, + expand: Breakpoint, /// Devuelve la lista de contenidos. items: Children, } @@ -99,9 +103,17 @@ 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))); + + let class: CowStr = match self.expand().resolved(cx) { + Some(entry) => util::join!("navbar-expand-", entry.name).into(), + None => "navbar-expand".into(), + }; + self.alter_prop(PropsOp::prepend_classes(class)); self.alter_prop(PropsOp::prepend_classes("navbar")); } @@ -124,12 +136,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 `