Compare commits
3 commits
825283caa3
...
6bea4a5793
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bea4a5793 | |||
| 20b6c77c9e | |||
| 8cb5e035dc |
11 changed files with 296 additions and 6 deletions
|
|
@ -276,6 +276,38 @@ input:disabled + label {
|
||||||
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Breadcrumb component
|
||||||
|
*/
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.breadcrumb-item + .breadcrumb-item {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.breadcrumb-item + .breadcrumb-item::before {
|
||||||
|
float: left;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
content: "/";
|
||||||
|
}
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: var(--val-color--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.breadcrumb-item a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Region Footer
|
* Region Footer
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ pub mod layout;
|
||||||
mod badge;
|
mod badge;
|
||||||
pub use badge::Badge;
|
pub use badge::Badge;
|
||||||
|
|
||||||
|
pub mod breadcrumb;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use breadcrumb::Breadcrumb;
|
||||||
|
|
||||||
mod block;
|
mod block;
|
||||||
pub use block::Block;
|
pub use block::Block;
|
||||||
|
|
||||||
|
|
@ -26,7 +30,7 @@ mod intro;
|
||||||
pub use intro::{Intro, IntroOpening};
|
pub use intro::{Intro, IntroOpening};
|
||||||
|
|
||||||
mod pager;
|
mod pager;
|
||||||
pub use pager::{Pager, PagerVisibility};
|
pub use pager::{Pager, PagerAlign, PagerVisibility};
|
||||||
|
|
||||||
mod poweredby;
|
mod poweredby;
|
||||||
pub use poweredby::PoweredBy;
|
pub use poweredby::PoweredBy;
|
||||||
|
|
|
||||||
7
src/base/component/breadcrumb.rs
Normal file
7
src/base/component/breadcrumb.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//! Definiciones para mostrar rutas de navegación ([`Breadcrumb`]).
|
||||||
|
|
||||||
|
mod component;
|
||||||
|
pub use component::Breadcrumb;
|
||||||
|
|
||||||
|
mod crumb;
|
||||||
|
pub use crumb::Crumb;
|
||||||
93
src/base/component/breadcrumb/component.rs
Normal file
93
src/base/component/breadcrumb/component.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Componente para representar una ruta de navegación (*breadcrumb*).
|
||||||
|
///
|
||||||
|
/// Renderiza la estructura HTML de cualquier breadcrumb encapsulando en un elemento `<nav>` una
|
||||||
|
/// lista con un [`breadcrumb::Crumb`] por cada nivel de la ruta de navegación. Deja en manos de
|
||||||
|
/// quien lo use la construcción de esos niveles. No sabe nada del origen de los datos, ni de menús,
|
||||||
|
/// ni de ningún otro esquema de navegación concreto; cada extensión resuelve sus propios datos (por
|
||||||
|
/// ejemplo, el *active trail* de un menú) y construye los [`breadcrumb::Crumb`] correspondientes.
|
||||||
|
///
|
||||||
|
/// Si no contiene ningún elemento, el componente **no se renderiza**.
|
||||||
|
///
|
||||||
|
/// # Clases CSS
|
||||||
|
///
|
||||||
|
/// - `.breadcrumb`: clase de la lista que contiene los niveles.
|
||||||
|
/// - `.breadcrumb-item`: presente en todos los elementos de la lista.
|
||||||
|
/// - `.active`: añadida al elemento actual (ver [`breadcrumb::Crumb::current()`]).
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// let bc = Breadcrumb::new()
|
||||||
|
/// .with_crumb(breadcrumb::Crumb::new(L10n::n("Home"), "/"))
|
||||||
|
/// .with_crumb(breadcrumb::Crumb::new(L10n::n("Users"), "/admin/users"))
|
||||||
|
/// .with_crumb(breadcrumb::Crumb::current(L10n::n("Julia")));
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
pub struct Breadcrumb {
|
||||||
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||||
|
props: Props,
|
||||||
|
/// Devuelve la lista de elementos del breadcrumb, en orden de aparición.
|
||||||
|
crumbs: Vec<breadcrumb::Crumb>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for Breadcrumb {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.props.get_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(&mut self, cx: &Context) {
|
||||||
|
for crumb in self.crumbs.iter_mut() {
|
||||||
|
crumb.setup(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
if self.crumbs().is_empty() {
|
||||||
|
return Ok(html! {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
nav (self.props()) aria-label=[L10n::l("breadcrumb_label").lookup(cx)] {
|
||||||
|
ol.breadcrumb {
|
||||||
|
@for crumb in self.crumbs() {
|
||||||
|
(crumb.render_crumb(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Breadcrumb {
|
||||||
|
// **< Breadcrumb BUILDER >*********************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||||
|
#[builder_fn]
|
||||||
|
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.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||||
|
self.props.alter_prop(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo elemento al final del breadcrumb.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_crumb(mut self, crumb: breadcrumb::Crumb) -> Self {
|
||||||
|
self.crumbs.push(crumb);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/base/component/breadcrumb/crumb.rs
Normal file
105
src/base/component/breadcrumb/crumb.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Representa un elemento de un [`Breadcrumb`](super::Breadcrumb).
|
||||||
|
///
|
||||||
|
/// Hay tres formas de crear un `Crumb`, según el papel que ocupe en la lista:
|
||||||
|
///
|
||||||
|
/// - [`Crumb::new()`]: un elemento enlazado, con destino propio.
|
||||||
|
/// - [`Crumb::current()`]: el elemento final, sin enlace, que representa la página actual.
|
||||||
|
/// - [`Crumb::text()`]: texto plano sin enlace y sin marcar como página actual, por ejemplo un
|
||||||
|
/// nivel intermedio sin URL propia.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// let bc = Breadcrumb::new()
|
||||||
|
/// .with_crumb(breadcrumb::Crumb::new(L10n::n("Home"), "/"))
|
||||||
|
/// .with_crumb(breadcrumb::Crumb::text(L10n::n("Users")))
|
||||||
|
/// .with_crumb(breadcrumb::Crumb::current(L10n::n("Julia")));
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
pub struct Crumb {
|
||||||
|
/// Devuelve identificador, clases CSS y atributos HTML del elemento.
|
||||||
|
props: Props,
|
||||||
|
/// Devuelve la etiqueta del elemento.
|
||||||
|
label: L10n,
|
||||||
|
/// Devuelve la ruta de destino del elemento, si es un enlace.
|
||||||
|
route: Option<Route>,
|
||||||
|
/// Devuelve si el elemento representa la página actual.
|
||||||
|
is_current: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crumb {
|
||||||
|
/// Crea un elemento enlazado a la ruta indicada.
|
||||||
|
pub fn new(label: L10n, route: impl Into<Route>) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
route: Some(route.into()),
|
||||||
|
is_current: false,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea el elemento final de la lista, sin enlace, que representa la página actual.
|
||||||
|
///
|
||||||
|
/// Al renderizarse dentro de un `Breadcrumb`, el elemento lleva `aria-current="page"` y la
|
||||||
|
/// clase `.active`.
|
||||||
|
pub fn current(label: L10n) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
is_current: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un elemento de sólo texto, sin enlace ni marca de página actual (por ejemplo, un nivel
|
||||||
|
/// intermedio sin URL propia).
|
||||||
|
pub fn text(label: L10n) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Crumb BUILDER >**************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único del elemento.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||||
|
self.props.alter_id(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica identificador, clases CSS o atributos HTML del elemento.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||||
|
self.props.alter_prop(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normaliza la clase base según el papel del elemento. Sólo lo usa `Breadcrumb`.
|
||||||
|
pub(super) fn setup(&mut self, _cx: &Context) {
|
||||||
|
if *self.is_current() {
|
||||||
|
self.alter_prop(PropsOp::prepend_classes("active"))
|
||||||
|
.alter_prop(PropsOp::set("aria-current", "page"));
|
||||||
|
}
|
||||||
|
self.alter_prop(PropsOp::prepend_classes("breadcrumb-item"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderiza con enlace si tiene ruta, o texto plano en otro caso. Sólo lo usa `Breadcrumb`.
|
||||||
|
pub(super) fn render_crumb(&self, cx: &Context) -> Markup {
|
||||||
|
let label = self.label().using(cx);
|
||||||
|
match self.route() {
|
||||||
|
Some(route) => html! {
|
||||||
|
li (self.props()) {
|
||||||
|
a href=(route.resolve(cx).to_string()) { (label) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => html! {
|
||||||
|
li (self.props()) { (label) }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// Define cuándo mostrar los botones de página anterior/siguiente o el formulario de salto a página
|
/// Establece la visibilidad de elementos esenciales de un [`Pager`].
|
||||||
/// de [`Pager`].
|
///
|
||||||
|
/// Se aplica al resumen de páginas mostradas ([`Pager::with_summary()`]), a los botones de
|
||||||
|
/// navegación anterior/siguiente ([`Pager::with_prev_next()`]) y al formulario de salto directo a
|
||||||
|
/// página ([`Pager::with_jump()`]).
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum PagerVisibility {
|
pub enum PagerVisibility {
|
||||||
/// Nunca se muestra.
|
/// Nunca se muestra.
|
||||||
|
|
@ -232,7 +235,7 @@ impl Component for Pager {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
nav (self.props()) aria-label=(self.aria_label().using(cx)) {
|
nav (self.props()) aria-label=[self.aria_label().lookup(cx)] {
|
||||||
@if show_summary {
|
@if show_summary {
|
||||||
@let items_per_page = self.items_per_page().max(1);
|
@let items_per_page = self.items_per_page().max(1);
|
||||||
@let first = (page - 1) * items_per_page + 1;
|
@let first = (page - 1) * items_per_page + 1;
|
||||||
|
|
@ -251,7 +254,7 @@ impl Component for Pager {
|
||||||
a.page-link
|
a.page-link
|
||||||
href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))]
|
href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))]
|
||||||
aria-disabled=[first_disabled.then_some("true")]
|
aria-disabled=[first_disabled.then_some("true")]
|
||||||
aria-label=(L10n::l("pager_previous_aria_label").using(cx)) {
|
aria-label=[L10n::l("pager_previous_aria_label").lookup(cx)] {
|
||||||
span.page-link-icon { (L10n::l("pager_previous_label").using(cx)) }
|
span.page-link-icon { (L10n::l("pager_previous_label").using(cx)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +283,7 @@ impl Component for Pager {
|
||||||
a.page-link
|
a.page-link
|
||||||
href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))]
|
href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))]
|
||||||
aria-disabled=[last_disabled.then_some("true")]
|
aria-disabled=[last_disabled.then_some("true")]
|
||||||
aria-label=(L10n::l("pager_next_aria_label").using(cx)) {
|
aria-label=[L10n::l("pager_next_aria_label").lookup(cx)] {
|
||||||
span.page-link-icon { (L10n::l("pager_next_label").using(cx)) }
|
span.page-link-icon { (L10n::l("pager_next_label").using(cx)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::core::action::add_action;
|
use crate::core::action::add_action;
|
||||||
use crate::core::extension::ExtensionRef;
|
use crate::core::extension::ExtensionRef;
|
||||||
|
use crate::core::theme::ThemeRef;
|
||||||
use crate::core::theme::all::THEMES;
|
use crate::core::theme::all::THEMES;
|
||||||
use crate::web::Router;
|
use crate::web::Router;
|
||||||
use crate::{global, serve_static_files, trace, web};
|
use crate::{global, serve_static_files, trace, web};
|
||||||
|
|
@ -45,6 +46,8 @@ fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
|
||||||
|
|
||||||
// Comprueba si la extensión tiene un tema asociado que deba registrarse.
|
// Comprueba si la extensión tiene un tema asociado que deba registrarse.
|
||||||
if let Some(theme) = extension.theme() {
|
if let Some(theme) = extension.theme() {
|
||||||
|
check_theme_parent_chain(theme);
|
||||||
|
|
||||||
let mut registered_themes = THEMES.write();
|
let mut registered_themes = THEMES.write();
|
||||||
// Asegura que el tema no esté ya registrado para evitar duplicados.
|
// Asegura que el tema no esté ya registrado para evitar duplicados.
|
||||||
if !registered_themes
|
if !registered_themes
|
||||||
|
|
@ -60,6 +63,31 @@ fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recorre la cadena de `Theme::parent()` para detectar referencias circulares. `parent()` se
|
||||||
|
// resuelve en tiempo de ejecución, así que un ciclo no puede descartarse al compilar. Se rechaza el
|
||||||
|
// arranque al detectar uno, antes de provocar un bucle infinito (en `Theme::handle_component()`) o
|
||||||
|
// un desbordamiento de pila (en los métodos predefinidos de `Theme` que delegan recursivamente en
|
||||||
|
// el tema padre).
|
||||||
|
fn check_theme_parent_chain(theme: ThemeRef) {
|
||||||
|
let mut chain: Vec<ThemeRef> = vec![theme];
|
||||||
|
let mut current = theme;
|
||||||
|
while let Some(parent) = current.parent() {
|
||||||
|
if let Some(pos) = chain.iter().position(|t| t.type_id() == parent.type_id()) {
|
||||||
|
let cycle: Vec<&str> = chain[pos..]
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.short_name())
|
||||||
|
.chain(std::iter::once(parent.short_name()))
|
||||||
|
.collect();
|
||||||
|
panic!(
|
||||||
|
"Circular theme parent chain detected: {}",
|
||||||
|
cycle.join(" -> ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
chain.push(parent);
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// **< REGISTRO DE LAS ACCIONES >*******************************************************************
|
// **< REGISTRO DE LAS ACCIONES >*******************************************************************
|
||||||
|
|
||||||
pub fn register_actions() {
|
pub fn register_actions() {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@
|
||||||
//! de [`Theme::handle_component()`], las páginas de error, etc.). Un tema hijo puede ser a su vez
|
//! de [`Theme::handle_component()`], las páginas de error, etc.). Un tema hijo puede ser a su vez
|
||||||
//! padre de otro, basta declararlo cada vez con [`Theme::parent()`].
|
//! padre de otro, basta declararlo cada vez con [`Theme::parent()`].
|
||||||
//!
|
//!
|
||||||
|
//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación
|
||||||
|
//! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese
|
||||||
|
//! ciclo provocaría un bucle infinito en [`Theme::handle_component()`] o un desbordamiento de pila
|
||||||
|
//! en los métodos predefinidos de `Theme` que delegan recursivamente en el padre. Para evitarlo,
|
||||||
|
//! PageTop recorre la cadena de cada tema al registrarlo y **aborta el arranque de la aplicación**
|
||||||
|
//! si detecta una referencia circular.
|
||||||
|
//!
|
||||||
//! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio
|
//! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio
|
||||||
//! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa
|
//! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa
|
||||||
//! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico
|
//! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// no los sobrescribe.
|
/// no los sobrescribe.
|
||||||
///
|
///
|
||||||
/// La implementación por defecto devuelve `None` (tema sin padre).
|
/// La implementación por defecto devuelve `None` (tema sin padre).
|
||||||
|
///
|
||||||
|
/// Una referencia circular (un tema acaba siendo padre de sí mismo, directa o transitivamente)
|
||||||
|
/// no puede descartarse en tiempo de compilación. PageTop la detecta al registrar el tema y
|
||||||
|
/// aborta el arranque de la aplicación si encuentra una, para evitar bucles infinitos o
|
||||||
|
/// desbordamientos de pila al usar el tema.
|
||||||
fn parent(&self) -> Option<ThemeRef> {
|
fn parent(&self) -> Option<ThemeRef> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ intro_have_fun = Coding is creating
|
||||||
# PoweredBy component.
|
# PoweredBy component.
|
||||||
poweredby_pagetop = Powered by { $pagetop_link }
|
poweredby_pagetop = Powered by { $pagetop_link }
|
||||||
|
|
||||||
|
# Breadcrumb component.
|
||||||
|
breadcrumb_label = Breadcrumb navigation
|
||||||
|
|
||||||
# Pager component.
|
# Pager component.
|
||||||
pager_aria_label = Page navigation
|
pager_aria_label = Page navigation
|
||||||
pager_previous_label = Previous
|
pager_previous_label = Previous
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ intro_have_fun = Programar es crear
|
||||||
# PoweredBy component.
|
# PoweredBy component.
|
||||||
poweredby_pagetop = Funciona con { $pagetop_link }
|
poweredby_pagetop = Funciona con { $pagetop_link }
|
||||||
|
|
||||||
|
# Breadcrumb component.
|
||||||
|
breadcrumb_label = Ruta de navegación
|
||||||
|
|
||||||
# Pager component.
|
# Pager component.
|
||||||
pager_aria_label = Navegación de páginas
|
pager_aria_label = Navegación de páginas
|
||||||
pager_previous_label = Anterior
|
pager_previous_label = Anterior
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue