✨ (pagetop): Añade Grid y extrae Flex de Container
This commit is contained in:
parent
dfff12881f
commit
307ca6f62c
44 changed files with 2554 additions and 735 deletions
|
|
@ -140,12 +140,9 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
.with_child(form::Hidden::field("origin", "form-selections"))
|
||||
// Botonera de acciones.
|
||||
.with_child(
|
||||
Container::new()
|
||||
.with_flex(
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||
)
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||
.with_child(
|
||||
Button::submit(Lc::t("btn_submit", &LOC))
|
||||
.with_style(button::Style::Solid(Intent::Primary)),
|
||||
|
|
@ -260,12 +257,9 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
.with_child(form::Hidden::field("origin", "form-text"))
|
||||
// Botonera de acciones.
|
||||
.with_child(
|
||||
Container::new()
|
||||
.with_flex(
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||
)
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||
.with_child(
|
||||
Button::submit(Lc::t("btn_submit", &LOC))
|
||||
.with_style(button::Style::Solid(Intent::Primary)),
|
||||
|
|
@ -440,12 +434,9 @@ fn form_lists() -> Form {
|
|||
.with_child(form::Hidden::field("origin", "form-lists"))
|
||||
// Botonera de acciones.
|
||||
.with_child(
|
||||
Container::new()
|
||||
.with_flex(
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||
)
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||
.with_child(
|
||||
Button::submit(Lc::t("btn_submit", &LOC))
|
||||
.with_style(button::Style::Solid(Intent::Primary)),
|
||||
|
|
|
|||
|
|
@ -530,10 +530,8 @@ fn flex_item(n: impl Into<CowStr>) -> 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.
|
||||
|
|
|
|||
544
examples/intro-grid.rs
Normal file
544
examples/intro-grid.rs
Normal file
|
|
@ -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<ExtensionRef> {
|
||||
vec![&pagetop_bootsier::Bootsier]
|
||||
}
|
||||
|
||||
fn configure_router(&self, router: Router) -> Router {
|
||||
router.route("/", web::get(intro_grid))
|
||||
}
|
||||
}
|
||||
|
||||
async fn intro_grid(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
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<CowStr>) -> 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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
49
examples/locale/en-US/intro-grid.ftl
Normal file
49
examples/locale/en-US/intro-grid.ftl
Normal file
|
|
@ -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
|
||||
49
examples/locale/es-ES/intro-grid.ftl
Normal file
49
examples/locale/es-ES/intro-grid.ftl
Normal file
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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::<BootsierColors>(EXTRA_COLOR)
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ impl RegionName for BootsierRegions {
|
|||
// **< Region RENDER >******************************************************************************
|
||||
|
||||
// Regiones de Bootsier: se renderizan sin el `<div role="region">` 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(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ use crate::theme::{ContainerBootsier, bs};
|
|||
use crate::{ADMINLTE_VERSION, LOCALES_BOOTSIER};
|
||||
|
||||
// Regiones de Bootsier: se renderiza sin el `<div role="region">` 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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -47,9 +47,6 @@ pub struct Container {
|
|||
props: Props,
|
||||
/// Devuelve el tipo semántico del contenedor.
|
||||
kind: Kind,
|
||||
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
||||
#[getters(copy)]
|
||||
flex: Flex,
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
children: Children,
|
||||
}
|
||||
|
|
@ -70,7 +67,7 @@ impl Component for Container {
|
|||
if output.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
let container_props = self.props().unpack_with_flex(cx, self.flex());
|
||||
let container_props = self.props().unpack(cx);
|
||||
Ok(match self.kind() {
|
||||
Kind::Default => html! { div (container_props) { (output) } },
|
||||
Kind::Main => html! { main (container_props) { (output) } },
|
||||
|
|
@ -138,12 +135,6 @@ impl Container {
|
|||
self
|
||||
}
|
||||
|
||||
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
|
||||
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||
self.flex = self.flex.merge(flex);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
||||
/// una operación [`ChildOp`].
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de los
|
||||
/// temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
|
||||
/// 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 `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de
|
||||
/// los temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
|
||||
///
|
||||
/// `align`/`align_content`/`gap` usan tipos de [`pagetop::html::align`] ([`align::Items`],
|
||||
/// [`align::Content`], [`align::Gap`]), compartidos con [`Grid`], mismo CSS y mismo catálogo de
|
||||
/// valores en los dos. Ver la documentación de [`align`](crate::html::align) para el criterio
|
||||
/// completo.
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
/// [`pagetop::html::align`]: crate::html::align
|
||||
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||
/// [`flex::FlexItem`]: crate::html::flex::FlexItem
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let actions = 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<DisplayFlex>,
|
||||
/// 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<Direction>,
|
||||
/// 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<ContentJustify>,
|
||||
/// Devuelve la alineación de los elementos en el eje transversal, por punto de corte.
|
||||
#[getters(copy)]
|
||||
align: Responsive<Align>,
|
||||
align: Responsive<align::Items>,
|
||||
/// Devuelve la alineación de las líneas cuando hay más de una, por punto de corte.
|
||||
#[getters(copy)]
|
||||
align_content: Responsive<AlignContent>,
|
||||
align_content: Responsive<align::Content>,
|
||||
/// Devuelve el espaciado entre elementos, por punto de corte.
|
||||
#[getters(copy)]
|
||||
gap: Responsive<Gap>,
|
||||
gap: Responsive<align::Gap>,
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Flex {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let output = self.children().render(cx).await;
|
||||
if output.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
let mut classes = String::new();
|
||||
self.flex_classes(cx, &mut classes);
|
||||
let container_props = self.props().unpack_with_classes(cx, classes);
|
||||
Ok(html! { div (container_props) { (output) } })
|
||||
}
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl Flex {
|
||||
/// Define una configuración 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<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la dirección del eje principal.
|
||||
pub fn with_direction(mut self, dir: Direction) -> Self {
|
||||
self.direction = self.direction.set(dir);
|
||||
|
|
@ -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<ChildOp>) -> 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<Option<Flex>>) -> 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;
|
||||
};
|
||||
376
src/base/component/grid.rs
Normal file
376
src/base/component/grid.rs
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
use crate::html::align::{Content, Gap, Items};
|
||||
use crate::html::grid::{AutoFlow, AxisTrack, ContentJustify, DefaultJustify, Tracks};
|
||||
use crate::prelude::*;
|
||||
|
||||
// **< DisplayGrid >********************************************************************************
|
||||
|
||||
// Posicionamiento CSS Grid del contenedor `Grid`. La API pública sólo expone los constructores
|
||||
// `Grid::new()`, `Grid::at()`, `Grid::inline()` e `Grid::inline_at()`, nunca la variante en sí.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
enum DisplayGrid {
|
||||
#[default]
|
||||
Always,
|
||||
AlwaysInline,
|
||||
At(Breakpoint),
|
||||
InlineAt(Breakpoint),
|
||||
}
|
||||
|
||||
// **< Grid >***************************************************************************************
|
||||
|
||||
/// Componente que crea una **rejilla CSS Grid** para posicionar componentes.
|
||||
///
|
||||
/// Es el único componente de PageTop que ofrece posicionamiento CSS Grid (con el mismo criterio que
|
||||
/// [`Flex`] para Flexbox). Sus hijos pueden ser **cualquier componente**, no sólo otro `Grid`. Para
|
||||
/// colocarlos dentro de la rejilla (columna, fila, alineación individual) se usa [`grid::GridItem`]
|
||||
/// vía `with_prop()`, exactamente igual que en cualquier otro contenedor. `GridItem` es una
|
||||
/// característica de [`PropsOp`], no exige ningún envoltorio propio.
|
||||
///
|
||||
/// Se aplica con clases CSS generadas dinámicamente (`display`, `grid-template-columns`,
|
||||
/// `grid-template-rows`, `grid-auto-columns`, `grid-auto-rows`, `grid-auto-flow`, `justify-items`,
|
||||
/// `align-items`, `justify-content`, `align-content`, `gap`, `row-gap`, `column-gap`), registradas
|
||||
/// vía [`AssetsOp::add_responsive_style()`] en [`ResponsiveStyles`] y renderizadas como reglas en
|
||||
/// el `<head>` del documento. Son propiedades nativas que no requieren interpretación por parte de
|
||||
/// los temas, siempre funcionan igual, sin una sola línea de CSS ni de código específico.
|
||||
///
|
||||
/// `align_items`/`align_content`/`gap` usan tipos de [`pagetop::html::align`] ([`align::Items`],
|
||||
/// [`align::Content`], [`align::Gap`]), compartidos con [`Flex`], mismo CSS y mismo catálogo de
|
||||
/// valores en los dos. Ver la documentación de [`align`](crate::html::align) para el criterio
|
||||
/// completo.
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
/// [`pagetop::html::align`]: crate::html::align
|
||||
/// [`grid::GridItem`]: crate::html::grid::GridItem
|
||||
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let cards = Grid::new()
|
||||
/// .with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)))
|
||||
/// .with_gap(align::Gap::Both(UnitValue::RelRem(1.0)))
|
||||
/// .with_child(Container::new())
|
||||
/// .with_child(Container::new())
|
||||
/// .with_child(Container::new());
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Grid {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
// Determina si esta configuración debe aplicarse (y con qué variante de `display`) o si `Grid`
|
||||
// no está en absoluto configurado. `None` es el estado real de ausencia: lo que tiene un
|
||||
// componente que nunca ha llamado a ninguno de sus constructores explícitos. Sin getter
|
||||
// público; `new()`, `at()`, `inline()` e `inline_at()` son la única forma de activarlo.
|
||||
#[getters(skip)]
|
||||
display: Option<DisplayGrid>,
|
||||
/// Devuelve las pistas de columna (`grid-template-columns`), por punto de corte.
|
||||
#[getters(copy)]
|
||||
columns: Responsive<Tracks>,
|
||||
/// Devuelve las pistas de fila (`grid-template-rows`), por punto de corte.
|
||||
#[getters(copy)]
|
||||
rows: Responsive<Tracks>,
|
||||
/// Devuelve el tamaño de las columnas generadas implícitamente (`grid-auto-columns`), por
|
||||
/// punto de corte.
|
||||
#[getters(copy)]
|
||||
auto_columns: Responsive<AxisTrack>,
|
||||
/// Devuelve el tamaño de las filas generadas implícitamente (`grid-auto-rows`), por punto de
|
||||
/// corte.
|
||||
#[getters(copy)]
|
||||
auto_rows: Responsive<AxisTrack>,
|
||||
/// Devuelve el algoritmo de colocación automática, por punto de corte.
|
||||
#[getters(copy)]
|
||||
auto_flow: Responsive<AutoFlow>,
|
||||
/// Devuelve la alineación de los elementos en el eje de columnas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
justify_items: Responsive<DefaultJustify>,
|
||||
/// Devuelve la alineación de los elementos en el eje de filas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
align_items: Responsive<Items>,
|
||||
/// Devuelve la alineación de las pistas en el eje de columnas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
justify_content: Responsive<ContentJustify>,
|
||||
/// Devuelve la alineación de las pistas en el eje de filas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
align_content: Responsive<Content>,
|
||||
/// Devuelve el espaciado entre pistas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
gap: Responsive<Gap>,
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Grid {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let output = self.children().render(cx).await;
|
||||
if output.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
let mut classes = String::new();
|
||||
self.grid_classes(cx, &mut classes);
|
||||
let container_props = self.props().unpack_with_classes(cx, classes);
|
||||
Ok(html! { div (container_props) { (output) } })
|
||||
}
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl Grid {
|
||||
/// Define una configuración con `display: grid`, sin punto de corte: se aplica siempre.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
display: Some(DisplayGrid::Always),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Define una configuración con `display: grid` que se aplica a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn at(bp: Breakpoint) -> Self {
|
||||
Self {
|
||||
display: Some(DisplayGrid::At(bp)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Define una configuración con `display: inline-grid`, sin punto de corte: se aplica
|
||||
/// siempre.
|
||||
pub fn inline() -> Self {
|
||||
Self {
|
||||
display: Some(DisplayGrid::AlwaysInline),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Define una configuración con `display: inline-grid` que se aplica a partir del punto de
|
||||
/// corte indicado.
|
||||
pub fn inline_at(bp: Breakpoint) -> Self {
|
||||
Self {
|
||||
display: Some(DisplayGrid::InlineAt(bp)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Grid BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de columna.
|
||||
pub fn with_columns(mut self, tracks: Tracks) -> Self {
|
||||
self.columns = self.columns.set(tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de columna a partir del punto de corte indicado.
|
||||
pub fn with_columns_at(mut self, bp: Breakpoint, tracks: Tracks) -> Self {
|
||||
self.columns = self.columns.set_at(bp, tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de fila.
|
||||
pub fn with_rows(mut self, tracks: Tracks) -> Self {
|
||||
self.rows = self.rows.set(tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece las pistas de fila a partir del punto de corte indicado.
|
||||
pub fn with_rows_at(mut self, bp: Breakpoint, tracks: Tracks) -> Self {
|
||||
self.rows = self.rows.set_at(bp, tracks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño de las columnas generadas implícitamente por la colocación automática.
|
||||
pub fn with_auto_columns(mut self, track: AxisTrack) -> Self {
|
||||
self.auto_columns = self.auto_columns.set(track);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño de las columnas generadas implícitamente, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_auto_columns_at(mut self, bp: Breakpoint, track: AxisTrack) -> Self {
|
||||
self.auto_columns = self.auto_columns.set_at(bp, track);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño de las filas generadas implícitamente por la colocación automática.
|
||||
pub fn with_auto_rows(mut self, track: AxisTrack) -> Self {
|
||||
self.auto_rows = self.auto_rows.set(track);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño de las filas generadas implícitamente, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_auto_rows_at(mut self, bp: Breakpoint, track: AxisTrack) -> Self {
|
||||
self.auto_rows = self.auto_rows.set_at(bp, track);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el algoritmo de colocación automática.
|
||||
pub fn with_auto_flow(mut self, auto_flow: AutoFlow) -> Self {
|
||||
self.auto_flow = self.auto_flow.set(auto_flow);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el algoritmo de colocación automática, a partir del punto de corte indicado.
|
||||
pub fn with_auto_flow_at(mut self, bp: Breakpoint, auto_flow: AutoFlow) -> Self {
|
||||
self.auto_flow = self.auto_flow.set_at(bp, auto_flow);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de los elementos en el eje de columnas.
|
||||
pub fn with_justify_items(mut self, justify_items: DefaultJustify) -> Self {
|
||||
self.justify_items = self.justify_items.set(justify_items);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de los elementos en el eje de columnas, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_justify_items_at(mut self, bp: Breakpoint, justify_items: DefaultJustify) -> Self {
|
||||
self.justify_items = self.justify_items.set_at(bp, justify_items);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de los elementos en el eje de filas.
|
||||
pub fn with_align_items(mut self, align_items: Items) -> Self {
|
||||
self.align_items = self.align_items.set(align_items);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de los elementos en el eje de filas, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_align_items_at(mut self, bp: Breakpoint, align_items: Items) -> Self {
|
||||
self.align_items = self.align_items.set_at(bp, align_items);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de las pistas en el eje de columnas (ver [`ContentJustify`]).
|
||||
pub fn with_justify_content(mut self, justify_content: ContentJustify) -> Self {
|
||||
self.justify_content = self.justify_content.set(justify_content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de las pistas en el eje de columnas, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_justify_content_at(
|
||||
mut self,
|
||||
bp: Breakpoint,
|
||||
justify_content: ContentJustify,
|
||||
) -> Self {
|
||||
self.justify_content = self.justify_content.set_at(bp, justify_content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de las pistas en el eje de filas (ver
|
||||
/// [`Content`](crate::html::align::Content)).
|
||||
pub fn with_align_content(mut self, align_content: Content) -> Self {
|
||||
self.align_content = self.align_content.set(align_content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación de las pistas en el eje de filas, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_align_content_at(mut self, bp: Breakpoint, align_content: Content) -> Self {
|
||||
self.align_content = self.align_content.set_at(bp, align_content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el espaciado entre pistas.
|
||||
pub fn with_gap(mut self, gap: Gap) -> Self {
|
||||
self.gap = self.gap.set(gap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el espaciado entre pistas, a partir del punto de corte indicado.
|
||||
pub fn with_gap_at(mut self, bp: Breakpoint, gap: Gap) -> Self {
|
||||
self.gap = self.gap.set_at(bp, gap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
||||
/// una operación [`ChildOp`].
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
// Calcula las clases CSS *responsive* de este contenedor Grid y las añade a `classes`,
|
||||
// separadas con un espacio de las que ya hubiera, para poder compartir un único acumulador con
|
||||
// `GridItem::apply()` (ver `Props::unpack_with_classes()`) sin cadenas intermedias.
|
||||
#[rustfmt::skip]
|
||||
fn grid_classes(&self, cx: &mut Context, classes: &mut String) {
|
||||
// Sin `display` no hay contenedor Grid: el resto de propiedades (`grid-template-columns`,
|
||||
// `gap`, etc.) no tienen ningún efecto en CSS sin `display: grid`/`inline-grid`, así que ni
|
||||
// se generan. Éste es el estado de un `Grid` que nunca ha llamado a ninguno de sus
|
||||
// constructores explícitos.
|
||||
let Some(display) = self.display else {
|
||||
return;
|
||||
};
|
||||
|
||||
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
|
||||
|
||||
let (prefix, value) = match display {
|
||||
DisplayGrid::Always
|
||||
| DisplayGrid::At(_) => ("_grid_", "grid"),
|
||||
DisplayGrid::AlwaysInline
|
||||
| DisplayGrid::InlineAt(_) => ("_inline-grid_", "inline-grid"),
|
||||
};
|
||||
let entry = match display {
|
||||
DisplayGrid::At(bp) | DisplayGrid::InlineAt(bp) => bp.resolved(cx),
|
||||
_ => None,
|
||||
};
|
||||
let class = match entry {
|
||||
None => prefix.into(),
|
||||
Some(entry) => util::join!(prefix, entry.name, "_").into(),
|
||||
};
|
||||
styles(cx, classes, entry, class, "display", value.into());
|
||||
|
||||
apply!(cx, classes, self.columns, "_grid-columns_", "grid-template-columns", val);
|
||||
apply!(cx, classes, self.rows, "_grid-rows_", "grid-template-rows", val);
|
||||
apply!(cx, classes, self.auto_columns, "_grid-auto-columns_", "grid-auto-columns", val);
|
||||
apply!(cx, classes, self.auto_rows, "_grid-auto-rows_", "grid-auto-rows", val);
|
||||
// `val`: a diferencia de `flex::Behavior` (una sola palabra en cualquier variante),
|
||||
// `AutoFlow::RowDense`/`ColumnDense` valen "row dense"/"column dense" (con espacio), así
|
||||
// que necesitan pasar por `value_to_token()` igual que `Tracks`/`ItemPlacement`.
|
||||
apply!(cx, classes, self.auto_flow, "_grid-auto-flow_", "grid-auto-flow", val);
|
||||
apply!(cx, classes, self.justify_items, "_grid-justify-items_", "justify-items");
|
||||
apply!(cx, classes, self.align_items, "_grid-align-items_", "align-items");
|
||||
apply!(cx, classes, self.justify_content, "_grid-justify-content_", "justify-content");
|
||||
apply!(cx, classes, self.align_content, "_grid-align-content_", "align-content");
|
||||
for (bp, gap) in self.gap.by_breakpoint() {
|
||||
let entry = bp.resolved(cx);
|
||||
for (property, value) in gap.styles().into_iter().flatten() {
|
||||
// El prefijo de `gap` no es literal (depende de la propiedad), así que se compone
|
||||
// aquí en un único `join!` en vez de pasar por `responsive_class!`.
|
||||
let token = value_to_token(&value);
|
||||
let class = match entry {
|
||||
None => util::join!("_grid-", property, "_", token, "_"),
|
||||
Some(e) => util::join!("_grid-", property, "_", token, "_", e.name, "_"),
|
||||
};
|
||||
styles(cx, classes, entry, class.into(), property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ use crate::prelude::*;
|
|||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **botón de despliegue** y **marca de identidad**, en ese orden:
|
||||
/// Barra con **botón de despliegue**, contenido y **marca de identidad**:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -81,9 +81,6 @@ pub struct Navbar {
|
|||
props: Props,
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
layout: navbar::Layout,
|
||||
/// Devuelve el posicionamiento Flexbox como contenedor, si tiene alguno.
|
||||
#[getters(copy)]
|
||||
flex: Flex,
|
||||
/// Devuelve la lista de contenidos.
|
||||
items: Children,
|
||||
}
|
||||
|
|
@ -99,8 +96,10 @@ impl Component for Navbar {
|
|||
}
|
||||
|
||||
fn setup(&mut self, cx: &mut Context) {
|
||||
// Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de
|
||||
// despliegue para referenciar el contenido colapsable con `aria-controls`.
|
||||
// Asegura que la barra de navegación tiene un identificador único: siempre se usa para
|
||||
// derivar el `id` del área de contenido (`id_content`, ver `prepare()`); además, el botón
|
||||
// de despliegue lo necesita para referenciar ese contenido colapsable con `aria-controls`
|
||||
// si el *layout* lo incluye.
|
||||
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
|
||||
self.alter_prop(PropsOp::prepend_classes("navbar"));
|
||||
}
|
||||
|
|
@ -124,12 +123,9 @@ impl Component for Navbar {
|
|||
aria-expanded="false"
|
||||
aria-controls=(&id_content)
|
||||
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
|
||||
{
|
||||
span class="navbar-toggle-icon" {}
|
||||
}
|
||||
{}
|
||||
};
|
||||
|
||||
// Posicionamiento Flexbox opcional (no del `<nav>`, cuya estructura la fija `layout()`).
|
||||
let content_props = Props::classes("navbar-content").with_id(id_content);
|
||||
|
||||
Ok(html! {
|
||||
|
|
@ -137,32 +133,32 @@ impl Component for Navbar {
|
|||
@match self.layout() {
|
||||
// Barra más sencilla: sólo contenido, siempre visible.
|
||||
navbar::Layout::Simple => {
|
||||
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||
div (content_props.unpack(cx)) { (items) }
|
||||
},
|
||||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
navbar::Layout::SimpleToggle => {
|
||||
(button)
|
||||
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||
div (content_props.unpack(cx)) { (items) }
|
||||
},
|
||||
|
||||
// Barra con marca, siempre visible, sin botón.
|
||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
||||
(brand.render(cx).await)
|
||||
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||
div (content_props.unpack(cx)) { (items) }
|
||||
},
|
||||
|
||||
// Barra con marca y botón, en ese orden.
|
||||
navbar::Layout::BrandLeft(brand) => {
|
||||
(brand.render(cx).await)
|
||||
(button)
|
||||
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||
div (content_props.unpack(cx)) { (items) }
|
||||
},
|
||||
|
||||
// Barra con botón y marca, en ese orden.
|
||||
// Barra con botón, contenido y marca (en ese orden).
|
||||
navbar::Layout::BrandRight(brand) => {
|
||||
(button)
|
||||
div (content_props.unpack_with_flex(cx, self.flex())) { (items) }
|
||||
div (content_props.unpack(cx)) { (items) }
|
||||
(brand.render(cx).await)
|
||||
},
|
||||
}
|
||||
|
|
@ -194,8 +190,10 @@ impl Navbar {
|
|||
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **botón de despliegue** y **marca de identidad**, en ese
|
||||
/// orden.
|
||||
/// Crea una barra de navegación con **botón de despliegue**, el contenido y, en último lugar,
|
||||
/// la **marca de identidad**. A diferencia de [`brand_left()`](Self::brand_left), aquí no queda
|
||||
/// junto al botón, sino que se renderiza tras el contenido, para acabar en el extremo derecho
|
||||
/// de la barra (el botón sólo es visible en viewports estrechos).
|
||||
pub fn brand_right(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
|
||||
}
|
||||
|
|
@ -220,15 +218,6 @@ impl Navbar {
|
|||
self
|
||||
}
|
||||
|
||||
/// Establece el posicionamiento Flexbox como contenedor (usa `None` para quitarlo).
|
||||
///
|
||||
/// No afecta a la posición de la marca ni del botón de despliegue, que quedan fijados con
|
||||
/// [`with_layout()`](Self::with_layout).
|
||||
pub fn with_flex(mut self, flex: impl Into<Option<Flex>>) -> Self {
|
||||
self.flex = self.flex.merge(flex);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||
/// barra con una operación [`TypedOp`].
|
||||
///
|
||||
|
|
|
|||
|
|
@ -451,9 +451,9 @@ impl Pager {
|
|||
// páginas ya cabe sin necesidad de truncar (ver más abajo), devuelve todas las páginas sin
|
||||
// elipsis. En caso contrario, siempre incluye la primera y la última página, la ventana de
|
||||
// páginas alrededor de la actual (`window` antes y después, recortada a los límites del
|
||||
// listado), y sustituye por una elipsis cualquier tramo oculto de dos o más páginas -- un
|
||||
// tramo de una sola página se muestra directamente, ya que ocultarla tras una elipsis no
|
||||
// ahorra espacio.
|
||||
// listado), y sustituye por una elipsis cualquier tramo oculto de dos o más páginas. Un tramo
|
||||
// de una sola página se muestra directamente, ya que ocultarla tras una elipsis no ahorra
|
||||
// espacio.
|
||||
fn page_items(&self, page: u64, total_pages: u64) -> Vec<PageItem> {
|
||||
// Acotado a `total_pages`: una ventana mayor no aporta nada (ya se mostrarían todas las
|
||||
// páginas) y evita operar con un valor arbitrariamente grande más abajo.
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ use std::sync::Arc;
|
|||
/// let route = Route::with(move |cx| cx.route(format!("/users/{user_id}")));
|
||||
/// ```
|
||||
///
|
||||
/// Si ya tienes un [`RoutePath`] resuelto -- por ejemplo, combinando
|
||||
/// [`Context::route()`] con [`Waypoint::append_to()`](crate::response::Waypoint::append_to) --
|
||||
/// se convierte directamente, sin volver a procesarlo:
|
||||
/// Si ya tienes un [`RoutePath`] resuelto (por ejemplo, combinando [`Context::route()`] con
|
||||
/// [`Waypoint::append_to()`](crate::response::Waypoint::append_to)), se convierte directamente, sin
|
||||
/// volver a procesarlo:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -178,7 +178,7 @@ impl Default for Route {
|
|||
///
|
||||
/// Ojo: esta conversión asume que el `RoutePath` ya pasó por `Context::route()`. Si construyes uno
|
||||
/// a mano (por ejemplo con `RoutePath::new(...)` directamente, sin pasar por `cx.route()`), esta
|
||||
/// conversión no añadirá `lang` por ti -- simplemente envuelve el valor tal cual, igual que
|
||||
/// conversión no añadirá `lang` por ti; simplemente envuelve el valor tal cual, igual que
|
||||
/// [`Route::external()`].
|
||||
impl From<RoutePath> for Route {
|
||||
fn from(path: RoutePath) -> Self {
|
||||
|
|
|
|||
|
|
@ -126,12 +126,15 @@ pub struct BreakpointEntry {
|
|||
/// responsabilidad de quien consuma [`by_breakpoint()`], normalmente para acabar registrado en
|
||||
/// [`ResponsiveStyles`].
|
||||
///
|
||||
/// Uso típico: los campos de [`Flex`]/[`FlexItem`] para el posicionamiento Flexbox de componentes.
|
||||
/// Se hace un uso extensivo de esta estructura en los campos de [`Flex`]/[`FlexItem`] y
|
||||
/// [`Grid`]/[`GridItem`] para el posicionamiento Flexbox y CSS Grid de componentes.
|
||||
///
|
||||
/// [`by_breakpoint()`]: Self::by_breakpoint
|
||||
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||
/// [`Flex`]: crate::html::Flex
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`FlexItem`]: crate::html::FlexItem
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`GridItem`]: crate::html::GridItem
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Responsive<T> {
|
||||
values: [Option<T>; 7],
|
||||
|
|
|
|||
|
|
@ -38,14 +38,18 @@ pub use unit::UnitValue;
|
|||
// **< HTML LAYOUT >********************************************************************************
|
||||
|
||||
// Mecanismo interno compartido por `flex` y `grid` para resolver clases CSS nativas *responsive*.
|
||||
mod responsive;
|
||||
pub(crate) mod responsive;
|
||||
|
||||
// Vocabulario de alineación compartido por `flex` y `grid`.
|
||||
pub mod align;
|
||||
|
||||
pub mod flex;
|
||||
#[doc(inline)]
|
||||
pub use flex::{Flex, FlexItem};
|
||||
pub use flex::FlexItem;
|
||||
|
||||
pub mod grid;
|
||||
#[doc(inline)]
|
||||
pub use grid::GridItem;
|
||||
|
||||
pub mod spacing;
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Definiciones de alineación compartidas por [`Flex`] y [`Grid`].
|
||||
//! Definiciones de alineación compartidas por los contenedores [`Flex`] y [`Grid`].
|
||||
//!
|
||||
//! Reúne las definiciones CSS que Flexbox y CSS Grid resuelven de forma idéntica, con el mismo
|
||||
//! nombre de propiedad y mismo catálogo de valores. La propia especificación CSS no los considera
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
//! comparte y qué no).
|
||||
//!
|
||||
//! [CSS Box Alignment Module]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_alignment
|
||||
//! [`Flex`]: crate::html::Flex
|
||||
//! [`Grid`]: crate::html::Grid
|
||||
//! [`Flex`]: crate::base::component::Flex
|
||||
//! [`Grid`]: crate::base::component::Grid
|
||||
//! [`align::Items`]: crate::html::align::Items
|
||||
//! [`align::Content`]: crate::html::align::Content
|
||||
//! [`align::ItemSelf`]: crate::html::align::ItemSelf
|
||||
|
|
@ -39,14 +39,14 @@ use crate::{AutoDefault, CowStr};
|
|||
|
||||
/// Alinea los elementos en un contenedor [`Flex`] (eje transversal) o [`Grid`] (eje de filas).
|
||||
///
|
||||
/// [`Flex`]: crate::html::Flex
|
||||
/// [`Grid`]: crate::html::Grid
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Items {
|
||||
/// Por defecto (`align-items: normal` no explícito), mismo efecto que [`Items::Stretch`], salvo
|
||||
/// que el elemento tenga su propio tamaño.
|
||||
#[default]
|
||||
Default,
|
||||
Normal,
|
||||
/// Alinea los elementos al inicio del eje (`align-items: flex-start`).
|
||||
Start,
|
||||
/// Alinea los elementos al final del eje (`align-items: flex-end`).
|
||||
|
|
@ -63,7 +63,7 @@ impl Items {
|
|||
// Devuelve el valor CSS de `align-items`, o "" para el valor por defecto.
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Normal => "".into(),
|
||||
Self::Start => "flex-start".into(),
|
||||
Self::End => "flex-end".into(),
|
||||
Self::Center => "center".into(),
|
||||
|
|
@ -84,8 +84,8 @@ impl Items {
|
|||
/// la suma de las pistas de fila sea menor que la altura del contenedor, sin depender de ningún
|
||||
/// ajuste de línea.
|
||||
///
|
||||
/// [`Flex`]: crate::html::Flex
|
||||
/// [`Grid`]: crate::html::Grid
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`Behavior::Wrap`]: crate::html::flex::Behavior::Wrap
|
||||
/// [`Behavior::WrapReverse`]: crate::html::flex::Behavior::WrapReverse
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
|
|
@ -94,7 +94,7 @@ pub enum Content {
|
|||
/// o pistas se estiran para ocupar el espacio sobrante, sin efecto visible si no hay ningún
|
||||
/// espacio sobrante que repartir (p. ej. una altura `auto` ajustada al contenido).
|
||||
#[default]
|
||||
Default,
|
||||
Normal,
|
||||
/// Alinea al inicio del eje (`align-content: flex-start`).
|
||||
Start,
|
||||
/// Alinea al final del eje (`align-content: flex-end`).
|
||||
|
|
@ -116,7 +116,7 @@ impl Content {
|
|||
// Devuelve el valor CSS de `align-content`, o "" para el valor por defecto.
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Normal => "".into(),
|
||||
Self::Start => "flex-start".into(),
|
||||
Self::End => "flex-end".into(),
|
||||
Self::Center => "center".into(),
|
||||
|
|
@ -139,8 +139,8 @@ impl Content {
|
|||
/// Esa combinación no aplica a Grid, donde el reparto de espacio entre pistas no tiene el mismo
|
||||
/// problema.
|
||||
///
|
||||
/// [`Flex`]: crate::html::Flex
|
||||
/// [`Grid`]: crate::html::Grid
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`ItemSize`]: crate::html::flex::ItemSize
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Gap {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ impl ResponsiveStyles {
|
|||
/// Si ya existe una declaración para la misma propiedad, en el mismo punto de corte y con las
|
||||
/// mismas clases, la llamada no hace nada: se conserva el valor ya almacenado, no se sustituye.
|
||||
/// Pensado para clases utilitarias generadas automáticamente, donde el mismo nombre de clase
|
||||
/// implica siempre el mismo valor -- declararla de nuevo es entonces una operación de sólo
|
||||
/// implica siempre el mismo valor. Declararla de nuevo es entonces una operación de sólo
|
||||
/// lectura, sin normalizar `value`, en vez de una escritura.
|
||||
///
|
||||
/// Si `classes` contiene caracteres no ASCII, o si `classes`, `property` o `value` quedan
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
//! Definiciones para el posicionamiento de componentes con [Flexbox].
|
||||
//!
|
||||
//! [`Flex`] configura un contenedor y sus hijos como un grupo sobre el que se aplican propiedades
|
||||
//! de presentación (dirección, ajuste de línea, alineación, espaciado). Lo usan componentes que
|
||||
//! ofrecen su propio `with_flex()`, como [`Container`] o [`Navbar`].
|
||||
//! Por un lado está [`Flex`], el **componente contenedor** disponible en
|
||||
//! [`pagetop::base::component`] que permite estructurar un conjunto de componentes hijo como un
|
||||
//! grupo Flexbox sobre el que se aplican propiedades de presentación (dirección, ajuste de línea,
|
||||
//! alineación, espaciado), con sus propios constructores (`new()`, `at()`, `inline()`,
|
||||
//! `inline_at()`) y builders (`with_direction()`, `with_gap()`, etc.).
|
||||
//!
|
||||
//! [`FlexItem`] configura, en cambio, un único elemento en relación con el contenedor flex de su
|
||||
//! padre (crecimiento, reducción, alineación individual, orden, ancho y desplazamiento). No tiene
|
||||
//! un builder propio ya que puede acabar aplicándose sobre cualquier componente (no sólo los que
|
||||
//! ofrecen `with_flex()`). Por eso se aplica pasándolo directamente al `with_prop()` que
|
||||
//! normalmente ya expone cualquier componente, gracias a su `From` hacia [`PropsOp`].
|
||||
//! Por otro, aquí se define [`FlexItem`], que es la **configuración** que se aplica sobre un único
|
||||
//! elemento respecto a su contenedor flex padre (crecimiento, reducción, alineación individual,
|
||||
//! orden, ancho y desplazamiento). No tiene un builder propio ya que podría usarse sobre cualquier
|
||||
//! componente. Por eso se aplica pasándolo directamente al `with_prop()` que normalmente ya expone
|
||||
//! cualquier componente, gracias a su implementación `From` hacia [`PropsOp`].
|
||||
//!
|
||||
//! # Un entorno nativo autosuficiente
|
||||
//!
|
||||
|
|
@ -20,20 +22,30 @@
|
|||
//! documento. Funciona igual conviva con quien conviva en la misma página, sin necesidad de
|
||||
//! coordinar nombres de clase ni orden alguno en la carga de hojas de estilo.
|
||||
//!
|
||||
//! # Alineación y espaciado: ver [`align`]
|
||||
//!
|
||||
//! `align`/`align_content`/`gap` en [`Flex`] y `align_self` en [`FlexItem`] no tienen tipos propios
|
||||
//! de este módulo. Usan [`align::Items`], [`align::Content`], [`align::Gap`] y [`align::ItemSelf`],
|
||||
//! tipos declarados en [`pagetop::html::align`] con las propiedades que Flexbox y CSS Grid
|
||||
//! resuelven de manera idéntica.
|
||||
//!
|
||||
//! [Flexbox]: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Flexible_box_layout
|
||||
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
//! [`pagetop::html::align`]: crate::html::align
|
||||
//! [`align`]: crate::html::align
|
||||
//! [`align::Items`]: crate::html::align::Items
|
||||
//! [`align::Content`]: crate::html::align::Content
|
||||
//! [`align::Gap`]: crate::html::align::Gap
|
||||
//! [`align::ItemSelf`]: crate::html::align::ItemSelf
|
||||
//! [`PropsOp`]: crate::html::props::PropsOp
|
||||
//! [`Container`]: crate::base::component::Container
|
||||
//! [`Navbar`]: crate::base::component::Navbar
|
||||
//! [`pagetop::base::component`]: crate::base::component
|
||||
//! [`Flex`]: crate::base::component::Flex
|
||||
|
||||
mod props_container;
|
||||
pub use props_container::{Align, AlignContent, Behavior, ContentJustify, Direction, Gap};
|
||||
pub use props_container::{Behavior, ContentJustify, Direction};
|
||||
|
||||
mod props_item;
|
||||
pub use props_item::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
|
||||
|
||||
mod container;
|
||||
pub use container::Flex;
|
||||
pub use props_item::{ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
|
||||
|
||||
mod item;
|
||||
pub use item::FlexItem;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,36 @@
|
|||
use crate::core::component::Context;
|
||||
use crate::core::theme::{Breakpoint, Responsive};
|
||||
use crate::html::PropsOp;
|
||||
use crate::html::flex::{ItemAlign, ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
|
||||
use crate::html::align;
|
||||
use crate::html::flex::{ItemGrow, ItemOffset, ItemOrder, ItemShrink, ItemSize};
|
||||
use crate::{AutoDefault, Getters, builder_impl, util};
|
||||
|
||||
/// Configuración de un elemento como ítem de un contenedor Flexbox.
|
||||
///
|
||||
/// A diferencia de [`Flex`](crate::html::flex::Flex), que configura el comportamiento Flexbox
|
||||
/// global de un contenedor y sus hijos como grupo, `FlexItem` configura un único elemento en
|
||||
/// relación con el contenedor flex padre: crecimiento ([`ItemGrow`]), reducción ([`ItemShrink`]),
|
||||
/// alineación individual ([`ItemAlign`]), orden visual ([`ItemOrder`]), tamaño ([`ItemSize`]) y
|
||||
/// desplazamiento ([`ItemOffset`]).
|
||||
/// A diferencia del componente [`Flex`], que define un contenedor que aplica Flexbox para el
|
||||
/// posicionamiento de sus componentes hijo, `FlexItem` se aplica sobre un único elemento en
|
||||
/// relación con el contenedor flex padre. Usa el método `with_prop()` que suele exponer cualquier
|
||||
/// componente, y acepta `FlexItem` directamente gracias a su `From` hacia [`PropsOp`].
|
||||
///
|
||||
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
|
||||
/// componente puede acabar siendo hijo de un contenedor flex, y ninguno debería necesitar un campo
|
||||
/// propio para esto. Se aplica sobre el `with_prop()` que suele exponer cualquier componente, que
|
||||
/// acepta `FlexItem` directamente gracias a su `From` hacia [`PropsOp`].
|
||||
/// propio para esto.
|
||||
///
|
||||
/// `FlexItem` actúa sobre propiedades del elemento para configurar su crecimiento ([`ItemGrow`]),
|
||||
/// reducción ([`ItemShrink`]), alineación individual ([`align::ItemSelf`]), orden visual
|
||||
/// ([`ItemOrder`]), tamaño ([`ItemSize`]) y desplazamiento ([`ItemOffset`]).
|
||||
///
|
||||
/// Un hijo de `Flex` sin ningún `FlexItem` aplicado participa igualmente como ítem flex, sólo que
|
||||
/// sin crecimiento, reducción, alineación individual, orden, tamaño ni desplazamiento propios
|
||||
/// (todos sus valores por defecto).
|
||||
///
|
||||
/// Con [`ItemSize`] y [`ItemOffset`] se pueden modelar rejillas de columnas fijas sobre Flexbox,
|
||||
/// combinando un tamaño en fracción del contenedor con un desplazamiento lateral cuando se
|
||||
/// necesite.
|
||||
///
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`align::ItemSelf`]: crate::html::align::ItemSelf
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
|
|
@ -50,7 +60,7 @@ pub struct FlexItem {
|
|||
shrink: Responsive<ItemShrink>,
|
||||
/// Devuelve la alineación individual en el eje transversal, por punto de corte.
|
||||
#[getters(copy)]
|
||||
align_self: Responsive<ItemAlign>,
|
||||
align_self: Responsive<align::ItemSelf>,
|
||||
/// Devuelve la posición en el orden visual, por punto de corte.
|
||||
#[getters(copy)]
|
||||
order: Responsive<ItemOrder>,
|
||||
|
|
@ -124,14 +134,14 @@ impl FlexItem {
|
|||
}
|
||||
|
||||
/// Establece la alineación individual en el eje transversal.
|
||||
pub fn with_align_self(mut self, align_self: ItemAlign) -> Self {
|
||||
pub fn with_align_self(mut self, align_self: align::ItemSelf) -> Self {
|
||||
self.align_self = self.align_self.set(align_self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación individual en el eje transversal, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: ItemAlign) -> Self {
|
||||
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: align::ItemSelf) -> Self {
|
||||
self.align_self = self.align_self.set_at(bp, align_self);
|
||||
self
|
||||
}
|
||||
|
|
@ -152,7 +162,7 @@ impl FlexItem {
|
|||
/// [`ItemShrink::Is0`](super::ItemShrink::Is0) por sí solo (consulta la documentación de
|
||||
/// [`ItemSize`] antes de combinarlo con [`with_shrink()`](Self::with_shrink) porque con un
|
||||
/// tamaño en porcentaje, forzar `ItemShrink::Is0` sólo es seguro si el contenedor no tiene
|
||||
/// [`Gap`](super::Gap)).
|
||||
/// [`align::Gap`](crate::html::align::Gap)).
|
||||
pub fn with_size(mut self, size: ItemSize) -> Self {
|
||||
self.size = self.size.set(size);
|
||||
self
|
||||
|
|
@ -189,10 +199,12 @@ impl FlexItem {
|
|||
/// que sólo establezca `with_size_at(Breakpoint::Lg, ...)` no borra el `with_size()` base que
|
||||
/// `self` ya tuviera, sólo sustituye la entrada de ese punto de corte.
|
||||
///
|
||||
/// Es el método que usa [`Props::with_prop()`](crate::html::props::Props::with_prop) para que
|
||||
/// sucesivas [`PropsOp::FlexItem`](crate::html::props::PropsOp::FlexItem) sobre el mismo
|
||||
/// componente vayan completando campos concretos sin repetir los ya establecidos, en vez de
|
||||
/// partir de cero en cada llamada.
|
||||
/// Es el método que usa [`Props::with_prop()`] para que sucesivas [`PropsOp::FlexItem`] sobre
|
||||
/// el mismo componente vayan completando campos concretos sin repetir los ya establecidos, en
|
||||
/// vez de partir de cero en cada llamada.
|
||||
///
|
||||
/// [`Props::with_prop()`]: crate::html::props::Props::with_prop
|
||||
/// [`PropsOp::FlexItem`]: crate::html::props::PropsOp::FlexItem
|
||||
pub fn merge(mut self, item: FlexItem) -> Self {
|
||||
self.grow = self.grow.merge(item.grow);
|
||||
self.shrink = self.shrink.merge(item.shrink);
|
||||
|
|
@ -204,14 +216,13 @@ impl FlexItem {
|
|||
}
|
||||
|
||||
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
|
||||
/// [`Flex::apply()`](super::Flex::apply): cada faceta con valor añade una declaración de
|
||||
/// estilo (por punto de corte, si se ha establecido alguno) y su propia clase. Un campo sin
|
||||
/// ningún valor establecido, o con un valor cuya variante es la "por defecto" del propio enum
|
||||
/// (p. ej. `ItemGrow::Default`), no añade nada.
|
||||
/// hace internamente [`Flex`](crate::base::component::Flex): cada propiedad con valor añade una
|
||||
/// declaración de estilo (por punto de corte, si se ha establecido alguno) y su propia clase.
|
||||
/// Un campo sin ningún valor establecido, o con un valor cuya variante es la "por defecto" del
|
||||
/// propio enum (p. ej. `ItemGrow::Default`), no añade nada.
|
||||
///
|
||||
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
|
||||
/// para poder compartir un único acumulador con [`Flex::apply()`](super::Flex::apply) sin
|
||||
/// cadenas intermedias.
|
||||
/// para poder compartir un único acumulador con las de `Flex` sin cadenas intermedias.
|
||||
#[rustfmt::skip]
|
||||
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
|
||||
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
|
||||
|
|
|
|||
|
|
@ -1,98 +1,17 @@
|
|||
//! Enums semánticos que configuran [`Flex`](super::Flex), a nivel de contenedor.
|
||||
|
||||
use crate::html::unit::UnitValue;
|
||||
use crate::{AutoDefault, CowStr};
|
||||
|
||||
// **< Align >**************************************************************************************
|
||||
|
||||
/// Alineación de los elementos en el eje transversal de un contenedor [`Flex`](super::Flex).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Align {
|
||||
/// Por defecto (`align-items: normal` no explícito), mismo efecto que [`Align::Stretch`], salvo
|
||||
/// que el elemento tenga su propio tamaño.
|
||||
#[default]
|
||||
Default,
|
||||
/// Alinea los elementos al inicio del eje transversal (`align-items: flex-start`).
|
||||
Start,
|
||||
/// Alinea los elementos al final del eje transversal (`align-items: flex-end`).
|
||||
End,
|
||||
/// Centra los elementos en el eje transversal (`align-items: center`).
|
||||
Center,
|
||||
/// Alinea los elementos por su línea base de texto (`align-items: baseline`).
|
||||
Baseline,
|
||||
/// Estira los elementos para ocupar todo el eje transversal (`align-items: stretch`).
|
||||
Stretch,
|
||||
}
|
||||
|
||||
impl Align {
|
||||
// Devuelve el valor CSS de `align-items`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Start => "flex-start".into(),
|
||||
Self::End => "flex-end".into(),
|
||||
Self::Center => "center".into(),
|
||||
Self::Baseline => "baseline".into(),
|
||||
Self::Stretch => "stretch".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< AlignContent >*******************************************************************************
|
||||
|
||||
/// Alineación de varias líneas en un contenedor [`Flex`](super::Flex).
|
||||
///
|
||||
/// Sólo tiene efecto si el contenedor usa [`Behavior::Wrap`] o [`Behavior::WrapReverse`] y genera
|
||||
/// más de una línea de elementos.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AlignContent {
|
||||
/// Por defecto (`align-content: normal` no explícito), como en [`AlignContent::Stretch`], las
|
||||
/// líneas se estiran para ocupar el espacio sobrante del eje transversal, sin efecto visible si
|
||||
/// el contenedor no tiene ningún espacio sobrante que repartir (p. ej. una altura `auto`
|
||||
/// ajustada al contenido).
|
||||
#[default]
|
||||
Default,
|
||||
/// Alinea las líneas al inicio del eje transversal (`align-content: flex-start`).
|
||||
Start,
|
||||
/// Alinea las líneas al final del eje transversal (`align-content: flex-end`).
|
||||
End,
|
||||
/// Centra las líneas en el eje transversal (`align-content: center`).
|
||||
Center,
|
||||
/// Reparte el espacio sobrante entre las líneas (`align-content: space-between`).
|
||||
SpaceBetween,
|
||||
/// Reparte el espacio sobrante alrededor de cada línea (`align-content: space-around`).
|
||||
SpaceAround,
|
||||
/// Reparte el espacio sobrante en partes iguales, incluidos los extremos
|
||||
/// (`align-content: space-evenly`).
|
||||
SpaceEvenly,
|
||||
/// Estira las líneas para ocupar todo el eje transversal (`align-content: stretch`).
|
||||
Stretch,
|
||||
}
|
||||
|
||||
impl AlignContent {
|
||||
// Devuelve el valor CSS de `align-content`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Start => "flex-start".into(),
|
||||
Self::End => "flex-end".into(),
|
||||
Self::Center => "center".into(),
|
||||
Self::SpaceBetween => "space-between".into(),
|
||||
Self::SpaceAround => "space-around".into(),
|
||||
Self::SpaceEvenly => "space-evenly".into(),
|
||||
Self::Stretch => "stretch".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Behavior >***********************************************************************************
|
||||
|
||||
/// Comportamiento de los elementos si no caben en una línea del contenedor [`Flex`](super::Flex).
|
||||
/// Comportamiento de los elementos si no caben en una línea del contenedor [`Flex`].
|
||||
///
|
||||
/// Si el contenedor aplica [`Gap`] y un [`ItemSize`](super::ItemSize) porcentual en los hijos,
|
||||
/// entonces usar [`Behavior::Wrap`] en vez de [`Behavior::NoWrap`] (su valor por defecto) puede
|
||||
/// provocar saltos de línea prematuros. En la sección "Cómo combinarlo con `Gap`" de `ItemSize`
|
||||
/// se explica el porqué.
|
||||
/// Si el contenedor aplica [`align::Gap`] y un [`ItemSize`] porcentual en los hijos, entonces usar
|
||||
/// [`Behavior::Wrap`] en vez de [`Behavior::NoWrap`] (su valor por defecto) puede provocar saltos
|
||||
/// de línea prematuros. En la sección "Cómo combinarlo con `Gap`" de `ItemSize` se explica el
|
||||
/// porqué.
|
||||
///
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`align::Gap`]: crate::html::align::Gap
|
||||
/// [`ItemSize`]: crate::html::flex::ItemSize
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Behavior {
|
||||
/// Por defecto, no se dividen en varias líneas: se comprimen o desbordan (`flex-wrap: nowrap`
|
||||
|
|
@ -108,7 +27,7 @@ pub enum Behavior {
|
|||
|
||||
impl Behavior {
|
||||
// Devuelve el valor CSS de `flex-wrap`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::NoWrap => "".into(),
|
||||
Self::Wrap => "wrap".into(),
|
||||
|
|
@ -119,13 +38,14 @@ impl Behavior {
|
|||
|
||||
// **< ContentJustify >*****************************************************************************
|
||||
|
||||
/// Alineación de los elementos en el eje principal de un contenedor [`Flex`](super::Flex).
|
||||
/// Alineación de los elementos en el eje principal de un contenedor
|
||||
/// [`Flex`](crate::base::component::Flex).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ContentJustify {
|
||||
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-content: normal` no
|
||||
/// explícito).
|
||||
#[default]
|
||||
Default,
|
||||
Normal,
|
||||
/// Alinea los elementos al inicio del eje principal (`justify-content: flex-start`).
|
||||
Start,
|
||||
/// Alinea los elementos al final del eje principal (`justify-content: flex-end`).
|
||||
|
|
@ -143,9 +63,9 @@ pub enum ContentJustify {
|
|||
|
||||
impl ContentJustify {
|
||||
// Devuelve el valor CSS de `justify-content`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Normal => "".into(),
|
||||
Self::Start => "flex-start".into(),
|
||||
Self::End => "flex-end".into(),
|
||||
Self::Center => "center".into(),
|
||||
|
|
@ -158,7 +78,7 @@ impl ContentJustify {
|
|||
|
||||
// **< Direction >**********************************************************************************
|
||||
|
||||
/// Dirección del eje principal de un contenedor [`Flex`](super::Flex).
|
||||
/// Dirección del eje principal de un contenedor [`Flex`](crate::base::component::Flex).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Direction {
|
||||
/// Por defecto, los elementos se disponen en fila, de izquierda a derecha
|
||||
|
|
@ -175,7 +95,7 @@ pub enum Direction {
|
|||
|
||||
impl Direction {
|
||||
// Devuelve el valor CSS de `flex-direction`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Row => "".into(),
|
||||
Self::RowReverse => "row-reverse".into(),
|
||||
|
|
@ -184,44 +104,3 @@ impl Direction {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Gap >****************************************************************************************
|
||||
|
||||
/// Espaciado entre los elementos de un contenedor [`Flex`](super::Flex).
|
||||
///
|
||||
/// Es un valor continuo, no una utilidad predefinida: se resuelve siempre como estilo
|
||||
/// `gap`/`row-gap`/`column-gap` en línea, igual que el resto de facetas de
|
||||
/// [`Flex`](super::Flex)/[`FlexItem`](super::FlexItem).
|
||||
///
|
||||
/// Si se combina con un [`ItemSize`](super::ItemSize) porcentual sobre los hijos, la sección "Cómo
|
||||
/// combinarlo con `Gap`" de `ItemSize` explica cómo evitar que el hueco desborde el contenedor.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Gap {
|
||||
/// Por defecto, no hay espaciado (`gap: normal` no explícito).
|
||||
#[default]
|
||||
None,
|
||||
/// Mismo espaciado entre filas y columnas.
|
||||
Both(UnitValue),
|
||||
/// Espaciado distinto entre filas y columnas.
|
||||
Distinct { row: UnitValue, column: UnitValue },
|
||||
}
|
||||
|
||||
impl Gap {
|
||||
// Declaraciones de estilo (propiedad, valor) para este espaciado; cada hueco a `None` si no hay
|
||||
// ninguna medible (`UnitValue::None`/`UnitValue::Auto` no producen ningún estilo).
|
||||
pub(super) fn styles(self) -> [Option<(&'static str, CowStr)>; 2] {
|
||||
match self {
|
||||
Self::None => [None, None],
|
||||
Self::Both(value) => [Self::style("gap", value), None],
|
||||
Self::Distinct { row, column } => [
|
||||
Self::style("row-gap", row),
|
||||
Self::style("column-gap", column),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Declaración (propiedad, valor) para un valor medible, o `None` si no lo es.
|
||||
fn style(property: &'static str, value: UnitValue) -> Option<(&'static str, CowStr)> {
|
||||
value.is_measurable().then(|| (property, value.into()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,6 @@
|
|||
//! Enums semánticos que configuran [`FlexItem`](super::FlexItem), a nivel de ítem.
|
||||
|
||||
use crate::html::unit::UnitValue;
|
||||
use crate::{AutoDefault, CowStr};
|
||||
|
||||
// **< ItemAlign >**********************************************************************************
|
||||
|
||||
/// Alineación en [`FlexItem`](super::FlexItem) para un ítem, sobrescribiendo la del contenedor.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ItemAlign {
|
||||
/// Por defecto, hereda la alineación del contenedor (`align-self: auto` no explícito).
|
||||
#[default]
|
||||
Default,
|
||||
/// Alinea el ítem al inicio del eje transversal (`align-self: flex-start`).
|
||||
Start,
|
||||
/// Alinea el ítem al final del eje transversal (`align-self: flex-end`).
|
||||
End,
|
||||
/// Centra el ítem en el eje transversal (`align-self: center`).
|
||||
Center,
|
||||
/// Alinea el ítem por su línea base de texto (`align-self: baseline`).
|
||||
Baseline,
|
||||
/// Estira el ítem para ocupar todo el eje transversal (`align-self: stretch`).
|
||||
Stretch,
|
||||
}
|
||||
|
||||
impl ItemAlign {
|
||||
// Devuelve el valor CSS de `align-self`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Start => "flex-start".into(),
|
||||
Self::End => "flex-end".into(),
|
||||
Self::Center => "center".into(),
|
||||
Self::Baseline => "baseline".into(),
|
||||
Self::Stretch => "stretch".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< ItemGrow >***********************************************************************************
|
||||
|
||||
/// Factor de crecimiento en [`FlexItem`](super::FlexItem) para un ítem dentro de un contenedor.
|
||||
|
|
@ -197,10 +161,12 @@ impl ItemOrder {
|
|||
/// elemento el que ceda (uno con [`ItemGrow::Is1`] y contenido que sí admita reajuste, como texto),
|
||||
/// no éste.
|
||||
///
|
||||
/// Con [`ItemSize`] en porcentaje, `Is0` es seguro si el contenedor no tiene [`Gap`](super::Gap)
|
||||
/// (sin `gap` no hay nada que compensar). Pero **si el contenedor tiene `Gap`, no combines `Is0`
|
||||
/// con un tamaño porcentual** porque desactivas la única pieza (el reparto del espacio negativo
|
||||
/// entre elementos) que compensa el hueco por ti. La explicación completa está en [`ItemSize`].
|
||||
/// Con [`ItemSize`] en porcentaje, `Is0` es seguro si el contenedor no tiene [`align::Gap`] (sin
|
||||
/// `gap` no hay nada que compensar). Pero **si el contenedor tiene `Gap`, no combines `Is0` con un
|
||||
/// tamaño porcentual** porque desactivas la única pieza (el reparto del espacio negativo entre
|
||||
/// elementos) que compensa el hueco por ti. La explicación completa está en [`ItemSize`].
|
||||
///
|
||||
/// [`align::Gap`]: crate::html::align::Gap
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ItemShrink {
|
||||
/// Por defecto, puede encoger si hace falta (`flex-shrink: 1` no explícito).
|
||||
|
|
@ -234,10 +200,10 @@ impl ItemShrink {
|
|||
/// # Cómo combinarlo con `Gap`
|
||||
///
|
||||
/// Un porcentaje se resuelve contra el ancho del contenedor sin contar el espacio que va a ocupar
|
||||
/// el [`Gap`](super::Gap). Es una limitación del propio CSS, porque `flex-basis` en porcentaje usa
|
||||
/// la misma regla de resolución que cualquier `width: %`. Si los porcentajes de una fila suman el
|
||||
/// 100% (una rejilla completa, el caso habitual), el hueco que añade `gap` sobra respecto al ancho
|
||||
/// del contenedor.
|
||||
/// el [`align::Gap`](crate::html::align::Gap). Es una limitación del propio CSS, porque
|
||||
/// `flex-basis` en porcentaje usa la misma regla de resolución que cualquier `width: %`. Si los
|
||||
/// porcentajes de una fila suman el 100% (una rejilla completa, el caso habitual), el hueco que
|
||||
/// añade `gap` sobra respecto al ancho del contenedor.
|
||||
///
|
||||
/// Ese sobrante se compensa solo, sin ningún ajuste manual, siempre que:
|
||||
///
|
||||
|
|
|
|||
54
src/html/grid.rs
Normal file
54
src/html/grid.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//! Definiciones para el posicionamiento de componentes con [CSS Grid].
|
||||
//!
|
||||
//! Al igual que ocurre con [`Flex`]/[`FlexItem`], por un lado está [`Grid`], el **componente
|
||||
//! contenedor** de [`pagetop::base::component`], que define una rejilla de filas y columnas sobre
|
||||
//! la que se aplican pistas, alineación y espaciado, con sus propios constructores (`new()`,
|
||||
//! `at()`, `inline()`, `inline_at()`) y builders (`with_columns()`, `with_gap()`, etc.).
|
||||
//!
|
||||
//! Y por otro, aquí se define [`GridItem`], que es la **configuración** que se aplica sobre un
|
||||
//! único elemento dentro de la rejilla de su padre (columna, fila, alineación individual). No tiene
|
||||
//! un builder propio ya que podría usarse sobre cualquier componente. Por eso se aplica pasándolo
|
||||
//! directamente al `with_prop()` que normalmente ya expone cualquier componente, gracias a su
|
||||
//! implementación `From` hacia [`PropsOp`].
|
||||
//!
|
||||
//! # Un entorno nativo autosuficiente
|
||||
//!
|
||||
//! `Grid`/`GridItem` reutilizan el mismo esquema que [`Flex`]/[`FlexItem`] para generar clases CSS
|
||||
//! dinámicamente, independientes de cualquier tema o framework CSS, registradas vía
|
||||
//! [`AssetsOp::add_responsive_style()`] y renderizadas en el `<head>` del documento. El nombre de
|
||||
//! cada clase se deriva de la propiedad y el valor que representa, así que dos elementos con la
|
||||
//! misma configuración comparten la misma regla generada en lugar de duplicarla.
|
||||
//!
|
||||
//! # Qué se comparte con Flex, y qué no
|
||||
//!
|
||||
//! Ambos sistemas comparten varias propiedades con el mismo catálogo de valores, `align-items`,
|
||||
//! `align-content`, `align-self`, `gap`, disponibles en [`pagetop::html::align`].
|
||||
//!
|
||||
//! No se comparte nada para `justify-items`/`justify-self` porque no hay equivalencia en Flexbox
|
||||
//! (sólo tiene un eje principal y uno transversal, intercambiables con [`Direction`], no dos ejes
|
||||
//! independientes como Grid) ni para `justify-content` ya que aunque el nombre de la propiedad
|
||||
//! coincide con el de Flex, Grid admite `stretch` (un valor sin efecto en Flexbox que
|
||||
//! [`flex::ContentJustify`] no modela), así que [`grid::ContentJustify`] es un catálogo propio de
|
||||
//! este módulo, no compartido.
|
||||
//!
|
||||
//! [CSS Grid]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout
|
||||
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
//! [`pagetop::html::align`]: crate::html::align
|
||||
//! [`FlexItem`]: crate::html::flex::FlexItem
|
||||
//! [`flex::ContentJustify`]: crate::html::flex::ContentJustify
|
||||
//! [`grid::ContentJustify`]: crate::html::grid::ContentJustify
|
||||
//! [`Direction`]: crate::html::flex::Direction
|
||||
//! [`PropsOp`]: crate::html::props::PropsOp
|
||||
//! [`pagetop::base::component`]: crate::base::component
|
||||
//! [`Flex`]: crate::base::component::Flex
|
||||
//! [`Grid`]: crate::base::component::Grid
|
||||
|
||||
mod props_container;
|
||||
pub use props_container::MAX_TRACKS;
|
||||
pub use props_container::{AutoFlow, AxisTrack, ContentJustify, DefaultJustify, Tracks};
|
||||
|
||||
mod props_item;
|
||||
pub use props_item::{ItemJustify, ItemPlacement};
|
||||
|
||||
mod item;
|
||||
pub use item::GridItem;
|
||||
166
src/html/grid/item.rs
Normal file
166
src/html/grid/item.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
use crate::core::component::Context;
|
||||
use crate::core::theme::{Breakpoint, Responsive};
|
||||
use crate::html::PropsOp;
|
||||
use crate::html::align;
|
||||
use crate::html::grid::{ItemJustify, ItemPlacement};
|
||||
use crate::{AutoDefault, Getters, builder_impl, util};
|
||||
|
||||
/// Configuración de un elemento como ítem de un contenedor CSS Grid.
|
||||
///
|
||||
/// A diferencia del componente [`Grid`], que define un contenedor que aplica CSS Grid para el
|
||||
/// posicionamiento de sus componentes hijo, `GridItem` se aplica sobre un único elemento en
|
||||
/// relación con la rejilla del contenedor padre. Usa el método `with_prop()` que suele exponer
|
||||
/// cualquier componente, y acepta `GridItem` directamente gracias a su `From` hacia [`PropsOp`].
|
||||
///
|
||||
/// No tiene un builder dedicado en ningún componente. De hecho, no tendría sentido porque cualquier
|
||||
/// componente puede acabar siendo hijo de un contenedor Grid, y ninguno debería necesitar un campo
|
||||
/// propio para esto.
|
||||
///
|
||||
/// `GridItem` actúa sobre propiedades del elemento para configurar su posición en columna y en
|
||||
/// fila ([`ItemPlacement`]), alineación individual en el eje de columnas ([`ItemJustify`]) y en el
|
||||
/// eje de filas ([`align::ItemSelf`]).
|
||||
///
|
||||
/// Un hijo de `Grid` sin ningún `GridItem` aplicado participa igualmente en la colocación
|
||||
/// automática, sólo que sin ninguna posición ni alineación propia.
|
||||
///
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`align::ItemSelf`]: crate::html::align::ItemSelf
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// // Ocupa las tres primeras columnas de la rejilla de su `Grid` padre.
|
||||
/// let banner = Container::new()
|
||||
/// .with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(3)));
|
||||
///
|
||||
/// // Empieza en la columna 2 y termina antes de la 4, alineado al final de su fila.
|
||||
/// let sidebar = Container::new().with_prop(
|
||||
/// GridItem::new()
|
||||
/// .with_column(grid::ItemPlacement::Range(2, 4))
|
||||
/// .with_align_self(align::ItemSelf::End),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
||||
pub struct GridItem {
|
||||
/// Devuelve la posición en columna, por punto de corte.
|
||||
#[getters(copy)]
|
||||
column: Responsive<ItemPlacement>,
|
||||
/// Devuelve la posición en fila, por punto de corte.
|
||||
#[getters(copy)]
|
||||
row: Responsive<ItemPlacement>,
|
||||
/// Devuelve la alineación individual en el eje de columnas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
justify_self: Responsive<ItemJustify>,
|
||||
/// Devuelve la alineación individual en el eje de filas, por punto de corte.
|
||||
#[getters(copy)]
|
||||
align_self: Responsive<align::ItemSelf>,
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl GridItem {
|
||||
/// Crea una configuración de ítem con todos los valores por defecto.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// **< GridItem BUILDER >***********************************************************************
|
||||
|
||||
/// Establece la posición en columna.
|
||||
pub fn with_column(mut self, placement: ItemPlacement) -> Self {
|
||||
self.column = self.column.set(placement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la posición en columna, a partir del punto de corte indicado.
|
||||
pub fn with_column_at(mut self, bp: Breakpoint, placement: ItemPlacement) -> Self {
|
||||
self.column = self.column.set_at(bp, placement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la posición en fila.
|
||||
pub fn with_row(mut self, placement: ItemPlacement) -> Self {
|
||||
self.row = self.row.set(placement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la posición en fila, a partir del punto de corte indicado.
|
||||
pub fn with_row_at(mut self, bp: Breakpoint, placement: ItemPlacement) -> Self {
|
||||
self.row = self.row.set_at(bp, placement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación individual en el eje de columnas.
|
||||
pub fn with_justify_self(mut self, justify_self: ItemJustify) -> Self {
|
||||
self.justify_self = self.justify_self.set(justify_self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación individual en el eje de columnas, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_justify_self_at(mut self, bp: Breakpoint, justify_self: ItemJustify) -> Self {
|
||||
self.justify_self = self.justify_self.set_at(bp, justify_self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación individual en el eje de filas.
|
||||
pub fn with_align_self(mut self, align_self: align::ItemSelf) -> Self {
|
||||
self.align_self = self.align_self.set(align_self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la alineación individual en el eje de filas, a partir del punto de corte
|
||||
/// indicado.
|
||||
pub fn with_align_self_at(mut self, bp: Breakpoint, align_self: align::ItemSelf) -> Self {
|
||||
self.align_self = self.align_self.set_at(bp, align_self);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl GridItem {
|
||||
/// Combina esta configuración con otra `GridItem`, campo a campo.
|
||||
///
|
||||
/// La fusión llega al nivel de cada punto de corte; donde `item` tenga un valor establecido,
|
||||
/// sustituye al de `self` y donde no lo tenga, se conserva el que ya hubiera. Por eso un `item`
|
||||
/// que sólo establezca `with_column_at(Breakpoint::Lg, ...)` no borra el `with_column()` base
|
||||
/// que `self` ya tuviera, sólo sustituye la entrada de ese punto de corte.
|
||||
///
|
||||
/// Es el método que usa [`Props::with_prop()`] para que sucesivas [`PropsOp::GridItem`] sobre
|
||||
/// el mismo componente vayan completando campos concretos sin repetir los ya establecidos, en
|
||||
/// vez de partir de cero en cada llamada.
|
||||
///
|
||||
/// [`Props::with_prop()`]: crate::html::props::Props::with_prop
|
||||
/// [`PropsOp::GridItem`]: crate::html::props::PropsOp::GridItem
|
||||
pub fn merge(mut self, item: GridItem) -> Self {
|
||||
self.column = self.column.merge(item.column);
|
||||
self.row = self.row.merge(item.row);
|
||||
self.justify_self = self.justify_self.merge(item.justify_self);
|
||||
self.align_self = self.align_self.merge(item.align_self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Aplica esta configuración como clases de utilidad responsive en el [`Context`], igual que
|
||||
/// hace internamente [`Grid`](crate::base::component::Grid): cada propiedad con valor añade una
|
||||
/// declaración de estilo (por punto de corte, si se ha establecido alguno) y su propia clase.
|
||||
/// Un campo sin ningún valor establecido, o con un valor cuya variante es la "por defecto" del
|
||||
/// propio enum (p. ej. `ItemJustify::Default`), no añade nada.
|
||||
///
|
||||
/// Las clases generadas se añaden a `classes`, separadas con un espacio de las que ya hubiera,
|
||||
/// para poder compartir un único acumulador con las de `Grid` sin cadenas intermedias.
|
||||
#[rustfmt::skip]
|
||||
pub(crate) fn apply(self, cx: &mut Context, classes: &mut String) {
|
||||
use crate::html::responsive::{apply, responsive_class, styles, value_to_token};
|
||||
|
||||
apply!(cx, classes, self.column, "_grid-item-column_", "grid-column", val);
|
||||
apply!(cx, classes, self.row, "_grid-item-row_", "grid-row", val);
|
||||
apply!(cx, classes, self.justify_self, "_grid-item-justify-self_", "justify-self");
|
||||
apply!(cx, classes, self.align_self, "_grid-item-align-self_", "align-self");
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GridItem> for PropsOp {
|
||||
fn from(item: GridItem) -> Self {
|
||||
Self::grid_item(item)
|
||||
}
|
||||
}
|
||||
263
src/html/grid/props_container.rs
Normal file
263
src/html/grid/props_container.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
use crate::html::unit::UnitValue;
|
||||
use crate::{AutoDefault, CowStr, util};
|
||||
|
||||
// **< AutoFlow >***********************************************************************************
|
||||
|
||||
/// Mecanismo de colocación automática de los elementos en un contenedor [`Grid`].
|
||||
///
|
||||
/// Aplica en aquellos elementos que no tienen una posición explícita ([`ItemPlacement`]) en el
|
||||
/// contenedor.
|
||||
///
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`ItemPlacement`]: super::ItemPlacement
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AutoFlow {
|
||||
/// Por defecto, coloca automáticamente rellenando filas (`grid-auto-flow: row` no explícito).
|
||||
#[default]
|
||||
Row,
|
||||
/// Coloca automáticamente rellenando columnas (`grid-auto-flow: column`).
|
||||
Column,
|
||||
/// Como `Row`, pero rellena huecos anteriores si un elemento más pequeño cabe en ellos
|
||||
/// (`grid-auto-flow: row dense`). Puede alterar el orden visual respecto al del documento con
|
||||
/// la misma cautela de accesibilidad que ya documenta [`ItemOrder`] en Flex.
|
||||
///
|
||||
/// [`ItemOrder`]: crate::html::flex::ItemOrder
|
||||
RowDense,
|
||||
/// Como `Column`, con el mismo relleno de huecos (`grid-auto-flow: column dense`).
|
||||
ColumnDense,
|
||||
}
|
||||
|
||||
impl AutoFlow {
|
||||
// Devuelve el valor CSS de `grid-auto-flow`, o "" para el valor por defecto.
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Row => "".into(),
|
||||
Self::Column => "column".into(),
|
||||
Self::RowDense => "row dense".into(),
|
||||
Self::ColumnDense => "column dense".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< DefaultJustify >*****************************************************************************
|
||||
|
||||
/// Alineación de los elementos en el eje de columnas (en línea) de un contenedor [`Grid`].
|
||||
///
|
||||
/// Es el valor por defecto que hereda cualquier elemento siempre que [`ItemJustify`] no lo
|
||||
/// sobrescriba.
|
||||
///
|
||||
/// Es al eje de columnas lo que [`align::Items`] es al eje transversal de [`Flex`], pero Grid, a
|
||||
/// diferencia de Flexbox, sí tiene una propiedad CSS propia para ello (`justify-items`), porque en
|
||||
/// Grid ningún eje se resuelve "girando" al otro.
|
||||
///
|
||||
/// [`align::Items`]: crate::html::align::Items
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`ItemJustify`]: super::ItemJustify
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DefaultJustify {
|
||||
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-items: normal` no
|
||||
/// explícito, equivalente a `stretch` salvo que el elemento tenga su propio tamaño).
|
||||
#[default]
|
||||
Normal,
|
||||
/// Alinea los elementos al inicio del eje de columnas (`justify-items: start`).
|
||||
Start,
|
||||
/// Alinea los elementos al final del eje de columnas (`justify-items: end`).
|
||||
End,
|
||||
/// Centra los elementos en el eje de columnas (`justify-items: center`).
|
||||
Center,
|
||||
/// Estira los elementos para ocupar toda su columna (`justify-items: stretch`).
|
||||
Stretch,
|
||||
}
|
||||
|
||||
impl DefaultJustify {
|
||||
// Devuelve el valor CSS de `justify-items`, o "" para el valor por defecto.
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Normal => "".into(),
|
||||
Self::Start => "start".into(),
|
||||
Self::End => "end".into(),
|
||||
Self::Center => "center".into(),
|
||||
Self::Stretch => "stretch".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< ContentJustify >*****************************************************************************
|
||||
|
||||
/// Alineación de las pistas en el eje de columnas de un contenedor [`Grid`].
|
||||
///
|
||||
/// Aplica cuando su tamaño total es menor que el del propio contenedor. No se reutiliza
|
||||
/// [`flex::ContentJustify`] porque Grid admite `stretch` (estira las pistas para repartir el
|
||||
/// espacio sobrante entre ellas), un valor sin efecto en Flexbox.
|
||||
///
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`flex::ContentJustify`]: crate::html::flex::ContentJustify
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ContentJustify {
|
||||
/// Por defecto, el navegador no fuerza ninguna alineación (`justify-content: normal` no
|
||||
/// explícito).
|
||||
#[default]
|
||||
Normal,
|
||||
/// Alinea las pistas al inicio del eje de columnas (`justify-content: start`).
|
||||
Start,
|
||||
/// Alinea las pistas al final del eje de columnas (`justify-content: end`).
|
||||
End,
|
||||
/// Centra las pistas en el eje de columnas (`justify-content: center`).
|
||||
Center,
|
||||
/// Estira las pistas para repartir el espacio sobrante entre ellas
|
||||
/// (`justify-content: stretch`).
|
||||
Stretch,
|
||||
/// Reparte el espacio sobrante entre las pistas (`justify-content: space-between`).
|
||||
SpaceBetween,
|
||||
/// Reparte el espacio sobrante alrededor de cada pista (`justify-content: space-around`).
|
||||
SpaceAround,
|
||||
/// Reparte el espacio sobrante en partes iguales, incluidos los extremos
|
||||
/// (`justify-content: space-evenly`).
|
||||
SpaceEvenly,
|
||||
}
|
||||
|
||||
impl ContentJustify {
|
||||
// Devuelve el valor CSS de `justify-content`, o "" para el valor por defecto.
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Normal => "".into(),
|
||||
Self::Start => "start".into(),
|
||||
Self::End => "end".into(),
|
||||
Self::Center => "center".into(),
|
||||
Self::Stretch => "stretch".into(),
|
||||
Self::SpaceBetween => "space-between".into(),
|
||||
Self::SpaceAround => "space-around".into(),
|
||||
Self::SpaceEvenly => "space-evenly".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< AxisTrack >**********************************************************************************
|
||||
|
||||
/// Anchura o altura de una única pista en un contenedor [`Grid`].
|
||||
///
|
||||
/// Se aplica dentro de una lista [`Tracks`], o vía [`Grid::with_columns()`]/[`Grid::with_rows()`]
|
||||
/// (la rejilla explícita); o suelto, vía [`Grid::with_auto_columns()`]/[`Grid::with_auto_rows()`]
|
||||
/// (el tamaño de las pistas que el navegador genera automáticamente fuera de esa rejilla).
|
||||
///
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`Grid::with_columns()`]: crate::base::component::Grid::with_columns
|
||||
/// [`Grid::with_rows()`]: crate::base::component::Grid::with_rows
|
||||
/// [`Grid::with_auto_columns()`]: crate::base::component::Grid::with_auto_columns
|
||||
/// [`Grid::with_auto_rows()`]: crate::base::component::Grid::with_auto_rows
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AxisTrack {
|
||||
/// Por defecto, ajusta al contenido repartiendo el espacio sobrante con las demás pistas `Auto`
|
||||
/// (`auto`).
|
||||
#[default]
|
||||
Auto,
|
||||
/// Medida fija o porcentual (p. ej. `AxisTrack::Fixed(UnitValue::Px(200))` o
|
||||
/// `AxisTrack::Fixed(UnitValue::RelPct(50.0))`).
|
||||
Fixed(UnitValue),
|
||||
/// Fracción del espacio sobrante repartido entre todas las pistas `Fraction` del mismo
|
||||
/// contenedor (`<n>fr`). Es la unidad idiomática de Grid, sin equivalente en [`UnitValue`]
|
||||
/// porque no es una medida absoluta ni relativa a nada fuera del propio reparto de Grid.
|
||||
Fraction(f32),
|
||||
/// El tamaño más pequeño en el que el contenido cabe sin desbordar. El texto se ajusta de línea
|
||||
/// todo lo posible, así que el resultado es el ancho de su elemento indivisible más largo
|
||||
/// (`min-content`).
|
||||
MinContent,
|
||||
/// El tamaño que ocuparía el contenido si no tuviera que ajustarse de línea en absoluto, siendo
|
||||
/// su ancho natural, sin envolver (`max-content`).
|
||||
MaxContent,
|
||||
}
|
||||
|
||||
impl AxisTrack {
|
||||
// Devuelve el valor CSS de esta pista. A diferencia del resto de propiedades de Grid/GridItem,
|
||||
// un `AxisTrack::Auto` (el valor por defecto del enum) sí produce un valor no vacío ("auto").
|
||||
// En vez de omitir la propiedad entera, una lista de pistas necesita un valor por cada hueco
|
||||
// para que el número de pistas declaradas sea el correcto.
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Auto => "auto".into(),
|
||||
Self::Fixed(unit) => unit.value(),
|
||||
Self::Fraction(n) => util::join!(n.to_string(), "fr").into(),
|
||||
Self::MinContent => "min-content".into(),
|
||||
Self::MaxContent => "max-content".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Tracks >*************************************************************************************
|
||||
|
||||
/// Número máximo de pistas que admite [`Tracks`].
|
||||
///
|
||||
/// Ver la nota de la propia `Tracks` sobre por qué es una capacidad fija y no un `Vec`.
|
||||
pub const MAX_TRACKS: usize = 12;
|
||||
|
||||
/// Lista ordenada de pistas para la rejilla explícita de un contenedor [`Grid`].
|
||||
///
|
||||
/// Sólo se aplica para [`Grid::with_columns()`] o [`Grid::with_rows()`].
|
||||
///
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`Grid::with_columns()`]: crate::base::component::Grid::with_columns
|
||||
/// [`Grid::with_rows()`]: crate::base::component::Grid::with_rows
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Tracks {
|
||||
// Es un array de capacidad fija (`[Option<AxisTrack>; MAX_TRACKS]`), no un `Vec<AxisTrack>`,
|
||||
// porque [`Responsive<T>`](crate::core::theme::Responsive) exige `T: Copy` y un `Vec` no lo es;
|
||||
// de lo contrario `Tracks` no podría variar por punto de corte (p. ej. una columna en móvil,
|
||||
// tres en escritorio), que es precisamente el caso de uso más habitual de Grid con breakpoints.
|
||||
// Doce pistas es más que suficiente para cualquier rejilla real (coincide, no por casualidad,
|
||||
// con la tradición de 12 columnas de Bootstrap y otros frameworks CSS).
|
||||
tracks: [Option<AxisTrack>; MAX_TRACKS],
|
||||
}
|
||||
|
||||
impl Tracks {
|
||||
/// Crea una lista de pistas vacía.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Añade una pista al final de la lista. Si ya hay [`MAX_TRACKS`] pistas, la llamada se ignora.
|
||||
pub fn with_track(mut self, track: AxisTrack) -> Self {
|
||||
if let Some(slot) = self.tracks.iter_mut().find(|t| t.is_none()) {
|
||||
*slot = Some(track);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Repite la misma pista `count` veces (equivalente a `repeat(count, track)` en CSS, para el
|
||||
/// caso de uso más común con pistas idénticas). Trunca a [`MAX_TRACKS`] si `count` lo supera.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let tracks = grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0));
|
||||
/// assert_eq!(tracks.to_string(), "1fr 1fr 1fr");
|
||||
/// ```
|
||||
pub fn repeat(count: usize, track: AxisTrack) -> Self {
|
||||
let mut tracks = Self::new();
|
||||
for _ in 0..count.min(MAX_TRACKS) {
|
||||
tracks = tracks.with_track(track);
|
||||
}
|
||||
tracks
|
||||
}
|
||||
|
||||
// Serializa como valor CSS. Las pistas separadas por un espacio, en orden, ignorando los huecos
|
||||
// vacíos. Cadena vacía si no hay ninguna pista (para que `apply!` no genere ninguna clase,
|
||||
// igual que el resto de propiedades con valor "por defecto" de Flex).
|
||||
pub(crate) fn value(self) -> CowStr {
|
||||
self.tracks
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|t| t.value())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// Único acceso público al valor CSS de `pub(crate) value()`, invisible fuera del crate, incluidos
|
||||
// los propios doctests, que compilan como si fueran un crate externo (ver el ejemplo de `repeat()`,
|
||||
// que depende de esta implementación para poder comprobar el resultado).
|
||||
impl std::fmt::Display for Tracks {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.value())
|
||||
}
|
||||
}
|
||||
83
src/html/grid/props_item.rs
Normal file
83
src/html/grid/props_item.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use crate::{AutoDefault, CowStr, util};
|
||||
|
||||
// **< ItemPlacement >******************************************************************************
|
||||
|
||||
/// Posición de un [`GridItem`](super::GridItem) en una línea de la rejilla (columna o fila).
|
||||
///
|
||||
/// El mismo tipo sirve para `grid-column` y `grid-row` ([`GridItem`](super::GridItem) guarda una
|
||||
/// instancia independiente para cada eje).
|
||||
///
|
||||
/// Las líneas se numeran desde `1` (la primera línea antes de la primera pista); un número negativo
|
||||
/// cuenta desde el final de la rejilla explícita (`-1` es la última línea). El `0` no es una línea
|
||||
/// válida en CSS Grid y se trata como [`ItemPlacement::Auto`].
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ItemPlacement {
|
||||
/// Colocación automática, según `grid-auto-flow` del contenedor (valor por defecto, sin
|
||||
/// declaración explícita).
|
||||
#[default]
|
||||
Auto,
|
||||
/// Ocupa `n` pistas a partir de donde lo coloque el algoritmo de colocación automática
|
||||
/// (`span n`).
|
||||
Span(u16),
|
||||
/// Empieza en la línea indicada, ocupando una sola pista.
|
||||
Line(i16),
|
||||
/// Ocupa desde la primera línea hasta la segunda, ambas indicadas explícitamente
|
||||
/// (`<start> / <end>`).
|
||||
Range(i16, i16),
|
||||
}
|
||||
|
||||
impl ItemPlacement {
|
||||
// Devuelve el valor CSS de `grid-column`/`grid-row`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Auto => "".into(),
|
||||
Self::Span(0) => "".into(),
|
||||
Self::Span(n) => util::join!("span ", n.to_string()).into(),
|
||||
Self::Line(0) => "".into(),
|
||||
Self::Line(n) => n.to_string().into(),
|
||||
Self::Range(0, _) | Self::Range(_, 0) => "".into(),
|
||||
Self::Range(start, end) => {
|
||||
util::join!(start.to_string(), " / ", end.to_string()).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< ItemJustify >*********************************************************************************
|
||||
|
||||
/// Alineación individual en el eje de columnas de un [`GridItem`](super::GridItem).
|
||||
///
|
||||
/// Análogo a [`DefaultJustify`], pero para un único elemento (`justify-self`) en vez de para todos
|
||||
/// los elementos del contenedor. Es la misma relación que ya tienen [`align::Items`] y
|
||||
/// [`align::ItemSelf`] en [`crate::html::align`].
|
||||
///
|
||||
/// [`DefaultJustify`]: super::DefaultJustify
|
||||
/// [`align::Items`]: crate::html::align::Items
|
||||
/// [`align::ItemSelf`]: crate::html::align::ItemSelf
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ItemJustify {
|
||||
/// Por defecto, hereda la alineación del contenedor (`justify-self: auto` no explícito).
|
||||
#[default]
|
||||
Default,
|
||||
/// Alinea el ítem al inicio de su columna (`justify-self: start`).
|
||||
Start,
|
||||
/// Alinea el ítem al final de su columna (`justify-self: end`).
|
||||
End,
|
||||
/// Centra el ítem en su columna (`justify-self: center`).
|
||||
Center,
|
||||
/// Estira el ítem para ocupar toda su columna (`justify-self: stretch`).
|
||||
Stretch,
|
||||
}
|
||||
|
||||
impl ItemJustify {
|
||||
// Devuelve el valor CSS de `justify-self`, o "" para el valor por defecto.
|
||||
pub(super) fn value(self) -> CowStr {
|
||||
match self {
|
||||
Self::Default => "".into(),
|
||||
Self::Start => "start".into(),
|
||||
Self::End => "end".into(),
|
||||
Self::Center => "center".into(),
|
||||
Self::Stretch => "stretch".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::core::TypeInfo;
|
||||
use crate::core::component::Context;
|
||||
use crate::html::flex::{Flex, FlexItem};
|
||||
use crate::html::flex::FlexItem;
|
||||
use crate::html::grid::GridItem;
|
||||
use crate::html::maud::{Escaper, RenderAttrs};
|
||||
use crate::html::props::{PropsError, PropsExtra, PropsOp};
|
||||
use crate::html::spacing::{Margin, Padding};
|
||||
|
|
@ -191,6 +192,7 @@ pub struct Props {
|
|||
attrs: Vec<(CowStr, CowStr)>,
|
||||
extras: HashMap<&'static str, PropsExtra>,
|
||||
flex_item: FlexItem,
|
||||
grid_item: GridItem,
|
||||
margin: Margin,
|
||||
padding: Padding,
|
||||
}
|
||||
|
|
@ -216,10 +218,10 @@ impl Props {
|
|||
}
|
||||
|
||||
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
|
||||
/// indicada, incluido el posicionamiento Flexbox y el espaciado (`FlexItem`, `Margin`,
|
||||
/// `Padding`). El método recomendado para construir cada operación es usar los constructores de
|
||||
/// [`PropsOp`], aunque `FlexItem`, `Margin` y `Padding` pueden pasarse directamente gracias a
|
||||
/// sus `From` hacia `PropsOp`.
|
||||
/// indicada, incluido el posicionamiento Flexbox y Grid y el espaciado (`FlexItem`,
|
||||
/// `GridItem`, `Margin`, `Padding`). El método recomendado para construir cada operación es
|
||||
/// usar los constructores de [`PropsOp`], aunque `FlexItem`, `GridItem`, `Margin` y `Padding`
|
||||
/// pueden pasarse directamente gracias a sus `From` hacia `PropsOp`.
|
||||
pub fn with_prop(mut self, op: impl Into<PropsOp>) -> Self {
|
||||
match op.into() {
|
||||
PropsOp::SetId(value) => {
|
||||
|
|
@ -344,6 +346,9 @@ impl Props {
|
|||
PropsOp::FlexItem(placement) => {
|
||||
self.flex_item = self.flex_item.merge(placement);
|
||||
}
|
||||
PropsOp::GridItem(placement) => {
|
||||
self.grid_item = self.grid_item.merge(placement);
|
||||
}
|
||||
PropsOp::Margin(margin) => {
|
||||
self.margin = self.margin.merge(margin);
|
||||
}
|
||||
|
|
@ -439,14 +444,19 @@ impl Props {
|
|||
self.attrs.is_empty()
|
||||
}
|
||||
|
||||
/// Devuelve `true` si no hay ningún identificador, clases, estilos o atributos adicionales
|
||||
/// definidos, sin tener en cuenta los valores extra.
|
||||
/// Devuelve `true` si no hay ningún identificador, clases, estilos, atributos adicionales,
|
||||
/// posicionamiento Flexbox/Grid ni espaciado definidos, sin tener en cuenta los valores extra
|
||||
/// (que nunca se emiten en el HTML, ver [`extra()`](Self::extra)).
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.id.is_none()
|
||||
&& self.classes.is_empty()
|
||||
&& self.styles.is_empty()
|
||||
&& self.attrs.is_empty()
|
||||
&& self.flex_item == FlexItem::default()
|
||||
&& self.grid_item == GridItem::default()
|
||||
&& self.margin == Margin::default()
|
||||
&& self.padding == Padding::default()
|
||||
}
|
||||
|
||||
/// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente.
|
||||
|
|
@ -572,11 +582,11 @@ impl Props {
|
|||
/// `Props` no implementa [`RenderAttrs`] directamente. Obliga a pasar siempre el `Context`
|
||||
/// vigente en el punto donde se renderiza, aunque no lo necesite ningún atributo propio. Recibe
|
||||
/// `&mut Context` porque aquí, en el momento de extraer los atributos, es donde se resuelven
|
||||
/// [`FlexItem`], [`Margin`] y [`Padding`]: las clases que devuelven se añaden a las del propio
|
||||
/// componente al escribir el atributo `class`.
|
||||
/// [`FlexItem`], [`GridItem`], [`Margin`] y [`Padding`]: las clases que devuelven se añaden a
|
||||
/// las del propio componente al escribir el atributo `class`.
|
||||
///
|
||||
/// Si el propio elemento actúa además como contenedor [`Flex`], utiliza [`unpack_with_flex()`]
|
||||
/// en su lugar.
|
||||
/// [`Flex`] y [`Grid`] resuelven su propio posicionamiento (Flexbox/CSS Grid) directamente: no
|
||||
/// son una propiedad de `Props`, son los únicos componentes que lo ofrecen.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -587,49 +597,38 @@ impl Props {
|
|||
/// ```
|
||||
///
|
||||
/// [`html!`]: crate::html::html
|
||||
/// [`Flex`]: crate::html::flex::Flex
|
||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||
/// [`GridItem`]: crate::html::grid::GridItem
|
||||
/// [`Margin`]: crate::html::spacing::Margin
|
||||
/// [`Padding`]: crate::html::spacing::Padding
|
||||
/// [`unpack_with_flex()`]: Self::unpack_with_flex
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
pub fn unpack<'a>(&'a self, cx: &mut Context) -> impl RenderAttrs + 'a {
|
||||
let mut classes = String::new();
|
||||
self.flex_item.apply(cx, &mut classes);
|
||||
self.margin.apply(cx, &mut classes);
|
||||
self.padding.apply(cx, &mut classes);
|
||||
PropsUnpack {
|
||||
props: self,
|
||||
classes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Igual que [`unpack()`], pero además resuelve `flex` con el posicionamiento [`Flex`].
|
||||
///
|
||||
/// A diferencia de [`FlexItem`]/[`Margin`]/[`Padding`] (que se acumulan con sus respectivas
|
||||
/// variantes de `PropsOp` porque cualquier componente ajeno puede necesitarlos sin tener un
|
||||
/// campo propio para ello), `Flex` sólo tiene sentido en los contenedores que ya declaran su
|
||||
/// propio campo `flex: Flex` (como `Container` o `Navbar`). Se les pasa aquí directamente, ya
|
||||
/// resuelto (`self.flex()`), sin pasar por `PropsOp`.
|
||||
///
|
||||
/// [`unpack()`]: Self::unpack
|
||||
/// [`Flex`]: crate::html::flex::Flex
|
||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||
/// [`Margin`]: crate::html::spacing::Margin
|
||||
/// [`Padding`]: crate::html::spacing::Padding
|
||||
pub fn unpack_with_flex<'a>(&'a self, cx: &mut Context, flex: Flex) -> impl RenderAttrs + 'a {
|
||||
let mut classes = String::new();
|
||||
flex.apply(cx, &mut classes);
|
||||
self.flex_item.apply(cx, &mut classes);
|
||||
self.margin.apply(cx, &mut classes);
|
||||
self.padding.apply(cx, &mut classes);
|
||||
PropsUnpack {
|
||||
props: self,
|
||||
classes,
|
||||
}
|
||||
self.unpack_with_classes(cx, String::new())
|
||||
}
|
||||
|
||||
// **< Props PRIVATE >**************************************************************************
|
||||
|
||||
// Cola común de `unpack()` y del propio `Flex`/`Grid` (`base::component::flex`/`grid`):
|
||||
// resuelve `FlexItem`/`GridItem`/`Margin`/`Padding` sobre las clases ya acumuladas (vacías, o
|
||||
// con las clases del propio contenedor ya calculadas por el llamador) y envuelve el resultado
|
||||
// para `html!`. `pub(crate)` porque `Flex` y `Grid` la usan directamente: cada uno construye
|
||||
// sus propias clases CSS (no hay ningún valor intermedio que aplicar aquí).
|
||||
pub(crate) fn unpack_with_classes<'a>(
|
||||
&'a self,
|
||||
cx: &mut Context,
|
||||
mut classes: String,
|
||||
) -> impl RenderAttrs + 'a {
|
||||
self.flex_item.apply(cx, &mut classes);
|
||||
self.grid_item.apply(cx, &mut classes);
|
||||
self.margin.apply(cx, &mut classes);
|
||||
self.padding.apply(cx, &mut classes);
|
||||
PropsUnpack {
|
||||
props: self,
|
||||
classes,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_id(&mut self, id: &str) {
|
||||
self.id = util::normalize_token(id);
|
||||
}
|
||||
|
|
@ -748,7 +747,7 @@ impl Props {
|
|||
w.push('"');
|
||||
}
|
||||
}
|
||||
// Clases propias del componente más las que aplica `Props::unpack()`/`unpack_with_flex()`.
|
||||
// Clases propias del componente + las que aplica `Props::unpack()`/`unpack_with_classes()`.
|
||||
let mut all_classes: Vec<&str> = self.classes.iter().map(String::as_str).collect();
|
||||
all_classes.extend(classes.split_ascii_whitespace());
|
||||
if let Some((first, rest)) = all_classes.split_first() {
|
||||
|
|
@ -817,10 +816,11 @@ impl Props {
|
|||
|
||||
// **< PropsUnpack >********************************************************************************
|
||||
|
||||
// Devuelto por `Props::unpack()`/`Props::unpack_with_flex()`. `classes` son las clases resueltas
|
||||
// por `Flex::apply()`/`FlexItem::apply()`/`Margin::apply()`/`Padding::apply()` (desde el propio
|
||||
// `unpack*()` usando el `&mut Context`), pendientes sólo de añadir a las del componente (ver
|
||||
// `Props::write_attrs()`).
|
||||
// Devuelto por `Props::unpack()` y por `unpack_with_classes()` (usada por `Flex`/`Grid`, y por
|
||||
// `unpack()` con un acumulador vacío). `classes` son las clases resueltas por `Flex`/`Grid` (sus
|
||||
// métodos privados `flex_classes()`/`grid_classes()`) más las de `FlexItem::apply()`/
|
||||
// `GridItem::apply()`/`Margin::apply()`/`Padding::apply()`, pendientes sólo de añadir a las del
|
||||
// componente (ver `Props::write_attrs()`).
|
||||
struct PropsUnpack<'a> {
|
||||
props: &'a Props,
|
||||
classes: String,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::CowStr;
|
||||
use crate::core::TypeInfo;
|
||||
use crate::html::flex::FlexItem;
|
||||
use crate::html::grid::GridItem;
|
||||
use crate::html::props::extra::PropsExtra;
|
||||
use crate::html::spacing::{Margin, Padding};
|
||||
|
||||
|
|
@ -41,9 +42,10 @@ use std::sync::Arc;
|
|||
/// se interpretan como si
|
||||
/// fueran valores internos del componente para tomar decisiones durante el renderizado.
|
||||
///
|
||||
/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem, mientras que
|
||||
/// [`FlexItem`](Self::FlexItem) aplica un posicionamiento Flexbox a nivel de ítem,
|
||||
/// [`GridItem`](Self::GridItem) aplica un posicionamiento CSS Grid a nivel de ítem, y
|
||||
/// [`Margin`](Self::Margin) y [`Padding`](Self::Padding) aplican márgenes y relleno interno; los
|
||||
/// tres sobre cualquier componente.
|
||||
/// cuatro sobre cualquier componente.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PropsOp {
|
||||
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
||||
|
|
@ -120,21 +122,46 @@ pub enum PropsOp {
|
|||
/// Existe como variante de `PropsOp`, y no como método builder de un componente, porque las
|
||||
/// propiedades de un ítem Flexbox tienen sentido sobre **cualquier** componente que pueda
|
||||
/// añadirse como hijo de un contenedor Flex (por ejemplo `Button`, `Nav`, un componente de
|
||||
/// terceros, incluso otro componente que sea, a su vez, un contenedor Flex para sus propios
|
||||
/// hijos).
|
||||
/// terceros, incluso otro componente que sea, a su vez, un contenedor Flex o [`Grid`] para sus
|
||||
/// propios hijos).
|
||||
///
|
||||
/// No existe una variante equivalente `PropsOp::Flex` para el componente contenedor. No hace
|
||||
/// falta porque los componentes contenedores, como `Container` o `Navbar`, ofrecen su propio
|
||||
/// `with_flex()` tipado y con introspección (p. ej. [`Container::flex()`]).
|
||||
/// No existe una variante equivalente `PropsOp::Flex` para el contenedor, por el mismo motivo
|
||||
/// que no existe `PropsOp::Grid`. El único componente contenedor [`Flex`] ofrece sus propios
|
||||
/// constructores y builders tipados, con introspección (p. ej. [`Flex::direction()`]). Esto no
|
||||
/// afecta a [`GridItem`]; un componente puede llevar las dos variantes a la vez, con
|
||||
/// independencia de qué tipo de contenedor sea él mismo para sus propios hijos.
|
||||
///
|
||||
/// [`Flex`]: crate::html::flex::Flex
|
||||
/// [`Container::flex()`]: crate::base::component::Container::flex
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`Flex::direction()`]: crate::base::component::Flex::direction
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`GridItem`]: Self::GridItem
|
||||
FlexItem(FlexItem),
|
||||
/// Aplica un posicionamiento [`GridItem`] a un componente particular en un contenedor
|
||||
/// [`Grid`]. Añade directamente sus estilos CSS Grid al propio componente, generando clases CSS
|
||||
/// dinámicas.
|
||||
///
|
||||
/// Misma razón de ser que [`FlexItem`]; las propiedades de un ítem Grid tienen sentido sobre
|
||||
/// **cualquier** componente que pueda añadirse como hijo de un contenedor Grid (por ejemplo
|
||||
/// `Button`, `Nav`, un componente de terceros, incluso otro componente que sea, a su vez, un
|
||||
/// contenedor Grid o [`Flex`] para sus propios hijos).
|
||||
///
|
||||
/// No existe una variante equivalente `PropsOp::Grid` para el contenedor, por el mismo motivo
|
||||
/// que no existe `PropsOp::Flex`. El único componente contenedor [`Grid`] ofrece sus propios
|
||||
/// constructores y builders tipados, con introspección (p. ej. [`Grid::columns()`]). Esto no
|
||||
/// afecta a [`FlexItem`]; un componente puede llevar las dos variantes a la vez, con
|
||||
/// independencia de qué tipo de contenedor sea él mismo para sus propios hijos.
|
||||
///
|
||||
/// [`Grid`]: crate::base::component::Grid
|
||||
/// [`Grid::columns()`]: crate::base::component::Grid::columns
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`FlexItem`]: Self::FlexItem
|
||||
GridItem(GridItem),
|
||||
/// Aplica un margen [`Margin`] a un componente particular. Añade directamente sus estilos al
|
||||
/// propio componente, generando clases CSS dinámicas.
|
||||
///
|
||||
/// Como [`FlexItem`](Self::FlexItem), existe como variante de `PropsOp` y no como método
|
||||
/// builder de un componente, porque el margen tiene sentido sobre **cualquier** componente.
|
||||
/// Como [`FlexItem`](Self::FlexItem) y [`GridItem`](Self::GridItem), existe como variante de
|
||||
/// `PropsOp` y no como método builder de un componente, porque el margen tiene sentido sobre
|
||||
/// **cualquier** componente.
|
||||
Margin(Margin),
|
||||
/// Aplica un relleno interno [`Padding`] a un componente particular, siguiendo los mismos
|
||||
/// criterios que [`Margin`](Self::Margin).
|
||||
|
|
@ -281,6 +308,11 @@ impl PropsOp {
|
|||
Self::FlexItem(placement)
|
||||
}
|
||||
|
||||
/// Crea la variante [`GridItem`](Self::GridItem) con el posicionamiento indicado.
|
||||
pub fn grid_item(placement: GridItem) -> Self {
|
||||
Self::GridItem(placement)
|
||||
}
|
||||
|
||||
/// Crea la variante [`Margin`](Self::Margin) con el margen indicado.
|
||||
pub fn margin(margin: Margin) -> Self {
|
||||
Self::Margin(margin)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
//! Mecanismo interno compartido para resolver clases CSS nativas *responsive*.
|
||||
//!
|
||||
//! Usado por [`flex`] y [`spacing`]. Ambos módulos resuelven su configuración generando clases CSS
|
||||
//! dinámicamente, de manera independiente a cualquier tema o framework CSS, registradas vía
|
||||
//! [`AssetsOp::add_responsive_style()`] y renderizadas como reglas en el `<head>` del documento.
|
||||
//! El nombre interno de cada clase se deriva de la propiedad y el valor que representa, así que dos
|
||||
//! elementos con la misma configuración comparten la misma regla en vez de duplicarla.
|
||||
//! Usado por [`flex`], [`grid`] y [`spacing`]. Los tres módulos resuelven su configuración
|
||||
//! generando clases CSS dinámicamente, de manera independiente a cualquier tema o framework CSS,
|
||||
//! registradas vía [`AssetsOp::add_responsive_style()`] y renderizadas como reglas en el `<head>`
|
||||
//! del documento. El nombre interno de cada clase se deriva de la propiedad y el valor que
|
||||
//! representa, así que dos elementos con la misma configuración comparten la misma regla en vez de
|
||||
//! duplicarla.
|
||||
//!
|
||||
//! [`flex`]: crate::html::flex
|
||||
//! [`grid`]: crate::html::grid
|
||||
//! [`spacing`]: crate::html::spacing
|
||||
//! [`AssetsOp::add_responsive_style()`]: crate::core::component::AssetsOp::add_responsive_style
|
||||
|
||||
|
|
@ -14,11 +16,21 @@ use crate::CowStr;
|
|||
use crate::core::component::{AssetsOp, Context, Contextual};
|
||||
use crate::core::theme::BreakpointEntry;
|
||||
|
||||
// Sustituye, en un valor CSS ya resuelto, los únicos caracteres (`.`, `%`) que no podrían usarse
|
||||
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"` y `"33.3333%"` quedaría
|
||||
// como `"33_3333pct"`.
|
||||
// Sustituye, en un valor CSS ya resuelto, los caracteres (`.`, `%`, ` `, `/`) que no podrían usarse
|
||||
// como fragmento de un nombre de clase. Así, `"1.5rem"` sería `"1_5rem"`, `"33.3333%"` quedaría en
|
||||
// `"33_3333pct"`, `"1fr 1fr 200px"` (varias pistas de `grid::Tracks`) dejaría
|
||||
// `"1fr-1fr-200px"`, y `"2 / 4"` (un rango de `grid::ItemPlacement`) quedaría como `"2---4"`.
|
||||
pub(crate) fn value_to_token(value: &str) -> String {
|
||||
value.replace('.', "_").replace('%', "pct")
|
||||
let mut token = String::with_capacity(value.len());
|
||||
for c in value.chars() {
|
||||
match c {
|
||||
'.' => token.push('_'),
|
||||
'%' => token.push_str("pct"),
|
||||
' ' | '/' => token.push('-'),
|
||||
c => token.push(c),
|
||||
}
|
||||
}
|
||||
token
|
||||
}
|
||||
|
||||
// Añade un estilo (`property: value`) al punto de corte indicado, y la clase a `classes`, separada
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ use crate::{AutoDefault, Getters, builder_impl, util};
|
|||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [`Flex`]: crate::html::flex::Flex
|
||||
/// [`Flex`]: crate::base::component::Flex
|
||||
/// [`FlexItem`]: crate::html::flex::FlexItem
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq, Getters)]
|
||||
pub struct Margin {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Container >**********************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn is_not_rendered_when_empty() {
|
||||
let mut container = Container::new();
|
||||
|
|
@ -33,183 +31,3 @@ async fn main_kind_renders_a_main_element() {
|
|||
assert!(html.starts_with("<main"));
|
||||
assert!(html.ends_with("</main>"));
|
||||
}
|
||||
|
||||
// **< Container + Flex >***************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn without_flex_no_style_attribute_is_added() {
|
||||
let mut container = Container::new().with_child(Lc::n("x"));
|
||||
let html = container
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(!html.contains("style="));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn default_flex_adds_only_display_flex() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::new())
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_flex_""#));
|
||||
assert!(assets.contains("_flex_{display:flex}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn column_direction_adds_flex_direction_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::new().with_direction(flex::Direction::Column))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex_"));
|
||||
assert!(html.contains("_flex-direction_column_"));
|
||||
assert!(assets.contains("_flex_{display:flex}"));
|
||||
assert!(assets.contains("_flex-direction_column_{flex-direction:column}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn wrap_justify_and_align_add_their_matching_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(
|
||||
Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_justify(flex::ContentJustify::Center)
|
||||
.with_align(flex::Align::Center)
|
||||
.with_align_content(flex::AlignContent::SpaceBetween),
|
||||
)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-wrap_wrap_"));
|
||||
assert!(html.contains("_flex-justify_center_"));
|
||||
assert!(html.contains("_flex-align-items_center_"));
|
||||
assert!(html.contains("_flex-align-content_space-between_"));
|
||||
assert!(assets.contains("_flex-wrap_wrap_{flex-wrap:wrap}"));
|
||||
assert!(assets.contains("_flex-justify_center_{justify-content:center}"));
|
||||
assert!(assets.contains("_flex-align-items_center_{align-items:center}"));
|
||||
assert!(assets.contains("_flex-align-content_space-between_{align-content:space-between}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_both_adds_a_single_gap_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::new().with_gap(flex::Gap::Both(UnitValue::RelRem(0.5))))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-gap_0_5rem_"));
|
||||
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_distinct_adds_row_and_column_gap_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::new().with_gap(flex::Gap::Distinct {
|
||||
row: UnitValue::Px(4),
|
||||
column: UnitValue::Px(8),
|
||||
}))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-row-gap_4px_"));
|
||||
assert!(html.contains("_flex-column-gap_8px_"));
|
||||
assert!(assets.contains("_flex-row-gap_4px_{row-gap:4px}"));
|
||||
assert!(assets.contains("_flex-column-gap_8px_{column-gap:8px}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_none_adds_no_gap_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::new())
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(!html.contains("gap"));
|
||||
assert!(!assets.contains("gap"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn inline_flex_uses_its_own_display_value() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::inline())
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_inline-flex_""#));
|
||||
assert!(assets.contains("_inline-flex_{display:inline-flex}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn inline_flex_at_a_breakpoint_adds_its_suffix() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(Flex::inline_at(Breakpoint::Lg))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_inline-flex_lg_""#));
|
||||
assert!(assets.contains("@media(min-width:992px){._inline-flex_lg_{display:inline-flex}}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn flex_and_flex_item_classes_keep_their_order() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_prop(PropsOp::add_classes("own"))
|
||||
.with_flex(Flex::new().with_direction(flex::Direction::Column))
|
||||
.with_prop(FlexItem::new().with_grow(flex::ItemGrow::Is1))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
|
||||
// The component's own classes come first, then those of `Flex`, then those of `FlexItem`,
|
||||
// which share a single accumulator in `Props::unpack_with_flex()`.
|
||||
assert!(html.contains(r#"class="own _flex_ _flex-direction_column_ _flex-item-grow_1_""#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn flex_without_display_adds_no_class_of_its_own() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_prop(PropsOp::add_classes("own"))
|
||||
.with_flex(Flex::default())
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"class="own""#));
|
||||
assert!(cx.render_assets().into_string().is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn breakpoints_add_their_suffix_and_wrap_rules_in_media_queries() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Container::new()
|
||||
.with_flex(
|
||||
Flex::at(Breakpoint::Md).with_direction_at(Breakpoint::Lg, flex::Direction::Column),
|
||||
)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_flex_md_ _flex-direction_column_lg_""#));
|
||||
assert!(assets.contains("@media(min-width:768px){._flex_md_{display:flex}}"));
|
||||
assert!(assets.contains("@media(min-width:992px){._flex-direction_column_lg_"));
|
||||
}
|
||||
|
|
|
|||
187
tests/component_flex.rs
Normal file
187
tests/component_flex.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Flex >***************************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn is_not_rendered_when_empty() {
|
||||
let mut container = Flex::default();
|
||||
let html = container.render(&mut Context::default()).await;
|
||||
|
||||
assert!(html.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn renders_a_div_element() {
|
||||
let mut container = Flex::default().with_child(Lc::n("x"));
|
||||
let html = container
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(html.starts_with("<div"));
|
||||
assert!(html.ends_with("</div>"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn without_flex_no_style_attribute_is_added() {
|
||||
let mut container = Flex::default().with_child(Lc::n("x"));
|
||||
let html = container
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(!html.contains("style="));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn default_flex_adds_only_display_flex() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new().with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_flex_""#));
|
||||
assert!(assets.contains("_flex_{display:flex}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn column_direction_adds_flex_direction_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new()
|
||||
.with_direction(flex::Direction::Column)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex_"));
|
||||
assert!(html.contains("_flex-direction_column_"));
|
||||
assert!(assets.contains("_flex_{display:flex}"));
|
||||
assert!(assets.contains("_flex-direction_column_{flex-direction:column}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn wrap_justify_and_align_add_their_matching_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new()
|
||||
.with_wrap(flex::Behavior::Wrap)
|
||||
.with_justify(flex::ContentJustify::Center)
|
||||
.with_align(align::Items::Center)
|
||||
.with_align_content(align::Content::SpaceBetween)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-wrap_wrap_"));
|
||||
assert!(html.contains("_flex-justify_center_"));
|
||||
assert!(html.contains("_flex-align-items_center_"));
|
||||
assert!(html.contains("_flex-align-content_space-between_"));
|
||||
assert!(assets.contains("_flex-wrap_wrap_{flex-wrap:wrap}"));
|
||||
assert!(assets.contains("_flex-justify_center_{justify-content:center}"));
|
||||
assert!(assets.contains("_flex-align-items_center_{align-items:center}"));
|
||||
assert!(assets.contains("_flex-align-content_space-between_{align-content:space-between}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_both_adds_a_single_gap_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new()
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-gap_0_5rem_"));
|
||||
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_distinct_adds_row_and_column_gap_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new()
|
||||
.with_gap(align::Gap::Distinct {
|
||||
row: UnitValue::Px(4),
|
||||
column: UnitValue::Px(8),
|
||||
})
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-row-gap_4px_"));
|
||||
assert!(html.contains("_flex-column-gap_8px_"));
|
||||
assert!(assets.contains("_flex-row-gap_4px_{row-gap:4px}"));
|
||||
assert!(assets.contains("_flex-column-gap_8px_{column-gap:8px}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_none_adds_no_gap_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new().with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(!html.contains("gap"));
|
||||
assert!(!assets.contains("gap"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn inline_flex_uses_its_own_display_value() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::inline().with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_inline-flex_""#));
|
||||
assert!(assets.contains("_inline-flex_{display:inline-flex}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn inline_flex_at_a_breakpoint_adds_its_suffix() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::inline_at(Breakpoint::Lg).with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_inline-flex_lg_""#));
|
||||
assert!(assets.contains("@media(min-width:992px){._inline-flex_lg_{display:inline-flex}}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn flex_and_flex_item_classes_keep_their_order() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::new()
|
||||
.with_direction(flex::Direction::Column)
|
||||
.with_prop(PropsOp::add_classes("own"))
|
||||
.with_prop(FlexItem::new().with_grow(flex::ItemGrow::Is1))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
|
||||
// The component's own classes come first, then those of `Flex`, then those of `FlexItem`,
|
||||
// which share a single accumulator in `Props::unpack_with_classes()`.
|
||||
assert!(html.contains(r#"class="own _flex_ _flex-direction_column_ _flex-item-grow_1_""#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn flex_without_display_adds_no_class_of_its_own() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::default()
|
||||
.with_prop(PropsOp::add_classes("own"))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"class="own""#));
|
||||
assert!(cx.render_assets().into_string().is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn breakpoints_add_their_suffix_and_wrap_rules_in_media_queries() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Flex::at(Breakpoint::Md)
|
||||
.with_direction_at(Breakpoint::Lg, flex::Direction::Column)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_flex_md_ _flex-direction_column_lg_""#));
|
||||
assert!(assets.contains("@media(min-width:768px){._flex_md_{display:flex}}"));
|
||||
assert!(assets.contains("@media(min-width:992px){._flex-direction_column_lg_"));
|
||||
}
|
||||
223
tests/component_grid.rs
Normal file
223
tests/component_grid.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Grid >***************************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn is_not_rendered_when_empty() {
|
||||
let mut container = Grid::default();
|
||||
let html = container.render(&mut Context::default()).await;
|
||||
|
||||
assert!(html.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn renders_a_div_element() {
|
||||
let mut container = Grid::default().with_child(Lc::n("x"));
|
||||
let html = container
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(html.starts_with("<div"));
|
||||
assert!(html.ends_with("</div>"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn without_grid_no_style_attribute_is_added() {
|
||||
let mut container = Grid::default().with_child(Lc::n("x"));
|
||||
let html = container
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(!html.contains("style="));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn default_grid_adds_only_display_grid() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new().with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid_""#));
|
||||
assert!(assets.contains("_grid_{display:grid}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn columns_and_rows_add_their_matching_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)))
|
||||
.with_rows(grid::Tracks::new().with_track(grid::AxisTrack::Fixed(UnitValue::Px(80))))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-columns_1fr-1fr-1fr_"));
|
||||
assert!(html.contains("_grid-rows_80px_"));
|
||||
assert!(assets.contains("_grid-columns_1fr-1fr-1fr_{grid-template-columns:1fr 1fr 1fr}"));
|
||||
assert!(assets.contains("_grid-rows_80px_{grid-template-rows:80px}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn auto_columns_and_auto_rows_add_their_matching_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_auto_columns(grid::AxisTrack::MinContent)
|
||||
.with_auto_rows(grid::AxisTrack::Fixed(UnitValue::RelRem(3.0)))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-auto-columns_min-content_"));
|
||||
assert!(html.contains("_grid-auto-rows_3rem_"));
|
||||
assert!(assets.contains("_grid-auto-columns_min-content_{grid-auto-columns:min-content}"));
|
||||
assert!(assets.contains("_grid-auto-rows_3rem_{grid-auto-rows:3rem}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn auto_flow_dense_sanitizes_the_space_in_its_class_name() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_auto_flow(grid::AutoFlow::RowDense)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
// `AutoFlow::RowDense` resolves to "row dense" (with a space); as a class name it must stay a
|
||||
// single, valid token (regression test: this used to split into two class list entries).
|
||||
assert!(html.contains(r#"class="_grid_ _grid-auto-flow_row-dense_""#));
|
||||
assert!(assets.contains("_grid-auto-flow_row-dense_{grid-auto-flow:row dense}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn justify_items_and_align_items_add_their_matching_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_justify_items(grid::DefaultJustify::Center)
|
||||
.with_align_items(align::Items::Stretch)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-justify-items_center_"));
|
||||
assert!(html.contains("_grid-align-items_stretch_"));
|
||||
assert!(assets.contains("_grid-justify-items_center_{justify-items:center}"));
|
||||
assert!(assets.contains("_grid-align-items_stretch_{align-items:stretch}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn justify_content_and_align_content_add_their_matching_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_justify_content(grid::ContentJustify::SpaceBetween)
|
||||
.with_align_content(align::Content::Center)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-justify-content_space-between_"));
|
||||
assert!(html.contains("_grid-align-content_center_"));
|
||||
assert!(assets.contains("_grid-justify-content_space-between_{justify-content:space-between}"));
|
||||
assert!(assets.contains("_grid-align-content_center_{align-content:center}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_both_adds_a_single_gap_style() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-gap_0_5rem_"));
|
||||
assert!(assets.contains("_grid-gap_0_5rem_{gap:0.5rem}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn gap_distinct_adds_row_and_column_gap_styles() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_gap(align::Gap::Distinct {
|
||||
row: UnitValue::Px(4),
|
||||
column: UnitValue::Px(8),
|
||||
})
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-row-gap_4px_"));
|
||||
assert!(html.contains("_grid-column-gap_8px_"));
|
||||
assert!(assets.contains("_grid-row-gap_4px_{row-gap:4px}"));
|
||||
assert!(assets.contains("_grid-column-gap_8px_{column-gap:8px}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn inline_grid_uses_its_own_display_value() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::inline().with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_inline-grid_""#));
|
||||
assert!(assets.contains("_inline-grid_{display:inline-grid}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn inline_grid_at_a_breakpoint_adds_its_suffix() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::inline_at(Breakpoint::Lg).with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_inline-grid_lg_""#));
|
||||
assert!(assets.contains("@media(min-width:992px){._inline-grid_lg_{display:inline-grid}}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn grid_and_grid_item_classes_keep_their_order() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::new()
|
||||
.with_columns(grid::Tracks::repeat(2, grid::AxisTrack::Fraction(1.0)))
|
||||
.with_prop(PropsOp::add_classes("own"))
|
||||
.with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2)))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
|
||||
// The component's own classes come first, then those of `Grid`, then those of `GridItem`,
|
||||
// which share a single accumulator in `Props::unpack_with_classes()`.
|
||||
assert!(
|
||||
html.contains(r#"class="own _grid_ _grid-columns_1fr-1fr_ _grid-item-column_span-2_""#)
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn grid_without_display_adds_no_class_of_its_own() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::default()
|
||||
.with_prop(PropsOp::add_classes("own"))
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"class="own""#));
|
||||
assert!(cx.render_assets().into_string().is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn grid_breakpoints_add_their_suffix_and_wrap_rules_in_media_queries() {
|
||||
let mut cx = Context::default();
|
||||
let mut container = Grid::at(Breakpoint::Md)
|
||||
.with_columns_at(
|
||||
Breakpoint::Lg,
|
||||
grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)),
|
||||
)
|
||||
.with_child(Lc::n("x"));
|
||||
let html = container.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid_md_ _grid-columns_1fr-1fr-1fr_lg_""#));
|
||||
assert!(assets.contains("@media(min-width:768px){._grid_md_{display:grid}}"));
|
||||
assert!(assets.contains("@media(min-width:992px){._grid-columns_1fr-1fr-1fr_lg_"));
|
||||
}
|
||||
|
|
@ -14,56 +14,6 @@ async fn is_not_rendered_when_empty() {
|
|||
assert!(html.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn nav_root_class_is_unaffected_by_content_flex() {
|
||||
let mut navbar = Navbar::simple()
|
||||
.with_flex(Flex::new().with_justify(flex::ContentJustify::End))
|
||||
.with_item(navbar::Item::nav(one_link_nav()));
|
||||
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"class="navbar""#));
|
||||
}
|
||||
|
||||
// **< Navbar + Flex (content area) >***************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn without_flex_content_area_has_no_style_attribute() {
|
||||
let mut navbar = Navbar::simple().with_item(navbar::Item::nav(one_link_nav()));
|
||||
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"class="navbar-content""#));
|
||||
assert!(!html.contains("style="));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn flex_adds_its_styles_to_the_content_area() {
|
||||
let mut cx = Context::default();
|
||||
let mut navbar = Navbar::simple()
|
||||
.with_flex(Flex::new().with_justify(flex::ContentJustify::End))
|
||||
.with_item(navbar::Item::nav(one_link_nav()));
|
||||
let html = navbar.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("navbar-content"));
|
||||
assert!(html.contains("_flex_"));
|
||||
assert!(html.contains("_flex-justify_flex-end_"));
|
||||
assert!(assets.contains("_flex_{display:flex}"));
|
||||
assert!(assets.contains("_flex-justify_flex-end_{justify-content:flex-end}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn flex_gap_adds_a_style_to_the_content_area() {
|
||||
let mut cx = Context::default();
|
||||
let mut navbar = Navbar::simple()
|
||||
.with_flex(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||
.with_item(navbar::Item::nav(one_link_nav()));
|
||||
let html = navbar.render(&mut cx).await.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_flex-gap_0_5rem_"));
|
||||
assert!(assets.contains("_flex-gap_0_5rem_{gap:0.5rem}"));
|
||||
}
|
||||
|
||||
// **< Navbar + FlexItem::push_end >****************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
|
|
|
|||
150
tests/html_props_grid_item.rs
Normal file
150
tests/html_props_grid_item.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
#[pagetop::test]
|
||||
async fn default_grid_item_adds_nothing() {
|
||||
let mut cx = Context::default();
|
||||
let props = Props::default().with_prop(GridItem::new());
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
|
||||
assert_eq!(html, "<span></span>");
|
||||
assert!(cx.render_assets().into_string().is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn column_span_adds_grid_column_style() {
|
||||
let mut cx = Context::default();
|
||||
let props =
|
||||
Props::default().with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2)));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-column_span-2_""#));
|
||||
assert!(assets.contains("_grid-item-column_span-2_{grid-column:span 2}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn column_line_adds_grid_column_style() {
|
||||
let mut cx = Context::default();
|
||||
let props =
|
||||
Props::default().with_prop(GridItem::new().with_column(grid::ItemPlacement::Line(3)));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-column_3_""#));
|
||||
assert!(assets.contains("_grid-item-column_3_{grid-column:3}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn column_range_sanitizes_the_slash_and_space_in_its_class_name() {
|
||||
let mut cx = Context::default();
|
||||
let props =
|
||||
Props::default().with_prop(GridItem::new().with_column(grid::ItemPlacement::Range(2, 4)));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
// "2 / 4" is a valid CSS value (with its spaces and slash), but not a valid class name
|
||||
// fragment: `value_to_token()` replaces both characters so the class stays a single token.
|
||||
assert!(html.contains(r#"class="_grid-item-column_2---4_""#));
|
||||
assert!(assets.contains("_grid-item-column_2---4_{grid-column:2 / 4}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn row_adds_grid_row_style() {
|
||||
let mut cx = Context::default();
|
||||
let props = Props::default().with_prop(GridItem::new().with_row(grid::ItemPlacement::Line(2)));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-row_2_""#));
|
||||
assert!(assets.contains("_grid-item-row_2_{grid-row:2}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn justify_self_adds_matching_style() {
|
||||
let mut cx = Context::default();
|
||||
let props =
|
||||
Props::default().with_prop(GridItem::new().with_justify_self(grid::ItemJustify::End));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-justify-self_end_""#));
|
||||
assert!(assets.contains("_grid-item-justify-self_end_{justify-self:end}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn align_self_adds_matching_style() {
|
||||
let mut cx = Context::default();
|
||||
let props =
|
||||
Props::default().with_prop(GridItem::new().with_align_self(align::ItemSelf::Center));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-align-self_center_""#));
|
||||
assert!(assets.contains("_grid-item-align-self_center_{align-self:center}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn combines_several_facets_in_one_call() {
|
||||
let mut cx = Context::default();
|
||||
let props = Props::default().with_prop(
|
||||
GridItem::new()
|
||||
.with_column(grid::ItemPlacement::Span(2))
|
||||
.with_row(grid::ItemPlacement::Line(2))
|
||||
.with_justify_self(grid::ItemJustify::Stretch)
|
||||
.with_align_self(align::ItemSelf::Start),
|
||||
);
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains("_grid-item-column_span-2_"));
|
||||
assert!(html.contains("_grid-item-row_2_"));
|
||||
assert!(html.contains("_grid-item-justify-self_stretch_"));
|
||||
assert!(html.contains("_grid-item-align-self_flex-start_"));
|
||||
assert!(assets.contains("_grid-item-column_span-2_{grid-column:span 2}"));
|
||||
assert!(assets.contains("_grid-item-row_2_{grid-row:2}"));
|
||||
assert!(assets.contains("_grid-item-justify-self_stretch_{justify-self:stretch}"));
|
||||
assert!(assets.contains("_grid-item-align-self_flex-start_{align-self:flex-start}"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn column_at_a_breakpoint_combines_token_and_suffix() {
|
||||
let mut cx = Context::default();
|
||||
let props = Props::default()
|
||||
.with_prop(GridItem::new().with_column_at(Breakpoint::Md, grid::ItemPlacement::Span(3)));
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-column_span-3_md_""#));
|
||||
assert!(
|
||||
assets
|
||||
.contains("@media(min-width:768px){._grid-item-column_span-3_md_{grid-column:span 3}}")
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn base_and_breakpoint_values_generate_one_class_each() {
|
||||
let mut cx = Context::default();
|
||||
let props = Props::default().with_prop(
|
||||
GridItem::new()
|
||||
.with_align_self(align::ItemSelf::Default)
|
||||
.with_column(grid::ItemPlacement::Span(2))
|
||||
.with_column_at(Breakpoint::Lg, grid::ItemPlacement::Span(4)),
|
||||
);
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
|
||||
// `ItemSelf::Default` resolves to an empty CSS value, so it adds no class at all.
|
||||
assert!(html.contains(r#"class="_grid-item-column_span-2_ _grid-item-column_span-4_lg_""#));
|
||||
assert!(!html.contains("_grid-item-align-self_"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn from_grid_item_for_props_op() {
|
||||
let mut cx = Context::default();
|
||||
let item = GridItem::new().with_column(grid::ItemPlacement::Span(2));
|
||||
let props = Props::default().with_prop(item);
|
||||
let html = html! { span (props.unpack(&mut cx)) {} }.into_string();
|
||||
let assets = cx.render_assets().into_string();
|
||||
|
||||
assert!(html.contains(r#"class="_grid-item-column_span-2_""#));
|
||||
assert!(assets.contains("_grid-item-column_span-2_{grid-column:span 2}"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue