✨ (pagetop): Añade puntos de corte a Flex/FlexItem
- `Breakpoint` gana variante `Xxxl` y métodos `name()`/`min_width()`/ `resolved()`, para consultarse contra el tema activo. - `Theme::breakpoint_entry()` sustituye a `breakpoint_min_width()`: devuelve un `BreakpointEntry` con nombre y ancho mínimo. - Nuevo `Responsive<T>`, valor en cascada mobile-first por punto de corte. - Ejemplo `examples/intro-responsive.rs` con los patrones de uso.
This commit is contained in:
parent
8577ca8a59
commit
0b8f3f3000
22 changed files with 1257 additions and 359 deletions
|
|
@ -9,7 +9,7 @@ use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
|||
use crate::locale::Lc;
|
||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||
use crate::web::HttpRequest;
|
||||
use crate::{builder_impl, util};
|
||||
use crate::{CowStr, builder_impl, util};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use thiserror::Error;
|
||||
|
|
@ -42,7 +42,7 @@ pub enum AssetsOp {
|
|||
/// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas,
|
||||
/// dentro del punto de corte dado (`None` para una regla siempre activa). Ver
|
||||
/// [`ResponsiveStyles::add_style()`].
|
||||
AddResponsiveStyle(Option<Breakpoint>, &'static str, &'static str, &'static str),
|
||||
AddResponsiveStyle(Option<Breakpoint>, CowStr, CowStr, CowStr),
|
||||
}
|
||||
|
||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@
|
|||
//! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre,
|
||||
//! identificado por [`Theme::parent()`]. Un tema hijo hereda automáticamente todos los métodos del
|
||||
//! padre y puede sobrescribirlos selectivamente. Esta herencia determina qué implementación de sus
|
||||
//! métodos se usa cuando el tema hijo no los sobrescribe (ya sea el renderizado del `<body>` o del
|
||||
//! `<head>`, la definición de los recursos necesarios, la traducción de puntos de corte y colores
|
||||
//! por intención vía [`Theme::breakpoint_min_width()`] y [`Theme::intent_color()`], la captura de
|
||||
//! componentes para alterar su comportamiento usando [`Theme::setup_component()`] y
|
||||
//! [`Theme::render_component()`], las páginas de error, etc.).
|
||||
//! métodos se usa cuando el tema hijo no los sobrescribe, ya sea el renderizado del `<body>` o del
|
||||
//! `<head>`, la definición de los recursos necesarios, la traducción de puntos de corte con
|
||||
//! [`Theme::breakpoint_entry()`] y colores según intención vía [`Theme::intent_color()`], la
|
||||
//! captura de componentes para alterar su comportamiento usando [`Theme::setup_component()`] y
|
||||
//! [`Theme::render_component()`], las páginas de error, etc.
|
||||
//!
|
||||
//! Un tema hijo puede ser a su vez padre de otro, basta declararlo cada vez en [`Theme::parent()`].
|
||||
//! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación
|
||||
|
|
@ -78,13 +78,14 @@
|
|||
//! variantes de plantilla. Para ajustarlo sin rehacer su marcado (añadir una clase, un
|
||||
//! atributo, etc.), se usa [`Theme::setup_component()`] en su lugar.
|
||||
//! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo
|
||||
//! [`Theme::breakpoint_min_width()`]. Por defecto, [`Breakpoint`] resuelve el ancho mínimo de
|
||||
//! cada variante (`Sm`, `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`) que
|
||||
//! cada tema puede adaptar. Cuando se genera CSS *responsive* a partir de un [`Breakpoint`], se
|
||||
//! consulta el punto de corte a través de [`Breakpoint::min_width()`], listo para interpolar en
|
||||
//! un `@media (min-width: ...)` sin ningún cálculo adicional. Un tema sin diseño *responsive*
|
||||
//! puede traducir todas las variantes a `""` porque al ser *mobile-first*, un punto de corte sin
|
||||
//! ancho real se aplicará siempre.
|
||||
//! [`Theme::breakpoint_entry()`]. [`Breakpoint`] no define ningún ancho propio; la
|
||||
//! implementación por defecto de este método resuelve el ancho mínimo de cada variante (`Sm`,
|
||||
//! `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`), que cada tema puede
|
||||
//! sobrescribir. Cuando se genera CSS *responsive* a partir de un [`Breakpoint`], se consulta el
|
||||
//! punto de corte a través de [`Breakpoint::min_width()`], listo para interpolar en un
|
||||
//! `@media (min-width: ...)` sin ningún cálculo adicional. Un tema sin diseño *responsive* puede
|
||||
//! traducir todas las variantes a `""` porque al ser *mobile-first*, un punto de corte sin ancho
|
||||
//! real se aplicará siempre.
|
||||
//! 5. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo
|
||||
//! [`Theme::intent_color()`]. Por defecto, este método devuelve el vocabulario semántico de
|
||||
//! [`Intent`] (`"primary"`, `"severe"`, etc.); un tema con su propio catálogo de colores (por
|
||||
|
|
@ -155,7 +156,7 @@ mod intent;
|
|||
pub use intent::Intent;
|
||||
|
||||
mod breakpoint;
|
||||
pub use breakpoint::Breakpoint;
|
||||
pub use breakpoint::{Breakpoint, BreakpointEntry, Responsive};
|
||||
|
||||
mod layout;
|
||||
pub use layout::{CoreRegions, RegionName, RegionRef};
|
||||
|
|
|
|||
|
|
@ -3,46 +3,182 @@ use crate::core::component::{Context, Contextual};
|
|||
|
||||
// **< Breakpoint >*********************************************************************************
|
||||
|
||||
/// Puntos de corte *responsive*, *mobile-first* (aplican "a partir de" el ancho indicado).
|
||||
/// Puntos de corte *responsive*, *mobile-first* (se aplican a partir del ancho indicado).
|
||||
///
|
||||
/// No define ningún valor en píxeles por sí mismo; cada tema decide a qué ancho corresponde cada
|
||||
/// variante en su sistema de diseño (ver [`Theme::breakpoint_min_width()`]).
|
||||
/// `Breakpoint` no define ningún valor en píxeles por sí mismo; cada tema decide qué nombre y a qué
|
||||
/// ancho mínimo corresponde cada variante en su especificación (ver [`Theme::breakpoint_entry()`]).
|
||||
///
|
||||
/// [`Theme::breakpoint_min_width()`]: crate::core::theme::Theme::breakpoint_min_width
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
/// [`Theme::breakpoint_entry()`]: crate::core::theme::Theme::breakpoint_entry
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Breakpoint {
|
||||
/// Base *mobile-first*, equivale a "siempre".
|
||||
/// Base *mobile-first*, equivale a "siempre aplica".
|
||||
#[default]
|
||||
Xs,
|
||||
/// A partir del ancho donde un tema suele pasar de móvil a tableta.
|
||||
/// Aplica a partir del ancho donde un tema suele pasar de móvil a tableta.
|
||||
Sm,
|
||||
/// A partir del ancho donde un tema suele pasar a un escritorio pequeño.
|
||||
/// Aplica a partir del ancho donde un tema suele pasar a un escritorio pequeño.
|
||||
Md,
|
||||
/// A partir del ancho donde un tema suele pasar a un escritorio normal.
|
||||
/// Aplica a partir del ancho donde un tema suele pasar a un escritorio normal.
|
||||
Lg,
|
||||
/// A partir del ancho donde un tema suele considerar el escritorio ancho.
|
||||
/// Aplica a partir del ancho donde un tema suele considerar el escritorio ancho.
|
||||
Xl,
|
||||
/// A partir del ancho donde un tema suele considerar el escritorio muy ancho.
|
||||
/// Aplica a partir del ancho donde un tema suele considerar el escritorio muy ancho.
|
||||
Xxl,
|
||||
/// Aplica a partir del ancho donde un tema suele considerar el escritorio extra ancho.
|
||||
Xxxl,
|
||||
}
|
||||
|
||||
impl Breakpoint {
|
||||
// Todas las variantes, en orden mobile-first (de Xs a Xxl).
|
||||
pub(crate) const ALL: [Breakpoint; 6] = [
|
||||
// Todas las variantes, en orden mobile-first (de Xs a Xxxl).
|
||||
pub(crate) const ALL: [Breakpoint; 7] = [
|
||||
Breakpoint::Xs,
|
||||
Breakpoint::Sm,
|
||||
Breakpoint::Md,
|
||||
Breakpoint::Lg,
|
||||
Breakpoint::Xl,
|
||||
Breakpoint::Xxl,
|
||||
Breakpoint::Xxxl,
|
||||
];
|
||||
|
||||
/// Ancho mínimo resuelto a través del tema activo del contexto actual, como valor CSS ya
|
||||
/// formateado (p. ej. `"768px"`), o `""` si la variante se aplica siempre, sin ancho real.
|
||||
/// Nombre del punto de corte resuelto a través del tema activo del contexto actual.
|
||||
///
|
||||
/// Atajo de [`Theme::breakpoint_min_width()`](crate::core::theme::Theme::breakpoint_min_width)
|
||||
/// a través de [`Context::theme()`].
|
||||
/// Depende de [`Context`] y puede cambiar entre temas. Es un atajo de acceso al campo `name` de
|
||||
/// [`BreakpointEntry`] devuelto por [`Theme::breakpoint_entry()`] en el tema activo del
|
||||
/// contexto ([`Context::theme()`]).
|
||||
///
|
||||
/// [`Theme::breakpoint_entry()`]: crate::core::theme::Theme::breakpoint_entry
|
||||
#[inline]
|
||||
pub fn name(&self, cx: &Context) -> &'static str {
|
||||
cx.theme().breakpoint_entry(*self).name
|
||||
}
|
||||
|
||||
/// Ancho mínimo resuelto para el punto de corte a través del tema activo del contexto actual,
|
||||
/// como valor CSS ya formateado (p. ej. `"768px"`); o devuelve `""` si la variante se aplica
|
||||
/// siempre, sin un ancho real asociado.
|
||||
///
|
||||
/// Normalmente se usará este método, aunque realmente es un atajo de acceso al campo
|
||||
/// `min_width` de [`BreakpointEntry`] que devuelve [`Theme::breakpoint_entry()`] en el tema
|
||||
/// activo del contexto ([`Context::theme()`]).
|
||||
///
|
||||
/// [`Theme::breakpoint_entry()`]: crate::core::theme::Theme::breakpoint_entry
|
||||
#[inline]
|
||||
pub fn min_width(&self, cx: &Context) -> &'static str {
|
||||
cx.theme().breakpoint_min_width(*self)
|
||||
cx.theme().breakpoint_entry(*self).min_width
|
||||
}
|
||||
|
||||
/// Resuelve la variante en el tema activo del contexto ([`Context::theme()`]). Devuelve `None`
|
||||
/// si el valor de `min_width` está vacío por lo que no representa ningún ancho mínimo real para
|
||||
/// este tema. Devuelve el [`BreakpointEntry`] completo en caso contrario.
|
||||
///
|
||||
/// Permite decidir si una variante debe tratarse como incondicional (sin envolver en `@media`
|
||||
/// y sin sufijo de punto de corte en nombres de clase), o como un punto de corte real. Se
|
||||
/// devuelve el `BreakpointEntry` completo para poder obtener el [`name`](BreakpointEntry::name)
|
||||
/// y el [`min_width`](BreakpointEntry::min_width) para el tema activo, sin tener que volver a
|
||||
/// consultar el tema.
|
||||
#[inline]
|
||||
pub fn resolved(self, cx: &Context) -> Option<BreakpointEntry> {
|
||||
let entry = cx.theme().breakpoint_entry(self);
|
||||
if entry.min_width.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// **< Breakpoint HELPERS >*********************************************************************
|
||||
|
||||
// Posición de esta variante en `Breakpoint::ALL`, para indexar `Responsive::values`. Válido
|
||||
// porque el orden de declaración del enum coincide con `ALL` (de `Xs` a `Xxxl`).
|
||||
fn index(self) -> usize {
|
||||
self as usize
|
||||
}
|
||||
}
|
||||
|
||||
// **< BreakpointEntry >****************************************************************************
|
||||
|
||||
/// Punto de corte [`Breakpoint`] con su nombre y ancho mínimo *responsive* en un tema.
|
||||
///
|
||||
/// Ver [`Theme::breakpoint_entry()`](crate::core::theme::Theme::breakpoint_entry) para entender
|
||||
/// cómo definir los puntos de corte en la implementación de un tema dado.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct BreakpointEntry {
|
||||
/// Variante de la que procede esta entrada. Coincide siempre con el [`Breakpoint`] pasado a
|
||||
/// [`Theme::breakpoint_entry()`](crate::core::theme::Theme::breakpoint_entry), incluso si el
|
||||
/// tema delega en su padre. Se incluye para que el valor siga siendo identificable aunque se
|
||||
/// conozca de partida.
|
||||
pub breakpoint: Breakpoint,
|
||||
/// Nombre del punto de corte según el tema (p. ej. `"md"` o `"tablet"` podrían ser nombres para
|
||||
/// `Breakpoint::Md` en dos temas diferentes).
|
||||
pub name: &'static str,
|
||||
/// Ancho mínimo *responsive*, como valor CSS ya formateado, por ejemplo `"768px"`. Se usará una
|
||||
/// cadena vacía `""` si la variante se aplica siempre (para cualquier ancho).
|
||||
pub min_width: &'static str,
|
||||
}
|
||||
|
||||
// **< Responsive >*********************************************************************************
|
||||
|
||||
/// Encapsula valores para cada punto de corte, aplicados en cascada *mobile-first*.
|
||||
///
|
||||
/// Guarda un valor opcional para cada variante de [`Breakpoint`]. No decide por sí mismo cómo se
|
||||
/// interpreta cada punto de corte. Con [`by_breakpoint()`] se pueden devolver los valores
|
||||
/// establecidos para cada variante, sin resolver ningún ancho ni consultar el tema activo. Traducir
|
||||
/// eso a CSS, incluida la decisión de envolver en `@media` según [`Breakpoint::min_width()`], es
|
||||
/// 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.
|
||||
///
|
||||
/// [`by_breakpoint()`]: Self::by_breakpoint
|
||||
/// [`ResponsiveStyles`]: crate::html::ResponsiveStyles
|
||||
/// [`Flex`]: crate::html::Flex
|
||||
/// [`FlexItem`]: crate::html::FlexItem
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Responsive<T> {
|
||||
values: [Option<T>; 7],
|
||||
}
|
||||
|
||||
impl<T: Copy> Responsive<T> {
|
||||
// **< Responsive BUILDER >*********************************************************************
|
||||
|
||||
/// Establece el valor base (sin punto de corte, activo siempre).
|
||||
pub fn set(mut self, value: T) -> Self {
|
||||
self.values[0] = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor a partir del punto de corte indicado.
|
||||
pub fn set_at(mut self, bp: Breakpoint, value: T) -> Self {
|
||||
self.values[bp.index()] = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Combina con otro `Responsive<T>`, punto de corte a punto de corte. Donde `other` tenga un
|
||||
/// valor, sustituye al de `self`; donde no, se conserva el de `self`.
|
||||
pub fn merge(mut self, other: Self) -> Self {
|
||||
for (slot, value) in self.values.iter_mut().zip(other.values) {
|
||||
if value.is_some() {
|
||||
*slot = value;
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
// **< Responsive GETTERS >*********************************************************************
|
||||
|
||||
/// Devuelve el valor establecido para el punto de corte exacto indicado, si existe.
|
||||
pub fn get_at(&self, bp: Breakpoint) -> Option<T> {
|
||||
self.values[bp.index()]
|
||||
}
|
||||
|
||||
// **< Responsive HELPERS >*********************************************************************
|
||||
|
||||
/// Recorre los valores establecidos, en orden, como pares `(punto de corte, valor)`. Decidir si
|
||||
/// su ancho mínimo resuelto la hace incondicional, el caso por defecto, es responsabilidad de
|
||||
/// quien consuma este iterador, vía [`Breakpoint::resolved()`].
|
||||
pub fn by_breakpoint(&self) -> impl Iterator<Item = (Breakpoint, T)> + '_ {
|
||||
Breakpoint::ALL
|
||||
.iter()
|
||||
.zip(self.values.iter())
|
||||
.filter_map(|(bp, value)| value.map(|value| (*bp, value)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout};
|
|||
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
|
||||
use crate::core::component::{Context, Contextual};
|
||||
use crate::core::extension::Extension;
|
||||
use crate::core::theme::{Breakpoint, CoreRegions, Intent};
|
||||
use crate::core::theme::{Breakpoint, BreakpointEntry, CoreRegions, Intent};
|
||||
use crate::global;
|
||||
use crate::html::{Markup, html};
|
||||
use crate::locale::Lc;
|
||||
|
|
@ -65,31 +65,59 @@ pub trait Theme: Extension + Send + Sync {
|
|||
None
|
||||
}
|
||||
|
||||
/// Traduce un [`Breakpoint`] al punto de corte *responsive*, *mobile-first*, propio del tema.
|
||||
/// Traduce un [`Breakpoint`] a su [`BreakpointEntry`] correspondiente, donde se asocia a cada
|
||||
/// variante su nombre y ancho mínimo *responsive*, *mobile-first*, propios del tema.
|
||||
///
|
||||
/// `Breakpoint` no define ningún valor propio en píxeles. Será cada tema el que decida a qué
|
||||
/// ancho corresponde cada variante como valor CSS ya formateado (p. ej. `"768px"`), listo para
|
||||
/// aplicar en un `@media (min-width: ...)` sin ningún cálculo adicional. La cadena vacía (`""`)
|
||||
/// indica que la variante no representa ningún ancho mínimo y se aplica siempre; es el caso de
|
||||
/// `Xs`.
|
||||
/// `Breakpoint` no define ningún nombre ni ancho mínimo propios. Será cada tema el que decida
|
||||
/// cómo se llama cada variante y a qué ancho corresponde (p. ej. `"768px"`) para aplicar en un
|
||||
/// `@media (min-width: ...)` sin ningún cálculo adicional. La cadena vacía (`""`) en
|
||||
/// `min_width` indica que la variante no representa ningún ancho mínimo y se aplica siempre.
|
||||
///
|
||||
/// Normalmente, para resolver un ancho *responsive* no se llamará a este método directamente,
|
||||
/// sino que se usará [`Breakpoint::min_width()`] a través de [`Context::theme()`].
|
||||
/// **Temas sin puntos de corte.** Devolver `""` como ancho para una variante no la deshabilita,
|
||||
/// de hecho la regla generada para ese punto de corte se sigue renderizando, pero sin incluirla
|
||||
/// en un `@media` (ver [`ResponsiveStyles::render()`]). Por tanto, pasa a aplicarse siempre,
|
||||
/// exactamente igual que si nunca se hubiera pedido ningún punto de corte. Un tema sin diseño
|
||||
/// *responsive* puede traducir así todas las variantes a `""`; no por eso se vuelve un tema
|
||||
/// "desktop-first", sino que cada punto de corte pedido pasa a aplicarse siempre, sin ninguna
|
||||
/// condición de ancho.
|
||||
///
|
||||
/// **Temas con menos puntos de corte que variantes.** Dos variantes consecutivas que devuelvan
|
||||
/// el mismo ancho no vacío se funden en la práctica: ambas generan un `@media (min-width: ...)`
|
||||
/// idéntico, así que no hay forma de distinguir en CSS "a partir de `Md`" de "a partir de `Lg`"
|
||||
/// si las dos resuelven, por ejemplo, a `"992px"`. Es la forma correcta de implementar menos
|
||||
/// puntos de corte reales que las siete variantes de `Breakpoint`: repetir el mismo ancho en
|
||||
/// las variantes consecutivas que no se quieran distinguir. Por ejemplo, un tema con tres
|
||||
/// franjas reales (`Xs`, `Md`-`Lg` y `Xl`-`Xxl`-`Xxxl`) devolvería `""` para `Xs`, el mismo
|
||||
/// ancho para `Md` y `Lg`, y otro ancho mayor, también repetido, para `Xl`, `Xxl` y `Xxxl`.
|
||||
/// Para que el resultado siga siendo coherente, los anchos deben mantenerse no decrecientes en
|
||||
/// el orden *mobile-first* (`Xs` a `Xxxl`); repetir un ancho en variantes no consecutivas, o no
|
||||
/// ordenarlos de menor a mayor, produce puntos de corte confusos o contradictorios, aunque nada
|
||||
/// en tiempo de compilación ni de ejecución lo impida.
|
||||
///
|
||||
/// Normalmente, para resolver el nombre o el ancho *responsive* de un punto de corte no se
|
||||
/// llamará a este método directamente, sino que se usarán [`Breakpoint::name()`] y
|
||||
/// [`Breakpoint::min_width()`], respectivamente, a través de [`Context::theme()`].
|
||||
///
|
||||
/// [`Breakpoint::name()`]: crate::core::theme::Breakpoint::name
|
||||
/// [`Breakpoint::min_width()`]: crate::core::theme::Breakpoint::min_width
|
||||
/// [`Context::theme()`]: crate::core::component::Context::theme
|
||||
/// [`ResponsiveStyles::render()`]: crate::html::ResponsiveStyles::render
|
||||
#[rustfmt::skip]
|
||||
fn breakpoint_min_width(&self, bp: Breakpoint) -> &'static str {
|
||||
fn breakpoint_entry(&self, bp: Breakpoint) -> BreakpointEntry {
|
||||
if let Some(parent) = self.parent() {
|
||||
return parent.breakpoint_min_width(bp);
|
||||
return parent.breakpoint_entry(bp);
|
||||
}
|
||||
|
||||
use Breakpoint::*;
|
||||
|
||||
match bp {
|
||||
Breakpoint::Xs => "",
|
||||
Breakpoint::Sm => "576px",
|
||||
Breakpoint::Md => "768px",
|
||||
Breakpoint::Lg => "992px",
|
||||
Breakpoint::Xl => "1200px",
|
||||
Breakpoint::Xxl => "1400px",
|
||||
Xs => BreakpointEntry { breakpoint: Xs, name: "xs", min_width: "" },
|
||||
Sm => BreakpointEntry { breakpoint: Sm, name: "sm", min_width: "576px" },
|
||||
Md => BreakpointEntry { breakpoint: Md, name: "md", min_width: "768px" },
|
||||
Lg => BreakpointEntry { breakpoint: Lg, name: "lg", min_width: "992px" },
|
||||
Xl => BreakpointEntry { breakpoint: Xl, name: "xl", min_width: "1200px" },
|
||||
Xxl => BreakpointEntry { breakpoint: Xxl, name: "xxl", min_width: "1400px" },
|
||||
Xxxl => BreakpointEntry { breakpoint: Xxxl, name: "xxxl", min_width: "1920px" },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue