Compare commits

...
Sign in to create a new pull request.

96 commits

Author SHA1 Message Date
bb6855e41f (theme): Añade ColorName y CoreColors
Nuevo trait `ColorName` para que cada tema aporte su propia paleta de
colores; `CoreColors` es la paleta por defecto.

Aprovecha para refactorizar `core/theme.rs` en submódulos por concepto.
2026-08-20 21:54:12 +02:00
1453764462 🚨 (macros): Corrige warning de clippy en expand_entry 2026-08-17 18:26:38 +02:00
fe2b2c176f ♻️ (bootsier): Mueve Brand al core de PageTop 2026-08-17 05:53:55 +02:00
bf4c635e59 ♻️ (pagetop): Sustituye Attr<T> por Option<T>
`Attr<T>` no aportaba nada sobre `Option<T>` directo: `Getters` ya trata
los campos `Option<T>` como caso especial (`Option<&T>`, o `Option<T>`
con `#[getters(copy)]`) y `#[builder_fn]` funciona igual sobre ellos.
2026-08-17 05:28:12 +02:00
213aa18b12 ♻️ (locale): Sustituye Attr<Lc> por Lc::none() 2026-08-17 03:16:49 +02:00
c3ff8a6ff8 ♻️ (bootsier): Mueve Image al core de PageTop
`Image`, `Size` y `Source` pasan a `pagetop::base::component` como
componente genérico reutilizable por cualquier tema. Bootsier
sustituye las clases semánticas por las de Bootstrap (`img-fluid`,
`img-thumbnail`) interceptando `setup()`.
2026-08-17 00:55:35 +02:00
745d492d5b (html): Evita duplicar atributos de Props
`html!` recopila los atributos literales de cada elemento y evita que
`Props` los duplique al renderizar.
2026-08-17 00:02:20 +02:00
05187c9343 (macros): Evita duplicar splices de attr en html! 2026-08-16 17:03:08 +02:00
ed30b2ae17 (props): Añade ReplaceAllClasses
Sustituye clases sólo si están todas las indicadas en el primer valor; a
diferencia de `ReplaceClasses`, no actúa parcialmente si falta alguna.
Renombra `Props::has_class()`/`has_any_class()` a `has_all_classes()`/
`has_classes()`, que la nueva variante usa internamente para comprobar
la condición.
2026-08-16 13:58:47 +02:00
946d335664 (base): Añade componente Dropdown 2026-08-14 20:01:15 +02:00
e5431bc7d7 (base): Añade componente Messages
Muestra los mensajes acumulados en el Context por severidad.
2026-08-13 17:35:23 +02:00
51e8b11090 (theme): Añade clases de tabla al tema Basic 2026-08-13 17:12:06 +02:00
3930499664 🐛 (table): Añade conversión From<C> a Cell 2026-08-13 15:29:54 +02:00
5842ea6efc (base): Añade componente Table
Incluye columnas ordenables (`table::SortLink`), paso de props/atributos
a filas y celdas para HTMX, y un mensaje de "sin datos" traducible con
valor por defecto.
2026-08-13 15:17:23 +02:00
17a9f0c9e0 ♻️ (form): Añade #[builder_fn] a Item y Group
Uniforma el criterio con el resto de elementos de componente (`Row`/
`Cell`/`Column`/`SortLink` en table, `Crumb` en breadcrumb) generando su
`alter_...` equivalente para poder mutarse con `&mut self`.
2026-08-13 11:01:32 +02:00
aa8c4100b0 (base): Añade conversión de Lc a Child/ChildOp 2026-08-12 23:57:31 +02:00
be10dd28a1 ♻️ (locale): Renombra L10n a Lc
Simplifica el nombre del tipo de localización. Actualiza todas las
referencias en el crate principal, extensiones, ejemplos y tests.
2026-08-12 22:56:09 +02:00
3198b74399 🐛 (locale): Corrige escapado de L10n::using()
`L10n::using()` insertaba el texto de `L10n::n()` sin escapar,
tratándolo igual que una traducción de confianza (`l()`/`t()`); ahora se
escapa como cualquier otro valor interpolado con `html!`.

Además, `using()` no debe usarse en valores de atributo. Su marcado de
confianza para `l()`/`t()` podría romper el delimitador si la traducción
contuviera una comilla. Se sustituye por `lookup()`
2026-08-12 14:28:47 +02:00
6bea4a5793 (base): Añade componente Breadcrumb 2026-08-11 00:46:45 +02:00
20b6c77c9e 🐛 (base): Exporta PagerAlign y corrige aria-label 2026-08-10 13:19:59 +02:00
8cb5e035dc (theme): Evita referencias cíclicas de parent()
Como `Theme::parent()` se resuelve en tiempo de ejecución, una
referencia cíclica (en la que un tema acaba siendo padre de sí mismo,
directa o transitivamente) no puede descartarse en compilación. Se
detecta al registrar el tema y aborta el arranque si procede.
2026-08-09 18:45:17 +02:00
825283caa3 ♻️ (base): El resumen de Pager usa PagerVisibility
Sustituye el bool de `summary` por `PagerVisibility`, con `Auto` como
valor por defecto: se activa igual que los botones de navegación y el
formulario de salto, sólo cuando el listado se trunca.
2026-08-07 23:47:14 +02:00
ee57d6cacf (base): Añade resumen de páginas en Pager 2026-08-07 19:03:59 +02:00
12c14afbf6 🐛 (base): Correcciones de visualización de Pager
Ajusta el cálculo de la ventana de páginas cerca de los extremos,
convierte la elipsis en una celda `.page-link` real (antes texto suelto
sin borde/fondo), separa el texto visible del aria-label accesible en
los botones anterior/siguiente, añade `with_align()` y ajusta el ancho
del campo de salto al número de dígitos.
2026-08-07 15:28:58 +02:00
3b01894024 (base): Añade componente Pager
Incluye soporte HTMX vía `sort_link()`/`hx_pager` para navegación sin
recarga.
2026-08-06 22:37:28 +02:00
e8bba0803d (form): Añade nuevo componente Number 2026-08-05 17:33:27 +02:00
502c00c891 🎨 Colapsa if anidados sugeridos por clippy 2026-08-05 13:07:35 +02:00
1cab0ae9a0 (theme): Añade región Aside
Introduce `CoreRegions::Aside` como nueva región de plantilla, entre
Header y Content, y Template ahora envuelve el cuerpo en `div.wrapper`,
omitiendo el renderizado si no hay contenido.

`CoreRegion`, `ReservedRegion` y `CoreTemplate` pasan a plural
(`CoreRegions`, `ReservedRegions`, `CoreTemplates`) para no confundirlos
con otras definiciones, y se amplía la documentación del módulo theme.
2026-08-02 10:40:36 +02:00
c49bf8f56a (form): Blinda Field contra el autorrelleno
Añade nuevas variantes `Kind::StrictText` y `StrictPassword`
(`Field::strict_text()` y `strict_password()`) para forzar en el campo
`autocomplete=off`, desactivar spellcheck y autocorrect, aplicar
`readonly+onfocus`, y enmascarar las contraseñas vía CSS en vez de
`type="password"`. Replicado en pagetop-bootsier.
2026-08-02 08:43:32 +02:00
545a87fbb0 ♻️ (bootsier): Ajusta componentes base
- Añade `#[builder_fn]` a `with_width()`/`with_floating_label()`.
- `Rounded::new()` pasa a aplicar el radio por defecto
  (`Rounded::default()` queda sin redondeo).
- `Form::action` usa `Route` en lugar de `AttrValue`, y `check::Item`
  expone `value` en vez de `name` para generar casillas con `name`
  compartido valores distintos.
2026-08-01 13:41:19 +02:00
4cc72aac0e (base): Añade componente Badge
Incluye soporte específico en Bootsier (`BadgeBootsier`) para fijar el
color de fondo con contraste de texto garantizado.
2026-08-01 12:15:00 +02:00
3e83f8e35e 🐛 (locale): Preserva grafía original de "?lang="
Antes, RequestLocale sólo guardaba el LanguageIdentifier ya resuelto,
así que `Context::route()` propagaba siempre la forma canónica en los
enlaces generados ("?lang=en-US"), aunque el usuario hubiera pedido
"?lang=en". Ahora se conserva el valor original de la query junto al
idioma resuelto, y se reutiliza al construir nuevos enlaces mientras
siga siendo válido.

El idioma efectivo para renderizar la petición no cambia: sigue siendo
siempre el LanguageIdentifier canónico soportado por la aplicación.
2026-08-01 08:05:48 +02:00
fdd2dca280 ♻️ (bootsier): Reorganiza clases y tokens utilitarios 2026-08-01 07:15:13 +02:00
1a3a295f18 (htmx): Uso de Context::default() por ::new(None) 2026-07-28 19:09:44 +02:00
c91b1cd35c (seaorm): Añade flatten_txn_err()
Convierte `TransactionError<E>` a `E` para propagar el resultado de una
transacción con `?` sin distinguir el origen del error.
2026-07-28 19:02:12 +02:00
8ddc26dae1 📝 (theme): Aclara alcance de la herencia de temas 2026-07-28 09:19:50 +02:00
dc2ed27da4 ♻️ (util): Extrae non_blank() y normalize_token() 2026-07-28 01:00:14 +02:00
747e64ce34 (auth): Añade permisos tipados
Sustituye las claves de permiso `&str` sueltas por el trait `Permission`
(clave, etiqueta y grupo), y añade `require_permission()` para cortar un
handler con `ErrorPage::AccessDenied` antes de construir nada.

Para que `require_permission()` pueda fallar sin un `Context` previo,
`ErrorPage` pasa a envolver `Option<HttpRequest>`, `Context::new()`
recibe ahora `HttpRequest` sin `Option`, y se añade `Context::admin()`
para la plantilla de administración.
2026-07-27 22:46:41 +02:00
ea0dc021ce (seaorm): Añade Paginated<T> y paginate()
Centraliza la paginación genérica junto con `Paginated::map_items()`
para enriquecer los elementos de una página con datos de otra tabla sin
perder la metadata de paginación.
2026-07-26 21:30:28 +02:00
9db1d4bab0 (htmx): Añade soporte OOB a HtmxResponse
Nuevo método `oob()` para incluir fragmentos adicionales marcados con
`hx-swap-oob` en el cuerpo de la respuesta, sin necesidad de construir
el envoltorio a mano ni depender del `hx-target` de la petición.

De paso, `set_header()` usa ya las constantes de `hx::response` en vez
de literales repetidos, y se revisan comentarios.
2026-07-26 17:55:25 +02:00
74dbcde5c5 ♻️ (core): Cambia HashMap x Vec en RegionComponents 2026-07-26 13:41:32 +02:00
7e6a9f8557 ♻️ (core): Clave &'static str en RegionComponents 2026-07-26 12:50:33 +02:00
845d91d8c1 ♻️ (core): Evita doble clonado de InRegion en Child
Los prototipos registrados con `InRegion` se clonaban dos veces por
petición: una al ensamblar la región (`ComponentGlobal::as_child`) y
otra al renderizar (`Child::render`). Ahora sólo se clonan al
renderizar.
2026-07-26 12:39:01 +02:00
ce6b4d2581 ♻️ (core): Simplifica Child a Arc<dyn Component>
Elimina el `RwLock<Box<dyn Component>>` interno ya que ningún punto del
ciclo de renderizado tomaba el write-lock.

De paso, `From<Embed<C>> for Child` pasa a una coerción de puntero sin
clonar, gracias al copy-on-write que ya ofrece `Embed::get_mut()`. Los
doctests en `rust,ignore` pasan a `rust,no_run`, verificados por el
compilador.
2026-07-26 11:30:16 +02:00
65c8a788c2 (web): Añade cabeceras HTTP a TestRequest 2026-07-26 10:03:54 +02:00
39f494539e ♻️ (htmx): Expone request y response como públicos 2026-07-26 10:02:36 +02:00
b9d9cdf601 🚧 Ajustes menores en rutas y localización
- Añade `Route::try_resolve()` y `RoutePath::is_empty()` para omitir
  atributos HTML opcionales cuando la ruta resultante está vacía.
- Renombra `Locale::configured_langid()` a `try_langid()` (convención
  `try_*` para Option).
- Reordena include_locales! y otros cambios menores.
2026-07-26 09:55:27 +02:00
8573aca29e (theme): Añade componentes Region y Template
Introduce Region y Template (`base::component::layout`) para componer el
`<body>` de una página, con captura explícita vía `handle_component()`
en lugar de maquetado implícito:

- `RegionName`/`TemplateName` (`core/theme.rs`) como interfaces de
  identidad; `CoreRegion` (`Header`/`Content`/`Footer`) y `CoreTemplate`
  (`Standard`/`Admin`) como catálogo por defecto.
- `ReservedRegion` (`PageTop`/`PageBottom`, en `response/page.rs`) queda
  fuera de `CoreRegion`. `Page::render()` las renderiza siempre,
  envolviendo `Theme::render_page_body()`, con independencia de la
  plantilla o el tema activos.
- Elimina `TemplateSource` en `Context`: la plantilla pasa a ser un
  `TemplateRef` fijo, sin resolución por tema (ese mecanismo nunca llegó
  a implementarse).
- La personalización por tema es por captura (`handle_component()` +
  `downcast_ref()`), no por sustituir qué constante se resuelve.
- Corrige referencias obsoletas a `DefaultRegions` por `CoreRegion`.
- Sustituye `tests/theme_template.rs` (API anterior) por
  `tests/component_template.rs`, que cubre el patrón de captura con el
  API nuevo.
2026-07-26 09:51:27 +02:00
e7f2563967 (htmx): Añade soporte HTMX a tablas ordenables
- Nuevo `SortDir` en `pagetop::html` para representar direcciones de
  orden (asc/desc) y calcular la siguiente al pulsar una cabecera.
- `hx_table::sort_link()` construye el enlace de ordenación con los
  cuatro atributos `hx-*` fijos, reutilizable en cualquier tabla.
- `HtmxResponse` usa `RoutePath` en `location`/`push_url`/`replace_url`/
  `redirect` para preservar "lang"; `location_json()` se separa de
  `location()` para el caso de configuración JSON personalizada.
- Añade el módulo `prelude` y una batería de tests para hx, hx_table,
  request, response y extension.
2026-07-23 07:21:27 +02:00
55159f6d8f 🌐 (tests): Traduce comentarios de tests a inglés 2026-07-19 23:34:38 +02:00
af548b03c9 (core): Añade Route, RoutePath y Waypoint
`Route` resuelve URLs sensibles al `Context`/idioma en propiedades de
componentes; `RoutePath` (renombrado desde html/route.rs) es el valor
concreto de ruta + query. `Waypoint` transporta la URL de retorno entre
pantallas de listado/alta/edición vía query string.

Ajusta `Context::route()`, `Redirect` y los componentes que aceptan
rutas para integrarse con el nuevo tipo `Route`.
2026-07-19 22:16:21 +02:00
6f82da220b 🐛 (web): HttpRequest clona ahora las extensiones
Documenta el nuevo comportamiento y aprovecha para pulir varios
comentarios y restringir visibilidad de símbolos internos.
2026-07-17 00:54:35 +02:00
370bc8ee47 ♻️ (seaorm): Renombra el re-export api a sea_orm
Amplía también los tipos reexportados (Condition, DbBackend, NotSet,
Order, Set, TransactionError, Unchanged) y habilita el mismo
re-export en migration.rs.
2026-07-14 18:37:43 +02:00
e6b4661f21 (web): Añade Form, RawForm, Request y el módulo middleware
Reexporta los extractores axum::extract::{Form, RawForm, Request} y
el módulo axum::middleware (from_fn, Next) desde pagetop::web.
2026-07-14 18:37:36 +02:00
5a8267032e 📝 Retoques menores en documentación
Quita las itálicas superfluas en "handlers" en pagetop-htmx y en el
doc de módulo de web.rs.
2026-07-14 18:37:10 +02:00
f6f78fddbe 🐛 (aliner): Corrige DefaultRegions en el pie 2026-07-14 18:31:16 +02:00
7a3419415e (datetime): Reexporta chrono::Duration 2026-07-14 18:26:52 +02:00
82e64238bd 🔥 (config): Elimina la opción app.description 2026-07-14 18:20:01 +02:00
eb3157c4a4 📝 Retoques menores en documentación 2026-07-14 07:27:51 +02:00
30151b6254 ♻️ (macros): Deja main/test sin depender de Tokio
Antes delegaban directamente en `#[tokio::main]`/`#[tokio::test]`; ahora
construyen su propio runtime vía `pagetop::util::build_runtime()`, con
el mismo comportamiento (`Builder::new_multi_thread().enable_all()`).
Las extensiones que usan estas macros ya no necesitan declarar Tokio
como dependencia directa en su `Cargo.toml`.
2026-07-14 00:44:21 +02:00
24bd13414c Añade gestión de errores catastróficos
- Los errores controlados (403, 404, fatales) se renderizan con el tema
  activo; un `panic!` no controlado se intercepta con un HTML mínimo y
  autónomo, sin depender del tema ni de los componentes.
- Corrige además que `Context::template()` no seguía los cambios de tema
  hechos con `with_theme()` después de crear la página.
- Renombra `DefaultRegion(s)`/`DefaultTemplate(s)`, añade
  `Theme::admin_template()` y `Page::admin()`, y documenta la creación
  de temas e `InRegion` en `core/theme.rs`.
2026-07-13 00:38:11 +02:00
0e72d51276 📝 Quita cursivas de algunos anglicismos en docs 2026-07-10 19:33:26 +02:00
771f61bf29 ♻️ (bootsier): Mueve Container al core de PageTop
`Container` y `Kind` pasan a `pagetop::base::component` como componente
genérico reutilizable por cualquier tema. Bootsier conserva `Width` y
añade el *trait* `ContainerBootsier`, que guarda el ancho como valor
extra en `Props`.
2026-07-09 21:44:52 +02:00
654618aa68 (html): Añade declaración de estilos a PropsOp 2026-07-09 21:40:50 +02:00
87ad78778d ♻️ (bootsier): Mueve input/select/textarea a theme
Consolida los handlers de estos campos en la capa del tema. Además,
`with_floating_label()` usa un valor extra que lee en `setup()` para
forzar `multiple`/`rows` en 'select` y `textarea`.
2026-07-09 01:26:52 +02:00
bff7cf1b38 (html): Añade PropsOp::replace_classes()
Permite sustituir una o varias clases por otras nuevas en una sola
operación, preservando su posición. Adapta el tema Bootsier para usarla
en `Button` y elimina el handler duplicado sin referencias.
2026-07-09 01:12:45 +02:00
9e8cd77140 📝 Retoques de documentación en html y componentes 2026-07-09 00:08:04 +02:00
25713a8a13 ♻️ (html): Añade valores extra a Props
- Reordena campos, variantes y métodos para mayor consistencia interna.
- Añade `extra_or`, `extra_or_default` y `extra_or_else` a `Props`.
- Mejora la documentación y amplía los tests de integración.
2026-07-09 00:05:28 +02:00
a26aa4d30a 💄 (intro): Reserva espacio vertical para *badges* 2026-07-07 15:32:30 +02:00
c7b680a7f7 (html): Añade Preload para precarga de recursos
Nuevo tipo `Preload` (junto a `StyleSheet` y `JavaScript`) que emite
`<link rel="preload">` con soporte para fuentes, estilos, scripts e
imágenes (WebP y AVIF incluidos). Bootsier lo usa para precargar las
fuentes Source Sans 3 y eliminar el FOUT en la carga inicial.
2026-07-07 00:03:36 +02:00
9b2d430d8b ♻️ (seaorm): Adapta al arranque async
- Elimina `run_now` y convierte `initialize()` en `async fn`.
- `DBCONN` pasa de `LazyLock` a `OnceLock`; `MigratorBase` adopta
  `#[pagetop::async_trait]` con métodos async nativos.
2026-07-06 19:40:54 +02:00
1139df6210 ♻️ (bootsier, htmx): Adapta ciclo de render async
Añade `#[async_trait]` a todos los `impl Extension`, `Theme` y
`Component`. Convierte `prepare()` en `async fn` y añade `.await` a las
llamadas `render()` y `render_offcanvas()`.
2026-07-06 07:55:30 +02:00
9354894b3a 🎨 Convierte el ciclo de renderizado en async
`prepare()` de `Component`, `render()` de `Region` y `Template`, y la
implementación de `ComponentRender` pasan a ser funciones async. Se
actualizan todos los componentes base, helpers, tests y ejemplos.
2026-07-06 00:48:41 +02:00
9acd8cc51a 🐛 Corrige orden de despacho en ActionsList
Las acciones con menor peso deben despacharse primero; `.rev()` lo
invertía. Eliminado de `for_each` y `try_for_each`.
2026-07-04 22:59:56 +02:00
aa8b109668 🐛 Corrige orden de extractores en handlers
`HttpRequest` vacía `parts.extensions` al extraerse; `Path<T>` y otros
extractores que dependen de ellas deben declararse antes en el handler.
Corregido en el ejemplo hello-name y documentado en HttpRequest.
2026-07-04 22:58:45 +02:00
8aa4372bdf ♻️ Convierte initialize() y Application en async
`async_trait` se re-exporta desde `pagetop` y se añade al preludio; ya
no es necesario declararlo como dependencia en las extensiones.
`initialize()` es ahora verdaderamente `async` (con `.await`), y
`Application::new()` y `prepare()` pasan también a ser funciones
`async`.
2026-07-04 22:56:19 +02:00
54d5b3e53d 📝 (seaorm): Documenta selección del motor de BD
Diferencia el patrón para aplicaciones (activan el motor via *feature*
en su `Cargo.toml`) y extensiones (sin *features*; la selección es
responsabilidad de la aplicación).
2026-07-02 22:08:28 +02:00
0e96bcce64 (auth): Añade CurrentUser y sistema de permisos
- Nuevo módulo `auth` que exporta `CurrentUser`, `CheckPermission` y
  `has_permission()`.
- `Context` y `Page` exponen `current_user()` vía `Contextual`.
- `HttpRequest` accede a extensiones de middleware con `extension<T>()`.
- `Extension` incluye nuevo método `configure_middleware`.
- `try_dispatch_actions` para despachar acciones con control del flujo.
2026-07-02 20:52:13 +02:00
3120fd9a6f 🚧 Expone as_str de AutofillField 2026-06-29 20:58:42 +02:00
ee6457acf6 🔧 Adapta ejemplos a la nueva estructura de bs:: 2026-06-29 20:41:06 +02:00
f37db56ec5 ♻️ (bootsier): Reorganiza en capas token/class/bs
Sustituye `attrs/` y `classes/` por tres capas diferenciadas:
- `token/`: primitivas CSS (BreakPoint, Color, ScaleSize, etc.).
- `class/`: tipos compuestos (Border, Background, Margin, etc.).
- `bs/*/props.rs`: props tipadas para cada componente de Bootstrap.

El patrón de construcción es `suffix()` -> `push_to()` -> `to_class()`.
`push_to()` es `pub` en `class/` y `bs/*/props.rs` para que los temas
derivados puedan acceder a los mismos mecanismos de construcción.
2026-06-29 20:32:20 +02:00
64db114eb0 🐛 (bootsier): Corrige ambigüedad en doctests 2026-06-23 07:33:52 +02:00
9435678e01 ♻️ (form): Mueve componentes de formulario a base 2026-06-22 02:12:09 +02:00
26f1cda831 Permite a plantillas cambiar atributos de <body> 2026-06-20 20:31:51 +02:00
d495c05b96 ♻️ (bootsier): Reorganiza activos estáticos
Compila SCSS, copia JS y fuentes desde `assets/` con prefijo bootsier.
2026-06-20 19:52:21 +02:00
dbf0894894 (bootsier): Añade ajuste dev.bootsier_static_dir
Permite servir `css`, `js` y `fonts` desde disco mientras la aplicación
está en ejecución, sin necesidad de recompilar.
2026-06-20 19:47:19 +02:00
2202a2350c ♻️ (bootsier): Usa nuevo Props en Navbar/Offcanvas 2026-06-20 16:26:20 +02:00
62219584b0 ♻️ (html): API para id's en Props y componentes 2026-06-20 15:02:23 +02:00
8d0103c257 ♻️ (bootsier): Adapta componentes a nueva API
Sustituye `Classes`/`ClassesOp` por `Props`/`PropsOp` en todos los
componentes: campo, *builder* `with_prop()`, `setup()` y `prepare()`.
2026-06-14 23:07:57 +02:00
1bd97d5705 (bootsier): Añade AdminLTE 4 y Bootstrap Icons
Integra AdminLTE 4.0.0 (SCSS y JS), Bootstrap Icons 1.13.1 y las fuentes
Source Sans 3 VF; reorganiza los SCSS del tema con la nueva estructura
de *pagetop-build*.
2026-06-14 21:25:52 +02:00
f27790c3a2 (build): Introduce nuevas funciones de build
Añade `compile_scss()`, `copy_dir()`, `copy_file()`,
`copy_file_replacing()` y `minify_js()` para preparar activos en
`build.rs`. Adopta el patrón `assets/ -> static/`: los archivos
estáticos se mueven a `assets/` y `static/` se añade a `.gitignore`. Los
`build.rs` de *pagetop* y *pagetop-htmx* se actualizan con el nuevo
patrón.  La documentación del módulo se reescribe para reflejar los
nuevos cambios.
2026-06-14 21:18:54 +02:00
6376e3e88c 📝 Ajusta formato de comentarios 2026-06-13 19:16:34 +02:00
38fd24453e (htmx): Añade integración con HTMX 2
Constantes `hx-*`, `HtmxRequestExt` y `HtmxResponse` cubren el ciclo
completo: escribir atributos, leer la petición y construir la respuesta.
La extensión Htmx inyecta el script automáticamente.

Añade `IntoResponse` y `Response` al prelude de PageTop.
2026-06-13 18:41:15 +02:00
511149caa7 📝 Añade no_run a doctests ilustrativos 2026-06-13 00:31:36 +02:00
41c4379bc3 📝 Limpia y corrige documentación de extensiones 2026-06-12 21:29:10 +02:00
f9e87058d8 ♻️ (html): Migra Classes/ClassesOp a Props/PropsOp
Introduce `Props`/`PropsOp` para gestionar pares `atributo="valor"` y
clases CSS para aplicar en componentes.

- Constructores `Props::new()`, `Props::classes()` y `Props::default()`.
- `Page.body_classes` reemplazado por `body_props` (permite atributos
  arbitrarios en `<body>`, no sólo clases).
- Tests nuevos para atributos y reescritos para clases.
2026-06-12 01:55:07 +02:00
419 changed files with 32066 additions and 7101 deletions

3
.gitignore vendored
View file

@ -1,6 +1,9 @@
# Ignora directorios de compilación
**/target
# Ignora directorios de archivos estáticos
**/static
# Archivos de log
**/log/*.log*

58
Cargo.lock generated
View file

@ -20,6 +20,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@ -933,7 +942,7 @@ version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"aho-corasick 1.1.4",
"bstr",
"log",
"regex-automata",
@ -965,6 +974,16 @@ dependencies = [
"rand",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
"bumpalo",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -1495,6 +1514,17 @@ dependencies = [
"unicase",
]
[[package]]
name = "minify-js"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1fa5546ee8bd66024113e506cabe4230e76635a094c06ea2051b66021dda92e"
dependencies = [
"aho-corasick 0.7.20",
"lazy_static",
"parse-js",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@ -1721,7 +1751,6 @@ version = "0.1.0"
dependencies = [
"pagetop",
"pagetop-build",
"tokio",
]
[[package]]
@ -1731,7 +1760,6 @@ dependencies = [
"pagetop",
"pagetop-build",
"serde",
"tokio",
]
[[package]]
@ -1739,9 +1767,18 @@ name = "pagetop-build"
version = "0.3.2"
dependencies = [
"grass",
"minify-js",
"pagetop-statics",
]
[[package]]
name = "pagetop-htmx"
version = "0.1.0"
dependencies = [
"pagetop",
"pagetop-build",
]
[[package]]
name = "pagetop-macros"
version = "0.3.0"
@ -1812,6 +1849,19 @@ dependencies = [
"windows-link",
]
[[package]]
name = "parse-js"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2742b5e32dcb5930447ed9f9e401a7dfd883867fc079c4fac44ae8ba3593710e"
dependencies = [
"aho-corasick 0.7.20",
"bumpalo",
"hashbrown 0.13.2",
"lazy_static",
"memchr",
]
[[package]]
name = "pastey"
version = "0.2.2"
@ -2094,7 +2144,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"aho-corasick 1.1.4",
"memchr",
"regex-syntax",
]

View file

@ -9,6 +9,7 @@ members = [
# Extensions
"extensions/pagetop-aliner",
"extensions/pagetop-bootsier",
"extensions/pagetop-htmx",
"extensions/pagetop-seaorm",
]
@ -35,6 +36,7 @@ indexmap = "2.14"
indoc = "2.0"
itoa = "1.0"
mime_guess = "2.0"
minify-js = "0.6"
parking_lot = "0.12"
pastey = "0.2"
path-slash = "0.2"
@ -62,6 +64,7 @@ pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
# Extensions
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
pagetop-htmx = { version = "0.1", path = "extensions/pagetop-htmx" }
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
# PageTop
pagetop = { version = "0.5", path = "." }

View file

@ -1,6 +1,6 @@
<div align="center">
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/banner.png" />
<h1>PageTop</h1>
@ -11,9 +11,8 @@
[![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop#licencia)
</div>
<br>
</div>
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
@ -29,8 +28,7 @@ según las necesidades de cada proyecto, incluyendo:
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
componentes.
## ⚡️ Guía rápida
## Guía rápida
La aplicación más sencilla de PageTop se ve así:
@ -39,43 +37,43 @@ use pagetop::prelude::*;
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::new().run()?.await
Application::new().await.run().await
}
```
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
Para personalizar el servicio, puedes crear una extensión de PageTop de la siguiente manera:
```rust,no_run
use pagetop::prelude::*;
struct HelloWorld;
#[async_trait]
impl Extension for HelloWorld {
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
scfg.route("/", service::web::get().to(hello_world));
fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(hello_world))
}
}
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request)
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render()
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render().await
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await
Application::prepare(&HelloWorld).await.run().await
}
```
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
(`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`.
## 📂 Proyecto
## Proyecto
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
@ -113,8 +111,7 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
## 🧪 Pruebas
## Pruebas
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
@ -134,15 +131,13 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
> `cargo test`.
## 🚧 Advertencia
## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
## 📜 Licencia
## Licencia
El código está disponible bajo una doble licencia:
@ -155,8 +150,7 @@ El código está disponible bajo una doble licencia:
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
el ecosistema Rust.
## ✨ Contribuir
## Contribuir
PageTop mantiene **un único repositorio oficial**:

View file

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Before After
Before After

520
assets/css/basic.css Normal file
View file

@ -0,0 +1,520 @@
:root {
/* Font families */
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif;
--val-font-serif: Georgia,"Times New Roman",serif;
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
--val-font-family: var(--val-font-sans);
/* Font size */
--val-fs--base: 1rem;
/* Font weight */
--val-fw--base: 400;
/* Line height */
--val-lh--base: 1.5;
/* Core Colors */
--val-color--primary: #0d6efd;
--val-color--secondary: #6c757d;
--val-color--success: #198754;
--val-color--info: #0dcaf0;
--val-color--warning: #ffc107;
--val-color--danger: #dc3545;
/* Other Colors */
--val-color--bg: #fafafa;
--val-color--bg--muted: #dee2e6;
--val-color--text: #212529;
--val-color--text--muted: color-mix(in srgb, var(--val-color--text) 50%, var(--val-color--bg));
--val-color--switch-off: #adb5bd;
--val-color--border: #212529;
}
*, *::before, *::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--val-font-family);
font-size: var(--val-fs--base);
font-weight: var(--val-fw--base);
line-height: var(--val-lh--base);
color: var(--val-color--text);
background-color: var(--val-color--bg);
-webkit-tap-highlight-color: transparent;
}
/*
* Form components
*/
.form-required {
color: var(--val-color--danger);
margin-left: 0.25rem;
}
.form-label {
display: block;
margin-top: 1em;
}
.form-text {
font-size: 0.9375rem;
color: var(--val-color--text--muted);
}
.form-field > select.form-select,
.form-field > input.form-control,
.form-field > input.form-range,
.form-field > textarea.form-control {
display: block;
padding: 0.25rem 0.5rem;
width: 100%;
}
fieldset {
margin: 2rem 0 1rem;
background: var(--val-color--bg);
}
fieldset > legend {
background-color: var(--val-color--bg);
border: 2px groove threedface;
padding: 0 0.5em;
}
.form-check {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 1.5rem;
margin-bottom: 0.25rem;
}
.form-check-inline {
display: inline-flex;
margin-right: 1rem;
}
.form-check-reverse {
flex-direction: row-reverse;
justify-content: flex-start;
text-align: right;
}
.form-check-input {
flex-shrink: 0;
cursor: pointer;
}
.form-check-label {
cursor: pointer;
}
.form-switch .form-check-input {
appearance: none;
-webkit-appearance: none;
width: 2em;
height: 1em;
background-color: var(--val-color--switch-off);
border-radius: 1em;
position: relative;
cursor: pointer;
transition: background-color .15s ease-in-out;
}
.form-switch .form-check-input::after {
content: "";
position: absolute;
width: 1em;
height: 1em;
background-color: white;
border-radius: 50%;
top: 0;
left: 0;
transition: transform .15s ease-in-out;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .25);
}
.form-switch .form-check-input:checked {
background-color: var(--val-color--primary);
}
.form-switch .form-check-input:checked::after {
transform: translateX(1em);
}
input:disabled,
input:disabled + label {
cursor: default !important;
}
input:disabled + label {
color: var(--val-color--text--muted);
}
/*
* Dropdown component
*/
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-button,
.dropdown-toggle {
padding: 0.5rem 0.75rem;
font: inherit;
color: var(--val-color--text);
background-color: transparent;
border: 1px solid var(--val-color--border);
border-radius: 0.375rem;
cursor: pointer;
}
.dropdown-toggle::after {
content: "";
display: inline-block;
width: 0.4em;
height: 0.4em;
margin-left: 0.35em;
border-right: 0.125em solid currentColor;
border-bottom: 0.125em solid currentColor;
transform: rotate(45deg);
transition: transform .15s ease-in-out;
}
.dropdown:has(> .dropdown-menu.show) > .dropdown-toggle::after {
transform: rotate(-135deg);
}
.dropdown-menu {
position: absolute;
z-index: 1000;
top: 100%;
left: 0;
min-width: 12rem;
margin: 0.25rem 0 0;
padding: 0.5rem 0;
list-style: none;
background-color: var(--val-color--bg);
border: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.dropdown-menu.hide {
display: none;
}
.dropdown-item {
display: block;
width: 100%;
padding: 0.375rem 1rem;
text-align: left;
color: var(--val-color--text);
text-decoration: none;
background-color: transparent;
border: none;
font: inherit;
cursor: pointer;
}
.dropdown-item:hover {
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
}
.dropdown-item.active {
color: var(--val-color--primary);
font-weight: 600;
}
.dropdown-item.disabled {
color: var(--val-color--text--muted);
pointer-events: none;
}
.dropdown-item-text {
display: block;
padding: 0.375rem 1rem;
color: var(--val-color--text--muted);
}
.dropdown-header {
display: block;
padding: 0.5rem 1rem 0.25rem;
margin: 0;
font-size: 0.8125rem;
font-weight: 600;
color: var(--val-color--text--muted);
}
.dropdown-divider {
height: 0;
margin: 0.5rem 0;
border: none;
border-top: 1px solid var(--val-color--border);
}
/* Utility: visually hidden but still accessible to assistive technology */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/*
* Messages component
*/
.message {
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.375rem;
}
.message-info {
color: color-mix(in srgb, var(--val-color--info) 40%, black);
background-color: color-mix(in srgb, var(--val-color--info) 15%, var(--val-color--bg));
border-color: color-mix(in srgb, var(--val-color--info) 40%, var(--val-color--bg));
}
.message-warning {
color: color-mix(in srgb, var(--val-color--warning) 40%, black);
background-color: color-mix(in srgb, var(--val-color--warning) 15%, var(--val-color--bg));
border-color: color-mix(in srgb, var(--val-color--warning) 40%, var(--val-color--bg));
}
.message-error {
color: color-mix(in srgb, var(--val-color--danger) 40%, black);
background-color: color-mix(in srgb, var(--val-color--danger) 15%, var(--val-color--bg));
border-color: color-mix(in srgb, var(--val-color--danger) 40%, var(--val-color--bg));
}
/*
* Table component
*/
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.table th,
.table td {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
}
.table thead th {
border-bottom-width: 2px;
font-weight: 600;
}
.table-sort {
color: inherit;
text-decoration: none;
}
.table-sort:hover {
text-decoration: underline;
}
.table-sort::after {
content: "\25BC";
margin-left: 0.25rem;
font-size: 0.8em;
opacity: 0.15;
}
.table-sort-asc::after {
content: "\25B2";
font-size: 0.8em;
opacity: 1;
}
.table-sort-desc::after {
content: "\25BC";
font-size: 0.8em;
opacity: 1;
}
.table-empty {
text-align: center;
font-style: italic;
color: var(--val-color--text--muted);
}
/*
* Pager component
*/
.pager {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
margin: 1rem 0;
padding: 0 1rem;
}
.pager-align-start {
justify-content: flex-start;
}
.pager-align-center {
justify-content: center;
}
.pager-align-end {
justify-content: flex-end;
}
.pager-summary {
color: var(--val-color--text--muted);
font-size: 0.8125rem;
align-self: flex-start;
margin-inline-end: auto;
}
.pager-align-start .pager-summary {
order: 1;
margin-inline-end: 0;
margin-inline-start: auto;
}
.pager-align-center .pager-summary {
order: 1;
flex-basis: 100%;
text-align: center;
margin-inline-end: 0;
}
.pagination {
display: flex;
align-items: center;
gap: 0.25rem;
list-style: none;
margin: 0;
padding: 0;
}
.pagination a {
display: flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
padding: 0.375rem 0.625rem;
border-radius: 0.375rem;
color: var(--val-color--text);
text-decoration: none;
}
.pagination a:hover {
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
}
.pagination .active a {
color: #fff;
background-color: var(--val-color--primary);
}
.pagination .disabled a {
color: var(--val-color--text--muted);
pointer-events: none;
cursor: default;
}
.page-ellipsis {
padding: 0.375rem 0.25rem;
color: var(--val-color--text--muted);
}
/* .page-link-icon muestra el texto de navegación de los botones anterior/siguiente. El texto
accesible completo va aparte, en el aria-label del enlace. Por eso sustituir el texto visible por
otro contenido no afecta a la accesibilidad, p.ej. para usar ""/"" se puede aplicar:
.page-link-icon {
display: inline-block;
font-size: 0;
}
.page-previous .page-link-icon::before {
content: "";
font-size: 1rem;
display: inline-block;
transform: translateY(-0.15em) scale(1.5);
}
.page-next .page-link-icon::before {
content: "";
font-size: 1rem;
display: inline-block;
transform: translateY(-0.15em) scale(1.5);
}
*/
.pager-jump {
display: flex;
align-items: center;
}
.pager-jump:focus-within {
border-radius: 0.375rem;
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--val-color--primary) 25%, transparent);
}
.pager-jump-input > input.form-control:focus,
.pager-jump-button:focus {
outline: none;
box-shadow: none;
}
.pager-jump-input > input.form-control {
width: calc(var(--pager-jump-width, 1ch) + 2.5rem);
padding: 0.375rem 0.625rem;
line-height: var(--val-lh--base);
border: 0;
border-start-end-radius: 0;
border-end-end-radius: 0;
background-color: var(--val-color--light);
}
.pager-jump-input > input.form-control {
text-align: center;
appearance: textfield;
-moz-appearance: textfield;
}
.pager-jump-input > input.form-control::-webkit-outer-spin-button,
.pager-jump-input > input.form-control::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pager-jump-button {
padding: 0.375rem 0.625rem;
border: 0;
border-start-start-radius: 0;
border-end-start-radius: 0;
}
.pager-jump-button:hover {
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 {
padding: .75rem 0 3rem;
text-align: center;
}

View file

@ -389,10 +389,10 @@ body {
.intro-text-body .block {
position: relative;
}
.intro-text-body .block__title {
.intro-text-body .block-title {
margin: 1em 0 .8em;
}
.intro-text-body .block__title span {
.intro-text-body .block-title span {
display: inline-block;
padding: 10px 30px 14px;
margin: 30px 20px 0;
@ -403,7 +403,7 @@ body {
border-color: orangered;
transform: rotate(-3deg) translateY(-25%);
}
.intro-text-body .block__title:before {
.intro-text-body .block-title:before {
content: "";
height: 5px;
position: absolute;
@ -416,7 +416,7 @@ body {
transform: rotate(2deg) translateY(-50%);
transform-origin: top left;
}
.intro-text-body .block__title:after {
.intro-text-body .block-title:after {
content: "";
height: 120%;
position: absolute;
@ -427,31 +427,35 @@ body {
background: var(--intro-bg-block-1);
transform: rotate(2deg);
}
.intro-text-body .block:nth-of-type(6n+1) .block__title:after {
.intro-text-body .block:nth-of-type(6n+1) .block-title:after {
background: var(--intro-bg-block-1);
}
.intro-text-body .block:nth-of-type(6n+2) .block__title:after {
.intro-text-body .block:nth-of-type(6n+2) .block-title:after {
background: var(--intro-bg-block-2);
}
.intro-text-body .block:nth-of-type(6n+3) .block__title:after {
.intro-text-body .block:nth-of-type(6n+3) .block-title:after {
background: var(--intro-bg-block-3);
}
.intro-text-body .block:nth-of-type(6n+4) .block__title:after {
.intro-text-body .block:nth-of-type(6n+4) .block-title:after {
background: var(--intro-bg-block-4);
}
.intro-text-body .block:nth-of-type(6n+5) .block__title:after {
.intro-text-body .block:nth-of-type(6n+5) .block-title:after {
background: var(--intro-bg-block-5);
}
.intro-text-body .block:nth-of-type(6n+6) .block__title:after {
.intro-text-body .block:nth-of-type(6n+6) .block-title:after {
background: var(--intro-bg-block-6);
}
#intro-badges {
display: none;
min-height: 3.2rem;
text-align: center;
visibility: hidden;
}
#intro-badges img {
margin-bottom: 1.1rem;
margin-bottom: 0.25rem;
}
#intro-release:not([src]) {
display: none;
}
/*

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Before After
Before After

3
assets/js/basic.menu.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,26 @@
// Conecta cada `Dropdown` (standalone o embebido en un `Nav`) con `accessible-menu` (ver
// basic.menu.min.js), que añade los roles ARIA, el `aria-expanded` y la navegación por teclado
// sobre el marcado ya generado en el servidor. Selecciona por clase, no por etiqueta, porque el
// disparador es un `<button>` en un `Dropdown` independiente y un `<a>` cuando cuelga de un
// `nav::Item::dropdown()`.
document.querySelectorAll(".dropdown > .dropdown-toggle").forEach(function (toggle) {
var container = toggle.parentElement;
var menu = container.querySelector(":scope > .dropdown-menu");
if (!menu) {
return;
}
new TopLinkDisclosureMenu({
menuElement: menu,
containerElement: container,
controllerElement: toggle,
// `Header`/`Divider`/`Label` no son interactivos: se excluyen de `menuItemSelector`
// (si no, la librería asume que todo `<li>` tiene un enlace y lanza un TypeError al
// intentar añadirle listeners de foco).
menuItemSelector: "li:has(> a.dropdown-item, > button.dropdown-item)",
menuLinkSelector: "a.dropdown-item, button.dropdown-item",
// La navegación con flechas/Home/End es opcional en la librería (`false` por defecto);
// se activa para igualar el comportamiento habitual de un menú desplegable.
optionalKeySupport: true,
});
});

View file

@ -1,6 +1,12 @@
use pagetop_build::StaticFilesBundle;
use pagetop_build::{StaticFilesBundle, copy_dir};
fn main() -> std::io::Result<()> {
// Regenera `static/` desde cero sólo si hay cambios en `assets/`.
println!("cargo:rerun-if-changed=assets");
let _ = std::fs::remove_dir_all("static");
copy_dir("assets", "static")?;
StaticFilesBundle::from_dir("./static", None)
.with_name("assets")
.build()

View file

@ -2,5 +2,5 @@ use pagetop::prelude::*;
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::new().run()?.await
Application::new().await.run().await
}

View file

@ -1,11 +1,11 @@
use pagetop::prelude::*;
use pagetop_bootsier::theme::*;
include_locales!(LOC from "examples/locale");
struct FormControls;
#[async_trait]
impl Extension for FormControls {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
@ -21,13 +21,13 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
.with_child(
Intro::default()
.with_opening(IntroOpening::Custom)
.with_title(L10n::t("title", &LOC))
.with_slogan(L10n::t("slogan", &LOC))
.with_button(None::<(L10n, FnPathByContext)>)
.with_title(Lc::t("title", &LOC))
.with_slogan(Lc::t("slogan", &LOC))
.with_button(None::<(Lc, Route)>)
// Bloque 1: casillas, interruptores y botones de opción.
.with_child(
Block::new()
.with_title(L10n::t("block_selections", &LOC))
.with_title(Lc::t("block_selections", &LOC))
.with_child(
Form::new()
.with_id("form-selections")
@ -36,69 +36,69 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
// Casillas e interruptores (form::Checkbox).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_checkbox", &LOC))
.with_description(L10n::t("desc_checkbox", &LOC))
.with_legend(Lc::t("fieldset_checkbox", &LOC))
.with_description(Lc::t("desc_checkbox", &LOC))
.with_child(
form::Checkbox::new()
.with_name("accept_terms")
.with_label(L10n::t("label_terms", &LOC))
.with_label(Lc::t("label_terms", &LOC))
.with_required(true),
)
.with_child(
form::Checkbox::new()
.with_name("accept_marketing")
.with_label(L10n::t("label_marketing", &LOC))
.with_label(Lc::t("label_marketing", &LOC))
.with_checked(true)
.with_inline(true),
)
.with_child(
form::Checkbox::new()
.with_name("newsletter")
.with_label(L10n::t("label_newsletter", &LOC))
.with_label(Lc::t("label_newsletter", &LOC))
.with_inline(true),
)
.with_child(
form::Checkbox::switch()
.with_name("notifications")
.with_label(L10n::t("label_notifications", &LOC))
.with_label(Lc::t("label_notifications", &LOC))
.with_checked(true)
.with_reverse(true),
)
.with_child(
form::Checkbox::switch()
.with_name("dark_mode")
.with_label(L10n::t("label_dark_mode", &LOC))
.with_label(Lc::t("label_dark_mode", &LOC))
.with_disabled(true),
),
)
// Grupo de casillas de verificación (form::check::Field).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_checkgroup", &LOC))
.with_legend(Lc::t("fieldset_checkgroup", &LOC))
.with_child(
form::check::Field::new()
.with_name("interests")
.with_label(L10n::t("label_interests", &LOC))
.with_help_text(L10n::t("help_interests", &LOC))
.with_label(Lc::t("label_interests", &LOC))
.with_help_text(Lc::t("help_interests", &LOC))
.with_item(
form::check::Item::new(
"rust",
L10n::t("check_rust", &LOC),
Lc::t("check_rust", &LOC),
)
.with_checked(true),
)
.with_item(form::check::Item::new(
"web",
L10n::t("check_web", &LOC),
Lc::t("check_web", &LOC),
))
.with_item(form::check::Item::new(
"ai",
L10n::t("check_ai", &LOC),
Lc::t("check_ai", &LOC),
))
.with_item(
form::check::Item::new(
"games",
L10n::t("check_games", &LOC),
Lc::t("check_games", &LOC),
)
.with_disabled(true),
),
@ -107,60 +107,59 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
// Botones de opción (form::radio::Field).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_radio", &LOC))
.with_legend(Lc::t("fieldset_radio", &LOC))
.with_child(
form::radio::Field::new()
.with_name("frequency")
.with_label(L10n::t("label_frequency", &LOC))
.with_label(Lc::t("label_frequency", &LOC))
.with_item(form::radio::Item::new(
"daily",
L10n::t("radio_daily", &LOC),
Lc::t("radio_daily", &LOC),
))
.with_item(
form::radio::Item::new(
"weekly",
L10n::t("radio_weekly", &LOC),
Lc::t("radio_weekly", &LOC),
)
.with_checked(true),
)
.with_item(form::radio::Item::new(
"monthly",
L10n::t("radio_monthly", &LOC),
Lc::t("radio_monthly", &LOC),
))
.with_item(
form::radio::Item::new(
"never",
L10n::t("radio_never", &LOC),
Lc::t("radio_never", &LOC),
)
.with_disabled(true),
),
),
)
// Campo oculto (form::Hidden).
.with_child(
form::Hidden::new()
.with_name("origin")
.with_value("form-selections"),
)
.with_child(form::Hidden::field("origin", "form-selections"))
// Botones de acción.
.with_child(Button::submit(Lc::t("btn_submit", &LOC)).with_prop(
PropsOp::add_classes(class::ButtonColor::solid(
ThemeColor::Primary,
)),
))
.with_child(Button::reset(Lc::t("btn_reset", &LOC)).with_prop(
PropsOp::add_classes(class::ButtonColor::outline(
ThemeColor::Secondary,
)),
))
.with_child(
Button::submit(L10n::t("btn_submit", &LOC))
.with_color(ButtonColor::Background(Color::Primary)),
)
.with_child(
Button::reset(L10n::t("btn_reset", &LOC))
.with_color(ButtonColor::Outline(Color::Secondary)),
)
.with_child(
Button::plain(L10n::t("btn_cancel", &LOC))
.with_color(ButtonColor::Link),
Button::plain(Lc::t("btn_cancel", &LOC)).with_prop(
PropsOp::add_classes(class::ButtonColor::link()),
),
),
),
)
// Bloque 2: campos de texto, multilínea y rango.
.with_child(
Block::new()
.with_title(L10n::t("block_text", &LOC))
.with_title(Lc::t("block_text", &LOC))
.with_child(
Form::new()
.with_id("form-text")
@ -169,22 +168,21 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
// Campos de texto (form::input::Field).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_text", &LOC))
.with_legend(Lc::t("fieldset_text", &LOC))
.with_child(
form::input::Field::text()
.with_name("name")
.with_label(L10n::t("label_name", &LOC))
.with_placeholder(L10n::t("placeholder_name", &LOC))
.with_label(Lc::t("label_name", &LOC))
.with_placeholder(Lc::t("placeholder_name", &LOC))
.with_help_text(Lc::t("help_name", &LOC))
.with_required(true),
)
.with_child(
form::input::Field::email()
.with_name("email")
.with_label(L10n::t("label_email", &LOC))
.with_placeholder(L10n::t(
"placeholder_email",
&LOC,
))
.with_label(Lc::t("label_email", &LOC))
.with_placeholder(Lc::t("placeholder_email", &LOC))
.with_help_text(Lc::t("help_email", &LOC))
.with_autocomplete(
Some(form::Autocomplete::email()),
)
@ -193,7 +191,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
.with_child(
form::input::Field::password()
.with_name("password")
.with_label(L10n::t("label_password", &LOC))
.with_label(Lc::t("label_password", &LOC))
.with_autocomplete(Some(
form::Autocomplete::new_password(),
))
@ -202,23 +200,20 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
.with_child(
form::input::Field::telephone()
.with_name("phone")
.with_label(L10n::t("label_phone", &LOC))
.with_placeholder(L10n::t(
"placeholder_phone",
&LOC,
)),
.with_label(Lc::t("label_phone", &LOC))
.with_placeholder(Lc::t("placeholder_phone", &LOC)),
)
.with_child(
form::input::Field::url()
.with_name("website")
.with_label(L10n::t("label_url", &LOC))
.with_placeholder(L10n::t("placeholder_url", &LOC)),
.with_label(Lc::t("label_url", &LOC))
.with_placeholder(Lc::t("placeholder_url", &LOC)),
)
.with_child(
form::input::Field::search()
.with_name("search")
.with_label(L10n::t("label_search", &LOC))
.with_placeholder(L10n::t(
.with_label(Lc::t("label_search", &LOC))
.with_placeholder(Lc::t(
"placeholder_search",
&LOC,
)),
@ -227,230 +222,197 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
// Área de texto (form::Textarea).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_textarea", &LOC))
.with_legend(Lc::t("fieldset_textarea", &LOC))
.with_child(
form::Textarea::new()
.with_name("comment")
.with_label(L10n::t("label_comment", &LOC))
.with_placeholder(L10n::t(
.with_label(Lc::t("label_comment", &LOC))
.with_placeholder(Lc::t(
"placeholder_comment",
&LOC,
))
.with_rows(Some(4))
.with_help_text(L10n::t("help_comment", &LOC)),
.with_help_text(Lc::t("help_comment", &LOC)),
),
)
// Control deslizante (form::Range).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_range", &LOC))
.with_legend(Lc::t("fieldset_range", &LOC))
.with_child(
form::Range::new()
.with_name("rating")
.with_label(L10n::t("label_rating", &LOC))
.with_label(Lc::t("label_rating", &LOC))
.with_min(Some(1.0))
.with_max(Some(10.0))
.with_step(Some(1.0))
.with_value(Some(5.0))
.with_help_text(L10n::t("help_rating", &LOC)),
.with_help_text(Lc::t("help_rating", &LOC)),
),
)
// Campo oculto (form::Hidden).
.with_child(
form::Hidden::new()
.with_name("origin")
.with_value("form-text"),
)
.with_child(form::Hidden::field("origin", "form-text"))
// Botones de acción.
.with_child(Button::submit(Lc::t("btn_submit", &LOC)).with_prop(
PropsOp::add_classes(class::ButtonColor::solid(
ThemeColor::Primary,
)),
))
.with_child(Button::reset(Lc::t("btn_reset", &LOC)).with_prop(
PropsOp::add_classes(class::ButtonColor::outline(
ThemeColor::Secondary,
)),
))
.with_child(
Button::submit(L10n::t("btn_submit", &LOC))
.with_color(ButtonColor::Background(Color::Primary)),
)
.with_child(
Button::reset(L10n::t("btn_reset", &LOC))
.with_color(ButtonColor::Outline(Color::Secondary)),
)
.with_child(
Button::plain(L10n::t("btn_cancel", &LOC))
.with_color(ButtonColor::Link),
Button::plain(Lc::t("btn_cancel", &LOC)).with_prop(
PropsOp::add_classes(class::ButtonColor::link()),
),
),
),
)
// Bloque 3: listas de selección y etiquetas flotantes.
.with_child(
Block::new()
.with_title(L10n::t("block_lists", &LOC))
.with_child(
Form::new()
.with_id("form-lists")
.with_action("/")
.with_method(form::Method::Post)
// Listas de selección (form::select::Field).
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_select", &LOC))
.with_child(
form::select::Field::new()
.with_name("language")
.with_label(L10n::t("label_language", &LOC))
.with_item(
form::select::Item::new(
"",
L10n::t("select_choose", &LOC),
)
.with_selected(true),
)
.with_group(
form::select::Group::new(L10n::t(
"select_group_europe",
&LOC,
))
.with_item(form::select::Item::new(
"es",
L10n::t("select_spanish", &LOC),
))
.with_item(form::select::Item::new(
"fr",
L10n::t("select_french", &LOC),
)),
)
.with_group(
form::select::Group::new(L10n::t(
"select_group_americas",
&LOC,
))
.with_item(form::select::Item::new(
"en",
L10n::t("select_english", &LOC),
))
.with_item(form::select::Item::new(
"pt",
L10n::t("select_portuguese", &LOC),
)),
)
.with_item(
form::select::Item::new(
"xx",
L10n::t("select_disabled", &LOC),
)
.with_disabled(true),
)
.with_required(true),
)
.with_child(
form::select::Field::new()
.with_name("technologies")
.with_label(L10n::t("label_technologies", &LOC))
.with_item(
form::select::Item::new(
"rust",
L10n::n("Rust"),
)
.with_selected(true),
)
.with_item(
form::select::Item::new(
"python",
L10n::n("Python"),
)
.with_selected(true),
)
.with_item(form::select::Item::new(
"javascript",
L10n::n("JavaScript"),
))
.with_item(form::select::Item::new(
"go",
L10n::n("Go"),
))
.with_item(form::select::Item::new(
"typescript",
L10n::n("TypeScript"),
))
.with_multiple(true)
.with_rows(Some(4))
.with_help_text(L10n::t("help_technologies", &LOC)),
),
)
// Etiquetas flotantes.
.with_child(
form::Fieldset::new()
.with_legend(L10n::t("fieldset_floating", &LOC))
.with_child(
form::input::Field::text()
.with_name("fl_name")
.with_label(L10n::t("label_name", &LOC))
.with_placeholder(L10n::t("placeholder_name", &LOC))
.with_floating_label(true)
.with_required(true),
)
.with_child(
form::Textarea::new()
.with_name("fl_comment")
.with_label(L10n::t("label_comment", &LOC))
.with_placeholder(L10n::t(
"placeholder_comment",
&LOC,
))
.with_floating_label(true),
)
.with_child(
form::select::Field::new()
.with_name("fl_country")
.with_label(L10n::t("label_country", &LOC))
.with_item(
form::select::Item::new(
"",
L10n::t("select_choose", &LOC),
)
.with_selected(true),
)
.with_item(form::select::Item::new(
"de",
L10n::t("select_germany", &LOC),
))
.with_item(form::select::Item::new(
"es",
L10n::t("select_spain", &LOC),
))
.with_item(form::select::Item::new(
"fr",
L10n::t("select_france", &LOC),
))
.with_item(form::select::Item::new(
"pt",
L10n::t("select_portugal", &LOC),
))
.with_floating_label(true)
.with_required(true),
),
)
// Campo oculto (form::Hidden).
.with_child(
form::Hidden::new()
.with_name("origin")
.with_value("form-lists"),
)
// Botones de acción.
.with_child(
Button::submit(L10n::t("btn_submit", &LOC))
.with_color(ButtonColor::Background(Color::Primary)),
)
.with_child(
Button::reset(L10n::t("btn_reset", &LOC))
.with_color(ButtonColor::Outline(Color::Secondary)),
)
.with_child(
Button::plain(L10n::t("btn_cancel", &LOC))
.with_color(ButtonColor::Link),
),
),
.with_title(
if global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier") {
Lc::t("block_lists_floating", &LOC)
} else {
Lc::t("block_lists", &LOC)
},
)
.with_child(form_lists()),
),
)
.render()
.await
}
fn form_lists() -> Form {
let mut form = Form::new()
.with_id("form-lists")
.with_action("/")
.with_method(form::Method::Post)
// Listas de selección (form::select::Field).
.with_child(
form::Fieldset::new()
.with_legend(Lc::t("fieldset_select", &LOC))
.with_child(
form::select::Field::new()
.with_name("language")
.with_label(Lc::t("label_language", &LOC))
.with_item(
form::select::Item::new("", Lc::t("select_choose", &LOC))
.with_selected(true),
)
.with_group(
form::select::Group::new(Lc::t("select_group_europe", &LOC))
.with_item(form::select::Item::new(
"es",
Lc::t("select_spanish", &LOC),
))
.with_item(form::select::Item::new(
"fr",
Lc::t("select_french", &LOC),
)),
)
.with_group(
form::select::Group::new(Lc::t("select_group_americas", &LOC))
.with_item(form::select::Item::new(
"en",
Lc::t("select_english", &LOC),
))
.with_item(form::select::Item::new(
"pt",
Lc::t("select_portuguese", &LOC),
)),
)
.with_item(
form::select::Item::new("xx", Lc::t("select_disabled", &LOC))
.with_disabled(true),
)
.with_required(true),
)
.with_child(
form::select::Field::new()
.with_name("technologies")
.with_label(Lc::t("label_technologies", &LOC))
.with_item(
form::select::Item::new("rust", Lc::n("Rust")).with_selected(true),
)
.with_item(
form::select::Item::new("python", Lc::n("Python")).with_selected(true),
)
.with_item(form::select::Item::new("javascript", Lc::n("JavaScript")))
.with_item(form::select::Item::new("go", Lc::n("Go")))
.with_item(form::select::Item::new("typescript", Lc::n("TypeScript")))
.with_multiple(true)
.with_rows(Some(4))
.with_help_text(Lc::t("help_technologies", &LOC)),
),
);
// Etiquetas flotantes: solo disponibles con el tema Bootsier.
if global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier") {
form = form.with_child(
form::Fieldset::new()
.with_legend(Lc::t("fieldset_floating", &LOC))
.with_child(
form::input::Field::text()
.with_name("fl_name")
.with_label(Lc::t("label_name", &LOC))
.with_placeholder(Lc::t("placeholder_name", &LOC))
.with_floating_label(true)
.with_required(true),
)
.with_child(
form::Textarea::new()
.with_name("fl_comment")
.with_label(Lc::t("label_comment", &LOC))
.with_placeholder(Lc::t("placeholder_comment", &LOC))
.with_floating_label(true),
)
.with_child(
form::select::Field::new()
.with_name("fl_country")
.with_label(Lc::t("label_country", &LOC))
.with_item(
form::select::Item::new("", Lc::t("select_choose", &LOC))
.with_selected(true),
)
.with_item(form::select::Item::new("de", Lc::t("select_germany", &LOC)))
.with_item(form::select::Item::new("es", Lc::t("select_spain", &LOC)))
.with_item(form::select::Item::new("fr", Lc::t("select_france", &LOC)))
.with_item(form::select::Item::new(
"pt",
Lc::t("select_portugal", &LOC),
))
.with_floating_label(true)
.with_required(true),
),
);
}
form
// Campo oculto (form::Hidden).
.with_child(form::Hidden::field("origin", "form-lists"))
// Botones de acción.
.with_child(
Button::submit(Lc::t("btn_submit", &LOC)).with_prop(PropsOp::add_classes(
class::ButtonColor::solid(ThemeColor::Primary),
)),
)
.with_child(
Button::reset(Lc::t("btn_reset", &LOC)).with_prop(PropsOp::add_classes(
class::ButtonColor::outline(ThemeColor::Secondary),
)),
)
.with_child(
Button::plain(Lc::t("btn_cancel", &LOC))
.with_prop(PropsOp::add_classes(class::ButtonColor::link())),
)
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&FormControls).run()?.await
Application::prepare(&FormControls).await.run().await
}

View file

@ -2,6 +2,7 @@ use pagetop::prelude::*;
struct HelloName;
#[async_trait]
impl Extension for HelloName {
fn configure_router(&self, router: Router) -> Router {
router.route("/hello/{name}", web::get(hello_name))
@ -19,9 +20,10 @@ async fn hello_name(
}
}))
.render()
.await
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&HelloName).run()?.await
Application::prepare(&HelloName).await.run().await
}

View file

@ -2,6 +2,7 @@ use pagetop::prelude::*;
struct HelloWorld;
#[async_trait]
impl Extension for HelloWorld {
fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(hello_world))
@ -16,9 +17,10 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
}
}))
.render()
.await
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await
Application::prepare(&HelloWorld).await.run().await
}

View file

@ -4,6 +4,7 @@ include_locales!(LOC from "examples/locale");
struct IntroColors;
#[async_trait]
impl Extension for IntroColors {
fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(intro_colors))
@ -15,68 +16,69 @@ async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
.with_child(
Intro::default()
.with_opening(IntroOpening::Custom)
.with_title(L10n::n("PageTop"))
.with_slogan(L10n::t("colors_slogan", &LOC))
.with_button(None::<(L10n, FnPathByContext)>)
.with_title(Lc::n("PageTop"))
.with_slogan(Lc::t("colors_slogan", &LOC))
.with_button(None::<(Lc, Route)>)
.with_child(
Block::new()
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "1"))
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "1"))
.with_child(Html::with(|cx| {
html! {
p { (L10n::t("colors_val_1", &LOC).using(cx)) }
p { (Lc::t("colors_val_1", &LOC).using(cx)) }
}
})),
)
.with_child(
Block::new()
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "2"))
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "2"))
.with_child(Html::with(|cx| {
html! {
p { (L10n::t("colors_val_2", &LOC).using(cx)) }
p { (Lc::t("colors_val_2", &LOC).using(cx)) }
}
})),
)
.with_child(
Block::new()
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "3"))
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "3"))
.with_child(Html::with(|cx| {
html! {
p { (L10n::t("colors_val_3", &LOC).using(cx)) }
p { (Lc::t("colors_val_3", &LOC).using(cx)) }
}
})),
)
.with_child(
Block::new()
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "4"))
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "4"))
.with_child(Html::with(|cx| {
html! {
p { (L10n::t("colors_val_4", &LOC).using(cx)) }
p { (Lc::t("colors_val_4", &LOC).using(cx)) }
}
})),
)
.with_child(
Block::new()
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "5"))
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "5"))
.with_child(Html::with(|cx| {
html! {
p { (L10n::t("colors_val_5", &LOC).using(cx)) }
p { (Lc::t("colors_val_5", &LOC).using(cx)) }
}
})),
)
.with_child(
Block::new()
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "6"))
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "6"))
.with_child(Html::with(|cx| {
html! {
p { (L10n::t("colors_val_6", &LOC).using(cx)) }
p { (Lc::t("colors_val_6", &LOC).using(cx)) }
}
})),
),
)
.render()
.await
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&IntroColors).run()?.await
Application::prepare(&IntroColors).await.run().await
}

View file

@ -2,7 +2,8 @@ title = Form controls
slogan = Bootsier form components showcase
block_selections = Checkboxes, switches and radio buttons
block_text = Text fields, multiline and range
block_lists = Select lists and floating labels
block_lists = Selection lists
block_lists_floating = Select lists and floating labels
fieldset_text = Text fields
label_name = Full name
@ -16,6 +17,8 @@ label_url = Website
placeholder_url = https://example.com
label_search = Search
placeholder_search = Search term...
help_name = Enter your full name as it appears on your ID.
help_email = We will only use your email to send important notifications.
fieldset_textarea = Multiline text
label_comment = Comment

View file

@ -2,7 +2,8 @@ title = Controles de formulario
slogan = Componentes Bootsier para formularios
block_selections = Casillas, interruptores y botones de opción
block_text = Campos de texto, multilínea y rango
block_lists = Listas de selección y etiquetas flotantes
block_lists = Listas de selección
block_lists_floating = Listas de selección y etiquetas flotantes
fieldset_text = Campos de texto
label_name = Nombre completo
@ -16,6 +17,8 @@ label_url = Sitio web
placeholder_url = https://ejemplo.com
label_search = Búsqueda
placeholder_search = Término de búsqueda...
help_name = Introduce tu nombre completo tal como aparece en tu documento de identidad.
help_email = Solo usaremos tu correo para enviarte notificaciones importantes.
fieldset_textarea = Texto multilínea
label_comment = Comentario

View file

@ -1,11 +1,11 @@
use pagetop::prelude::*;
use pagetop_bootsier::theme::*;
include_locales!(LOC from "examples/locale");
struct SuperMenu;
#[async_trait]
impl Extension for SuperMenu {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
@ -15,86 +15,90 @@ impl Extension for SuperMenu {
]
}
fn initialize(&self) {
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
async fn initialize(&self) {
let navbar_menu = bs::Navbar::brand_left(bs::navbar::Brand::new())
.with_expand(BreakPoint::LG)
.with_item(navbar::Item::nav(
Nav::new()
.with_item(nav::Item::link(L10n::t("menus_item_link", &LOC), |cx| {
cx.route("/")
}))
.with_item(nav::Item::link_blank(
L10n::t("menus_item_blank", &LOC),
|_| "https://docs.rs/pagetop".into(),
.with_item(bs::navbar::Item::nav(
bs::Nav::new()
.with_item(bs::nav::Item::link(Lc::t("menus_item_link", &LOC), "/"))
.with_item(bs::nav::Item::link_blank(
Lc::t("menus_item_blank", &LOC),
"https://docs.rs/pagetop",
))
.with_item(nav::Item::dropdown(
Dropdown::new()
.with_title(L10n::t("menus_test_title", &LOC))
.with_item(dropdown::Item::header(L10n::t("menus_dev_header", &LOC)))
.with_item(dropdown::Item::link(
L10n::t("menus_dev_getting_started", &LOC),
|cx| cx.route("/dev/getting-started"),
.with_item(bs::nav::Item::dropdown(
bs::Dropdown::new()
.with_title(Lc::t("menus_test_title", &LOC))
.with_item(bs::dropdown::Item::header(Lc::t("menus_dev_header", &LOC)))
.with_item(bs::dropdown::Item::link(
Lc::t("menus_dev_getting_started", &LOC),
"/dev/getting-started",
))
.with_item(dropdown::Item::link(
L10n::t("menus_dev_guides", &LOC),
|cx| cx.route("/dev/guides"),
.with_item(bs::dropdown::Item::link(
Lc::t("menus_dev_guides", &LOC),
"/dev/guides",
))
.with_item(dropdown::Item::link_blank(
L10n::t("menus_dev_forum", &LOC),
|_| "https://forum.example.dev".into(),
.with_item(bs::dropdown::Item::link_blank(
Lc::t("menus_dev_forum", &LOC),
"https://forum.example.dev",
))
.with_item(dropdown::Item::divider())
.with_item(dropdown::Item::header(L10n::t("menus_sdk_header", &LOC)))
.with_item(dropdown::Item::link(
L10n::t("menus_sdk_rust", &LOC),
|cx| cx.route("/dev/sdks/rust"),
.with_item(bs::dropdown::Item::divider())
.with_item(bs::dropdown::Item::header(Lc::t("menus_sdk_header", &LOC)))
.with_item(bs::dropdown::Item::link(
Lc::t("menus_sdk_rust", &LOC),
"/dev/sdks/rust",
))
.with_item(dropdown::Item::link(L10n::t("menus_sdk_js", &LOC), |cx| {
cx.route("/dev/sdks/js")
}))
.with_item(dropdown::Item::link(
L10n::t("menus_sdk_python", &LOC),
|cx| cx.route("/dev/sdks/python"),
.with_item(bs::dropdown::Item::link(
Lc::t("menus_sdk_js", &LOC),
"/dev/sdks/js",
))
.with_item(dropdown::Item::divider())
.with_item(dropdown::Item::header(L10n::t("menus_plugin_header", &LOC)))
.with_item(dropdown::Item::link(
L10n::t("menus_plugin_auth", &LOC),
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
.with_item(bs::dropdown::Item::link(
Lc::t("menus_sdk_python", &LOC),
"/dev/sdks/python",
))
.with_item(dropdown::Item::link(
L10n::t("menus_plugin_cache", &LOC),
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
.with_item(bs::dropdown::Item::divider())
.with_item(bs::dropdown::Item::header(Lc::t(
"menus_plugin_header",
&LOC,
)))
.with_item(bs::dropdown::Item::link(
Lc::t("menus_plugin_auth", &LOC),
"/dev/sdks/rust/plugins/auth",
))
.with_item(dropdown::Item::divider())
.with_item(dropdown::Item::label(L10n::t("menus_item_label", &LOC)))
.with_item(dropdown::Item::link_disabled(
L10n::t("menus_item_disabled", &LOC),
|cx| cx.route("#"),
.with_item(bs::dropdown::Item::link(
Lc::t("menus_plugin_cache", &LOC),
"/dev/sdks/rust/plugins/cache",
))
.with_item(bs::dropdown::Item::divider())
.with_item(bs::dropdown::Item::label(Lc::t("menus_item_label", &LOC)))
.with_item(bs::dropdown::Item::link_disabled(
Lc::t("menus_item_disabled", &LOC),
"#",
)),
))
.with_item(nav::Item::link_disabled(
L10n::t("menus_item_disabled", &LOC),
|cx| cx.route("#"),
.with_item(bs::nav::Item::link_disabled(
Lc::t("menus_item_disabled", &LOC),
"#",
)),
))
.with_item(navbar::Item::nav(
Nav::new()
.with_classes(
ClassesOp::Add,
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
)
.with_item(nav::Item::link(L10n::t("menus_item_sign_up", &LOC), |cx| {
cx.route("/auth/sign-up")
}))
.with_item(nav::Item::link(L10n::t("menus_item_login", &LOC), |cx| {
cx.route("/auth/login")
})),
.with_item(bs::navbar::Item::nav(
bs::Nav::new()
.with_prop(PropsOp::add_classes(class::Margin::with(
BoxSide::Start,
ScaleSize::Auto,
)))
.with_item(bs::nav::Item::link(
Lc::t("menus_item_sign_up", &LOC),
"/auth/sign-up",
))
.with_item(bs::nav::Item::link(
Lc::t("menus_item_login", &LOC),
"/auth/login",
)),
));
InRegion::Global(&DefaultRegion::Header).add(
Container::new()
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
InRegion::Global(&CoreRegions::Header).add(
bs::Container::new()
.with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0)))
.with_child(navbar_menu),
);
}
@ -102,5 +106,5 @@ impl Extension for SuperMenu {
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&SuperMenu).run()?.await
Application::prepare(&SuperMenu).await.run().await
}

View file

@ -17,8 +17,5 @@ authors.workspace = true
[dependencies]
pagetop.workspace = true
[dev-dependencies]
tokio.workspace = true
[build-dependencies]
pagetop-build.workspace = true

View file

@ -11,14 +11,13 @@
</div>
## 🧭 Sobre PageTop
## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
## ⚡️ Guía rápida
## Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
@ -35,6 +34,7 @@ use pagetop::prelude::*;
struct MyApp;
#[async_trait]
impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
@ -44,11 +44,6 @@ impl Extension for MyApp {
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
@ -67,26 +62,24 @@ use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request)
.with_theme(&Aliner)
.add_child(
.with_child(
Block::new()
.with_title(L10n::l("sample_title"))
.add_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
.with_title(Lc::l("sample_title"))
.with_child(Html::with(|cx| html! {
p { (Lc::l("sample_content").using(cx)) }
})),
)
.render()
.render().await
}
```
## 🚧 Advertencia
## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
## 📜 Licencia
## Licencia
El código está disponible bajo una doble licencia:

View file

@ -3,14 +3,13 @@
<h1>PageTop Aliner</h1>
<p>Tema para <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
<p>Tema de <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
[![Doc API](https://img.shields.io/docsrs/pagetop-aliner?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-aliner)
[![Crates.io](https://img.shields.io/crates/v/pagetop-aliner.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-aliner)
[![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
<br>
</div>
## Sobre PageTop
@ -19,8 +18,7 @@
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
# Guía rápida
## Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
@ -37,6 +35,7 @@ use pagetop::prelude::*;
struct MyApp;
#[async_trait]
impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
@ -46,11 +45,6 @@ impl Extension for MyApp {
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
@ -71,12 +65,12 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
.with_theme(&Aliner)
.with_child(
Block::new()
.with_title(L10n::l("sample_title"))
.with_title(Lc::l("sample_title"))
.with_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
p { (Lc::l("sample_content").using(cx)) }
})),
)
.render()
.render().await
}
```
*/
@ -96,13 +90,14 @@ include_locales!(LOCALES_ALINER);
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
pub struct Aliner;
#[async_trait]
impl Extension for Aliner {
fn name(&self) -> L10n {
L10n::t("extension_name", &LOCALES_ALINER)
fn name(&self) -> Lc {
Lc::t("extension_name", &LOCALES_ALINER)
}
fn description(&self) -> L10n {
L10n::t("extension_description", &LOCALES_ALINER)
fn description(&self) -> Lc {
Lc::t("extension_description", &LOCALES_ALINER)
}
fn theme(&self) -> Option<ThemeRef> {
@ -115,6 +110,7 @@ impl Extension for Aliner {
}
}
#[async_trait]
impl Theme for Aliner {
fn before_render_page_body(&self, page: &mut Page) {
page.alter_assets(AssetsOp::AddStyleSheet(
@ -133,7 +129,7 @@ impl Theme for Aliner {
.with_weight(-99),
))
.alter_child_in(
&DefaultRegion::Footer,
&CoreRegions::Footer,
ChildOp::AddIfEmpty(PoweredBy::new().into()),
);
}

View file

@ -18,8 +18,5 @@ authors.workspace = true
pagetop.workspace = true
serde.workspace = true
[dev-dependencies]
tokio.workspace = true
[build-dependencies]
pagetop-build.workspace = true

View file

@ -11,14 +11,13 @@
</div>
## 🧭 Sobre PageTop
## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
## ⚡️ Guía rápida
## Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
@ -44,11 +43,6 @@ impl Extension for MyApp {
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
@ -67,10 +61,10 @@ use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request)
.with_theme(&Bootsier)
.add_child(
.with_child(
Block::new()
.with_title(L10n::l("sample_title"))
.add_child(Html::with(|cx| html! {
.with_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
})),
)
@ -78,22 +72,19 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
}
```
## 📚 Créditos
## Créditos
Este *crate* integra la biblioteca de estilos [Bootstrap 5.3.8](https://getbootstrap.com/) para
definir el comportamiento, la apariencia y los componentes de la interfaz. Bootstrap se distribuye
bajo licencia [MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
## 🚧 Advertencia
## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
## 📜 Licencia
## Licencia
El código está disponible bajo una doble licencia:

View file

@ -0,0 +1,85 @@
// Bootsier CSS rules: self-hosted fonts, form components, and regions.
// Self-hosted Source Sans 3 (SIL OFL 1.1), served from /bootsier/fonts.
// Required by AdminLTE 4, which declares it as the primary font family in $font-family-sans-serif.
// Font URLs must match the Preload declarations in src/lib.rs; a mismatch causes double downloads.
@font-face {
font-family: "Source Sans 3";
src: url("/bootsier/fonts/bootsier.font.woff2") format("woff2");
font-weight: 200 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Source Sans 3";
src: url("/bootsier/fonts/bootsier.font.italic.woff2") format("woff2");
font-weight: 200 900;
font-style: italic;
font-display: swap;
}
// Font-relative top offset to keep the skip-link hidden at any font size.
.skip-link {
top: -3em;
}
// Required field indicator in forms.
.form-required {
color: var(--bs-danger);
margin: 0 0.25rem;
}
// Form field with consistent bottom margin.
.form-field {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
// Fieldset with border and floating legend.
fieldset {
position: relative;
background-color: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
padding: 2rem 1rem 1rem;
margin: 2rem 0 1rem;
}
fieldset > legend {
position: absolute;
top: 0;
left: 1rem;
transform: translateY(-50%);
color: var(--bs-secondary-color);
background-color: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
padding: 0.125rem 0.75rem;
font-size: ($font-size-sm + $font-size-base) / 2;
font-weight: var(--bs-font-weight-normal);
line-height: 1.25;
width: fit-content;
max-width: 75%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fieldset-description {
margin-bottom: 1rem;
}
// Gap between group label and first inline check.
.form-label + .form-check-inline {
margin-left: 1rem;
}
// Footer region.
.region-footer {
padding: 0.75rem 0 3rem;
text-align: center;
}

View file

@ -0,0 +1,5 @@
// Bootstrap Icons v1.13.1, fonts served from /bootsier/fonts.
$bootstrap-icons-font: "bootsier.icons";
$bootstrap-icons-font-dir: "/bootsier/fonts";
@import "bootstrap-icons-1.13.1/bootstrap-icons";

View file

@ -1,80 +1,14 @@
// Enable CSS Grid
// Bootstrap utility extensions. Imported just before utilities/api so variable changes take effect.
// CSS Grid (Bootstrap 5): disable legacy grid classes and enable native CSS Grid.
$enable-grid-classes: false;
$enable-cssgrid: true;
// Opacity
.bg-opacity-0 {
--bs-bg-opacity: 0;
}
.border-opacity-0 {
--bs-border-opacity: 0;
}
.text-opacity-0 {
--bs-text-opacity: 0;
}
.text-opacity-10 {
--bs-text-opacity: 0.1;
}
// FORMS
// Required field indicator
.form-required {
color: var(--bs-danger);
margin: 0 0.25rem;
}
// Form fields
.form-field {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
// Fieldset
fieldset {
position: relative;
background-color: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
padding: 2rem 1rem 1rem;
margin: 2rem 0 1rem;
}
fieldset > legend {
position: absolute;
top: 0;
left: 1rem;
transform: translateY(-50%);
background-color: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
padding: 0.125rem 0.75rem;
font-size: $font-size-sm;
line-height: 1.25;
width: fit-content;
max-width: 75%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fieldset-description {
margin-bottom: 1rem;
}
// Check buttons, gap between label and first inline check
.form-label + .form-check-inline {
margin-left: 1rem;
}
// Extending utilities
// Extend the $utilities map with additional classes.
$utilities: map-merge(
$utilities,
(
// Individual border widths
// Individual border widths per side.
"border-top": (
property: border-top-width,
class: border-top,
@ -95,7 +29,7 @@ $utilities: map-merge(
class: border-start,
values: $border-widths
),
// Individual rounded values
// Individual corner radii.
"rounded-top-start": (
property: border-top-left-radius,
class: rounded-top-start,
@ -156,11 +90,18 @@ $utilities: map-merge(
pill: var(--#{$prefix}border-radius-pill)
)
),
// Opacity: add missing values (0 for bg/border/text; 10 for text) not in Bootstrap defaults.
"bg-opacity": map-merge(
map-get($utilities, "bg-opacity"),
(values: map-merge(map-get(map-get($utilities, "bg-opacity"), "values"), (0: 0)))
),
"border-opacity": map-merge(
map-get($utilities, "border-opacity"),
(values: map-merge(map-get(map-get($utilities, "border-opacity"), "values"), (0: 0)))
),
"text-opacity": map-merge(
map-get($utilities, "text-opacity"),
(values: map-merge(map-get(map-get($utilities, "text-opacity"), "values"), (0: 0, 10: .1)))
),
)
);
// Region Footer
.region-footer {
padding: .75rem 0 3rem;
text-align: center;
}

View file

@ -0,0 +1,4 @@
// Bootsier overrides for Bootstrap variables. Imported before bootstrap/scss/variables
// so that Bootstrap's !default declarations do not override these values.
$font-size-base: 1.125rem;

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,337 @@
/* ==========================================================================
AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance
========================================================================== */
/* Skip Links - WCAG 2.4.1: Bypass Blocks */
.skip-link {
position: absolute;
top: -40px;
left: 6px;
z-index: 999999;
padding: 8px 16px;
font-weight: 600;
color: var(--bs-white);
text-decoration: none;
background: var(--bs-primary);
&:focus {
top: 0;
outline: 3px solid var(--bs-warning);
outline-offset: 2px;
}
&:hover {
color: var(--bs-white);
text-decoration: none;
background: var(--bs-primary-emphasis);
}
}
/* Enhanced Focus Indicators - WCAG 2.4.7: Focus Visible */
.focus-enhanced {
&:focus {
outline: 3px solid var(--bs-focus-ring-color, #0d6efd);
outline-offset: 2px;
box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25);
}
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.card {
border: 2px solid;
}
.btn {
border-width: 2px;
}
.nav-link {
border: 1px solid transparent;
&:hover,
&:focus {
border-color: currentcolor;
}
}
}
/* Reduced Motion Support - WCAG 2.3.3: Animation from Interactions */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
.fade {
opacity: 1 !important;
/* stylelint-disable-next-line property-disallowed-list */
transition: none !important;
}
.collapse {
/* stylelint-disable-next-line property-disallowed-list */
transition: none !important;
}
.modal.fade .modal-dialog {
transform: none !important;
}
}
/* Screen Reader Only Content */
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.sr-only-focusable:focus {
position: static !important;
width: auto !important;
height: auto !important;
padding: inherit !important;
margin: inherit !important;
overflow: visible !important;
clip: auto !important;
white-space: normal !important;
}
/* Focus Trap Utilities */
.focus-trap {
&:focus {
box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25);
}
}
/* Accessible Color Combinations - WCAG 1.4.3: Contrast (Minimum) */
.text-accessible-primary {
color: #003d82; /* 4.5:1 contrast on white */
}
.text-accessible-success {
color: #0f5132; /* 4.5:1 contrast on white */
}
.text-accessible-danger {
color: #842029; /* 4.5:1 contrast on white */
}
.text-accessible-warning {
color: #664d03; /* 4.5:1 contrast on white */
}
/* ARIA Live Regions */
.live-region {
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
&.live-region-visible {
position: static;
left: auto;
width: auto;
height: auto;
overflow: visible;
}
}
/* Enhanced Error States - WCAG 3.3.1: Error Identification */
.form-control.is-invalid {
border-color: var(--bs-danger);
&:focus {
border-color: var(--bs-danger);
box-shadow: 0 0 0 .25rem rgba(220, 53, 69, .25);
}
}
.invalid-feedback {
&[role="alert"] {
font-weight: 600;
}
}
/* Target Size - WCAG 2.5.8: Target Size (Minimum) */
.touch-target {
min-width: 44px;
min-height: 44px;
&.touch-target-small {
min-width: 24px;
min-height: 24px;
}
}
/* Table Accessibility */
.table-accessible {
th {
font-weight: 600;
background-color: var(--bs-secondary-bg);
&[scope="col"] {
border-bottom: 2px solid var(--bs-border-color);
}
&[scope="row"] {
border-right: 2px solid var(--bs-border-color);
}
}
caption {
padding: .75rem;
font-weight: 600;
color: var(--bs-secondary);
text-align: left;
caption-side: top;
}
}
/* Navigation Landmarks */
nav[role="navigation"] {
&:not([aria-label]):not([aria-labelledby]) {
&::before {
position: absolute;
left: -10000px;
content: "Navigation";
}
}
}
/* Form Fieldset Styling */
fieldset {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid var(--bs-border-color);
legend {
padding: 0 .5rem;
margin-bottom: .5rem;
font-size: 1.1em;
font-weight: 600;
}
}
/* Loading States */
.loading[aria-busy="true"] {
position: relative;
pointer-events: none;
&::after {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
content: "";
border: 2px solid var(--bs-primary);
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
&::after {
border-top-color: var(--bs-primary);
animation: none;
}
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Dark Mode Accessibility */
[data-bs-theme="dark"] {
.text-accessible-primary {
color: #6ea8fe;
}
.text-accessible-success {
color: #75b798;
}
.text-accessible-danger {
color: #f1aeb5;
}
.text-accessible-warning {
color: #ffda6a;
}
}
/* Print Accessibility */
@media print {
.skip-link,
.btn,
.nav-link {
color: #000 !important;
background: transparent !important;
border: 1px solid #000 !important;
}
a[href^="http"]::after {
font-size: .8em;
content: " (" attr(href) ")";
}
/* Print Layout Fix - Ensure sidebar and main content are both visible */
.app-wrapper {
display: grid !important;
grid-template-rows: auto 1fr auto !important;
grid-template-columns: auto 1fr !important;
}
.sidebar-overlay {
display: none !important;
}
.app-sidebar {
position: static !important;
display: block !important;
min-width: 200px !important;
max-width: 200px !important;
max-height: none !important;
margin-left: 0 !important;
overflow: visible !important;
}
.sidebar-wrapper {
height: auto !important;
overflow: visible !important;
}
.app-header {
position: static !important;
}
.app-main {
width: auto !important;
max-width: 100% !important;
overflow: visible !important;
}
.app-content {
overflow: visible !important;
}
.app-footer {
position: static !important;
}
}

View file

@ -0,0 +1,3 @@
.app-content {
padding: $lte-content-padding-y $lte-content-padding-x;
}

View file

@ -0,0 +1,32 @@
//
// Core: Main Footer
//
.app-footer {
grid-area: #{$lte-prefix}app-footer;
width: inherit;
max-width: 100vw;
min-height: 3rem;
padding: $lte-app-footer-padding;
color: $lte-app-footer-color;
background-color: $lte-app-footer-bg;
border-top: $lte-app-footer-border-top;
@include transition($lte-transition-speed $lte-transition-fn);
}
.fixed-footer {
.app-footer {
position: sticky;
bottom: 0;
z-index: $lte-zindex-fixed-footer;
}
// When layout-fixed is used, app-main has overflow: auto which prevents
// position: sticky from working on the footer. Ensure the grid keeps the
// footer pinned at the bottom while app-main scrolls independently.
&.layout-fixed {
.app-main {
min-height: 0;
}
}
}

View file

@ -0,0 +1,61 @@
//
// Core: Main Header
//
.app-header {
z-index: $lte-zindex-app-header;
grid-area: #{$lte-prefix}app-header;
max-width: 100vw;
border-bottom: $lte-app-header-bottom-border;
@include transition($lte-transition-speed $lte-transition-fn);
.nav-link {
position: relative;
height: $nav-link-height;
}
}
// Navbar badge
.navbar-badge {
position: absolute;
top: 9px;
right: 5px;
padding: 2px 4px;
font-size: .6rem;
font-weight: 400;
}
.fixed-header {
.app-header {
position: sticky;
top: 0;
z-index: $lte-zindex-fixed-header;
}
// Fixes #6020: when only `fixed-header` is used (without `layout-fixed`),
// the sidebar including its branding used to scroll with the page
// because nothing made it sticky. Pin the sidebar so the brand and menu
// stay visible while the page content scrolls beneath the fixed header.
// Only applied on `sidebar-expand-*` breakpoints, since the mobile sidebar
// is an off-canvas overlay that handles its own positioning.
@each $breakpoint in map-keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
&.sidebar-expand#{$infix} {
@include media-breakpoint-up($next) {
.app-sidebar {
position: sticky;
top: 0;
max-height: 100vh;
.sidebar-wrapper {
height: subtract(100vh, add($lte-app-header-height, 1px));
overflow-x: hidden;
overflow-y: auto;
}
}
}
}
}
}

View file

@ -0,0 +1,41 @@
.app-main {
position: relative;
display: flex;
flex-direction: column;
grid-area: #{$lte-prefix}app-main;
max-width: 100vw;
padding-bottom: $lte-app-main-padding-bottom;
@include transition($lte-transition-speed $lte-transition-fn);
.app-content-header {
padding: 1rem $lte-content-padding-x;
.breadcrumb {
padding: 0;
margin-bottom: 0;
line-height: 2.5rem;
a {
text-decoration: none;
}
}
}
.app-content-top-area,
.app-content-bottom-area {
color: $lte-app-content-bottom-area-color;
background-color: $lte-app-content-bottom-area-bg;
}
.app-content-top-area {
padding: $lte-app-content-top-area-padding-y $lte-app-content-top-area-padding-x;
border-bottom: $lte-app-content-top-area-top-border;
}
.app-content-bottom-area {
padding: $lte-app-content-bottom-area-padding-y $lte-app-content-bottom-area-padding-x;
margin-top: auto;
margin-bottom: $lte-app-content-bottom-area-margin-bottom;
border-top: $lte-app-content-bottom-area-top-border;
}
}

View file

@ -0,0 +1,619 @@
.app-sidebar {
--#{$lte-prefix}sidebar-hover-bg: #{$lte-sidebar-hover-bg};
--#{$lte-prefix}sidebar-color: #{$lte-sidebar-color};
--#{$lte-prefix}sidebar-hover-color: #{$lte-sidebar-hover-color};
--#{$lte-prefix}sidebar-active-color: #{$lte-sidebar-active-color};
--#{$lte-prefix}sidebar-menu-active-bg: #{$lte-sidebar-menu-active-bg};
--#{$lte-prefix}sidebar-menu-active-color: #{$lte-sidebar-menu-active-color};
--#{$lte-prefix}sidebar-submenu-bg: #{$lte-sidebar-submenu-bg};
--#{$lte-prefix}sidebar-submenu-color: #{$lte-sidebar-submenu-color};
--#{$lte-prefix}sidebar-submenu-hover-color: #{$lte-sidebar-submenu-hover-color};
--#{$lte-prefix}sidebar-submenu-hover-bg: #{$lte-sidebar-submenu-hover-bg};
--#{$lte-prefix}sidebar-submenu-active-color: #{$lte-sidebar-submenu-active-color};
--#{$lte-prefix}sidebar-submenu-active-bg: #{$lte-sidebar-submenu-active-bg};
--#{$lte-prefix}sidebar-header-color: #{$lte-sidebar-header-color};
z-index: $lte-zindex-sidebar;
grid-area: #{$lte-prefix}app-sidebar;
min-width: var(--#{$lte-prefix}sidebar-width);
max-width: var(--#{$lte-prefix}sidebar-width);
@include transition($lte-sidebar-transition);
}
.sidebar-brand {
display: flex;
align-items: center;
justify-content: center;
height: $lte-app-header-height;
padding: $lte-brand-link-padding-y $lte-brand-link-padding-x;
overflow: hidden;
font-size: $navbar-brand-font-size;
white-space: nowrap;
border-bottom: $lte-brand-link-border-buttom solid var(--#{$prefix}border-color);
@include transition(width $lte-transition-speed $lte-transition-fn);
.brand-link {
display: flex;
align-items: center;
text-decoration: none;
.brand-image {
float: left;
width: auto;
max-height: 33px;
line-height: .8;
}
.brand-image-xs {
float: left;
width: auto;
max-height: 33px;
margin-top: -.1rem;
line-height: .8;
}
.brand-image-xl {
width: auto;
max-height: 40px;
line-height: .8;
&.single {
margin-top: -.3rem;
}
}
}
.brand-text {
margin-left: .5rem;
color: rgba(var(--#{$prefix}emphasis-color-rgb), .8);
@include transition(flex $lte-transition-speed $lte-transition-fn, width $lte-transition-speed $lte-transition-fn);
&:hover {
color: var(--#{$prefix}emphasis-color);
}
}
}
.sidebar-wrapper {
padding-top: $lte-sidebar-padding-y;
padding-right: $lte-sidebar-padding-x;
padding-bottom: $lte-sidebar-padding-y;
padding-left: $lte-sidebar-padding-x;
overscroll-behavior: contain;
@include scrollbar-color-gray();
@include scrollbar-width-thin();
.nav-item {
max-width: 100%;
}
.nav-link {
display: flex;
justify-content: flex-start;
p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.nav-icon {
display: flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
max-width: 1.5rem;
}
// Sidebar Menu. First level links
.sidebar-menu > .nav-item {
// links
> .nav-link {
color: var(--#{$lte-prefix}sidebar-color);
&:hover,
&:focus {
color: var(--#{$lte-prefix}sidebar-hover-color);
background-color: var(--#{$lte-prefix}sidebar-hover-bg);
}
&.active {
color: var(--#{$lte-prefix}sidebar-menu-active-color);
background-color: var(--#{$lte-prefix}sidebar-menu-active-bg);
}
}
// Open state
&.menu-open > .nav-link {
color: var(--#{$lte-prefix}sidebar-hover-color);
background-color: var(--#{$lte-prefix}sidebar-hover-bg);
&.active {
color: var(--#{$lte-prefix}sidebar-menu-active-color);
background-color: var(--#{$lte-prefix}sidebar-menu-active-bg);
}
}
// First Level Submenu
> .nav-treeview {
background-color: var(--#{$lte-prefix}sidebar-submenu-bg);
}
}
// Section Heading
.nav-header {
display: inline;
overflow: hidden;
color: var(--#{$lte-prefix}sidebar-header-color);
text-overflow: ellipsis;
background-color: inherit;
}
// All links within the sidebar menu
a {
color: var(--#{$lte-prefix}sidebar-color);
}
// All submenus
.nav-treeview {
> .nav-item {
> .nav-link {
color: var(--#{$lte-prefix}sidebar-submenu-color);
&:hover,
&:focus {
color: var(--#{$lte-prefix}sidebar-submenu-hover-color);
background-color: var(--#{$lte-prefix}sidebar-submenu-hover-bg);
}
&.active {
color: var(--#{$lte-prefix}sidebar-submenu-active-color);
background-color: var(--#{$lte-prefix}sidebar-submenu-active-bg);
}
}
}
}
}
// Sidebar navigation menu
.sidebar-menu {
// All levels
.nav-item {
> .nav-link {
margin-bottom: .2rem;
.nav-arrow {
@include transition(transform $lte-transition-fn $lte-transition-speed);
transform: translateY(-50%) #{"/*rtl:append:rotate(180deg)*/"};
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
}
}
// All levels
.nav-link > .nav-badge,
.nav-link > p > .nav-badge {
position: absolute;
top: 50%;
right: 1rem;
transform: translateY(-50%);
}
.nav-link > .nav-arrow,
.nav-link > p > .nav-arrow {
position: absolute;
top: 50%;
right: 1rem;
}
.nav-link {
position: relative;
width: 100%;
@include transition(width $lte-transition-fn $lte-transition-speed);
@include border-radius($border-radius);
p {
display: inline;
padding-left: .5rem;
margin: 0;
}
}
.nav-header {
position: relative;
width: 100%;
padding: $nav-link-padding-y ($nav-link-padding-y * 1.5);
font-size: .9rem;
@include transition(width $lte-transition-fn $lte-transition-speed);
}
// Tree view menu
.nav-treeview {
display: none;
padding: 0;
list-style: none;
}
.menu-open {
> .nav-treeview {
display: block;
}
> .nav-link {
.nav-arrow {
transform: translateY(-50%) rotate(90deg) #{"/*rtl:ignore*/"};
}
}
}
// Override: ensure badges/arrows are aligned to the end (right in LTR, left in RTL)
.nav-link > .nav-badge,
.nav-link > p > .nav-badge,
.nav-link > .nav-arrow,
.nav-link > p > .nav-arrow {
right: 1rem !important; // place at end in LTR
left: auto !important;
}
}
.nav-indent {
.nav-treeview {
padding-left: $lte-sidebar-padding-x;
}
}
.nav-compact.nav-indent {
.nav-treeview {
padding-left: 0;
.nav-item {
padding-left: $lte-sidebar-padding-x;
}
}
}
.sidebar-mini.sidebar-collapse.nav-indent {
.app-sidebar:hover {
.nav-treeview {
padding-left: 0;
.nav-item {
padding-left: $lte-sidebar-padding-x;
}
}
}
}
.sidebar-collapse {
&.nav-compact.nav-indent {
.nav-treeview {
.nav-item {
padding-left: 0;
}
}
}
}
.nav-compact .nav-link {
@include border-radius(0);
margin-bottom: 0 !important;
}
// A fix for text overflow while transitioning from sidebar mini to full sidebar
.sidebar-menu,
.sidebar-menu > .nav-header,
.sidebar-menu .nav-link {
white-space: nowrap;
}
// Logo style
.logo-xs,
.logo-xl {
position: absolute;
visibility: visible;
opacity: 1;
&.brand-image-xs {
top: 12px;
left: 18px;
}
&.brand-image-xl {
top: 6px;
left: 12px;
}
}
.logo-xs {
visibility: hidden;
opacity: 0;
&.brand-image-xl {
top: 8px;
left: 16px;
}
}
.brand-link {
&.logo-switch {
&::before {
content: "\00a0";
}
}
}
.sidebar-mini.sidebar-collapse {
.app-sidebar {
min-width: $lte-sidebar-mini-width;
max-width: $lte-sidebar-mini-width;
}
// Make the sidebar headers
.sidebar-menu .nav-header {
display: none;
}
.sidebar-menu {
.nav-link {
width: $lte-sidebar-mini-width - $lte-sidebar-padding-x * 2;
p {
display: inline-block;
width: 0;
white-space: nowrap;
}
}
.nav-badge,
.nav-arrow {
display: none;
animation-name: fadeOut;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
}
.brand-text {
display: inline-block;
max-width: 0;
overflow: hidden;
}
.sidebar-menu .nav-link p,
.brand-text,
.logo-xl,
.nav-arrow {
visibility: hidden;
animation-name: fadeOut;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
.logo-xs {
display: inline-block;
visibility: visible;
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
&:not(.sidebar-without-hover) {
.app-sidebar:hover {
min-width: var(--#{$lte-prefix}sidebar-width);
max-width: var(--#{$lte-prefix}sidebar-width);
.sidebar-menu .nav-header {
display: inline;
}
.sidebar-menu .nav-link {
width: auto;
}
.sidebar-menu .nav-link p,
.brand-text,
.logo-xl {
width: auto;
margin-left: 0;
visibility: visible;
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
.brand-text {
display: inline;
max-width: inherit;
margin-left: .5rem;
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
.nav-badge,
.nav-arrow {
display: inline-block;
visibility: visible;
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
animation-delay: $lte-transition-speed;
}
.nav-link p {
padding-left: .5rem;
}
.logo-xs {
visibility: hidden;
animation-name: fadeOut;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
}
}
}
}
.sidebar-collapse:not(.sidebar-mini) {
.app-sidebar {
margin-left: calc(var(--#{$lte-prefix}sidebar-width) * -1); // stylelint-disable-line function-disallowed-list
}
}
.sidebar-expand {
@each $breakpoint in map-keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
/* stylelint-disable-next-line scss/selector-no-union-class-name */
&#{$infix} {
@include media-breakpoint-up($next) {
&.layout-fixed {
.app-main-wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-sidebar-wrapper {
position: relative;
}
.app-main {
flex: 1 1 auto;
overflow: auto;
}
.app-sidebar {
position: sticky;
top: 0;
bottom: 0;
max-height: 100vh;
.sidebar-wrapper {
height: subtract(100vh, add($lte-app-header-height, 1px));
overflow-x: hidden;
overflow-y: auto;
}
}
}
&.sidebar-open {
.nav-link > .nav-badge,
.nav-link > p > .nav-badge {
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
animation-delay: $lte-transition-speed;
}
.nav-link > .nav-arrow,
.nav-link > p > .nav-arrow {
animation-name: fadeIn;
animation-duration: $lte-transition-speed;
animation-fill-mode: both;
animation-delay: $lte-transition-speed;
}
}
}
@include media-breakpoint-down($next) {
$max: breakpoint-max($next);
&::before {
display: none;
content: "#{$max}";
}
.app-sidebar {
position: fixed;
top: 0;
bottom: 0;
max-height: 100vh;
margin-left: calc(var(--#{$lte-prefix}sidebar-width) * -1); // stylelint-disable-line function-disallowed-list
.sidebar-wrapper {
height: subtract(100vh, add($lte-app-header-height, 1px));
overflow-x: hidden;
overflow-y: auto;
}
}
&.sidebar-open {
.app-sidebar {
margin-left: 0;
}
.sidebar-overlay {
position: absolute;
inset: 0;
z-index: $lte-zindex-sidebar-overlay;
width: 100%;
height: 100%;
cursor: pointer;
visibility: visible;
background-color: rgba(0, 0, 0, .2);
animation-name: fadeIn;
animation-fill-mode: both;
}
}
}
}
}
}
.sidebar-menu .nav-link p,
.app-sidebar .brand-text,
.app-sidebar .logo-xs,
.app-sidebar .logo-xl {
@include transition(margin-left $lte-transition-speed linear, opacity $lte-transition-speed ease, visibility $lte-transition-speed ease);
}
// To prevent onload transition and animation
.app-loaded {
&.sidebar-mini.sidebar-collapse {
.sidebar-menu .nav-link p,
.brand-text {
animation-duration: $lte-transition-speed;
}
}
}
body:not(.app-loaded) {
.app-header,
.app-sidebar,
.app-main,
.app-footer {
@include transition(none !important);
animation-duration: 0s !important;
}
}
.hold-transition {
.app-header,
.app-sidebar,
.app-main,
.app-footer,
.nav-arrow,
.nav-badge {
@include transition(none !important);
animation-duration: 0s !important;
}
}
@if $enable-dark-mode {
@include color-mode(dark) {
&.app-sidebar,
.app-sidebar {
--#{$lte-prefix}sidebar-hover-bg: #{$lte-sidebar-hover-bg-dark};
--#{$lte-prefix}sidebar-color: #{$lte-sidebar-color-dark};
--#{$lte-prefix}sidebar-hover-color: #{$lte-sidebar-hover-color-dark};
--#{$lte-prefix}sidebar-active-color: #{$lte-sidebar-active-color-dark};
--#{$lte-prefix}sidebar-menu-active-bg: #{$lte-sidebar-menu-active-bg-dark};
--#{$lte-prefix}sidebar-menu-active-color: #{$lte-sidebar-menu-active-color-dark};
--#{$lte-prefix}sidebar-submenu-bg: #{$lte-sidebar-submenu-bg-dark};
--#{$lte-prefix}sidebar-submenu-color: #{$lte-sidebar-submenu-color-dark};
--#{$lte-prefix}sidebar-submenu-hover-color: #{$lte-sidebar-submenu-hover-color-dark};
--#{$lte-prefix}sidebar-submenu-hover-bg: #{$lte-sidebar-submenu-hover-bg-dark};
--#{$lte-prefix}sidebar-submenu-active-color: #{$lte-sidebar-submenu-active-color-dark};
--#{$lte-prefix}sidebar-submenu-active-bg: #{$lte-sidebar-submenu-active-bg-dark};
--#{$lte-prefix}sidebar-header-color: #{$lte-sidebar-header-color-dark};
}
}
}

View file

@ -0,0 +1,23 @@
//
// Core: Layout
//
.app-wrapper {
position: relative;
display: grid;
grid-template-areas:
"#{$lte-prefix}app-sidebar #{$lte-prefix}app-header"
"#{$lte-prefix}app-sidebar #{$lte-prefix}app-main"
"#{$lte-prefix}app-sidebar #{$lte-prefix}app-footer";
grid-template-rows: min-content 1fr min-content;
grid-template-columns: auto 1fr;
grid-gap: 0;
align-content: stretch;
align-items: stretch;
max-width: 100vw;
min-height: 100vh;
> * {
min-width: 0;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
//
// Callouts
//
.callout {
--#{$prefix}link-color-rgb: var(--#{$lte-prefix}callout-link);
--#{$prefix}code-color: var(--#{$lte-prefix}callout-code-color);
padding: 1.25rem;
color: var(--#{$lte-prefix}callout-color, inherit);
background-color: var(--#{$lte-prefix}callout-bg, var(--bs-gray-100));
border-left: .25rem solid var(--#{$lte-prefix}callout-border, var(--bs-gray-300));
.callout-link {
font-weight: $lte-callout-link-font-weight;
color: var(--#{$prefix}callout-link-color);
}
h4 {
margin-bottom: .25rem;
}
> :last-child {
margin-bottom: 0;
}
+ .callout {
margin-top: -.25rem;
}
}
// Variations
@each $name, $color in $theme-colors {
.callout-#{$name} {
--#{$lte-prefix}callout-color: var(--#{$prefix}#{$name}-text-emphasis);
--#{$lte-prefix}callout-bg: var(--#{$prefix}#{$name}-bg-subtle);
--#{$lte-prefix}callout-border: var(--#{$prefix}#{$name}-border-subtle);
--#{$prefix}callout-link-color: var(--#{$prefix}#{$name}-text-emphasis);
}
}

View file

@ -0,0 +1,311 @@
//
// Component: Cards
//
// Color variants
.card {
@include box-shadow($lte-card-shadow);
&[class*="card-"]:not(.card-outline),
&[class*="text-bg-"]:not(.card-outline) {
> .card-header {
color: var(--#{$lte-prefix}card-variant-color);
background-color: var(--#{$lte-prefix}card-variant-bg);
.btn-tool {
--#{$prefix}btn-color: rgba(var(--#{$lte-prefix}card-variant-color-rgb), .8);
--#{$prefix}btn-hover-color: var(--#{$lte-prefix}card-variant-color);
}
}
}
&.card-outline {
border-top: 3px solid var(--#{$lte-prefix}card-variant-bg);
}
&.maximized-card {
position: fixed;
top: 0;
left: 0;
z-index: $zindex-modal-backdrop;
width: 100% !important;
max-width: 100% !important;
height: 100% !important;
max-height: 100% !important;
&.was-collapsed .card-body {
display: block !important;
}
.card-body {
overflow: auto;
}
[data-lte-toggle="card-collapse"] {
display: none;
}
[data-lte-icon="maximize"] {
display: none;
}
.card-header,
.card-footer {
@include border-radius(0 !important);
}
}
&:not(.maximized-card) {
[data-lte-icon="minimize"] {
display: none;
}
}
// collapsed mode
&.collapsed-card {
// Use > .card-header to scope to direct card only, not nested cards
> .card-header [data-lte-icon="collapse"] {
display: none;
}
> .card-body,
> .card-footer {
display: none;
}
}
&:not(.collapsed-card) {
// Use > .card-header to scope to direct card only, not nested cards
> .card-header [data-lte-icon="expand"] {
display: none;
}
}
.nav.flex-column {
> li {
margin: 0;
border-bottom: 1px solid $card-border-color;
&:last-of-type {
border-bottom: 0;
}
}
}
// fixed height to 300px
&.height-control {
.card-body {
max-height: 300px;
overflow: auto;
}
}
.border-end {
border-right: 1px solid $card-border-color;
}
.border-start {
border-left: 1px solid $card-border-color;
}
&.card-tabs {
&:not(.card-outline) {
> .card-header {
border-bottom: 0;
.nav-item {
&:first-child .nav-link {
border-left-color: transparent;
}
}
}
}
&.card-outline {
.nav-item {
border-bottom: 0;
&:first-child .nav-link {
margin-left: 0;
border-left: 0;
}
}
}
.card-tools {
margin: .3rem .5rem;
}
&:not(.expanding-card).collapsed-card {
.card-header {
border-bottom: 0;
.nav-tabs {
border-bottom: 0;
.nav-item {
margin-bottom: 0;
}
}
}
}
&.expanding-card {
.card-header {
.nav-tabs {
.nav-item {
margin-bottom: -1px;
}
}
}
}
}
&.card-outline-tabs {
border-top: 0;
.card-header {
.nav-item {
&:first-child .nav-link {
margin-left: 0;
border-left: 0;
}
}
a {
text-decoration: none;
border-top: 3px solid transparent;
&:hover {
border-top: 3px solid $nav-tabs-border-color;
}
&.active {
&:hover {
margin-top: 0;
}
}
}
}
.card-tools {
margin: .5rem .5rem .3rem;
}
&:not(.expanding-card).collapsed-card .card-header {
border-bottom: 0;
.nav-tabs {
border-bottom: 0;
.nav-item {
margin-bottom: 0;
}
}
}
&.expanding-card {
.card-header {
.nav-tabs {
.nav-item {
margin-bottom: -1px;
}
}
}
}
}
}
// Maximized Card Body Scroll fix
html.maximized-card {
overflow: hidden;
}
// Add clearfix to header, body and footer
.card-header,
.card-body,
.card-footer {
@include clearfix();
}
// Box header
.card-header {
position: relative;
padding: (($card-spacer-y * .5) * 2) $card-spacer-x;
background-color: transparent;
border-bottom: 1px solid $card-border-color;
@if $enable-rounded {
@include border-top-radius($border-radius);
}
.collapsed-card & {
border-bottom: 0;
}
> .card-tools {
float: right;
margin-right: -$card-spacer-x * .5;
.input-group,
.nav,
.pagination {
margin-top: -$card-spacer-y * .4;
margin-bottom: -$card-spacer-y * .4;
}
[data-bs-toggle="tooltip"] {
position: relative;
}
}
}
.card-title {
float: left;
margin: 0;
font-size: $lte-card-title-font-size;
font-weight: $lte-card-title-font-weight;
}
// Box Tools Buttons
.btn-tool {
--#{$prefix}btn-padding-x: .5rem;
--#{$prefix}btn-padding-y: .25rem;
&:not(.btn-tool-custom) {
--#{$prefix}btn-color: var(--#{$prefix}tertiary-color);
--#{$prefix}btn-bg: transparent;
--#{$prefix}btn-box-shadow: none;
--#{$prefix}btn-hover-color: var(--#{$prefix}secondary-color);
--#{$prefix}btn-active-border-color: transparent;
}
margin: -$card-spacer-y 0;
font-size: $font-size-sm;
}
@each $name, $color in $theme-colors {
.card-#{$name},
.bg-#{$name},
.text-bg-#{$name} {
--#{$lte-prefix}card-variant-bg: #{$color};
--#{$lte-prefix}card-variant-bg-rgb: #{to-rgb($color)};
--#{$lte-prefix}card-variant-color: #{color-contrast($color)};
--#{$lte-prefix}card-variant-color-rgb: #{to-rgb(color-contrast($color))};
}
}
// Box Body
.card-body {
// Tables within the box body
> .table {
margin-bottom: 0;
> thead > tr > th,
> thead > tr > td {
border-top-width: 0;
}
}
}

View file

@ -0,0 +1,53 @@
.compact-mode {
.app-header {
max-height: $lte-app-header-height-compact;
.nav-link {
max-height: $nav-link-height-compact;
}
}
.nav-link {
--bs-nav-link-padding-y: .25rem;
--bs-nav-link-padding-x: .5rem;
}
&.sidebar-mini.sidebar-collapse {
.app-sidebar:not(:hover) {
min-width: $lte-sidebar-mini-width-compact;
max-width: $lte-sidebar-mini-width-compact;
.sidebar-menu {
.nav-link {
width: $lte-sidebar-mini-width-compact - $lte-sidebar-padding-x * 2 !important;
}
}
}
}
.logo-xs,
.logo-xl {
max-height: $lte-app-header-height-compact;
}
.brand-image {
width: $nav-link-height-compact;
height: $nav-link-height-compact;
}
.sidebar-brand {
height: $lte-app-header-height-compact;
}
.app-footer {
padding: $lte-app-footer-padding-compact;
}
.sidebar-wrapper {
.nav-icon {
min-width: 1.1rem;
max-width: 1.1rem;
}
}
}

View file

@ -0,0 +1,235 @@
//
// Component: Direct Chat
//
.direct-chat {
.card-body {
position: relative;
padding: 0;
overflow-x: hidden;
}
&.chat-pane-open {
.direct-chat-contacts {
transform: translate(0, 0);
}
}
&.timestamp-light {
.direct-chat-timestamp {
color: rgba(var(--#{$prefix}body-color-rgb), .65);
}
}
&.timestamp-dark {
.direct-chat-timestamp {
color: rgba(var(--#{$prefix}body-color-rgb), .9);
}
}
}
.direct-chat-messages {
height: 250px;
padding: 10px;
overflow: auto;
transform: translate(0, 0);
}
.direct-chat-msg,
.direct-chat-text {
display: block;
}
.direct-chat-msg {
@include clearfix();
margin-bottom: 10px;
}
.direct-chat-messages,
.direct-chat-contacts {
@include transition(transform .5s ease-in-out);
}
.direct-chat-text {
@if $enable-rounded {
@include border-radius($border-radius-lg);
}
position: relative;
padding: 5px 10px;
margin: 5px 0 0 50px;
color: $lte-direct-chat-default-font-color;
background-color: $lte-direct-chat-default-msg-bg;
border: 1px solid $lte-direct-chat-default-msg-border-color;
//Create the arrow
&::after,
&::before {
position: absolute;
top: 15px;
right: 100%;
width: 0;
height: 0;
pointer-events: none;
content: " ";
border: solid transparent;
border-right-color: $lte-direct-chat-default-msg-border-color;
}
&::after {
margin-top: -5px;
border-width: 5px;
}
&::before {
margin-top: -6px;
border-width: 6px;
}
.end & {
margin-right: 50px;
margin-left: 0;
&::after,
&::before {
right: auto;
left: 100%;
border-right-color: transparent;
border-left-color: $lte-direct-chat-default-msg-border-color;
}
}
}
.direct-chat-img {
@include border-radius(50%);
float: left;
width: 40px;
height: 40px;
.end & {
float: right;
}
}
.direct-chat-infos {
display: block;
margin-bottom: 2px;
font-size: $font-size-sm;
}
.direct-chat-name {
font-weight: 600;
}
.direct-chat-timestamp {
color: rgba(var(--#{$prefix}body-color-rgb), .75);
}
//Direct chat contacts pane
.direct-chat-contacts-open {
.direct-chat-contacts {
transform: translate(0, 0);
}
}
.direct-chat-contacts {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 250px;
overflow: auto;
color: var(--#{$prefix}body-bg);
background-color: var(--#{$prefix}body-color);
transform: translate(101%, 0);
}
.direct-chat-contacts-light {
background-color: var(--#{$prefix}light-bg-subtle);
.contacts-list-name {
color: var(--#{$prefix}body-color);
}
.contacts-list-date {
color: var(--#{$prefix}secondary-color);
}
.contacts-list-msg {
color: var(--#{$prefix}secondary-color);
}
}
//Contacts list -- for displaying contacts in direct chat contacts pane
.contacts-list {
@include list-unstyled();
> li {
@include clearfix();
padding: 10px;
margin: 0;
text-decoration: none;
border-bottom: 1px solid rgba($black, .2);
&:last-of-type {
border-bottom: 0;
}
a {
text-decoration: none;
}
}
}
.contacts-list-img {
@include border-radius(50%);
float: left;
width: 40px;
}
.contacts-list-info {
margin-left: 45px;
color: var(--#{$prefix}body-bg);
}
.contacts-list-name,
.contacts-list-status {
display: block;
}
.contacts-list-name {
font-weight: 600;
}
.contacts-list-status {
font-size: $font-size-sm;
}
.contacts-list-date {
font-weight: 400;
color: var(--#{$prefix}secondary-bg);
}
.contacts-list-msg {
color: var(--#{$prefix}secondary-bg);
}
.end > .direct-chat-text {
color: var(--#{$lte-prefix}direct-chat-color);
background-color: var(--#{$lte-prefix}direct-chat-bg);
border-color: var(--#{$lte-prefix}direct-chat-bg);
&::after,
&::before {
border-left-color: var(--#{$lte-prefix}direct-chat-bg);
}
}
// Color variants
@each $name, $color in $theme-colors {
.direct-chat-#{$name} {
--#{$lte-prefix}direct-chat-color: #{color-contrast($color)};
--#{$lte-prefix}direct-chat-bg: #{$color};
}
}

View file

@ -0,0 +1,820 @@
//
// Component: Docs
//
// Visual treatment for documentation pages. Body class `.docs-page` opts in.
// Demo pages are not affected.
//
// Inline code blocks rendered by MDX (Astro highlight default class)
.astro-code {
padding: .75rem;
@include border-radius($border-radius);
}
.docs-page {
// Constrain the docs card to a comfortable reading width.
// Tables and code blocks inside can still scroll horizontally if needed.
.app-content > .container-fluid {
max-width: 60rem; // ~960px
@include media-breakpoint-up(xxl) {
max-width: 64rem;
}
}
//
// Card body breathing room + prose typography.
// Only applied inside docs content cards so demo-page card content
// stays unchanged.
//
.card > .card-body {
padding: 1.5rem;
font-size: 1rem;
line-height: 1.7;
color: var(--#{$prefix}body-color);
@include media-breakpoint-up(md) {
padding: 2.5rem;
}
//
// Heading hierarchy. The MDX content in src/html/components/docs/ uses
// ##### (h5) as the dominant section heading and ###### (h6) as a
// sub-heading (most visible in FAQ.mdx, where h5 is a topic and h6 is
// a question). Style h5 as a major section and h6 as a sub-section so
// the visual hierarchy matches the semantic one.
//
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
margin-top: 2em;
margin-bottom: .5em;
font-weight: 600;
line-height: 1.3;
color: var(--#{$prefix}emphasis-color);
scroll-margin-top: 4rem; // anchor-link friendly
&:first-child {
margin-top: 0;
}
}
h2,
.h2 {
padding-bottom: .35em;
font-size: 1.75rem;
border-bottom: 1px solid var(--#{$prefix}border-color);
}
h3,
.h3 {
font-size: 1.4rem;
}
h4,
.h4 {
font-size: 1.2rem;
}
// h5 is the dominant section heading across docs MDX files. Give it
// a visible separator above so sections are easy to scan.
h5,
.h5 {
padding-top: 1.5em;
margin-top: 2.5em;
font-size: 1.3rem;
border-top: 1px solid var(--#{$prefix}border-color);
&:first-child {
padding-top: 0;
border-top: 0;
}
}
// h6 is the sub-section / FAQ question style. Smaller than h5 but
// still distinctly heading-like, with a touch of accent colour to
// separate from body text.
h6,
.h6 {
margin-top: 1.75em;
font-size: 1.08rem;
color: var(--#{$prefix}emphasis-color);
}
//
// Paragraphs and lists.
//
p,
ul,
ol,
dl,
blockquote,
table {
margin-bottom: 1.1rem;
}
ul,
ol {
padding-left: 1.5rem;
li {
margin-bottom: .35rem;
}
// Tighter nested lists
ul,
ol {
margin-top: .35rem;
margin-bottom: 0;
}
}
// Lead-style first paragraph (just after a heading)
h2 + p,
h3 + p,
h5 + p {
margin-top: 0;
}
//
// Inline code pill style that complements MDX's syntax-highlighted blocks.
//
p code,
li code,
td code,
th code,
h2 code,
h3 code,
h4 code,
h5 code,
h6 code {
padding: .15em .4em;
font-size: .875em;
color: var(--#{$prefix}emphasis-color);
background: var(--#{$prefix}tertiary-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius-sm);
}
//
// Code blocks.
//
pre.astro-code,
pre {
padding: 1rem 1.25rem;
margin-top: 1.25rem;
margin-bottom: 1.5rem;
overflow-x: auto;
font-size: .875rem;
line-height: 1.6;
@include border-radius($border-radius);
code {
padding: 0;
background: transparent;
border: 0;
}
}
//
// Tables reference-doc styling with hairline borders.
//
table {
width: 100%;
margin-top: 1.25rem;
margin-bottom: 1.5rem;
overflow: hidden;
font-size: .95rem;
border-collapse: collapse;
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius-sm);
th,
td {
padding: .65rem .9rem;
text-align: left;
vertical-align: top;
border-bottom: 1px solid var(--#{$prefix}border-color);
}
th {
font-weight: 600;
color: var(--#{$prefix}emphasis-color);
background: var(--#{$prefix}tertiary-bg);
}
tr:last-child td {
border-bottom: 0;
}
code {
white-space: nowrap;
}
}
//
// Blockquotes used as info callouts.
//
blockquote {
padding: .85rem 1.1rem;
margin-top: 1.25rem;
color: var(--#{$prefix}emphasis-color);
background: var(--#{$prefix}info-bg-subtle);
border-left: 4px solid var(--#{$prefix}info-border-subtle);
@include border-radius($border-radius-sm);
p:last-child {
margin-bottom: 0;
}
code {
background: rgba(0, 0, 0, .05);
}
}
//
// Prose links stand out more than Bootstrap defaults.
//
a:not(.btn):not(.nav-link) {
color: var(--#{$prefix}primary);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
&:hover {
text-decoration-thickness: 2px;
}
}
//
// Horizontal rule used as additional section separator.
//
hr {
margin: 2.5rem 0;
border: 0;
border-top: 1px solid var(--#{$prefix}border-color);
opacity: 1;
}
//
// Lead paragraph the first paragraph in any doc is a summary.
// Visually heavier so it functions as a deck.
//
> p:first-child,
> p:first-of-type {
margin-bottom: 2rem;
font-size: 1.125rem;
line-height: 1.6;
color: var(--#{$prefix}body-color);
}
//
// FAQ / disclosure accordion uses native <details>/<summary>.
// Add `.faq-item` to <details> to opt in.
//
details.faq-item {
margin-bottom: .5rem;
overflow: hidden;
background: var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius);
@include transition(border-color .15s ease, box-shadow .15s ease);
&:hover {
border-color: var(--#{$prefix}primary-border-subtle);
}
&[open] {
background: var(--#{$prefix}body-bg);
border-color: var(--#{$prefix}primary-border-subtle);
box-shadow: 0 1px 3px rgba(0, 0, 0, .04);
}
summary {
position: relative;
padding: .9rem 3rem .9rem 1.1rem;
font-size: 1rem;
font-weight: 600;
color: var(--#{$prefix}emphasis-color);
list-style: none;
cursor: pointer;
user-select: none;
@include transition(color .15s ease);
// Hide the default disclosure triangle in Webkit/Blink
&::-webkit-details-marker {
display: none;
}
// Hide the marker in modern Firefox too
&::marker {
content: "";
}
&:hover {
color: var(--#{$prefix}primary);
}
// Right-side chevron, rotates on open
&::after {
position: absolute;
top: 50%;
right: 1.1rem;
display: inline-block;
width: .55rem;
height: .55rem;
margin-top: -.4rem;
content: "";
border-right: 2px solid currentcolor;
border-bottom: 2px solid currentcolor;
opacity: .55;
transform: rotate(45deg);
@include transition(transform .2s ease);
}
}
&[open] > summary {
color: var(--#{$prefix}primary);
border-bottom: 1px solid var(--#{$prefix}border-color);
&::after {
margin-top: -.15rem;
opacity: 1;
transform: rotate(-135deg);
}
}
// Content (everything after <summary>) gets the inner padding
> *:not(summary) {
padding-right: 1.1rem;
padding-left: 1.1rem;
}
> *:not(summary):first-of-type {
padding-top: 1rem;
}
> *:not(summary):last-child {
padding-bottom: 1rem;
}
// Tighten paragraphs/lists inside an FAQ answer
p,
ul,
ol {
margin-bottom: .65rem;
}
> *:last-child p:last-child,
> p:last-child,
> ul:last-child,
> ol:last-child {
margin-bottom: 0;
}
// Code blocks inside an FAQ answer get a less-distracting bg
pre.astro-code,
pre {
margin-right: 0;
margin-left: 0;
}
}
}
// Dark-mode tweak: blockquote info bg needs more contrast on dark
&[data-bs-theme="dark"],
[data-bs-theme="dark"] & {
.card > .card-body blockquote code {
background: rgba(255, 255, 255, .08);
}
}
}
//
// FAQ page custom layout that breaks out of the standard docs card
// to give the FAQ a more distinctive visual identity. Opt in via
// body.faq-page (set in pages/docs/faq.astro).
//
.faq-page {
// The FAQ doesn't use the standard card wrapper used by other docs,
// so the container can stretch wider for the chip strip and section
// grid.
.app-content > .container-fluid {
max-width: 64rem;
@include media-breakpoint-up(xxl) {
max-width: 72rem;
}
}
// --- Hero -------------------------------------------------------------
.faq-hero {
position: relative;
padding: 3rem 1.5rem 2.5rem;
margin-top: 1rem;
overflow: hidden;
background: radial-gradient(ellipse at top left, rgba(var(--#{$prefix}primary-rgb), .12), transparent 60%), radial-gradient(ellipse at bottom right, rgba(var(--#{$prefix}info-rgb), .1), transparent 65%), var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius-xl);
}
.faq-hero-eyebrow {
display: inline-flex;
gap: .4rem;
align-items: center;
padding: .35rem .85rem;
margin-bottom: 1rem;
font-size: .75rem;
font-weight: 600;
color: var(--#{$prefix}primary);
text-transform: uppercase;
letter-spacing: .08em;
background: var(--#{$prefix}primary-bg-subtle);
border: 1px solid var(--#{$prefix}primary-border-subtle);
@include border-radius(50rem);
.bi {
font-size: .9rem;
}
}
.faq-hero-title {
margin: 0 0 .5rem;
font-size: 2.25rem;
font-weight: 700;
line-height: 1.15;
color: var(--#{$prefix}emphasis-color);
@include media-breakpoint-up(md) {
font-size: 2.75rem;
}
}
.faq-hero-lead {
max-width: 36rem;
margin: 0 auto 1.75rem;
font-size: 1.05rem;
line-height: 1.55;
color: var(--#{$prefix}secondary-color);
}
.faq-search {
position: relative;
max-width: 32rem;
margin: 0 auto;
.form-control {
height: 3rem;
padding-right: 3rem;
padding-left: 3rem;
font-size: 1rem;
background: var(--#{$prefix}body-bg);
@include border-radius(50rem);
@include transition(box-shadow .15s ease, border-color .15s ease);
&:focus {
box-shadow: 0 0 0 .25rem rgba(var(--#{$prefix}primary-rgb), .15);
}
}
}
.faq-search-icon {
position: absolute;
top: 50%;
left: 1.2rem;
color: var(--#{$prefix}secondary-color);
pointer-events: none;
transform: translateY(-50%);
}
.faq-search-clear {
position: absolute;
top: 50%;
right: .65rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
color: var(--#{$prefix}secondary-color);
background: var(--#{$prefix}tertiary-bg);
border: 0;
transform: translateY(-50%);
@include border-radius(50%);
@include transition(background .15s ease, color .15s ease);
&:hover {
color: var(--#{$prefix}body-color);
background: var(--#{$prefix}secondary-bg);
}
}
.faq-empty-state {
max-width: 32rem;
padding: 1rem;
margin: 1.5rem auto 0;
font-size: .95rem;
color: var(--#{$prefix}secondary-color);
background: var(--#{$prefix}tertiary-bg);
@include border-radius($border-radius);
.bi {
margin-right: .35rem;
}
}
// --- Section nav chips ------------------------------------------------
.faq-chips {
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
}
.faq-chip {
display: inline-flex;
gap: .45rem;
align-items: center;
padding: .45rem .85rem;
font-size: .875rem;
font-weight: 500;
color: var(--#{$prefix}body-color);
text-decoration: none;
background: var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius(50rem);
@include transition(transform .15s ease, border-color .15s ease, background-color .15s ease, color .15s ease);
&:hover {
color: var(--#{$prefix}emphasis-color);
background: var(--#{$prefix}tertiary-bg);
transform: translateY(-1px);
}
.bi {
font-size: 1rem;
}
}
.faq-chip-count {
padding: 0 .45rem;
font-size: .7rem;
font-weight: 600;
color: var(--#{$prefix}secondary-color);
background: var(--#{$prefix}tertiary-bg);
@include border-radius(50rem);
}
// Per-section chip accent on hover/active
@each $tone in (primary, info, warning, success, danger, secondary) {
.faq-chip-#{$tone}:hover {
color: var(--#{$prefix}#{$tone}-text-emphasis);
background: var(--#{$prefix}#{$tone}-bg-subtle);
border-color: var(--#{$prefix}#{$tone}-border-subtle);
.faq-chip-count {
color: var(--#{$prefix}#{$tone}-text-emphasis);
background: rgba(0, 0, 0, .06);
}
}
}
// --- Sections ---------------------------------------------------------
.faq-section {
margin-bottom: 2.5rem;
scroll-margin-top: 5rem;
}
.faq-section-header {
display: flex;
gap: 1rem;
align-items: center;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
background: var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
border-left: 4px solid var(--#{$prefix}primary);
@include border-radius($border-radius);
}
.faq-section-icon {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
font-size: 1.35rem;
color: var(--#{$prefix}primary);
background: var(--#{$prefix}primary-bg-subtle);
@include border-radius($border-radius);
}
.faq-section-title {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
line-height: 1.2;
color: var(--#{$prefix}emphasis-color);
}
.faq-section-count {
margin: .1rem 0 0;
font-size: .8rem;
color: var(--#{$prefix}secondary-color);
}
// Per-section tone accents
@each $tone in (primary, info, warning, success, danger, secondary) {
.faq-section-#{$tone} {
border-left-color: var(--#{$prefix}#{$tone});
.faq-section-icon {
color: var(--#{$prefix}#{$tone}-text-emphasis);
background: var(--#{$prefix}#{$tone}-bg-subtle);
}
}
}
// --- FAQ items (override the generic .docs-page details styling) ------
.faq-section-items .faq-item {
margin-bottom: .5rem;
overflow: hidden;
background: var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius);
@include transition(border-color .15s ease, box-shadow .15s ease, transform .15s ease);
&:hover {
border-color: var(--#{$prefix}primary-border-subtle);
transform: translateX(2px);
}
&[open] {
border-color: var(--#{$prefix}primary-border-subtle);
box-shadow: 0 2px 8px rgba(0, 0, 0, .04);
transform: none;
}
summary {
display: flex;
gap: .85rem;
align-items: center;
padding: 1rem 1.25rem;
font-size: 1rem;
font-weight: 500;
color: var(--#{$prefix}emphasis-color);
list-style: none;
cursor: pointer;
user-select: none;
&::-webkit-details-marker {
display: none;
}
&::marker {
content: "";
}
&::after {
content: none; // disable the generic chevron, we use our own
}
}
.faq-q-icon {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
font-size: .85rem;
color: var(--#{$prefix}primary);
background: var(--#{$prefix}primary-bg-subtle);
@include border-radius(50%);
}
.faq-q-text {
flex-grow: 1;
}
.faq-q-chevron {
flex-shrink: 0;
color: var(--#{$prefix}secondary-color);
@include transition(transform .2s ease, color .2s ease);
}
&[open] .faq-q-chevron {
color: var(--#{$prefix}primary);
transform: rotate(180deg);
}
&[open] summary {
color: var(--#{$prefix}primary);
border-bottom: 1px solid var(--#{$prefix}border-color);
}
.faq-answer {
padding: 1.1rem 1.25rem 1.1rem 3.85rem; // align with the question text (icon + gap)
font-size: .95rem;
line-height: 1.65;
color: var(--#{$prefix}body-color);
p:last-child,
ul:last-child,
ol:last-child,
pre:last-child {
margin-bottom: 0;
}
a {
color: var(--#{$prefix}primary);
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
text-decoration-thickness: 2px;
}
}
code {
padding: .12em .35em;
font-size: .875em;
color: var(--#{$prefix}emphasis-color);
background: var(--#{$prefix}tertiary-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius-sm);
}
pre.astro-code,
pre {
padding: .85rem 1rem;
margin-top: .75rem;
margin-bottom: .75rem;
overflow-x: auto;
font-size: .85rem;
line-height: 1.55;
@include border-radius($border-radius-sm);
code {
padding: 0;
background: transparent;
border: 0;
}
}
}
}
// --- CTA footer -------------------------------------------------------
.faq-cta {
padding: 2.5rem 1.5rem;
text-align: center;
background: radial-gradient(ellipse at top, rgba(var(--#{$prefix}primary-rgb), .08), transparent 60%), var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
@include border-radius($border-radius-xl);
}
.faq-cta-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
margin-bottom: 1rem;
font-size: 1.6rem;
color: var(--#{$prefix}primary);
background: var(--#{$prefix}primary-bg-subtle);
@include border-radius(50%);
}
.faq-cta h2 {
margin: 0 0 .5rem;
font-size: 1.5rem;
font-weight: 600;
color: var(--#{$prefix}emphasis-color);
}
.faq-cta p {
max-width: 32rem;
margin: 0 auto 1.5rem;
color: var(--#{$prefix}secondary-color);
}
.faq-cta-actions {
display: inline-flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: center;
}
}

View file

@ -0,0 +1,225 @@
//
// Component: Dropdown
//
// General Dropdown Rules
// Ensure children cannot overflow and break the dropdown border radius
.dropdown-menu {
overflow: hidden;
}
.fs-7 {
.dropdown-menu {
font-size: $font-size-sm !important;
}
.dropdown-toggle::after {
vertical-align: .2rem;
}
}
.dropdown-item-title {
margin: 0;
font-size: $font-size-base;
}
.dropdown-icon {
&::after {
margin-left: 0;
}
}
// Dropdown Sizes
.dropdown-menu-lg {
min-width: 280px;
max-width: 300px;
padding: 0;
.dropdown-divider {
margin: 0;
}
.dropdown-item {
padding: $dropdown-padding-y $dropdown-item-padding-x;
}
p {
margin: 0;
word-wrap: break-word;
white-space: normal;
}
}
// Dropdown Submenu
.dropdown-submenu {
position: relative;
> a::after {
@include caret-end();
float: right;
margin-top: .5rem;
margin-left: .5rem;
}
> .dropdown-menu {
top: 0;
left: 100%;
margin-top: 0;
margin-left: 0;
}
}
// Dropdown Hover
.dropdown-hover {
&:hover,
&.nav-item.dropdown:hover,
.dropdown-submenu:hover,
&.dropdown-submenu:hover {
> .dropdown-menu {
display: block;
}
}
}
// Dropdown Sizes
.dropdown-menu-xl {
min-width: 360px;
max-width: 420px;
padding: 0;
.dropdown-divider {
margin: 0;
}
.dropdown-item {
padding: $dropdown-padding-y $dropdown-item-padding-x;
}
p {
margin: 0;
word-wrap: break-word;
white-space: normal;
}
}
// Dropdown header and footer
.dropdown-footer,
.dropdown-header {
display: block;
padding: .5rem $dropdown-item-padding-x;
font-size: $font-size-sm;
text-align: center;
}
// Add fade animation to dropdown menus by appending
// the class .animated-dropdown-menu to the .dropdown-menu ul (or ol)
.open:not(.dropup) > .animated-dropdown-menu {
animation: flipInX .7s both;
backface-visibility: visible !important;
}
// Fix dropdown menu in navbars
.navbar-custom-menu > .navbar-nav {
> li {
position: relative;
> .dropdown-menu {
position: absolute;
right: 0;
left: auto;
}
}
}
@include media-breakpoint-down(sm) {
.navbar-custom-menu > .navbar-nav {
float: right;
> li {
position: static;
> .dropdown-menu {
position: absolute;
right: 5%;
left: auto;
background-color: var(--#{$prefix}body-bg);
border: 1px solid var(--#{$prefix}border-color);
}
}
}
}
// User Menu
.navbar-nav > .user-menu {
> .nav-link::after {
content: none;
}
> .dropdown-menu {
width: 280px;
padding: 0;
// Header menu
> li.user-header {
min-height: 175px;
padding: 10px;
text-align: center;
// User image
> img {
z-index: 5;
width: 90px;
height: 90px;
border: 3px solid;
border-color: transparent;
border-color: var(--#{$prefix}border-color-translucent);
}
> p {
z-index: 5;
margin-top: 10px;
font-size: 17px;
word-wrap: break-word;
> small {
display: block;
font-size: 12px;
}
}
}
// Menu Body
> .user-body {
@include clearfix();
padding: 15px;
border-top: 1px solid var(--#{$prefix}border-color);
border-bottom: 1px solid var(--#{$prefix}border-color-translucent);
a {
text-decoration: none;
}
}
// Menu Footer
> .user-footer {
@include clearfix();
padding: 10px;
background-color: var(--#{$prefix}light-bg);
}
}
.user-image {
@include media-breakpoint-up(sm) {
float: none;
margin-top: -8px;
margin-right: .4rem;
line-height: 10px;
}
float: left;
width: $lte-sidebar-user-image-width;
height: $lte-sidebar-user-image-width;
margin-top: -2px;
// margin-right: 10px;
@include border-radius(50%);
}
}

View file

@ -0,0 +1,131 @@
//
// Component: Info Box
//
.info-box {
@include box-shadow($lte-card-shadow);
@include border-radius($border-radius);
position: relative;
display: flex;
width: 100%;
min-height: 80px;
padding: .5rem;
margin-bottom: map-get($spacers, 3);
color: var(--#{$prefix}body-color);
background-color: var(--#{$prefix}body-bg);
.progress {
height: 2px;
margin: 5px 0;
background-color: rgba(var(--#{$lte-prefix}card-variant-color-rgb), .125);
.progress-bar {
background-color: var(--#{$lte-prefix}card-variant-color);
}
}
.info-box-icon {
display: flex;
align-items: center;
justify-content: center;
width: 70px;
font-size: 1.875rem;
text-align: center;
@include border-radius($border-radius);
> img {
max-width: 100%;
}
}
.info-box-content {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
padding: 0 10px;
line-height: 1.8;
}
.info-box-number {
display: block;
margin-top: .25rem;
font-weight: $font-weight-bold;
}
.progress-description,
.info-box-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-box-more {
display: block;
}
.progress-description {
margin: 0;
}
@include media-breakpoint-up(md) {
.col-xl-2 &,
.col-lg-2 &,
.col-md-2 & {
.progress-description {
display: none;
}
}
.col-xl-3 &,
.col-lg-3 &,
.col-md-3 & {
.progress-description {
display: none;
}
}
}
@include media-breakpoint-up(lg) {
.col-xl-2 &,
.col-lg-2 &,
.col-md-2 & {
.progress-description {
@include font-size(.75rem);
display: block;
}
}
.col-xl-3 &,
.col-lg-3 &,
.col-md-3 & {
.progress-description {
@include font-size(.75rem);
display: block;
}
}
}
@include media-breakpoint-up(xl) {
.col-xl-2 &,
.col-lg-2 &,
.col-md-2 & {
.progress-description {
@include font-size(1rem);
display: block;
}
}
.col-xl-3 &,
.col-lg-3 &,
.col-md-3 & {
.progress-description {
@include font-size(1rem);
display: block;
}
}
}
}

View file

@ -0,0 +1,22 @@
//
// Misc: Miscellaneous
//
// Image sizes
.img-size-64,
.img-size-50,
.img-size-32 {
height: auto;
}
.img-size-64 {
width: 64px;
}
.img-size-50 {
width: 50px;
}
.img-size-32 {
width: 32px;
}

View file

@ -0,0 +1,6 @@
//
// General: Mixins
//
@import "mixins/animations";
@import "mixins/scrollbar";

View file

@ -0,0 +1,66 @@
//
// Component: Progress Bar
//
//General CSS
.progress {
@include box-shadow(null);
@include border-radius($lte-progress-bar-border-radius);
// Vertical bars
&.vertical {
position: relative;
display: inline-block;
width: 30px;
height: 200px;
margin-right: 10px;
> .progress-bar {
position: absolute;
bottom: 0;
width: 100%;
}
//Sizes
&.sm,
&.progress-sm {
width: 20px;
}
&.xs,
&.progress-xs {
width: 10px;
}
&.xxs,
&.progress-xxs {
width: 3px;
}
}
}
.progress-group {
margin-bottom: map-get($spacers, 2);
}
// size variation
.progress-sm {
height: 10px;
}
.progress-xs {
height: 7px;
}
.progress-xxs {
height: 3px;
}
// Remove margins from progress bars when put in a table
.table {
tr > td {
.progress {
margin: 0;
}
}
}

View file

@ -0,0 +1,5 @@
:root,
[data-bs-theme="light"] {
// Sidebar
--#{$lte-prefix}sidebar-width: #{$lte-sidebar-width};
}

View file

@ -0,0 +1,127 @@
//
// Component: Small Box
//
.small-box {
@include border-radius($border-radius);
@include box-shadow($lte-card-shadow);
position: relative;
display: block;
margin-bottom: 1.25rem;
--bs-link-color-rgb: none;
--bs-link-hover-color-rgb: none;
--bs-heading-color: none;
// content wrapper
> .inner {
padding: 10px;
}
> .small-box-footer {
position: relative;
z-index: 10;
display: block;
padding: 3px 0;
text-align: center;
background-color: rgba($black, .07);
&:hover {
background-color: rgba($black, .1);
}
}
h3 {
@include font-size(2.2rem);
padding: 0;
margin: 0 0 10px;
font-weight: 700;
white-space: nowrap;
}
@include media-breakpoint-up(lg) {
.col-xl-2 &,
.col-lg-2 &,
.col-md-2 & {
h3 {
@include font-size(1.6rem);
}
}
.col-xl-3 &,
.col-lg-3 &,
.col-md-3 & {
h3 {
@include font-size(1.6rem);
}
}
}
@include media-breakpoint-up(xl) {
.col-xl-2 &,
.col-lg-2 &,
.col-md-2 & {
h3 {
@include font-size(2.2rem);
}
}
.col-xl-3 &,
.col-lg-3 &,
.col-md-3 & {
h3 {
@include font-size(2.2rem);
}
}
}
p {
font-size: 1rem;
> small {
display: block;
margin-top: 5px;
font-size: .9rem;
color: $gray-100;
}
}
h3,
p {
z-index: 5;
}
// the icon
.small-box-icon {
position: absolute;
top: 15px;
right: 15px;
z-index: 0;
height: 70px;
font-size: 70px;
color: rgba($black, .15);
@include transition(transform $lte-transition-speed linear);
}
// Small box hover state
&:hover {
// Animate icons on small box hover
.small-box-icon {
transform: scale(1.1);
}
}
}
@include media-breakpoint-down(sm) {
// No need for icons on very small devices
.small-box {
text-align: center;
.small-box-icon {
display: none;
}
p {
font-size: 12px;
}
}
}

View file

@ -0,0 +1,67 @@
//
// Component: Table
//
.table {
&:not(.table-dark) {
color: inherit;
}
// fixed table head
// Uses Bootstrap CSS variables so the sticky header follows light/dark mode
// automatically. Fixes #6026.
&.table-head-fixed {
thead tr:nth-child(1) th {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--bs-body-bg, #{$white});
border-bottom: 0;
box-shadow: inset 0 1px 0 var(--bs-border-color, #{$table-border-color}), inset 0 -1px 0 var(--bs-border-color, #{$table-border-color});
}
}
// no border
&.no-border {
&,
td,
th {
border: 0;
}
}
// .text-center in tables
&.text-center {
&,
td,
th {
text-align: center;
}
}
&.table-valign-middle {
thead > tr > th,
thead > tr > td,
tbody > tr > th,
tbody > tr > td {
vertical-align: middle;
}
}
.card-body.p-0 & {
thead > tr > th,
thead > tr > td,
tfoot > tr > th,
tfoot > tr > td,
tbody > tr > th,
tbody > tr > td {
&:first-of-type {
padding-left: map-get($spacers, 4);
}
&:last-of-type {
padding-right: map-get($spacers, 4);
}
}
}
}

View file

@ -0,0 +1,121 @@
//
// Component: Timeline
//
.timeline {
position: relative;
padding: 0;
margin: 0 0 45px;
// The line
&::before {
@include border-radius($border-radius);
position: absolute;
top: 0;
bottom: 0;
left: 31px;
width: 4px;
margin: 0;
content: "";
background-color: var(--#{$prefix}border-color);
}
// Element
> div {
&::before,
&::after {
display: table;
content: "";
}
position: relative;
margin-right: 10px;
margin-bottom: 15px;
// The content
> .timeline-item {
@include box-shadow($lte-card-shadow);
@include border-radius($border-radius);
position: relative;
padding: 0;
margin-top: 0;
margin-right: 15px;
margin-left: 60px;
color: var(--#{$prefix}body-color);
background-color: var(--#{$prefix}body-bg);
// The time and header
> .time {
float: right;
padding: 10px;
font-size: 12px;
color: var(--#{$prefix}secondary-color);
}
// Header
> .timeline-header {
padding: 10px;
margin: 0;
font-size: 16px;
line-height: 1.1;
color: var(--#{$prefix}secondary-color);
border-bottom: 1px solid var(--#{$prefix}border-color);
// Link in header
> a {
font-weight: 600;
text-decoration: none;
}
}
// Item body and footer
> .timeline-body,
> .timeline-footer {
padding: 10px;
}
> .timeline-body {
> img {
margin: 10px;
}
> dl,
ol,
ul {
margin: 0;
}
}
}
.timeline-icon {
position: absolute;
top: 0;
left: 18px;
width: 30px;
height: 30px;
font-size: 16px;
line-height: 30px;
text-align: center;
background-color: var(--#{$prefix}secondary-bg);
border-radius: 50%; // stylelint-disable-line property-disallowed-list
}
}
// Time label
> .time-label {
> span {
@include border-radius(4px);
display: inline-block;
padding: 5px;
font-weight: 600;
background-color: var(--#{$prefix}body-bg);
}
}
}
.timeline-inverse {
> div {
> .timeline-item {
@include box-shadow(none);
background-color: var(--#{$prefix}tertiary-bg);
border: 1px solid var(--#{$prefix}border-color);
> .timeline-header {
border-bottom-color: var(--#{$prefix}border-color);
}
}
}
}

View file

@ -0,0 +1,33 @@
//
// Toast
//
@each $name, $color in $theme-colors {
.toast-#{$name} {
--#{$prefix}toast-header-color: #{color-contrast($color)};
--#{$prefix}toast-header-bg: #{$color};
--#{$prefix}toast-header-border-color: #{$color};
--#{$prefix}toast-border-color: #{$color};
--#{$prefix}toast-bg: var(--#{$prefix}#{$name}-bg-subtle);
@if color-contrast($color) == $color-contrast-light {
.btn-close {
@include btn-close-white();
}
}
}
}
@if $enable-dark-mode {
@include color-mode(dark) {
@each $name, $color in $theme-colors {
.toast-#{$name} {
@if color-contrast($color) == $color-contrast-dark {
.btn-close {
--#{$prefix}btn-close-white-filter: none;
}
}
}
}
}
}

View file

@ -0,0 +1,16 @@
// SIDEBAR SKINS
// --------------------------------------------------------
$lte-sidebar-hover-bg-dark: rgba(255, 255, 255, .1) !default;
$lte-sidebar-color-dark: #c2c7d0 !default;
$lte-sidebar-hover-color-dark: $white !default;
$lte-sidebar-active-color-dark: $white !default;
$lte-sidebar-menu-active-bg-dark: rgba(255, 255, 255, .1) !default;
$lte-sidebar-menu-active-color-dark: $white !default;
$lte-sidebar-submenu-bg-dark: transparent !default;
$lte-sidebar-submenu-color-dark: #c2c7d0 !default;
$lte-sidebar-submenu-hover-color-dark: $white !default;
$lte-sidebar-submenu-hover-bg-dark: rgba(255, 255, 255, .1) !default;
$lte-sidebar-submenu-active-color-dark: $white !default;
$lte-sidebar-submenu-active-bg-dark: rgba(255, 255, 255, .1) !default;
$lte-sidebar-header-color-dark: tint-color(#c2c7d0, 5%) !default;

View file

@ -0,0 +1,131 @@
//
// Custom AdminLTE Variables
//
// Prefix for :root CSS variables and others.
$lte-prefix: lte- !default;
// TRANSITIONS SETTINGS
// --------------------------------------------------------
// Transition global options
$lte-transition-speed: .3s !default;
$lte-transition-fn: ease-in-out !default;
// SIDEBAR
// --------------------------------------------------------
$lte-sidebar-width: 250px !default;
$lte-sidebar-breakpoint: lg !default;
$lte-sidebar-padding-x: .5rem !default;
$lte-sidebar-padding-x-compact: .5rem !default;
$lte-sidebar-padding-y: .5rem !default;
$lte-sidebar-padding-y-compact: .25rem !default;
$lte-sidebar-transition: min-width $lte-transition-speed $lte-transition-fn,
max-width $lte-transition-speed $lte-transition-fn,
margin-left $lte-transition-speed $lte-transition-fn,
margin-right $lte-transition-speed $lte-transition-fn !default;
// SIDEBAR SKINS
// --------------------------------------------------------
$lte-sidebar-hover-bg: rgba($black, .1) !default;
$lte-sidebar-color: $gray-800 !default;
$lte-sidebar-hover-color: $gray-900 !default;
$lte-sidebar-active-color: $black !default;
$lte-sidebar-menu-active-bg: rgba($black, .1) !default;
$lte-sidebar-menu-active-color: $black !default;
$lte-sidebar-submenu-bg: transparent !default;
$lte-sidebar-submenu-color: #777 !default;
$lte-sidebar-submenu-hover-color: $black !default;
$lte-sidebar-submenu-hover-bg: rgba($black, .1) !default;
$lte-sidebar-submenu-active-color: $gray-900 !default;
$lte-sidebar-submenu-active-bg: rgba($black, .1) !default;
$lte-sidebar-header-color: shade-color($gray-800, 5%) !default;
// SIDEBAR MINI
// --------------------------------------------------------
$nav-link-padding-x-compact: .25rem !default;
$lte-sidebar-mini-width: ($nav-link-padding-x + $lte-sidebar-padding-x + .8rem) * 2 !default;
$lte-sidebar-mini-width-compact: ($nav-link-padding-x-compact + $lte-sidebar-padding-x-compact + .8rem) * 2 !default;
$lte-sidebar-nav-icon-width: $lte-sidebar-mini-width - (($lte-sidebar-padding-x + $nav-link-padding-x) * 2) !default;
$lte-sidebar-user-image-width: $lte-sidebar-nav-icon-width + ($nav-link-padding-x * .4) !default;
// MAIN HEADER
// --------------------------------------------------------
$nav-link-height-compact: 1.75rem !default;
$lte-app-header-bottom-border-width: $border-width !default;
$lte-app-header-bottom-border-color: var(--#{$prefix}border-color) !default;
$lte-app-header-bottom-border: $lte-app-header-bottom-border-width solid $lte-app-header-bottom-border-color !default;
$lte-app-header-link-padding-y: $navbar-padding-y !default;
$lte-app-header-height: ($nav-link-height + ($lte-app-header-link-padding-y * 2)) !default;
$lte-app-header-height-compact: ($nav-link-height-compact + ($lte-app-header-link-padding-y * 2)) !default;
$lte-zindex-fixed-header: $zindex-fixed !default;
// APP MAIN
// --------------------------------------------------------
$lte-app-main-padding-bottom: $grid-gutter-width * .5 !default;
// CONTENT PADDING
// --------------------------------------------------------
$lte-content-padding-y: 0 !default;
$lte-content-padding-x: .5rem !default;
// MAIN FOOTER
// --------------------------------------------------------
$lte-app-footer-padding: 1rem !default;
$lte-app-footer-padding-compact: .5rem !default;
$lte-app-footer-border-top-width: 1px !default;
$lte-app-footer-border-top-color: var(--#{$prefix}border-color) !default;
$lte-app-footer-border-top: $lte-app-footer-border-top-width solid $lte-app-footer-border-top-color !default;
$lte-app-footer-bg: var(--#{$prefix}body-bg) !default;
$lte-app-footer-color: var(--#{$prefix}secondary-color) !default;
$lte-zindex-fixed-footer: $zindex-fixed !default;
// CONTENT BOTTOM AREA
// --------------------------------------------------------
$lte-app-content-bottom-area-margin-bottom: -$lte-app-main-padding-bottom !default;
$lte-app-content-bottom-area-color: $lte-app-footer-color !default;
$lte-app-content-bottom-area-bg: $lte-app-footer-bg !default;
$lte-app-content-bottom-area-top-border: $lte-app-footer-border-top !default;
$lte-app-content-bottom-area-padding-y: $lte-app-footer-padding !default;
$lte-app-content-bottom-area-padding-x: 0 !default;
// CONTENT TOP AREA
// --------------------------------------------------------
$lte-app-content-top-area-top-border: $lte-app-footer-border-top !default;
$lte-app-content-top-area-padding-y: $lte-app-footer-padding !default;
$lte-app-content-top-area-padding-x: 0 !default;
// BRAND LINK
// --------------------------------------------------------
$navbar-brand-padding-y-compact: $navbar-brand-padding-y * .75 !default;
$navbar-padding-y-compact: $navbar-padding-y * .5 !default;
$lte-brand-link-padding-y: $navbar-brand-padding-y + $navbar-padding-y !default;
$lte-brand-link-padding-y-compact: $navbar-brand-padding-y-compact + $navbar-padding-y-compact !default;
$lte-brand-link-padding-x: $lte-sidebar-padding-x !default;
$lte-brand-link-padding-x-compact: $lte-sidebar-padding-x-compact !default;
$lte-brand-link-border-buttom: 1px !default;
// CARDS
// --------------------------------------------------------
$lte-card-shadow: 0 0 1px rgba(var(--#{$prefix}body-color-rgb), .125), 0 1px 3px rgba(var(--#{$prefix}body-color-rgb), .2) !default;
$lte-card-title-font-size: 1.1rem !default;
$lte-card-title-font-weight: $font-weight-normal !default;
// PROGRESS BARS
// --------------------------------------------------------
$lte-progress-bar-border-radius: 1px !default;
// CALLOUTS
// --------------------------------------------------------
$lte-callout-link-font-weight: $alert-link-font-weight !default;
// DIRECT CHAT
// --------------------------------------------------------
$lte-direct-chat-default-msg-bg: var(--#{$prefix}secondary-bg) !default;
$lte-direct-chat-default-font-color: var(--#{$prefix}emphasis-color) !default;
$lte-direct-chat-default-msg-border-color: var(--#{$prefix}border-color) !default;
// Z-INDEX
// --------------------------------------------------------
$lte-zindex-app-header: $zindex-fixed + 4 !default;
$lte-zindex-sidebar: $zindex-fixed + 8 !default;
$lte-zindex-sidebar-overlay: $lte-zindex-sidebar - 1 !default;

View file

@ -0,0 +1,81 @@
/*!
* AdminLTE v4.0.0
* Author: Colorlib
* Website: AdminLTE.io <https://adminlte.io>
* License: Open source - MIT <https://opensource.org/licenses/MIT>
*/
// Bootstrap Configuration
// ---------------------------------------------------
@import "bootstrap/scss/functions";
// AdminLTE Configuration
// ---------------------------------------------------
@import "bootstrap-variables"; // little modified are here
// Bootstrap Configuration
// ---------------------------------------------------
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
// Bootstrap Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
@import "bootstrap/scss/card";
@import "bootstrap/scss/accordion";
@import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
@import "bootstrap/scss/close";
@import "bootstrap/scss/toasts";
@import "bootstrap/scss/modal";
@import "bootstrap/scss/tooltip";
@import "bootstrap/scss/popover";
@import "bootstrap/scss/carousel";
@import "bootstrap/scss/spinners";
@import "bootstrap/scss/offcanvas";
@import "bootstrap/scss/placeholders";
// Bootstrap Helpers
@import "bootstrap/scss/helpers";
// Bootstrap Utilities
@import "bootstrap/scss/utilities/api";
// AdminLTE Configuration
// ---------------------------------------------------
@import "variables";
@import "variables-dark";
@import "mixins";
// AdiminLTE Parts
// ---------------------------------------------------
@import "parts/core";
@import "parts/components";
@import "parts/extra-components";
@import "parts/pages";
@import "parts/miscellaneous";
// AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance
@import "accessibility";
// AdminLTE Documentation Styles (only applies to pages with body.docs-page)
@import "docs";

View file

@ -0,0 +1,116 @@
//
// Mixins: Animation
//
@keyframes flipInX {
0% {
opacity: 0;
transition-timing-function: ease-in;
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
}
40% {
transition-timing-function: ease-in;
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
}
60% {
opacity: 1;
transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
}
80% {
transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
}
100% {
transform: perspective(400px);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes shake {
0% {
transform: translate(2px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-2deg);
}
20% {
transform: translate(-3px, 0) rotate(3deg);
}
30% {
transform: translate(0, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(2px, 1px) rotate(-2deg);
}
80% {
transform: translate(-1px, -1px) rotate(4deg);
}
90% {
transform: translate(2px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
@keyframes wobble {
0% {
transform: none;
}
15% {
transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
}
30% {
transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
}
45% {
transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
}
60% {
transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
}
75% {
transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
}
100% {
transform: none;
}
}

View file

@ -0,0 +1,36 @@
//
// Mixins: Scrollbar
//
@mixin scrollbar-color-gray() {
scrollbar-color: var(--#{$prefix}secondary-bg) transparent;
&::-webkit-scrollbar-thumb {
background-color: var(--#{$prefix}secondary-bg);
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-corner {
background-color: transparent;
}
}
@mixin scrollbar-width-thin() {
scrollbar-width: thin;
&::-webkit-scrollbar {
width: .5rem;
height: .5rem;
}
}
@mixin scrollbar-width-none() {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
}
}

View file

@ -0,0 +1,75 @@
//
// Pages: Lock Screen
//
// ADD THIS CLASS TO THE <BODY> TAG
.lockscreen {
// User name [optional]
.lockscreen-name {
font-weight: 600;
text-align: center;
}
.lockscreen-logo {
margin-bottom: 25px;
font-size: 35px;
font-weight: 300;
text-align: center;
a {
color: var(--#{$prefix}emphasis-color);
text-decoration: none;
}
}
.lockscreen-wrapper {
max-width: 400px;
margin: 0 auto;
margin-top: 10%;
}
// Will contain the image and the sign in form
.lockscreen-item {
position: relative;
width: 290px;
padding: 0;
margin: 10px auto 30px;
background-color: var(--#{$prefix}body-bg);
@include border-radius(4px);
}
// User image
.lockscreen-image {
position: absolute;
top: -25px;
left: -10px;
z-index: 10;
padding: 5px;
background-color: var(--#{$prefix}body-bg);
@include border-radius(50%);
> img {
@include border-radius(50%);
width: 70px;
height: 70px;
}
}
// Contains the password input and the login button
.lockscreen-credentials {
margin-left: 70px;
.form-control {
border: 0;
}
.btn {
padding: 0 10px;
border: 0;
}
}
.lockscreen-footer {
margin-top: 10px;
}
}

View file

@ -0,0 +1,100 @@
//
// Pages: Login & Register
//
.login-logo,
.register-logo {
margin-bottom: .9rem;
font-size: 2.1rem;
font-weight: 300;
text-align: center;
a {
color: var(--#{$prefix}secondary-color);
text-decoration: none;
}
}
.login-page,
.register-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-box,
.register-box {
width: 400px;
@media (max-width: map-get($grid-breakpoints, sm)) {
width: 90%;
margin-top: .5rem;
}
.card {
margin-bottom: 0;
}
}
.login-card-body,
.register-card-body {
padding: 20px;
color: var(--#{$prefix}secondary-color);
background-color: var(--#{$prefix}body-bg);
border-top: 0;
.input-group {
.form-control {
&:focus {
box-shadow: none;
~ .input-group-prepend .input-group-text,
~ .input-group-append .input-group-text {
border-color: $input-focus-border-color;
}
}
&.is-valid {
&:focus {
box-shadow: none;
}
~ .input-group-prepend .input-group-text,
~ .input-group-append .input-group-text {
border-color: $success;
}
}
&.is-invalid {
&:focus {
box-shadow: none;
}
~ .input-group-append .input-group-text {
border-color: $danger;
}
}
}
.input-group-text {
color: var(--#{$prefix}secondary-color);
background-color: transparent;
@include border-top-end-radius($border-radius);
@include border-bottom-end-radius($border-radius);
@include transition($input-transition);
}
}
}
.login-box-msg,
.register-box-msg {
padding: 0 20px 20px;
margin: 0;
text-align: center;
}
.social-auth-links {
margin: 10px 0;
}

View file

@ -0,0 +1,7 @@
//
// Part: Components
//
@import "../progress-bars";
@import "../cards";
@import "../table";

View file

@ -0,0 +1,15 @@
//
// Part: Core
//
@import "../root";
@import "../app-wrapper";
@import "../app-content";
@import "../app-header";
@import "../app-sidebar";
@import "../app-main";
@import "../app-footer";
@import "../dropdown";
@import "../callouts";
@import "../compact-mode";
@import "../docs";

View file

@ -0,0 +1,9 @@
//
// Part: Extra Components
//
@import "../small-box";
@import "../info-box";
@import "../timeline";
@import "../direct-chat";
@import "../toasts";

View file

@ -0,0 +1,5 @@
//
// Part: Miscellaneous
//
@import "../miscellaneous";

View file

@ -0,0 +1,6 @@
//
// Part: Pages
//
@import "../pages/login_and_register";
@import "../pages/lockscreen";

View file

@ -0,0 +1,91 @@
// AdminLTE v4.0.0 + Bootstrap 5.3.8 - PageTop Bootsier Styles.
// Bootstrap Configuration
// ---------------------------------------------------
@import "bootstrap-5.3.8/scss/functions";
// AdminLTE Configuration (little modified are here)
// ---------------------------------------------------
@import "adminlte-4.0.0/scss/bootstrap-variables";
// Bootsier Bootstrap variable overrides
// ---------------------------------------------------
@import "bootsier-variables";
// Bootstrap Configuration
// ---------------------------------------------------
@import "bootstrap-5.3.8/scss/variables";
@import "bootstrap-5.3.8/scss/variables-dark";
@import "bootstrap-5.3.8/scss/maps";
@import "bootstrap-5.3.8/scss/mixins";
@import "bootstrap-5.3.8/scss/utilities";
// Bootstrap Layout & components
@import "bootstrap-5.3.8/scss/root";
@import "bootstrap-5.3.8/scss/reboot";
@import "bootstrap-5.3.8/scss/type";
@import "bootstrap-5.3.8/scss/images";
@import "bootstrap-5.3.8/scss/containers";
@import "bootstrap-5.3.8/scss/grid";
@import "bootstrap-5.3.8/scss/tables";
@import "bootstrap-5.3.8/scss/forms";
@import "bootstrap-5.3.8/scss/buttons";
@import "bootstrap-5.3.8/scss/transitions";
@import "bootstrap-5.3.8/scss/dropdown";
@import "bootstrap-5.3.8/scss/button-group";
@import "bootstrap-5.3.8/scss/nav";
@import "bootstrap-5.3.8/scss/navbar";
@import "bootstrap-5.3.8/scss/card";
@import "bootstrap-5.3.8/scss/accordion";
@import "bootstrap-5.3.8/scss/breadcrumb";
@import "bootstrap-5.3.8/scss/pagination";
@import "bootstrap-5.3.8/scss/badge";
@import "bootstrap-5.3.8/scss/alert";
@import "bootstrap-5.3.8/scss/progress";
@import "bootstrap-5.3.8/scss/list-group";
@import "bootstrap-5.3.8/scss/close";
@import "bootstrap-5.3.8/scss/toasts";
@import "bootstrap-5.3.8/scss/modal";
@import "bootstrap-5.3.8/scss/tooltip";
@import "bootstrap-5.3.8/scss/popover";
@import "bootstrap-5.3.8/scss/carousel";
@import "bootstrap-5.3.8/scss/spinners";
@import "bootstrap-5.3.8/scss/offcanvas";
@import "bootstrap-5.3.8/scss/placeholders";
// Bootstrap Helpers
@import "bootstrap-5.3.8/scss/helpers";
// Bootsier Utilities (must precede utilities/api to extend $utilities)
@import "bootsier-utilities";
// Bootstrap Utilities
@import "bootstrap-5.3.8/scss/utilities/api";
// AdminLTE Configuration
// ---------------------------------------------------
@import "adminlte-4.0.0/scss/variables";
@import "adminlte-4.0.0/scss/variables-dark";
@import "adminlte-4.0.0/scss/mixins";
// AdminLTE Parts
// ---------------------------------------------------
@import "adminlte-4.0.0/scss/parts/core";
@import "adminlte-4.0.0/scss/parts/components";
@import "adminlte-4.0.0/scss/parts/extra-components";
@import "adminlte-4.0.0/scss/parts/pages";
@import "adminlte-4.0.0/scss/parts/miscellaneous";
// AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance
@import "adminlte-4.0.0/scss/accessibility";
// AdminLTE Documentation Styles (only applies to pages with body.docs-page)
@import "adminlte-4.0.0/scss/docs";
// Bootstrap Icons 1.13.1
// ---------------------------------------------------
@import "bootsier-icons";
// Bootsier customizations
// ---------------------------------------------------
@import "bootsier-custom";

View file

@ -0,0 +1,65 @@
(function () {
'use strict';
// Fullscreen: keeps maximize/minimize icons in sync with the actual fullscreen state.
document.addEventListener('fullscreenchange', function () {
var isFs = !!document.fullscreenElement;
document.querySelectorAll('[data-lte-icon="maximize"]').forEach(function (el) {
el.classList.toggle('d-none', isFs);
});
document.querySelectorAll('[data-lte-icon="minimize"]').forEach(function (el) {
el.classList.toggle('d-none', !isFs);
});
});
// Color mode selector (light / dark / auto).
var STORAGE_KEY = 'lte-theme';
var getStored = function () { return localStorage.getItem(STORAGE_KEY); };
var setStored = function (theme) { localStorage.setItem(STORAGE_KEY, theme); };
var prefersDark = function () {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
var setTheme = function (theme) {
var resolved = (theme === 'auto') ? (prefersDark() ? 'dark' : 'light') : theme;
document.documentElement.setAttribute('data-bs-theme', resolved);
};
var showActiveTheme = function (theme) {
document.querySelectorAll('[data-bs-theme-value]').forEach(function (el) {
el.classList.remove('active');
el.setAttribute('aria-pressed', 'false');
var check = el.querySelector('.bi-check-lg');
if (check) { check.classList.add('d-none'); }
});
var active = document.querySelector('[data-bs-theme-value="' + theme + '"]');
if (active) {
active.classList.add('active');
active.setAttribute('aria-pressed', 'true');
var check = active.querySelector('.bi-check-lg');
if (check) { check.classList.remove('d-none'); }
}
document.querySelectorAll('[data-lte-theme-icon]').forEach(function (icon) {
icon.classList.toggle('d-none', icon.dataset.lteThemeIcon !== theme);
});
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
var stored = getStored();
if (!stored || stored === 'auto') { setTheme('auto'); }
});
document.addEventListener('DOMContentLoaded', function () {
var theme = getStored() || 'auto';
setTheme(theme);
showActiveTheme(theme);
document.querySelectorAll('[data-bs-theme-value]').forEach(function (toggle) {
toggle.addEventListener('click', function () {
var t = toggle.getAttribute('data-bs-theme-value');
setStored(t);
setTheme(t);
showActiveTheme(t);
});
});
});
}());

Some files were not shown because too many files have changed in this diff Show more